Move the board out to the service every game shares

The leaderboard started in this repository because when it was written there
was one game. It was already being treated as shared -- games.jcoffey.dev
calls it "the shared leaderboard" in its compose file and nginx proxies it at
a site-wide /api/ rather than under /lemonade/ -- but it was still this game's
code, and the second game would have meant a second container, a second volume
and a second database to back up for every game after that.

It lives in https://github.com/Coffey-Labs/games-scores now, which is its own
repository rather than either game's. A shared board belongs to no game, and
leaving it here would have made every future game depend on the lemonade stand
for its leaderboard.

What changes here is smaller than the diff suggests. server/ goes, and with it
scripts/dev-all.mjs and two package scripts. The client names the game on every
call and is otherwise untouched: rows still come back as assets, days, glasses
and broke, because the shared service hands each game its own field names back.

Nothing about the deployed board moves. The volume keeps its name, the rows are
migrated in place on first boot, and the original table is kept alongside as
scores_lemonade_v1 rather than dropped -- it is the only copy of a board real
people are on. Bundles already in browsers keep working too: a post with no
game at all is treated as this one, deliberately, until this build has been out
long enough that nobody is running the old one.

Build clean.
This commit is contained in:
2026-09-08 22:33:51 -07:00
parent 87474ad5ec
commit 931c2dc299
9 changed files with 33 additions and 399 deletions
+13 -2
View File
@@ -3,8 +3,16 @@
* service rather than the browser. Nothing about it is trusted on the way in:
* the server decides what is plausible, and the client re-checks the shape of
* whatever comes back before putting it on screen.
*
* The service is no longer ours. It is shared with the other games on the site
* -- see https://github.com/Coffey-Labs/games-scores -- which is why every
* call names the game. Nothing else about it leaks in here: rows come back in
* this game's own field names, so this file is otherwise what it always was.
*/
/** Which board on the shared service is ours. */
export const GAME = 'lemonade'
const BASE = (import.meta.env.VITE_SCORES_API ?? '/api').replace(/\/+$/, '')
const TIMEOUT_MS = 8000
@@ -62,14 +70,17 @@ async function call(path: string, init?: RequestInit): Promise<unknown> {
}
export async function fetchScores(): Promise<Score[]> {
return parseBoard(await call('/scores'))
return parseBoard(await call(`/scores?game=${GAME}`))
}
/** Posts a whole table's worth of players at once and returns the new board. */
export async function submitScores(
entries: NewScore[],
): Promise<{ ids: string[]; scores: Score[] }> {
const body = await call('/scores', { method: 'POST', body: JSON.stringify({ entries }) })
const body = await call('/scores', {
method: 'POST',
body: JSON.stringify({ game: GAME, entries }),
})
const ids = (body as { ids?: unknown })?.ids
return {
ids: Array.isArray(ids) ? ids.filter((i): i is string => typeof i === 'string') : [],