Courtside Manager score keeper page per court; Phase below Courts on the desk; estimated finish on court cards

A per-court signed link from the desk opens a full-screen scoring page with
big +/- buttons, undo, best-of sets, and a confirmed Finalize step; it follows
whatever match is on the court and can never edit a finished result. Public
court cards show the estimated finish time. Release 0.4.0.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
This commit is contained in:
2026-09-04 02:10:40 +00:00
co-authored by Claude Fable 5.1
parent acc5fdc2f3
commit eb58eb9ad9
14 changed files with 254 additions and 25 deletions
+4 -3
View File
@@ -14,14 +14,14 @@
const isMine = m => myTeam && (m.a?.id === myTeam || m.b?.id === myTeam);
const phaseLabel = { checkin: 'Registration open', closed: 'Registration closed', live: 'Live', final: 'Final' };
const score = m => m.status === 'final' ? m.sets.map(s => `${s[0]}${s[1]}`).join(', ')
: m.status === 'forfeit' ? 'forfeit' : m.live ? `${m.live[0]}${m.live[1]}` : m.status === 'live' ? '00' : '';
: m.status === 'forfeit' ? 'forfeit' : m.live ? [...(m.liveSets ?? []), m.live].map(s => `${s[0]}${s[1]}`).join(', ') : m.status === 'live' ? '00' : '';
// score from one team's point of view (their points first)
const scoreFor = (m, teamId) => {
const flip = m.b?.id === teamId;
const pair = ([a, b]) => flip ? `${b}${a}` : `${a}${b}`;
if (m.status === 'final') return m.sets.map(pair).join(', ');
if (m.status === 'forfeit') return 'forfeit';
if (m.live) return pair(m.live);
if (m.live) return [...(m.liveSets ?? []), m.live].map(pair).join(', ');
return m.status === 'live' ? '00' : '';
};
const rulesLine = r => `${r.teamSize}s · to ${r.pointsTo}, win by ${r.winBy}${r.cap ? `, cap ${r.cap}` : ''}${r.bestOf > 1 ? `, best of ${r.bestOf}` : ''}`;
@@ -74,7 +74,8 @@
const avg = state.avgMatchMin;
const elapsed = m.startedAt ? Math.max(0, Math.round((Date.now() - m.startedAt) / 60000)) : null;
const left = avg != null && elapsed != null ? Math.max(0, avg - elapsed) : null;
const timing = avg != null ? `avg game ${avg} min${elapsed != null ? ` · ${elapsed} min played${left ? `, ~${left} left` : ''}` : ''}` : '';
const finish = avg != null && m.startedAt ? clock(Math.max(Date.now(), m.startedAt + avg * 60000)) : null;
const timing = avg != null ? `avg game ${avg} min${elapsed != null ? ` · ${elapsed} min played` : ''}${finish ? ` · est. finish ~${finish}${left ? ` (${left} min)` : ''}` : ''}` : '';
return `<div class="court ${isMine(m) ? 'mine' : ''}"><div class="lbl">Court ${c.court} · ${m.stage === 'pool' ? `Pool ${esc(m.a && state.teams.find(t => t.id === m.a.id)?.poolId || '')} R${m.round}` : (m.label ?? `Bracket R${m.round}`)}</div>
<div class="teams"><div class="t">${name(m.a)}</div><div class="s mono">${score(m) || '00'}</div><div class="t">${name(m.b)}</div></div>
${timing ? `<div class="timing">${timing}</div>` : ''}