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
+22 -18
View File
@@ -213,7 +213,7 @@ async function clickPlanetCentre(page: Page): Promise<void> {
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
}
test("rename a seeded planet, submit, observe overlay + persist after reload", async ({
test("rename a seeded planet auto-syncs and the overlay survives reload", async ({
page,
}, testInfo) => {
test.skip(
@@ -241,32 +241,30 @@ test("rename a seeded planet, submit, observe overlay + persist after reload", a
await input.fill("New-Earth");
await sidebar.getByTestId("inspector-planet-rename-confirm").click();
// Open the order tab and assert the row.
// Overlay applies immediately on `valid` — no Submit click is
// required because the auto-sync pipeline drives the network.
await expect(sidebar.getByTestId("inspector-planet-name")).toHaveText(
"New-Earth",
);
// Open the order tab and assert the row plus the synced status bar.
await page.getByTestId("sidebar-tab-order").click();
const orderTool = page.getByTestId("sidebar-tool-order");
await expect(orderTool.getByTestId("order-command-label-0")).toContainText(
"New-Earth",
);
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
"valid",
);
await orderTool.getByTestId("order-submit").click();
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
"applied",
);
await expect(orderTool.getByTestId("order-sync")).toHaveAttribute(
"data-sync-status",
"synced",
);
expect(handle.submittedRenameName).toBe("New-Earth");
// Switch back to the inspector — overlay should reflect the new name.
await page.getByTestId("sidebar-tab-inspector").click();
await expect(sidebar.getByTestId("inspector-planet-name")).toHaveText(
"New-Earth",
);
// Reload: the order draft is persisted; on cache-miss boots the
// hydrate-from-server path takes over. Both round-trips re-apply
// the overlay so the player still sees the renamed planet.
// Reload: the layout always polls user.games.order.get on boot,
// so the overlay is rebuilt from the server's stored order even
// when the local cache was wiped.
await page.reload();
await expect(page.getByTestId("active-view-map")).toHaveAttribute(
"data-status",
@@ -303,11 +301,17 @@ test("rejected submit keeps the old name and surfaces the failure", async ({
await page.getByTestId("sidebar-tab-order").click();
const orderTool = page.getByTestId("sidebar-tool-order");
await orderTool.getByTestId("order-submit").click();
// The auto-sync pipeline reaches the server immediately after
// the inline confirm; the rejected verdict surfaces through the
// per-row status badge and the sync bar.
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
"rejected",
);
await expect(orderTool.getByTestId("order-sync")).toHaveAttribute(
"data-sync-status",
"error",
);
await page.getByTestId("sidebar-tab-inspector").click();
// Overlay does not apply rejected commands — old name persists.