Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27eed98922 | |||
| f304917d62 | |||
| d49c478efa | |||
| 29f9b9358e | |||
| 9ef5759f40 | |||
| 9c9c0c76d3 | |||
| d4fb9e36a8 | |||
| ab35fcf906 | |||
| 32991301dd | |||
| c5fd928dcb | |||
| f6907671be | |||
| a54fff7257 | |||
| 533bcec2d8 | |||
| ba786f5a09 | |||
| 7ee7cb6d93 | |||
| 14324b09ef | |||
| 124f1f5cf5 | |||
| a6a73b5f36 | |||
| b84fe79806 | |||
| 3248f00d66 | |||
| c680a043ae | |||
| d0ab7ed97b | |||
| 1144a96757 | |||
| ac6668cee7 | |||
| eba1f66b45 | |||
| 90959728b1 | |||
| 8b30f8778b | |||
| d6a7924f14 | |||
| 4db43fb3fb | |||
| 01d6b27e61 | |||
| 3cffbc2c51 | |||
| 2ef25934ac | |||
| bb670d6cc6 | |||
| 76911c57c9 | |||
| 8391235a1a | |||
| 2f3a6b9586 | |||
| 4d20b70809 | |||
| bfadcf0e0d | |||
| 356dbebe57 | |||
| c90c783177 | |||
| bbf4b2c14a | |||
| 62be72e918 | |||
| 1f46785b31 | |||
| 2e5d82f83c | |||
| 396ba6bc97 | |||
| 88298206bb | |||
| 0f65031114 | |||
| c91ce9436e | |||
| ace96b4a47 | |||
| ea079af9e1 | |||
| c66d81c73a | |||
| 20b7a617e0 | |||
| 7a0d57b2b1 | |||
| 93ec4a7478 | |||
| 72dfd741c4 | |||
| 3837a10b15 | |||
| 574115cb71 | |||
| 1707553790 | |||
| 6905f26b56 | |||
| 1b7c4d92aa | |||
| d685224ce6 | |||
| 539779d78b | |||
| f6506c57e5 | |||
| b88f3df119 | |||
| 0dcb783e94 | |||
| ea17f94b6c | |||
| d60dc18add | |||
| 38eefb22e8 | |||
| a579c25d5c | |||
| c40817d845 | |||
| c6c03b8bff | |||
| 5b3925a619 | |||
| 8485b3d1e0 | |||
| 8325bf6cf7 | |||
| ea58f5dd64 | |||
| c518255a2d | |||
| f5da9398f2 | |||
| b82573e7b1 | |||
| 40818f5bd2 | |||
| 228ebbad8a | |||
| 2b33feafc9 | |||
| f8c8c9158e | |||
| 9cc0837088 | |||
| b47462bd27 | |||
| 08d22c822a | |||
| feb581005c | |||
| 00f2d890f1 | |||
| 9533a7d420 | |||
| 5ec5ac1a19 | |||
| 86aea206b8 | |||
| 1bd1c0f927 | |||
| 7be7f4395c | |||
| 66c2907c25 | |||
| c2811fa661 | |||
| 933cc55ea9 | |||
| 58faae1911 | |||
| 96be1b85fb | |||
| bbf7709912 | |||
| 9983b873f9 | |||
| 079349dc0f | |||
| 8f82b9fcb5 | |||
| 0ebe87a411 | |||
| 1e6d153cd0 | |||
| af5ac68947 | |||
| 859b69b3c5 | |||
| 24ab25b0b7 | |||
| 918d83420b | |||
| a381a42f21 | |||
| 04f3dab563 | |||
| d204662415 | |||
| 4f0080dfbc | |||
| 46c3bf4bb2 | |||
| 6beb9f68ac | |||
| a0081a251c | |||
| 7411468e10 | |||
| 9af4046ac3 | |||
| d06af28aef | |||
| 27b58a5b71 | |||
| 3b6c8d2aab | |||
| 51fc8f65b1 | |||
| 65cb41461f | |||
| 24f5d140df | |||
| 03be4fcc67 | |||
| 9564f54fc0 | |||
| b4ada2a07e | |||
| d44cedbea0 | |||
| 75146847f6 | |||
| 566b112d9e | |||
| 198df75f94 | |||
| 40d07122ba | |||
| 08f74d1e25 | |||
| 6e6f3ef1ff | |||
| 549a817bb1 | |||
| 613bbf8799 | |||
| b129664344 | |||
| 7d7c83ab28 | |||
| bd388fef26 | |||
| 272d31f851 | |||
| 6ce55646d8 | |||
| 432061c3ec | |||
| 22303c62ff | |||
| b1731fe68a | |||
| 2b01f741b4 | |||
| 3110702c74 | |||
| 33fb9627a8 | |||
| 4398403418 | |||
| 002d96f2c8 | |||
| cc161cc37f | |||
| 8a3e30bd16 | |||
| 2a206b994c | |||
| ae7c6c97f1 | |||
| 016fb7214d | |||
| 948864e653 | |||
| 76a754d8e5 | |||
| 9fb59c7d47 | |||
| d714a11cfb | |||
| e107f5e218 | |||
| 463b7465ed | |||
| 92a5ebb15e | |||
| 89a21c0587 | |||
| 304cb050a7 | |||
| fcc7337c97 | |||
| 16ce2b88d2 | |||
| b9aa2620b8 | |||
| 47f02a60ae | |||
| a5c3188686 | |||
| 6a289b7b50 | |||
| bee712c5ab | |||
| 0db5e9dac4 | |||
| 681a54d9bb | |||
| 7894559ca7 | |||
| ab803c07af | |||
| e43b329fc1 | |||
| 7c07f71f02 | |||
| c1329bbb21 | |||
| 4303ef3f5b | |||
| 4df962ee07 | |||
| f281425b45 | |||
| 2c822ba2d7 | |||
| 7ddf2733c9 | |||
| 585570559c | |||
| 45436d0eda | |||
| 2062bd06f3 | |||
| 0cb15872b1 | |||
| 395a322adc | |||
| 5199a5e499 | |||
| 16242e6d77 | |||
| 202a64db45 | |||
| c0415eb0ee | |||
| a449f60bc5 | |||
| ad5f613277 | |||
| c50eaf81f7 | |||
| b44d2777ec | |||
| 52407e7256 | |||
| da3e5423dc | |||
| a1864271de | |||
| f63db769ae | |||
| 4437a1aaf9 | |||
| e7345aed6c | |||
| 140251beae | |||
| d6f32d3154 | |||
| 8fdc41f36f | |||
| 2e25476d0a | |||
| d3cb1a51d4 | |||
| c8358f4275 | |||
| a2432dfe7a | |||
| 511550232c | |||
| e5c4f51a6e | |||
| 23902cdc44 | |||
| 3cc8eacafa | |||
| 90e24d9711 | |||
| decbe0bbd9 | |||
| 1873b3f9be | |||
| d11d97e677 | |||
| d322abf67b | |||
| c9e4c0b4cd | |||
| fe68861e10 | |||
| c33b39cf11 | |||
| 23ff62c397 | |||
| 0b2ffca016 | |||
| fbe48acef6 | |||
| cd79877933 | |||
| 52befa6199 | |||
| e63046700c | |||
| ab857bbb6e | |||
| 886e0cf8a1 | |||
| 3d92a91e3b | |||
| 9113cdb483 | |||
| c153363626 | |||
| 93b67f1d0b | |||
| 279e23d0af | |||
| 12fba2157a | |||
| f23df3b805 | |||
| 68d50b5021 | |||
| ec804d54c6 | |||
| d87761d451 | |||
| 2fb2d638bf | |||
| c9af1ead22 | |||
| ed152e2d8f | |||
| 279a834f9d | |||
| daa655a0af | |||
| 4d48cad4e3 | |||
| dd970215cc | |||
| ddb65403c2 | |||
| 62b61cc786 | |||
| 31139ae455 | |||
| 07e035771c | |||
| c5787c6953 | |||
| 716a025352 | |||
| 3eb3a26789 | |||
| 0c1cc40266 |
@@ -0,0 +1,131 @@
|
|||||||
|
name: Android Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
# Rebuild whenever app/engine/asset code changes.
|
||||||
|
# Skip server-only, deploy, and doc changes.
|
||||||
|
paths-ignore:
|
||||||
|
- 'deploy/**'
|
||||||
|
- 'argocd/**'
|
||||||
|
- 'solitaire_server/**'
|
||||||
|
- '**.md'
|
||||||
|
|
||||||
|
env:
|
||||||
|
ANDROID_SDK: /opt/android-sdk
|
||||||
|
NDK_VERSION: "25.2.9519653"
|
||||||
|
BUILD_TOOLS_VERSION: "34.0.0"
|
||||||
|
PLATFORM: "android-34"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-apk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set short SHA
|
||||||
|
id: meta
|
||||||
|
run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ── System dependencies ────────────────────────────────────────────
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y openjdk-17-jdk-headless unzip zip
|
||||||
|
|
||||||
|
# ── Android SDK (shared cache key with release workflow) ──────────
|
||||||
|
- name: Cache Android SDK
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: sdk-cache
|
||||||
|
with:
|
||||||
|
path: ${{ env.ANDROID_SDK }}
|
||||||
|
key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Android SDK + NDK
|
||||||
|
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
sudo mkdir -p ${{ env.ANDROID_SDK }}/cmdline-tools
|
||||||
|
curl -sL \
|
||||||
|
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||||
|
-o /tmp/cmdtools.zip
|
||||||
|
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||||
|
sudo mv /tmp/cmdtools/cmdline-tools ${{ env.ANDROID_SDK }}/cmdline-tools/latest
|
||||||
|
yes | sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
|
--sdk_root=${{ env.ANDROID_SDK }} --licenses > /dev/null 2>&1 || true
|
||||||
|
sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
|
--sdk_root=${{ env.ANDROID_SDK }} \
|
||||||
|
"build-tools;${{ env.BUILD_TOOLS_VERSION }}" \
|
||||||
|
"platforms;${{ env.PLATFORM }}" \
|
||||||
|
"ndk;${{ env.NDK_VERSION }}"
|
||||||
|
|
||||||
|
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||||
|
- name: Install Rust stable
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
|
| sh -s -- -y --default-toolchain stable --no-modify-path
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Add Android cross-compilation targets
|
||||||
|
run: |
|
||||||
|
rustup target add \
|
||||||
|
aarch64-linux-android \
|
||||||
|
armv7-linux-androideabi \
|
||||||
|
x86_64-linux-android
|
||||||
|
|
||||||
|
# ── Cargo caches ───────────────────────────────────────────────────
|
||||||
|
- name: Cache Cargo registry
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry/index
|
||||||
|
~/.cargo/registry/cache
|
||||||
|
~/.cargo/git/db
|
||||||
|
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: cargo-registry-
|
||||||
|
|
||||||
|
- name: Cache cargo-ndk binary
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: ndk-tool-cache
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/bin/cargo-ndk
|
||||||
|
key: cargo-ndk-${{ runner.os }}-stable
|
||||||
|
|
||||||
|
- name: Cache build artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: android-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
||||||
|
restore-keys: android-target-${{ hashFiles('**/Cargo.lock') }}-
|
||||||
|
|
||||||
|
- name: Install cargo-ndk
|
||||||
|
if: steps.ndk-tool-cache.outputs.cache-hit != 'true'
|
||||||
|
run: cargo install cargo-ndk --locked
|
||||||
|
|
||||||
|
# ── Build APK ──────────────────────────────────────────────────────
|
||||||
|
# Debug CI only builds arm64-v8a — full three-ABI debug builds blow
|
||||||
|
# past the runner's disk budget (~25 GB of target/ + intermediate
|
||||||
|
# APKs caused apksigner to OOM-on-disk in the previous run). Release
|
||||||
|
# CI still ships all three ABIs from android-release.yml.
|
||||||
|
- name: Build debug APK
|
||||||
|
env:
|
||||||
|
ANDROID_HOME: ${{ env.ANDROID_SDK }}
|
||||||
|
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }}
|
||||||
|
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOLS_VERSION }}
|
||||||
|
PLATFORM: ${{ env.PLATFORM }}
|
||||||
|
PROFILE: debug
|
||||||
|
ABIS: arm64-v8a
|
||||||
|
run: ./scripts/build_android_apk.sh
|
||||||
|
|
||||||
|
# ── Artifact ───────────────────────────────────────────────────────
|
||||||
|
# Pinned to v3 because Gitea Actions doesn't implement the github.com
|
||||||
|
# artifact service that upload-artifact@v4+ requires; v3 uses the
|
||||||
|
# older chunked HTTP API that Gitea's GHES-compatibility layer
|
||||||
|
# supports.
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: solitaire-quest-debug-${{ steps.meta.outputs.sha }}
|
||||||
|
path: target/debug/apk/solitaire-quest.apk
|
||||||
|
retention-days: 30
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
name: Android Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
ANDROID_SDK: /opt/android-sdk
|
||||||
|
NDK_VERSION: "25.2.9519653"
|
||||||
|
BUILD_TOOLS_VERSION: "34.0.0"
|
||||||
|
PLATFORM: "android-34"
|
||||||
|
GITEA_API: https://git.aleshym.co/api/v1
|
||||||
|
REPO: funman300/Rusty_Solitare
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-release-apk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: meta
|
||||||
|
run: echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ── System dependencies ────────────────────────────────────────────
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y openjdk-17-jdk-headless unzip zip jq
|
||||||
|
|
||||||
|
# ── Android SDK (shared cache key with debug workflow) ─────────────
|
||||||
|
- name: Cache Android SDK
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: sdk-cache
|
||||||
|
with:
|
||||||
|
path: ${{ env.ANDROID_SDK }}
|
||||||
|
key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Android SDK + NDK
|
||||||
|
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
sudo mkdir -p ${{ env.ANDROID_SDK }}/cmdline-tools
|
||||||
|
curl -sL \
|
||||||
|
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||||
|
-o /tmp/cmdtools.zip
|
||||||
|
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||||
|
sudo mv /tmp/cmdtools/cmdline-tools ${{ env.ANDROID_SDK }}/cmdline-tools/latest
|
||||||
|
yes | sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
|
--sdk_root=${{ env.ANDROID_SDK }} --licenses > /dev/null 2>&1 || true
|
||||||
|
sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||||
|
--sdk_root=${{ env.ANDROID_SDK }} \
|
||||||
|
"build-tools;${{ env.BUILD_TOOLS_VERSION }}" \
|
||||||
|
"platforms;${{ env.PLATFORM }}" \
|
||||||
|
"ndk;${{ env.NDK_VERSION }}"
|
||||||
|
|
||||||
|
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||||
|
- name: Install Rust stable
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
|
| sh -s -- -y --default-toolchain stable --no-modify-path
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Add Android cross-compilation targets
|
||||||
|
run: |
|
||||||
|
rustup target add \
|
||||||
|
aarch64-linux-android \
|
||||||
|
armv7-linux-androideabi \
|
||||||
|
x86_64-linux-android
|
||||||
|
|
||||||
|
# ── Cargo caches ───────────────────────────────────────────────────
|
||||||
|
- name: Cache Cargo registry
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry/index
|
||||||
|
~/.cargo/registry/cache
|
||||||
|
~/.cargo/git/db
|
||||||
|
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: cargo-registry-
|
||||||
|
|
||||||
|
- name: Cache cargo-ndk binary
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: ndk-tool-cache
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/bin/cargo-ndk
|
||||||
|
key: cargo-ndk-${{ runner.os }}-stable
|
||||||
|
|
||||||
|
- name: Cache build artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: android-release-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
||||||
|
restore-keys: android-release-target-${{ hashFiles('**/Cargo.lock') }}-
|
||||||
|
|
||||||
|
- name: Install cargo-ndk
|
||||||
|
if: steps.ndk-tool-cache.outputs.cache-hit != 'true'
|
||||||
|
run: cargo install cargo-ndk --locked
|
||||||
|
|
||||||
|
# ── Build & sign with release keystore ─────────────────────────────
|
||||||
|
- name: Decode keystore
|
||||||
|
run: |
|
||||||
|
secret_len=$(echo -n "${{ secrets.KEYSTORE_BASE64 }}" | wc -c)
|
||||||
|
echo "KEYSTORE_BASE64 secret length: ${secret_len} chars"
|
||||||
|
set +e
|
||||||
|
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > /tmp/solitaire-release.jks 2>/tmp/b64_err.txt
|
||||||
|
b64_exit=$?
|
||||||
|
set -e
|
||||||
|
size=$(wc -c < /tmp/solitaire-release.jks)
|
||||||
|
echo "base64 exit code: ${b64_exit}, keystore size: ${size} bytes"
|
||||||
|
[ -s /tmp/b64_err.txt ] && echo "base64 error: $(cat /tmp/b64_err.txt)" || true
|
||||||
|
[ "$size" -gt 0 ] || { echo "ERROR: KEYSTORE_BASE64 is empty or invalid base64"; exit 1; }
|
||||||
|
|
||||||
|
- name: Build signed release APK
|
||||||
|
env:
|
||||||
|
ANDROID_HOME: ${{ env.ANDROID_SDK }}
|
||||||
|
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }}
|
||||||
|
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOLS_VERSION }}
|
||||||
|
PLATFORM: ${{ env.PLATFORM }}
|
||||||
|
PROFILE: release
|
||||||
|
KEYSTORE: /tmp/solitaire-release.jks
|
||||||
|
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
KEY_PASS: ${{ secrets.KEY_PASS }}
|
||||||
|
APK_OUT: ferrous-solitaire-${{ steps.meta.outputs.tag }}.apk
|
||||||
|
run: ./scripts/build_android_apk.sh
|
||||||
|
|
||||||
|
# ── Publish to Gitea release ───────────────────────────────────────
|
||||||
|
- name: Create Gitea release
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
RESPONSE=$(curl -s -o /tmp/release.json -w "%{http_code}" \
|
||||||
|
-X POST "$GITEA_API/repos/$REPO/releases" \
|
||||||
|
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}")
|
||||||
|
if [ "$RESPONSE" = "409" ]; then
|
||||||
|
curl -sf "$GITEA_API/repos/$REPO/releases/tags/$TAG" \
|
||||||
|
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||||||
|
> /tmp/release.json
|
||||||
|
elif [ "$RESPONSE" != "201" ]; then
|
||||||
|
echo "Release creation failed with HTTP $RESPONSE"
|
||||||
|
cat /tmp/release.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
|
||||||
|
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Upload signed APK
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
APK="ferrous-solitaire-${TAG}.apk"
|
||||||
|
curl -sf -X POST \
|
||||||
|
"$GITEA_API/repos/$REPO/releases/${{ steps.release.outputs.release_id }}/assets?name=$APK" \
|
||||||
|
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"$APK"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
# Only run when server code changes, not when CI itself updates deploy/.
|
||||||
|
paths-ignore:
|
||||||
|
- 'deploy/**'
|
||||||
|
- 'argocd/**'
|
||||||
|
- '**.md'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.aleshym.co
|
||||||
|
IMAGE: git.aleshym.co/funman300/solitaire-server
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Need full history so we can push the tag-update commit back.
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.CI_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set image tag
|
||||||
|
id: meta
|
||||||
|
run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to Gitea registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
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:
|
||||||
|
context: .
|
||||||
|
file: solitaire_server/Dockerfile
|
||||||
|
push: true
|
||||||
|
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: |
|
||||||
|
curl -sL "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
||||||
|
sudo mv kustomize /usr/local/bin/kustomize
|
||||||
|
|
||||||
|
- name: Pin image tag in deploy manifests
|
||||||
|
run: |
|
||||||
|
cd deploy
|
||||||
|
kustomize edit set image solitaire-server=${{ env.IMAGE }}:${{ steps.meta.outputs.sha }}
|
||||||
|
|
||||||
|
- name: Commit and push updated kustomization
|
||||||
|
run: |
|
||||||
|
git config user.email "ci@gitea.local"
|
||||||
|
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
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUSTFLAGS: "-D warnings"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Test & Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: clippy
|
|
||||||
|
|
||||||
- name: Install Linux audio/display dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
libasound2-dev \
|
|
||||||
libudev-dev \
|
|
||||||
libwayland-dev \
|
|
||||||
libxkbcommon-dev
|
|
||||||
|
|
||||||
- name: Cache cargo registry and build artifacts
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- name: Clippy (all crates, zero warnings)
|
|
||||||
run: cargo clippy --workspace -- -D warnings
|
|
||||||
|
|
||||||
- name: Test (headless crates only — no display required)
|
|
||||||
run: |
|
|
||||||
cargo test -p solitaire_core
|
|
||||||
cargo test -p solitaire_sync
|
|
||||||
cargo test -p solitaire_data
|
|
||||||
cargo test -p solitaire_server
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Release Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install Linux audio/display dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
libasound2-dev \
|
|
||||||
libudev-dev \
|
|
||||||
libwayland-dev \
|
|
||||||
libxkbcommon-dev
|
|
||||||
|
|
||||||
- name: Cache cargo registry and build artifacts
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-release-
|
|
||||||
|
|
||||||
- name: Build release binaries
|
|
||||||
run: cargo build --workspace --release
|
|
||||||
@@ -7,3 +7,17 @@
|
|||||||
*.tmp
|
*.tmp
|
||||||
data/
|
data/
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# IDE project files
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# 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,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": "DELETE FROM refresh_tokens WHERE jti = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "168e205e3eb832b78d085b48281a1bae74d5a0e64c4c793c18a6400605bacf76"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"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": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -34,5 +34,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112"
|
"hash": "2b814989a6632ca930ae1e895f97a7fc3389c91d1d2abf6900a21fb0d6e94ef3"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM refresh_tokens WHERE user_id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "40db0910531d4418d4d58d31f0f8ea3894248406cc016020a6e211ed66da91c0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT jti FROM refresh_tokens WHERE jti = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "jti",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "893c45c27854ba15ea611e8254ef980ced21dc64e6ca95fde56af513f86ffa46"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO refresh_tokens (jti, user_id, expires_at) VALUES (?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "c9ee5c64ca547f0c730379919a642bd649cbf81fc1804159101a70efabf08b33"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE users SET password_hash = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "e4eb622073cbdf868ec1568a6bdb132e962480b0530d542102c05aa9e901463b"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM refresh_tokens WHERE expires_at < ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ef7af925a8715c329dcafca5257c691e6bca31755eb5f54be47114f21fc04c8c"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT OR IGNORE INTO analytics_events\n (id, user_id, session_id, event_type, payload, client_time, received_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 7
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "f23630e78ae88e72d7930184f7cd8fc67e71f3930609f1cf14061d35d6de8ec3"
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# Solitaire Quest — Architecture Document
|
# Ferrous Solitaire — Architecture Document
|
||||||
|
|
||||||
> **Version:** 1.1
|
> **Version:** 1.3
|
||||||
> **Language:** Rust (Edition 2024)
|
> **Language:** Rust (Edition 2024)
|
||||||
> **Engine:** Bevy (latest stable)
|
> **Engine:** Bevy (latest stable)
|
||||||
> **Last Updated:** 2026-04-29
|
> **Last Updated:** 2026-05-12
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
## 1. Project Overview
|
## 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
|
### Sync Backend by Platform
|
||||||
|
|
||||||
@@ -43,6 +43,7 @@ Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, tar
|
|||||||
| macOS | Self-hosted server | Full feature set |
|
| macOS | Self-hosted server | Full feature set |
|
||||||
| Windows | Self-hosted server | Full feature set |
|
| Windows | Self-hosted server | Full feature set |
|
||||||
| Linux | Self-hosted server | Full feature set |
|
| Linux | Self-hosted server | Full feature set |
|
||||||
|
| Android | Self-hosted server | Touch input; safe-area insets via JNI; `cargo-apk` build |
|
||||||
|
|
||||||
### Design Principles
|
### Design Principles
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ solitaire_quest/
|
|||||||
├── solitaire_data/ # Persistence, sync client, settings
|
├── solitaire_data/ # Persistence, sync client, settings
|
||||||
├── solitaire_engine/ # Bevy ECS systems, components, plugins
|
├── solitaire_engine/ # Bevy ECS systems, components, plugins
|
||||||
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite)
|
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite)
|
||||||
|
├── solitaire_wasm/ # WebAssembly bindings — browser-side replay player
|
||||||
└── solitaire_app/ # Main binary entry point
|
└── solitaire_app/ # Main binary entry point
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -160,6 +162,20 @@ Owns:
|
|||||||
- Daily challenge seed generation
|
- Daily challenge seed generation
|
||||||
- Leaderboard management
|
- Leaderboard management
|
||||||
|
|
||||||
|
### `solitaire_wasm`
|
||||||
|
**Dependencies:** `solitaire_core`, `serde`, `serde_json`, `chrono`, `wasm-bindgen`, `serde-wasm-bindgen`.
|
||||||
|
|
||||||
|
WebAssembly bindings for browser-side replay playback. Compiled to `cdylib` via `wasm-pack build`; the output lives in `solitaire_server/web/pkg/` and is served statically by the server.
|
||||||
|
|
||||||
|
Intentionally **does not** depend on `solitaire_data` (which pulls in `dirs`, `keyring`, `reqwest`, and other non-WASM crates). Instead it defines a minimal `Replay` mirror with the same serde shape as `solitaire_data::Replay` — the JSON wire format is the compatibility contract.
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
- `ReplayPlayer` — WASM-exported state machine; steps through a replay's `Vec<ReplayMove>` against a live `GameState`
|
||||||
|
- `StateSnapshot` — JS-facing pile snapshot returned by each `step()` call
|
||||||
|
- `ReplayMove` / `Replay` mirrors — same serde shape as `solitaire_data` v2 equivalents
|
||||||
|
|
||||||
|
Because `ReplayPlayer` uses the same `solitaire_core::GameState` as the desktop client, the two implementations cannot drift: the same seed + move list produces identical pile state at every step on both platforms.
|
||||||
|
|
||||||
### `solitaire_app`
|
### `solitaire_app`
|
||||||
**Dependencies:** `bevy`, `solitaire_engine`.
|
**Dependencies:** `bevy`, `solitaire_engine`.
|
||||||
|
|
||||||
@@ -261,6 +277,8 @@ The "Shortcut" column lists optional keyboard accelerators. Every action in this
|
|||||||
| `HomePlugin` | M | Main-menu overlay with keyboard shortcut reference |
|
| `HomePlugin` | M | Main-menu overlay with keyboard shortcut reference |
|
||||||
| `ProfilePlugin` | P | Player profile overlay: level, XP, achievements, sync status |
|
| `ProfilePlugin` | P | Player profile overlay: level, XP, achievements, sync status |
|
||||||
| `SettingsPlugin` | O | Settings panel: audio, draw mode, theme, sync, cosmetics |
|
| `SettingsPlugin` | O | Settings panel: audio, draw mode, theme, sync, cosmetics |
|
||||||
|
| `ThemePlugin` | — | Owns `ActiveTheme` resource; registers the `CardTheme` SVG asset loader; rasterises themes once per (theme, target size) at load time and caches the resulting `Image`; handles the embedded default theme and user themes from `user_theme_dir()` |
|
||||||
|
| `SyncSetupPlugin` | — | Sync setup modal (URL / username / password fields, "Log In" / "Register" buttons); account deletion confirm modal; re-auth trigger when `SyncError::Auth` is returned by a pull |
|
||||||
| `LeaderboardPlugin` | L | Leaderboard overlay |
|
| `LeaderboardPlugin` | L | Leaderboard overlay |
|
||||||
| `HelpPlugin` | H | Help / controls overlay |
|
| `HelpPlugin` | H | Help / controls overlay |
|
||||||
| `PausePlugin` | Esc | Pause and resume |
|
| `PausePlugin` | Esc | Pause and resume |
|
||||||
@@ -305,6 +323,12 @@ struct FontResource(Handle<Font>);
|
|||||||
struct BackgroundImageSet {
|
struct BackgroundImageSet {
|
||||||
handles: Vec<Handle<Image>>, // indices 0–4 match selected_background setting
|
handles: Vec<Handle<Image>>, // indices 0–4 match selected_background setting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OS-reserved edge insets (physical px); zero on desktop
|
||||||
|
struct SafeAreaInsets { top: f32, bottom: f32, left: f32, right: f32 }
|
||||||
|
|
||||||
|
// Whether the HUD band is visible (auto-hide chrome feature)
|
||||||
|
enum HudVisibility { Visible, Hidden }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Bevy Events
|
### Key Bevy Events
|
||||||
@@ -365,10 +389,22 @@ All sync backends implement a single trait in `solitaire_data`. The `SyncPlugin`
|
|||||||
```rust
|
```rust
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait SyncProvider: Send + Sync {
|
pub trait SyncProvider: Send + Sync {
|
||||||
|
// Required — must be implemented by every backend:
|
||||||
async fn pull(&self) -> Result<SyncPayload, SyncError>;
|
async fn pull(&self) -> Result<SyncPayload, SyncError>;
|
||||||
async fn push(&self, payload: &SyncPayload) -> Result<SyncResponse, SyncError>;
|
async fn push(&self, payload: &SyncPayload) -> Result<SyncResponse, SyncError>;
|
||||||
fn backend_name(&self) -> &'static str;
|
fn backend_name(&self) -> &'static str;
|
||||||
fn is_authenticated(&self) -> bool;
|
fn is_authenticated(&self) -> bool;
|
||||||
|
|
||||||
|
// Optional — all have default no-op / empty implementations:
|
||||||
|
async fn mirror_achievement(&self, _id: &str) -> Result<(), SyncError>;
|
||||||
|
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError>;
|
||||||
|
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError>;
|
||||||
|
async fn opt_in_leaderboard(&self, _display_name: &str) -> Result<(), SyncError>;
|
||||||
|
async fn opt_out_leaderboard(&self) -> Result<(), SyncError>;
|
||||||
|
async fn delete_account(&self) -> Result<(), SyncError>;
|
||||||
|
// Returns the shareable web URL on success; defaults to Err(UnsupportedPlatform)
|
||||||
|
// so LocalOnlyProvider silently no-ops the push-on-win path.
|
||||||
|
async fn push_replay(&self, _replay: &Replay) -> Result<String, SyncError>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -454,6 +490,24 @@ CREATE TABLE leaderboard (
|
|||||||
recorded_at TEXT NOT NULL,
|
recorded_at TEXT NOT NULL,
|
||||||
PRIMARY KEY (user_id)
|
PRIMARY KEY (user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- migrations/002_replays.sql
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS replays (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID v4 minted server-side
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
seed INTEGER NOT NULL,
|
||||||
|
draw_mode TEXT NOT NULL, -- "DrawOne" | "DrawThree"
|
||||||
|
mode TEXT NOT NULL, -- "Classic" | "Zen" | "Challenge" | "TimeAttack"
|
||||||
|
time_seconds INTEGER NOT NULL,
|
||||||
|
final_score INTEGER NOT NULL,
|
||||||
|
recorded_at TEXT NOT NULL, -- replay-side date (YYYY-MM-DD)
|
||||||
|
received_at TEXT NOT NULL, -- server insert timestamp (ISO 8601)
|
||||||
|
replay_json TEXT NOT NULL -- full Replay serialisation
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS replays_received_at_idx ON replays(received_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS replays_user_id_idx ON replays(user_id);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Request Lifecycle
|
### Request Lifecycle
|
||||||
@@ -579,12 +633,25 @@ pub struct AchievementRecord {
|
|||||||
|
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub draw_mode: DrawMode,
|
pub draw_mode: DrawMode,
|
||||||
pub sfx_volume: f32, // 0.0–1.0
|
pub sfx_volume: f32, // 0.0–1.0
|
||||||
pub music_volume: f32,
|
pub music_volume: f32,
|
||||||
pub animation_speed: AnimSpeed,
|
pub animation_speed: AnimSpeed,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
pub sync_backend: SyncBackend, // Local | SolitaireServer
|
pub sync_backend: SyncBackend, // Local | SolitaireServer
|
||||||
|
pub selected_card_back: usize, // index into PlayerProgress::unlocked_card_backs
|
||||||
|
pub selected_background: usize, // index into PlayerProgress::unlocked_backgrounds
|
||||||
pub first_run_complete: bool,
|
pub first_run_complete: bool,
|
||||||
|
pub color_blind_mode: bool, // blue tint on red suits
|
||||||
|
pub high_contrast_mode: bool, // boosted luminance for low-vision users
|
||||||
|
pub reduce_motion_mode: bool, // WCAG reduce-motion — snaps instead of slides
|
||||||
|
pub window_geometry: Option<WindowGeometry>, // persisted size + position; None on first run
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WindowGeometry {
|
||||||
|
pub width: u32, // logical pixels
|
||||||
|
pub height: u32,
|
||||||
|
pub x: i32, // physical pixels, top-left corner
|
||||||
|
pub y: i32,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -600,7 +667,7 @@ All endpoints are under the base URL configured by the user (e.g., `https://soli
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| POST | `/api/auth/register` | None | `{username, password}` | `{access_token, refresh_token}` |
|
| POST | `/api/auth/register` | None | `{username, password}` | `{access_token, refresh_token}` |
|
||||||
| POST | `/api/auth/login` | None | `{username, password}` | `{access_token, refresh_token}` |
|
| POST | `/api/auth/login` | None | `{username, password}` | `{access_token, refresh_token}` |
|
||||||
| POST | `/api/auth/refresh` | None | `{refresh_token}` | `{access_token}` |
|
| POST | `/api/auth/refresh` | None | `{refresh_token}` | `{access_token, refresh_token}` (rotated) |
|
||||||
|
|
||||||
### Sync
|
### Sync
|
||||||
|
|
||||||
@@ -617,6 +684,21 @@ All endpoints are under the base URL configured by the user (e.g., `https://soli
|
|||||||
| GET | `/api/leaderboard` | Bearer JWT | — | `Vec<LeaderboardEntry>` |
|
| GET | `/api/leaderboard` | Bearer JWT | — | `Vec<LeaderboardEntry>` |
|
||||||
| POST | `/api/leaderboard/opt-in` | Bearer JWT | — | `{ok: true}` |
|
| POST | `/api/leaderboard/opt-in` | Bearer JWT | — | `{ok: true}` |
|
||||||
|
|
||||||
|
### Replays
|
||||||
|
|
||||||
|
| Method | Path | Auth | Body | Response |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| POST | `/api/replays` | Bearer JWT | Replay JSON | `{id, share_url}` |
|
||||||
|
| GET | `/api/replays/recent` | None | — (`?limit=N`) | `Vec<ReplaySummary>` |
|
||||||
|
| GET | `/api/replays/:id` | None | — | Full Replay JSON |
|
||||||
|
|
||||||
|
### Web Replay Player
|
||||||
|
|
||||||
|
| Method | Path | Auth | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/replays/:id` | None | Serves `web/index.html`; JS fetches `/api/replays/:id` and steps through via the `solitaire_wasm` WASM module |
|
||||||
|
| GET | `/web/*` | None | Static assets served via `ServeDir` from `solitaire_server/web/` (includes `web/pkg/` with wasm-bindgen output) |
|
||||||
|
|
||||||
### Account Management
|
### Account Management
|
||||||
|
|
||||||
| Method | Path | Auth | Body | Response |
|
| Method | Path | Auth | Body | Response |
|
||||||
@@ -825,7 +907,7 @@ All sound effect WAV files are embedded at compile time via `include_bytes!()` i
|
|||||||
| macOS | Primary | Self-hosted server | x86_64 + Apple Silicon (universal binary via `cargo-lipo`) |
|
| macOS | Primary | Self-hosted server | x86_64 + Apple Silicon (universal binary via `cargo-lipo`) |
|
||||||
| Windows | Primary | Self-hosted server | x86_64, MSVC toolchain |
|
| Windows | Primary | Self-hosted server | x86_64, MSVC toolchain |
|
||||||
| Linux | Primary | Self-hosted server | x86_64, tested on Ubuntu 22.04+ and Fedora 39+ |
|
| Linux | Primary | Self-hosted server | x86_64, tested on Ubuntu 22.04+ and Fedora 39+ |
|
||||||
| Android | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
| Android | Active | Self-hosted server | `cargo-apk`; touch + long-press + double-tap; safe-area JNI; portrait layout |
|
||||||
| iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
| iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
||||||
|
|
||||||
Minimum Bevy window size enforced: 800×600. Desktop windows are freely resizable; layout recomputes on `WindowResized`.
|
Minimum Bevy window size enforced: 800×600. Desktop windows are freely resizable; layout recomputes on `WindowResized`.
|
||||||
@@ -945,6 +1027,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
|
|||||||
| Password storage | bcrypt, cost factor 12 — never stored in plaintext |
|
| Password storage | bcrypt, cost factor 12 — never stored in plaintext |
|
||||||
| Token security | JWTs signed with HS256, stored in OS keychain via `keyring` crate |
|
| Token security | JWTs signed with HS256, stored in OS keychain via `keyring` crate |
|
||||||
| Token expiry | Access: 24h, Refresh: 30d |
|
| Token expiry | Access: 24h, Refresh: 30d |
|
||||||
|
| Refresh token rotation | Each `/api/auth/refresh` call consumes the incoming refresh token (deletes its jti row) and issues a new one. Reuse of a consumed token returns 401. Expired rows are pruned inline. |
|
||||||
| Brute force | `tower-governor`: 10 req/min per IP on `/api/auth/*` |
|
| Brute force | `tower-governor`: 10 req/min per IP on `/api/auth/*` |
|
||||||
| Payload abuse | 1MB max request body, enforced by Axum middleware |
|
| Payload abuse | 1MB max request body, enforced by Axum middleware |
|
||||||
| Data deletion | `DELETE /api/account` removes all rows via `ON DELETE CASCADE` |
|
| Data deletion | `DELETE /api/account` removes all rows via `ON DELETE CASCADE` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
version: unified-3.0
|
version: unified-4.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,8 +29,9 @@ solitaire_sync/ # Shared API + merge logic
|
|||||||
solitaire_data/ # Persistence + sync client
|
solitaire_data/ # Persistence + sync client
|
||||||
solitaire_engine/ # Bevy ECS + UI + gameplay orchestration
|
solitaire_engine/ # Bevy ECS + UI + gameplay orchestration
|
||||||
solitaire_server/ # Axum backend (optional sync layer)
|
solitaire_server/ # Axum backend (optional sync layer)
|
||||||
|
solitaire_wasm/ # WASM bindings for browser-side replay player
|
||||||
solitaire_app/ # Entry binary
|
solitaire_app/ # Entry binary
|
||||||
assets/ # Runtime assets (except audio)
|
assets/ # Runtime assets (except audio + default theme)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -72,12 +73,16 @@ These override all other instructions.
|
|||||||
|
|
||||||
* NO `unwrap()`
|
* NO `unwrap()`
|
||||||
* NO `panic!()` in runtime/game logic
|
* NO `panic!()` in runtime/game logic
|
||||||
* All state transitions:
|
* Core game state mutations MUST return:
|
||||||
|
|
||||||
```rust id="err_model"
|
```rust id="err_model"
|
||||||
Result<T, MoveError>
|
Result<T, MoveError>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* Engine / UI state changes follow ECS patterns (Resources, Events) —
|
||||||
|
they do not return `MoveError`
|
||||||
|
* Use `thiserror`-derived types for any new error enums outside `solitaire_core`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.4 Threading Rules
|
## 2.4 Threading Rules
|
||||||
@@ -126,10 +131,15 @@ trait SyncProvider
|
|||||||
## 3.1 ECS Design
|
## 3.1 ECS Design
|
||||||
|
|
||||||
* systems = single responsibility
|
* systems = single responsibility
|
||||||
* communication = Events only
|
* cross-system communication = Events (fire-and-forget triggers)
|
||||||
* shared state = Resources only
|
* persistent shared state = Resources (polled every frame or on change)
|
||||||
* per-entity state = Components only
|
* per-entity state = Components only
|
||||||
|
|
||||||
|
Events and Resources are both valid communication paths — use Events when
|
||||||
|
the receiver needs to react once; use Resources when the receiver polls
|
||||||
|
or when multiple systems read the same value (e.g. `SafeAreaInsets`,
|
||||||
|
`HudVisibility`, `LayoutResource`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3.2 Game State Authority
|
## 3.2 Game State Authority
|
||||||
@@ -149,11 +159,22 @@ Every player action MUST:
|
|||||||
Keyboard shortcuts are:
|
Keyboard shortcuts are:
|
||||||
→ optional accelerators only
|
→ optional accelerators only
|
||||||
|
|
||||||
|
**Exception — UI chrome gestures:**
|
||||||
|
Tap-to-toggle visibility of UI chrome (e.g. auto-hiding HUD band) is
|
||||||
|
permitted without a visible button. The gesture MUST:
|
||||||
|
* affect only chrome visibility, never game state
|
||||||
|
* restore chrome automatically when any modal opens
|
||||||
|
* be purely additive (game remains fully playable with chrome always visible)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3.4 Layout System
|
## 3.4 Layout System
|
||||||
|
|
||||||
* recompute on `WindowResized`
|
* recompute on `WindowResized`
|
||||||
|
* recompute on `SafeAreaInsets` changed
|
||||||
|
* recompute on `HudVisibility` changed
|
||||||
|
* `compute_layout` MUST accept `hud_visible: bool`; pass `HUD_BAND_HEIGHT`
|
||||||
|
when `true`, `0.0` when `false`
|
||||||
* no fixed resolution assumptions
|
* no fixed resolution assumptions
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -178,11 +199,18 @@ Includes:
|
|||||||
|
|
||||||
## 4.2 Embedded Assets
|
## 4.2 Embedded Assets
|
||||||
|
|
||||||
Only audio:
|
Embed via `include_bytes!()` only when ALL of the following are true:
|
||||||
|
|
||||||
```text id="audio_rule"
|
* the asset is small (< 500 KB uncompressed)
|
||||||
include_bytes!()
|
* it changes rarely (not user-customisable)
|
||||||
```
|
* a missing file would be a hard crash, not a graceful degradation
|
||||||
|
|
||||||
|
Currently embedded:
|
||||||
|
* **Audio** — all `.wav` files in `audio_plugin.rs`
|
||||||
|
* **Default card theme** — shipped via `embedded://` scheme in `ThemePlugin`
|
||||||
|
|
||||||
|
Do NOT embed card face PNGs, background images, or user fonts —
|
||||||
|
these are loaded via `AssetServer` so art can be swapped without recompile.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -210,7 +238,9 @@ Must degrade gracefully under `MinimalPlugins`.
|
|||||||
## 5.2 Public API Rules
|
## 5.2 Public API Rules
|
||||||
|
|
||||||
* prefer `Into<T>` over concrete types
|
* prefer `Into<T>` over concrete types
|
||||||
* all public items require doc comments
|
* publicly exported functions, traits, and non-trivial types require doc comments
|
||||||
|
* simple marker components, newtype wrappers, and internal `pub` items
|
||||||
|
used only within the same crate are exempt from doc comment requirements
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -276,11 +306,13 @@ NEVER commit otherwise
|
|||||||
|
|
||||||
Claude must request confirmation before:
|
Claude must request confirmation before:
|
||||||
|
|
||||||
* adding dependencies
|
* adding dependencies to `solitaire_core` or `solitaire_sync`
|
||||||
* modifying `solitaire_sync`
|
(engine/server crates may add deps without confirmation)
|
||||||
* changing DB schema
|
* modifying `solitaire_sync` types or the `SyncProvider` trait
|
||||||
|
* changing DB schema (migrations are append-only)
|
||||||
* introducing `unsafe`
|
* introducing `unsafe`
|
||||||
* changing merge strategy
|
* changing the merge strategy in `solitaire_sync::merge`
|
||||||
|
* changing the `SyncPayload` wire format (breaking change for existing servers)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -304,10 +336,29 @@ Core is always the source of truth.
|
|||||||
|
|
||||||
Must always be handled explicitly:
|
Must always be handled explicitly:
|
||||||
|
|
||||||
|
**All platforms**
|
||||||
* Bevy `Time` uses `f32`
|
* Bevy `Time` uses `f32`
|
||||||
* `sqlx::migrate!()` path is crate-relative
|
* `sqlx::migrate!()` path is crate-relative
|
||||||
* `dirs::data_dir()` may return `None`
|
* `dirs::data_dir()` may return `None`
|
||||||
* Linux may lack keyring backend
|
* Linux may lack keyring backend — handle `keyring::Error` gracefully
|
||||||
|
|
||||||
|
**Android (active target — not stretch)**
|
||||||
|
* Safe-area insets arrive in frames 1–3 via JNI polling, not at startup;
|
||||||
|
UI that depends on them must handle the zero-inset initial state
|
||||||
|
* Physical pixels ≠ logical pixels: `SafeAreaInsets` values are physical
|
||||||
|
(from `WindowInsets` API); divide by `window.scale_factor()` before
|
||||||
|
passing to Bevy `Val::Px`
|
||||||
|
* `adb shell input tap` uses physical pixel coordinates
|
||||||
|
* FiraMono (bundled font) covers: ASCII, card suits U+2660–2666,
|
||||||
|
Arrows U+2190–21FF. It does NOT cover Geometric Shapes (U+25xx) —
|
||||||
|
those render as missing-glyph rectangles on Android
|
||||||
|
* The gesture/navigation bar at the bottom (≈132px physical on common
|
||||||
|
devices) is inside the Bevy viewport; use `SafeAreaInsets.bottom` to
|
||||||
|
avoid placing interactive elements in that zone
|
||||||
|
* `HUD_BAND_HEIGHT` is 128px on Android (two-row wrap) vs 64px on desktop;
|
||||||
|
layout constants are `#[cfg(target_os = "android")]` gated
|
||||||
|
* JNI calls must use `attach_current_thread_permanently` — not
|
||||||
|
`attach_current_thread` — to avoid detach-on-drop panics
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -318,6 +369,12 @@ Must always be handled explicitly:
|
|||||||
* blocking async calls in ECS
|
* blocking async calls in ECS
|
||||||
* insecure credential storage
|
* insecure credential storage
|
||||||
* bypassing core logic layer
|
* bypassing core logic layer
|
||||||
|
* hardcoded pixel coordinates in layout — always derive from `compute_layout`
|
||||||
|
* Unicode Geometric Shapes block (U+25xx) in UI text — not in FiraMono
|
||||||
|
* spawning a second `ModalScrim` while one already exists without first
|
||||||
|
dismissing the existing one (use `scrims.is_empty()` guard)
|
||||||
|
* reading `SafeAreaInsets` physical values directly into `Val::Px` without
|
||||||
|
dividing by `window.scale_factor()`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -345,9 +402,74 @@ If unclear:
|
|||||||
| Both combined | full system understanding |
|
| Both combined | full system understanding |
|
||||||
|
|
||||||
---
|
---
|
||||||
# 14. Context Injection System (AUTOMATIC SCOPE FILTER)
|
# 14. Modal System Conventions
|
||||||
|
|
||||||
## 14.1 Purpose
|
All full-screen overlay panels MUST use the `spawn_modal` / `ModalScrim` pattern
|
||||||
|
from `solitaire_engine::ui_modal`.
|
||||||
|
|
||||||
|
## 14.1 Spawn pattern
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let scrim = spawn_modal(commands, MyScreenMarker, Z_MODAL_PANEL, |card| {
|
||||||
|
spawn_modal_header(card, "Title", font_res);
|
||||||
|
// ... body nodes ...
|
||||||
|
spawn_modal_actions(card, |actions| {
|
||||||
|
spawn_modal_button(actions, MyCloseButton, "Done", None,
|
||||||
|
ButtonVariant::Primary, font_res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Optional: allow clicking the scrim outside the card to dismiss
|
||||||
|
commands.entity(scrim).insert(ScrimDismissible);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14.2 Guard rule
|
||||||
|
|
||||||
|
Before spawning a new modal, check `scrims: Query<(), With<ModalScrim>>`
|
||||||
|
and return early if `!scrims.is_empty()` — unless the new modal is
|
||||||
|
explicitly replacing the current one (despawn first, then spawn).
|
||||||
|
|
||||||
|
## 14.3 Safe area
|
||||||
|
|
||||||
|
Every `ModalScrim` automatically receives `padding.bottom` equal to the
|
||||||
|
logical gesture-bar height via `apply_safe_area_to_modal_scrims` in
|
||||||
|
`SafeAreaInsetsPlugin`. Do not manually add bottom padding to scrim nodes.
|
||||||
|
|
||||||
|
## 14.4 Z-ordering
|
||||||
|
|
||||||
|
Use `Z_MODAL_PANEL` from `ui_theme` for all modal scrims. Do not use
|
||||||
|
raw `z_index` values — they drift and cause ordering bugs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 15. Android Build & Verification
|
||||||
|
|
||||||
|
## 15.1 Build command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo apk build --package solitaire_app --lib
|
||||||
|
adb install -r target/debug/apk/solitaire-quest.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15.2 Coordinate system reminder
|
||||||
|
|
||||||
|
Device physical: 1080×2400. Bevy logical: 900×2000. Scale factor: 1.20.
|
||||||
|
`adb shell input tap X Y` takes PHYSICAL coordinates.
|
||||||
|
To convert from what you see on screen (logical): multiply by 1.20.
|
||||||
|
|
||||||
|
## 15.3 Android-specific test checklist
|
||||||
|
|
||||||
|
Before shipping any Android build:
|
||||||
|
- [ ] Safe area insets arrive and shift HUD correctly (check after 3s)
|
||||||
|
- [ ] All modal Done buttons are above the gesture bar
|
||||||
|
- [ ] No Geometric Shapes glyphs in UI text
|
||||||
|
- [ ] HUD band does not overlap the top status bar
|
||||||
|
- [ ] Touch drag-and-drop works on all pile types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 16. Context Injection System (AUTOMATIC SCOPE FILTER)
|
||||||
|
|
||||||
|
## 16.1 Purpose
|
||||||
|
|
||||||
Before generating any response, Claude MUST construct a **minimal relevant context set**.
|
Before generating any response, Claude MUST construct a **minimal relevant context set**.
|
||||||
|
|
||||||
@@ -360,7 +482,7 @@ This prevents:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.2 Input Classification Step (MANDATORY)
|
## 16.2 Input Classification Step (MANDATORY)
|
||||||
|
|
||||||
Every request MUST be classified into exactly one task type:
|
Every request MUST be classified into exactly one task type:
|
||||||
|
|
||||||
@@ -381,13 +503,13 @@ If uncertain → ask clarification.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.3 Context Selection Engine
|
## 16.3 Context Selection Engine
|
||||||
|
|
||||||
After classification, Claude MUST include ONLY the relevant sections below.
|
After classification, Claude MUST include ONLY the relevant sections below.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.4 Context Map (CORE RULESET)
|
## 16.4 Context Map (CORE RULESET)
|
||||||
|
|
||||||
### feature
|
### feature
|
||||||
|
|
||||||
@@ -495,7 +617,7 @@ Include:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.5 Context Compression Rules
|
## 16.5 Context Compression Rules
|
||||||
|
|
||||||
Claude MUST obey:
|
Claude MUST obey:
|
||||||
|
|
||||||
@@ -506,7 +628,7 @@ Claude MUST obey:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.6 Context Priority Order
|
## 16.6 Context Priority Order
|
||||||
|
|
||||||
When space is limited:
|
When space is limited:
|
||||||
|
|
||||||
@@ -517,7 +639,7 @@ When space is limited:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.7 “No Context Pollution” Rule
|
## 16.7 “No Context Pollution” Rule
|
||||||
|
|
||||||
Claude must NOT include:
|
Claude must NOT include:
|
||||||
|
|
||||||
@@ -529,7 +651,7 @@ Claude must NOT include:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.8 Self-Check Before Execution
|
## 16.8 Self-Check Before Execution
|
||||||
|
|
||||||
Before writing code, Claude MUST verify:
|
Before writing code, Claude MUST verify:
|
||||||
|
|
||||||
@@ -542,7 +664,7 @@ If any fail → revise context selection.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.9 Injection Output Format (Internal Model)
|
## 16.9 Injection Output Format (Internal Model)
|
||||||
|
|
||||||
Claude should behave as if it constructed:
|
Claude should behave as if it constructed:
|
||||||
|
|
||||||
@@ -560,7 +682,7 @@ Claude should behave as if it constructed:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14.10 Relationship to ARCHITECTURE.md
|
## 16.10 Relationship to ARCHITECTURE.md
|
||||||
|
|
||||||
* ARCHITECTURE.md = source of truth
|
* ARCHITECTURE.md = source of truth
|
||||||
* CLAUDE.md = execution constraints
|
* CLAUDE.md = execution constraints
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ You must follow CLAUDE_SPEC.md strictly.
|
|||||||
Rules:
|
Rules:
|
||||||
- Do not expand scope beyond what is defined
|
- Do not expand scope beyond what is defined
|
||||||
- Do not refactor unrelated code
|
- Do not refactor unrelated code
|
||||||
- Do not introduce new dependencies
|
- Do not introduce new dependencies to solitaire_core or solitaire_sync without confirmation
|
||||||
- Prefer minimal, surgical changes
|
- Prefer minimal, surgical changes
|
||||||
- Use existing patterns in the codebase
|
- Use existing patterns in the codebase
|
||||||
- Return minimal diffs or changed functions only
|
- Return minimal diffs or changed functions only
|
||||||
@@ -360,7 +360,7 @@ notes:
|
|||||||
target:
|
target:
|
||||||
"<what is slow>"
|
"<what is slow>"
|
||||||
|
|
||||||
constraints:CLAUDE_WORKFLOW.md
|
constraints:
|
||||||
- no behavior change
|
- no behavior change
|
||||||
- no architecture change
|
- no architecture change
|
||||||
- minimal code changes
|
- minimal code changes
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ solitaire_server:
|
|||||||
depends_on: [solitaire_sync, axum, sqlx, jsonwebtoken]
|
depends_on: [solitaire_sync, axum, sqlx, jsonwebtoken]
|
||||||
role: "backend"
|
role: "backend"
|
||||||
|
|
||||||
|
solitaire_wasm:
|
||||||
|
depends_on: [solitaire_core, wasm-bindgen, serde-wasm-bindgen]
|
||||||
|
role: "wasm_replay_player"
|
||||||
|
|
||||||
solitaire_app:
|
solitaire_app:
|
||||||
depends_on: [solitaire_engine]
|
depends_on: [solitaire_engine]
|
||||||
role: "entrypoint"
|
role: "entrypoint"
|
||||||
@@ -180,7 +184,7 @@ threading:
|
|||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
pattern: "feature_isolation"
|
pattern: "feature_isolation"
|
||||||
communication: "events"
|
communication: "events and resources"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Credits
|
# 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
|
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. This file lists every component that ships in the binary or in the
|
||||||
`assets/` directory.
|
`assets/` directory.
|
||||||
@@ -43,7 +43,7 @@ copyleft code is statically linked into the game binary.
|
|||||||
| File(s) | Source | License |
|
| 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/{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/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) |
|
| `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
|
backs, every audio file) are original work covered by this project's MIT
|
||||||
license.
|
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)
|
`LICENSE` file alongside the binary so the MIT (project + hayeah card art)
|
||||||
and OFL (FiraMono) notices remain visible to end users.
|
and OFL (FiraMono) notices remain visible to end users.
|
||||||
|
|||||||
@@ -6957,6 +6957,8 @@ dependencies = [
|
|||||||
"keyring",
|
"keyring",
|
||||||
"solitaire_data",
|
"solitaire_data",
|
||||||
"solitaire_engine",
|
"solitaire_engine",
|
||||||
|
"tiny-skia 0.12.0",
|
||||||
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6965,6 +6967,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"png 0.17.16",
|
"png 0.17.16",
|
||||||
|
"solitaire_core",
|
||||||
|
"solitaire_data",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6982,8 +6986,10 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"bevy",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"jni 0.21.1",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"keyring-core",
|
"keyring-core",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -7007,10 +7013,12 @@ dependencies = [
|
|||||||
"bevy",
|
"bevy",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"jni 0.21.1",
|
||||||
"kira",
|
"kira",
|
||||||
"resvg",
|
"resvg",
|
||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"solitaire_core",
|
"solitaire_core",
|
||||||
"solitaire_data",
|
"solitaire_data",
|
||||||
"solitaire_sync",
|
"solitaire_sync",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ keyring = "4"
|
|||||||
keyring-core = "1"
|
keyring-core = "1"
|
||||||
reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
|
reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
|
||||||
arboard = { version = "3", default-features = false }
|
arboard = { version = "3", default-features = false }
|
||||||
|
jni = { version = "0.21", default-features = false }
|
||||||
|
|
||||||
solitaire_core = { path = "solitaire_core" }
|
solitaire_core = { path = "solitaire_core" }
|
||||||
solitaire_sync = { path = "solitaire_sync" }
|
solitaire_sync = { path = "solitaire_sync" }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Solitaire Quest
|
# Ferrous Solitaire
|
||||||
|
|
||||||
A cross-platform Klondike Solitaire game written in Rust, with a card-theme
|
A cross-platform Klondike Solitaire game written in Rust, with a card-theme
|
||||||
system, full progression (XP / levels / achievements / daily challenges), and
|
system, full progression (XP / levels / achievements / daily challenges), and
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Solitaire Quest — Self-Hosting Guide
|
# Ferrous Solitaire — Self-Hosting Guide
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -42,3 +42,29 @@ git pull
|
|||||||
docker compose build
|
docker compose build
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Admin — Password Reset
|
||||||
|
|
||||||
|
If a player loses access to their account, the server binary includes a
|
||||||
|
built-in password reset command. Run it on the host (or inside the container)
|
||||||
|
with `DATABASE_URL` pointing at your database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive (prompts for the new password):
|
||||||
|
DATABASE_URL=sqlite://./data/solitaire.db \
|
||||||
|
./solitaire_server --reset-password <username>
|
||||||
|
|
||||||
|
# Non-interactive (piped from a script or password manager):
|
||||||
|
echo "new_password" | \
|
||||||
|
DATABASE_URL=sqlite://./data/solitaire.db \
|
||||||
|
./solitaire_server --reset-password <username>
|
||||||
|
|
||||||
|
# Inside a running Docker container:
|
||||||
|
docker compose exec server sh -c \
|
||||||
|
'echo "new_password" | ./solitaire_server --reset-password alice'
|
||||||
|
```
|
||||||
|
|
||||||
|
On success the user's `password_hash` is updated and **all active refresh
|
||||||
|
tokens are deleted**, so every open session must log in again with the new
|
||||||
|
password. `JWT_SECRET` does not need to be set for this command.
|
||||||
|
|||||||
@@ -1,668 +1,177 @@
|
|||||||
# Solitaire Quest — Session Handoff
|
# Ferrous Solitaire — Session Handoff
|
||||||
|
|
||||||
**Last updated:** 2026-05-08 — v0.20.0 cut and tagged at `41a009a`,
|
**Last updated:** 2026-05-12 — Leaderboard display name shipped (`03be4fc`). All commits pushed to origin.
|
||||||
all post-cut commits pushed to origin (HEAD = `dd101b3`), working
|
|
||||||
tree clean.
|
Phase 8 closes the self-hosted-server connection arc end-to-end: login/register
|
||||||
The cut itself shipped two through-lines: a full **Terminal visual-
|
modal, re-auth on token expiry, account deletion flow, server deployment
|
||||||
identity port** (token system, modal scaffold, gameplay-feedback,
|
artifacts (Dockerfile + docker-compose), replay upload on win, web replay
|
||||||
toasts, table / card chrome, splash cursor) and the **Android
|
player (WASM + HTML/CSS/JS served by the server), leaderboard opt-in/out,
|
||||||
persistence shim** that closes the `dirs::data_dir() = None` pitfall
|
and full server integration tests.
|
||||||
flagged in CLAUDE.md §10. Since the cut, the post-tag work split
|
|
||||||
into two arcs: (1) splash boot-screen port + replay-overlay
|
---
|
||||||
banner enrichments + desktop-adaptation spec — closing Resume-prompt
|
|
||||||
Options B and C (see "Since the v0.20.0 cut" entries below); and
|
## Current state
|
||||||
(2) **the card-face artwork regeneration arc — Option D, closed
|
|
||||||
2026-05-08** — full Terminal cards rendering on every face, plus
|
- **HEAD locally:** `03be4fc` (feat: leaderboard custom display name).
|
||||||
three follow-up fixes that surfaced during sign-off (default-theme
|
- **HEAD on origin:** `03be4fc` (fully pushed).
|
||||||
SVG override, table backgrounds, top-bar overlap), plus a
|
- **Working tree:** clean (only `solitaire-release.jks.bak2` untracked — intentional).
|
||||||
glyph-orientation tweak (no 180° inverted-corner rotation).
|
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings` clean.
|
||||||
|
- **Tests:** **1300+ passing / 0 failing** across the workspace.
|
||||||
## Status at pause
|
- **Tags on origin:** `v0.9.0` through `v0.22.0`.
|
||||||
|
|
||||||
- **HEAD locally:** see `git rev-parse HEAD`. Most recent narrative
|
---
|
||||||
entry below names the latest substantive commit; this status line
|
|
||||||
intentionally avoids hard-coding the SHA so a docs-only edit
|
## What shipped in Phase 8 (432061c – bd388fe)
|
||||||
doesn't immediately stale the handoff.
|
|
||||||
- **HEAD on origin:** matches local. All post-cut commits pushed
|
| Commit | Summary |
|
||||||
through `dd101b3`. Decide whether to roll the post-tag work
|
|--------|---------|
|
||||||
into v0.20.1 / v0.21.0-candidates the next time a release is cut.
|
| `432061c` | Sync setup modal (login/register/connect/disconnect) |
|
||||||
- **Working tree:** clean. No WIP outstanding.
|
| `6ce5564` | Re-auth on expired session + server deployment artifacts |
|
||||||
- **`artwork/` directory:** still untracked. Intentional.
|
| `272d31f` | Account deletion flow + `handle_sync_buttons` refactor |
|
||||||
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
|
| `bd388fe` | CHANGELOG v0.23.0 documentation |
|
||||||
clean.
|
|
||||||
- **Tests:** **1184 passing / 0 failing** across the workspace.
|
Also shipped (pre-Phase 8 but post-v0.22.0, already in CHANGELOG):
|
||||||
Net delta from the 1180 baseline: splash polish added two
|
- `solitaire_wasm` crate: WASM ReplayPlayer bindings for browser-side replay playback
|
||||||
(`build_scanline_image_has_expected_2x2_rgba_bytes`,
|
- Server replay API: `POST /api/replays`, `GET /api/replays/recent`, `GET /api/replays/:id`
|
||||||
`scanline_overlay_spawns_and_fades_with_splash`); the
|
- Server web UI: `/replays/:id` HTML route + `ServeDir /web` static assets
|
||||||
card-face migration added one (`card_face_svg_pin` integration
|
- DB migration 002: `replays` table + two indexes
|
||||||
test) and consolidated two (`face_colour` CBM tests folded
|
- Full server integration tests for replay endpoints
|
||||||
into `text_colour` CBM tests, net −2 then +1 from pin);
|
- `push_replay` in `sync_plugin` (uploads on win, writes share URL into replay history)
|
||||||
call it +4 net.
|
- Stats panel "Copy Share Link" button reads `share_url` from replay history
|
||||||
- **Tags on origin:** `v0.9.0` through `v0.20.0`. v0.20.0 is on
|
|
||||||
`41a009a`.
|
---
|
||||||
|
|
||||||
## Since the v0.20.0 cut (un-pushed)
|
## Open punch list (ordered by priority)
|
||||||
|
|
||||||
### `39b8496` `docs(ui): add Terminal desktop-adaptation spec`
|
### 1. Documentation debt (no code)
|
||||||
|
- [x] CHANGELOG [Unreleased] → v0.23.0 — done this session
|
||||||
`docs/ui-mockups/desktop-adaptation.md` — 283 lines covering
|
- [x] ARCHITECTURE.md update — all 8 gaps closed, bumped to v1.3
|
||||||
viewport assumptions, seven universal adaptation rules, and per-
|
- [x] SESSION_HANDOFF.md update — this file
|
||||||
screen geometry rules for the priority surfaces (Game Table, Win
|
|
||||||
Summary, Settings, Help, Pause, Home, Splash, Stats, and the
|
### 2. Leaderboard wiring gaps
|
||||||
modal-pattern screens Profile / Achievements / Theme Picker /
|
- [x] **Best-score auto-post.** Done (`303c78a`): `update_leaderboard_if_opted_in`
|
||||||
Daily Challenge). Closes the spec gap — 23 of 24 mockups were
|
called from both first-push and merge paths in `sync.rs`; uses SQLite `MIN`/`MAX`
|
||||||
mobile-only, but the v0.20.0 token-port pass was already layout-
|
in the UPDATE so scores never regress on stale data.
|
||||||
agnostic so nothing shipped broken. The spec matters for *next*
|
- [x] **Display name = username.** Done (`03be4fc`): `leaderboard_display_name:
|
||||||
ports.
|
Option<String>` added to `Settings`; editor modal in leaderboard panel; persists
|
||||||
|
to `settings.json`; `handle_opt_in_button` prefers custom name over username.
|
||||||
**Why rules > visual mockups for this gap:** Stitch's
|
|
||||||
`generate_variants` API timed out on the layout-only adaptation
|
### 3. Security hardening
|
||||||
prompt (server-side flake, not a prompt-shape issue — confirmed
|
- [x] **Refresh token rotation.** Done (`b129664`): `refresh_tokens` table
|
||||||
by polling `list_screens` with no new variant landing). A markdown
|
(migration 003); jti embedded in JWT; rotate-on-use pattern; 3 integration
|
||||||
rules file applies to every screen including the 9 missing-plugin
|
tests.
|
||||||
surfaces (splash, challenge, time-attack, weekly-goals,
|
- [x] **Sync endpoint rate limiting.** Done (`6e6f3ef`): `UserIdKeyExtractor`
|
||||||
leaderboard, sync, level-up, replay-overlay, radial-menu) that
|
decodes JWT for per-user identity; falls back to IP; burst 10 / 6 min
|
||||||
aren't in the Stitch project at all. It's also referenceable from
|
steady-state; integration test passes.
|
||||||
code comments and commit messages without loading an image.
|
|
||||||
|
### 4. Android validation
|
||||||
### `cacb19c` `feat(engine): port the splash to the Terminal boot-screen treatment`
|
- [x] **Android Keystore functional test.** Done (2026-05-11, Pixel 7 AVD,
|
||||||
|
Android 14): `load_access_token()` exercised via `start_pull`; logcat confirmed
|
||||||
Implements the full mockup-spec splash from
|
`NotFound` returned cleanly — no JNI panic. See `docs/android/PLAYABILITY_TODO.md` P4.
|
||||||
`docs/ui-mockups/splash-mobile.html` plus the desktop adaptation
|
- [x] **JNI clipboard functional test.** Done (2026-05-11): temporary `KEYCODE_C`
|
||||||
rules:
|
hook confirmed `ClipboardManager.setPrimaryClip()` succeeds on Android 14.
|
||||||
|
Hook reverted. Production path requires Interaction::Pressed + non-null `share_url`.
|
||||||
- **Header**: cursor block (96 px `▌`), wordmark ("Solitaire
|
Note: `adb shell input tap` doesn't deliver touch events on headless AVD (documented).
|
||||||
Quest"), 192 px divider, "TERMINAL EDITION" subtitle.
|
- [x] **`cargo apk build --lib` noisy stderr** — upstream cargo-apk bug; `--lib`
|
||||||
- **Boot log**: three ✓ check rows (`assets loaded`,
|
is the canonical command (CLAUDE.md §15.1, docs/ANDROID.md). No in-repo fix possible.
|
||||||
`theme: terminal`, `progress restored`) + a `▌ ready_` line.
|
|
||||||
Capped at 480 px width on desktop (else 70 % viewport).
|
### 5. Feature completeness
|
||||||
- **Progress bar**: 1 px track (`BORDER_SUBTLE`) with a 100 %-
|
- [x] **Theme importer UI.** Done (`613bbf8`): "Scan for new themes" button in
|
||||||
width cyan (`ACCENT_PRIMARY`) fill + `DONE · 247 ASSETS`
|
Settings Appearance section. Shows import path label, scans user_theme_dir()
|
||||||
caption. Capped at 720 px on desktop (else 80 %).
|
for .zip archives, fires InfoToastEvent per file, refreshes ThemeRegistry.
|
||||||
- **Footer**: `BASE16-EIGHTIES` label, eight palette swatches
|
- [x] **`mirror_achievement` removed.** Done (`549a817`): method was a no-op
|
||||||
(12 × 12 px each — one per named token in the design system),
|
default never overridden and never called; achievements already sync via
|
||||||
version line.
|
`SyncPayload` push. Deleted from trait and blanket impl.
|
||||||
|
- [x] **WASM build script.** Done (`40d0712`): `build_wasm.sh` at repo root
|
||||||
**Refactored the alpha-fade scaffold** from per-marker queries
|
documents `wasm-pack build --target web`, cleans up pkg metadata files,
|
||||||
(`SplashTitle` / `SplashSubtitle` / `SplashCursor`) to a single
|
includes dependency guard + install instructions.
|
||||||
`SplashFadable { base_color: Color }` + `SplashFadableBg`
|
- [x] **Server password reset.** Done (`7514684`): `--reset-password <username>`
|
||||||
variant. ~15 fadable elements share one global query each;
|
subcommand reads new password from stdin, bcrypt-hashes it, invalidates all
|
||||||
adding more is one component-attach, not three new query types.
|
active sessions for the user.
|
||||||
|
|
||||||
**Skipped, with rationale captured in the commit:**
|
### 5b. Android UX polish (2026-05-12)
|
||||||
- Scanline overlay (needs a tiled-pattern asset or custom shader).
|
|
||||||
*Open in "Visual-identity follow-ups" below.*
|
- [x] **UX-1 — Modal Done button in gesture zone.** `apply_safe_area_to_modal_scrims` system
|
||||||
- Pulsing cursor on the "ready_" line (would fight the global
|
added to `SafeAreaInsetsPlugin` (`safe_area.rs`). Pads every `ModalScrim` bottom by
|
||||||
fade timeline). *Open in "Visual-identity follow-ups" below.*
|
`insets.bottom / scale`. Fires on resource change + `Added<ModalScrim>`. Verified on device.
|
||||||
- "RUSTY SOLITAIRE" wordmark from the mockup (the actual product
|
- [x] **UX-5b — Home mode glyph corruption.** Geometric Shapes (U+25xx, absent from FiraMono)
|
||||||
is "Solitaire Quest"; the mockup leaked the repo name). *Closed
|
replaced with card suits U+2660–2666 in `home_plugin.rs`. Affects Zen/Challenge/Daily mode
|
||||||
— the in-engine wordmark stays "Solitaire Quest".*
|
selector buttons at level 5+.
|
||||||
|
- [x] **UX-7 — Help text wrap.** Android HUD entry shortened to
|
||||||
### `c84d9f4` `feat(engine): scrub fill bar + per-frame updater for replay overlay`
|
`"Open menu (Stats, Settings, Profile...)"` in `help_plugin.rs` — fits one line.
|
||||||
|
- [x] **BUG-3 — Multi-modal stacking.** `handle_menu_button` now checks
|
||||||
Closes the WIP described in the prior handoff. Adds the 1 px cyan
|
`scrims: Query<(), With<ModalScrim>>` and guards `spawn_menu_popover` with `scrims.is_empty()`.
|
||||||
scrub bar called for in `docs/ui-mockups/replay-overlay-mobile.html`:
|
Verified on device: ≡ tap while Stats open does nothing.
|
||||||
a track in `BORDER_SUBTLE` spans the bottom edge of the banner and
|
|
||||||
the cyan `ACCENT_PRIMARY` fill mirrors `cursor / total` via a new
|
**Note:** These 4 fixes are implemented and verified but not yet committed.
|
||||||
`ReplayOverlayScrubFill` component + `update_scrub_fill` system.
|
|
||||||
The pure `scrub_pct` helper is shared between the spawn path
|
### 6. Testing gaps
|
||||||
(initial fill width) and the per-frame updater so the first paint
|
- [x] **Server 401 → refresh → retry path.** Done (`198df75`): both
|
||||||
already reflects state instead of popping `0 → cursor` on the
|
`jwt_refresh_on_401_succeeds` (pull) and
|
||||||
first tick — same shape as the existing `format_progress` /
|
`push_retries_after_401_on_expired_access_token` (push) in
|
||||||
`update_progress_text` split. Two new tests cover the four corners
|
`solitaire_data/tests/sync_round_trip.rs`.
|
||||||
of `scrub_pct` and an end-to-end drive of `ReplayPlaybackState`
|
- [x] **WASM winning-replay step-through.** Done (`b4ada2a`): greedy solver
|
||||||
asserting `Node.width` on the unique scrub-fill entity. Same
|
searches seeds 1–200 at test time; steps every move through `ReplayPlayer`;
|
||||||
change-detection guard as the text updaters, so an idle replay
|
asserts `is_won = true` on the final `StateSnapshot`.
|
||||||
leaves the node untouched.
|
|
||||||
|
---
|
||||||
Header text treatment (closed by `6204db8` immediately below),
|
|
||||||
move-log scroll, MOVE chip, and WIN MOVE callout from the same
|
## ARCHITECTURE.md gaps (for the update pass)
|
||||||
mockup are still open — separate commits.
|
|
||||||
|
Items missing from the doc:
|
||||||
### `6204db8` `feat(engine): port replay banner label to ▌ cursor-block treatment`
|
1. `solitaire_wasm` crate (§2 workspace + §3 responsibilities)
|
||||||
|
2. Replay API endpoints (§9 API Reference — 3 new routes)
|
||||||
Aligns the replay overlay's headline with the splash boot-screen
|
3. Web replay player route (`/replays/:id` + `ServeDir /web`)
|
||||||
idiom landed in `cacb19c`: `Replay` → `▌ replay` and
|
4. `SyncProvider` trait: 6 added methods
|
||||||
`Replay complete` → `▌ replay complete`. The cursor block (`▌`,
|
5. Theme system in Bevy plugin table (§5)
|
||||||
U+258C) prefixed to a lowercased label reads as a Terminal output
|
6. `Settings` new fields: `color_blind_mode`, `high_contrast_mode`,
|
||||||
line rather than a generic UI title, tightening the family
|
`reduce_motion_mode`, `window_geometry`, `selected_card_back`,
|
||||||
resemblance between the two top-level overlay surfaces. Pure
|
`selected_background`
|
||||||
text-content change; no behavioural shift, no new components, no
|
7. DB migration 002 (§7)
|
||||||
new systems.
|
8. Update "Last Updated" date
|
||||||
|
|
||||||
**Mockup deviation (intentional):** the source mockup string in
|
---
|
||||||
`docs/ui-mockups/replay-overlay-mobile.html` is `▌replay.tsx`. The
|
|
||||||
`.tsx` is a prototyping leak — Stitch renders in React, so the
|
## Process notes
|
||||||
mockup author reached for a familiar filename — and was dropped
|
|
||||||
for the in-engine version since the codebase is Rust. The `▌` +
|
- **Commit attribution:** use `funman300` as git user. Co-author line:
|
||||||
lowercase pattern is what reads as a Terminal-output-line; the
|
`Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>`.
|
||||||
extension is incidental. (Same shape as the "RUSTY SOLITAIRE"
|
- **Commit format:** `type(scope): description` per CLAUDE.md §7.
|
||||||
wordmark deviation noted under `cacb19c` — the mockup leaked the
|
- **Never commit without:** `cargo test --workspace` passing + clippy clean.
|
||||||
repo name; the actual product is "Solitaire Quest".)
|
- **Sub-agents** stage/verify only; orchestrator commits.
|
||||||
|
- **`CARD_PLAN.md`** referenced in `theme/` module comments but not present in
|
||||||
### `54005d5` `feat(engine): add GAME #YYYY-DDD caption beneath the replay headline`
|
repo. Clean up references or commit the file.
|
||||||
|
- **Token-port pattern** (v0.20.0): when migrating tokens, walk every concrete
|
||||||
Adds the right-anchored game-identifier piece of the replay-overlay
|
artifact downstream — PNGs, SVGs, literals, comments. Three "walked past this"
|
||||||
mockup, adapted to live *under* the existing "▌ replay" headline as
|
follow-ups in v0.21.0 all had this shape.
|
||||||
a `TYPE_CAPTION` (11 px) / `TEXT_SECONDARY` subtitle. Format is
|
|
||||||
`GAME #{year}-{ordinal:03}` (e.g. `GAME #2026-122` for a replay
|
---
|
||||||
recorded 2026-05-02) — year + chrono ordinal gives a compact,
|
|
||||||
monotonically-increasing identifier matching the mockup's
|
|
||||||
`GAME #2024-127` motif. New `ReplayOverlayGameCaption` marker, new
|
|
||||||
pure helper `format_game_caption(state) -> Option<String>` (None
|
|
||||||
for Inactive / Completed since the replay is consumed in those
|
|
||||||
branches; spawn-time fall-through to empty string).
|
|
||||||
|
|
||||||
**Layout impact:** `BANNER_HEIGHT` bumped 48 → 60 px so the new
|
|
||||||
left column (headline + 2 px gap + caption ≈ 39 px content) fits
|
|
||||||
under the scrub bar with room to spare. +12 px banner mass is the
|
|
||||||
deliberate cost of the new content; no other plugin observes
|
|
||||||
`BANNER_HEIGHT` so the change is local.
|
|
||||||
|
|
||||||
Two new tests (1180 → 1182): `format_game_caption_covers_state_corners`
|
|
||||||
pins the three branches plus the zero-pad-to-3-digits invariant
|
|
||||||
for early-January ordinals; `overlay_game_caption_shows_replay_date`
|
|
||||||
drives `ReplayPlaybackState` end-to-end.
|
|
||||||
|
|
||||||
### `e080b49` `feat(engine): restyle replay progress text as Terminal MOVE chip`
|
|
||||||
|
|
||||||
Closes the centre-text half of the replay-overlay enrichments. The
|
|
||||||
plain "Move N of M" text becomes a 1px `ACCENT_PRIMARY`-bordered
|
|
||||||
chip containing "MOVE N/M" — uppercase + slash separator reads as
|
|
||||||
a Terminal output line and matches the floating-chip motif in
|
|
||||||
`docs/ui-mockups/replay-overlay-mobile.html`. The chip lives
|
|
||||||
in-banner rather than floating above the focused card (the
|
|
||||||
screen-takeover treatment that requires plumbing cursor → card
|
|
||||||
identity remains deferred).
|
|
||||||
|
|
||||||
**Implementation note:** `BorderColor` in Bevy 0.18 is a per-side
|
|
||||||
struct, not a tuple — `BorderColor::all(ACCENT_PRIMARY)` is the
|
|
||||||
correct constructor. Worth pinning for next time we touch a
|
|
||||||
border-painted UI surface. The `ReplayOverlayProgressText` marker
|
|
||||||
stays on the inner Text rather than the new chip Node so
|
|
||||||
`update_progress_text` keeps repainting unchanged — a deliberate
|
|
||||||
"markers belong on the entity that updates change" choice.
|
|
||||||
|
|
||||||
Test count unchanged (1182); `overlay_progress_text_reflects_cursor`
|
|
||||||
swapped its assertion from "Move 5 of 10" to "MOVE 5/10".
|
|
||||||
|
|
||||||
This pair (`54005d5` + `e080b49`) closes Option C from the
|
|
||||||
SESSION_HANDOFF Resume prompt's banner-local enrichments. Floating-
|
|
||||||
chip-above-focused-card and the full screen-takeover redesign
|
|
||||||
remain — both data-layer or cross-plugin and intentionally still
|
|
||||||
open.
|
|
||||||
|
|
||||||
### `29136d8` `feat(engine): add pulsing trailing cursor to splash "▌ ready_" line`
|
|
||||||
|
|
||||||
Closes the cursor-pulse half of the splash polish arc deferred in
|
|
||||||
`cacb19c`. The "▌ ready_" line now ends with a 6×12 px cyan Node
|
|
||||||
that pulses on a 1 s sine cadence, multiplied with the global
|
|
||||||
splash fade timeline so the cursor never reaches full alpha while
|
|
||||||
the rest of the splash is still fading in.
|
|
||||||
|
|
||||||
**The "multiply, don't override" pattern.** Two systems write the
|
|
||||||
same `BackgroundColor` per frame: `advance_splash` writes the
|
|
||||||
global-fade alpha, `pulse_splash_cursor` overwrites with
|
|
||||||
`global_alpha × pulse_factor`. Both derive from `SplashAge` on the
|
|
||||||
root, so the writes are commensurate — the second one isn't
|
|
||||||
"fighting" the first, just refining it. This is the cleanest fix
|
|
||||||
for the "fight the global fade timeline" warning the original
|
|
||||||
`cacb19c` skip note flagged.
|
|
||||||
|
|
||||||
**Defensive division guard.** `cursor_pulse_factor(age, period, min)`
|
|
||||||
short-circuits to `1.0` when `period <= 0.0` so a future
|
|
||||||
misconfiguration produces a steady cursor rather than NaN
|
|
||||||
propagation (NaN in alpha = invisible UI, hard to debug). Worth
|
|
||||||
mirroring on every trig/division helper, not just this one.
|
|
||||||
|
|
||||||
One new test (1182 → 1183): `cursor_pulse_factor_corners` pins the
|
|
||||||
peak (factor = 1 at age = period / 4), trough (factor = min at age =
|
|
||||||
period × 3 / 4), and the zero/negative-period guard.
|
|
||||||
|
|
||||||
### `a27cf5a` `feat(engine): add tiled scanline overlay to splash`
|
|
||||||
|
|
||||||
Closes the scanline half of the splash polish arc. A fullscreen
|
|
||||||
`ImageNode` tiles a runtime-generated 2×2 RGBA8 texture over the
|
|
||||||
splash content — top row transparent, bottom row `#1a1a1a` at
|
|
||||||
~30 % alpha — producing the 1 px-pitch horizontal scanline pattern
|
|
||||||
called for in `docs/ui-mockups/splash-mobile.html`.
|
|
||||||
|
|
||||||
**Texture-α × tint-α composite for fade integration.** The 30 %
|
|
||||||
alpha is baked into the texture pixels, not the `ImageNode.color`
|
|
||||||
tint. `advance_splash`'s new third query writes
|
|
||||||
`(1, 1, 1, global_alpha)` into the tint each tick; the GPU
|
|
||||||
multiplies texture-α by tint-α, so the visible composite is
|
|
||||||
`0.3 × global_alpha`. Cleaner than building a "multiplicative
|
|
||||||
fadable" abstraction in the ECS — the GPU already does this
|
|
||||||
multiplication for free.
|
|
||||||
|
|
||||||
**Bevy 0.18 API surprises (worth pinning):**
|
|
||||||
- `RenderAssetUsages` re-exports under `bevy::asset::`, not
|
|
||||||
`bevy::render::render_asset::`. Type name unchanged; module
|
|
||||||
path moved.
|
|
||||||
- `TextureFormat::pixel_size()` returns `Result<usize, _>` rather
|
|
||||||
than the bare `usize` you'd expect for a static format query.
|
|
||||||
Annoying enough that the `debug_assert_eq!` against the buffer
|
|
||||||
length just hard-codes the `2 × 2 × 4 = 16` literal.
|
|
||||||
|
|
||||||
Headless test fixture now also `init_resource::<Assets<Image>>()`
|
|
||||||
since `MinimalPlugins` doesn't pull `AssetPlugin` — same pattern
|
|
||||||
`settings_plugin::tests` already used. Without it, the
|
|
||||||
`Option<ResMut<Assets<Image>>>` parameter on `spawn_splash` would
|
|
||||||
fall through and the scanline overlay would silently skip,
|
|
||||||
defeating the new tests.
|
|
||||||
|
|
||||||
Two new tests (1183 → 1185):
|
|
||||||
`build_scanline_image_has_expected_2x2_rgba_bytes` locks the
|
|
||||||
texture pixels literally so a future tweak can't drift the
|
|
||||||
appearance silently; `scanline_overlay_spawns_and_fades_with_splash`
|
|
||||||
asserts spawn placement under `SplashRoot` and the new
|
|
||||||
fade-images branch's correctness end-to-end.
|
|
||||||
|
|
||||||
This pair (`29136d8` + `a27cf5a`) closes Option B from the
|
|
||||||
SESSION_HANDOFF Resume prompt — both splash polish pieces now
|
|
||||||
shipped.
|
|
||||||
|
|
||||||
### `5623368`…`dd101b3` — Option D card-face migration arc
|
|
||||||
|
|
||||||
Closed 2026-05-08 across nine commits. The full Terminal card
|
|
||||||
artwork now renders end-to-end. Detail breakdown lives in the
|
|
||||||
"Visual-identity follow-ups" punch-list entry below; the short
|
|
||||||
version:
|
|
||||||
|
|
||||||
- Migration plan + pipeline tooling: `5623368` (plan doc),
|
|
||||||
`3a4bb63` (single-card PoC proving the `usvg`/`resvg` pipeline
|
|
||||||
at per-card grain), `babe5cc` (full
|
|
||||||
`solitaire_engine/examples/card_face_generator.rs` example
|
|
||||||
emitting 52 faces + 5 backs into `assets/cards/`), `48b28d2`
|
|
||||||
(the `card_face_svg_pin` integration test pinning rasteriser
|
|
||||||
output via inline FNV-1a hashing of raw RGBA8 bytes — the
|
|
||||||
pin's bootstrap pattern, "empty `EXPECTED` → run → paste",
|
|
||||||
is the maintenance interface for future intentional changes).
|
|
||||||
- Lockstep step 4+5: `e8bf9d7`. New PNG bytes + the 5
|
|
||||||
`card_plugin` constants (`CARD_FACE_COLOUR`,
|
|
||||||
`RED_SUIT_COLOUR`, `BLACK_SUIT_COLOUR`,
|
|
||||||
`CARD_FACE_COLOUR_RED_CBM` → `RED_SUIT_COLOUR_CBM`,
|
|
||||||
`card_back_colour`) + signature shifts in one commit.
|
|
||||||
`face_colour` deleted — Terminal face is uniformly
|
|
||||||
`CARD_FACE_COLOUR` regardless of CBM, so the function
|
|
||||||
collapsed to a constant. `text_colour` gained a
|
|
||||||
`color_blind: bool` parameter (red→cyan suit-glyph swap when
|
|
||||||
CBM is on). Four `face_colour` CBM tests folded into two
|
|
||||||
`text_colour` CBM tests in lockstep.
|
|
||||||
- Three follow-ups that surfaced during sign-off, all from the
|
|
||||||
same "fallback path the migration walked past" pattern:
|
|
||||||
`a14200a` regenerated the embedded **default-theme SVGs** at
|
|
||||||
`solitaire_engine/assets/themes/default/*.svg`; those bytes
|
|
||||||
`include_bytes!()`-embed into the binary and override
|
|
||||||
`assets/cards/*.png` at startup, so the PNG migration alone
|
|
||||||
didn't change what production rendered. `8719f77`
|
|
||||||
regenerated `assets/backgrounds/bg_*.png` to flat Terminal
|
|
||||||
near-black (5 solid-colour PNGs via a new
|
|
||||||
`solitaire_engine/examples/background_generator.rs` example).
|
|
||||||
`ae84dc1` cleared the **top-bar overlap** at portrait/narrow
|
|
||||||
window widths by swapping the action-button row's hardcoded
|
|
||||||
`font_size: 16.0` to `TYPE_BODY` (a typography-migration
|
|
||||||
miss) and stepping horizontal padding from `VAL_SPACE_3`
|
|
||||||
to `VAL_SPACE_2`.
|
|
||||||
- Glyph-rendering fix: `af414b6`. The bundled `FiraMono`
|
|
||||||
doesn't carry usable U+2660-2666 glyphs at the requested
|
|
||||||
size — `usvg` was silently substituting tiny "tofu" marks.
|
|
||||||
Switched suit glyphs from `<text>` elements to inline SVG
|
|
||||||
`<path>` elements via a new `suit_path_d` helper. Path-based
|
|
||||||
rendering bypasses the font system entirely; same bytes on
|
|
||||||
every machine, no fontdb dependency, no substitution risk.
|
|
||||||
Same path data renders correctly whether filled (♥ ♠) or
|
|
||||||
outlined (♦ ♣ — the always-on color-blind glyph
|
|
||||||
differentiation).
|
|
||||||
- Glyph-orientation tweak: `dd101b3`. Removed the 180° rotation
|
|
||||||
from the bottom-right large suit glyph at user request. Both
|
|
||||||
glyphs now render upright. `design-system.md` § Game Cards
|
|
||||||
line 220 updated in lockstep — the deliberate deviation from
|
|
||||||
the traditional inverted-corner-indicator convention is
|
|
||||||
documented in the spec, not just the code.
|
|
||||||
|
|
||||||
The pin test fired exactly twice during this arc (once for the
|
|
||||||
text→path switch, once for the unrotation) and rebaselined
|
|
||||||
cleanly each time via the empty-then-paste pattern. The 5
|
|
||||||
`back_*` hashes stayed identical across both rebaselines —
|
|
||||||
secondary signal that the FNV-1a fingerprinting is purely
|
|
||||||
deterministic on rasteriser output.
|
|
||||||
|
|
||||||
This arc closes Option D from the SESSION_HANDOFF Resume prompt
|
|
||||||
and effectively completes the Terminal visual-identity port —
|
|
||||||
only the toast warning/error variant slots remain wired-but-
|
|
||||||
unused.
|
|
||||||
|
|
||||||
## What shipped in v0.20.0 (frozen at `41a009a`)
|
|
||||||
|
|
||||||
### Terminal visual-identity port
|
|
||||||
|
|
||||||
Top-down stack — every commit downstream of the token system
|
|
||||||
reads from it, so swapping the palette is now a one-file edit:
|
|
||||||
|
|
||||||
- **`ui_theme` token system** (`0d477ac`). base16-eighties
|
|
||||||
palette, 5-rung type scale, 7-rung 4-multiple spacing scale,
|
|
||||||
3-step radius, 14-rung z-index hierarchy, full motion budget,
|
|
||||||
4 invariant-pinning unit tests. Card-shadow alphas pinned to 0
|
|
||||||
(Terminal achieves depth via 1px borders + tonal layering).
|
|
||||||
- **Modal scaffold already on tokens** — `ui_modal` was ported
|
|
||||||
in the same commit's wake; three stale "loud yellow" /
|
|
||||||
"magenta secondary" doc comments fixed.
|
|
||||||
- **Gameplay feedback → semantic state tokens** (`ceec4fc`).
|
|
||||||
Selection / valid-drop tints route through `ACCENT_PRIMARY` /
|
|
||||||
`STATE_WARNING` / `STATE_SUCCESS`.
|
|
||||||
- **Toasts** (`a137607`). New `ToastVariant` enum
|
|
||||||
(Info / Warning / Error / Celebration); opaque `BG_ELEVATED`
|
|
||||||
+ 1px accent border + bottom-anchor. All ten call sites pass
|
|
||||||
their semantic variant.
|
|
||||||
- **`table_plugin` chrome** (`651f406`).
|
|
||||||
`PILE_MARKER_DEFAULT_COLOUR` promoted; `cursor_plugin` imports
|
|
||||||
it, replacing a "kept in sync" doc comment with a compile-
|
|
||||||
enforced invariant. `HINT_PILE_HIGHLIGHT_COLOUR` →
|
|
||||||
`STATE_WARNING`.
|
|
||||||
- **`card_plugin` chrome** (`d752870`). Drag-elevation shadow
|
|
||||||
routes through `CARD_SHADOW_*` tokens. `RIGHT_CLICK_HIGHLIGHT_COLOUR`
|
|
||||||
→ `STATE_SUCCESS`. Stock recycle "↺" text → `TEXT_PRIMARY @ 0.7α`.
|
|
||||||
Card-face / suit / card-back palette intentionally NOT migrated
|
|
||||||
(artwork dependency — see open-list item below).
|
|
||||||
- **Splash cursor** (`cdcadda`). The signature `▌` cyan glyph
|
|
||||||
(96 px) added above the wordmark, matching the spec.
|
|
||||||
*Subsequently expanded post-cut by `cacb19c` into the full
|
|
||||||
boot-screen treatment.*
|
|
||||||
- **Hint-source / dest pairing** (`9891ae4`). `input_plugin`'s
|
|
||||||
source-card tint now matches the destination pile's
|
|
||||||
`STATE_WARNING`.
|
|
||||||
- **Design system + 24-mockup library** (`fa7f98a`).
|
|
||||||
`docs/ui-mockups/design-system.md` + 24 Stitch mockups (HTML +
|
|
||||||
PNG) covering every screen plus 9 missing-plugin surfaces.
|
|
||||||
- **`card_shadow_params` test aligned** (`1d1543e`). Drag-vs-
|
|
||||||
idle shadow assertion loosened to `>=` to accept the Terminal
|
|
||||||
"no shadow" intent without losing the regression-guard.
|
|
||||||
|
|
||||||
### Android persistence
|
|
||||||
|
|
||||||
- **`solitaire_data::data_dir` shim** (`4b51e50`). New
|
|
||||||
`solitaire_data::platform::data_dir()` falls through to
|
|
||||||
`dirs::data_dir()` on desktop and returns the per-app sandbox
|
|
||||||
at `/data/data/com.solitairequest.app/files` on Android — no
|
|
||||||
JNI needed (package id pinned in `[package.metadata.android]`).
|
|
||||||
Six `solitaire_data` callsites + `solitaire_engine/assets/user_dir.rs`
|
|
||||||
migrated. Settings, stats, achievements, replays, game-state,
|
|
||||||
time-attack sessions, and user themes now persist on Android.
|
|
||||||
|
|
||||||
### Inherited from earlier in the cycle (pre-session)
|
|
||||||
|
|
||||||
- Android build target + APK (`fb8b2ac`), runbook (`59424a3`),
|
|
||||||
F3 FPS overlay (`690e1d2`), Smart Window Size opt-out
|
|
||||||
(`e1b8766`), Shareable badge (`9b065e5`), Help cheat-sheet
|
|
||||||
M/P/Enter rows (`35516d3`), `pull_failure_sets_error_status`
|
|
||||||
flake fix (`67c150b`).
|
|
||||||
|
|
||||||
## Open punch list
|
|
||||||
|
|
||||||
### Phase Android (build + persistence shipped; runtime gaps remain)
|
|
||||||
|
|
||||||
- **APK launch verification on AVD / device.** `adb install` then
|
|
||||||
`adb logcat` against the `bevy_test` AVD or an x86_64 device.
|
|
||||||
The build works and persistence is wired, but no end-to-end
|
|
||||||
device run has been logged. Shakes out runtime bugs the build +
|
|
||||||
unit tests can't catch.
|
|
||||||
- **JNI ClipboardManager bridge.** Replaces the Android stub for
|
|
||||||
the Stats "Copy share link" toast. `arboard` doesn't ship an
|
|
||||||
Android backend; small custom JNI call.
|
|
||||||
- **Android Keystore for credentials.** `keyring` is target-gated
|
|
||||||
to a stub returning `KeychainUnavailable`; replace with Android
|
|
||||||
Keystore via JNI when sync auth ships on mobile.
|
|
||||||
- **Google Play Games (gpgs) integration.** Listed as a
|
|
||||||
Phase-Android target since Phase 1; now unblocked by the build
|
|
||||||
target.
|
|
||||||
- **Cosmetic `cargo apk build --lib` workaround.** Post-sign
|
|
||||||
panic doesn't affect the APK on disk but produces noisy stderr.
|
|
||||||
Either upstream a cargo-apk fix or document `--lib` as
|
|
||||||
canonical in the runbook.
|
|
||||||
|
|
||||||
### Visual-identity follow-ups (opened by v0.20.0's port)
|
|
||||||
|
|
||||||
- *Card-face / suit / card-back artwork regeneration — closed
|
|
||||||
2026-05-08 by the commit chain `5623368` → `dd101b3`.* The
|
|
||||||
Terminal spec called for dark `#1a1a1a` cards with light suit
|
|
||||||
pips (pink for hearts/diamonds, foreground gray for spades/
|
|
||||||
clubs). Closed across nine commits over two arcs:
|
|
||||||
- **Plan + tooling (`5623368`–`48b28d2`):** migration plan
|
|
||||||
doc, single-card PoC, full `card_face_generator` example
|
|
||||||
(52 faces + 5 backs into `assets/cards/`), and the
|
|
||||||
`card_face_svg_pin` integration test pinning rasteriser
|
|
||||||
output via FNV-1a so future `usvg`/`resvg` upgrades surface
|
|
||||||
as test failures rather than silent visual drift.
|
|
||||||
- **Lockstep step 4+5 (`e8bf9d7`):** PNGs + the 5 `card_plugin`
|
|
||||||
constants + signature shifts in one commit.
|
|
||||||
`CARD_FACE_COLOUR_RED_CBM` renamed to `RED_SUIT_COLOUR_CBM`
|
|
||||||
and repurposed from a face-tint to a suit-glyph swap (the
|
|
||||||
Terminal face is uniform `CARD_FACE_COLOUR` regardless of
|
|
||||||
CBM; CBM only swaps red suits to cyan in the glyph itself).
|
|
||||||
`face_colour` deleted, `text_colour` gained a `color_blind`
|
|
||||||
parameter.
|
|
||||||
- **Three follow-ups that surfaced during sign-off:**
|
|
||||||
`a14200a` regenerated the **default-theme SVGs** at
|
|
||||||
`solitaire_engine/assets/themes/default/*.svg` — those
|
|
||||||
`include_bytes!()`-embed into the binary and override
|
|
||||||
`assets/cards/*.png` at runtime, so the PNG migration alone
|
|
||||||
didn't change what production rendered. `8719f77`
|
|
||||||
regenerated `assets/backgrounds/bg_*.png` to flat Terminal
|
|
||||||
near-black (5 solid-colour PNGs via a new
|
|
||||||
`background_generator` example). `ae84dc1` cleared the
|
|
||||||
**top-bar overlap** at portrait/narrow window widths by
|
|
||||||
swapping the action-button row's hardcoded `font_size: 16.0`
|
|
||||||
to `TYPE_BODY` and stepping horizontal padding from
|
|
||||||
`VAL_SPACE_3` to `VAL_SPACE_2`.
|
|
||||||
- **Glyph-rendering fix (`af414b6`):** suit glyphs render as
|
|
||||||
inline SVG paths (not `<text>`) because the bundled
|
|
||||||
`FiraMono` doesn't carry usable U+2660-2666 at the
|
|
||||||
requested size — `usvg` was silently substituting tiny
|
|
||||||
"tofu" marks. Path-based rendering bypasses the font system
|
|
||||||
entirely; same bytes on every machine. The pin test
|
|
||||||
rebaselined cleanly via the empty-then-paste pattern.
|
|
||||||
- **Glyph-orientation tweak (`dd101b3`):** removed the 180°
|
|
||||||
rotation from the bottom-right large suit glyph at user
|
|
||||||
request — both glyphs now render in the same upright
|
|
||||||
orientation. `design-system.md` § Game Cards line 220
|
|
||||||
updated in lockstep to document the deliberate deviation
|
|
||||||
from the traditional inverted-corner-indicator convention.
|
|
||||||
- *Splash boot-loader scanline overlay — closed by `a27cf5a`.*
|
|
||||||
Runtime-generated 2 × 2 RGBA8 texture tiled via
|
|
||||||
`NodeImageMode::Tiled`; per-pixel alpha × tint alpha gives
|
|
||||||
multiplicative fade integration without new abstractions.
|
|
||||||
- *Splash cursor pulse — closed by `29136d8`.* Trailing 6 × 12 px
|
|
||||||
cyan Node, sine-pulsed, multiplied with the global splash fade
|
|
||||||
(the "multiply, don't override" pattern that resolves the
|
|
||||||
original `cacb19c` skip-rationale).
|
|
||||||
- **Replay-overlay enrichments beyond the scrub bar.** Banner-local
|
|
||||||
pieces of the mockup (`docs/ui-mockups/replay-overlay-mobile.html`)
|
|
||||||
all shipped: scrub bar (`c84d9f4`), `▌ replay` cursor-block label
|
|
||||||
(`6204db8`), `GAME #YYYY-DDD` caption (`54005d5`), `MOVE N/M`
|
|
||||||
chip restyle (`e080b49`). What's still open are the cross-plugin
|
|
||||||
/ data-layer pieces: a `MOVE N/M` chip *floating above the
|
|
||||||
focused card* during playback (would need to thread the cursor
|
|
||||||
through to the card layer — `update_progress_text` writes the
|
|
||||||
banner chip but the card-position lookup belongs in `card_plugin`).
|
|
||||||
The full mockup's screen-takeover treatment — mini-tableau
|
|
||||||
preview, playback controls, move-log scroll, WIN MOVE marker on
|
|
||||||
the scrub bar — is a multi-session redesign with
|
|
||||||
data-layer impact (move-log scroller; the WIN MOVE marker
|
|
||||||
needs a `win_move_index` field on `Replay` that doesn't yet
|
|
||||||
exist). Banner-overlay behaviour is intentionally preserved
|
|
||||||
for now.
|
|
||||||
- **Toast Warning / Error variants.** The `ToastVariant` enum
|
|
||||||
has slots for `Warning` (gold) and `Error` (pink) but no
|
|
||||||
in-engine event uses them yet. Wire when a warning- or error-
|
|
||||||
flavoured toast event materialises.
|
|
||||||
|
|
||||||
### Carried forward from v0.19.0
|
|
||||||
|
|
||||||
- **App icon round.** `Window::icon` not yet wired; no
|
|
||||||
`.icns` / `.ico` / Linux hicolor PNG hierarchy. The 11-size
|
|
||||||
icon export the v0.19 handoff referenced is *not* currently
|
|
||||||
in `artwork/` (current `artwork/` holds the reverted Rusty
|
|
||||||
Pixel card PNGs and is intentionally untracked); icon-export
|
|
||||||
needs to be re-run before this item can be picked up.
|
|
||||||
Half-day task once the PNGs are back in place. No cert
|
|
||||||
dependency.
|
|
||||||
|
|
||||||
### Other small candidates
|
|
||||||
|
|
||||||
- **Prev/Next selector chips spawn site.** v0.19.0's `9b065e5`
|
|
||||||
noted Prev/Next markers exist in `stats_plugin` but no spawn
|
|
||||||
site renders them today — the Shareable badge therefore lands
|
|
||||||
on the single-replay caption. If/when Prev/Next is plumbed,
|
|
||||||
the badge will need to follow.
|
|
||||||
- **Toast queue / immediate unification.** The two toast paths
|
|
||||||
(`spawn_queued_toast` for `InfoToastEvent` queue; `spawn_toast`
|
|
||||||
for fire-and-forget) now share visual treatment but remain
|
|
||||||
separate functions because they serve different temporal
|
|
||||||
needs (sequential vs. parallel). If overlap becomes a UX
|
|
||||||
issue, merge into one queue with priority lanes.
|
|
||||||
|
|
||||||
### Process notes
|
|
||||||
|
|
||||||
- **The desktop-adaptation spec is the canonical reference for
|
|
||||||
geometry decisions** when porting any future plugin. Read
|
|
||||||
`docs/ui-mockups/desktop-adaptation.md` first; apply the
|
|
||||||
universal rules to every surface; consult the per-screen
|
|
||||||
table for the priority surfaces. The 9 missing-plugin screens
|
|
||||||
(splash now ported; eight remaining) inherit the universal
|
|
||||||
rules without dedicated guidance.
|
|
||||||
- **Stitch `generate_variants` is unreliable for layout-only
|
|
||||||
adaptation prompts** as of 2026-05-07. The first call timed
|
|
||||||
out and no variant ever landed in `list_screens`. If a future
|
|
||||||
session wants visual desktop mockups, prefer
|
|
||||||
`generate_screen_from_text` with a fresh narrow prompt per
|
|
||||||
screen rather than `generate_variants` against existing
|
|
||||||
mobile screens.
|
|
||||||
- **Token-port pattern.** v0.20.0's chrome-migration commits
|
|
||||||
set a reusable shape for "centralised design system applied
|
|
||||||
across N plugins":
|
|
||||||
1. Constants module (`ui_theme.rs`) is the source of truth.
|
|
||||||
2. Const sites that can't call `Alpha::with_alpha` (not yet
|
|
||||||
`const` on stable) use a literal RGB matching the token,
|
|
||||||
with a unit test pinning the RGB to the token (e.g.
|
|
||||||
`MARKER_VALID`, `HINT_PILE_HIGHLIGHT_COLOUR`,
|
|
||||||
`RIGHT_CLICK_HIGHLIGHT_COLOUR`).
|
|
||||||
3. Cross-plugin duplication (e.g. `MARKER_DEFAULT` ↔
|
|
||||||
`PILE_MARKER_DEFAULT_COLOUR`) collapses to a single
|
|
||||||
promoted const re-exported from one plugin and imported
|
|
||||||
by the other — replaces "kept in sync" doc comments with a
|
|
||||||
compile-time invariant.
|
|
||||||
4. Domain colours (suit pips, card faces, lerp helpers) stay
|
|
||||||
as literals with a comment naming the rationale; only UI
|
|
||||||
chrome routes through tokens.
|
|
||||||
- **`SplashFadable` scaffolding pattern** (introduced in
|
|
||||||
`cacb19c`). Any future overlay that needs to fade `N >> 3`
|
|
||||||
elements together should follow the same shape: one tiny
|
|
||||||
marker carrying the full-alpha base colour, one global query
|
|
||||||
that lerps every marker's alpha each frame, no per-element
|
|
||||||
query plumbing. Cleanly outscales the `Without<X>, Without<Y>`
|
|
||||||
query exclusion pattern that the old splash was hitting at
|
|
||||||
three siblings.
|
|
||||||
|
|
||||||
### Canonical remote
|
|
||||||
|
|
||||||
`github.com/funman300/Rusty_Solitaire` is the canonical repo.
|
|
||||||
Always push there. **Local master has unpushed post-cut commits**
|
|
||||||
— run `git log --oneline origin/master..HEAD` for the live list;
|
|
||||||
`git push` is the next durability step (or roll the post-cut
|
|
||||||
commits into v0.20.1).
|
|
||||||
|
|
||||||
### Design direction (Terminal — base16-eighties)
|
|
||||||
|
|
||||||
- **Tone:** retro-terminal / synthwave — flat depth (no box-shadows),
|
|
||||||
monospaced-forward typography (JetBrains Mono / FiraMono), tight
|
|
||||||
16 px edge margins, 8 px card radius.
|
|
||||||
- **Palette:** near-black surface ramp (`#151515` / `#202020` /
|
|
||||||
`#2a2a2a` / `#353535`), cyan primary CTA (`#6fc2ef`), lime
|
|
||||||
success (`#acc267`), gold warning (`#ddb26f`), pink error /
|
|
||||||
suit-red (`#fb9fb1`), lavender celebration (`#e1a3ee`), teal
|
|
||||||
info (`#12cfc0`).
|
|
||||||
- **Two-color suits.** Red = `#fb9fb1`, black = `#d0d0d0`.
|
|
||||||
Outlined glyphs for diamonds & clubs are *always on*; the
|
|
||||||
Settings "color-blind mode" toggle only swaps red → cyan.
|
|
||||||
|
|
||||||
## Resume prompt
|
## Resume prompt
|
||||||
|
|
||||||
```
|
```
|
||||||
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
You are a senior Rust + Bevy developer working on Ferrous Solitaire.
|
||||||
Working directory: <Rusty_Solitaire clone path on this machine>.
|
Working directory: <Rusty_Solitaire clone path>.
|
||||||
Branch: master. v0.20.0 is tagged at 41a009a; the post-cut work
|
Branch: master. v0.23.0 is the current version (HEAD: 03be4fc). Fully pushed.
|
||||||
through dd101b3 is pushed to origin (Options B, C, D all closed).
|
|
||||||
Run `git log --oneline 41a009a..HEAD` to see what landed since the
|
|
||||||
tag — substantives: desktop-adaptation spec, splash boot-screen
|
|
||||||
port, replay-overlay banner enrichments, and the full card-face
|
|
||||||
artwork arc (52 faces + 5 backs as Terminal SVG-rasterised PNGs,
|
|
||||||
default-theme SVGs in lockstep, table backgrounds flattened,
|
|
||||||
top-bar layout fix, glyph orientation upright).
|
|
||||||
|
|
||||||
State: HEAD locally — see `git rev-parse HEAD`. Working tree is
|
READ FIRST (in order):
|
||||||
clean. All workspace tests pass (~1180+; check with
|
|
||||||
`cargo test --workspace`), clippy clean.
|
|
||||||
|
|
||||||
READ FIRST (in order, before doing anything):
|
|
||||||
1. SESSION_HANDOFF.md — this file
|
1. SESSION_HANDOFF.md — this file
|
||||||
2. CHANGELOG.md — [0.20.0] section is the most recent cut
|
2. CHANGELOG.md — [0.23.0] section has full Phase 8 detail
|
||||||
3. CLAUDE.md — unified-3.0 rule set
|
3. CLAUDE.md — unified-4.0 rule set
|
||||||
4. CLAUDE_SPEC.md — formal architecture spec
|
4. ARCHITECTURE.md — v1.3, fully up to date
|
||||||
5. ARCHITECTURE.md — crate responsibilities + data flow
|
5. docs/ui-mockups/ — design system + mockup library
|
||||||
6. docs/ui-mockups/ — design system + 24-mockup library +
|
6. docs/android/ — Android setup + build runbook
|
||||||
desktop-adaptation.md (the rules-based
|
7. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
||||||
companion to the mockups; read this
|
|
||||||
before any plugin port)
|
|
||||||
7. docs/android/* — Android setup + build runbook
|
|
||||||
8. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
|
||||||
— saved feedback / project context
|
|
||||||
(machine-local; may be missing on a
|
|
||||||
fresh machine)
|
|
||||||
|
|
||||||
DECISION TO ASK THE PLAYER FIRST:
|
OPEN WORK:
|
||||||
A. Push the post-cut commits to origin. Either as-is on master
|
Phase 8 punch list is fully closed. All items verified complete.
|
||||||
or rolled into a v0.20.1 cut (CHANGELOG entry + tag).
|
Remaining nuisance: `cargo apk build --lib` noisy stderr (cosmetic, non-blocking).
|
||||||
Mechanical, but local master diverges from origin until done.
|
|
||||||
B. *Closed by `29136d8` + `a27cf5a`.* Both splash polish
|
|
||||||
pieces shipped (cursor pulse + scanline overlay). No further
|
|
||||||
splash work pending unless a new mockup detail surfaces.
|
|
||||||
C. *Closed by `54005d5` + `e080b49`.* Banner-local replay-overlay
|
|
||||||
pieces all shipped (scrub bar, ▌ label, GAME caption, MOVE
|
|
||||||
chip). Remaining are cross-plugin (floating MOVE chip above
|
|
||||||
the focused card — needs cursor → card-position plumbing) or
|
|
||||||
multi-session (full screen-takeover redesign — move-log
|
|
||||||
scroll, mini tableau, WIN MOVE marker, data-layer impact).
|
|
||||||
Either belongs in its own decision tree the next time replay
|
|
||||||
work surfaces.
|
|
||||||
D. *Closed 2026-05-08 by `5623368`…`dd101b3`.* The full
|
|
||||||
card-face / suit / card-back / default-theme / table-
|
|
||||||
background / top-bar / glyph-orientation arc landed across
|
|
||||||
nine commits. Terminal cards rendering on every face (dark
|
|
||||||
`#1a1a1a` background, pink/gray suit glyphs as inline SVG
|
|
||||||
paths, scanline-pattern cyan-accent backs); both rendering
|
|
||||||
paths (`assets/cards/*.png` and the bundled-default theme
|
|
||||||
SVGs at `solitaire_engine/assets/themes/default/*.svg`) in
|
|
||||||
lockstep; pin test (`card_face_svg_pin`) guards against
|
|
||||||
future rasteriser drift. Visual-identity arc effectively
|
|
||||||
complete — only the toast warning/error variant slots
|
|
||||||
remain wired-but-unused.
|
|
||||||
E. App icon round — re-run artwork/Icon Export.html (the
|
|
||||||
export PNGs are not currently in `artwork/`), then wire
|
|
||||||
Window::icon + generate .icns / .ico. Half-day task. No
|
|
||||||
cert dependency.
|
|
||||||
F. APK launch verification on AVD / device + the JNI bridges
|
|
||||||
it would shake out (ClipboardManager, Keystore).
|
|
||||||
|
|
||||||
WORKFLOW NOTES:
|
4 Android UX fixes are implemented and verified but NOT YET COMMITTED:
|
||||||
- Use the system git config (already correct).
|
- BUG-3 (hud_plugin.rs): multi-modal stacking guard
|
||||||
- When attributing playtester feedback in commits/docs, use
|
- UX-7 (help_plugin.rs): help text wrap on Android
|
||||||
"Quat" not "Rhys" (saved feedback memory).
|
- UX-5b (home_plugin.rs): FiraMono glyph corruption in mode selector
|
||||||
- Sub-agents stage + verify only; orchestrator commits.
|
- UX-1 (safe_area.rs): modal Done button in gesture zone
|
||||||
- Every commit must pass build / clippy / test before pushing.
|
|
||||||
- Push to GitHub (origin) — gh auth setup-git wired on
|
|
||||||
primary dev box; verify on laptop before first push.
|
|
||||||
|
|
||||||
OPEN AT THE START: ask which of A–F. Don't pick unilaterally.
|
Commit those first, then suggest Phase 9 planning.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: solitaire-server
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
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
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 369 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 759 B |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 927 B |
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rebuild the solitaire_wasm crate and install the output into
|
||||||
|
# solitaire_server/web/pkg/ so the server can serve the replay viewer.
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# cargo install wasm-pack
|
||||||
|
# rustup target add wasm32-unknown-unknown
|
||||||
|
#
|
||||||
|
# Run from the repo root:
|
||||||
|
# ./build_wasm.sh
|
||||||
|
#
|
||||||
|
# The generated files (solitaire_wasm.js + solitaire_wasm_bg.wasm) are
|
||||||
|
# committed to git so self-hosters who don't touch the WASM crate can
|
||||||
|
# skip this step. Regenerate after any change to solitaire_wasm/ or
|
||||||
|
# solitaire_core/.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
OUT_DIR="$REPO_ROOT/solitaire_server/web/pkg"
|
||||||
|
|
||||||
|
if ! command -v wasm-pack &> /dev/null; then
|
||||||
|
echo "error: wasm-pack not found." >&2
|
||||||
|
echo " Install with: cargo install wasm-pack" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building solitaire_wasm (target: web)..."
|
||||||
|
wasm-pack build \
|
||||||
|
--target web \
|
||||||
|
--out-dir "$OUT_DIR" \
|
||||||
|
--no-typescript \
|
||||||
|
"$REPO_ROOT/solitaire_wasm"
|
||||||
|
|
||||||
|
# wasm-pack writes a package.json and .gitignore into the output dir.
|
||||||
|
# Remove them — we manage the output directory ourselves.
|
||||||
|
rm -f "$OUT_DIR/package.json" "$OUT_DIR/.gitignore"
|
||||||
|
|
||||||
|
echo "Done. Output:"
|
||||||
|
ls -lh "$OUT_DIR"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: solitaire-server
|
||||||
|
namespace: solitaire
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: solitaire-server
|
||||||
|
# SQLite is single-writer; Recreate avoids two pods owning the PVC at once.
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: solitaire-server
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitea-registry
|
||||||
|
containers:
|
||||||
|
- name: server
|
||||||
|
image: solitaire-server
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
value: sqlite:///data/sol.db
|
||||||
|
- name: JWT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: solitaire-secrets
|
||||||
|
key: jwt-secret
|
||||||
|
- name: SERVER_PORT
|
||||||
|
value: "8080"
|
||||||
|
volumeMounts:
|
||||||
|
- name: db-data
|
||||||
|
mountPath: /data
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
volumes:
|
||||||
|
- name: db-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: solitaire-db
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: solitaire-analytics
|
||||||
|
namespace: solitaire
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: analytics.aleshym.co
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: matomo
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- analytics.aleshym.co
|
||||||
|
secretName: analytics-tls
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: solitaire-server
|
||||||
|
namespace: solitaire
|
||||||
|
annotations:
|
||||||
|
# Remove the next two lines if you are not using cert-manager.
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: klondike.aleshym.co
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: solitaire-server
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
|
# Remove the tls block if you are not using cert-manager.
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- klondike.aleshym.co
|
||||||
|
secretName: solitaire-tls
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- pvc.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- mariadb-pvc.yaml
|
||||||
|
- mariadb-deployment.yaml
|
||||||
|
- mariadb-service.yaml
|
||||||
|
- matomo-pvc.yaml
|
||||||
|
- matomo-deployment.yaml
|
||||||
|
- matomo-service.yaml
|
||||||
|
- ingress-analytics.yaml
|
||||||
|
|
||||||
|
# CI updates this block automatically via `kustomize edit set image`.
|
||||||
|
# The image name here matches the `image: solitaire-server` stub in deployment.yaml.
|
||||||
|
images:
|
||||||
|
- name: solitaire-server
|
||||||
|
newName: git.aleshym.co/funman300/solitaire-server
|
||||||
|
newTag: 9ef5759f
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mariadb
|
||||||
|
namespace: solitaire
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: mariadb
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mariadb
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mariadb
|
||||||
|
image: mariadb:11
|
||||||
|
env:
|
||||||
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: matomo-secret
|
||||||
|
key: MYSQL_ROOT_PASSWORD
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: matomo-secret
|
||||||
|
key: MYSQL_DATABASE
|
||||||
|
- name: MYSQL_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: matomo-secret
|
||||||
|
key: MYSQL_USER
|
||||||
|
- name: MYSQL_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: matomo-secret
|
||||||
|
key: MYSQL_PASSWORD
|
||||||
|
ports:
|
||||||
|
- containerPort: 3306
|
||||||
|
volumeMounts:
|
||||||
|
- name: mariadb-data
|
||||||
|
mountPath: /var/lib/mysql
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- healthcheck.sh
|
||||||
|
- --connect
|
||||||
|
- --innodb_initialized
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- healthcheck.sh
|
||||||
|
- --connect
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
volumes:
|
||||||
|
- name: mariadb-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mariadb-data
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: mariadb-data
|
||||||
|
namespace: solitaire
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: mariadb
|
||||||
|
namespace: solitaire
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mariadb
|
||||||
|
ports:
|
||||||
|
- name: mysql
|
||||||
|
port: 3306
|
||||||
|
targetPort: 3306
|
||||||
|
clusterIP: None
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: matomo
|
||||||
|
namespace: solitaire
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: matomo
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: matomo
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: matomo
|
||||||
|
image: matomo:5.10.0
|
||||||
|
env:
|
||||||
|
- name: MATOMO_DATABASE_HOST
|
||||||
|
value: mariadb
|
||||||
|
- name: MATOMO_DATABASE_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: MATOMO_DATABASE_ADAPTER
|
||||||
|
value: PDO\MYSQL
|
||||||
|
- name: MATOMO_DATABASE_DBNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: matomo-secret
|
||||||
|
key: MYSQL_DATABASE
|
||||||
|
- name: MATOMO_DATABASE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: matomo-secret
|
||||||
|
key: MYSQL_USER
|
||||||
|
- name: MATOMO_DATABASE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: matomo-secret
|
||||||
|
key: MYSQL_PASSWORD
|
||||||
|
# Traefik terminates SSL; tell Matomo to trust X-Forwarded-* headers
|
||||||
|
- name: MATOMO_GENERAL_ASSUME_SECURE_PROTOCOL
|
||||||
|
value: "1"
|
||||||
|
- name: MATOMO_GENERAL_PROXY_CLIENT_HEADERS
|
||||||
|
value: HTTP_X_FORWARDED_FOR
|
||||||
|
- name: MATOMO_GENERAL_PROXY_HOST_HEADERS
|
||||||
|
value: HTTP_X_FORWARDED_HOST
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
volumeMounts:
|
||||||
|
- name: matomo-data
|
||||||
|
mountPath: /var/www/html
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /matomo.php
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /matomo.php
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
volumes:
|
||||||
|
- name: matomo-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: matomo-data
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: matomo-data
|
||||||
|
namespace: solitaire
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# DO NOT COMMIT THE REAL VERSION OF THIS FILE.
|
||||||
|
# deploy/matomo-secret.yaml is gitignored — apply it manually once:
|
||||||
|
#
|
||||||
|
# cp deploy/matomo-secret.yaml.example deploy/matomo-secret.yaml
|
||||||
|
# # edit the passwords below, then:
|
||||||
|
# kubectl apply -f deploy/matomo-secret.yaml
|
||||||
|
# kubectl annotate secret matomo-secret -n solitaire \
|
||||||
|
# argocd.argoproj.io/sync-options=Prune=false --overwrite
|
||||||
|
#
|
||||||
|
# Generate strong passwords with:
|
||||||
|
# python3 -c "import secrets; print(secrets.token_urlsafe(18))"
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: matomo-secret
|
||||||
|
namespace: solitaire
|
||||||
|
stringData:
|
||||||
|
MYSQL_ROOT_PASSWORD: "CHANGE_ME"
|
||||||
|
MYSQL_DATABASE: matomo
|
||||||
|
MYSQL_USER: matomo
|
||||||
|
MYSQL_PASSWORD: "CHANGE_ME"
|
||||||
|
MATOMO_ADMIN_PASSWORD: "CHANGE_ME"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: matomo
|
||||||
|
namespace: solitaire
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: matomo
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: solitaire
|
||||||