1 Commits
Author SHA1 Message Date
bkvargyasandClaude Fable 5.1 3f60a39e45 TV display: courts pinned at top, lower pane scrolls and pages itself
Release 0.5.3.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
2026-09-05 16:20:55 +00:00
4 changed files with 25 additions and 4 deletions

No files matched your search

+7
View File
@@ -9,6 +9,13 @@ so you can always tell what a deployment is actually serving.
## [Unreleased] ## [Unreleased]
## [0.5.3] - 2026-09-03
### Changed
- TV display: the courts (and banner) stay pinned at the top of the screen; up next, brackets
and standings scroll in a pane underneath, paging automatically every 12 seconds and
wrapping back to the top. Idle courts and the banner now use dark-theme colors on the TV.
## [0.5.2] - 2026-09-03 ## [0.5.2] - 2026-09-03
### Changed ### Changed
+1 -1
View File
@@ -1 +1 @@
{"name":"courtside","type":"module","version":"0.5.2","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}} {"name":"courtside","type":"module","version":"0.5.3","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}}
+2 -1
View File
@@ -94,7 +94,8 @@ ul.plain,ol.plain{list-style:none;padding:0;margin:8px 0}ol.plain{counter-reset:
.loading{color:var(--muted);padding:24px} .loading{color:var(--muted);padding:24px}
/* display (TV) mode */ /* display (TV) mode */
body.display{font-size:22px;background:#0F1418;color:#F2F5F7} body.display{font-size:22px;background:#0F1418;color:#F2F5F7}
body.display .top,body.display .foot{display:none}body.display main{max-width:none;padding:20px 28px} body.display .top,body.display .foot{display:none}body.display main{max-width:none;padding:14px 24px 0;height:100vh;height:100dvh;box-sizing:border-box}
body.display .app{display:flex;flex-direction:column;height:100%}body.display .pin{flex:none;padding-bottom:10px;border-bottom:1px solid #2B3640}body.display .scrollpane{flex:1;min-height:0;overflow-y:auto;padding:10px 0 24px;scrollbar-width:none}body.display .scrollpane::-webkit-scrollbar{display:none}body.display .courts{margin-bottom:0}body.display .court.idle,body.display .court.paused{background:#1A222B;color:#9AA6B1;border-color:#2B3640}body.display .court.idle .nextup{color:#F2F5F7;border-top-color:#2B3640}body.display .banner{background:#3A3120;color:#F0C46B;border-left-color:#D9B26E}body.display .brow .bs{color:#9AA6B1}body.display .brow.won .bs{color:#F2F5F7}
body.display .display-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}body.display h1{font-size:56px} body.display .display-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}body.display h1{font-size:56px}
body.display .court .t{font-size:40px}body.display .court .s{font-size:52px}body.display .court{min-height:150px} body.display .court .t{font-size:40px}body.display .court .s{font-size:52px}body.display .court{min-height:150px}
body.display .card{background:#1A222B;border-color:#2B3640;color:#F2F5F7}body.display td{border-color:#2B3640}body.display .bm{background:#12181F;border-color:#2B3640}body.display .brow+.brow{border-color:#2B3640}body.display .bcanvas path{stroke:#3A4652}body.display .btag{background:#1A222B;border-color:#2B3640} body.display .card{background:#1A222B;border-color:#2B3640;color:#F2F5F7}body.display td{border-color:#2B3640}body.display .bm{background:#12181F;border-color:#2B3640}body.display .brow+.brow{border-color:#2B3640}body.display .bcanvas path{stroke:#3A4652}body.display .btag{background:#1A222B;border-color:#2B3640}
+15 -2
View File
@@ -45,7 +45,15 @@
if (s.pools.length) parts.push(renderPools()); if (s.pools.length) parts.push(renderPools());
if (!s.pools.length && !s.bracket.length) parts.push(renderRoster()); if (!s.pools.length && !s.bracket.length) parts.push(renderRoster());
if (s.withdrawn.length && !display) parts.push(`<p class="muted small">Withdrawn: ${s.withdrawn.map(t => esc(t.name)).join(', ')}</p>`); if (s.withdrawn.length && !display) parts.push(`<p class="muted small">Withdrawn: ${s.withdrawn.map(t => esc(t.name)).join(', ')}</p>`);
app.innerHTML = parts.join(''); if (display) {
// TV: courts stay pinned at the top; everything else scrolls underneath
const pane = app.querySelector('.scrollpane');
const keep = pane ? pane.scrollTop : 0;
const pinCount = parts.findIndex(p => p.startsWith('<section class="queue"') || p.startsWith('<section class="card bracketwrap') || p.startsWith('<section class="pools"') || p.startsWith('<section class="card"'));
const cut = pinCount === -1 ? parts.length : pinCount;
app.innerHTML = `<div class="pin">${parts.slice(0, cut).join('')}</div><div class="scrollpane">${parts.slice(cut).join('')}</div>`;
const np = app.querySelector('.scrollpane'); if (np) np.scrollTop = keep;
} else app.innerHTML = parts.join('');
fitBracket(); fitBracket();
notifyMine(); notifyMine();
} }
@@ -257,5 +265,10 @@
connect(); connect();
// belt and braces: a full refresh every 2 minutes in case a proxy drops the socket silently // belt and braces: a full refresh every 2 minutes in case a proxy drops the socket silently
setInterval(() => fetch(`/t/${state.slug}/state.json`).then(r => r.json()).then(s => { state = s; render(); }).catch(() => {}), 120000); setInterval(() => fetch(`/t/${state.slug}/state.json`).then(r => r.json()).then(s => { state = s; render(); }).catch(() => {}), 120000);
if (display) setInterval(() => document.documentElement.scrollTo({ top: (Date.now() / 1000 | 0) % 2 ? 0 : document.body.scrollHeight, behavior: 'smooth' }), 15000); // TV: page the lower pane (up next, brackets, standings) every 12 s and wrap to the top; courts never move
if (display) setInterval(() => {
const pane = app.querySelector('.scrollpane'); if (!pane || pane.scrollHeight <= pane.clientHeight) return;
const atEnd = pane.scrollTop + pane.clientHeight >= pane.scrollHeight - 4;
pane.scrollTo({ top: atEnd ? 0 : pane.scrollTop + pane.clientHeight * 0.85, behavior: 'smooth' });
}, 12000);
})(); })();