Files
galaxy-game/ui/PLAN.md
T
Ilia Denisov fbc0260720 phase 5: wasm core, GalaxyClient skeleton, Connect-Web stubs
Compile `ui/core` to WebAssembly via TinyGo (903 KB) and expose four
canonical-bytes / signature-verification functions on
`globalThis.galaxyCore` from `ui/wasm/main.go`. The TypeScript-side
`Core` interface plus a `WasmCore` adapter (browser + JSDOM loader)
bridge those into a typed shape, and a `GalaxyClient` skeleton wires
`Core.signRequest` → injected `Signer` → typed Connect client →
`Core.verifyPayloadHash` / `verifyResponse`.

Wire `ui/buf.gen.yaml` against the local
`@bufbuild/protoc-gen-es` v2 binary (devDependency) so the codegen
step does not depend on the buf.build BSR. Vitest covers the bridge
end-to-end: per-method WasmCore tests under JSDOM, byte-for-byte
canon parity against the gateway fixtures committed in Phase 3, and
a `GalaxyClient` orchestration test using
`createRouterTransport`. The committed `core.wasm` snapshot tracks
TinyGo output so contributors run `make wasm` only when `ui/core/`
changes; CI consumes the snapshot directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 12:58:37 +02:00

76 KiB
Raw Blame History

UI Client Implementation Plan

This plan stages the implementation of the cross-platform UI client for Galaxy. The client builds from a single TypeScript + Svelte codebase to five targets: web, web-mobile, standalone PC (mac/win/linux), iOS, and Android. A shared Go module (ui/core) carries envelope cryptography, FlatBuffers codec, keypair management, and a thin bridge over pkg/calc/ for UI-side game math; it is compiled to WASM (web), gomobile native libraries (iOS/Android), and embedded directly in Wails (desktop). All network I/O lives on the TypeScript side via ConnectRPC, so the Go module is a pure compute boundary on every platform.

The existing Fyne client in client/ is deprecated and is not modified or imported by the new code. The strategy and rationale behind these choices live in the plan file at /Users/id/.claude/plans/buzzing-questing-fountain.md; the architectural overview is mirrored into ui/README.md as part of Phase 1.

Each phase ends with a runnable artifact. The visual progression is: empty page → navigation skeleton → stubbed views → live data → real actions. Phases are sized so that any one of them can be shipped, run, and reviewed before the next starts; if a direction proves wrong, the plan can be adjusted with at most one phase of rework.


Summary

This plan breaks implementation into 36 small reviewable phases. Each phase has a single primary goal, clear deliverables, explicit dependencies, acceptance criteria, and focused tests. Tests live alongside the code added in the phase; a phase is not closed until its tests are green on the targets it claims to support.

The intended v1 architecture is:

  • TypeScript + Svelte 5 frontend, shared across all five build targets;
  • PixiJS v8 with dual WebGPU/WebGL backend for the world map renderer;
  • Go module ui/core as a compute-only library (canonical bytes, sign/verify, FlatBuffers codec, keypair, thin bridge to pkg/calc/) compiled to WASM, gomobile, and Wails-embedded native;
  • TypeScript-side Core interface with three adapters (WasmCore, WailsCore, CapacitorCore) selected at build time;
  • GalaxyClient on top of Core performs all network I/O via ConnectRPC (@connectrpc/connect-web) on every platform;
  • per-platform storage: WebCrypto + IndexedDB on web, OS keychain + SQLite on desktop, iOS Keychain / Android Keystore + SQLite on mobile, all behind a single KeyStore and Cache TypeScript interface;
  • mobile-first navigation: one active view occupies the main area at a time; sidebar holds a single tool (calculator, inspector, or order) with persistent state on switch.

Assumptions and Defaults

  • Target Go version follows go.mod of the parent module; TinyGo for WASM must support crypto/ed25519 and crypto/sha256. If TinyGo support is insufficient, fall back to standard Go GOOS=js GOARCH=wasm with a larger bundle (~2 MB).
  • The gateway exposes server-streaming gRPC. Browsers cannot speak raw gRPC; ConnectRPC support is added to the gateway so a single set of Go handlers serves native gRPC and browser clients simultaneously.
  • TypeScript-side network code uses @connectrpc/connect-web for unary calls and server-streaming push events on every platform.
  • Ed25519 private keys never leave the device. Loss of secure storage is acceptable on every platform and triggers a re-login flow.
  • Build pipeline is a single pnpm workspace at ui/; Make targets wrap TinyGo, gomobile, Wails CLI, Capacitor CLI, and Vite.
  • All file/directory names, code, comments, identifiers, and docs in ui/ are written in English. Russian appears only in i18n bundles delivered in Phase 35.
  • Pre-production migration rule from the project root applies: schema changes are inlined into the existing init schema rather than producing new migrations; clean rebuilds on every checkout.
  • The existing client/ package is deprecated. New code does not import from it. Existing types in pkg/model/client/ are not migrated; UI types are written from scratch in ui/core/types/ as needed.
  • The client/world/ algorithm is treated as a reference description for the new TypeScript renderer. Tile-based spatial indexing is intentionally omitted in the first iteration; PixiJS native culling and bounds-based hit testing carry the renderer until profiling proves otherwise.
  • Game math that must stay synchronised between server and client lives in pkg/calc/. The UI client never duplicates calc functions; instead a bridge layer in ui/core/calc/ wraps selected pkg/calc/ functions for the Core API. New shared math is added to pkg/calc/ first; gaps are surfaced at the start of each phase that needs them.
  • State preservation is a global rule: switching active view or sidebar tab does not reset state. State resets only on explicit user discard actions or logout.
  • History mode is a global read-only toggle that applies to every active view. The Order sidebar tab is hidden in history mode.
  • Wails v2 is the desktop baseline. At the start of Phase 31, the current state of Wails v3 is re-evaluated; if v3 has reached a stable release, the migration is folded into that phase.
  • CI uses Gitea Actions (workflow files under .gitea/workflows/, format-compatible with GitHub Actions). Linux runners cover Tier 1 tests; a macOS runner is provisioned only when Tier 2 iOS smoke is needed.

Information Architecture and Navigation

The client is a single-page application with one active view at a time. Navigation is mobile-first: floating panels never overlap the map, the main area never splits into multiple visible panels on small screens. Desktop and mobile share the same model; on desktop, the sidebar sits beside the active view, on mobile it lives behind a bottom-tab bar.

View model

ActiveView ∈ {
  /login,                            (anonymous only)
  /lobby,                            (auth required)
  /games/:id/map,                    (default in-game view)
  /games/:id/table/:entity,          (entity ∈
                                      planets | ship-classes |
                                      ship-groups | fleets |
                                      sciences | races)
  /games/:id/report,
  /games/:id/battle/:battleId,
  /games/:id/mail,
  /games/:id/designer/ship-class/:id?,
  /games/:id/designer/science/:id?,
}

Switching between views happens through the header dropdown (desktop) or hamburger menu (mobile). Double-tapping a row in a table: view returns to /map with focus=<objectId>. Some views can push a transient map overlay with a back affordance (for example, ship-class designer pushes a range-preview overlay onto the map). The transient overlay clears when the user navigates to any other view.

Layout per breakpoint

Desktop (≥ 1024 px):

┌──────────────────────────────────────────────────────────┐
│ Header: race · turn N · countdown · view dropdown · ⚙    │
├────────────────────────────────────────────┬─────────────┤
│                                            │ tabs        │
│              active view                   │ ┌─────────┐ │
│             (map / table /                 │ │ Calc    │ │
│              battle / mail /               │ │ Inspect │ │
│              designer / report)            │ │ Order   │ │
│                                            │ └─────────┘ │
│                                            │ tool        │
│                                            │ content     │
│                                            │             │
└────────────────────────────────────────────┴─────────────┘

Tablet (7681024 px): same as desktop but sidebar collapses to a swipe-from-right drawer; a tab bar of three icons sits in the header right corner.

Mobile (< 768 px):

┌──────────────────────┐
│ ☰  race · turn N · ⚙ │
├──────────────────────┤
│                      │
│     active view      │
│                      │
│                      │
│                      │
├──────────────────────┤
│  ▣    🧮    📝   ☰   │
│ Map  Calc  Order More│
└──────────────────────┘

On mobile, Inspector is not a bottom tab — tapping an object on the map raises a bottom-sheet showing inspector content. The sheet swipes down to dismiss. More opens a hamburger menu that lists Mail, Battle log, Tables (planets, ship classes, ship groups, fleets, sciences, races), History, Settings, Logout.

