- Reorganise card PNGs into assets/cards/faces/{classic,dark}/ and
assets/cards/backs/{classic,dark}/
- Rasterise dark SVG theme alongside existing classic set
- Add "Dark / Classic" toggle button in the game HUD; persists to
localStorage as fs_theme (defaults to classic)
- Preload both themes on page load so switching is instant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only game.html had the snippet; the other five pages were missing it,
causing the Matomo installation verification check to fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
check_auto_complete no longer requires the waste pile to be empty —
only the stock must be exhausted and all tableau cards face-up.
next_auto_complete_move checks the waste top card before scanning
tableau, and auto_complete_step falls back to draw() when no direct
foundation move is available so the waste drains automatically.
Fixes the end-game state where the player could see a clear win but
the auto-complete interval never fired because the waste was non-empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the hand-rolled analytics endpoint and SQLite event table in favour
of Matomo — a self-hosted, full-featured analytics platform.
k8s:
- Deploy MariaDB 11 + Bitnami Matomo 5 in the solitaire namespace
- Route analytics.aleshym.co ingress to the Matomo service
- Remove Datasette sidecar and its BasicAuth middleware/secret
- Remove the analytics port from the solitaire-server Service
Rust:
- Replace AnalyticsClient (custom HTTP endpoint) with MatomoClient (Matomo
HTTP Tracking API bulk endpoint); maps game events to Matomo categories
- Add matomo_url + matomo_site_id fields to Settings (serde default → None/1)
- Privacy toggle in Settings now activates when matomo_url is set (not tied
to SyncBackend::SolitaireServer)
- Remove POST /api/analytics route from solitaire_server
Web:
- Add Matomo JS tracking snippet to game.html (/play page)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.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>
When a card flipped face-up, the browser fetched the PNG on demand,
showing the cream fallback colour until the image arrived. Preloading
all 52 faces and the back at module load ensures they are cached before
any flip can occur.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add account.html: tabbed form for login and registration, signed-in
state with sign-out, links to leaderboard and replays
- Wire /account route in build_router_inner
- Add Account card to landing page
- Link leaderboard login prompt to /account for new users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all display-name occurrences across web pages, Rust source,
docs, and Cargo metadata. Update localStorage token key from sq_token
to fs_token. Tagline "Klondike Solitaire" retained as genre descriptor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add leaderboard.html: JWT login form + localStorage token + table
- Add replays.html: public listing of recent replays, row click to viewer
- Wire /leaderboard and /replays routes in build_router_inner
- Fix home.html Recent Replays link from /api/replays/recent to /replays
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns /play with the landing page and app color scheme — same
bg, panel, accent, and felt tokens from ui_theme.rs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replay viewer was using the old midnight-purple palette. Both pages now
use the exact color tokens from ui_theme.rs — matching the desktop and
Android app exactly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without top:0;left:0, Firefox and other non-Chrome engines place
absolute elements at the content edge (padding offset = 20px) before
the JS transform is applied, shifting slots 20px below/right of cards.
Cards already had explicit top:0;left:0; slots now match.
.recycle-label also had top:50%;left:50% which combined with the JS
inline transform would place the ↺ symbol halfway across the board.
Changed to top:0;left:0 so JS transform is the sole position source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- header: position sticky so HUD/controls never scroll off screen
- .card .corner.bottom: remove rotate(180deg) — ♠ rotated looks like ♥,
causing players to misread suit on the bottom corner
- main: add min-width:0 so flex container doesn't push board off-edge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `SolitaireGame` WASM binding to `solitaire_wasm` exposing draw(),
move_cards(), undo(), auto_complete_step(), and state() — all backed by
the real solitaire_core rules engine.
Add /play route to solitaire_server serving a full vanilla-JS
interactive Klondike game (game.html / game.css / game.js). Features:
drag-and-drop card moves (mouse + touch via PointerEvents), click stock
to draw, double-click card to auto-move to foundation, undo, draw-1/3
toggle, new game, auto-complete animation, win overlay, seed display.
Rebuild solitaire_wasm.js + solitaire_wasm_bg.wasm.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The replay viewer's renderer used to wipe and rebuild every card
from scratch on every step (`board.replaceChildren()`). Each step
was a discrete redraw — fine for correctness, abrupt for the eye.
Restructured to a persistent card-element model:
- `#board` is now a positioned context (relative) instead of a
CSS grid. The dashed empty-pile placeholders are absolutely-
positioned `.slot` elements painted once at bootstrap.
- Each card lives as a sibling of the slots, absolutely-positioned
with `transform: translate(x, y)`. The CSS transition on
`transform` (280 ms cubic-bezier) runs every move as a flight
rather than a redraw.
- `cardEls: Map<id, HTMLElement>` persists across renders. Cards
unchanged between steps don't re-create their DOM at all.
- Z-index is set per-render from the card's pile index so a card
flying out from the bottom of a tableau passes behind the cards
above it.
- Newly-spawned cards (rare — only on Restart) fade in at their
target position via a `requestAnimationFrame` opacity flip;
cards that disappear (also rare) fade out and despawn after the
220 ms fade.
- `will-change: transform` lets the browser composite the
animation, keeping it smooth on low-spec hardware.
Restart now drops every existing card before resetting so the
fresh deal looks like a new game, not a continuation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the WASM module from the previous commit into a minimal web
viewer served at <server>/replays/<id>. Two new server routes:
- `GET /replays/:id` — returns the same embedded HTML page for any
id; the page itself reads the path from window.location in JS and
fetches the replay JSON via /api/replays/:id.
- `/web/*` — ServeDir for the static assets (replay.css, replay.js,
and the wasm-bindgen-generated pkg/).
Web layer:
- index.html — header, board, controls, status. Module script.
- replay.css — midnight-purple palette matching the desktop client,
dark felt board, CSS-grid pile layout, tableau fan via per-card
inline `top` offset.
- replay.js — fetches the replay, instantiates the wasm
ReplayPlayer, drives state(), step(). Controls: Restart, Play/Pause
toggle, Step. Auto-tick at 600 ms.
- pkg/ — generated by wasm-bindgen (committed so deployers don't
have to install wasm-bindgen-cli + the wasm32 target).
`tower-http = "0.6"` added to solitaire_server with the `fs` feature
for ServeDir.
To regenerate pkg/ after a solitaire_wasm change:
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' \
cargo build -p solitaire_wasm \
--target wasm32-unknown-unknown --release
wasm-bindgen --target web \
--out-dir solitaire_server/web/pkg --no-typescript \
target/wasm32-unknown-unknown/release/solitaire_wasm.wasm
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>