Elevates the physical-device smoke test as the single remaining task for the v0.40.0 release and clarifies the Matomo validation is an independent task, not a release blocker. 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. Physical-device smoke test — THE ONLY REMAINING v0.40.0 ITEM
This is the single outstanding task for the v0.40.0 Android release. Everything
else is done and verified: workspace gates, aarch64-linux-android cross-compile +
clippy, release manifest sanity, a full local signed APK, the published release, and
Obtainium-facing delivery (public releases API, latest non-draft release, APK
downloadable anonymously). The only thing that cannot be done without hardware is
running the app on a real phone.
Install the published 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.
The signed release APK is 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 (independent — NOT a v0.40.0 release blocker)
Separate, ongoing task unrelated to the Android release. 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.