Files
ducoterra 95ca3fd1fa phase: 07_canvas_client_and_hud
**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`.
2026-10-10 18:03:52 -04:00

85 lines
2.8 KiB
JavaScript

// Undercooked — the landing screen (phase 07 task 02).
//
// The door of the app: *Create lobby* (a `POST /api/rooms`, the token in
// the body — the session is minted server-side and returned once) and the
// join form (code normalised to five uppercase characters, remembered in
// `sessionStorage` so a reload rejoins fast). Server refusals that land
// here — the process's `room_full` on create, an unreachable server —
// render in the status region, which stays on the screen (the app only
// leaves for a real room).
import { el, frag, normalizeCode, CODE_LENGTH } from "./dom.js";
let statusEl = null;
let joinInput = null;
let bannerText = "";
export function mount(target, _data, app) {
bannerText = app.banner ?? "";
statusEl = el(
"p",
{ id: "status", class: "status", role: "status", "aria-live": "polite" },
bannerText || "Pick a kitchen to start.",
);
joinInput = el("input", {
id: "join-code",
name: "code",
type: "text",
inputmode: "text",
autocapitalize: "characters",
spellcheck: "false",
maxlength: String(CODE_LENGTH),
size: String(CODE_LENGTH),
placeholder: "ABCDE",
"aria-describedby": "join-hint",
required: true,
value: app.lastCode || "",
});
const form = el("form", { id: "join-form", class: "panel", autocomplete: "off" },
el("h2", { id: "lobby-heading" }, "Kitchen lobby"),
el("p", { id: "lobby-help", class: "tagline" },
"Create a kitchen and read its 5-character code out loud, or join one that is already open. Nothing is saved and nothing is asked for."),
el("button", {
id: "create-lobby",
class: "btn btn-primary",
type: "button",
onClick: () => {
app.banner = null;
app.createLobby();
},
}, "Create lobby"),
el("label", { for: "join-code" }, "Room code"),
el("div", { class: "join-row" }, joinInput, el("button", { class: "btn", type: "submit" }, "Join")),
el("p", { id: "join-hint", class: "hint" },
"Five characters, letters and digits, never ",
el("code", {}, "I"), ", ",
el("code", {}, "L"), ", ",
el("code", {}, "O"), " or ",
el("code", {}, "U"), "."),
statusEl,
);
form.addEventListener("submit", (event) => {
event.preventDefault();
const code = normalizeCode(joinInput.value);
joinInput.value = code;
if (code.length !== CODE_LENGTH) {
setBanner(`A room code is ${CODE_LENGTH} characters.`);
joinInput.focus();
return;
}
app.banner = null;
app.join(code);
});
target.append(frag(form));
}
export function unmount() {
/* no timers; the app's show() clears the root */
}
export function setBanner(message) {
if (statusEl === null) return;
bannerText = message;
statusEl.textContent = message || "Pick a kitchen to start.";
}