21 Commits

Author SHA1 Message Date
funman300 5559f32672 feat(web): smart-move on double-click and right-click
Build and Deploy / build-and-push (push) Successful in 3m55s
Double-clicking or right-clicking a face-up card now auto-places it to
the best valid pile (foundation preferred for single cards, tableau
otherwise). Right-click also suppresses the browser context menu.
Theme button re-render now calls game.state() instead of reusing snap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:13:00 -07:00
funman300 407cae2040 feat(auth): add /api/me endpoint, avatar upload, and profile picture support
Build and Deploy / build-and-push (push) Successful in 5m7s
- Add migration 005: nullable avatar_url column on users table
- Add GET /api/me: returns id, username, avatar_url from DB (fixes UUID-on-profile bug)
- Add PUT /api/me/avatar: accepts raw image bytes (≤1 MB, jpeg/png/webp/gif),
  writes to avatars/ dir, updates avatar_url in DB
- Serve /avatars via ServeDir so uploaded images are publicly accessible
- Update account.html: fetch username from /api/me instead of parsing JWT;
  add circular avatar display with initials fallback and click-to-upload
- Add SolitaireServerClient::fetch_me() for desktop/Android profile display
- Add avatar_url field to SyncBackend::SolitaireServer settings (serde default None)
- Update sqlx offline query cache for new avatar_url queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:14:42 -07:00
funman300 8d31a37a39 feat(web): add classic/dark card theme picker
Build and Deploy / build-and-push (push) Successful in 4m10s
- 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>
2026-05-14 16:55:43 -07:00
funman300 72dfd741c4 fix(web): add Matomo tracking snippet to all pages
Build and Deploy / build-and-push (push) Successful in 4m10s
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>
2026-05-13 22:30:08 -07:00
funman300 1b7c4d92aa fix(web): auto-complete now works with cards remaining in waste
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>
2026-05-13 21:30:46 -07:00
funman300 539779d78b feat(analytics): replace custom pipeline with Matomo
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>
2026-05-13 21:10:15 -07:00
funman300 38eefb22e8 fix(server): XSS, missing score submission, leaderboard never updated, no LIMIT
- 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>
2026-05-13 19:32:14 -07:00
funman300 c40817d845 fix(web): preload card images to prevent white-flash on flip
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>
2026-05-13 17:17:33 -07:00
funman300 5b3925a619 feat(web): account page with sign in / sign up tabs
- 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>
2026-05-13 17:09:57 -07:00
funman300 8325bf6cf7 chore: rename app from Solitaire Quest to Ferrous Solitaire
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>
2026-05-13 17:04:45 -07:00
funman300 c518255a2d feat(web): leaderboard and replays pages with nav from landing
- 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>
2026-05-13 16:50:54 -07:00
funman300 b82573e7b1 feat(web): add home arrow link to game page header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:38:58 -07:00
funman300 b47462bd27 fix(web): apply Terminal palette and UX fixes to game page
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>
2026-05-13 16:26:51 -07:00
funman300 feb581005c fix(web): align replay and landing page to Terminal (base16-eighties) palette
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>
2026-05-13 16:23:16 -07:00
funman300 00f2d890f1 feat(web): add landing page at / with links to play, leaderboard, replays
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:21:38 -07:00
funman300 079349dc0f fix(web): explicit top/left on .slot and .recycle-label
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>
2026-05-13 11:20:56 -07:00
funman300 8f82b9fcb5 fix(web): sticky header, correct bottom-corner suit glyphs, main min-width
- 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>
2026-05-13 11:06:15 -07:00
funman300 0ebe87a411 fix(web): browser game UX pass — shake feedback, timer, stock count, HUD
- game.js fully rewritten: correct coordinate system (PAD baked into
  PILE_ORIGIN), undo driven by undo_stack_len, flashIllegal shake with
  --card-tx CSS variable, game timer, stock count HUD, URL seed persist,
  foundation suit hints, auto-complete step loop
- game.html: adds hud-timer, hud-stock, win-time elements
- game.css: @keyframes illegal-shake, .slot-hint, overflow-x on main
- solitaire_wasm: adds undo_stack_len to GameSnapshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 10:27:05 -07:00
funman300 1e6d153cd0 feat(wasm): playable browser game at /play
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>
2026-05-13 09:42:56 -07:00
funman300 1fcd032b0a feat(web): card flight animations between piles
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>
2026-05-05 19:01:02 +00:00
funman300 07b8ecd9b2 feat(server): web replay viewer (HTML/CSS + WASM bindings)
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>
2026-05-05 18:54:01 +00:00