Compare commits

...

113 Commits

Author SHA1 Message Date
funman300 51fecb24b0 chore: remove stale docs, mockups, and one-off artifacts
Build and Deploy / build-and-push (push) Failing after 43s
Delete CLAUDE_PROMPT_PACK/SPEC/WORKFLOW (superseded by CLAUDE.md),
SESSION_HANDOFF files, old android investigation notes, phase-plan docs
under docs/superpowers/, and all docs/ui-mockups/ (HTML+PNG mockups
from the pre-Terminal design pass). Also removes local artifacts:
analytics_impl_prompt.md, review_project.py, delete_runs.sh, ruvector.db
files, and the stray solitaire_wasm/solitaire_server/ build artefact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:15:11 -07:00
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
Gitea CI 17c320c08f chore(deploy): bump image to c35c045f [skip ci] 2026-05-15 01:02:19 +00:00
funman300 c35c045f08 fix(core): enable take-from-foundation by default (closes #3)
Build and Deploy / build-and-push (push) Successful in 4m55s
Standard Klondike allows returning the top card of a foundation pile to a
compatible tableau column. The flag was previously off by default, making the
move impossible for all players.

Changes:
- GameState::new_with_mode: take_from_foundation now initialises to true
- Settings: default changed to true; custom serde default function ensures
  older settings.json files without the key also resolve to true
- Tests: rename "blocked_by_default" → "allowed_by_default" (asserts the
  move now succeeds); add "blocked_when_disabled" to cover the flag=false path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:57:25 -07:00
Gitea CI 8fe0891866 chore(deploy): bump image to 677999a5 [skip ci] 2026-05-15 00:31:41 +00:00
funman300 677999a51e feat(engine): wire avatar download and display into profile modal
Build and Deploy / build-and-push (push) Successful in 4m15s
- Add avatar_plugin: AvatarPlugin, AvatarResource, AvatarFetchEvent
  - After AvatarFetchEvent fires, spawns an async reqwest download task
  - On completion, decodes image bytes via image::load_from_memory →
    Image::from_dynamic and inserts into Assets<Image>
- Expand auth task to also call fetch_me_with_token immediately after
  login/register so avatar_url is available without a second round-trip
- poll_auth_task fires AvatarFetchEvent when avatar_url is Some, building
  the full URL from base_url + relative avatar path
- Profile modal shows 48px circular avatar ImageNode when AvatarResource
  is populated, or an initials disc (first letter of username) as fallback
- Add image = "0.25" and reqwest to solitaire_engine deps
- Add fetch_me_with_token helper to SolitaireServerClient for use when
  the access token hasn't been persisted to keychain yet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:27:27 -07:00
Gitea CI 7177f0eb1b chore(deploy): bump image to 407cae20 [skip ci] 2026-05-15 00:19:45 +00: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
Gitea CI eb906fe968 chore(deploy): bump image to 8d31a37a [skip ci] 2026-05-14 23:59:49 +00: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
Gitea CI 2bf990388b chore(deploy): bump image to 7238ef22 [skip ci] 2026-05-14 23:51:51 +00:00
funman300 7238ef225e feat(web): replace dark-theme card PNGs with classic (white) theme
Build and Deploy / build-and-push (push) Successful in 26s
Rasterized all 52 classic SVGs via rsvg-convert at 256×384. The web
game was showing dark-background cards; it now shows the traditional
white card face style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:51:27 -07:00
funman300 b984161d46 ci: remove android build and release workflows
Build and Deploy / build-and-push (push) Has been cancelled
Building locally or via a different pipeline; the self-hosted runner's
resource constraints made the 3-ABI release build unreliable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:41:01 -07:00
funman300 c69d732d5a ci(android): add disk-space diagnostic step before release build
Android Build / build-apk (push) Has been cancelled
Build and Deploy / build-and-push (push) Has been cancelled
Helps diagnose whether the 3-ABI release build is hitting disk limits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:39:24 -07:00
Gitea CI 5e0e8d000b chore(deploy): bump image to 27eed989 [skip ci] 2026-05-14 23:21:28 +00:00
funman300 27eed98922 ci(android): remove workflow_dispatch — unsupported in this Gitea version
Android Build / build-apk (push) Successful in 12m3s
Build and Deploy / build-and-push (push) Successful in 41s
Android Release / build-release-apk (push) Has been cancelled
workflow_dispatch caused the entire android-release workflow to be silently
dropped; tag-triggered releases (v0.25.6) worked fine before this trigger
was added. Removing it restores tag-based release triggering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:08:40 -07:00
funman300 f304917d62 ci(android): add workflow_dispatch trigger to release workflow
Android Build / build-apk (push) Successful in 13m48s
Build and Deploy / build-and-push (push) Failing after 53s
Android Release / build-release-apk (push) Has been cancelled
Allows manually triggering the release build from the Gitea UI
(Actions tab → Android Release → Run workflow) without needing
to push a version tag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:44:24 -07:00
funman300 d49c478efa fix(ci): diagnose KEYSTORE_BASE64 decode failure
Android Build / build-apk (push) Has been cancelled
Build and Deploy / build-and-push (push) Has been cancelled
Android Release / build-release-apk (push) Has been cancelled
base64 -d is failing (1s step, keystore 0 bytes). Add diagnostics:
- Print secret length to check for truncation
- Capture base64 error output
- Print exit code and file size
- Use --decode instead of -d for clarity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:37:17 -07:00
Gitea CI 29f9b9358e chore(deploy): bump image to 9ef5759f [skip ci] 2026-05-14 22:27:55 +00:00
funman300 9ef5759f40 fix(ci): fail fast on empty keystore before 7-min cargo build
Android Build / build-apk (push) Successful in 14m52s
Build and Deploy / build-and-push (push) Successful in 44s
Android Release / build-release-apk (push) Failing after 3m42s
If KEYSTORE_BASE64 is unset, base64 -d writes an empty file silently,
cargo ndk then spends ~7 min compiling all ABIs, and only then does
apksigner fail. Add a size check after decode so the job fails in
seconds with a clear error message instead of wasting a full build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:12:25 -07:00
funman300 9c9c0c76d3 fix(ci): restore 3-ABI release build now that LXC has 106 GB disk
Android Build / build-apk (push) Successful in 20m40s
Build and Deploy / build-and-push (push) Failing after 50s
Android Release / build-release-apk (push) Failing after 10m29s
The runner LXC was bumped from ~56 GB to 106 GB, giving ~70 GB of free
space — well above the ~40 GB a full 3-ABI release build needs. Revert
the disk-budget workarounds added in ab35fcf:

- Remove "Free disk space" step (no longer needed)
- Restore x86_64 target (arm64-v8a + armeabi-v7a + x86_64)
- Remove ABIS override so build script uses its full default set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 14:04:09 -07:00
Gitea CI d4fb9e36a8 chore(deploy): bump image to 32991301 [skip ci] 2026-05-14 21:00:48 +00:00
funman300 ab35fcf906 fix(ci): free disk space + drop x86_64 from release build to fix OOM
Android Build / build-apk (push) Successful in 16m44s
Build and Deploy / build-and-push (push) Failing after 30s
Android Release / build-release-apk (push) Failing after 12m5s
Run 181 (v0.25.0 tag) failed at "Build signed release APK" after ~7 min —
same disk-exhaustion pattern that hit the debug build. The debug workflow
was already fixed to arm64-v8a only; the release workflow still built all 3
ABIs and exceeded the runner's disk budget.

Changes:
- Add "Free disk space" step before system deps: removes /usr/local/lib/android,
  /usr/share/dotnet, /opt/ghc, /usr/local/share/boost (~10 GB reclaimed).
- Limit ABIS to arm64-v8a + armeabi-v7a (drops x86_64, which is emulator-only).
- Remove x86_64 from rustup target add to match.

arm64-v8a covers all modern Android devices; armeabi-v7a covers legacy ARM.
x86_64 can be re-added later if a simulator-targeted test build is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 13:53:48 -07:00
funman300 32991301dd fix(engine): restore Dark as default theme; migrate stale theme IDs
Android Build / build-apk (push) Successful in 12m19s
Build and Deploy / build-and-push (push) Successful in 55s
- default_theme_id() returns "dark" (was briefly "classic" after the
  rename commit 20b7a61)
- sanitized() migrates "default" and "classic" → "dark" so existing
  settings.json files are upgraded automatically on next launch
- Registry lists Dark first so the Settings picker opens with it at top
- Classic remains available as an option in the picker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 13:47:43 -07:00
Gitea CI c5fd928dcb chore(deploy): bump image to f6907671 [skip ci] 2026-05-14 20:20:49 +00:00
funman300 f6907671be fix(ci): pin upload-artifact to v3 for Gitea Actions compatibility
Android Build / build-apk (push) Successful in 12m49s
Build and Deploy / build-and-push (push) Successful in 47s
Android Release / build-release-apk (push) Failing after 10m11s
The disk-budget fix worked — debug APK now builds, signs, and verifies
in ~6 minutes on a single ABI. But the upload step failed with:

  GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
  and download-artifact@v4+ are not currently supported on GHES.

upload-artifact@v4 rewrote the upload path to use a new artifact
service hosted on github.com; Gitea's GHES-compatibility layer doesn't
implement that endpoint. v3 still uses the older chunked HTTP upload
API that Gitea supports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:07:09 -07:00
Gitea CI a54fff7257 chore(deploy): bump image to 533bcec2 [skip ci] 2026-05-14 20:01:12 +00:00
funman300 533bcec2d8 fix(ci): limit debug APK to arm64-v8a so apksigner has disk to write
Android Build / build-apk (push) Failing after 9m10s
Build and Deploy / build-and-push (push) Successful in 52s
The previous run got all the way through compile + link + zipalign and
then died inside apksigner with `IOException: No space left on device`.
Cross-compiling all three Android ABIs (arm64-v8a, armeabi-v7a, x86_64)
in debug mode blows target/ past 25 GB, and by the time apksigner is
streaming the signed APK to disk the runner has nothing left.

Two changes:

  1. build_android_apk.sh now reads `ABIS` from the environment (defaults
     to all three for backwards compat) and uses it to assemble the
     cargo-ndk `-t` flags.
  2. android-build.yml passes ABIS=arm64-v8a, since the debug artifact
     is consumed by adb-installing to a single arm64 device and the
     other two ABIs were dead weight.

Also frees \$STAGING/app-unsigned.apk right after zipalign so it's not
sitting next to the aligned APK and the output APK during signing.

Release workflow is untouched — release APKs still ship all three ABIs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:51:17 -07:00
Gitea CI ba786f5a09 chore(deploy): bump image to 7ee7cb6d [skip ci] 2026-05-14 19:17:40 +00:00
funman300 7ee7cb6d93 ci(android): replace cargo-apk with cargo-ndk + manual APK assembly
Android Build / build-apk (push) Failing after 23m0s
Build and Deploy / build-and-push (push) Successful in 43s
cargo-apk 0.10 and its fork cargo-apk2 both failed to discover the
installed Android platform in this Gitea runner, despite ANDROID_HOME,
platforms;android-34, build-tools, and NDK all being present, readable,
and pointed at correctly. We never isolated whether the bug is in the
shared ndk-build crate's discovery logic or in the runner's env-var
propagation through cargo subcommand exec, so this commit stops fighting
either tool and assembles the APK from explicit toolchain steps instead:

  cargo ndk          -> per-ABI .so files
  aapt2 compile/link -> manifest + resources -> base APK
  zip                -> bundle native libs into lib/<abi>/
  zipalign           -> 4-byte alignment
  apksigner          -> v2/v3 signing (debug keystore for CI, real for release)

The pipeline lives in scripts/build_android_apk.sh so it's reproducible
locally (same env vars, same commands). AndroidManifest.xml is now
checked in under solitaire_app/android/ and mirrors what cargo-apk would
have generated from [package.metadata.android] — keep them in sync if
either is changed. Local `cargo apk build` still works on developer
machines where cargo-apk is happy; CI just stops depending on it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:53:55 -07:00
funman300 14324b09ef ci(android): switch from cargo-apk 0.10.0 to cargo-apk2
Android Build / build-apk (push) Failing after 3m49s
Build and Deploy / build-and-push (push) Failing after 23s
cargo-apk 0.10.0 has been unable to discover an installed Android
platform in this runner environment despite ANDROID_HOME, NDK,
build-tools, and platforms;android-34 all being present and readable.
cargo-apk2 is the maintained community fork on crates.io that reads
the same `[package.metadata.android]` block, so the solitaire_app
Cargo.toml needs no changes. Cache keys updated to apk2- so we don't
restore the broken cargo-apk binary from prior runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:43:50 -07:00
Gitea CI 124f1f5cf5 chore(deploy): bump image to a6a73b5f [skip ci] 2026-05-14 18:38:04 +00:00
funman300 a6a73b5f36 fix(ci): add permission and env diagnostics to build step
Android Build / build-apk (push) Failing after 3m47s
Build and Deploy / build-and-push (push) Successful in 36s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:33:54 -07:00
Gitea CI b84fe79806 chore(deploy): bump image to 3248f00d [skip ci] 2026-05-14 18:32:22 +00:00
funman300 3248f00d66 fix(ci): deeper SDK verification — find android.jar actual location
Android Build / build-apk (push) Failing after 3m41s
Build and Deploy / build-and-push (push) Successful in 25s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:17 -07:00
Gitea CI c680a043ae chore(deploy): bump image to d0ab7ed9 [skip ci] 2026-05-14 18:26:23 +00:00
funman300 d0ab7ed97b fix(ci): add SDK verification step to diagnose platforms-not-found
Android Build / build-apk (push) Failing after 3m28s
Build and Deploy / build-and-push (push) Successful in 39s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:22:11 -07:00
Gitea CI 1144a96757 chore(deploy): bump image to eba1f66b [skip ci] 2026-05-14 18:18:13 +00:00
funman300 ac6668cee7 fix(ci): apply template-expansion pattern to release workflow
Android Build / build-apk (push) Failing after 2m59s
Build and Deploy / build-and-push (push) Failing after 46s
Mirror the fix from android-build.yml: rename ANDROID_HOME -> ANDROID_SDK
in the env block to avoid the Docker-image-baked ANDROID_HOME overriding
the workflow value in run scripts. Use ${{ env.ANDROID_SDK }} template
expressions throughout, and explicitly export ANDROID_HOME/ANDROID_NDK_HOME
before cargo-apk build so it finds the SDK at the right path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:17:58 -07:00
funman300 eba1f66b45 fix(ci): use template-expanded paths in run scripts to bypass Docker ENV
Android Build / build-apk (push) Failing after 3m6s
Build and Deploy / build-and-push (push) Successful in 31s
Replace shell variable $ANDROID_HOME references in run blocks with
${{ env.ANDROID_SDK }} template expressions. Gitea runner v1 may not
override Docker-image-baked ENV vars via docker exec; template expansion
happens at workflow compilation time, so the literal path is hardcoded
into the script before the shell runs it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:14:34 -07:00
Gitea CI 90959728b1 chore(deploy): bump image to 8b30f877 [skip ci] 2026-05-14 18:11:34 +00:00
funman300 8b30f8778b fix(ci): use fresh /opt/android-sdk path to avoid container ENV conflict
Android Build / build-apk (push) Failing after 3m9s
Build and Deploy / build-and-push (push) Successful in 50s
Remove SDK detection logic and install directly to /opt/android-sdk,
matching the release workflow. The container Docker image has ANDROID_HOME
baked in at /usr/local/lib/android/sdk; installing there with sudo while
cargo-apk resolves ANDROID_HOME from the image ENV created a divergence.
Using a controlled path we own eliminates that class of conflict entirely.
Add SDK cache shared with the release workflow (same key prefix v2-).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:07:46 -07:00
Gitea CI d6a7924f14 chore(deploy): bump image to 4db43fb3 [skip ci] 2026-05-14 18:02:16 +00:00
funman300 4db43fb3fb fix(ci): replace ANDROID_SDK_ROOT with ANDROID_HOME in release workflow
Android Build / build-apk (push) Failing after 3m49s
Build and Deploy / build-and-push (push) Successful in 54s
ANDROID_SDK_ROOT was never set; zipalign and apksigner were resolving
to empty paths and would fail. All three occurrences replaced with
ANDROID_HOME which is defined in the workflow env block.

Also adds sudo to the cache-miss SDK install (mkdir/mv/sdkmanager) to
match the debug workflow pattern — /opt/android-sdk requires root on
a fresh runner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:56:42 -07:00
funman300 01d6b27e61 fix(ci): detect existing container SDK before installing, set ANDROID_HOME via GITHUB_ENV
Android Build / build-apk (push) Failing after 3m50s
Build and Deploy / build-and-push (push) Failing after 33s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:53:14 -07:00
funman300 3cffbc2c51 feat(engine): embed classic theme into binary like dark theme
Classic SVGs and manifest are now compiled in via include_bytes!(),
making the theme available on all platforms (desktop, Android) without
requiring filesystem assets. Removes the now-redundant Dockerfile COPY
of solitaire_engine/assets/themes/classic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:53:14 -07:00
Gitea CI 2ef25934ac chore(deploy): bump image to bb670d6c [skip ci] 2026-05-14 17:51:40 +00:00
funman300 bb670d6cc6 fix(ci): drop ANDROID_SDK_ROOT, pass --sdk_root to sdkmanager explicitly
Android Build / build-apk (push) Failing after 3m19s
Build and Deploy / build-and-push (push) Successful in 30s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:47:52 -07:00
Gitea CI 76911c57c9 chore(deploy): bump image to 8391235a [skip ci] 2026-05-14 17:45:46 +00:00
funman300 8391235a1a fix(ci): check android.jar existence in platform dir
Android Build / build-apk (push) Failing after 3m25s
Build and Deploy / build-and-push (push) Successful in 30s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:41:57 -07:00
funman300 2f3a6b9586 fix(ci): dump env at build time to diagnose ANDROID_HOME visibility
Android Build / build-apk (push) Failing after 3m23s
Build and Deploy / build-and-push (push) Failing after 40s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:37:37 -07:00
Gitea CI 4d20b70809 chore(deploy): bump image to bfadcf0e [skip ci] 2026-05-14 17:32:04 +00:00
funman300 bfadcf0e0d fix(ci): add SDK layout debug step to diagnose platforms-not-found error
Android Build / build-apk (push) Failing after 3m19s
Build and Deploy / build-and-push (push) Successful in 42s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:28:10 -07:00
Gitea CI 356dbebe57 chore(deploy): bump image to c90c7831 [skip ci] 2026-05-14 17:28:04 +00:00
funman300 c90c783177 fix(ci): set ANDROID_HOME/NDK_HOME in workflow env block instead of GITHUB_ENV
Android Build / build-apk (push) Failing after 3m23s
Build and Deploy / build-and-push (push) Successful in 40s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:23:58 -07:00
Gitea CI bbf4b2c14a chore(deploy): bump image to 1f46785b [skip ci] 2026-05-14 17:19:04 +00:00
funman300 62be72e918 fix(ci): bust SDK cache key to force fresh SDK install after prior broken cache
Android Build / build-apk (push) Failing after 3m16s
Build and Deploy / build-and-push (push) Failing after 39s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:18:57 -07:00
funman300 1f46785b31 fix(ci): add apt-get update before package install to fix exit code 100
Android Build / build-apk (push) Failing after 3m50s
Build and Deploy / build-and-push (push) Successful in 39s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:14:32 -07:00
Gitea CI 2e5d82f83c chore(deploy): bump image to 396ba6bc [skip ci] 2026-05-14 17:12:21 +00:00
funman300 396ba6bc97 fix(ci): always install Java regardless of SDK cache hit; harden release creation
Android Build / build-apk (push) Failing after 13s
Build and Deploy / build-and-push (push) Successful in 38s
Android Release / build-release-apk (push) Failing after 11s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:11:42 -07:00
Gitea CI 88298206bb chore(deploy): bump image to 0f650311 [skip ci] 2026-05-14 17:11:05 +00:00
funman300 0f65031114 ci: add Android release workflow — sign and publish APK on version tag
Android Build / build-apk (push) Failing after 11s
Build and Deploy / build-and-push (push) Successful in 23s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:10:27 -07:00
funman300 c91ce9436e fix(deploy): copy classic theme assets into Docker runtime image
Build and Deploy / build-and-push (push) Failing after 27s
solitaire_engine/assets/themes/classic/ was absent from the container
because only the workspace-root assets/ directory was copied. The
AssetServer serves themes/classic/ from that same root, so the classic
theme manifested as a missing-asset load failure at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 23:00:43 -07:00
Gitea CI ace96b4a47 chore(deploy): bump image to ea079af9 [skip ci] 2026-05-14 05:58:01 +00:00
funman300 ea079af9e1 ci: add Android APK build workflow
Android Build / build-apk (push) Failing after 59s
Build and Deploy / build-and-push (push) Successful in 26s
Triggers on every master push that touches app/engine/asset code
(ignores deploy/, argocd/, solitaire_server/, *.md).

Three-layer cache strategy:
  1. Android SDK + NDK keyed by NDK + build-tools versions (~2 GB, stable)
  2. cargo-apk binary keyed by OS + toolchain (avoids recompiling the tool)
  3. Cargo registry + build artifacts keyed by Cargo.lock + SHA

Outputs a debug APK as a workflow artifact retained for 30 days.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:56:34 -07:00
Gitea CI c66d81c73a chore(deploy): bump image to 20b7a617 [skip ci] 2026-05-14 05:53:08 +00:00
funman300 20b7a617e0 feat(engine): rename themes — Classic is default, Dark replaces Default
Build and Deploy / build-and-push (push) Successful in 33s
- Rename assets/themes/default/ → assets/themes/dark/; update theme.ron
  id/name to "dark"/"Dark"
- Rename all DEFAULT_THEME_* constants → DARK_THEME_* and
  default_theme_svg_bytes / populate_embedded_default_theme → dark_*
- Add bundled_theme_url() helper for URL resolution without needing the
  registry (used by Startup systems where ordering isn't guaranteed)
- Registry now lists Classic first (new player default), Dark second
- settings.rs default_theme_id() returns "classic" so fresh installs
  start on the white card theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:52:44 -07:00
funman300 7a0d57b2b1 feat(engine): add Classic card theme
White/cream card faces with traditional red (hearts/diamonds) and black
(clubs/spades) colours, plus a navy diamond-pattern card back. Shipped
as a bundled AssetServer theme alongside the existing Default theme.

Registry updated to include the Classic entry; registry tests updated
to reflect the new BUNDLED_COUNT of 2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:52:44 -07:00
Gitea CI 93ec4a7478 chore(deploy): bump image to 72dfd741 [skip ci] 2026-05-14 05:34:53 +00: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 3837a10b15 fix(deploy): use matomo.php for liveness/readiness probes
/index.php returns 302 after tables are created (installer redirect),
which fails k8s HTTP probes. /matomo.php is the tracker endpoint and
always returns 200 regardless of installation state. Also add
timeoutSeconds: 5 since PHP startup can exceed the 1s default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:03:07 -07:00
funman300 574115cb71 fix(deploy): switch matomo to official image 5.10.0
bitnami/matomo was removed from Docker Hub (0 tags). Switch to the
official matomo:5.10.0 image; update port 8080→80, volume path to
/var/www/html, and env var names to match the official image schema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:52:00 -07:00
Gitea CI 1707553790 chore(deploy): bump image to 6905f26b [skip ci] 2026-05-14 04:37:19 +00:00
funman300 6905f26b56 security: remove secrets from git, gitignore k8s secret files
Build and Deploy / build-and-push (push) Successful in 35s
Secrets committed in prior commits (matomo-secret.yaml,
secret-analytics-auth.yaml) have been scrubbed from history via
filter-branch — rotate those credentials immediately.

Going forward:
- deploy/*-secret.yaml is gitignored; apply manually with kubectl
- deploy/matomo-secret.yaml.example shows the required shape
- ArgoCD ignoreDifferences on matomo-secret prevents it pruning a
  manually-applied secret
- Remove matomo-secret.yaml from kustomization.yaml so ArgoCD never
  manages it again

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:36:46 -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
Gitea CI d685224ce6 chore(deploy): bump image to 3e006a1e [skip ci] 2026-05-14 04:14:55 +00: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 f6506c57e5 feat(deploy): Datasette analytics sidecar + analytics.aleshym.co ingress
Adds a Datasette container alongside the existing server in the same pod so
it can read the SQLite PVC without a second ReadWriteOnce mount. Protected
by a Traefik BasicAuth middleware at analytics.aleshym.co.

Also fixes the ArgoCD repoURL to point to the migrated Gitea hostname
(git.aleshym.co) instead of the old bare IP.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 20:17:20 -07:00
Gitea CI b88f3df119 chore(deploy): bump image to 3cec200a [skip ci] 2026-05-14 03:10:52 +00:00
funman300 0dcb783e94 feat(analytics): opt-in usage analytics with server ingest and settings toggle
- Server: POST /api/analytics endpoint with per-IP rate limit (5/min),
  batch validation (≤50 events, event_type regex, UUID dedup, clock check),
  INSERT OR IGNORE for idempotency, and migration 004_analytics.sql
- Client (solitaire_data): AnalyticsClient with in-memory Mutex buffer,
  UUID session_id per launch, async flush via background task
- Engine: AnalyticsPlugin records game_won, game_forfeit, game_start,
  achievement_unlocked; flushes immediately on game-end, every 60 s otherwise
- Settings UI: Privacy section with ON/OFF toggle, hidden in local-only mode
- Default: analytics_enabled = false (explicit opt-in required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 20:06:34 -07:00
Gitea CI ea17f94b6c chore(deploy): bump image to 09fcd209 [skip ci] 2026-05-14 02:43:38 +00:00
funman300 d60dc18add fix(server): add CSP/security headers middleware, gitignore jks.bak*
Content-Security-Policy, X-Content-Type-Options, and X-Frame-Options are
now injected by a single Axum middleware on the web router subtree, so
all HTML pages get consistent headers without touching each file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 19:41:50 -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
Gitea CI a579c25d5c chore(deploy): bump image to d5c95f9a [skip ci] 2026-05-14 00:21:16 +00: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
Gitea CI c6c03b8bff chore(deploy): bump image to b0478117 [skip ci] 2026-05-14 00:14:00 +00: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
Gitea CI 8485b3d1e0 chore(deploy): bump image to e6c67d03 [skip ci] 2026-05-14 00:09:08 +00: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
Gitea CI ea58f5dd64 chore(deploy): bump image to 4315c0ae [skip ci] 2026-05-13 23:54:33 +00: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
Gitea CI f5da9398f2 chore(deploy): bump image to 31d0a1b6 [skip ci] 2026-05-13 23:43:30 +00: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
Gitea CI 40818f5bd2 chore(deploy): bump image to 56dbc3ff [skip ci] 2026-05-13 23:37:19 +00:00
funman300 228ebbad8a fix(ci): rebase before kustomization push to handle concurrent runs
Two runs for the same SHA racing to push the kustomization update
caused the second to fail with "failed to push some refs".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:36:42 -07:00
Gitea CI 2b33feafc9 chore(deploy): bump image to 3e98872f [skip ci] 2026-05-13 23:33:23 +00:00
funman300 f8c8c9158e ci: add Docker BuildKit registry cache to speed up Rust builds
Caches compiled dependency layers in the Gitea registry under
:buildcache. Subsequent builds that only touch solitaire_server/src/
skip recompiling the full workspace dependency tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:28:10 -07:00
Gitea CI 9cc0837088 chore(deploy): bump image to 98f9933e [skip ci] 2026-05-13 23:28:10 +00: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
Gitea CI 08d22c822a chore(deploy): bump image to a6030f4b [skip ci] 2026-05-13 23:24:43 +00: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
Gitea CI 9533a7d420 chore(deploy): bump image to 022a749f [skip ci] 2026-05-13 22:45:42 +00:00
funman300 5ec5ac1a19 fix(server): create SQLite database file if missing on first start
SqlitePool::connect defaults create_if_missing=false in SQLx 0.8, causing
SQLITE_CANTOPEN (error 14) when the PVC is empty on first deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 15:44:22 -07:00
Gitea CI 86aea206b8 chore(deploy): bump image to 0c673e3b [skip ci] 2026-05-13 22:32:46 +00:00
funman300 1bd1c0f927 fix(docker): add libsqlite3-0 to runtime image to fix SQLite CANTOPEN error
The server binary dynamically links against libsqlite3.so.0, which is not
present in debian:bookworm-slim by default, causing SQLite error code 14
at startup when connecting to the database.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 15:32:09 -07:00
Gitea CI 7be7f4395c chore(deploy): bump image to 597aba20 [skip ci] 2026-05-13 15:04:01 -07:00
funman300 66c2907c25 fix(docker): rename binary to ./server to avoid collision with solitaire_server/web dir 2026-05-13 15:03:45 -07:00
funman300 c2811fa661 ci: trigger with dockerfile change for debug 2026-05-13 14:46:09 -07:00
funman300 933cc55ea9 fix(docker): copy web/ to builder stage for include_str! macros 2026-05-13 14:18:05 -07:00
funman300 58faae1911 fix(docker): stub all workspace crates for cargo fetch in CI 2026-05-13 14:15:24 -07:00
funman300 96be1b85fb ci: retrigger after fixing runner instance URL 2026-05-13 14:11:54 -07:00
funman300 bbf7709912 ci: retrigger build after enabling Actions 2026-05-13 14:05:23 -07:00
410 changed files with 4577 additions and 13026 deletions
+8
View File
@@ -36,6 +36,11 @@ jobs:
username: ${{ gitea.actor }}
password: ${{ secrets.CI_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v5
with:
@@ -45,6 +50,8 @@ jobs:
tags: |
${{ env.IMAGE }}:${{ steps.meta.outputs.sha }}
${{ env.IMAGE }}:latest
cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max
- name: Install kustomize
run: |
@@ -62,4 +69,5 @@ jobs:
git config user.name "Gitea CI"
git add deploy/kustomization.yaml
git commit -m "chore(deploy): bump image to ${{ steps.meta.outputs.sha }} [skip ci]" || true
git pull --rebase origin master
git push
+6
View File
@@ -14,4 +14,10 @@ data/
# Android signing keystores — never commit
*.jks
*.jks.bak
*.jks.bak*
*.keystore
# Kubernetes secrets — apply manually, never commit
deploy/matomo-secret.yaml
deploy/*-secret.yaml
deploy/*-auth-secret.yaml
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT username FROM users WHERE id = ?",
"describe": {
"columns": [
{
"name": "username",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "06c945e50567c6801f1346d436cdc86a82a4e13dd45d8286295ba37cdbdc045e"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE leaderboard\n SET best_score = ?,\n best_time_secs = ?,\n recorded_at = ?\n WHERE user_id = ?\n AND (\n best_score IS NULL\n OR ? > best_score\n OR (? = best_score AND (best_time_secs IS NULL OR ? < best_time_secs))\n )",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "0e199cafab7e71b0c7f28ede85a622e38649d2fe5a73a5c715f2319f5450f729"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE users SET avatar_url = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "15d08e02b102147bc74848ec3692b99edbf4c33078191f0132991ee94d4507b1"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT l.display_name, l.best_score, l.best_time_secs, l.recorded_at\n FROM leaderboard l\n JOIN users u ON u.id = l.user_id\n WHERE u.leaderboard_opt_in = 1\n ORDER BY\n CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,\n l.best_score DESC,\n CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,\n l.best_time_secs ASC",
"query": "SELECT l.display_name, l.best_score, l.best_time_secs, l.recorded_at\n FROM leaderboard l\n JOIN users u ON u.id = l.user_id\n WHERE u.leaderboard_opt_in = 1\n ORDER BY\n CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,\n l.best_score DESC,\n CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,\n l.best_time_secs ASC\n LIMIT 100",
"describe": {
"columns": [
{
@@ -34,5 +34,5 @@
false
]
},
"hash": "57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112"
"hash": "2b814989a6632ca930ae1e895f97a7fc3389c91d1d2abf6900a21fb0d6e94ef3"
}
@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "SELECT username, avatar_url FROM users WHERE id = ?",
"describe": {
"columns": [
{
"name": "username",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "avatar_url",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true
]
},
"hash": "fae33e7159b43c756131b1545360dcb0250988b43550881e3f0a336d9516dd45"
}
+2 -2
View File
@@ -1,4 +1,4 @@
# Solitaire Quest — Architecture Document
# Ferrous Solitaire — Architecture Document
> **Version:** 1.3
> **Language:** Rust (Edition 2024)
@@ -34,7 +34,7 @@
## 1. Project Overview
Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops. It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
Ferrous Solitaire is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops. It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
### Sync Backend by Platform
+1 -1
View File
@@ -1,6 +1,6 @@
# Changelog
All notable changes to Solitaire Quest are documented here. The format is
All notable changes to Ferrous Solitaire are documented here. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
project follows [Semantic Versioning](https://semver.org/).
-497
View File
@@ -1,497 +0,0 @@
# CLAUDE_PROMPT_PACK.md
version: 1.0
---
# 0. GLOBAL INSTRUCTION (prepend to every prompt)
```
You must follow CLAUDE_SPEC.md strictly.
Rules:
- Do not expand scope beyond what is defined
- Do not refactor unrelated code
- Do not introduce new dependencies to solitaire_core or solitaire_sync without confirmation
- Prefer minimal, surgical changes
- Use existing patterns in the codebase
- Return minimal diffs or changed functions only
Before writing code:
1. List relevant constraints from CLAUDE_SPEC.md
2. Identify risks
3. Then implement
```
---
# 1. FEATURE IMPLEMENTATION
```
# TASK: Feature Implementation
feature: "<name>"
goal:
"<clear outcome>"
scope:
crates: []
systems: []
files: []
non_goals:
- ""
constraints:
- must follow CLAUDE_SPEC.md
- event-driven architecture required
- no blocking operations
- no cross-crate leakage
acceptance_criteria:
- ""
- ""
edge_cases:
- ""
---
## Required Patterns
Use this pattern for systems:
<PASTE EXISTING SYSTEM SNIPPET HERE>
---
## Output Format
intent:
plan:
constraints_used:
risks:
code_changes:
(minimal diffs only)
notes:
```
---
# 2. BUGFIX
```
# TASK: Bug Fix
bug_description:
"<what is broken>"
expected_behavior:
"<correct behavior>"
root_cause_hint (optional):
""
scope:
crates: []
files: []
constraints:
- minimal fix only
- no refactors unless required
- must add regression protection if applicable
---
## Requirements
1. Identify root cause
2. Fix it minimally
3. Preserve all invariants
4. Do not change unrelated logic
---
## Output Format
analysis:
root_cause:
fix_strategy:
code_changes:
(minimal diff)
regression_test (only if high-value):
notes:
```
---
# 3. REFACTOR
```
# TASK: Refactor
target:
"<what is being improved>"
goal:
"<what improves>"
scope:
crates: []
files: []
non_goals:
- no behavior changes
- no new features
constraints:
- must preserve behavior exactly
- must respect crate boundaries
- must not duplicate logic
---
## Refactor Type
- [ ] simplify logic
- [ ] reduce duplication
- [ ] improve readability
- [ ] performance (non-invasive)
---
## Output Format
analysis:
issues_found:
refactor_plan:
code_changes:
(diff only)
verification:
- behavior unchanged: yes/no
- invariants preserved: yes/no
notes:
```
---
# 4. SYSTEM DESIGN (NEW FEATURE)
```
# TASK: System Design
feature:
"<name>"
goal:
"<what problem it solves>"
constraints:
- must fit existing architecture
- must follow plugin + event model
- must not violate crate boundaries
---
## Required Output
design:
components:
- plugins:
- systems:
- events:
- resources:
data_flow:
(step-by-step)
integration_points:
- where it connects to existing systems
risks:
- ""
tradeoffs:
- ""
---
## DO NOT
- write full implementation
- modify unrelated systems
```
---
# 5. NEW BEVY SYSTEM
```
# TASK: Add Bevy System
system_name:
""
trigger:
(event or condition)
reads:
[Resources]
writes:
[Resources]
emits:
[Events]
constraints:
- must be event-driven
- must not directly mutate unrelated state
- must be single responsibility
---
## Output Format
system_signature:
implementation:
(code only)
notes:
```
---
# 6. CORE LOGIC FUNCTION (solitaire_core)
```
# TASK: Core Logic Implementation
function:
"<name>"
goal:
"<what it does>"
rules:
- no IO
- no async
- no Bevy
- deterministic
invariants:
- ""
- ""
errors:
- ""
---
## Output Format
constraints_checked:
implementation:
(code only)
edge_case_handling:
notes:
```
---
# 7. SYNC / MERGE LOGIC
```
# TASK: Sync Logic
goal:
"<what is being merged or synced>"
constraints:
- must be deterministic
- must be idempotent
- must be lossless
- must not delete data
rules:
- counters → max
- times → min
- collections → union
---
## Output Format
analysis:
merge_logic:
code_changes:
invariants_verified:
- deterministic
- idempotent
- lossless
notes:
```
---
# 8. PERFORMANCE OPTIMIZATION
```
# TASK: Optimization
target:
"<what is slow>"
constraints:
- no behavior change
- no architecture change
- minimal code changes
---
## Output Format
analysis:
bottleneck:
optimization_strategy:
code_changes:
impact_estimate:
notes:
```
---
# 9. TEST GENERATION (STRICT MODE)
```
# TASK: Test Generation
target:
"<function/system>"
reason:
- bugfix | complex logic | invariant protection
constraints:
- no redundant tests
- must test real behavior
- must fail if logic breaks
---
## Output Format
test_cases:
- ""
test_code:
notes:
```
---
# 10. DEBUGGING / INVESTIGATION
```
# TASK: Debug
problem:
"<symptom>"
context:
"<relevant code or system>"
---
## Required Steps
1. List possible causes
2. Narrow down most likely
3. Suggest verification steps
4. Provide minimal fix
---
## Output Format
hypotheses:
most_likely:
verification_steps:
fix:
notes:
```
---
# 11. HARD CONSTRAINT OVERRIDE (RARE)
```
# TASK: Exception Handling
reason:
"<why constraints must be bent>"
requested_exception:
"<rule being broken>"
justification:
"<why unavoidable>"
---
## Output Format
analysis:
alternatives_considered:
final_decision:
risk:
```
---
# 12. STOP CONDITIONS (always append)
```
Stop when:
- acceptance criteria are met
- code is minimal and correct
Do NOT:
- expand scope
- refactor unrelated code
- optimize prematurely
```
---
# END
-296
View File
@@ -1,296 +0,0 @@
# CLAUDE_SPEC.md
version: 1.0
---
## 0. Global Rules
(Core determinism, panic policy, and event-driven engine constraints live in CLAUDE.md §2.1, §2.3, §3.1. Listed here only when they add information CLAUDE.md doesn't carry.)
rules:
* id: single_source_of_truth
description: "GameStateResource is the only mutable game state in runtime"
* id: sync_is_additive
description: "Remote data must never destructively overwrite local data"
---
## 1. Crate Graph
crates:
solitaire_core:
depends_on: [rand, serde, chrono]
forbidden_deps: [bevy, reqwest, tokio, std::fs]
solitaire_sync:
depends_on: [serde, serde_json, uuid, chrono]
role: "shared_types"
solitaire_data:
depends_on: [solitaire_core, solitaire_sync, reqwest, tokio, keyring]
role: "persistence_and_sync"
solitaire_engine:
depends_on: [bevy, kira, solitaire_core, solitaire_data]
role: "runtime_engine"
solitaire_server:
depends_on: [solitaire_sync, axum, sqlx, jsonwebtoken]
role: "backend"
solitaire_wasm:
depends_on: [solitaire_core, wasm-bindgen, serde-wasm-bindgen]
role: "wasm_replay_player"
solitaire_app:
depends_on: [solitaire_engine]
role: "entrypoint"
---
## 2. Data Ownership
ownership:
GameState:
owner: solitaire_core
mutable_in: solitaire_engine
access_pattern: "via GameStateResource only"
StatsSnapshot:
owner: solitaire_data
PlayerProgress:
owner: solitaire_data
AchievementRecord:
owner: solitaire_data
SyncPayload:
owner: solitaire_sync
---
## 3. State Transitions
state_machine:
GameState:
transitions:
- action: move_cards
returns: Result<GameState, MoveError>
```
- action: draw
returns: Result<GameState, MoveError>
- action: undo
returns: Result<GameState, MoveError>
invariants:
- "52 cards always exist"
- "no duplicate card IDs"
- "all cards belong to exactly one pile"
```
---
## 4. Event System
events:
input:
- MoveRequestEvent
- DrawRequestEvent
- UndoRequestEvent
- NewGameRequestEvent
state:
- StateChangedEvent
- GameWonEvent
meta:
- AchievementUnlockedEvent
- SyncCompleteEvent
rules:
* "Input events trigger core logic"
* "Core logic emits state events"
* "UI reacts to state events only"
---
## 5. Sync Contract
sync:
provider_trait:
methods:
- pull() -> SyncPayload
- push(payload) -> SyncResponse
guarantees:
- "non-blocking during gameplay"
- "blocking allowed on exit only"
merge:
rules:
counters: "max"
best_times: "min"
collections: "union"
achievements: "never removed"
```
properties:
- deterministic
- idempotent
- lossless
```
---
## 6. Persistence
storage:
format: json
files:
- stats.json
- progress.json
- achievements.json
- settings.json
- game_state.json
guarantees:
- atomic_write: true
- crash_safe: true
---
## 7. Engine Rules
engine:
mutation_rules:
- "Only GameLogicSystem mutates GameState"
- "UI systems are read-only"
threading:
- "sync runs on AsyncComputeTaskPool"
- "main thread must never block"
plugins:
pattern: "feature_isolation"
communication: "events and resources"
---
## 8. Server Contract
server:
auth:
method: jwt
access_expiry: 24h
refresh_expiry: 30d
endpoints:
- POST /api/auth/register
- POST /api/auth/login
- GET /api/sync/pull
- POST /api/sync/push
limits:
payload_max: 1MB
rate_limit: "10 req/min auth routes"
---
## 9. Achievement System
achievements:
definition_location: solitaire_core
state_location: solitaire_data
types:
- condition_based
- event_driven
rule:
- "achievements cannot be revoked"
---
## 10. Testing Rules
testing:
philosophy:
- "test real failures"
- "avoid redundant tests"
required_coverage:
solitaire_core:
- move_validation
- undo_integrity
- win_detection
```
solitaire_sync:
- merge_correctness
- idempotency
```
---
## 11. Prohibited Patterns
(See CLAUDE.md §11 for the canonical forbidden-patterns list.)
---
## 12. Extension Points
extensibility:
sync_backends:
pattern: "implement SyncProvider"
game_modes:
location: solitaire_core::GameMode
plugins:
rule: "new feature = new plugin"
---
## 13. Validation Checklist (for Claude)
validation:
* check: "crate dependency rules respected"
* check: "no panics in core"
* check: "events used for cross-system communication"
* check: "GameState mutations centralized"
* check: "merge function properties preserved"
* check: "no blocking operations in main loop"
---
## 14. Mental Model
model:
layers:
- core
- engine
- data
- server
flow:
- input -> engine -> core -> engine -> ui
- data <-> sync <-> server
-335
View File
@@ -1,335 +0,0 @@
# CLAUDE_WORKFLOW.md
version: 1.0
---
## 0. Overview
This workflow defines a **two-agent system**:
* **Builder Agent** → writes and modifies code
* **Guardian Agent** → enforces architecture + rejects invalid changes
No code is considered valid unless it passes Guardian validation.
---
## 1. Agent Roles
### 1.1 Builder Agent
role: "code_generation"
responsibilities:
* implement features
* refactor code
* generate tests (only when justified)
* follow CLAUDE_SPEC.md
constraints:
* cannot bypass validation
* must declare intent before writing code
output_contract:
must_produce:
- change_summary
- files_modified
- reasoning (short)
- code_diff
---
### 1.2 Guardian Agent
role: "architecture_enforcement"
responsibilities:
* validate against CLAUDE_SPEC.md
* detect violations
* reject or approve changes
* suggest minimal fixes (not full rewrites)
constraints:
* no feature implementation
* no large rewrites
* must be deterministic
output_contract:
must_produce:
- status: APPROVED | REJECTED
- violations[]
- required_fixes[]
- optional_improvements[]
---
## 2. Workflow Pipeline
```text
User Request
Builder Agent (proposal + code)
Guardian Agent (validation)
IF approved → commit
IF rejected → feedback → Builder retry
```
---
## 3. Builder Protocol
### Step 1 — Intent Declaration
Builder MUST start with:
```yaml
intent:
feature: "<name>"
crates_touched: []
systems_affected: []
risk_level: low|medium|high
```
---
### Step 2 — Plan
```yaml
plan:
- step: "..."
- step: "..."
```
---
### Step 3 — Implementation
* Only modify declared crates
* Follow ownership rules
* Use events for cross-system communication
---
### Step 4 — Output
```yaml
change_summary: "..."
files_modified:
- path: ...
change: "..."
violations_self_check:
- none | list
notes: "short reasoning"
```
---
## 4. Guardian Protocol
### Step 1 — Spec Validation
Check against:
* crate boundaries
* mutation rules
* event system usage
* sync guarantees
* forbidden patterns
---
### Step 2 — Invariant Validation
Must verify:
* GameState invariants preserved
* no new panic paths
* no blocking calls in engine
* merge properties unchanged
---
### Step 3 — Output Decision
#### APPROVED
```yaml
status: APPROVED
notes:
- "no violations"
```
---
#### REJECTED
```yaml
status: REJECTED
violations:
- id: core_purity_violation
file: "solitaire_core/src/..."
reason: "uses std::fs"
required_fixes:
- "move IO to solitaire_data"
optional_improvements:
- "simplify event naming"
```
---
## 5. Enforcement Rules
### Hard Fail (automatic rejection)
* core crate uses IO / Bevy / network
* GameState mutated outside GameLogicSystem
* blocking async on main thread
* duplicate logic across crates
* merge function altered incorrectly
---
### Soft Fail (allowed but flagged)
* unnecessary complexity
* redundant tests
* minor architectural drift
---
## 6. Iteration Loop
Max attempts per task: **3**
```text
Attempt 1 → Reject → Fix
Attempt 2 → Reject → Fix
Attempt 3 → Final decision
```
If still failing:
→ escalate to user
---
## 7. Diff Strategy
Builder MUST produce:
* minimal diffs
* no unrelated refactors
* no formatting-only changes
---
## 8. Test Strategy Integration
Builder rules:
* only add tests if:
* fixing a bug
* protecting complex logic
* validating invariants
Guardian rejects:
* redundant tests
* no-op tests
---
## 9. Optional Extensions
### 9.1 Third Agent (Optimizer)
role: performance + cleanup
runs AFTER approval:
* reduce allocations
* simplify logic
* improve ECS scheduling
---
### 9.2 CI Integration
Pipeline:
```text
Builder → Guardian → cargo check → clippy → tests
```
Guardian runs BEFORE compilation to catch structural issues early.
---
## 10. Example Interaction
### Builder
```yaml
intent:
feature: "undo stack limit fix"
crates_touched: [solitaire_core]
risk_level: low
```
```yaml
change_summary: "limit undo stack to 64 entries"
files_modified:
- solitaire_core/src/game_state.rs
notes: "prevents unbounded memory growth"
```
---
### Guardian
```yaml
status: APPROVED
notes:
- "respects core constraints"
- "no invariant violations"
```
---
## 11. Mental Model
* Builder = **creative**
* Guardian = **strict**
Builder explores
Guardian enforces
Neither replaces the other.
---
## 12. Success Criteria
System is working if:
* architectural violations go to ~0
* code stays consistent across features
* refactors become safe
* complexity grows sub-linearly
+3 -3
View File
@@ -1,6 +1,6 @@
# Credits
Solitaire Quest is MIT-licensed (see [LICENSE](LICENSE)). It is built on top of
Ferrous Solitaire is MIT-licensed (see [LICENSE](LICENSE)). It is built on top of
the work of many open-source projects and a small handful of third-party
assets. This file lists every component that ships in the binary or in the
`assets/` directory.
@@ -43,7 +43,7 @@ copyleft code is statically linked into the game binary.
| File(s) | Source | License |
|---|---|---|
| `solitaire_engine/assets/themes/default/{suit}_{rank}.svg` (52 SVGs) | [hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets) | MIT |
| `solitaire_engine/assets/themes/default/back.svg` | Original — Solitaire Quest | MIT (this project) |
| `solitaire_engine/assets/themes/default/back.svg` | Original — Ferrous Solitaire | MIT (this project) |
| `assets/cards/faces/{RANK}{SUIT}.png` (52 PNGs) | Pre-rendered from the same `playing-cards-assets` SVGs | MIT (passed through from hayeah) |
| `assets/cards/backs/back_0.png` `back_4.png` | Original — generated by `solitaire_assetgen::gen_art` | MIT (this project) |
@@ -107,6 +107,6 @@ Audio files are MIT-licensed alongside the rest of this project.
backs, every audio file) are original work covered by this project's MIT
license.
If you redistribute Solitaire Quest, you must ship this `CREDITS.md` and the
If you redistribute Ferrous Solitaire, you must ship this `CREDITS.md` and the
`LICENSE` file alongside the binary so the MIT (project + hayeah card art)
and OFL (FiraMono) notices remain visible to end users.
Generated
+8
View File
@@ -4034,9 +4034,14 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png 0.18.1",
"zune-core",
"zune-jpeg",
]
[[package]]
@@ -7013,11 +7018,14 @@ dependencies = [
"bevy",
"chrono",
"dirs",
"image",
"jni 0.21.1",
"kira",
"reqwest",
"resvg",
"ron",
"serde",
"serde_json",
"solitaire_core",
"solitaire_data",
"solitaire_sync",
+3
View File
@@ -110,6 +110,9 @@ ron = "0.12"
# only `deflate` is needed because the importer rejects other
# compression methods anyway (see Phase 7 spec).
zip = { version = "8.6", default-features = false, features = ["deflate"] }
# Image decoding for avatar bytes received from the server.
# Features mirror what Bevy already enables via bevy_image.
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
# Importer-only test dependency: tests build zip archives in a
# scratch directory so they don't pollute the real user themes path
+1 -1
View File
@@ -1,4 +1,4 @@
# Solitaire Quest
# Ferrous Solitaire
A cross-platform Klondike Solitaire game written in Rust, with a card-theme
system, full progression (XP / levels / achievements / daily challenges), and
+1 -1
View File
@@ -1,4 +1,4 @@
# Solitaire Quest — Self-Hosting Guide
# Ferrous Solitaire — Self-Hosting Guide
## Prerequisites
-177
View File
@@ -1,177 +0,0 @@
# Solitaire Quest — Session Handoff
**Last updated:** 2026-05-12 — Leaderboard display name shipped (`03be4fc`). All commits pushed to origin.
Phase 8 closes the self-hosted-server connection arc end-to-end: login/register
modal, re-auth on token expiry, account deletion flow, server deployment
artifacts (Dockerfile + docker-compose), replay upload on win, web replay
player (WASM + HTML/CSS/JS served by the server), leaderboard opt-in/out,
and full server integration tests.
---
## Current state
- **HEAD locally:** `03be4fc` (feat: leaderboard custom display name).
- **HEAD on origin:** `03be4fc` (fully pushed).
- **Working tree:** clean (only `solitaire-release.jks.bak2` untracked — intentional).
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings` clean.
- **Tests:** **1300+ passing / 0 failing** across the workspace.
- **Tags on origin:** `v0.9.0` through `v0.22.0`.
---
## What shipped in Phase 8 (432061c bd388fe)
| Commit | Summary |
|--------|---------|
| `432061c` | Sync setup modal (login/register/connect/disconnect) |
| `6ce5564` | Re-auth on expired session + server deployment artifacts |
| `272d31f` | Account deletion flow + `handle_sync_buttons` refactor |
| `bd388fe` | CHANGELOG v0.23.0 documentation |
Also shipped (pre-Phase 8 but post-v0.22.0, already in CHANGELOG):
- `solitaire_wasm` crate: WASM ReplayPlayer bindings for browser-side replay playback
- Server replay API: `POST /api/replays`, `GET /api/replays/recent`, `GET /api/replays/:id`
- Server web UI: `/replays/:id` HTML route + `ServeDir /web` static assets
- DB migration 002: `replays` table + two indexes
- Full server integration tests for replay endpoints
- `push_replay` in `sync_plugin` (uploads on win, writes share URL into replay history)
- Stats panel "Copy Share Link" button reads `share_url` from replay history
---
## Open punch list (ordered by priority)
### 1. Documentation debt (no code)
- [x] CHANGELOG [Unreleased] → v0.23.0 — done this session
- [x] ARCHITECTURE.md update — all 8 gaps closed, bumped to v1.3
- [x] SESSION_HANDOFF.md update — this file
### 2. Leaderboard wiring gaps
- [x] **Best-score auto-post.** Done (`303c78a`): `update_leaderboard_if_opted_in`
called from both first-push and merge paths in `sync.rs`; uses SQLite `MIN`/`MAX`
in the UPDATE so scores never regress on stale data.
- [x] **Display name = username.** Done (`03be4fc`): `leaderboard_display_name:
Option<String>` added to `Settings`; editor modal in leaderboard panel; persists
to `settings.json`; `handle_opt_in_button` prefers custom name over username.
### 3. Security hardening
- [x] **Refresh token rotation.** Done (`b129664`): `refresh_tokens` table
(migration 003); jti embedded in JWT; rotate-on-use pattern; 3 integration
tests.
- [x] **Sync endpoint rate limiting.** Done (`6e6f3ef`): `UserIdKeyExtractor`
decodes JWT for per-user identity; falls back to IP; burst 10 / 6 min
steady-state; integration test passes.
### 4. Android validation
- [x] **Android Keystore functional test.** Done (2026-05-11, Pixel 7 AVD,
Android 14): `load_access_token()` exercised via `start_pull`; logcat confirmed
`NotFound` returned cleanly — no JNI panic. See `docs/android/PLAYABILITY_TODO.md` P4.
- [x] **JNI clipboard functional test.** Done (2026-05-11): temporary `KEYCODE_C`
hook confirmed `ClipboardManager.setPrimaryClip()` succeeds on Android 14.
Hook reverted. Production path requires Interaction::Pressed + non-null `share_url`.
Note: `adb shell input tap` doesn't deliver touch events on headless AVD (documented).
- [x] **`cargo apk build --lib` noisy stderr** — upstream cargo-apk bug; `--lib`
is the canonical command (CLAUDE.md §15.1, docs/ANDROID.md). No in-repo fix possible.
### 5. Feature completeness
- [x] **Theme importer UI.** Done (`613bbf8`): "Scan for new themes" button in
Settings Appearance section. Shows import path label, scans user_theme_dir()
for .zip archives, fires InfoToastEvent per file, refreshes ThemeRegistry.
- [x] **`mirror_achievement` removed.** Done (`549a817`): method was a no-op
default never overridden and never called; achievements already sync via
`SyncPayload` push. Deleted from trait and blanket impl.
- [x] **WASM build script.** Done (`40d0712`): `build_wasm.sh` at repo root
documents `wasm-pack build --target web`, cleans up pkg metadata files,
includes dependency guard + install instructions.
- [x] **Server password reset.** Done (`7514684`): `--reset-password <username>`
subcommand reads new password from stdin, bcrypt-hashes it, invalidates all
active sessions for the user.
### 5b. Android UX polish (2026-05-12)
- [x] **UX-1 — Modal Done button in gesture zone.** `apply_safe_area_to_modal_scrims` system
added to `SafeAreaInsetsPlugin` (`safe_area.rs`). Pads every `ModalScrim` bottom by
`insets.bottom / scale`. Fires on resource change + `Added<ModalScrim>`. Verified on device.
- [x] **UX-5b — Home mode glyph corruption.** Geometric Shapes (U+25xx, absent from FiraMono)
replaced with card suits U+26602666 in `home_plugin.rs`. Affects Zen/Challenge/Daily mode
selector buttons at level 5+.
- [x] **UX-7 — Help text wrap.** Android HUD entry shortened to
`"Open menu (Stats, Settings, Profile...)"` in `help_plugin.rs` — fits one line.
- [x] **BUG-3 — Multi-modal stacking.** `handle_menu_button` now checks
`scrims: Query<(), With<ModalScrim>>` and guards `spawn_menu_popover` with `scrims.is_empty()`.
Verified on device: ≡ tap while Stats open does nothing.
**Note:** These 4 fixes are implemented and verified but not yet committed.
### 6. Testing gaps
- [x] **Server 401 → refresh → retry path.** Done (`198df75`): both
`jwt_refresh_on_401_succeeds` (pull) and
`push_retries_after_401_on_expired_access_token` (push) in
`solitaire_data/tests/sync_round_trip.rs`.
- [x] **WASM winning-replay step-through.** Done (`b4ada2a`): greedy solver
searches seeds 1200 at test time; steps every move through `ReplayPlayer`;
asserts `is_won = true` on the final `StateSnapshot`.
---
## ARCHITECTURE.md gaps (for the update pass)
Items missing from the doc:
1. `solitaire_wasm` crate (§2 workspace + §3 responsibilities)
2. Replay API endpoints (§9 API Reference — 3 new routes)
3. Web replay player route (`/replays/:id` + `ServeDir /web`)
4. `SyncProvider` trait: 6 added methods
5. Theme system in Bevy plugin table (§5)
6. `Settings` new fields: `color_blind_mode`, `high_contrast_mode`,
`reduce_motion_mode`, `window_geometry`, `selected_card_back`,
`selected_background`
7. DB migration 002 (§7)
8. Update "Last Updated" date
---
## Process notes
- **Commit attribution:** use `funman300` as git user. Co-author line:
`Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>`.
- **Commit format:** `type(scope): description` per CLAUDE.md §7.
- **Never commit without:** `cargo test --workspace` passing + clippy clean.
- **Sub-agents** stage/verify only; orchestrator commits.
- **`CARD_PLAN.md`** referenced in `theme/` module comments but not present in
repo. Clean up references or commit the file.
- **Token-port pattern** (v0.20.0): when migrating tokens, walk every concrete
artifact downstream — PNGs, SVGs, literals, comments. Three "walked past this"
follow-ups in v0.21.0 all had this shape.
---
## Resume prompt
```
You are a senior Rust + Bevy developer working on Solitaire Quest.
Working directory: <Rusty_Solitaire clone path>.
Branch: master. v0.23.0 is the current version (HEAD: 03be4fc). Fully pushed.
READ FIRST (in order):
1. SESSION_HANDOFF.md — this file
2. CHANGELOG.md — [0.23.0] section has full Phase 8 detail
3. CLAUDE.md — unified-4.0 rule set
4. ARCHITECTURE.md — v1.3, fully up to date
5. docs/ui-mockups/ — design system + mockup library
6. docs/android/ — Android setup + build runbook
7. ~/.claude/projects/<this-project>/memory/MEMORY.md
OPEN WORK:
Phase 8 punch list is fully closed. All items verified complete.
Remaining nuisance: `cargo apk build --lib` noisy stderr (cosmetic, non-blocking).
4 Android UX fixes are implemented and verified but NOT YET COMMITTED:
- BUG-3 (hud_plugin.rs): multi-modal stacking guard
- UX-7 (help_plugin.rs): help text wrap on Android
- UX-5b (home_plugin.rs): FiraMono glyph corruption in mode selector
- UX-1 (safe_area.rs): modal Done button in gesture zone
Commit those first, then suggest Phase 9 planning.
```
+9 -1
View File
@@ -6,12 +6,20 @@ metadata:
spec:
project: default
source:
repoURL: http://10.10.0.64:3000/funman300/Rusty_Solitare.git
repoURL: https://git.aleshym.co/funman300/Rusty_Solitare.git
targetRevision: master
path: deploy
destination:
server: https://kubernetes.default.svc
namespace: solitaire
# Secrets are applied manually and must not be pruned by ArgoCD.
ignoreDifferences:
- group: ""
kind: Secret
name: matomo-secret
namespace: solitaire
jsonPointers:
- /data
syncPolicy:
automated:
prune: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Some files were not shown because too many files have changed in this diff Show More