Sidebar tools (single-tool with state preservation)

  • Calculator — independent ship/path calculator, callable from any view. Holds in-progress inputs across navigation.
  • Inspector — context-sensitive details for the currently selected map object. Empty state when nothing is selected: select an object on the map.
  • Order — the draft order being composed. Vertical list of commands, top-to-bottom. Each command shows its local-validation result while composing and its server result after submit. Order persists across page reloads and across view switches.

Map active view

PixiJS canvas with pan/zoom over the torus. A gear icon in the corner opens a popover (desktop) or bottom sheet (mobile) with category toggles:

Toggleable Default
Hyperspace groups on
Incoming groups (not necessarily enemy) on
Cargo routes on
Reach / visibility zones off
Battle and bombing markers on

Planets are always shown and cannot be hidden.

Header turn counter and history mode

The turn counter is clickable. Click expands to a turn navigator (popover desktop, bottom sheet mobile) listing recent turns with a search field for jumping to a specific turn number. Selecting a past turn enters history mode: every active view switches its data source to that turn's snapshot, the Order sidebar tab disappears, and a persistent banner reads Viewing turn N · read-only with a Return to turn current action.

Cross-cutting shell

  • Push-event toasts surface from the top of the screen for: turn ready, lobby state changes, invitations, session revoked, incoming attack.
  • A connection-state indicator in the header shows online / reconnecting / offline based on push-stream state and last successful unary call.
  • The account menu (top-right on desktop, last hamburger entry on mobile) holds Settings, Sessions, Theme, Language, Logout.

Authenticated route transitions

  • /login/lobby after successful confirm-email-code.
  • /lobby/games/:id/map when a game card is selected.
  • Any view → /login immediately on session revocation push event.
  • Designer views can push a transient overlay onto /map; the back affordance returns to the originating designer.

Per-screen behaviour (validations, exact field names, error mappings) is derived from docs/FUNCTIONAL.md sections cited in the relevant phases below. UI-specific decisions (animations, layout, microcopy) live in per-phase topic docs under ui/docs/.


Phase 1. Workspace Skeleton

Status: done.

Goal: bring up the ui/ workspace with a runnable empty SvelteKit + Vite frontend and architectural anchors.

Artifacts:

  • ui/README.md mirroring the architectural overview from this plan
  • ui/Makefile with placeholder targets for every build type (web, wasm, gomobile, desktop-{mac,win,linux}, ios, android, all)
  • ui/pnpm-workspace.yaml declaring the single-package pnpm workspace
  • ui/frontend/ Svelte 5 + SvelteKit + Vite + TypeScript project (the SvelteKit scaffold provides +layout.svelte, +page.svelte, static/, and the file-system router used by later phases)
  • ui/frontend/src/routes/+page.svelte minimal landing page rendering the app version string in the page footer; the version is read at build time by Vite define from ui/frontend/package.json
  • ui/frontend/{vitest.config.ts, tests/} minimum Vitest harness needed to run the smoke test below (vitest, jsdom, @testing-library/svelte); the rest of the test toolchain (Playwright, @testing-library/jest-dom, CI workflows) lands in Phase 2
  • ui/.gitignore covering node_modules, dist, *.wasm, build outputs for Wails and Capacitor, Playwright artefacts
  • ui/docs/ empty directory ready for per-phase topic docs

Dependencies: none.

Acceptance criteria:

  • pnpm install && pnpm dev from ui/frontend starts a dev server that serves the landing page at a free local port;
  • make lists every planned build target as a placeholder;
  • ui/README.md lists the five target platforms, the layered architecture, and points readers to per-phase topic docs under ui/docs/.

Targeted tests:

  • a single Vitest smoke test that mounts the landing component and asserts the rendered version string is non-empty.

Phase 2. Testing Infrastructure

Status: done.

Goal: install and configure the test toolchain that every later phase depends on, including Tier 1 (per-PR) and Tier 2 (release) targets.

Artifacts:

  • ui/frontend/package.json dev-dependencies (added on top of the Phase 1 minimum of vitest, jsdom, @testing-library/svelte): @testing-library/jest-dom, playwright, @playwright/test
  • ui/frontend/vitest.config.ts extended with setupFiles: ["./tests/setup.ts"] to wire @testing-library/jest-dom matchers into Vitest (the JSDOM environment itself is wired in Phase 1)
  • ui/frontend/tests/setup.ts registering jest-dom matchers
  • ui/frontend/tests/e2e/landing.spec.ts placeholder Playwright test asserting the version footer renders
  • ui/frontend/playwright.config.ts with four projects: chromium-desktop, webkit-desktop, chromium-mobile-iphone-13, chromium-mobile-pixel-5; tracing and screenshots enabled on failure; webServer: pnpm run dev on port 5173
  • .gitea/workflows/ui-test.yaml running Tier 1 on every push and PR on a Linux runner: monorepo Go service tests for backend/, gateway/, game/, and every pkg/<name>/ module (each pkg module is enumerated explicitly because they sit as independent go.work modules under a shared pkg/ directory, and ./pkg/... does not recurse across module boundaries). All Go tests run with -count=1 so the cache never masks a failing run; backend tests additionally run with -p 1 because most backend packages spawn their own Postgres testcontainer and parallel bootstraps starve each other on the runner. The integration suite stays gated behind make -C integration integration and lives outside Tier 1; the deprecated client/ Fyne client (see §74) is also excluded — its tests, code, and documentation are frozen and CI must not run them. Then pnpm install --frozen-lockfile from ui/, pnpm exec playwright install --with-deps, pnpm test, pnpm exec playwright test; Playwright reports and traces uploaded as artefacts on failure
  • .gitea/workflows/ui-release.yaml running Tier 2 on tag push (v*): same Tier 1 step set today; visual-regression and macOS-runner iOS-smoke jobs live as commented sections marked with the phase number that re-enables them (Phase 33 and Phase 32 respectively)
  • ui/docs/testing.md topic doc naming the two tiers, the tools per tier, and the rule that visual regression baselines live in ui/frontend/tests/__snapshots__/ until shifted to Argos

Dependencies: Phase 1.

Acceptance criteria:

  • a placeholder Vitest test passes locally and in CI;
  • a placeholder Playwright test passes in chromium-desktop and webkit-desktop projects locally;
  • the Gitea Actions Tier 1 workflow runs end-to-end against a clean clone of the repo on a Linux runner. Until the Gitea runner is provisioned, the workflow is exercised locally with act -W .gitea/workflows/ui-test.yaml.

Targeted tests:

  • placeholder Vitest test from Phase 1 runs in CI and passes;
  • placeholder Playwright test runs in CI on Linux runner and passes in both chromium-desktop and webkit-desktop projects;
  • intentional failure produces a Playwright trace artefact in CI.

Phase 3. Go Core: Canonical Bytes and Keypair

Status: done.

Goal: implement the canonical-bytes serializer and Ed25519 keypair management in pure Go, with bit-for-bit parity to the gateway-side implementation. No network, no UI.

