ui/phase-14: rename planet end-to-end + order read-back
Wires the first end-to-end command through the full pipeline:
inspector rename action → local order draft → user.games.order
submit → optimistic overlay on map / inspector → server hydration
on cache miss via the new user.games.order.get message type.
Backend: GET /api/v1/user/games/{id}/orders forwards to engine
GET /api/v1/order. Gateway parses the engine PUT response into the
extended UserGamesOrderResponse FBS envelope and adds
executeUserGamesOrderGet for the read-back path. Frontend ports
ValidateTypeName to TS, lands the inline rename editor + Submit
button, and exposes a renderedReport context so consumers see the
overlay-applied snapshot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+144
-25
@@ -25,13 +25,22 @@ 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.
|
||||
|
||||
When the submit pipeline lands (Phase 25), it iterates the draft
|
||||
in order, sending one `command` per line. The gateway's per-line
|
||||
result rejoins the draft entry through `cmdId`, and the entry's
|
||||
`CommandStatus` flips to `applied` or `rejected`. Successfully
|
||||
applied entries stay visible until the next turn cutoff so the
|
||||
player can see what was committed; rejected entries stay until the
|
||||
player edits or removes them.
|
||||
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.
|
||||
|
||||
Phase 25 is reserved for one extension on top of this: per-line
|
||||
sequencing if a future use case needs to submit commands
|
||||
individually rather than in one batch. The wire shape is already
|
||||
flexible enough — the response carries an array of results — so
|
||||
Phase 25 only changes the client-side iteration policy.
|
||||
|
||||
## Local-validation invariant
|
||||
|
||||
@@ -42,10 +51,13 @@ 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 12 ships the skeleton without any concrete validators: the
|
||||
single `placeholder` variant is content-free and stays at `draft`
|
||||
forever. Phase 14's `planetRename` is the first variant that
|
||||
exercises the `draft → valid | invalid` transition.
|
||||
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
|
||||
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.
|
||||
|
||||
## Command status state machine
|
||||
|
||||
@@ -65,14 +77,25 @@ Transitions:
|
||||
- **`submitting → applied` / `submitting → rejected`**: the gateway
|
||||
responded; the entry is no longer in flight.
|
||||
|
||||
Phase 12 stores the type but does not implement any transitions.
|
||||
Every entry remains at `draft` until later phases land the
|
||||
validators (Phase 14) and the submit pipeline (Phase 25).
|
||||
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.
|
||||
|
||||
Statuses are runtime-only — they are not persisted alongside the
|
||||
commands themselves. On every `init` the store re-runs
|
||||
`validateEntityName` over each command and seeds `draft → valid` /
|
||||
`invalid`. Submitted-then-applied entries lose their `applied`
|
||||
status on reload but stay in the draft as `valid`; the user sees
|
||||
the same row, the overlay reapplies, and re-submitting is
|
||||
idempotent on the engine side (the rename already matches the
|
||||
stored value).
|
||||
|
||||
## Discriminated union shape
|
||||
|
||||
`OrderCommand` is a discriminated union on the `kind` field. Phase
|
||||
12 ships a single variant:
|
||||
12 shipped the skeleton with a single content-free variant; Phase
|
||||
14 adds the first real one:
|
||||
|
||||
```ts
|
||||
interface PlaceholderCommand {
|
||||
@@ -80,15 +103,25 @@ interface PlaceholderCommand {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
}
|
||||
type OrderCommand = PlaceholderCommand;
|
||||
|
||||
interface PlanetRenameCommand {
|
||||
readonly kind: "planetRename";
|
||||
readonly id: string;
|
||||
readonly planetNumber: number;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
type OrderCommand = PlaceholderCommand | PlanetRenameCommand;
|
||||
```
|
||||
|
||||
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 adds the first real variant
|
||||
(`planetRename`) and updates this list.
|
||||
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
|
||||
`pkg/util/string.go.ValidateTypeName`), and the submit pipeline.
|
||||
|
||||
## Store
|
||||
|
||||
@@ -124,6 +157,70 @@ 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.
|
||||
|
||||
## Submit pipeline
|
||||
|
||||
`sync/submit.ts` wraps `GalaxyClient.executeCommand("user.games.order", …)`:
|
||||
|
||||
1. The order tab filters the draft to `valid` entries, calls
|
||||
`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.
|
||||
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)
|
||||
`cmdErrorCode`.
|
||||
4. The gateway re-encodes that JSON into FBS
|
||||
`UserGamesOrderResponse`, and the frontend parses it back into
|
||||
`Map<cmdId, "applied" | "rejected">`.
|
||||
5. The order tab calls `applyResults` on the draft, then
|
||||
`gameState.refresh()` to fetch a fresh report. The optimistic
|
||||
overlay (`api/game-state.ts.applyOrderOverlay`) keeps the
|
||||
player's intent visible on the map / inspector even if the
|
||||
engine has not yet applied the rename — turn cutoff resolves
|
||||
the divergence on the next report.
|
||||
|
||||
If the gateway answers with a non-`ok` `resultCode` (auth /
|
||||
transcoder / engine validation), the submit pipeline marks every
|
||||
in-flight entry as `rejected` and surfaces the gateway's error
|
||||
message inline; no refresh is issued. Network exceptions revert
|
||||
in-flight entries back to `valid` so the operator can retry.
|
||||
|
||||
## Optimistic overlay
|
||||
|
||||
`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.
|
||||
|
||||
The overlay has its own context (`RENDERED_REPORT_CONTEXT_KEY`,
|
||||
`lib/rendered-report.svelte.ts`) — the in-game shell layout owns
|
||||
the source and exposes it to the inspector tab, the mobile sheet,
|
||||
and the map renderer. Raw `gameState.report` stays available for
|
||||
debugging and for any future consumer that needs the un-overlaid
|
||||
snapshot (history mode is the planned reader).
|
||||
|
||||
## Server hydration on cache miss
|
||||
|
||||
`OrderDraftStore` records `needsServerHydration = true` when no
|
||||
cache row exists for the active game (fresh install, cleared
|
||||
storage, switching device). After the layout boot resolves both
|
||||
`gameState.init` and `orderDraft.init`, it calls
|
||||
`orderDraft.hydrateFromServer({ client, turn })` which issues
|
||||
`user.games.order.get` against the gateway. A `found = false`
|
||||
answer leaves the draft empty; a stored order is decoded into
|
||||
`OrderCommand[]` and persisted to the local cache so subsequent
|
||||
reloads use the cached copy.
|
||||
|
||||
An *explicitly* empty cache row (the user has removed every
|
||||
command they composed) does not trigger hydration — local intent
|
||||
always wins over the server snapshot. The "did this row exist?"
|
||||
distinction matters: `Cache.get` returns `undefined` on a miss
|
||||
and `[]` on an explicitly stored empty array.
|
||||
|
||||
## Persistence
|
||||
|
||||
Cache row layout:
|
||||
@@ -168,19 +265,41 @@ its own test suite.
|
||||
|
||||
## Testing
|
||||
|
||||
Two test artifacts cover the skeleton:
|
||||
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,
|
||||
and dispose hygiene.
|
||||
dispose hygiene, the Phase 14 status machine
|
||||
(`validate` / `markSubmitting` / `applyResults` /
|
||||
`revertSubmittingToValid`), and the
|
||||
`hydrateFromServer` cache-miss fallback.
|
||||
- [`../frontend/tests/entity-name.test.ts`](../frontend/tests/entity-name.test.ts)
|
||||
— Vitest tests for the validator. Aligned with
|
||||
`pkg/util/string_test.go.TestValidateString` for parity.
|
||||
- [`../frontend/tests/submit.test.ts`](../frontend/tests/submit.test.ts)
|
||||
— Vitest tests for the submit pipeline. Hand-builds FBS
|
||||
responses to verify per-command parsing and batch-level
|
||||
fallback.
|
||||
- [`../frontend/tests/order-load.test.ts`](../frontend/tests/order-load.test.ts)
|
||||
— Vitest tests for `fetchOrder`. Covers the populated /
|
||||
not-found / negative-turn / non-ok paths.
|
||||
- [`../frontend/tests/order-overlay.test.ts`](../frontend/tests/order-overlay.test.ts)
|
||||
— Vitest tests for the pure `applyOrderOverlay` projection.
|
||||
- [`../frontend/tests/order-tab.test.ts`](../frontend/tests/order-tab.test.ts)
|
||||
— Vitest component tests for the Submit button states and the
|
||||
applied / rejected verdict flow.
|
||||
- [`../frontend/tests/inspector-planet.test.ts`](../frontend/tests/inspector-planet.test.ts)
|
||||
— 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. Seeds three commands through
|
||||
`__galaxyDebug.seedOrderDraft`, navigates into
|
||||
`/games/<id>/map`, opens the Order tool (sidebar tab on
|
||||
desktop, bottom tab on mobile), asserts the rows, reloads, and
|
||||
asserts the rows again.
|
||||
— Playwright spec for the Phase 12 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`.
|
||||
|
||||
The `__galaxyDebug.seedOrderDraft(gameId, commands)` and
|
||||
`__galaxyDebug.clearOrderDraft(gameId)` helpers in
|
||||
|
||||
Reference in New Issue
Block a user