v0.5.2 — the splash's multiplayer door opens, and knows whether it should

The "Play multiplayer" door on index.html had sat disabled, labelled
"Coming soon", since before the server existed — Phases 2 through 4 built
a working lobby and nothing ever linked to it. Loading the site landed on
the same solitaire splash whether a real multiplayer server was behind it
or not, with no visible way in. Found packaging Phase 6 for StartOS.

The door is now a live link to ./play.html?lobby, and main.ts's start()
routes ?lobby straight to the lobby screen — the same showScreen('lobby');
runLobby(beginRemote) the in-game Multiplayer button already used —
instead of dealing a solitaire game first.

GET /api/health is new, and exists to be failed. The same dist/ ships both
served by src/server/ and uploaded as flat files by deploy-web.ts, and the
bundle is identical either way (D4), so the page cannot know from its own
build which it is; every other route 404s an unknown path exactly as a
static host does, so nothing distinguished them. The splash probes it on
load and closes the door when nothing names itself in reply.

The door starts open and only ever closes, deliberately: a wrong "no
server" is the bug above again — invisible, and it strands a player who
does have one — while a wrong "there is one" costs a click and a lobby
that says it cannot connect. The reply must name itself rather than merely
return 200, or a host answering every path with its index page would pass.

Verified: tsc clean; 659 tests pass (656 + 3); /api/health exercised live
against a running server — 200 with the right body, unauthenticated, while
an unknown path and a wrong method both still 404, which is what makes the
probe discriminate at all.

The probe's own test was vacuous on the first attempt — both its "closes"
cases reached close() through the .catch arm, so deleting the body-naming
check outright still passed. Caught by mutating splash.ts and re-running;
the test now covers all three closing routes and fails without the check.
This commit is contained in:
Jesse
2026-08-21 11:00:06 -04:00
parent e76bd77099
commit 62b6ed7e1b
8 changed files with 272 additions and 7 deletions
+19
View File
@@ -197,6 +197,25 @@ export function startServer(opts: ServerOptions): void {
void (async () => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
// -- Is anyone home? --------------------------------------------------------------------
/**
* THE ONE ROUTE THAT EXISTS TO BE FAILED.
*
* The same `dist/` is served two ways: by this server, and as a plain static upload with no
* server behind it at all (`scripts/deploy-web.ts`). The bundle is byte-identical either way
* — one client, mode decided at runtime (D4) — so the page cannot know from its own build
* which it is, and every other route here answers a 404 for a path it does not have, exactly
* as a static host would. Nothing distinguished them until this did.
*
* Unauthenticated on purpose: it says only that a Station Master server is answering, which
* is what the client is about to offer the player anyway. It reveals no game and no seat.
*/
if (url.pathname === '/api/health' && req.method === 'GET') {
sendJson(res, 200, { ok: true, service: 'station-master', engineVersion: opts.engineVersion });
return;
}
// -- Lobby: creating and joining (the door — join-secret gated) --------------------------
if (url.pathname === '/api/lobby/create' && req.method === 'POST') {