ui/phase-26: history mode (turn navigator + read-only banner)
Split GameStateStore into currentTurn (server's latest) and viewedTurn (displayed snapshot) so history excursions don't corrupt the resume bookmark or the live-turn bound. Add viewTurn / returnToCurrent / historyMode rune, plus a game-history cache namespace that stores past-turn reports for fast re-entry. OrderDraftStore.bindClient takes a getHistoryMode getter and short-circuits add / remove / move while the user is viewing a past turn; RenderedReportSource skips the order overlay in the same case. Header replaces the static "turn N" with a clickable triplet (TurnNavigator), the layout mounts HistoryBanner under the header, and visibility-refresh is a no-op while history is active. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+123
-22
@@ -1381,9 +1381,13 @@ Decisions taken with the project owner during implementation:
|
||||
6. **`historyMode` as a prop, not a module.** Layout passes
|
||||
`historyMode={false}` (a constant in Phase 12) to `Sidebar` and
|
||||
`BottomTabs`; both forward to their tab-bar children which omit
|
||||
the order entry when the flag is true. Phase 26 introduces the
|
||||
real `lib/history-mode.ts` module and replaces the constant in
|
||||
one place.
|
||||
the order entry when the flag is true. Phase 26 superseded the
|
||||
"introduce `lib/history-mode.ts`" half of this decision: the
|
||||
single derivation `historyMode = $derived(gameState.historyMode)`
|
||||
lives directly in `+layout.svelte`, the rune split between
|
||||
`currentTurn` and `viewedTurn` lives in `GameStateStore`, and
|
||||
no separate module is introduced. See Phase 26 decisions for
|
||||
the rationale.
|
||||
7. **Empty-state copy is `order is empty` / `приказ пуст`.** The
|
||||
`coming soon` placeholder text is replaced; per-row delete
|
||||
button reads `delete` / `удалить`.
|
||||
@@ -2805,24 +2809,116 @@ Targeted tests:
|
||||
|
||||
## Phase 26. History Mode
|
||||
|
||||
Status: pending.
|
||||
Status: pending (awaiting local-ci verification).
|
||||
|
||||
Goal: let the user navigate to past turns and view all data as it was,
|
||||
with no order composition allowed.
|
||||
|
||||
Artifacts:
|
||||
Decisions baked in during implementation:
|
||||
|
||||
- `ui/frontend/src/lib/header/turn-navigator.svelte` clickable turn
|
||||
counter expansion: popover (desktop) / bottom-sheet (mobile) listing
|
||||
recent turns and a search field for jumping to a turn number
|
||||
- `ui/frontend/src/lib/history-mode.ts` global toggle wired into every
|
||||
view's data source: when active, all `state-binding`, table, report,
|
||||
inspector, and map sources read from the historical snapshot for the
|
||||
selected turn
|
||||
- `ui/frontend/src/lib/header/history-banner.svelte` persistent banner
|
||||
reading `Viewing turn N · read-only` with a `Return to current turn`
|
||||
action
|
||||
- order tab hidden in history mode (already prepared in Phase 12)
|
||||
1. **History state lives in `GameStateStore`, no separate module.**
|
||||
The Phase 12 plan-line "introduce `lib/history-mode.ts`" is
|
||||
superseded: the only consumer needs a one-line derivation
|
||||
(`historyMode = $derived(gameState.historyMode)`), and the
|
||||
project's compactness rule rejects an abstraction with no second
|
||||
caller. The store ships two distinct turn runes — `currentTurn`
|
||||
(server's authoritative latest, set by `setGame` /
|
||||
`advanceToPending`) and `viewedTurn` (what the UI displays, set
|
||||
by `viewTurn` / `returnToCurrent`) — plus the derived
|
||||
`historyMode` rune that flips when `viewedTurn < currentTurn`.
|
||||
2. **`OrderDraftStore` gates mutations at one chokepoint.**
|
||||
`bindClient` gains an optional `getHistoryMode: () => boolean`
|
||||
alongside the existing `getCurrentTurn`; `add` / `remove` /
|
||||
`move` return early when it reports `true`. Every Phase 14–22
|
||||
inspector that calls `orderDraft.add(...)` becomes inert in
|
||||
history mode without per-component edits.
|
||||
3. **Turn navigator UX.** Header replaces the static `turn N` text
|
||||
with `← turn N →`: arrows step ±1 (disabled at `0` and
|
||||
`currentTurn`), the middle button opens a dropdown of every
|
||||
turn `Turn #0`…`Turn #currentTurn` with the current row carrying
|
||||
a badge. No free-text input. Desktop uses an absolute popover
|
||||
under the header; mobile reuses `view-menu.svelte`'s fixed-
|
||||
drawer pattern (no new primitive). Selecting the current row
|
||||
routes through `returnToCurrent()` so the "leave history" path
|
||||
has one canonical entry.
|
||||
4. **History is ephemeral across reloads.** `last-viewed-turn` is
|
||||
written only when `viewedTurn === currentTurn`; historical
|
||||
excursions never advance the resume bookmark. Page reload exits
|
||||
history mode. The visibility-refresh listener is a no-op while
|
||||
`historyMode` is true so a tab-focus event cannot silently kick
|
||||
the user back onto the live turn. Push events (Phase 24) continue
|
||||
to deliver new-turn notifications, so the pending-turn toast
|
||||
still appears.
|
||||
5. **Past-turn report cache.** New `game-history/{gameId}/turn/{N}`
|
||||
namespace stores past-turn reports; `viewTurn(N)` reads cache
|
||||
first and falls back to the network on miss. Past turns are
|
||||
immutable so the cache has no TTL and no eviction. The current
|
||||
turn deliberately skips the cache (it is mutable until the next
|
||||
tick).
|
||||
6. **Order overlay short-circuits in history mode.**
|
||||
`RenderedReportSource.report` returns the raw server snapshot
|
||||
instead of running `applyOrderOverlay`: the draft is composed
|
||||
against the current turn, projecting it onto a past report would
|
||||
render fictional intent.
|
||||
7. **`game.shell.headline` removed.** The Phase 11 i18n key that
|
||||
formatted `{race} @ {game}, turn {turn}` is deleted; the header
|
||||
composes `race @ game` in plain text and delegates `turn N` to
|
||||
`turn-navigator.svelte`. The existing `game-shell-headline`
|
||||
testid moves to the `.left` wrapper so e2e specs that match
|
||||
`toContainText("turn N")` continue to find the substring inside
|
||||
the navigator's button.
|
||||
|
||||
Artifacts (delivered):
|
||||
|
||||
- `ui/frontend/src/lib/game-state.svelte.ts` — `viewedTurn` rune,
|
||||
derived `historyMode` rune, `viewTurn(turn)` /
|
||||
`returnToCurrent()` public methods, `loadTurn(turn, { isCurrent })`
|
||||
refactor that gates `last-viewed-turn` writes, `readReport` cache
|
||||
layer over the `game-history` namespace, visibility-refresh
|
||||
short-circuit in history mode, `initSynthetic` keeps
|
||||
`currentTurn === viewedTurn`.
|
||||
- `ui/frontend/src/sync/order-draft.svelte.ts` — `bindClient` accepts
|
||||
`getHistoryMode`, `add` / `remove` / `move` no-op when active.
|
||||
- `ui/frontend/src/lib/rendered-report.svelte.ts` — overlay short-
|
||||
circuit when `gameState.historyMode === true`.
|
||||
- `ui/frontend/src/lib/header/turn-navigator.svelte` (new) — header
|
||||
triplet `← turn N →` + dropdown popover / drawer, reuses
|
||||
`view-menu.svelte`'s outside-click / Escape pattern.
|
||||
- `ui/frontend/src/lib/header/history-banner.svelte` (new) — sticky
|
||||
read-only banner under the header with a `Return to current turn`
|
||||
action.
|
||||
- `ui/frontend/src/lib/header/header.svelte` — embeds
|
||||
`<TurnNavigator />` next to the race-and-game identity span;
|
||||
drops the static turn portion.
|
||||
- `ui/frontend/src/routes/games/[id]/+layout.svelte` —
|
||||
`historyMode` derived rune, `getHistoryMode` passed to
|
||||
`orderDraft.bindClient`, `<HistoryBanner />` mounted between
|
||||
header and body.
|
||||
- `ui/frontend/src/lib/i18n/locales/{en,ru}.ts` — new
|
||||
`game.shell.history.*` and `game.shell.turn.*` keys; the now-
|
||||
unused `game.shell.headline` entry is removed.
|
||||
- `ui/docs/storage.md` — `game-history` namespace row; also adds
|
||||
the `game-prefs/{gameId}/last-viewed-turn` row (Phase 11 doc
|
||||
gap).
|
||||
- `ui/docs/game-state.md` — current/viewed-turn rune table, the
|
||||
new History mode section.
|
||||
- `ui/docs/navigation.md` — describes the navigator, the read-only
|
||||
banner, and the `historyMode` derivation wiring.
|
||||
- `ui/docs/order-composer.md` — notes the mutation gate, the
|
||||
overlay short-circuit, and the cross-doc references.
|
||||
- Vitest: `tests/game-state.test.ts` extended with `viewTurn` /
|
||||
`returnToCurrent` / `historyMode` derivation / cache hit /
|
||||
visibility-refresh short-circuit / resume-from-stale-bookmark
|
||||
flips; `tests/order-draft.test.ts` extended with the history-
|
||||
mode gate cases; `tests/turn-navigator.test.ts` and
|
||||
`tests/history-banner.test.ts` (new) cover the components in
|
||||
isolation.
|
||||
- Playwright: `tests/e2e/history-mode.spec.ts` (new) — drives the
|
||||
full chrome flow against `/games/<id>/table/planets`. The map
|
||||
view is deliberately avoided because the Pixi renderer can
|
||||
monopolise the headless Chromium main thread long enough to let
|
||||
the `toContainText` poll find stale "turn ?" content; the table
|
||||
view exercises the same wiring without that rendering tail.
|
||||
|
||||
Dependencies: Phases 11, 12, 23.
|
||||
|
||||
@@ -2834,15 +2930,20 @@ Acceptance criteria:
|
||||
available;
|
||||
- returning to the current turn restores live data and re-shows the
|
||||
order tab with the prior draft intact (state preservation);
|
||||
- all UI views (map, tables, report, battle, mail) work in history
|
||||
mode.
|
||||
- battle / mail stub views still render correctly while the
|
||||
read-only banner is visible (Phases 27 / 28 will replace the
|
||||
stubs with real implementations; the wiring is sufficient
|
||||
today).
|
||||
|
||||
Targeted tests:
|
||||
|
||||
- Vitest unit tests for `history-mode` toggle and per-view source
|
||||
selection;
|
||||
- Playwright e2e: enter history mode, navigate three views, return,
|
||||
confirm the order draft survived.
|
||||
- Vitest unit tests for current/viewed turn rune split, view-turn
|
||||
cache behaviour, visibility-refresh short-circuit, order-draft
|
||||
history-mode gate, turn-navigator interactions, history-banner
|
||||
rendering / action;
|
||||
- Playwright e2e: enter history mode via arrow, navigate via
|
||||
dropdown, return via banner action, confirm the order draft
|
||||
survives the round-trip.
|
||||
|
||||
## Phase 27. Battle Viewer
|
||||
|
||||
|
||||
Reference in New Issue
Block a user