New `solitaire_data::replay` module:
- `Replay` struct: seed + draw_mode + mode + ordered move list +
presentation metadata (time / score / date). Replays are
reconstructed by rebuilding `GameState::new_with_mode` and applying
the move list in order — a deterministic state machine driven by
atomic player inputs, no per-step snapshots stored.
- `ReplayMove`: one variant per atomic player input. `Move {from, to,
count}` covers card moves; `StockClick` covers every click on the
stock (the engine resolves draw-vs-recycle deterministically from
current state during both record and playback).
- Schema-versioned (`REPLAY_SCHEMA_VERSION = 2`); legacy files are
rejected via the version gate so older replays just disappear from
the UI rather than half-loading.
- Atomic save (.tmp -> rename), `dirs::data_dir()`-based path
resolution. 5 round-trip / atomic / version-gate / corruption tests.
Sync trait extension:
- `SyncProvider::push_replay(&Replay)` — default returns
`UnsupportedPlatform` so `LocalOnlyProvider` is silently no-op'd by
the future push-on-win path. Mirrors the existing `pull` / `push`
default-impl pattern.
- `SolitaireServerClient::push_replay` — `POST /api/replays`, same
401-refresh-and-retry shape as `push`.
The wire format is the contract: `solitaire_wasm` (added in a later
commit) parses the JSON via its own minimal mirror struct so it can
compile to wasm32 without pulling the desktop client's transitive
deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deletes the solitaire_gpgs crate and all GPGS references from settings,
sync client, profile plugin, CLAUDE.md, and ARCHITECTURE.md. The
self-hosted server covers all sync needs without the Android-only backend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds delete_account() as a default no-op on the SyncProvider trait.
SolitaireServerClient sends DELETE /api/account with JWT (retry on 401).
The server handler already existed (DELETE /api/account, ON DELETE CASCADE).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add fetch_daily_challenge() to SyncProvider trait (default: Ok(None))
- SolitaireServerClient calls GET /api/daily-challenge (public endpoint)
and returns the ChallengeGoal; non-2xx responses return Ok(None) so
callers fall back to the local date-hash seed
- DailyChallengePlugin spawns an async task on Startup (only when
SyncProviderResource is present) and polls it in Update; on success
it overwrites DailyChallengeResource.seed with the server's seed,
ensuring all players worldwide get the same deal on a given date
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Press L to open a leaderboard overlay. On open, an async fetch is
dispatched against the active SyncProvider. Results cache in
LeaderboardResource; the panel rebuilds live when data arrives.
Closing during a fetch is safe (ClosedThisFrame flag prevents
re-spawning the panel in the same frame as the user's despawn command).
Format: ranked table with player name, best score, and fastest win time.
Non-authenticated / LocalOnly providers return an empty list gracefully.
- solitaire_data: add fetch_leaderboard() to SyncProvider trait
(default → Ok([])) and implement in SolitaireServerClient
- solitaire_engine: new LeaderboardPlugin with 5 unit tests
- solitaire_app: register LeaderboardPlugin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- solitaire_server: Axum auth, sync push/pull, leaderboard, daily
challenge, account deletion, JWT middleware, rate limiting via
tower_governor, SQLite migrations, health endpoint
- solitaire_server: expose build_test_router (no rate limiting) so
integration tests work without a peer IP in oneshot requests
- solitaire_sync: SyncPayload, merge logic, shared API types
- solitaire_data: SyncProvider trait, LocalOnlyProvider,
SolitaireServerClient, auth_tokens keyring integration, blanket
Box<dyn SyncProvider> impl
- solitaire_data/settings: derive Default on SyncBackend (clippy fix)
- .sqlx/: offline query cache so server compiles without a live DB
- sqlx: removed non-existent "offline" feature flag
- keyring v2: fixed Entry::new() returning Result<Entry>
- sqlx 0.8: all SQLite TEXT columns wrapped in Option<T>
- Integration tests: max_connections(1) on in-memory pool so all
connections share the same schema
All 191 tests pass; cargo clippy -D warnings clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>