**Final report — phase 07 final verification pass** - **Verified:** all 5 tasks present in `complete/`; app boots (`/healthz` 200, `/` 200, static + atlas + `/api/levels` served); `docs/CLIENT.md` module map matches the on-disk `app/static/` tree; no code defects found. - **Tests/lint/coverage:** `bash .agents/validate.sh` → ruff clean, pyright 0 errors, `undercooked-levels validate` pass, **full pytest 1838 passed, 97.21% coverage (floor 90%)**, slow sim stage 2 passed, repo hygiene clean. `uv run pytest tests/e2e -q` → **53 passed** (all 8 suites added this phase). - **Criteria:** (1) two tabs create→join→ready→start→serve one dish, keyboard only — green via `test_us01/02/03/04` ✔; (2) e2e green ✔; (3) canvas non-blank, pixels change after a move — `test_us04_render_smoke` ✔; (4) HUD score within 200 ms of the `served` event — asserted, green ✔; (5) zero console errors + zero off-origin requests — `test_atlas_loads` / `test_us01_landing_surface` ✔; (6) `docs/CLIENT.md` matches layout ✔; (7) 100% suite + 97.21% ✔; (8) docs pass below ✔; (9) no regression in completed phases (all prior suites green) ✔. - **README.md fixed:** status still said "next pending phase is 07" → now 07 complete, 08 pending; the phase-07 client text buried in the phase-05 bullet → its own "canvas client, art, HUD (phase 07)" bullet; stale "What does not exist yet" (claimed movement/controls missing) → rewritten to phases 08–14; "once the room flow ships" → "live in the browser today"; Layout `app/static/` "renderer arrives with the rest of phase 07" → full shipped-client map; Layout `docs/` "CLIENT lands with phase 07" → `CLIENT.md` listed; added `gen_art_index.py` Layout entry. - **AGENTS.md fixed:** "the eight screens" (7 listed) → "the seven screens" (+ `dom.js` is the shared builder, not a screen); e2e naming line → notes the non-story guard `test_atlas_loads.py`. - **Decisions:** no code changes were needed; only the two doc fixes above. Docs re-verified by `test_docs_commands`/`test_docs_index` (44 passed) plus the final full gate. - **Next pending phase:** `08_client_prediction_and_touch`.
71 lines
2.3 KiB
JavaScript
71 lines
2.3 KiB
JavaScript
// Undercooked — the tab identity (PLAN A16; carried over from the
|
|
// phase-01 shell, where these helpers lived in `app.js`).
|
|
//
|
|
// Identity is a 24-byte random token per browser tab (the browser's
|
|
// `crypto.getRandomValues` is the same entropy the server's
|
|
// `secrets.token_urlsafe(24)` uses, and the only option a browser has),
|
|
// held in `sessionStorage` so a reload re-joins instead of re-minting.
|
|
// The last room code is remembered the same way, so a reload can
|
|
// rejoin fast. Nothing here is personal: no names, no PII (PLAN §11).
|
|
|
|
const TOKEN_KEY = "uc.token";
|
|
const LAST_CODE_KEY = "uc.lastCode";
|
|
const CODE_LENGTH = 5;
|
|
|
|
/** Base64url without padding, so the token is safe in a query string or header. */
|
|
function base64url(bytes) {
|
|
let binary = "";
|
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
}
|
|
|
|
/** A random, unguessable id for this browser tab (identity, not an account). */
|
|
export function randomToken() {
|
|
const bytes = new Uint8Array(24);
|
|
globalThis.crypto.getRandomValues(bytes);
|
|
return base64url(bytes);
|
|
}
|
|
|
|
function read(storage, key) {
|
|
try {
|
|
return storage.getItem(key);
|
|
} catch {
|
|
return null; // private browsing / storage disabled: stay functional for this tab only
|
|
}
|
|
}
|
|
|
|
function write(storage, key, value) {
|
|
try {
|
|
storage.setItem(key, value);
|
|
} catch {
|
|
/* nothing to persist to; the in-memory value still works */
|
|
}
|
|
}
|
|
|
|
/** The tab's session token, created once per tab and kept in sessionStorage. */
|
|
export function getToken(storage = globalThis.sessionStorage) {
|
|
const existing = read(storage, TOKEN_KEY);
|
|
if (typeof existing === "string" && existing.length > 0) return existing;
|
|
const token = randomToken();
|
|
write(storage, TOKEN_KEY, token);
|
|
return token;
|
|
}
|
|
|
|
/** Uppercase what the user typed and keep only characters a code can contain. */
|
|
export function normalizeCode(value) {
|
|
return String(value ?? "")
|
|
.toUpperCase()
|
|
.replace(/[^0-9A-Z]/g, "")
|
|
.slice(0, CODE_LENGTH);
|
|
}
|
|
|
|
export function lastCode(storage = globalThis.sessionStorage) {
|
|
return read(storage, LAST_CODE_KEY) || "";
|
|
}
|
|
|
|
export function rememberCode(code, storage = globalThis.sessionStorage) {
|
|
write(storage, LAST_CODE_KEY, normalizeCode(code));
|
|
}
|
|
|
|
export { CODE_LENGTH };
|