Commit Graph

6 Commits

Author SHA1 Message Date
funman300 d761a150d7 chore: rename app from Solitaire Quest to Ferrous Solitaire
Build and Deploy / build-and-push (push) Successful in 4m40s
Updates all in-tree references:
- Android package: com.solitairequest.app → com.ferrousapp.solitaire
- APK name: solitaire-quest → ferrous-solitaire
- Data dir: solitaire_quest → ferrous_solitaire (across all 6 data modules + engine)
- Keyring service: solitaire_quest_server → ferrous_solitaire_server
- Android Keystore key: solitaire_quest_token_key → ferrous_solitaire_token_key
- Gitea repo: Rusty_Solitare → Ferrous-Solitaire (also fixes "Solitare" typo)
- Renamed pkg/solitaire-quest* → pkg/ferrous-solitaire*
- Updated ArgoCD, docker-compose, CI workflow, build script, all docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:23:49 -07:00
funman300 ab857bbb6e feat(data): add Replay::win_move_index for the WIN MOVE scrub marker
First finite step toward the B-2 replay screen-takeover redesign:
the data foundation. Adds an additive optional `win_move_index:
Option<usize>` field on `Replay`, defaulting to `None` via
`#[serde(default)]` so older `latest_replay.json` /
`replays.json` files load unchanged — no `REPLAY_SCHEMA_VERSION`
bump needed since the field is purely additive and nullable.

Populated at the live recording site (`game_plugin::handle_game_won`)
via a new builder-style setter `Replay::with_win_move_index`. For
fresh recordings the value is always `Some(moves.len() - 1)`
because recording freezes on win, but storing the index
explicitly lets the playback UI read the WIN MOVE position
directly without re-deriving it on every render — and leaves
room for future recording semantics that capture post-win state.

UI consumption (the WIN MOVE marker on the scrub bar, plus the
broader screen-takeover redesign — move-log scroller, mini-
tableau preview, playback controls) lands in subsequent commits.

Test coverage: default value, builder set / set-None, on-disk
round-trip, and the legacy-JSON-loads-with-None backward-compat
contract (the test that pins the no-schema-bump claim).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:45:02 -07:00
funman300 4b51e50203 fix(data): route data_dir() through a per-platform shim so Android persists
dirs::data_dir() returns None on Android, which silently disabled
every persistence path (settings, stats, achievements, replays,
game-state, time-attack sessions, user themes). New
solitaire_data::platform::data_dir() shim 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,
since the package id is pinned in
[package.metadata.android].

CLAUDE.md §10 already flagged this as a known pitfall; the shim
pays it down at the one chokepoint instead of per feature.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:55:49 -07:00
funman300 42d90b199c feat(data,engine): persist replay share URL alongside the replay
The v0.18.0 share-link affordance lived in an in-memory
LastSharedReplayUrl resource that was wiped on quit; the player had
to re-open Stats and re-share within the same session of the win.
The Stats overlay's Prev/Next selector also surfaced older replays
that had no share link at all even when those wins had been
uploaded successfully.

This bundles the URL with the replay it belongs to:

- Replay (solitaire_data) gains share_url: Option<String> with
  #[serde(default)]. No REPLAY_SCHEMA_VERSION bump — older
  replays.json files load unchanged with share_url == None on
  every entry. Replay::new() defaults the field to None.
- poll_replay_upload_result (sync_plugin) writes the resolved URL
  into ReplayHistoryResource::0.replays[0].share_url and persists
  the updated history via save_replay_history_to. The
  cancel-on-replace contract in push_replay_on_win guarantees
  replays[0] is the win whose URL the task is carrying — at most
  one upload is ever in flight, and it's always the most recent
  win.
- handle_copy_share_link_button (stats_plugin) reads from
  history.0.replays[selected.0].share_url instead of
  LastSharedReplayUrl, so the Prev/Next selector's currently-
  displayed replay drives the clipboard contents. Each historical
  win keeps its own URL.
