Updates SESSION_HANDOFF.md with the v0.40.0 Android release (PRs #105/#106/#108), the pre-release validation performed, and the still-open physical-device smoke test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Ferrous Solitaire — Session Handoff
Last updated: 2026-06-25 — v0.40.0 released (Android APK published); physical-device gate remains.
Current state
- Branch state:
masterpushed to origin; latest commits are the Draw-Three waste fan fix, its regression tests, and the NDK doc update (PRs #105–#108). - Latest tag:
v0.40.0(released — signed arm64-v8a APK published to the Gitea release for Obtainium/sideload).v0.39.1was the prior published release. - Working tree: clean. Local
scripts/helpers (incl.scripts/watch_deploy.sh) are intentionally not committed. - Latest verification this session:
cargo clippy --workspace --all-targets -- -D warnings;cargo test --workspace;cargo build -p solitaire_app; Android cross-compile + clippy foraarch64-linux-android(clean); full local signed arm64-v8a APK viascripts/build_android_apk.sh; CIandroid-releaseforv0.40.0completed/success with APK download HTTP 200. - Full previous gate: card_game work pushed to origin with
cargo test/clippygates passing.
v0.40.0 release (2026-06-25)
Released via tag push → .gitea/workflows/android-release.yml built and signed the
arm64-v8a release APK (release keystore, versionCode 4000 / versionName 0.40.0,
29.2 MB) and published it to the Gitea release. Obtainium clients tracking the repo
pick it up automatically.
| PR | Summary |
|---|---|
| #106 | fix(engine): Draw-Three waste fan hit-test now shares the renderer's fan step (card_plugin::waste_fan_step / tableau_col_step). The two had diverged under Android's tighter column spacing (H_GAP_DIVISOR=32), shifting the top fanned waste card's click target onto the card beneath it — so dragging the visible top card played the wrong one. Desktop/web were unaffected (the formulas already coincided there). |
| #105 | test(engine): waste-card draggability regression tests (find_draggable_at picks the waste top with multiple cards and as a lone card). |
| #108 | docs(android): NDK reference updated 26.3.11579264 → 30.0.14904198; noted versions are not load-bearing and build_android_apk.sh auto-discovers the newest NDK/build-tools. |
Pre-release validation performed locally this session: workspace clippy/test/build
gates; aarch64-linux-android cross-compile + clippy clean (covers the
#[cfg(target_os = "android")] paths that host CI never lints); release manifest
sanity (solitaire_app/android/AndroidManifest.xml has no version fields so CI
injection works; lib_name matches [lib].name); and a full signed local APK
proving the build_android_apk.sh packaging pipeline end-to-end.
What shipped since v0.39.0
- Browser Bevy canvas route and
window.__FERROUS_DEBUG__automation bridge landed, with Playwright coverage for/play. - In-place
card_game/klondikerewrite phases are complete through the latest follow-up:5e87358integrates upstream deps cleanly.ae1ecc8unifiesSuit/Rankwith upstreamcard_gametypes.d864d98routes klondike/card imports throughsolitaire_core.9bcf13d,56e3b62,26f1b00finish schema-v3 migration coverage, undo/recycle score correctness, and rewrite-plan docs.
- Android keystore wiring, Android build-script hardening, server auth/runtime hardening, and modal safe-area centering have landed.
CHANGELOG.mdhas been caught up fromv0.34.0through current unreleased work and committed in7fe6ac6.- Matomo analytics was re-reviewed:
MatomoClientandAnalyticsPluginare wired throughCoreGamePluginon non-wasm targets, and targeted tests now cover opt-in client creation, event encoding, buffer trimming, and analytics mode labels. - Native analytics and Android physical-device validation now have runbooks in
docs/analytics-validation.mdanddocs/ANDROID.md.
Historical notes before v0.39.0
See git log and CHANGELOG.md. The changelog now includes v0.34.0 through v0.39.0, plus current unreleased work.
What shipped since the last handoff (v0.23.0 → v0.35.1)
v0.34.0 — Android polish + code-quality sweep (2026-05-16/17)
| Commit | Summary |
|---|---|
9623bde |
Wire FiraMono to Android corner label; CardImageSet load tests |
980312c |
Fix wrong bottom-right suit symbol on JS/QS/KS card assets |
04e99a8 |
Correct Android waste fan overlap and resume layout desync |
3bb3ddb |
Eliminate panics, fix dismiss hit-test scope, guard home respawn |
f8f1f26 |
Adaptive drop zones, touch event correctness, modal lifecycle guards |
1eb4043 |
Auth-guard avatar serving; atomic write; user_id assertion in merge |
69c6e88 |
Deterministic pile serialization, undo skip, url-encode bytes, merge_at |
aa7b0f6 |
Gate frame-hot ECS systems on resource changes (perf) |
6727126 |
Consolidate APP_DIR_NAME; add #[must_use] on pure fns |
a4dfb0c |
Differentiate leaderboard opt-in vs opt-out error toasts (M-12) |
7fc98f8 |
WASM: state() and step() return Result, errors throw JS exceptions (CR-6) |
ffed6b2 |
Share Tokio runtime across all network tasks (M-16) |
fa84152 |
Correct Android help hint label → to ! (M-17) |
18d7937 |
Derive Copy for DrawMode; drop redundant .clone() calls (M-18) |
132fea9 |
Use saturating_add for move_count increments (M-19) |
0ecc1a9 |
Add missing derives to AchievementContext (M-20) |
2301cc6 |
Align android_keystore temp extension with cleanup glob (M-21) |
2e52f54 |
Enforce 32-char display_name limit at sync client boundary (M-22) |
c8878d6 |
Fix stale FOCUS_RING colour comment (M-23) |
4aafc0a |
Name HUD popover Z-layers; replace raw Z arithmetic (M-24) |
v0.35.0 — Accessibility + sync reliability (2026-05-18)
| Commit | Summary |
|---|---|
eb6c93f |
Silence B0004 by adding Transform to ModalScrim |
6f5cebd |
Fire WarningToastEvent on sync pull failure (was InfoToastEvent) |
87aec5b |
Gate all decorative motion animations under reduce_motion_mode |
reduce_motion_mode now gates: score pulse, score floater, streak flourish
(hud_plugin), card-shake on rejected move, foundation completion flourish
(feedback_anim_plugin). Pattern: gate at the trigger/start system, never at
the tick system — if the component isn't inserted, the tick path never runs.
v0.35.1 — Leaderboard bug fixes (2026-05-18)
| Commit | Summary |
|---|---|
8f86d66 |
Fix three leaderboard bugs: wrong toast type, stale label, name not synced |
Three bugs fixed:
-
Wrong toast type on error —
poll_opt_in_task/poll_opt_out_taskerror branches now fireWarningToastEventinstead ofInfoToastEvent. -
Display name not pushed to server on change —
Settingsgainsleaderboard_opted_in: bool(serde-defaultedfalse). Settrue/falsewhen opt-in/out tasks succeed and persisted tosettings.json.handle_display_name_confirmnow spawns anopt_in_leaderboardtask when already opted in — the server's upsert endpoint updates onlydisplay_namewithout re-opting-in. -
"Public name" label stale after name change —
LeaderboardPublicNameTextmarker component added to the label node.update_leaderboard_public_name_labelsystem rewrites the text each frame the panel is open; O(0) cost when panel is closed.
5 new regression tests cover all three bugs.
Open punch list
1. Android APK launch verification (Option A)
Physical device test: install the latest APK on a real Android device (not AVD),
and run the checklist in docs/ANDROID.md §4. This has never been gated in CI.
AVD adb shell input tap doesn't deliver real touch events, so physical-device
smoke testing is the only gate.
Still open after the v0.40.0 release. The signed release APK is now published
(grab it from the release page, or use the local target/debug/apk/ferrous-solitaire.apk).
When testing, specifically exercise the Draw-Three waste fan fixed in #106: switch
to Draw-Three, draw several cards, and confirm dragging the visible top waste card
plays that card, not the one beneath it.
Latest AVD smoke (2026-06-08 local / 2026-06-09 UTC): built
target/debug/apk/ferrous-solitaire.apk for x86_64-linux-android, installed
it on AVD Pixel_7, launched android.app.NativeActivity, confirmed Bevy
rendered the board, safe-area insets resolved as top=136 bottom=63 left=0 right=0 after 2 frames, onboarding could be dismissed via AVD input, and
filtered logcat showed no Ferrous panic/fatal/ANR.
2. Matomo analytics live validation
Settings has analytics_enabled, matomo_url, and matomo_site_id; the engine
consumes them via AnalyticsPlugin on non-wasm targets. Remaining work is live
validation against the deployed Matomo instance. Use
docs/analytics-validation.md for the native validation checklist and the
current web/WASM decision notes.
Architectural notes for next session
-
Reduce-motion pattern: always gate in the
start_*/detect_*system (the trigger), not thetick_*system. If the component is never inserted, the tick path never runs. Seehud_plugin.rs::detect_score_changeandfeedback_anim_plugin.rs::start_shake_animfor the canonical pattern. -
Leaderboard server upsert:
POST /api/leaderboard/opt-inis idempotent — calling it when already opted in just updatesdisplay_name. Safe to call fromhandle_display_name_confirmwithout tracking a separate "needs update" flag. -
Messages<T>API (Bevy 0.18.1): write withresource_mut::<Messages<T>>().write(value); read in tests withmsgs.get_cursor()+cursor.read(msgs).next(). -
Test input-state pitfall:
MinimalPluginshas no input-tick system, soButtonInput::just_pressedstate persists across frames unless explicitly cleared withinput.release(key); input.clear()between updates. -
/playdebug bridge design:play.htmlruns two independent WASM instances inPromise.all([bootstrap(), init()]).bootstrap()setswindow.__FERROUS_DEBUG__(logic layer viasolitaire_wasm.js);init()starts the Bevy canvas. The bridge operates its ownSolitaireGame— moves applied through the bridge do NOT affect the Bevy visual game. This is intentional for automation/invariant checking. -
HiDPI Bevy canvas:
WindowResolution::default().with_scale_factor_override(1.0)is set in the canvas app. Without this, physical pixels exceed WebGL2's 2048px limit on HiDPI displays, causing an immediate wgpu panic on the first resize event. -
/play-classicvs/playin e2e:smoke.spec.js+gameplay_review.spec.jstarget/play-classic(DOM-heavy game.html);play_canvas.spec.jstargets/playusing only the__FERROUS_DEBUG__bridge (no DOM selectors).cycle_metrics.jssupports both via--route play-classic|play.