From 058636fae31589cf20b5c1946fdafb2cff71ef0c Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Mon, 31 Aug 2026 16:19:51 +0200 Subject: [PATCH] The cup board asked for itself and got the gore board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GET filter spelled its modes out as a literal `[^gzc]` character class, so `?m=u` sanitised to nothing, fell back to 'g', and the server answered the cup board's request with the gore board — which the client then printed in the cup board's words: 7121 kills as LEAGUE 7121. The filter is derived from MODES now, where the mode list has lived all along. And the client checks the answer it got: the response names the board it is, so a server that does not know a board (an old one, mid-deploy) makes the screen say THIS BOARD IS NOT UP YET instead of mislabelling another board's rows. Nothing beats something wrong on a leaderboard. Co-Authored-By: Claude Fable 5 --- server.py | 8 ++++++-- src/stats.js | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/server.py b/server.py index 9192ece..d76a897 100644 --- a/server.py +++ b/server.py @@ -320,8 +320,12 @@ class Handler(SimpleHTTPRequestHandler): if path == '/ws': return self.handle_ws() if path == '/api/scores': - want = re.sub(r'[^gzc]', '', (self.path.split('m=')[1].split('&')[0] - if 'm=' in self.path else ''))[:1] or 'g' + # DERIVED FROM MODES, never spelled out again (2026-08-31): this filter was a literal + # `[^gzc]` character class, so the day the cup board was added the server answered its + # requests with the GORE board — and the client printed 7121 kills as "LEAGUE 7121". + want = (self.path.split('m=')[1].split('&')[0] if 'm=' in self.path else '')[:1] + if want not in MODES: + want = 'g' # an unknown board is the default one, never a new one with lock: return self.reply(200, {'m': want, 'scores': public([r for r in load() if mode_of(r) == want])}) diff --git a/src/stats.js b/src/stats.js index 78a17a5..c638a9d 100644 --- a/src/stats.js +++ b/src/stats.js @@ -220,7 +220,13 @@ export function lastRun() { const r = last; last = null; return r; } export async function board(mode = MODE_GORE) { const r = await fetch(API + 'scores?m=' + mode, { cache: 'no-store' }); if (!r.ok) throw new Error('board ' + r.status); - return (await r.json()).scores || []; + const d = await r.json(); + // THE SERVER NAMES THE BOARD IT ANSWERED WITH, and we check it (2026-08-31): a server that does + // not know a board falls back to the default one, and rendering those rows here prints one + // board's currency in another's words — gore kills as LEAGUE 7121, which is exactly what a + // stale server did the day the cup board shipped. Nothing is better than something wrong. + if (d.m && d.m !== mode) { const e = new Error('board not up'); e.absent = true; throw e; } + return d.scores || []; } // The SAME normalisation server.py's clean() applies. Both ends have to agree or nothing lines up: @@ -331,7 +337,11 @@ export async function drawRanks(rows, mode = MODE_GORE) { if (!rows) { el.innerHTML = '

READING THE BOARD…

'; try { rows = await board(mode); } - catch { el.innerHTML = '

THE BOARD IS UNREACHABLE.

'; return; } + catch (e) { + el.innerHTML = `

${e && e.absent ? 'THIS BOARD IS NOT UP YET.' + : 'THE BOARD IS UNREACHABLE.'}

`; + return; + } } if (!rows.length) { el.innerHTML = '

NOBODY IS ON THIS BOARD YET. BE FIRST.

'; return; } const me = savedName();