Commit Graph

2 Commits

Author SHA1 Message Date
thejayman77 065ab98598 Games sync hardening (Codex audit): server-side state normalization
Don't trust client JSON at the storage layer:
- sanitize_game_state() runs before merge AND on the merged result (heals legacy
  rows). Word Search: keep only finds whose cells actually spell a real word in
  that day's grid (validated when the puzzle exists, shape-only 4-12 alpha +
  cell-length otherwise), dedupe, renumber ci. Word: validate status enum, guess
  count/length/alpha, colour-row shape, terminal answer/why.
- Completion is now derived from the real puzzle word count (foundWords ==
  expected), not a client-sent `ms` — so stats can't be inflated by junk.
- Date validated as YYYY-MM-DD at the API (400 otherwise) — no junk/future rows.

Tests: sanitizer-rejects-junk + bad-date 400; existing tests updated to use
real-shaped data (the sanitizer is a good forcing function). 237 pytest + 11
vitest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:51:24 -04:00
thejayman77 dd0df64d76 Games: cross-device sync + overlap colour-blend
Two game polish items:

- Word Search: overlapping cells now multiply-blend the crossing words' colours
  (deepening to a darker shade with readable text) instead of the newest colour
  stomping the rest — matches the new interlocking grids.

- Cross-device game-state sync (signed-in): per-puzzle progress + stats now
  follow you between devices. New game_state table; server-side merge on every
  save so two devices converge regardless of push order, tailored per game:
  * Word Search → UNION of finds (monotonic; can't un-find), earliest start,
    best completion time.
  * Word → furthest-progress wins (terminal beats in-progress; more guesses
    beats fewer) — picks one device's game whole, never splices guesses.
  Stats (streak/distribution/best) derived server-side from the synced states,
  so they're consistent instead of per-device counters. Endpoints GET/PUT
  /api/games/state + GET /api/games/stats (signed-in; size-capped). Frontend is
  local-first: games paint instantly from localStorage, then reconcile in the
  background; both game components push debounced on each move and adopt the
  merge. Conflict handling unit-tested + an API two-device convergence test.

235→ tests + 11 vitest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:35:20 -04:00