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
+64 -80
View File
@@ -25,29 +25,28 @@ during a connectivity hiccup keeps every line the player typed. A
remote-first composer that reflects the gateway's pending-orders
queue would force a sync on every keystroke.
Phase 14 lands the submit pipeline with batch semantics: every
entry the user has marked `valid` is collected into one signed
`user.games.order` request. The engine validates and stores the
order, returning per-command `cmdApplied` / `cmdErrorCode` in the
response body. The gateway re-encodes that JSON into the FBS
`UserGamesOrderResponse` envelope (with `commands: [CommandItem]`
populated), and `submitOrder` rejoins the verdict to each draft
entry by `cmdId`. Successfully applied entries stay visible in
the draft (the player keeps composing until turn cutoff);
rejected entries stay until the player edits or removes them.
The submit pipeline uses batch semantics: every entry the user has
marked `valid` is collected into one signed `user.games.order`
request. The engine validates and stores the order, returning
per-command `cmdApplied` / `cmdErrorCode` in the response body. The
gateway re-encodes that JSON into the FBS `UserGamesOrderResponse`
envelope (with `commands: [CommandItem]` populated), and `submitOrder`
rejoins the verdict to each draft entry by `cmdId`. Successfully
applied entries stay visible in the draft (the player keeps composing
until turn cutoff); rejected entries stay until the player edits or
removes them.
Phase 25 layers a transport-level policy on top of this baseline
without changing the batch semantics. The submit pipeline now
goes through `OrderQueue` (see
[`sync-protocol.md`](sync-protocol.md)): the queue holds the
submit while the browser is offline, classifies
`turn_already_closed` and `game_paused` server replies into
matching banners on the order tab, and exits the loop on the
sticky states so a stream of mutations does not re-elicit the
same gateway reply. Recovery from a `conflict` or `paused`
banner happens on the next `game.turn.ready` push frame via
`OrderDraftStore.resetForNewTurn`, which clears the local draft
and re-hydrates from the server for the new turn.
A transport-level policy layers on top of the batch baseline without
changing the batch semantics. The submit pipeline goes through
`OrderQueue` (see [`sync-protocol.md`](sync-protocol.md)): the queue
holds the submit while the browser is offline, classifies
`turn_already_closed` and `game_paused` server replies into matching
banners on the order tab, and exits the loop on the sticky states so
a stream of mutations does not re-elicit the same gateway reply.
Recovery from a `conflict` or `paused` banner happens on the next
`game.turn.ready` push frame via `OrderDraftStore.resetForNewTurn`,
which clears the local draft and re-hydrates from the server for the
new turn.
## Local-validation invariant
@@ -58,10 +57,9 @@ pipeline refuses to drain a draft that contains any `invalid`
entries. The validation step is per-command and pure — it consults
the current `GameStateStore` snapshot only, never the network.
Phase 14's `planetRename` is the first variant that exercises the
`draft → valid | invalid` transition. The validator
(`lib/util/entity-name.ts`) is a TS port of
`pkg/util/string.go.ValidateTypeName`, exercised on every render
The `planetRename` variant exercises the `draft → valid | invalid`
transition. The validator (`lib/util/entity-name.ts`) is a TS port
of `pkg/util/string.go.ValidateTypeName`, exercised on every render
in the inline editor and re-run by the store on every `add`. The
submit pipeline filters the draft to `valid` entries only — any
`invalid` row blocks the Submit button.
@@ -79,13 +77,12 @@ draft ──validate──▶ valid ──submit──▶ submitting ──ack
Transitions:
- **`draft → valid` / `draft → invalid`**: local validation. May
re-run when the underlying `GameStateStore` snapshot changes
(Phase 14+).
re-run when the underlying `GameStateStore` snapshot changes.
- **`valid → submitting`**: the submit pipeline picks the entry off
the draft and sends it to the gateway.
- **`submitting → applied` / `submitting → rejected`**: the gateway
responded; the entry is no longer in flight.
- **`submitting → conflict`** (Phase 25): the gateway returned
- **`submitting → conflict`**: the gateway returned
`resultCode = "turn_already_closed"`. The order tab surfaces a
banner above the command list. Any subsequent mutation
re-validates the conflict row back to `valid` / `invalid`; a
@@ -94,10 +91,10 @@ Transitions:
[`sync-protocol.md`](sync-protocol.md) for the full state
table and recovery paths.
Phase 14 lands the local validators (`draft → valid | invalid`),
the submit pipeline (`valid → submitting → applied | rejected`),
and the optimistic overlay that shows the player's intent on the
map and inspector while the order is in flight.
Local validators (`draft → valid | invalid`), the submit pipeline
(`valid → submitting → applied | rejected`), and the optimistic
overlay that shows the player's intent on the map and inspector while
the order is in flight are all implemented.
Statuses are runtime-only — they are not persisted alongside the
commands themselves. On every `init` the store re-runs
@@ -110,9 +107,7 @@ stored value).
## Discriminated union shape
`OrderCommand` is a discriminated union on the `kind` field. Phase
12 shipped the skeleton with a single content-free variant; Phase
14 added the first real one and Phase 15 added the second:
`OrderCommand` is a discriminated union on the `kind` field:
```ts
interface PlaceholderCommand {
@@ -148,9 +143,9 @@ The `id` field is the canonical identifier the store uses for
remove and reorder; later variants must keep `id: string` so the
store API stays uniform. The whole draft round-trips through
IndexedDB structured clone, so every variant must use only
JSON-friendly value types. Phase 14 lands `planetRename` together
with the inline editor in `lib/inspectors/planet.svelte`, the
local validator (`lib/util/entity-name.ts`, parity with
JSON-friendly value types. `planetRename` ships with the inline
editor in `lib/inspectors/planet.svelte`, the local validator
(`lib/util/entity-name.ts`, parity with
`pkg/util/string.go.ValidateTypeName`), and the submit pipeline.
`setProductionType` is the wire-mirror of the engine's
@@ -164,10 +159,10 @@ optimistic overlay rewrites `planet.production` using
mirrors the engine's `Cache.PlanetProductionDisplayName` so the
overlay stays byte-equal with the next server report.
### Collapse-by-target rule (Phase 15)
### Collapse-by-target rule
`setProductionType` is the first variant to carry a
collapse-by-target rule. `OrderDraftStore.add` enforces it:
`setProductionType` carries a collapse-by-target rule.
`OrderDraftStore.add` enforces it:
when the incoming command's `kind` is `"setProductionType"` it
drops every prior `setProductionType` entry with the same
`planetNumber` (and the matching keys from `statuses`) before
@@ -187,9 +182,7 @@ coexist — the rules apply within a `kind`, not across.
`OrderDraftStore` lives in
[`../frontend/src/sync/order-draft.svelte.ts`](../frontend/src/sync/order-draft.svelte.ts).
The class is a Svelte 5 runes store, so the file extension is
`.svelte.ts` (the original PLAN.md artifact line listed `.ts`
the deviation is documented inline in `PLAN.md`'s Phase 12
"Decisions" subsection).
`.svelte.ts`.
Lifecycle:
@@ -212,9 +205,8 @@ Layout integration mirrors `GameStateStore`:
- Exposed through the `ORDER_DRAFT_CONTEXT_KEY` Svelte context.
- Disposed in the layout's `onDestroy`.
The order tab consumes the store via
`getContext(ORDER_DRAFT_CONTEXT_KEY)`; Phase 14's planet inspector
will use the same key to push a new command.
The order tab and the planet inspector both consume the store via
`getContext(ORDER_DRAFT_CONTEXT_KEY)` to push new commands.
## Submit pipeline
@@ -224,9 +216,9 @@ will use the same key to push a new command.
`markSubmitting(ids)` so each row reads `submitting`, then
posts the snapshot through `submitOrder`.
2. `submitOrder` builds the FBS `UserGamesOrder` request (game_id,
`updatedAt = 0` in Phase 14, every command encoded as a
`CommandItem` with the typed payload union) and signs it via
the existing `executeCommand` orchestration.
`updatedAt`, every command encoded as a `CommandItem` with the
typed payload union) and signs it via the existing
`executeCommand` orchestration.
3. The engine validates, stores, and answers `202 Accepted` with
the stored order body — `game_id`, `updatedAt`, plus each
command echoed with `cmdApplied` and (on rejection)
@@ -252,8 +244,7 @@ in-flight entries back to `valid` so the operator can retry.
`applyOrderOverlay(report, commands, statuses)` (in
`api/game-state.ts`) returns a copy of the server `GameReport`
with every command in `applied` or `submitting` status projected
on top. Phase 14 understands `planetRename` only; future phases
extend the switch.
on top.
The overlay has its own context (`RENDERED_REPORT_CONTEXT_KEY`,
`lib/rendered-report.svelte.ts`) — the in-game shell layout owns
@@ -288,9 +279,7 @@ Cache row layout:
| -------------- | ------------------ | ---------------- |
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` |
The store writes the full draft on every mutation. Phase 25 may
profile the submit pipeline and batch into a microtask if write
amplification becomes a problem; until then the deterministic
The store writes the full draft on every mutation. The deterministic
write-on-every-mutation model is what tests assert and what the
layout relies on for crash safety.
@@ -300,14 +289,12 @@ order composer uses the namespace.
## History mode wiring
Phase 26 implements history mode: the user can step back through
past turns and see the report as it was. The IA section specifies
that the Order tab is hidden when history mode is active — the
player is browsing an immutable snapshot, and composing commands
History mode lets the user step back through past turns and see the
report as it was. The Order tab is hidden when history mode is active
the player is browsing an immutable snapshot, and composing commands
against it would be confusing.
Phase 12 wires the flag end-to-end as a prop. The layout owns the
flag and passes it to:
The layout owns the `historyMode` flag and passes it to:
- `Sidebar` as `historyMode`. The sidebar forwards it to its
`TabBar` as `hideOrder`. The Order entry is filtered out of the
@@ -318,17 +305,16 @@ flag and passes it to:
- `BottomTabs` as `hideOrder`. The mobile bottom-tab `Order`
button is suppressed when true.
Phase 26 turns the constant into a derived value driven by
`GameStateStore.historyMode` (`viewedTurn < currentTurn` while
`status === "ready"`). The same getter is also passed into
`OrderDraftStore.bindClient` as `getHistoryMode`, which short-
circuits the `add` / `remove` / `move` mutations to a no-op while
the flag is true. This makes every Phase 1422 inspector affordance
that calls `orderDraft.add(...)` inert in history mode without
per-component edits — the gate lives in the one chokepoint that
all callers go through. The conflict / paused banners and the
in-flight sync pipeline are untouched: they describe state that
exists independently of the user's current view.
`historyMode` is a derived value driven by `GameStateStore.historyMode`
(`viewedTurn < currentTurn` while `status === "ready"`). The same
getter is also passed into `OrderDraftStore.bindClient` as
`getHistoryMode`, which short-circuits the `add` / `remove` / `move`
mutations to a no-op while the flag is true. This makes every
inspector affordance that calls `orderDraft.add(...)` inert in history
mode without per-component edits — the gate lives in the one
chokepoint that all callers go through. The conflict / paused banners
and the in-flight sync pipeline are untouched: they describe state
that exists independently of the user's current view.
The store itself stays alive across history-mode round-trips so
the draft survives the toggle. The `RenderedReportSource` overlay
@@ -346,13 +332,11 @@ the chrome.
## Testing
Phase 12 + Phase 14 test artifacts:
- [`../frontend/tests/order-draft.test.ts`](../frontend/tests/order-draft.test.ts)
— Vitest unit tests for the store. Drives `OrderDraftStore`
directly with `IDBCache` over `fake-indexeddb`. Covers init,
add, remove, move, per-game isolation, mutations-before-init,
dispose hygiene, the Phase 14 status machine
dispose hygiene, the status machine
(`validate` / `markSubmitting` / `applyResults` /
`revertSubmittingToValid`), and the
`hydrateFromServer` cache-miss fallback.
@@ -375,12 +359,12 @@ Phase 12 + Phase 14 test artifacts:
— Vitest component tests for the rename action and the inline
editor's local validation.
- [`../frontend/tests/e2e/order-composer.spec.ts`](../frontend/tests/e2e/order-composer.spec.ts)
— Playwright spec for the Phase 12 skeleton (seed three
— Playwright spec for the order composer skeleton (seed three
commands, reload, persistence).
- [`../frontend/tests/e2e/rename-planet.spec.ts`](../frontend/tests/e2e/rename-planet.spec.ts)
Phase 14 end-to-end: select a planet, rename, submit, observe
the overlay-applied name on the inspector + map, reload, and
see the rename hydrated from `user.games.order.get`.
End-to-end: select a planet, rename, submit, observe the
overlay-applied name on the inspector + map, reload, and see the
rename hydrated from `user.games.order.get`.
The `__galaxyDebug.seedOrderDraft(gameId, commands)` and
`__galaxyDebug.clearOrderDraft(gameId)` helpers in