- LastSharedReplayUrl resource removed entirely — its only role
  was bridging the upload-poll system to the Copy button, and
  that channel is now the share_url field on the replay record.

Tests:

- solitaire_data: replay_loads_when_share_url_field_is_absent
  pins backwards-compat — a pre-v0.19.0 Replay JSON without the
  field deserialises with share_url == None.
- solitaire_engine sync_plugin: upload_result_writes_share_url_into_replay_and_persists
  drives a pre-resolved AsyncComputeTaskPool task into
  PendingReplayUpload, pumps update() until the poll system
  resolves it, and asserts both the in-memory replays[0]
  carries the URL and a fresh load_replay_history_from(path)
  picks it up.

Workspace: 1170 passing tests / 0 failing, was 1168 (+2 net).
cargo clippy --workspace --all-targets -- -D warnings clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 18:10:16 -07:00
funman300 13a8a012ee feat(data,engine): rolling replay history (last 8 wins)
Promotes replay storage from a single overwriting slot at
latest_replay.json to a rolling list of the most recent 8 wins at
replays.json so the player can revisit a memorable game even after
winning more recently.

Storage layer

solitaire_data::replay gains ReplayHistory (schema_version=1, Vec<Replay>
capped at REPLAY_HISTORY_CAP = 8) plus save_replay_history_to,
load_replay_history_from, append_replay_to_history, and
replay_history_path. append_replay_to_history inserts at the front,
drops the oldest when the cap is hit, and persists atomically via
the existing .tmp + rename pattern. The legacy single-slot helpers
are #[deprecated] but kept for one release as a migration safety
net via the new migrate_legacy_latest_replay helper.

Engine integration

game_plugin's record_replay_on_win now appends to the history
instead of overwriting latest_replay.json. On Startup, if a legacy
latest_replay.json exists but replays.json doesn't, the migration
helper seeds the new file from the legacy entry — so the player's
last v0.14.0 replay carries forward.

Stats UI

LatestReplayResource → ReplayHistoryResource holding the full
history. New SelectedReplayIndex resource (default 0 = most
recent) drives a Prev / Next / "Replay N / M" selector at the top
of the Stats overlay. ReplayPrevButton, ReplayNextButton, and
ReplaySelectorCaption marker components let the repaint system
update the caption as the selection changes. The Watch button
launches the selected replay rather than always the most recent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:32:37 +00:00
funman300 42535f5109 feat(data): replay storage layer with atomic StockClick input
New `solitaire_data::replay` module:
- `Replay` struct: seed + draw_mode + mode + ordered move list +
  presentation metadata (time / score / date). Replays are
  reconstructed by rebuilding `GameState::new_with_mode` and applying
  the move list in order — a deterministic state machine driven by
  atomic player inputs, no per-step snapshots stored.
- `ReplayMove`: one variant per atomic player input. `Move {from, to,
  count}` covers card moves; `StockClick` covers every click on the
  stock (the engine resolves draw-vs-recycle deterministically from
  current state during both record and playback).
- Schema-versioned (`REPLAY_SCHEMA_VERSION = 2`); legacy files are
  rejected via the version gate so older replays just disappear from
  the UI rather than half-loading.
- Atomic save (.tmp -> rename), `dirs::data_dir()`-based path
  resolution. 5 round-trip / atomic / version-gate / corruption tests.

Sync trait extension:
- `SyncProvider::push_replay(&Replay)` — default returns
  `UnsupportedPlatform` so `LocalOnlyProvider` is silently no-op'd by
  the future push-on-win path. Mirrors the existing `pull` / `push`
  default-impl pattern.
- `SolitaireServerClient::push_replay` — `POST /api/replays`, same
  401-refresh-and-retry shape as `push`.

The wire format is the contract: `solitaire_wasm` (added in a later
commit) parses the JSON via its own minimal mirror struct so it can
compile to wasm32 without pulling the desktop client's transitive
deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:36:25 +00:00