docs(ui): finalize MVP plan structure and de-archaeologize topic docs

MVP web client (Phases 1-30) is complete; reorganize planning + living docs around that.

- PLAN.md kept as the staged MVP record (1-30) with a status block + pointers; removed the 31-36 stages, regression scenarios, and deferred-TODO section (moved out); fixed a stale cross-machine plan path.

- ui/PLAN-finalize.md (new): active web-finalization plan in 8 stages (visual system, a11y, i18n, error UX, PWA, build hygiene, docs, owner manual-QA loop); absorbs former Phases 33 and 35.

- ui/ROADMAP.md (new): post-MVP (Wails, Capacitor, realistic projection, acceptance + regression scenarios) and triaged deferred follow-ups.

- ui/docs/README.md (new): grouped topic-doc index.

- De-archaeologized all 20 ui/docs topic docs + ui/README.md + ui/core/README.md: stripped Phase-N build history, rewritten as current-state; deferred work now points at ROADMAP.md / PLAN-finalize.md. Docs-only; no code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-21 23:17:51 +02:00
parent 51865b8cf4
commit a89048f6c5
26 changed files with 836 additions and 929 deletions
+50 -58
View File
@@ -1,10 +1,8 @@
# Per-game state store
This document describes the per-game state owned by the in-game shell
layout. Phase 11 introduces the store and uses it for two consumers
(the header turn counter and the map view); later phases plug
inspector tabs, the order composer, and the calculator on top of the
same instance.
layout. The store serves the header turn counter, the map view,
inspector tabs, the order composer, and the calculator.
## Lifecycle
@@ -23,10 +21,9 @@ gameId })`. `init`:
2. calls `setGame(gameId)`, which:
- reads the per-game wrap-mode preference from `Cache`
(`game-prefs / <gameId>/wrap-mode`, default `torus`);
- calls `lobby.my.games.list` and finds the game record (the
Phase 11 wire schema extension on `GameSummary` adds
`current_turn`); if the user is not a member, the store flips
to `error`;
- calls `lobby.my.games.list` and finds the game record
(`GameSummary` carries `current_turn`); if the user is not a
member, the store flips to `error`;
- calls `user.games.report` for the discovered turn and decodes
the FlatBuffers response into a TS-friendly `GameReport` shape.
@@ -44,24 +41,21 @@ The store exposes:
| `wrapMode` | `torus / no-wrap` | per-game preference, persisted via `Cache` |
| `error` | `string \| null` | localised error message when `status === "error"` |
## Phase boundaries
## Store extensions
- Phase 11 surfaces only the planet subset of the report. Later
phases extend `GameReport` and `decodeReport` as their slice of
the wire lands (ships, fleets, sciences, routes, battles, mail).
- Phase 26 splits `currentTurn` from the turn whose snapshot is
displayed (`viewedTurn`) and adds `viewTurn(turn)` /
`returnToCurrent()` for history navigation. The derived
`historyMode` rune flips automatically when `viewedTurn <
currentTurn`; the layout passes it to Phase 12's sidebar /
bottom-tabs wiring (which hides the order tab) and to
`OrderDraftStore.bindClient` (which gates `add` / `remove` /
`move`). See "History mode" below for the cache and refresh
rules.
- Phase 24 replaces the tab-focus refresh with push-event-driven
refreshes; the visibility listener stays as a fallback for
background tabs that miss a push.
- Phase 29 wires the wrap-mode toggle UI on top of `setWrapMode`.
`GameReport` and `decodeReport` are extended as each slice of the
wire lands (ships, fleets, sciences, routes, battles, mail).
`currentTurn` is split from `viewedTurn`, and `viewTurn(turn)` /
`returnToCurrent()` handle history navigation. The derived
`historyMode` rune flips automatically when `viewedTurn <
currentTurn`; the layout passes it to the sidebar / bottom-tabs
wiring (which hides the order tab) and to
`OrderDraftStore.bindClient` (which gates `add` / `remove` / `move`).
See "History mode" below for the cache and refresh rules.
Tab-focus refreshes are supplemented by push-event-driven refreshes;
the visibility listener stays as a fallback for background tabs that
miss a push. The wrap-mode toggle UI is wired on top of
`setWrapMode`.
## Why `current_turn` lives on `GameSummary`
@@ -79,20 +73,19 @@ Extending `GameSummary` reuses the existing lobby pipeline; the
backend already tracks `current_turn` in its runtime projection
(`backend/internal/server/handlers_user_lobby_helpers.go`
`gameSummaryToWire` reads it from `g.RuntimeSnapshot.CurrentTurn`).
The wire change touches Phase 8's already-shipped catalogue, but the
`current_turn` field defaults to zero on the FB side, so existing
The `current_turn` field defaults to zero on the FB side, so existing
tests and the dev sandbox flow continue to work unchanged.
## State binding
`map/state-binding.ts::reportToWorld(report)` translates a
`GameReport` into a renderer-ready `World`. Phase 11 emits one Point
`GameReport` into a renderer-ready `World`. It emits one Point
primitive per planet across all four kinds (local / other /
uninhabited / unidentified). Each kind gets a distinct fill colour,
fill alpha, and point radius so the four classes are
visually-distinguishable at a glance; later phases will refine the
colour palette as the visual language stabilises (Phase 35 polish).
visually-distinguishable at a glance; colour-palette refinement is
deferred to the finalization plan
([../PLAN-finalize.md](../PLAN-finalize.md)).
The planet engine number is reused as the primitive id so a hit-test
result can resolve back to a planet without an extra lookup table.
@@ -108,9 +101,9 @@ unchanged), so a no-op refresh does not flicker the canvas.
In history mode `refresh()` is a no-op — forcing a reload would
silently bump the user back onto the current turn while they are
intentionally viewing a past one. Push events (Phase 24) still
deliver new-turn notifications asynchronously while the user
explores history, so the pending-turn toast continues to work.
intentionally viewing a past one. Push events still deliver
new-turn notifications asynchronously while the user explores
history, so the pending-turn toast continues to work.
`setWrapMode(mode)` writes to `Cache` and updates the rune; the
map view's effect picks the change up and re-mounts the renderer
@@ -118,20 +111,19 @@ with the new mode.
## Map visibility toggles
Phase 29 adds a `mapToggles: MapToggles` rune that drives the
gear popover in the map view. Every flag defaults to `true`
including `unreachablePlanets` (showing every planet by default)
and `visibleHyperspace` (the fog overlay on by default). The
exhaustive shape lives in `src/lib/game-state.svelte.ts`; the
gear popover (`src/lib/active-view/map-toggles.svelte`) is a
thin view of the rune.
A `mapToggles: MapToggles` rune drives the gear popover in the map
view. Every flag defaults to `true` including `unreachablePlanets`
(showing every planet by default) and `visibleHyperspace` (the fog
overlay on by default). The exhaustive shape lives in
`src/lib/game-state.svelte.ts`; the gear popover
(`src/lib/active-view/map-toggles.svelte`) is a thin view of the
rune.
`setMapToggle(key, value)` flips one entry in place and
persists the whole blob to `Cache` under the
`game-map-toggles/{gameId}` key. The blob carries a companion
`lastResetTurn` number — the turn at which the toggles were last
reset to defaults — so the new-turn reset path (below) can detect
a stale blob even across a cross-session gap.
`setMapToggle(key, value)` flips one entry in place and persists
the whole blob to `Cache` under the `game-map-toggles/{gameId}` key.
The blob carries a companion `lastResetTurn` number — the turn at
which the toggles were last reset to defaults — so the new-turn reset
path (below) can detect a stale blob even across a cross-session gap.
### New-turn reset
@@ -156,7 +148,7 @@ The cache namespace and blob shape are documented in
## History mode
Phase 26 lets the user step backward through the report timeline
The store lets the user step backward through the report timeline
without losing the live snapshot. The store keeps two turn runes:
- `currentTurn` — the server's authoritative latest. Only
@@ -170,7 +162,7 @@ The derived `historyMode` rune (`status === "ready" && viewedTurn
< currentTurn`) drives every history-aware consumer:
- the layout passes it to `Sidebar` / `BottomTabs` so the order
tab vanishes (Phase 12 prop wiring);
tab vanishes;
- the layout passes a `getHistoryMode` getter to
`OrderDraftStore.bindClient` so `add` / `remove` / `move` are
no-ops while the user is looking at a past turn;
@@ -179,17 +171,17 @@ The derived `historyMode` rune (`status === "ready" && viewedTurn
- the new `HistoryBanner` component renders the sticky "Viewing
turn N · read-only" strip when the flag is true.
`last-viewed-turn` semantics keep their Phase 11 meaning: "the
latest turn the user was caught up on". `loadTurn` only writes the
cache row when called with `isCurrent === true` (i.e. when the
load matches `currentTurn`). Historical excursions are therefore
ephemeral: closing the tab and reopening the game resumes on the
last caught-up turn, not on the last clicked one.
`last-viewed-turn` means "the latest turn the user was caught up
on". `loadTurn` only writes the cache row when called with
`isCurrent === true` (i.e. when the load matches `currentTurn`).
Historical excursions are therefore ephemeral: closing the tab and
reopening the game resumes on the last caught-up turn, not on the
last clicked one.
Past-turn reports are cached in the `game-history` namespace
(`{gameId}/turn/{N}``GameReport`). The cache is written by
`loadTurn` on every successful historical fetch and read first by
`viewTurn(N)` before falling back to the network. Past turns are
immutable, so the cache has no TTL and no eviction in Phase 26.
The current-turn snapshot is deliberately *not* cached — it is
mutable until the next engine tick.
immutable, so the cache has no TTL and no eviction. The current-turn
snapshot is deliberately *not* cached — it is mutable until the next
engine tick.