Replays previously persisted only seed + moves and re-dealt the board
from the seed at playback time, so any change to the seed->deal mapping
(RNG bumps, upstream upgrades) silently invalidated every existing
replay. Schema v4 instead embeds a SessionRecording - the upstream
card_game Session serde ({config, initial_state, instructions}) - so
playback rebuilds the exact recorded board; seed/draw_mode/mode remain
caption metadata only.
- core: SessionRecording newtype delegating to Session<Klondike> serde;
GameState::recording() / from_recording(); from_instructions_unchecked
fixture helper; serde_json added to dev-deps (tests only)
- data: Replay v4 (recording replaces moves); v1-v3 files rejected by
the existing version gate
- engine: win-recording and sync upload freeze game.recording();
playback rebuilds from the recording; Playing carries the extracted
move list (+ Box<Replay> for clippy large_enum_variant)
- wasm: replay_export() builds the full v4 upload payload so JS never
hand-assembles it (the old game.js path hardcoded schema_version: 2
and corrupted u64 seeds via Math.round); ReplayPlayer::from_json
enforces schema_version == 4 with a descriptive error
- web: game.js/play.html use replay_export; replay.js surfaces player
construction errors in the caption instead of dying silently
- server: mode validation accepts data-carrying GameMode variants
(Difficulty uploads previously 400'd against the String field)
Both replays on prod are May-era v1 rows with empty move lists - every
shared replay was already unplayable; the viewer now says why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- sync.rs: replace Uuid::nil() placeholder with the authenticated
user's real UUID before the mismatch check so desktop client pushes
no longer fail with 400 user_id mismatch (#73)
- replays.rs: use server-computed received_at instead of client-supplied
header.recorded_at when updating leaderboard recorded_at to prevent
timestamp spoofing (#74)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess
- #68: Move fire_flush outside per-event loop in analytics (batch flush once)
- #56: Persist progress before marking reward_granted to prevent XP loss on crash
- #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh
- #62: Add validate_header() in replay upload with mode/draw_mode allowlists
- #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original
queries already in .sqlx cache; EXISTS variant would require sqlx prepare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- leaderboard.html, replays.html: escape user-supplied display_name and
username before inserting into innerHTML to prevent stored XSS
- game.js: call POST /api/replays on win so browser-game completions are
recorded; scores were never submitted before this fix
- replays.rs: after replay insert, upsert leaderboard best_score /
best_time_secs for opted-in users when the new score beats their current
best (classic mode only); scores were never updated before this fix
- leaderboard.rs: add LIMIT 100 to GET /api/leaderboard to prevent
unbounded query growth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
API surface for the web replay viewer to come:
- `POST /api/replays` — auth required; persists the JSON body
verbatim, mints a server-side UUID, returns `{id}`. Three columns
(final_score, time_seconds, recorded_at) are projected out of the
payload at insert time so list endpoints don't have to scan blobs.
- `GET /api/replays/recent` — public; returns the N most-recent
replays across users (limit defaults to 20, capped at 50). Joins
the username so the feed reads as "AliceWon · 2:14 win".
- `GET /api/replays/:id` — public; returns the full replay JSON
the desktop client uploaded.
Migration `002_replays.sql` adds the `replays` table with indexes
on `received_at DESC` (recent feed) and `user_id` (per-user views).
Schema-version compatibility is the playback side's responsibility,
matching the desktop's existing `schema_version` gate — the server
just stores and serves whatever JSON came in.
`AppError::NotFound` added so `GET /api/replays/:id` can return a
proper 404 instead of an internal-server-error.
`.sqlx` cache regenerated for the new `query!` invocations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>