Tentative next-up for bracket matches waiting on live courts; per-court score keeper QR codes

Matches whose opponent comes from a live match now show in the queue and under
courts as 'X vs winner on Court 2 · ready ~time', which is what double
elimination needed. Score keeper access is a QR per court on the desk plus a
printable page; the copy-link button is gone. Release 0.4.1.

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:28:04 +00:00
co-authored by Claude Fable 5.1
parent eb58eb9ad9
commit ae44014e22
10 changed files with 69 additions and 15 deletions
+18 -2
View File
@@ -102,7 +102,7 @@ export class CourtEngine {
const m = c.matchId ? this.t.match(c.matchId) : null;
return { court: c.number, status: c.status, match: m ? this._matchView(m) : null };
}),
upNext: q.slice(0, 3).map(x => ({ ...this._matchView(x.match), court: x.court, etaMin: Math.round(x.eta / MIN) })),
upNext: q.slice(0, 3).map(x => ({ ...this._matchView(x.match), court: x.court, etaMin: Math.round(x.eta / MIN), tentative: !!x.tentative, aFrom: x.aFrom ?? null, bFrom: x.bFrom ?? null })),
avgMatchMin: Math.round(this.avgMatchMs() / MIN),
};
}
@@ -121,6 +121,22 @@ export class CourtEngine {
sims[0].freeAt = start + this.avgMatchMs();
teamFree.set(m.teamA, sims[0].freeAt); teamFree.set(m.teamB, sims[0].freeAt);
}
// Bracket matches still waiting on a live match (one or both sides TBD) get a tentative slot too,
// so late in a bracket the board still says "Net Gains vs winner on Court 2 · ready ~2:10".
const decided = x => ['final', 'forfeit', 'bye', 'void'].includes(x.status);
const liveCourt = x => this.courts.find(c => c.matchId === x.id)?.number ?? null;
for (const m of this.t.matches) {
if (m.status !== 'pending' || m.conditional || m.stage === 'pool' || (m.teamA && m.teamB)) continue;
const feeders = this.t.matches.filter(x => x.feeds === m.id || x.loserFeeds === m.id);
if (!feeders.length || !feeders.every(x => decided(x) || x.status === 'live') || !feeders.some(x => x.status === 'live')) continue;
const side = (which) => { const f = feeders.find(x => (x.feeds === m.id && x.feedsSide === which) || (x.loserFeeds === m.id && x.loserFeedsSide === which)); return f && f.status === 'live' ? { court: liveCourt(f), loser: f.loserFeeds === m.id } : null; };
const waitOn = feeders.filter(x => x.status === 'live').map(x => this._expectedFinish(this.courts.find(c => c.matchId === x.id)));
sims.sort((a, b) => a.freeAt - b.freeAt);
const ready = Math.max(sims[0].freeAt, ...waitOn, m.teamA ? teamFree.get(m.teamA) ?? 0 : 0, m.teamB ? teamFree.get(m.teamB) ?? 0 : 0);
const start = ready + this.changeoverMs;
out.push({ match: m, court: sims[0].number, eta: start - this.now(), tentative: true, aFrom: m.teamA ? null : side('A'), bFrom: m.teamB ? null : side('B') });
sims[0].freeAt = start + this.avgMatchMs();
}
return out;
}
@@ -173,7 +189,7 @@ export class CourtEngine {
_freeCourt(c) { c.matchId = null; c.startedAt = null; c.freeAt = null; }
_refreshOnDeck() {
const q = this.queue();
const q = this.queue().filter(x => !x.tentative);
const deck = q.slice(0, this.courts.filter(c => c.status === 'open').length);
const deckIds = new Set(deck.map(x => x.match.id));
for (const m of this.t.matches) if (m.status === 'on_deck' && !deckIds.has(m.id)) { m.status = 'pending'; m.court = null; }