ui/phase-14: auto-sync order draft + always GET on boot + header headline

Replaces the manual Submit button with an auto-sync pipeline driven
by `OrderDraftStore`: every successful add / remove / move
coalesces a `submitOrder` call so the engine always mirrors the
local draft. Removing the last command sends an empty cmd[] PUT —
the engine, repo, and rest model now accept that as a valid
"player cleared their draft" state.

`hydrateFromServer` is now invoked unconditionally on game boot so
a fresh device picks up the player's stored order, and the local
cache is overwritten by the server's view (server is the source of
truth).

Header replaces the static "race ?" + turn counter with a single
headline string `<race> @ <game>, turn <n>`, sourced from the
engine's Report.race + the lobby's GameSummary.gameName + the live
turn number, with a `?` fallback while any piece is loading.

Tests:
- engine: empty PUT round-trips, repo round-trips empty Commands
- order-draft: auto-sync sends full draft on every mutation,
  rejected response surfaces error sync status, rapid mutations
  coalesce, server hydration overwrites cache
- order-tab: per-row status flips through the auto-sync lifecycle,
  remove → empty cmd[] PUT, rejected → retry button
- inspector overlay: applied + valid + submitting all participate
  in the optimistic projection
- header: live race / game / turn rendering with fall-back

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-09 13:34:10 +02:00
parent 68d8607eaa
commit 229c43beb5
26 changed files with 1144 additions and 728 deletions
+7 -6
View File
@@ -83,9 +83,8 @@ const en = {
"lobby.error.internal_error": "internal server error",
"lobby.error.unknown": "{message}",
"game.shell.race_placeholder": "race ?",
"game.shell.turn_label": "turn",
"game.shell.turn_unknown": "?",
"game.shell.unknown": "?",
"game.shell.headline": "{race} @ {game}, turn {turn}",
"game.shell.connection.online": "online",
"game.shell.connection.reconnecting": "reconnecting…",
"game.shell.connection.offline": "offline",
@@ -120,8 +119,11 @@ const en = {
"game.sidebar.empty.inspector": "select an object on the map",
"game.sidebar.empty.order": "order is empty",
"game.sidebar.order.command_delete": "delete",
"game.sidebar.order.submit": "submit",
"game.sidebar.order.submit_in_flight": "submitting…",
"game.sidebar.order.sync.idle": "no changes yet",
"game.sidebar.order.sync.in_flight": "syncing…",
"game.sidebar.order.sync.synced": "synced with server",
"game.sidebar.order.sync.error": "sync failed: {message}",
"game.sidebar.order.sync.retry": "retry",
"game.sidebar.order.status.draft": "draft",
"game.sidebar.order.status.valid": "valid",
"game.sidebar.order.status.invalid": "invalid",
@@ -130,7 +132,6 @@ const en = {
"game.sidebar.order.status.rejected": "rejected",
"game.sidebar.order.label.placeholder": "{label}",
"game.sidebar.order.label.planet_rename": "rename planet {planet} → {name}",
"game.sidebar.order.error.batch_failed": "submit failed: {message}",
"game.bottom_tabs.map": "map",
"game.bottom_tabs.calc": "calc",
"game.bottom_tabs.order": "order",