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

88 lines
2.6 KiB
JavaScript

// Undercooked — the error screen (phase 07 task 02).
//
// Friendly copy per failure (the `err` codes of docs/PROTOCOL.md and
// the close codes the connection classifies), with the *right* retry
// affordance for each: a `4400` protocol mismatch must not auto-retry
// (the client is out of date), a `4401` room-gone must not retry the
// dead code, a `4402` full room points at another room — and the room
// code is shown for support.
import { el } from "./dom.js";
/** code → { title, detail, retry } (retry = "a retry is offered"). */
const COPY = {
room_gone: {
title: "That kitchen is gone",
detail: "It may have closed, or the code was mistyped.",
retry: false,
},
room_full: {
title: "That kitchen is full",
detail: "Every seat is taken. Start your own room, or try another code.",
retry: false,
},
protocol_mismatch: {
title: "Protocol mismatch",
detail:
"This server speaks a protocol version this client does not. Retrying will not help — the client needs an update.",
retry: false,
},
evicted: {
title: "You are on another tab",
detail: "A second tab took this seat. Use the other tab, or come back later.",
retry: false,
},
internal: {
title: "The kitchen hit an internal error",
detail: "The round was aborted for everyone. Start a new room.",
retry: false,
},
closed: {
title: "The connection closed",
detail: "The server closed the socket. You can try the same room again.",
retry: true,
},
unreachable: {
title: "Cannot reach the server",
detail: "The socket could not be opened. Check the address and try again.",
retry: true,
},
};
export function mount(target, data, app) {
const code = data.code ?? "closed";
const copy = COPY[code] ?? COPY.closed;
const buttonRow = el(
"div",
{ class: "error-actions" },
el("button", {
id: "error-back",
class: "btn btn-primary",
type: "button",
onClick: () => app.backToLanding(),
}, "Back to landing"),
);
if (copy.retry && app.roomCode !== "") {
buttonRow.prepend(
el("button", {
id: "error-retry",
class: "btn",
type: "button",
onClick: () => app.retryJoin(),
}, "Try again"),
);
}
const children = [
el("h2", { id: "error-title" }, copy.title),
el("p", { id: "error-detail" }, copy.detail),
];
if (app.roomCode !== "") {
children.push(el("p", { id: "error-code" }, `Room code: ${app.roomCode}`));
}
target.append(el("div", { class: "panel error" }, el("div", { role: "alert" }, ...children), buttonRow));
}
export function unmount() {
/* static */
}