Artifacts:

  • ui/core/go.mod module galaxy/core declared in the project Go workspace (go.work use and replace directives)
  • .gitea/workflows/ui-test.yaml and .gitea/workflows/ui-release.yaml extended to add ./ui/core/... to the Tier 1 / Tier 2 go test command list introduced in Phase 2
  • ui/core/canon/ canonical bytes for galaxy-request-v1, galaxy-response-v1, and galaxy-event-v1, matching docs/ARCHITECTURE.md §15 byte-for-byte. Server-only signers (Ed25519ResponseSigner, PKCS#8 PEM loaders) intentionally stay in gateway/authnui/core is verify-only on the server side
  • ui/core/keypair/ Ed25519 generate, marshal, unmarshal helpers over opaque []byte blobs; Generate accepts an injected io.Reader so the WASM build can wire in crypto.getRandomValues
  • ui/core/types/ full v1 transport-envelope structs with SigningFields() projection helpers; result-code and protocol-version constants (ProtocolVersionV1, ResultCodeOK). TraceID is part of the request envelope but deliberately excluded from the request signing input (matches §15)
  • ui/core/canon/testdata/ golden JSON test vectors for the three Phase-3 message types plus one response and one event
  • ui/core/README.md documenting the public API and the network-free / storage-free / no-x509 / no-PEM / no-os invariant
  • gateway/authn/parity_with_ui_core_test.go (cross-module test) proving canonical-bytes parity and bidirectional sign/verify acceptance between gateway/authn and galaxy/core. The test adds require galaxy/core to gateway/go.mod (test-only in practice — gateway production binary does not link ui/core)

Dependencies: Phase 1.

Acceptance criteria:

  • canonical-bytes output matches gateway-side output byte-for-byte for the three Phase-3 message types (user.account.get, lobby.my.games.list, user.games.command);
  • a request signed by ui/core is accepted by the gateway's own verifier in a unit test (TestParityRequestSignedByUICoreAcceptedByGateway);
  • a response signed by gateway/authn's Ed25519ResponseSigner is accepted by ui/core's verifier (TestParityResponseSignedByGatewayAcceptedByUICore); the same applies to gateway-signed events;
  • tampered payload_hash, mismatched request_id, mismatched timestamp_ms, and invalid signature length are rejected with stable error codes from ui/core/canon. Server-side freshness enforcement (the symmetric ±5 minutes around server time) stays in gateway/internal/grpcapi/freshness_replay.go and is not duplicated in ui/core.

Targeted tests:

  • canonical-bytes equality tests on golden JSON fixtures (testdata/) for every envelope kind;
  • round-trip sign-then-verify across all three envelope kinds;
  • negative tests: tampered payload_hash, mismatched request_id, mismatched timestamp_ms, invalid signature lengths (too short, too long, empty), bit-flipped signature, wrong public key, malformed base64 public key;
  • gateway/authn cross-module parity tests as listed under Artifacts.

Phase 4. ConnectRPC Support in Gateway

Status: done. Cross-service phase — work happened in gateway/ and integration/, not ui/.

Goal: enable browsers to call the gateway's authenticated edge surface through ConnectRPC, without keeping a separate gRPC server bootstrap alive purely for test clients.

Decision (taken with the project owner before implementation): the existing native-gRPC grpc.NewServer bootstrap was replaced with a single connectrpc.com/connect HTTP/h2c listener, since Connect-Go natively serves the Connect, gRPC, and gRPC-Web protocols on the same port. No production gRPC clients existed to preserve. The package gateway/internal/grpcapi keeps its name for diff-size reasons and documents the historical labelling in its package doc.

Artifacts (delivered):

  • gateway/buf.gen.yaml extended with buf.build/connectrpc/go, generating gateway/proto/galaxy/gateway/v1/gatewayv1connect/edge_gateway.connect.go
  • gateway/internal/grpcapi/server.go rewritten around http.Server
    • h2c.NewHandler + gatewayv1connect.NewEdgeGatewayHandler
  • new gateway/internal/grpcapi/connect_handler.go adapting the existing gatewayv1.EdgeGatewayServer decorator stack to the Connect handler interface, including a grpc.ServerStreamingServer shim around *connect.ServerStream[GatewayEvent] and a gRPC status.Error*connect.Error translation helper
  • new gateway/internal/grpcapi/connect_observability.go Connect interceptor recording the same metric and structured-log shape the gRPC interceptors emitted; the rate-limit decorator now reads peer IP from a context value populated by the interceptor instead of peer.FromContext
  • updated gateway/README.md (Transport Matrix + "Authenticated Edge Surface"), gateway/docs/runtime.md, gateway/docs/flows.md, gateway/docs/runbook.md, and docs/ARCHITECTURE.md §15
  • migrated tests: gateway/internal/grpcapi/server_test.go, test_fixtures_test.go, and every *_integration_test.go in that package now drive a gatewayv1connect.EdgeGatewayClient over HTTP/2 cleartext loopback
  • migrated harness: integration/testenv/grpc_client.goconnect_client.go. SignedGatewayClient keeps the same public shape (Execute, SubscribeEvents, Close) but speaks Connect internally; Is* helpers now use connect.CodeOf

Dependencies: Phase 3 (canonical bytes are needed for the fixture-level signing the migrated tests use).

Acceptance criteria (met):

  • unary Connect calls from outside the gateway process succeed end-to-end against the authenticated surface — verified by the migrated grpcapi/server_test.go and command_routing_integration_test.go scenarios driving the Connect client over loopback h2c;
  • server-streaming SubscribeEvents works over Connect with the signed gateway.server_time bootstrap event delivered first — verified by TestSubscribeEventsValidEnvelopeSendsBootstrapEventAndWaitsForCancellation;
  • the unified listener still natively accepts gRPC and gRPC-Web framing for any future native client (Connect-Go's documented multi-protocol support);
  • the Connect handler shares the same upstream business code as the unified listener — there is exactly one decorator stack (grpcapi.NewServers.service).

Targeted tests (delivered):

  • Connect unary integration tests in gateway/internal/grpcapi/ exercising the full envelope → signature → freshness/replay → rate-limit → routing pipeline through the new Connect transport;
  • Connect streaming integration tests asserting bootstrap-event delivery, replay rejection on stream open, and shutdown closure;
  • the existing gateway test suite (go test ./gateway/...) stays green.

Decision deviation note: the planned standalone gateway/internal/grpcapi/connect_server_test.go was not added as a separate file because the migrated *_test.go files in the same package already cover unary happy + streaming bootstrap + protocol- version reject through the Connect client. A duplicate file would not add coverage. Future contributors looking for "the Connect tests" can read any file in gateway/internal/grpcapi/ — they all use the Connect client now.

Phase 5. WASM Build, WasmCore Adapter, GalaxyClient Skeleton

Status: done.

Goal: package ui/core as a WASM module, expose it to TypeScript through a typed adapter, and prove the WASM-side crypto pipeline at unit level. End-to-end Connect round-trip is validated in Phase 7 (authenticated calls only become possible after login).

Decisions taken with the project owner before implementation:

  1. TinyGo as primary toolchain. core.wasm lands at 903 KB — well under the 1 MB acceptance bar. The GOOS=js GOARCH=wasm fallback path stays documented in ui/docs/wasm-toolchain.md.
  2. Core.signRequest returns canonical bytes only. No private key inside WASM; Phase 6 plugs WebCrypto's non-exportable keys at the orchestration layer. GalaxyClient takes a pluggable Signer so Phase 5 tests pass a fixture-key signer and Phase 6 swaps in WebCrypto without touching the orchestration.
  3. TS codegen runs locally, not against buf.build BSR. A new ui/buf.gen.yaml invokes frontend/node_modules/.bin/protoc-gen-es (added as a devDependency). This sidesteps BSR rate limiting and removes the network dependency from the codegen step.
  4. Field naming is camelCase end-to-end. Both the TS Core interface and the Go bridge in ui/wasm/main.go use camelCase field names; there is no snake-case translation layer.

Artifacts (delivered):

  • ui/wasm/main.go TinyGo entry point on globalThis.galaxyCore with four functions: signRequest, verifyResponse, verifyEvent, verifyPayloadHash.
  • ui/Makefile wasm and ts-protos targets.
  • ui/buf.gen.yaml with the local Protobuf-ES plugin (single plugin — protobuf-es v2 emits both message types and Connect service descriptors in one file).
  • ui/frontend/src/platform/core/index.ts — typed Core interface plus a loadCore() resolver (Phase 5 ships only the WASM adapter).
  • ui/frontend/src/platform/core/wasm.tsWasmCore adapter for browsers; the JSDOM test path lives next to it in ui/frontend/tests/setup-wasm.ts.
  • ui/frontend/src/api/connect.ts — typed Connect-Web transport + EdgeGatewayClient factory.
  • ui/frontend/src/api/galaxy-client.tsGalaxyClient skeleton with injected Signer and Sha256 dependencies.
  • ui/frontend/src/proto/galaxy/gateway/v1/edge_gateway_pb.ts (generated) and ui/frontend/src/proto/buf/validate/validate_pb.ts (generated as a transitive import via --include-imports).
  • ui/frontend/static/core.wasm (903 KB) + wasm_exec.js (TinyGo shim).
  • Three Vitest files exercising the bridge end-to-end: tests/wasm-core.test.ts (each Core method, including a sanity signRequest check that the canonical bytes start with the v1 domain marker), tests/wasm-core-canon-parity.test.ts (byte-for- byte parity against three request fixtures plus the response and event signature fixtures from ui/core/canon/testdata/), and tests/galaxy-client.test.ts (orchestration through a mock Core and createRouterTransport from @connectrpc/connect).
  • Topic doc ui/docs/wasm-toolchain.md.
  • ui/README.md repository-layout block.

Dependencies: Phases 2, 3, 4.

Acceptance criteria (met):

  • make wasm produces core.wasm deterministically under 1 MB (903 KB measured);
  • WasmCore.signRequest produces canonical bytes byte-for-byte identical to the gateway-side fixtures for three message types (request_user_account_get, request_user_games_command, request_lobby_my_games_list);
  • WasmCore exposes the same Core TypeScript types future WailsCore and CapacitorCore adapters will satisfy.

Targeted tests (delivered):

  • Vitest unit tests for WasmCore calling each public method with the WASM module loaded in JSDOM via tests/setup-wasm.ts;
  • Vitest unit tests for GalaxyClient using a mock Core and the in-memory createRouterTransport;
  • Vitest tests asserting WasmCore.signRequest output matches the committed gateway fixtures byte-for-byte for the three request message types listed above.

Decision deviation note: the initial plan listed protoc-gen-es and protoc-gen-connect-es as separate plugins. Protobuf-ES v2 generates service descriptors in the _pb.ts file directly, so a single @bufbuild/protoc-gen-es plugin is sufficient — @connectrpc/connect v2 consumes those descriptors via createClient. The connect-es plugin is a v1-only path and is intentionally not used here.

Phase 6. Storage Layer (Web)

Status: pending.

Goal: persist the device session keypair securely in browsers, and provide a generic local cache for game state. Defines the TypeScript-side KeyStore and Cache interfaces that desktop and mobile adapters will satisfy in later phases.

Artifacts:

  • ui/frontend/src/platform/store/index.ts defining KeyStore and Cache interfaces
  • ui/frontend/src/platform/store/idb-cache.ts IndexedDB-backed Cache using the idb library
  • ui/frontend/src/platform/store/webcrypto-keystore.ts WebCrypto non-exportable Ed25519 key generation and IndexedDB handle persistence
  • ui/frontend/src/api/session.ts thin layer that loads or creates the device session at app startup

Dependencies: Phase 5.

Acceptance criteria:

  • a freshly generated keypair survives page reloads and produces signatures that the gateway accepts;
  • clearing site data removes the keypair, and the next request triggers a re-login flow;
  • KeyStore and Cache interfaces have full TypeScript types and zero web-specific imports in their public signatures.

Targeted tests:

  • Vitest unit tests for IDBCache with fake-indexeddb;
  • Vitest unit tests for WebCryptoKeyStore exercising generate, load, sign, clear;
  • Playwright integration test: generate keypair, sign a request through WasmCore, send through Connect, verify gateway accepts, reload the page, sign another request, verify accepted.

Phase 7. Auth Flow UI

Status: pending.

Goal: implement the full email-code login flow with device session registration and post-login redirect to a placeholder lobby.

Artifacts:

  • ui/frontend/src/routes/login two-step form (email → code)
  • ui/frontend/src/api/auth.ts wraps public.auth.send_email_code and public.auth.confirm_email_code, registers the public key, persists via KeyStore
  • ui/frontend/src/lib/session-store.ts Svelte store exposing the current session state
  • ui/frontend/src/routes/+layout.svelte redirect to /login for unauthenticated routes; redirect to /lobby on successful confirm
  • placeholder ui/frontend/src/routes/lobby/+page.svelte rendering you are logged in
  • topic doc ui/docs/auth-flow.md describing error UX, code resend, expired challenge handling, and re-login on revocation

Dependencies: Phase 6.

Acceptance criteria:

  • a fresh browser completes login end-to-end against a local gateway+backend stack;
  • the first authenticated Connect call after login (e.g. user.account.read) succeeds end-to-end through WasmCoreGalaxyClient → ConnectRPC → gateway, with the response signature verified and the payload decoded back to JSON;
  • a returning browser resumes the session without re-login;
  • gateway-side session revocation closes the active client immediately and routes back to /login.

Targeted tests:

  • Vitest component tests for the login forms with mocked GalaxyClient;
  • Playwright e2e test driving the full flow against a local stack in desktop and mobile viewports, asserting the first authenticated Connect call returns successfully after login;
  • regression test for revocation: server-side revoke causes client redirect within one second.

Phase 8. Lobby UI

Status: pending.

Goal: replace the placeholder lobby with a working list of games allowing the user to view membership, accept invitations, join public games, and create new games.

Artifacts:

  • ui/frontend/src/routes/lobby/+page.svelte landing page sections: my games (docs/FUNCTIONAL.md §4.5), public games (§4.2), pending invitations (§4.3), action to create a new game (§3.3)
  • ui/frontend/src/api/lobby.ts typed wrappers over the relevant authenticated RPCs
  • ui/frontend/src/routes/lobby/create/+page.svelte create-game form matching backend contract
  • routing wiring: clicking a game card navigates to /games/:gameId/map (placeholder until Phase 10)

Dependencies: Phase 7.

Acceptance criteria:

  • the user can list, create, join a game, and accept an invitation end-to-end against a local stack;
  • mobile viewport renders without horizontal scroll;
  • empty states are explicit (no games yet, no public games).

Targeted tests:

  • Vitest component tests for each section with mocked API responses;
  • Playwright e2e: complete a create-game flow and confirm the new game appears in my games;
  • mobile-viewport Playwright run for the same flow.

Phase 9. Map Renderer with Fixture Data

Status: pending.

Goal: stand up the PixiJS map renderer with pan/zoom, primitive drawing, torus wrap behaviour and bounded-plane (no-wrap) mode against a fixture dataset, before wiring it to live game state. Both modes are first-class — no-wrap is a real user-selectable view option, not a deferred nicety.

Artifacts:

  • ui/frontend/src/map/world.ts data model (Point2D, Primitive, Style, theme bindings) with fixed-point coordinate handling
  • ui/frontend/src/map/render.ts PixiJS scene graph: background layer, primitive container, viewport pan/zoom, torus wrap copies, dual WebGPU/WebGL backend selection
  • ui/frontend/src/map/hit-test.ts PixiJS-native hit test wrapping eventMode and per-primitive hit slop
  • ui/frontend/src/map/no-wrap.ts camera clamp helpers (CorrectCameraZoom, ClampCameraNoWrapViewport, ClampRenderParamsNoWrap, PivotZoomCameraNoWrap) for bounded plane mode
  • ui/frontend/src/routes/playground/+page.svelte development page rendering a fixture world with a mode switch between torus and no-wrap for visual verification
  • topic doc ui/docs/renderer.md describing departures from the Go reference algorithm in client/world/, the rationale for skipping tile-based spatial indexing, and the no-wrap semantics

Dependencies: Phase 1.

Acceptance criteria:

  • a 1000-primitive fixture world pans and zooms at 60 fps on a mid-range laptop with WebGPU and falls back cleanly to WebGL in both torus and no-wrap modes;
  • hit testing returns the same primitive as the reference Go algorithm on a shared set of fixture cursor positions, in both modes;
  • torus wrap renders all four corner copies correctly across the viewport edges;
  • no-wrap mode clamps the camera at world boundaries; pivot zoom keeps the world point under the cursor stable; viewport never becomes larger than the world.

Targeted tests:

  • Vitest unit tests for fixed-point math, torus-shortest distance, no-wrap clamps, no-wrap pivot zoom invariants;
  • Vitest hit-test parity tests against fixtures derived from the Go reference, covering both torus and no-wrap fixtures;
  • Playwright visual smoke test of the playground page in chromium-desktop and webkit-desktop, exercising mode switch torus → no-wrap and back, and verifying camera clamp behaviour at bounded-plane edges.

Phase 10. In-Game Shell with View-Replacement Skeleton

Status: pending.

Goal: assemble the in-game layout shell (header, sidebar, main area) with empty placeholder content for every view, so navigation works end-to-end before any data is wired.

Artifacts:

  • ui/frontend/src/routes/games/[id]/+layout.svelte shell layout with responsive breakpoints (desktop / tablet / mobile)
  • ui/frontend/src/lib/header/ header component: race name, turn counter (static placeholder turn ?), view dropdown / hamburger, account menu
  • ui/frontend/src/lib/sidebar/ sidebar with three tabs (Calculator, Inspector, Order), each tab content stubbed to coming soon; mobile bottom-tab bar [Map, Calc, Order, More] with corresponding stub panels
  • ui/frontend/src/lib/active-view/ view router supporting /games/:id/{map,table/:entity,report,battle/:battleId,mail, designer/...} with stub content per view
  • topic doc ui/docs/navigation.md documenting the active-view model, the state-preservation rule, and the transient map-overlay concept (the back-stack mechanism itself is implemented in Phase 34 when the first overlay user, ship-designer reach circles, ships)

Dependencies: Phase 8.

Acceptance criteria:

  • entering /games/:id/map from the lobby renders the shell with all navigation chrome;
  • header dropdown switches to every other view; mobile hamburger does the same;
  • sidebar tabs preserve their stub state across switches;
  • the responsive layout matches the breakpoint diagrams in Information Architecture and Navigation.

Targeted tests:

  • Vitest component tests for header navigation actions;
  • Playwright e2e: visit every view stub via header dropdown, assert empty state copy renders;
  • multi-viewport Playwright run validating layout switches at the 768 px and 1024 px breakpoints.

Phase 11. Map Wired to Live Game State

Status: pending.

Goal: replace the map fixture with real planet data fetched from the gateway for the selected game; planets only, read-only.

Artifacts:

  • ui/frontend/src/api/game-state.ts fetch latest game state via user.games.report
  • ui/frontend/src/map/state-binding.ts map-state synchroniser applying planets to the renderer
  • ui/frontend/src/lib/active-view/map.svelte integrates the renderer with live data and a loading state, defaulting to torus mode and reading the per-game wrap-scrolling preference from Cache (toggle itself is exposed in Phase 29)
  • ui/frontend/src/lib/header/turn-counter.svelte reads the live turn number from game state

Dependencies: Phases 9, 10.

Acceptance criteria:

  • entering /games/:id/map for a game with real planets renders them on the map;
  • the turn counter in the header reflects the actual turn number;
  • map state refreshes on tab focus;
  • view mode (torus / no-wrap) honours the per-game preference if set, defaults to torus otherwise.

Targeted tests:

  • Vitest unit tests for state-binding.ts translating report data to primitives;
  • Playwright e2e against a local stack with seeded game state;
  • regression test: zero-planet game shows the map empty without errors;
  • regression test: per-game wrap-scrolling preference persists and is applied on next visit to the game.

Phase 12. Order Composer Skeleton

Status: pending.

Goal: implement the empty order composer as a persistent vertical list that survives navigation and reloads, ready to receive commands in later phases.

Artifacts:

  • ui/frontend/src/lib/sidebar/order-tab.svelte vertical command list with empty state copy
  • ui/frontend/src/sync/order-draft.ts draft order store backed by Cache, persisting across reloads
  • ui/frontend/src/sync/order-types.ts typed command shape (OrderCommand discriminated union)
  • topic doc ui/docs/order-composer.md describing the draft-replaces-server-order model, the local-validation invariant, and command status semantics

Dependencies: Phases 6, 10.

Acceptance criteria:

  • programmatically adding a stub command shows it in the order tab;
  • closing and reopening the browser preserves the draft;
  • removing a command from the order tab persists the removal;
  • the order tab is hidden in history mode (Phase 26) — wired here as a prop so later phases can flip it.

Targeted tests:

  • Vitest unit tests for order-draft covering add, remove, reorder, persistence;
  • Playwright e2e: programmatically add three stub commands, reload, assert all three persist.

Phase 13. Inspector — Planet (Read-Only)

Status: pending.

Goal: show planet details in the inspector when a planet is clicked on the map; read-only, no actions yet.

Artifacts:

  • ui/frontend/src/lib/sidebar/inspector-tab.svelte empty state (select an object on the map) and routing per selected-object kind
  • ui/frontend/src/lib/inspectors/planet.svelte read-only display of every planet field documented in docs/FUNCTIONAL.md §6 and the rules.txt planet section: name, coordinates, size, population, industry, materials stockpile, industry stockpile, colonists, natural resources, current production type, free production potential
  • map click handler that selects the planet and switches sidebar to Inspector (or raises bottom-sheet on mobile)
  • selection store ui/frontend/src/lib/selection.ts holding the currently selected map object id

Dependencies: Phase 11.

Acceptance criteria:

  • clicking any visible planet on the map shows its details in the inspector tab on desktop and bottom-sheet on mobile;
  • selection state persists across view switches (per global state- preservation rule);
  • empty inspector renders the empty-state message when no planet is selected.

Targeted tests:

  • Vitest component tests for the planet inspector with fixture data;
  • Playwright e2e: click a seeded planet, assert all expected fields are rendered;
  • mobile-viewport Playwright run validating the bottom-sheet presentation.

Phase 14. First End-to-End Command — Rename Planet

Status: pending.

Goal: prove the entire pipeline (inspector → composer → submit → server → state refresh) by wiring up exactly one action: renaming a planet.

Artifacts:

  • ui/frontend/src/lib/inspectors/planet.svelte adds a Rename action that opens a small inline editor and adds a RenamePlanet command to the order draft on confirm
  • ui/frontend/src/sync/submit.ts submitOrder() function that POSTs the entire draft via GalaxyClient.execute('user.games.order', ...) and applies per-command results
  • ui/frontend/src/lib/sidebar/order-tab.svelte adds a Submit order button calling submitOrder() and renders accepted / rejected status per command after submit
  • on successful submit, refresh game state so the rename appears on the map and in the inspector

Dependencies: Phases 12, 13.

Acceptance criteria:

  • the user can select a planet, click Rename, type a new name, see the command appear in the order tab, click Submit, and observe the planet's name change everywhere within one second;
  • attempting an empty or invalid name is blocked locally (button disabled with tooltip);
  • a server-side rejection (race condition) is surfaced as rejected status in the order tab.

Targeted tests:

  • Vitest unit tests for submitOrder with mocked GalaxyClient;
  • Vitest component test for the inline rename editor including validation;
  • Playwright e2e: rename a seeded planet, reload, confirm the new name persists.

Phase 15. Inspector — Planet Production Controls

Status: pending.

Goal: let the user switch a planet's production type to industry, materials, research a science, or build a ship class; each change appends a command to the order draft.

Artifacts:

  • ui/frontend/src/lib/inspectors/planet/production.svelte segmented control with the four production options; a sub-picker for science and ship class targets
  • ui/frontend/src/sync/order-types.ts extends with SetProductionType command variant
  • references to pkg/calc/ predictions (free production potential, forecast output for current type) — wired through ui/core/calc/
  • audit ui/docs/calc-bridge.md updates this phase's required calc functions; if any are missing in pkg/calc/, raise as blocker

Dependencies: Phase 14.

Acceptance criteria:

  • changing production type adds exactly one SetProductionType command to the order draft;
  • repeated changes for the same planet collapse to the latest choice (no duplicate commands per planet);
  • forecast output number reflects the chosen production type and matches pkg/calc/ outputs.

Targeted tests:

  • Vitest unit tests for the collapse-duplicates logic in order draft;
  • Vitest component tests for forecast number rendering;
  • Playwright e2e: switch production three times, submit, confirm server reflects the latest choice.

Phase 16. Inspector — Cargo Routes

Status: pending.

Goal: configure up to four cargo routes per planet (colonists, industry, materials, empty) through the inspector.

Artifacts:

  • ui/frontend/src/lib/inspectors/planet/cargo-routes.svelte four-slot UI listing existing routes and offering add / edit / remove
  • ui/frontend/src/sync/order-types.ts extends with SetCargoRoute and RemoveCargoRoute command variants
  • destination-planet picker filtered by reach (uses pkg/calc/ reach function via ui/core/calc/)
  • ui/frontend/src/map/cargo-routes.ts renders route arrows on the map between source and destination planet, styled per cargo type
  • topic doc ui/docs/cargo-routes-ux.md capturing the priority semantics from rules.txt (colonists → industry → materials → empty)

Dependencies: Phase 15.

Acceptance criteria:

  • the user can add, edit, and remove cargo routes through the inspector;
  • destination picker disables planets outside reach with a tooltip explaining the constraint;
  • the four route types are mutually exclusive — only one route per type per source planet;
  • configured routes are rendered as arrows on the map between source and destination planets, distinguishable per cargo type.

Targeted tests:

  • Vitest unit tests for slot-conflict detection;
  • Vitest unit tests for cargo-route arrow rendering on torus and no-wrap fixtures;
  • Playwright e2e: add a route end-to-end, confirm server applies it on next turn and the arrow is visible on the map.

Phase 17. Ship Classes — CRUD Without Calc

Status: pending.

Goal: list, view, and edit ship classes through a dedicated table view and a designer view; numeric calculations are stubbed pending Phase 18.

Artifacts:

  • ui/frontend/src/routes/games/[id]/table/ship-classes/+page.svelte table of ship classes with sort and filter
  • ui/frontend/src/routes/games/[id]/designer/ship-class/[id]/+page.svelte designer form with all five fields (Drive, Armament, Weapons, Shields, Cargo) plus name; validation rules from rules.txt (each field 0 or ≥1; armament integer; weapons and armament both zero or both nonzero)
  • ui/frontend/src/sync/order-types.ts extends with CreateShipClass and UpdateShipClass command variants

Dependencies: Phase 14.

Acceptance criteria:

  • the user can create, list, edit, and delete ship classes;
  • field validation matches rules.txt constraints with disabled Submit + tooltip when invalid;
  • double-tapping a row in the ship-classes table opens its designer.

Targeted tests:

  • Vitest component tests for designer field validation;
  • Playwright e2e: create a class, list it, edit it, delete it.

Phase 18. Ship Classes — Calc Bridge

Status: pending.

Goal: wire pkg/calc/ ship math into the designer for live mass, speed, range, and cargo capacity previews.

Artifacts:

  • ui/core/calc/ship.go thin Go bridge wrapping pkg/calc/.FullMass, EmptyMass, Speed, CargoCapacity, WeaponsBlockMass, DriveEffective in JSON-marshallable signatures, exported through the Core API
  • ui/frontend/src/platform/core/index.ts extends Core interface with the new calc methods
  • live-updating preview pane in the ship-class designer showing mass, full-load mass, max speed, range, and cargo capacity at the player's current tech levels
  • audit step recorded in ui/docs/calc-bridge.md: every wired function listed against its pkg/calc/ source
  • if any required pkg/calc/ function is missing, this phase raises a blocker and the function is added to pkg/calc/ first (owner-led)

Dependencies: Phases 5 (Core skeleton), 17.

Acceptance criteria:

  • changing any field in the designer updates the preview within one frame on a mid-range laptop;
  • preview values are byte-for-byte identical to direct pkg/calc/ calls on shared fixtures;
  • the bridge contains zero math beyond marshalling adapters.

Targeted tests:

  • Go parity tests in ui/core/calc/ against pkg/calc/ outputs on shared fixtures;
  • Vitest snapshot tests for the preview pane on canonical inputs;
  • Playwright e2e: edit a ship class, observe preview updates and submit, confirm server-side mass matches.

Phase 19. Inspector — Ship Group (Read-Only)

Status: pending.

Goal: render ship groups on the map and display group details in the inspector when a group is selected; read-only, no actions yet.

Artifacts:

  • ui/frontend/src/map/ship-groups.ts renders ship groups on the map: own and visible foreign groups stationed on planets, groups in hyperspace at their current coordinates, and incoming groups with a distinct visual style and an ETA label
  • ui/frontend/src/map/state-binding.ts extends to feed groups into the renderer alongside planets
  • ui/frontend/src/lib/inspectors/ship-group.svelte read-only display of group fields: class, count, tech levels of components, location (planet or hyperspace coordinates), cargo type and amount, fleet membership
  • map click handler that selects a group and switches sidebar to Inspector (or raises bottom-sheet on mobile)
  • selection store extended to support ShipGroup selections

Dependencies: Phases 11, 13.

Acceptance criteria:

  • own and visible foreign ship groups render on the map for a seeded game in both torus and no-wrap modes;
  • incoming groups are visually distinct and show ETA;
  • clicking any rendered group shows its details in the inspector;
  • groups in hyperspace show coordinates and remaining distance in the inspector;
  • cargo type and amount display when applicable.

Targeted tests:

  • Vitest unit tests for the rendering of each group variant (on-planet, in-hyperspace, incoming);
  • Vitest component tests for the ship-group inspector with fixture data covering planet-based, hyperspace, and cargo-loaded variants;
  • Playwright e2e: click each variant from a seeded game, assert all expected fields render.

Phase 20. Inspector — Ship Group Actions

Status: pending.

Goal: enable group operations from the inspector: split, send, load, unload, modernize, dismantle, transfer to race, add to fleet.

Artifacts:

  • action buttons in ui/frontend/src/lib/inspectors/ship-group.svelte with disabled-state and tooltip when local validation rejects
  • ui/frontend/src/sync/order-types.ts extends with SplitGroup, SendGroup, LoadCargo, UnloadCargo, Modernize, Dismantle, TransferToRace, AssignToFleet command variants
  • Send action picks destination through a planet picker filtered by the group's reach (uses pkg/calc/ reach function via Core)
  • Modernize cost preview using pkg/calc/ formula via Core
  • confirmation dialog for Dismantle over a foreign planet with colonists onboard (special-case from rules.txt: colonists die)

Dependencies: Phases 18, 19.

Acceptance criteria:

  • every action either adds the corresponding command to the order draft or is disabled with a tooltip explaining why;
  • splitting a group of N into K and N-K results in two valid commands (the implicit split + the action);
  • destructive actions surface explicit confirmation dialogs;
  • end-to-end execution: send a group, submit order, observe arrival next turn.

Targeted tests:

  • Vitest unit tests for action enablement logic per action;
  • Vitest component tests for the dismantle-with-colonists confirmation;
  • Playwright e2e for at least one complete flow (send a group between two planets) against a local stack.

Phase 21. Sciences — CRUD List + Designer

Status: pending.

Goal: define and manage sciences (named mixes of tech proportions summing to 1.0) through a table view and a designer.

Artifacts:

  • ui/frontend/src/routes/games/[id]/table/sciences/+page.svelte list of sciences with name and four tech proportions
  • ui/frontend/src/routes/games/[id]/designer/science/[id]/+page.svelte designer with four numeric inputs that auto-normalise to 1.0 and a name field
  • ui/frontend/src/sync/order-types.ts extends with CreateScience and UpdateScience command variants
  • topic doc ui/docs/science-designer-ux.md covering auto-normalisation, validation, and the relationship to the planet production picker (Phase 15)

Dependencies: Phase 17.

Acceptance criteria:

  • the user can create, edit, and delete sciences;
  • proportions auto-normalise on edit so the sum is always 1.0;
  • the planet production picker (Phase 15) lists the user's sciences and lets the user select one for research production;
  • name validation matches rules.txt constraints (length, allowed characters, special characters not at start/end, no triple repeats).

Targeted tests:

  • Vitest unit tests for proportion normalisation;
  • Vitest unit tests for science name validation;
  • Playwright e2e: create a science, set a planet to research it, submit, confirm.

Phase 22. Races View — War/Peace Toggle and Votes

Status: pending.

Goal: list other races with their visible stats, expose war/peace toggle and the voting UI.

Artifacts:

  • ui/frontend/src/routes/games/[id]/table/races/+page.svelte table with one row per race, including name, tech levels, total population, total production, planet count, war-or-peace from this race's perspective, votes received
  • per-row toggle for declaring war or peace (adds SetDiplomaticStance command)
  • voting control: a single slot for give my votes to <race> (adds SetVoteRecipient command)
  • alliance summary panel showing the current vote graph and any alliance reaching ≥ 2/3 of total votes

Dependencies: Phase 14.

Acceptance criteria:

  • the user can toggle war / peace and change vote recipient;
  • the alliance summary updates after a server roundtrip;
  • vote counts match server state byte-for-byte.

Targeted tests:

  • Vitest component tests for the alliance summary on canonical fixtures (chain of votes, fork, win condition);
  • Playwright e2e: change diplomatic stance and vote, submit, confirm.

Phase 23. Reports View — Current Turn Sections

Status: pending.

Goal: present every section of the current turn's report as readable panels, mirroring the structure documented in rules.txt and docs/FUNCTIONAL.md §6.4.

Artifacts:

  • ui/frontend/src/routes/games/[id]/report/+page.svelte scrollable layout with one section per report category (galaxy summary, votes, player status, my sciences, foreign sciences, my ship classes, foreign ship classes, battles, bombings, approaching groups, my planets, ships in production, cargo routes, foreign planets, uninhabited planets, unknown planets, my fleets, my ship groups, foreign ship groups, unidentified groups)
  • per-section anchor navigation in a sticky sidebar for quick jumping
  • a back to map action visible at all times

Dependencies: Phase 11.

Acceptance criteria:

  • every report section renders for a seeded game with non-empty data;
  • empty sections render explicit empty-state copy;
  • scroll position resets when switching to another view and is restored on return.

Targeted tests:

  • Vitest component tests for one representative section per data shape (table, list, sub-table);
  • Playwright e2e: open the report, scroll to each section via anchor navigation, assert content present.

Phase 24. Push Events — Turn-Ready

Status: pending.

Goal: subscribe to the server push stream and refresh client state when a turn-ready event arrives.

Artifacts:

  • ui/frontend/src/api/events.ts push-stream subscription wired through GalaxyClient.subscribeEvents and Connect server-streaming
  • on game.turn.ready event: invalidate (game_id, current_turn) cache entries and trigger a fresh report fetch
  • a top-of-screen toast: Turn N+1 is ready. View now. with a button that re-renders the active view against the new turn
  • mandatory event signature verification through ui/core — any verification failure tears down the stream and reconnects with exponential backoff

Dependencies: Phases 23, 4 (Connect streaming in gateway).

Acceptance criteria:

  • a server-side turn cutoff produces a toast within one second;
  • accepting the toast refreshes the active view to the new turn's data without a full page reload;
  • a forged event (test fixture with bad signature) is rejected and the stream reconnects.

Targeted tests:

  • Vitest unit tests for events.ts handling subscribe, event dispatch, error backoff;
  • Playwright e2e: trigger a server turn, observe toast and refresh.

Phase 25. Sync Protocol — Order Queue, Retry, Conflict

Status: pending.

Goal: make the order draft survive network failures and turn cutoffs gracefully, with explicit user feedback on conflicts.

Artifacts:

  • ui/frontend/src/sync/order-queue.ts send loop: on disconnect, hold the most recent submit; on reconnect, retry once; on persistent failure, surface error to the order tab
  • conflict detection: if the server returns turn_already_closed for a submit, mark the entire draft as conflict and surface a Turn N closed before your order was accepted. Edit and resubmit. banner in the order tab
  • topic doc ui/docs/sync-protocol.md covering queue semantics, retry budgets, and conflict UX

Dependencies: Phases 14, 24.

Acceptance criteria:

  • submitting an order while offline queues it and submits successfully on reconnect;
  • a turn cutoff between draft and submit produces a visible conflict banner with no data loss;
  • the order tab clearly distinguishes draft, submitting, accepted, rejected, conflict states per command.

Targeted tests:

  • Vitest unit tests for order-queue covering all state transitions;
  • Playwright e2e: simulate network drop using Playwright's offline mode, submit an order, restore network, confirm submission;
  • regression test: force a turn cutoff during submit, assert conflict banner appears.

Phase 26. History Mode

Status: pending.

Goal: let the user navigate to past turns and view all data as it was, with no order composition allowed.

Artifacts:

  • ui/frontend/src/lib/header/turn-navigator.svelte clickable turn counter expansion: popover (desktop) / bottom-sheet (mobile) listing recent turns and a search field for jumping to a turn number
  • ui/frontend/src/lib/history-mode.ts global toggle wired into every view's data source: when active, all state-binding, table, report, inspector, and map sources read from the historical snapshot for the selected turn
  • ui/frontend/src/lib/header/history-banner.svelte persistent banner reading Viewing turn N · read-only with a Return to current turn action
  • order tab hidden in history mode (already prepared in Phase 12)

Dependencies: Phases 11, 12, 23.

Acceptance criteria:

  • selecting a past turn from the navigator switches every view to that turn's data;
  • order tab disappears from the sidebar; calculator tab remains available;
  • returning to the current turn restores live data and re-shows the order tab with the prior draft intact (state preservation);
  • all UI views (map, tables, report, battle, mail) work in history mode.

Targeted tests:

  • Vitest unit tests for history-mode toggle and per-view source selection;
  • Playwright e2e: enter history mode, navigate three views, return, confirm the order draft survived.

Phase 27. Battle Viewer

Status: pending.

Goal: render battles as a dedicated view with playback controls (play, pause, step forward, step backward, rewind), driven by the server-side combat log; render battle and bombing markers on the map.

Artifacts:

  • ui/frontend/src/map/battle-markers.ts renders markers on the map for current-turn battles and bombings within visibility, clickable to open the battle viewer
  • ui/frontend/src/routes/games/[id]/battle/[battleId]/+page.svelte view with the combatant list, the round-by-round log, and a player control bar
  • ui/frontend/src/lib/battle-player/ round timeline, current-round highlight, per-shot animation
  • entry points to the viewer: marker on map, row in the report's battles section, push-event toast when a battle this turn involved the player
  • topic doc ui/docs/battle-viewer-ux.md covering playback semantics, accessibility (the combat log must be readable as text for users who skip animations)

Dependencies: Phase 23.

Acceptance criteria:

  • battle and bombing markers render on the map for the seeded current-turn report and are clickable to open the viewer;
  • the viewer plays back any battle in the seeded report including multi-round and one-sided battles;
  • step controls allow precise inspection;
  • the same data is accessible as a static text log for accessibility.

Targeted tests:

  • Vitest unit tests for round-state transitions;
  • Vitest unit tests for marker rendering on torus and no-wrap fixtures;
  • Playwright e2e: click a battle marker on the map, play through, step backward, return to the report.

Phase 28. Diplomatic Mail View

Status: pending.

Goal: implement a mail inbox and compose flow as a dedicated view that replaces the map.

Artifacts:

  • ui/frontend/src/routes/games/[id]/mail/+page.svelte two-pane on desktop (list + reading), one-pane stack on mobile
  • compose form for new messages targeting any other race in the game
  • inbox sorted by arrival time, with read/unread state persisted via Cache
  • push-event integration: new mail surfaces a toast and increments an unread badge in the header

Dependencies: Phases 22, 24.

Acceptance criteria:

  • the user can read incoming mail, compose new mail, and reply to mail end-to-end;
  • unread state persists across reloads;
  • server-side delivery confirmations appear on the message thread.

Targeted tests:

  • Vitest component tests for the compose form including field validation;
  • Playwright e2e: send a message between two seeded players, confirm arrival.

Phase 29. Map Toggles

Status: pending.

Goal: deliver the gear-icon control for hiding categories of map content and switching between torus and no-wrap view modes. All toggleable categories are already rendered by earlier phases; this phase only exposes the controls.

Artifacts:

  • ui/frontend/src/lib/active-view/map-toggles.svelte gear icon in the map view's corner; popover (desktop) / bottom sheet (mobile)
  • two sections inside the popover:
    • object visibility: hyperspace groups, incoming groups, cargo routes, reach / visibility zones, battle and bombing markers
    • view options: wrap scrolling (torus / no-wrap)
  • planets are always rendered and not toggleable
  • ui/frontend/src/lib/map/reach-zones.ts implementation of reach / visibility zone overlays, off by default (the only category not yet rendered by earlier phases)
  • toggle state persists per game in Cache

Dependencies: Phases 9 (no-wrap engine), 11 (planets), 16 (cargo routes), 19 (groups, incoming), 27 (battle markers).

Acceptance criteria:

  • toggling each object visibility category hides or shows the corresponding objects on the map within one frame;
  • switching wrap scrolling switches the renderer between torus and no-wrap mode without losing camera position when possible;
  • toggle state persists across reloads per game;
  • the gear popover is reachable on mobile through a comfortable tap target (≥ 44 px).

Targeted tests:

  • Vitest component tests for toggle state persistence;
  • Vitest unit tests for reach-zone rendering on torus and no-wrap fixtures;
  • Playwright e2e in desktop and mobile viewports: toggle each category and the wrap scrolling, assert visual change.

Phase 30. Calculator Tab

Status: pending.

Goal: ship an independent calculator in the sidebar, callable from any view, exposing the full set of pkg/calc/ functions wired through Core.

Artifacts:

  • ui/frontend/src/lib/sidebar/calculator-tab.svelte UI with mode selector (ship calculator, path calculator, modernization cost, bombing power) and per-mode forms
  • bridge entries in ui/core/calc/ for any function not already wrapped by Phase 18
  • topic doc ui/docs/calculator-ux.md documenting modes, layouts, and the rule that calculator inputs persist across navigation

Dependencies: Phase 18.

Acceptance criteria:

  • every calculator mode produces results identical to direct pkg/calc/ calls;
  • inputs persist across view switches per global state-preservation rule;
  • calculator works in history mode against the snapshot's tech levels.

Targeted tests:

  • Vitest snapshot tests per mode on canonical inputs;
  • Playwright e2e: switch modes, confirm input persistence.

Phase 31. Wails Desktop Wrapper

Status: pending. Re-evaluate Wails v2 vs v3 at phase start.

Goal: build a native desktop app for macOS, Windows, and Linux that runs the same frontend bundle and replaces the WASM core with embedded Go code.

Artifacts:

  • topic doc ui/docs/wails-version.md recording the v2-vs-v3 decision made at phase start with rationale
  • ui/desktop/main.go Wails entry point
  • ui/desktop/app.go IPC bindings exposing ui/core API to the WebView through a structured adapter
  • ui/desktop/keychain/ per-OS secure-storage helpers (macOS Keychain via Security framework, Windows DPAPI, Linux Secret Service / file fallback at ~/.config/galaxy/keypair with mode 0600)
  • ui/desktop/sqlite/ modernc.org/sqlite cache wired through Wails IPC
  • ui/frontend/src/platform/core/wails.ts WailsCore adapter
  • ui/frontend/src/platform/store/wails.ts WailsKeyStore and WailsCache adapters
  • ui/desktop/build/icon.icns macOS app icon
  • ui/desktop/build/icon.ico Windows app icon
  • ui/desktop/build/icon.png Linux app icon
  • ui/Makefile targets desktop-mac, desktop-win, desktop-linux
  • topic doc ui/docs/desktop-secure-storage.md documenting the Linux/Windows file fallback for missing keychains

Dependencies: Phase 6 (KeyStore and Cache interfaces); Phases 7 through 30 in their web form (the desktop wrapper exercises the same TypeScript code).

Acceptance criteria:

  • the macOS, Windows, and Linux binaries each launch, complete login, and preserve the keypair across restarts on a fresh user profile;
  • a single source codebase produces all three OS bundles;
  • the same Core and Storage TypeScript interfaces are satisfied as on web, with no platform-specific code outside platform/;
  • Linux file fallback activates when Secret Service is absent and writes with 0600 permissions.

Targeted tests:

  • Go unit tests for each keychain helper, including file fallback;
  • desktop e2e smoke test driven by Wails headless mode running the Phase 7 login Playwright scenario via CDP;
  • regression test: keychain absence on a Linux container without libsecret falls back to file storage.

Phase 32. Capacitor Mobile Wrapper

Status: pending.

Goal: build native iOS and Android apps that run the same frontend bundle and call into a gomobile-compiled ui/core.

Artifacts:

  • ui/mobile-bridge/bridge.go gomobile-friendly façade over ui/core
  • ui/Makefile target gomobile producing Galaxy.framework and galaxy.aar
  • ui/mobile/capacitor.config.ts Capacitor project configuration
  • ui/mobile/plugins/galaxy-core/ custom Capacitor plugin (Swift + Kotlin) wrapping the gomobile artifacts
  • ui/frontend/src/platform/core/capacitor.ts CapacitorCore adapter
  • ui/frontend/src/platform/store/capacitor.ts CapacitorKeyStore and CapacitorCache using @capacitor-community/secure-storage-plugin and @capacitor-community/sqlite
  • ui/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/ iOS app icon set
  • ui/mobile/android/app/src/main/res/mipmap-*/ Android app icon set
  • iOS launch screen and Android splash screen
  • ui/Makefile targets ios and android
  • topic doc ui/docs/mobile-bridge.md describing the plugin API, marshalling strategy, and the manual smoke procedure for this phase

Dependencies: Phase 6; Phases 7 through 30 in their web form.

Acceptance criteria:

  • both the iOS Simulator and an Android Emulator launch the app, complete login, and preserve the keypair across restarts (validated by manual smoke);
  • the same Core and Storage TypeScript interfaces are satisfied as on web and desktop;
  • gomobile build produces deterministic outputs reproducible in CI on a macOS runner.

Targeted tests:

  • Go unit tests for the mobile-bridge façade;
  • Capacitor plugin unit tests on iOS (XCTest) and Android (Espresso);
  • manual smoke procedure: login flow on iOS Simulator and Android Emulator, recorded in ui/docs/mobile-bridge.md. Full Appium automation lands in Phase 36 as part of the acceptance pass.

Phase 33. PWA — Service Worker, Manifest, Web Icons

Status: pending.

Goal: make the web build installable and offline-tolerant on every browser. Native packaging icons live with their respective wrapper phases (31 for desktop, 32 for mobile) — this phase is web-only.

Artifacts:

  • ui/frontend/src/service-worker.ts cache-first asset strategy with stale invalidation on app update
  • ui/frontend/static/manifest.webmanifest PWA manifest
  • ui/frontend/static/icons/ web icon set sized per manifest.webmanifest requirements
  • topic doc ui/docs/pwa-strategy.md covering update flow and offline scope

Dependencies: Phase 25 (offline order queue).

Acceptance criteria:

  • the web app installs as a PWA on Chrome, Edge, and iOS Safari;
  • the service worker survives an app update without serving stale code on the next reload.

Targeted tests:

  • Lighthouse PWA audit at score ≥ 90;
  • Playwright test: install the app, take it offline, verify the cached login route still loads;
  • regression test: bumping the app version invalidates the prior service worker.

Phase 34. Multi-Turn Projection — Single-Turn Forecast and Range Circles

Status: pending. Long-term scope deferred but this phase ships real features.

Goal: ship two concrete projection features (planet next-turn forecast and ship-designer reach circles) plus the transient map-overlay back-stack mechanism that the reach-circles feature is the first user of.

Artifacts:

  • ui/frontend/src/lib/projection/ minimal projection engine that computes one-turn-ahead state for a single planet using pkg/calc/
  • planet inspector forecast section showing next-turn population, industry, materials stockpile, and production progress
  • ui/frontend/src/lib/navigation/transient-overlay.ts push/pop back-stack mechanism for map overlays driven by other views, with a back-button affordance on the map that returns to the originating view with state preserved
  • ship-designer Preview range on map action that pushes a transient overlay onto the map showing concentric reach circles for 1, 2, 3, 4 turns from a chosen origin, computed from the in-progress ship design and the player's current Drive tech via ui/core/calc/
  • topic doc ui/docs/multi-turn-projection.md describing the long-term vision (multi-turn planning mode, scenario branches) and the phased path to it

Dependencies: Phases 17, 18.

Acceptance criteria:

  • the planet inspector shows a forecast section with next-turn values matching pkg/calc/ outputs;
  • the ship-designer Preview range on map button transitions to the map with reach circles drawn from the chosen origin; back returns to the designer with all in-progress state intact;
  • the transient overlay is cleared if the user navigates to any other view via the header dropdown.

Targeted tests:

  • Vitest unit tests for the projection engine on canonical fixtures;
  • Vitest unit tests for the transient-overlay push/pop logic and state preservation;
  • Playwright e2e: open a planet inspector, observe one-turn forecast; open a ship designer, click Preview range on map, see reach circles, click back, return with state intact.

Phase 35. Polish — Accessibility, Localisation, Error UX

Status: pending.

Goal: prepare the client for technical beta with end-user-quality polish.

Artifacts:

  • ui/frontend/src/lib/i18n/ translation bundles for English and Russian, covering every visible string
  • ui/frontend/src/lib/error/ central error surface with stable codes and retry / escalation guidance
  • accessibility audit results recorded under ui/docs/a11y.md
  • keyboard-only navigation paths for lobby, game view, and login
  • focus rings, ARIA labels, screen-reader-only text where needed

Dependencies: Phase 33.

Acceptance criteria:

  • WCAG 2.2 AA compliance on lobby, login, and the in-game shell per axe-core scan;
  • the entire UI is reachable by keyboard only with visible focus rings;
  • every server-side error is mapped to a translated, actionable user message in both languages;
  • locale switch persists across reloads on every platform.

Targeted tests:

  • axe-core integration tests on every top-level view;
  • Vitest tests for the i18n bundle structure and missing-translation detection;
  • Playwright keyboard-only navigation tests.

Phase 36. Acceptance Pass

Status: pending.

Goal: reconcile implementation, documentation, and regression coverage before declaring the client ready for technical beta.

Artifacts:

  • updated ui/README.md, topic docs, and any drift in docs/ARCHITECTURE.md or docs/FUNCTIONAL.md (mirrored to docs/FUNCTIONAL_ru.md)
  • final cross-platform regression run on a release-candidate build
  • ui/docs/release-checklist.md for repeatable releases
  • visual regression baselines committed under ui/frontend/tests/__snapshots__/; if maintenance proves heavy, follow-up issue to switch to self-hosted Argos
  • Appium harness for iOS Simulator and Android Emulator covering the login flow, push-event flow, and at least one full turn loop; .gitea/workflows/ui-release.yaml extended with macOS-runner Appium job (mandatory pre-release gate)

Dependencies: Phases 1 through 35.

Acceptance criteria:

  • implementation matches every documented contract and live topic doc;
  • the cross-cutting regression scenarios listed below pass on web, desktop, and mobile;
  • Appium smoke passes on both iOS and Android in CI.

Targeted tests:

  • run focused package tests for ui/core and every TypeScript module;
  • rerun cross-platform Playwright suites against release-candidate builds;
  • run Tier 2 visual regression baselines;
  • run Appium smoke suites on iOS and Android.

Cross-Cutting Regression Scenarios

  • A fresh device generates a keypair, completes email-code login, and successfully signs a follow-up authenticated request on every target platform.
  • A returning device resumes its session without re-login, preserves queued orders, and continues receiving push events without gaps.
  • Server-side session revocation tears down the active push stream and forces a re-login on every target platform within one second.
  • Tampering with payload_bytes, payload_hash, request_id, message_type, or any signature byte is rejected by the verifier in ui/core with a stable error code.
  • Requests outside the freshness window are rejected before they reach network, and the client surfaces a clock-skew warning when its local clock disagrees with the server time event by more than the freshness window.
  • The map renderer holds 60 fps with a 1000-primitive fixture on mid-range hardware on web (Chrome, Edge, Safari, Firefox), desktop (Wails on macOS, Windows, Linux), and mobile (latest iPhone, mid- range Android).
  • The single-tool sidebar preserves state across tab switches; the active view preserves state across view switches; designers preserve their in-progress state when navigating to the map and back through a transient overlay.
  • Order draft is preserved across page reloads, view switches, network drops, and history-mode entry / exit.
  • Orders queued offline are flushed in order on reconnect; a turn- cutoff conflict surfaces as a clearly failed-order banner without retrying forever.
  • History mode applies to every view; the order tab disappears in history mode and the prior draft is restored on return to the current turn.
  • The ship-class designer's calculations match pkg/calc/ byte-for- byte; any drift between client mirror and server fails CI.
  • Linux desktop builds without Secret Service still complete login by falling back to the 0600 file under ~/.config/galaxy/.
  • The web service worker invalidates correctly on app update and never serves stale code on the first load after a deploy.
  • Push-event signature verification is mandatory; any verification failure tears down the stream and reconnects with backoff.
  • Locale switch persists across reloads and applies to every visible string on every platform.