118f7c17a2
Replace the native-gRPC server bootstrap with a single `connectrpc.com/connect` HTTP/h2c listener. Connect-Go natively serves Connect, gRPC, and gRPC-Web on the same port, so browsers can now reach the authenticated surface without giving up the gRPC framing native and desktop clients may use later. The decorator stack (envelope → session → payload-hash → signature → freshness/replay → rate-limit → routing/push) is reused unchanged behind a small Connect → gRPC adapter and a `grpc.ServerStream` shim around `*connect.ServerStream`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1862 lines
73 KiB
Markdown
1862 lines
73 KiB
Markdown
# 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
|
||
|
||
```text
|
||
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):
|
||
|
||
```text
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ Header: race · turn N · countdown · view dropdown · ⚙ │
|
||
├────────────────────────────────────────────┬─────────────┤
|
||
│ │ tabs │
|
||
│ active view │ ┌─────────┐ │
|
||
│ (map / table / │ │ Calc │ │
|
||
│ battle / mail / │ │ Inspect │ │
|
||
│ designer / report) │ │ Order │ │
|
||
│ │ └─────────┘ │
|
||
│ │ tool │
|
||
│ │ content │
|
||
│ │ │
|
||
└────────────────────────────────────────────┴─────────────┘
|
||
```
|
||
|
||
Tablet (768–1024 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):
|
||
|
||
```text
|
||
┌──────────────────────┐
|
||
│ ☰ 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/authn` — `ui/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.go` →
|
||
`connect_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.NewServer` → `s.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: pending.
|
||
|
||
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).
|
||
|
||
Artifacts:
|
||
|
||
- `ui/wasm/main.go` TinyGo entry point exporting `Core` API to JS
|
||
- `ui/Makefile` target `wasm` producing `core.wasm` and `wasm_exec.js`
|
||
under `ui/frontend/static/`
|
||
- `ui/frontend/src/platform/core/index.ts` `Core` interface and
|
||
build-time target resolver
|
||
- `ui/frontend/src/platform/core/wasm.ts` `WasmCore` adapter
|
||
- `ui/frontend/src/api/galaxy-client.ts` `GalaxyClient` orchestrating
|
||
`Core.signRequest` → ConnectRPC fetch → `Core.verifyResponse`
|
||
- `ui/frontend/src/api/connect.ts` typed Connect client built from
|
||
generated stubs (Connect codegen via `@bufbuild/protoc-gen-es` and
|
||
`@connectrpc/protoc-gen-connect-es`)
|
||
- topic doc `ui/docs/wasm-toolchain.md` documenting TinyGo vs
|
||
standard Go choice and bundle size measured
|
||
|
||
Dependencies: Phases 2, 3, 4.
|
||
|
||
Acceptance criteria:
|
||
|
||
- `make wasm` produces a deterministic bundle under 1 MB (TinyGo) or
|
||
under 3 MB (standard Go fallback);
|
||
- `WasmCore.signRequest` produces canonical bytes byte-for-byte
|
||
identical to the gateway-side verifier output on shared fixtures
|
||
(validated via Vitest with the WASM module loaded in JSDOM);
|
||
- `WasmCore` exposes the same TypeScript types as the future
|
||
`WailsCore` and `CapacitorCore` will need to satisfy.
|
||
|
||
Targeted tests:
|
||
|
||
- Vitest unit tests for `WasmCore` calling each public method with a
|
||
fixture WASM module loaded in JSDOM;
|
||
- Vitest unit tests for `GalaxyClient` using a mock `Core` and a mock
|
||
Connect transport;
|
||
- Vitest tests asserting `WasmCore.signRequest` output matches gateway
|
||
fixtures byte-for-byte for at least three message types.
|
||
|
||
## 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 `WasmCore` →
|
||
`GalaxyClient` → 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`](../game/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`](../game/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`](../game/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`](../game/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`](../game/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`](../game/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`](../game/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.
|