Three items the multi-agent sweep flagged, now removed with user
approval (§8 for the solitaire_sync changes):
- WinCascadePlugin: never registered; handle_win_cascade in
AnimationPlugin is the live win cascade and builds its own targets.
Its now-orphaned helpers (win_scatter_targets, cascade_delay,
WIN_CASCADE_INTERVAL_SECS) had no callers outside their own tests
and go with it.
- SyncCompleteEvent: written by the pull-completion system, zero
readers — UI reads SyncStatusResource instead.
- solitaire_sync::ApiError: unused by client and server; the merge_at
crate-root re-export goes too (merge::merge_at stays for the merge
module's own use).
ARCHITECTURE.md updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wasm bundles in solitaire_server/web/pkg/ are no longer tracked. The
web-wasm-rebuild workflow (which rebuilt them in CI and committed them
back to master) is gone; instead:
- solitaire_server/Dockerfile gains a wasm-builder stage that runs
build_wasm.sh with the same pinned toolchain (wasm-bindgen 0.2.120,
wasm-pack 0.14.0, binaryen 130) and the runtime image copies pkg/
from it — the image build is now the artifacts' single source of truth.
- web-e2e builds the wasm before Playwright runs, and its trigger paths
now include the wasm-feeding crates it actually tests.
- docker-build triggers on solitaire_data/** and build_wasm.sh too, so
every wasm-affecting change redeploys.
- Self-hosters serving /web or /play from a source checkout run
./build_wasm.sh once (script header and ARCHITECTURE.md updated).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Double-click/tap auto-move previously tried the run's top card alone
before the full run headed by the clicked card, so a top card with a
foundation move hijacked the intended whole-stack move. Both handlers
now share auto_move_for_run: a lone top card goes foundation-first, a
multi-card run moves whole to a tableau or not at all. The double-click
key is now the clicked card, so two clicks on different cards of the
same stack no longer register as a double-click.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compiler-verified via RUSTFLAGS=--force-warn dead_code plus workspace-wide
reference greps; three parallel audit agents covered the engine crate, the
other eight crates, and Copilot commit-message-vs-diff drift.
Removed:
- replay_overlay/input.rs: 19 orphaned twins (~950 lines) of items also
defined in mod.rs — the glob re-export made the mod.rs copies win and
the file-level #![allow(dead_code)] hid the corpses. The live keyboard/
button handlers and ReplayScrubKeyHold stay; the allow is retired.
- retarget_animation (never called; doc examples were its only refs)
- ScanThemesRequestEvent (never registered/written/read; its doc claimed
a handle_scan_themes consumer that does not exist)
- _VEC3_REFERENCED workaround const + now-unneeded Vec3 import
- solitaire_data: load_stats/save_stats/time_attack_session_with_now
default-path wrappers (the _from/_to variants are the live API) and
surplus re-export names (settings MIN/MAX bounds, token loaders)
- solitaire_core: Session re-export (no external consumer)
- solitaire_wasm: ReplayPlayer::is_finished (no JS caller)
- solitaire_app: build_app wrapper (real entry is run())
Doc fixes:
- audio_plugin: WAV count 5→7, add FoundationCompletedEvent table row,
drop bogus 'placeholder' label, bevy_kira_audio→kira
- ToastVariant::Warning: variant is live (5 writer plugins); dropped the
stale allow(dead_code) and its 'currently unused' comment
Deliberately kept: Spider module (staged forward work), WinCascadePlugin
(documented alternative cascade, pending owner decision), SyncCompleteEvent
and solitaire_sync ApiError/merge_at (§8 change-controlled, flagged to owner).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 38e4c03 (shipped in v0.40.0) switched Android's focused_mode to
reactive_low_power(100 ms) on the premise that every animation tick
system writes RequestRedraw while it has active work. The writers were
never actually added — the diff only contained the WinitSettings change,
imports, and add_message registrations. Result: with no touch input,
nothing wakes the winit loop during a card slide except the 100 ms
fallback ceiling, so animations render at ~10 fps on Android. Desktop
and web keep Continuous mode, which is why only Android was affected
(reported by Rhys the day v0.40.0 shipped; confirmed absent on v0.39.1).
Adds the missing MessageWriter<RequestRedraw> to all seven systems the
original commit message named:
- advance_card_animations (CardAnimationPlugin)
- advance_card_anims (AnimationPlugin — deal/win cascade/slides)
- tick_shake_anim, tick_settle_anim, tick_foundation_flourish
(FeedbackAnimPlugin)
- drive_toast_display (AnimationPlugin — toast countdown)
- drive_auto_complete (AutoCompletePlugin — step-interval keepalive)
Each writes one RequestRedraw per frame while active work exists
(including delay phases, which also need per-frame ticks). Regression
test asserts an active CardAnimation emits RequestRedraw and an idle
board does not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase G of docs/ui-redesign-2026-07.md. The win modal now leads with
actions and reads the stats recap quietly below them:
- Play Again: primary, full-row, Enter accelerator unchanged; still
fires NewGameRequestEvent::default() (same mode; deal options from
Settings) so the rematch is one tap.
- Watch Replay / Share Replay: secondary pair reusing the global
stats_plugin markers (WatchReplayButton / CopyShareLinkButton), so
both act on the just-won replay — SelectedReplayIndex snaps to 0 on
every win. This also fixes the old win-modal handler picking
replays.last(), which after the newest-first history refactor was
the OLDEST replay, not the newest.
- Watch closes the celebration overlay (new close_overlay_on_watch_-
replay system); Share keeps it open and relies on the existing
copy-feedback toasts.
- Stats recap (score breakdown reveal, time, XP, achievements) moves
below the actions; the Time line drops from headline/primary to
body/secondary styling.
Tests: 4 new (action presence, Play Again close+request, Watch closes,
Share keeps open) on a manual-clock fixture that steps the 0.5 s
celebration delay deterministically. Workspace suite green, workspace
clippy -D warnings clean, fmt applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase C dismissal audit: Esc / scrim-tap / Done must behave the same
on every modal. Stragglers found and fixed — none of these had any
Esc path (pause's toggle guard swallowed the key while they were
open):
- Settings: Esc clears SettingsScreen, gated on being the topmost
modal so a stacked sync-setup / theme-store dialog owns Esc
- Help: Esc closes alongside F1/Done (the code comment already
claimed an Esc path existed — now it does)
- Leaderboard: Esc closes when topmost; the display-name dialog
stacked above it now Esc-cancels like sync-setup's dialog
- Theme store: Esc closes (always topmost when open)
Scrim-tap opt-ins are unchanged — ui_modal documents which modals
deliberately stay non-dismissible on outside clicks.
Tests: escape_closes_help_screen, escape_closes_settings_screen_flag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase C of docs/ui-redesign-2026-07.md. The Modes row is gone — Home
owns mode selection, so the popover's Play section carries a Home row
firing the new ToggleHomeRequestEvent (read by toggle_home_screen
alongside the existing M accelerator). Section headers are quiet
caption-size labels inside the existing panel widget, not a new
widget. The action-bar Modes button and its popover are untouched
(their removal is Phase B territory when Home gains hierarchy).
- MenuOption: Modes variant replaced by Home; rows grouped Play (Home)
· You (Profile, Stats, Achievements) · Community (Leaderboard) ·
System (Settings, Help)
- handle_menu_option_click no longer chains into spawn_modes_popover
- Tests: tooltip sweep updated (7 rows still), new
toggle_home_event_opens_home_screen covers the popover's open path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses upstream author review of the Spider core (PR #157):
- RNG: drop the hand-rolled SplitMix64 + Fisher-Yates; deals now use
rand::rngs::StdRng seeded via seed_from_u64 + SliceRandom::shuffle,
exactly like klondike::with_seed. solitaire_core gains the same
pinned rand dep klondike uses (0.10.1, std_rng only — already in the
lock, no new transitive deps). Deals for a given seed change; Spider
has no persisted games yet, so nothing breaks.
- Enums over integers: pile indices and card counts in the instruction
type are now SpiderTableau (Tableau1..Tableau10, with an ALL const)
and RunLength (Run1..Run13); out-of-range piles and zero-card moves
are unrepresentable. Rank comparisons use Rank::checked_add instead
of u8 arithmetic.
- Fixed-size containers: build_deck returns Stack<104> instead of
Vec<Card>; possible_instructions is a const-iterated SpiderIter +
validity filter (klondike's KlondikeIter pattern) instead of
collecting into a Vec.
- Upstream naming: SpiderGame -> Spider (matches Klondike),
tableau_face_up/_down -> tableau_face_up_cards/_down_cards,
stock_len -> stock() accessor, with_rng added alongside with_seed.
- Solver access: SpiderGameState now exposes session(), the same
escape hatch GameState has — the wrapper no longer pretends to hide
solve(), which SessionConfig budgets already gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs 514/516/519 (incl. master itself) died on 'ld terminated with
signal 9' — the workspace now links enough large test binaries that
per-core parallel linking OOMs the runner even at line-tables-only
debuginfo. CARGO_BUILD_JOBS=2 keeps at most two links in flight.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase E of docs/ui-redesign-2026-07.md. New you_hub_plugin owns the
modal shell (header, shared tab chips, single Done); each tab's content
is a body builder extracted from its original plugin with every marker
component unchanged, so per-row update/scroll/selector systems keep
working. The replay selector gets its own Replays tab (Watch/Copy
buttons move into the tab body).
- Toggle*RequestEvents + P/S/A accelerators open the hub on the right
tab, switch tabs in place, or toggle closed on a same-tab request;
Esc/Done/scrim-click close
- Legacy ProfileScreen/StatsScreen/AchievementsScreen markers ride the
hub scrim for the active tab — external queries and tests keep their
meaning
- Standalone toggle/close systems and per-screen Done buttons removed
(ProfileCloseButton, StatsCloseButton, AchievementsCloseButton)
- Tests: 2 new hub lifecycle tests; profile/stats/achievements modal
tests adapted (fixtures add YouHubPlugin; selector tests target the
Replays tab). Engine suite 916 green, clippy -D warnings, fmt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settings' tab_chip becomes a thin wrapper; the You hub (Phase E) will
reuse the same widget so tabbed modals stay visually identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the 37-row single-scroll settings modal with five tabs
(Audio, Gameplay, Appearance, Accessibility, Account); only the active
tab's rows spawn, so every tab fits without scroll-hunting.
- SettingsTab + ActiveSettingsTab (session-only) + SettingsTabButton
chips under the modal header; rebuild-on-switch mirrors the
leaderboard despawn/respawn pattern via a shared build_panel helper
- Accessibility tab gathers color-blind, high-contrast, reduce-motion,
touch-input, and tooltip-delay from the old Gameplay/Cosmetic mix;
Account = Sync + Privacy
- SettingsButton enum, input handlers, and persistence untouched
- Tests: per-tab Focusable/Tooltip sweeps, tab-switch rebuild test,
picker/thumbnail tests pinned to the Appearance tab
Plan: docs/ui-redesign-2026-07.md (also added, phases A–M)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SolutionPlaybackPlugin: the pause modal's 'Show solution' button
resumes the game and requests a solve of the live position via
GameState::winning_line on AsyncComputeTaskPool (never blocks the main
thread; stale results discarded via a move_count snapshot). The line
then plays back one instruction per 0.45 s through the normal
MoveRequestEvent / DrawRequestEvent pipeline — animations, scoring,
undo history and win detection behave as if the player made the moves.
Tableau run counts are decoded against the live state at step time.
Playback cancels on Esc, pause, undo / new-game requests, and any
rejected move (the signature of the player diverging the board).
Toasts cover search start, line found, unwinnable, and budget-
exhausted outcomes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two-deck (104-card) Spider on the upstream Stack/Pile containers,
exercising the multi-deck Card encoding for the first time:
- SpiderGame: 10-pile deal (4x6 + 6x5), build-down-any-suit,
same-suit-run pickup, deal-10 gated on no empty pile, automatic
K->A run removal, win at 8 runs
- SpiderSuits difficulty (1/2/4 suits over the same 104 cards);
1- and 2-suit games contain identical Card values by construction
(documented — engine entity mapping will need positional keys)
- Seeded deals via inline SplitMix64 + Fisher-Yates (core has no
rand dep; Spider's seed space is deliberately self-contained)
- SpiderGameState session wrapper mirroring GameState conventions:
Result<_, MoveError> mutations, upstream undo/score bookkeeping,
Microsoft-style scoring (500 base, -1/move, +100/run, -1/undo)
- 17 unit tests + card-conservation/validity proptest over random
legal walks; stacked-deal win-path test included
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New GameState::winning_line(moves_budget, states_budget) returns the
complete instruction sequence to a win (Ok(None) when unwinnable or
already won, Err on budget exhaustion), compacting the raw DFS trace
with the previously unused card_game Solution::clean_solution and
stripping foundation→foundation no-ops when the stripped line still
replays to a win — an internal replay check guarantees the returned
sequence always applies cleanly via apply_instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #152 poller fixes resolution + layout on a missed fold/unfold, but
a transient clip could survive if card sprites held stale visuals after
geometry converged. Emit StateChangedEvent alongside the synthetic
WindowResized so card_plugin re-renders every sprite from scratch —
silent self-heal, no player-facing prompt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Points THEME_STORE_DIR at the existing solitaire-db PVC so catalog
content survives pod restarts. Scan is startup-only; restart the
deployment after adding zips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1+2 of the theme-store roadmap: a free catalog served by
solitaire_server and an in-app browse/install flow, making custom
themes installable on Android for the first time (the manual
drop-a-zip flow can't reach the app-private themes dir there).
- solitaire_sync: ThemeCatalogEntry/ThemeCatalogResponse wire types
(additive module; SyncPayload and SyncProvider untouched)
- solitaire_server: THEME_STORE_DIR scan at startup (meta-only
theme.ron parse, sha256, 20 MiB cap, best-effort per archive);
public GET /api/themes, /api/themes/{id}/download, /{id}/preview;
compose volume + README_SERVER docs
- solitaire_data: ThemeStoreClient — catalog fetch + download with
mandatory size/sha256 verification before bytes are released
- solitaire_engine: ThemeStorePlugin — 'Browse theme store' button in
Settings → Cosmetic, modal catalog (leaderboard-style rebuild),
download on AsyncComputeTaskPool, atomic .tmp+rename write into
user_theme_dir, then the existing hardened import_theme pipeline and
an in-place registry refresh
New deps: sha2 (workspace, server+data); ron/zip reused in server;
serde_json added to solitaire_sync dev-deps for DTO round-trip tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- set_theme resolved every id to themes:// — the bundled dark/classic
themes live at embedded://, so switching to them via the public API
would NotFound and silently keep the old theme. All three load paths
now share one theme_manifest_url resolver.
- load_initial_theme's settings-absent fallback was "classic", a
leftover from v0.33 when classic was the default; it now derives from
Settings::default() so it can't drift from the data crate again, with
a test pinning the default id to a bundled theme.
- settings.rs doc no longer claims "classic" is migrated to "dark";
only the pre-rename "default" id is rewritten.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refresh tokens are single-use since the rotation change (PR #136). Two
in-flight requests hitting 401 together (replay upload racing a manual
sync) both called /api/auth/refresh; the second presented an already-
consumed token, got rejected, and surfaced as a spurious 'session
expired' re-login prompt.
refresh_token() now takes the stale access token that earned the 401
and runs behind a tokio::sync::Mutex; a loser of the lock race that
finds the stored token already rotated returns Ok and retries with it
instead of spending the new refresh token.
Untested caveat: exercising the race in a unit test needs an in-memory
auth_tokens double (the real keyring is unavailable on headless Linux
runners) — noted for a future test-support addition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
§8 documented a Waste variant that doesn't exist in klondike 0.4; the
real convention (Stock denotes the waste pile in pile-coordinate space)
is now stated where the enum is sketched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>