From 36142de6c518cc4d1cd84a773986ce3266e90223 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Tue, 28 Apr 2026 10:03:54 +0700 Subject: [PATCH] chore(plans): add todo-backlog implementation plan 9-phase plan covering the highest-leverage items from `plans/todo.md` plus tech-debt and UX polish. YAGNI cuts: GhostBoardPreview (rule of three not met), upstream-blocked override removals, parking-lot features. Phase 1 (auto-tick test) marked completed. --- .../phase-01-auto-tick-test.md | 92 +++++++++++++ .../phase-02-ci-inline-script-guard.md | 96 +++++++++++++ .../phase-03-mode-picker-glyphs.md | 68 +++++++++ .../phase-04-settings-modal-sticky.md | 80 +++++++++++ .../phase-05-cho-row-indicator.md | 130 ++++++++++++++++++ .../phase-06-confetti-polish.md | 99 +++++++++++++ .../phase-07-strict-csp-hashed.md | 128 +++++++++++++++++ .../phase-08-audio-cache-lru.md | 85 ++++++++++++ .../phase-09-pwa-verify-install.md | 92 +++++++++++++ .../plan.md | 49 +++++++ 10 files changed, 919 insertions(+) create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-01-auto-tick-test.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-02-ci-inline-script-guard.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-03-mode-picker-glyphs.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-04-settings-modal-sticky.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-05-cho-row-indicator.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-06-confetti-polish.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-07-strict-csp-hashed.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-08-audio-cache-lru.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/phase-09-pwa-verify-install.md create mode 100644 web/plans/260428-0927-implement-todo-backlog/plan.md diff --git a/web/plans/260428-0927-implement-todo-backlog/phase-01-auto-tick-test.md b/web/plans/260428-0927-implement-todo-backlog/phase-01-auto-tick-test.md new file mode 100644 index 0000000..f55d90e --- /dev/null +++ b/web/plans/260428-0927-implement-todo-backlog/phase-01-auto-tick-test.md @@ -0,0 +1,92 @@ +--- +name: Auto-tick integration test +phase: 1 +status: completed +priority: high +effort: 1-2h +completed: 2026-04-28 +--- + +# Phase 1 — Auto-tick integration test + +## Context +- TODO entry: highest-leverage item ("dedup-by-`at` fix already caught a P0") +- `src/lib/PlayerBoard.svelte:157-171` (auto-tick effect, dedup guard) +- `src/lib/call-bus.svelte.js` (bus driver — already tested) +- `src/lib/game-logic.js` — `findUncrossedCell` (pure helper) +- Existing test infra: vitest + happy-dom, **no** `@testing-library/svelte` + +## Decision +Don't add `@testing-library/svelte` just for one component. Extract the +auto-tick body into a pure helper in `game-logic.js`, test that. The +effect in `PlayerBoard.svelte` becomes a thin wrapper that calls the +helper and applies the result. + +## Files +- Create: `src/lib/auto-tick.js` +- Create: `src/lib/auto-tick.test.js` +- Modify: `src/lib/PlayerBoard.svelte` — replace effect body with helper call + +## Helper API + +```js +/** + * @param {object} args + * @param {number[][]} args.grid + * @param {boolean[][]} args.crossed + * @param {{num: number, at: number} | null} args.lastDraw + * @param {number} args.lastHandledAt + * @param {"player" | "master" | "both"} args.mode + * @returns {{ crossed: boolean[][], lastHandledAt: number, changed: boolean }} + */ +export function processAutoTick({ grid, crossed, lastDraw, lastHandledAt, mode }) { + if (!lastDraw) return { crossed, lastHandledAt, changed: false }; + if (lastDraw.at === lastHandledAt) return { crossed, lastHandledAt, changed: false }; + const next = { lastHandledAt: lastDraw.at }; + if (mode !== "both") return { crossed, ...next, changed: false }; + if (!grid || crossed.length === 0) return { crossed, ...next, changed: false }; + const target = findUncrossedCell(grid, crossed, lastDraw.num); + if (!target) return { crossed, ...next, changed: false }; + const updated = crossed.map((row, ri) => + ri === target.row ? row.map((v, ci) => (ci === target.col ? true : v)) : row + ); + return { crossed: updated, ...next, changed: true }; +} +``` + +## Test cases (5) +1. NEW draw, mode=both, number on board → cell crossed, `changed: true` +2. Same `at` re-fire → no mutation, `changed: false`, `lastHandledAt` unchanged +3. Manual untick after auto-tick, then SAME number with NEW `at` → re-crosses +4. mode=player → no mutation regardless of draw +5. Number not on board → no mutation, `lastHandledAt` advances (we still consume the event) + +## Steps +1. Create `auto-tick.js` with `processAutoTick` (above). Import `findUncrossedCell`. +2. Create `auto-tick.test.js` — mirror style of `call-bus.test.js`. +3. Refactor `PlayerBoard.svelte:157-171` effect: + ```js + $effect(() => { + const drawn = bus.lastDrawn; + const result = processAutoTick({ + grid, crossed, lastDraw: drawn, + lastHandledAt: lastHandledDrawAt, + mode: settings.mode, + }); + lastHandledDrawAt = result.lastHandledAt; + if (result.changed) crossed = result.crossed; + }); + ``` +4. `npm test` — all green. +5. Manual smoke: open app in mode=both, draw a number from master panel, confirm player cell crosses. + +## Success +- 5 unit cases pass. +- `npm test` green (no regression in existing tests). +- Manual smoke confirms behavior unchanged. + +## Risks +- The effect tracks `bus.lastDrawn` reactively — extracting body into a + helper means the effect still needs to *read* `bus.lastDrawn` for + reactivity. The wrapper above does that correctly (read inside the + `$effect` body before passing to helper). diff --git a/web/plans/260428-0927-implement-todo-backlog/phase-02-ci-inline-script-guard.md b/web/plans/260428-0927-implement-todo-backlog/phase-02-ci-inline-script-guard.md new file mode 100644 index 0000000..41a6ad2 --- /dev/null +++ b/web/plans/260428-0927-implement-todo-backlog/phase-02-ci-inline-script-guard.md @@ -0,0 +1,96 @@ +--- +name: CI inline-script guard +phase: 2 +status: todo +priority: high +effort: 30m +--- + +# Phase 2 — CI inline-script guard + +## Context +- TODO: SvelteKit emits one inline bootstrap script. CSP relaxed to + `'unsafe-inline'` to accommodate. If a future SvelteKit upgrade adds + another inline block, we want CI to fail loudly. +- `static/_headers:2` — current CSP includes `script-src 'self' 'unsafe-inline'`. +- No CI build job today (Cloudflare Pages builds on its own; only + `.github/workflows/deploy-github-pages.yml` is a redirect-only job). + +## Decision +Add an `npm run verify:build` script that: +1. Counts `