Merge pull request 'docs(ui): finalize MVP plan structure + de-archaeologize topic docs' (#25) from feature/ui-finalization-plan into development
This commit was merged in pull request #25.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# UI Client — Finalization Plan
|
||||
|
||||
The MVP web client (Phases 1–30, [PLAN.md](PLAN.md)) is functionally
|
||||
complete. This plan finalizes the **web** experience — visual
|
||||
consistency, accessibility, localisation, error UX, installability, and
|
||||
documentation — before the native platform wrappers in
|
||||
[ROADMAP.md](ROADMAP.md). It absorbs the original Phases 33 (PWA) and 35
|
||||
(Polish), which were pulled forward to here.
|
||||
|
||||
**This is a finalization pass, not the final word.** After it ships, the
|
||||
owner exercises the whole UI by hand and brings small-nuance fixes; the
|
||||
last stage (F8) is an explicit, open-ended owner-driven refinement loop.
|
||||
The earlier stages are "done" when their acceptance holds; the feature
|
||||
set is "final" only when the owner signs off.
|
||||
|
||||
Each stage ends with a runnable, reviewable artifact and is gated on the
|
||||
per-stage CI rule (push, watch `go-unit` / `ui-test` to green) before
|
||||
being marked done.
|
||||
|
||||
---
|
||||
|
||||
## F1 — Visual design system
|
||||
|
||||
Goal: replace the ad-hoc per-component styling (inline hex colors like
|
||||
`#0a0e1a`, one-off spacing) with a shared design language so every view
|
||||
looks like one product.
|
||||
|
||||
- `ui/frontend/src/lib/theme/` (or `app.css` `:root`) design tokens:
|
||||
color palette (surface / border / text / accent / danger), spacing
|
||||
scale, radii, typography, focus-ring style — as CSS custom properties.
|
||||
- Migrate components to the tokens (calculator, inspectors, sidebar,
|
||||
tables, lobby, auth) — replace literal hex/px with `var(--…)`.
|
||||
- Topic doc `ui/docs/design-system.md` documenting the tokens and usage.
|
||||
|
||||
Acceptance: no literal theme colors left in component `<style>` blocks
|
||||
(spot-checked); a single token change restyles the app coherently.
|
||||
Consider the `frontend-design` skill for the palette/spacing pass.
|
||||
|
||||
## F2 — Accessibility (WCAG 2.2 AA)
|
||||
|
||||
(From Phase 35.) Goal: the whole client is usable by keyboard and
|
||||
assistive tech.
|
||||
|
||||
- Keyboard-only paths for login, lobby, and the in-game shell; visible
|
||||
focus rings (from the F1 token); ARIA labels/roles; screen-reader-only
|
||||
text where needed.
|
||||
- `ui/docs/a11y.md` audit results.
|
||||
|
||||
Acceptance: WCAG 2.2 AA on lobby, login, and the in-game shell per an
|
||||
axe-core scan; full keyboard reachability with visible focus.
|
||||
Tests: axe-core integration tests on every top-level view; Playwright
|
||||
keyboard-only navigation.
|
||||
|
||||
## F3 — Localisation completeness
|
||||
|
||||
(From Phase 35.) Goal: every visible string is translated (en + ru).
|
||||
|
||||
- Audit all components for hard-coded strings; move them into the i18n
|
||||
bundles; missing-key detection test; locale-switch persistence across
|
||||
reloads.
|
||||
|
||||
Acceptance: no untranslated visible strings; missing-translation test is
|
||||
green; locale persists. Tests: Vitest i18n bundle-structure +
|
||||
missing-key detection.
|
||||
|
||||
## F4 — Error & state UX
|
||||
|
||||
(From Phase 35, plus the deferred Phase 13/35 items.) Goal: consistent,
|
||||
actionable feedback everywhere.
|
||||
|
||||
- `ui/frontend/src/lib/error/` central error surface with stable codes
|
||||
and retry/escalation guidance; every server-side error mapped to a
|
||||
translated, actionable message (en/ru).
|
||||
- Consistent empty / loading / error states across views.
|
||||
- Selected-planet visual on the map (ring/halo) off the `SelectionStore`
|
||||
(deferred from Phase 35).
|
||||
- Mobile bottom-sheet swipe-down + tap-outside dismissal (deferred from
|
||||
Phase 13/35).
|
||||
|
||||
Acceptance: every server error → a translated actionable message;
|
||||
consistent empty/loading/error states; the selected planet is visually
|
||||
marked.
|
||||
|
||||
## F5 — PWA (was Phase 33)
|
||||
|
||||
Goal: the web build is installable and offline-tolerant. Depends on F6
|
||||
(wasm in CI) so the service worker caches a freshly-built artefact.
|
||||
|
||||
- `ui/frontend/src/service-worker.ts` cache-first assets with
|
||||
stale-invalidation on app update; `manifest.webmanifest`;
|
||||
`ui/frontend/static/icons/` web icon set; `ui/docs/pwa-strategy.md`.
|
||||
|
||||
Acceptance: installs as a PWA on Chrome, Edge, and iOS Safari; the SW
|
||||
survives an app update without serving stale code. Tests: Lighthouse PWA
|
||||
≥ 90; Playwright install→offline→cached-login; version-bump invalidation.
|
||||
|
||||
## F6 — Build hygiene: build core.wasm in CI
|
||||
|
||||
(From the PLAN.md TODO; timely — the binary is currently committed and
|
||||
must be rebuilt by hand on every Go-bridge change, which has already
|
||||
bitten us.) Goal: stop shipping the binary in git.
|
||||
|
||||
- Install TinyGo on the gitea Actions runner (Linux tarball /
|
||||
`curl … | tar -xz`, since `brew` is macOS-only); add `make -C ui wasm`
|
||||
ahead of the Vitest step in `.gitea/workflows/ui-test.yaml`.
|
||||
- Remove `ui/frontend/static/core.wasm` and `wasm_exec.js` from the repo
|
||||
and re-tighten `ui/.gitignore`.
|
||||
|
||||
Acceptance: CI builds the wasm and Vitest/Playwright pass against the
|
||||
freshly built artefact; the binaries are no longer tracked.
|
||||
|
||||
## F7 — Documentation finalization
|
||||
|
||||
Goal: living docs read as current state, not build archaeology.
|
||||
|
||||
- De-archaeologize the 20 `ui/docs/` topic docs: strip "Phase N adds…"
|
||||
history, rewrite as present-tense descriptions of how things work.
|
||||
- `ui/docs/README.md` index (added during this reorganization).
|
||||
- Sync `ui/README.md`, `docs/ARCHITECTURE.md`, and `docs/FUNCTIONAL.md`
|
||||
(+ `_ru` mirror) with the finalized UI.
|
||||
|
||||
Acceptance: no stray "Phase N" references in `ui/docs/`; the index links
|
||||
every topic doc; READMEs/ARCHITECTURE describe current state.
|
||||
|
||||
## F8 — Owner manual-QA refinement loop
|
||||
|
||||
Open-ended, owner-driven. The owner exercises the whole UI by hand and
|
||||
files small-nuance fixes; each is folded in as it surfaces. There is no
|
||||
fixed acceptance gate here — finalization is "done" when the owner signs
|
||||
off and the client moves on to the ROADMAP platform wrappers.
|
||||
+14
-382
@@ -10,11 +10,18 @@ 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.
|
||||
|
||||
> **Status — MVP complete.** Phases 1–30 below (the full web client) are
|
||||
> done. Remaining work is tracked outside this file: the active web
|
||||
> finalization pass (visual system, accessibility, localisation, error
|
||||
> UX, PWA, docs) in [PLAN-finalize.md](PLAN-finalize.md), and the
|
||||
> deferred platform wrappers (Wails desktop, Capacitor mobile), the
|
||||
> realistic multi-turn projection, and the cross-platform acceptance
|
||||
> pass in [ROADMAP.md](ROADMAP.md). This file is retained as the staged
|
||||
> record of how the MVP was built.
|
||||
|
||||
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.
|
||||
or imported by the new code. The architectural overview is mirrored into
|
||||
`ui/README.md`.
|
||||
|
||||
Each phase ends with a runnable artifact. The visual progression is:
|
||||
empty page → navigation skeleton → stubbed views → live data → real
|
||||
@@ -26,7 +33,9 @@ plan can be adjusted with at most one phase of rework.
|
||||
|
||||
## Summary
|
||||
|
||||
This plan breaks implementation into 36 small reviewable phases. Each
|
||||
This plan staged the MVP web client as 30 small reviewable phases (1–30,
|
||||
all complete; the finalization pass and post-MVP work live in
|
||||
[PLAN-finalize.md](PLAN-finalize.md) and [ROADMAP.md](ROADMAP.md)). 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
|
||||
@@ -3408,380 +3417,3 @@ Note: the WASM artefact `ui/frontend/static/core.wasm` must be rebuilt
|
||||
(`make wasm`, needs TinyGo) for the new bridge functions to be present
|
||||
at runtime and in the Playwright suite; Vitest injects a fake `Core`
|
||||
and does not need the rebuild.
|
||||
|
||||
## 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 — Realistic Planet Forecast
|
||||
|
||||
Status: pending. Long-term scope deferred but this phase ships real
|
||||
features.
|
||||
|
||||
Goal: ship a realistic multi-turn planet projection and surface it in
|
||||
the planet inspector and in the calculator's planet area. Reach circles
|
||||
already shipped in Phase 30 (auto-drawn from the calculator's selected
|
||||
planet); this phase no longer owns them.
|
||||
|
||||
The Phase 30 planet area is single-turn (MAT-only): it answers "ships
|
||||
this turn / turns per ship" at the current or overridden MAT. This phase
|
||||
makes it realistic and multi-turn by extracting the planet economy into
|
||||
`pkg/calc` and simulating turns: population growth (`×1.08`), material /
|
||||
capital / colonist supply, and the capital/colonist unpacking that
|
||||
mirrors `MakeTurn` steps 09/12/14/15. CAP and COL only affect future
|
||||
turns (post-production unpacking), so they become meaningful here and
|
||||
are added to the calculator's planet area as supply inputs alongside
|
||||
MAT.
|
||||
|
||||
Artifacts:
|
||||
|
||||
- `pkg/calc/` planet-economy extraction (single-sourced, engine
|
||||
delegates): `PlanetProduction`, `ProducePopulation`,
|
||||
`UnpackColonists`, `UnpackCapital`, reusing `ProduceShipsInTurn`; a
|
||||
multi-turn projector `ProjectPlanetBuild` answering "K ships in M
|
||||
turns" under guaranteed per-turn supply
|
||||
- thin bridges in `ui/core/calc/` + `Core` typings
|
||||
- planet inspector forecast section (next-turn population, industry,
|
||||
materials, production progress)
|
||||
- calculator planet area gains CAP and COL supply inputs and switches
|
||||
its readout to the multi-turn projector
|
||||
- topic doc `ui/docs/multi-turn-projection.md` (long-term vision:
|
||||
multi-turn planning mode, scenario branches)
|
||||
|
||||
Dependencies: Phases 17, 18, 30.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- projector output is byte-identical to running the engine's per-turn
|
||||
planet update over the same turns (Go parity);
|
||||
- the planet inspector shows a forecast section matching it;
|
||||
- the calculator planet area honours MAT / CAP / COL supply and shows
|
||||
"K ships in M turns" consistent with the projector.
|
||||
|
||||
Targeted tests:
|
||||
|
||||
- Go parity tests for each extracted economy formula and the projector;
|
||||
- Vitest for the calculator planet area with supply inputs;
|
||||
- Playwright e2e: planet inspector forecast section.
|
||||
|
||||
## 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
|
||||
- mobile bottom-sheet swipe-down dismissal and tap-outside dismissal,
|
||||
on top of the close button shipped in Phase 13
|
||||
- selected-planet visual on the map (ring or halo), wired off the
|
||||
Phase 13 `SelectionStore`
|
||||
|
||||
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.
|
||||
|
||||
## TODO — deferred follow-ups from Phases 1-5
|
||||
|
||||
These items are explicit decisions to defer, not unknown work. Each
|
||||
should be picked up either as a follow-up patch or folded into the
|
||||
phase listed in the parenthesis when that phase lands.
|
||||
|
||||
- **Build `core.wasm` in CI, drop the committed artefacts** — install
|
||||
TinyGo on the Gitea Actions runner (`brew install tinygo` is not
|
||||
available on Linux runners, so use the official tarball or
|
||||
`curl … | tar -xz` step), add `make -C ui wasm` ahead of the Vitest
|
||||
step in `.gitea/workflows/ui-test.yaml`, then remove
|
||||
`ui/frontend/static/core.wasm` and `ui/frontend/static/wasm_exec.js`
|
||||
from the repo and re-tighten `ui/.gitignore`. Phase 5 committed the
|
||||
binaries only as a stop-gap so contributors did not have to install
|
||||
TinyGo. (Phase 5 cleanup, blocks before Phase 33 PWA.)
|
||||
- **Restore `js.CopyBytesToGo` when TinyGo fixes the
|
||||
`instanceof Uint8Array` check** — the per-element loop in
|
||||
`ui/wasm/main.go::copyBytesFromJS` is a workaround for TinyGo 0.41
|
||||
panicking on Uint8Arrays whose prototype chain crosses Node's
|
||||
`Buffer`. Track upstream
|
||||
(<https://github.com/tinygo-org/tinygo/issues>) and revert the
|
||||
helper once a release is pinned. (Phase 5 follow-up.)
|
||||
- **Migrate TS codegen to Connect-ES v2 BSR plugin once published** —
|
||||
`ui/buf.gen.yaml` runs `protoc-gen-es` v2 locally because
|
||||
`buf.build/connectrpc/es` is still on v1.6.1 and emits
|
||||
v1-incompatible imports. When the v2 plugin lands on the BSR, we
|
||||
can either keep the local plugin (no network dep) or move back to
|
||||
the remote, depending on whether buf.build rate limits are hit in
|
||||
CI. (Phase 5 follow-up; revisit when next regenerating.)
|
||||
- **Rename `gateway/internal/grpcapi/` → `gateway/internal/connectapi/`**
|
||||
— the package now hosts a Connect-Go listener that natively serves
|
||||
Connect, gRPC, and gRPC-Web; the `grpcapi` name is historical.
|
||||
Touches imports in `gateway/cmd/gateway/main.go` and a couple of
|
||||
cross-package refs. Pure rename, no behaviour change. (Phase 4
|
||||
cleanup; do alongside the next gateway change.)
|
||||
- **Rename `GATEWAY_AUTHENTICATED_GRPC_*` env vars to drop the `GRPC`
|
||||
infix** — they label the authenticated-edge tier, not the wire
|
||||
protocol. Affects `gateway/internal/config/`, the integration
|
||||
testenv defaults in `integration/testenv/gateway.go`, the README,
|
||||
and the runbook. Coordinated with the package rename above.
|
||||
(Phase 4 cleanup; not before the env vars are referenced by
|
||||
external operators.)
|
||||
- **Add a Docker-stack integration test for Connect end-to-end** —
|
||||
Phase 4 closed with service-level Connect tests only. Once a phase
|
||||
already brings up the full stack (Phase 7 onward, since auth flow
|
||||
needs backend), drop a `integration/connect_call_test.go` that
|
||||
exercises a unary Connect call and a server-streaming Connect call
|
||||
through `testenv.Bootstrap`. (Phase 7+, fold into the phase that
|
||||
needs it.)
|
||||
- **Battle viewer — push event `game.battle.new`** — when a battle
|
||||
involving the current player lands, emit a backend notification
|
||||
intent (idempotency `battle-new:<game_id>:<turn>:<battle_id>`,
|
||||
payload `{game_id, turn, battle_id}`) so the in-game shell
|
||||
surfaces a toast with a deep link into the Battle Viewer.
|
||||
(Phase 27 deferred; needs an engine emit-side change.)
|
||||
- **Battle viewer — richer ship-class visuals** — current MVP draws
|
||||
one small circle plus `<class>:<numLeft>` label per `(race,
|
||||
className)` pair. Future work derives shape / scale from mass,
|
||||
armament, shields, and the number of ships in the group.
|
||||
(Phase 27 deferred.)
|
||||
- **Battle viewer — animated re-distribution on elimination** —
|
||||
current implementation hard-jumps to the new spacing on the next
|
||||
frame; replace with an easing so the survivors visibly slide
|
||||
along the outer ring. (Phase 27 deferred.)
|
||||
|
||||
+41
-53
@@ -14,24 +14,22 @@ boundary on every platform.
|
||||
The legacy Fyne client under `client/` is reference-only.
|
||||
Nothing in `ui/` imports from it.
|
||||
|
||||
The full staged implementation plan lives in `PLAN.md`. The
|
||||
strategic rationale (why Svelte, why PixiJS, why Go-as-WASM, why
|
||||
The strategic rationale (why Svelte, why PixiJS, why Go-as-WASM, why
|
||||
Wails+Capacitor) lives outside the repo at
|
||||
`~/.claude/plans/buzzing-questing-fountain.md`. This README is a
|
||||
quick orientation; deeper per-phase design notes earn their place
|
||||
under `ui/docs/` as they are introduced.
|
||||
quick orientation; deeper design notes live under `ui/docs/`.
|
||||
|
||||
## Targets
|
||||
|
||||
| Target | Wrapper | Toolchain | Phase |
|
||||
| --------------- | ---------------- | ----------------------- | -------- |
|
||||
| web | browser tab | Vite + WASM | 5+ |
|
||||
| web-mobile | mobile browser | Vite + WASM | 5+ |
|
||||
| desktop (mac) | Wails v2 | Go + Wails CLI | 31 |
|
||||
| desktop (win) | Wails v2 | Go + Wails CLI | 31 |
|
||||
| desktop (linux) | Wails v2 | Go + Wails CLI | 31 |
|
||||
| iOS | Capacitor | gomobile + Xcode | 32+ |
|
||||
| Android | Capacitor | gomobile + Gradle | 32+ |
|
||||
| Target | Wrapper | Toolchain | Status |
|
||||
| --------------- | ---------------- | ----------------------- | ------------------------------ |
|
||||
| web | browser tab | Vite + WASM | implemented |
|
||||
| web-mobile | mobile browser | Vite + WASM | implemented |
|
||||
| desktop (mac) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
|
||||
| desktop (win) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
|
||||
| desktop (linux) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
|
||||
| iOS | Capacitor | gomobile + Xcode | planned (see ROADMAP.md) |
|
||||
| Android | Capacitor | gomobile + Gradle | planned (see ROADMAP.md) |
|
||||
|
||||
## Layered architecture
|
||||
|
||||
@@ -58,7 +56,9 @@ under `ui/docs/` as they are introduced.
|
||||
|
||||
```text
|
||||
ui/
|
||||
├── PLAN.md staged implementation plan (Phases 1-36)
|
||||
├── PLAN.md staged implementation plan
|
||||
├── ROADMAP.md planned desktop / mobile / multi-turn features
|
||||
├── PLAN-finalize.md PWA, accessibility, localisation, error UX
|
||||
├── Makefile wasm / ts-protos / web / mobile / desktop targets
|
||||
├── README.md this file
|
||||
├── buf.gen.yaml local-plugin TS Protobuf-ES generator
|
||||
@@ -71,6 +71,9 @@ ui/
|
||||
│ └── wasm-toolchain.md TinyGo build, JSDOM loading, bundle budget
|
||||
├── core/ ui/core Go module (canonical bytes, keypair)
|
||||
├── wasm/ TinyGo entry point exposing Core to JS
|
||||
├── mobile-bridge/ gomobile bindings (planned — see ROADMAP.md)
|
||||
├── desktop/ Wails project (planned — see ROADMAP.md)
|
||||
├── mobile/ Capacitor project (planned — see ROADMAP.md)
|
||||
└── frontend/ SvelteKit / Vite source
|
||||
├── src/api/ GalaxyClient + typed Connect client + auth + session
|
||||
├── src/lib/ env config, session store, revocation watcher
|
||||
@@ -98,40 +101,23 @@ Linked topic docs:
|
||||
- [`docs/testing.md`](docs/testing.md) — Tier 1 per-PR + Tier 2
|
||||
release test tiers.
|
||||
|
||||
```text
|
||||
ui/
|
||||
├── README.md this file
|
||||
├── PLAN.md staged implementation plan
|
||||
├── Makefile cross-target build placeholders
|
||||
├── pnpm-workspace.yaml pnpm workspace root
|
||||
├── .gitignore
|
||||
├── docs/ per-phase topic docs (added per phase)
|
||||
├── frontend/ TS + Svelte source, shared across targets
|
||||
├── core/ Go module ui/core (Phase 3+)
|
||||
├── wasm/ TinyGo entry point for core.wasm (Phase 5)
|
||||
├── mobile-bridge/ gomobile bindings (Phase 32+)
|
||||
├── desktop/ Wails project (Phase 31)
|
||||
├── mobile/ Capacitor project (Phase 32+)
|
||||
└── web/ static deploy assets (Phase 30+)
|
||||
```
|
||||
|
||||
## Build pipeline
|
||||
|
||||
Every cross-target build flows through `make` at this level. All
|
||||
named targets are placeholders until the named phase lands; running
|
||||
`make` with no arguments prints the current placeholder map.
|
||||
Every cross-target build flows through `make` at this level.
|
||||
Native targets are placeholders until their platform work lands;
|
||||
running `make` with no arguments prints the current placeholder map.
|
||||
|
||||
```text
|
||||
make web Vite production build Phase 5+
|
||||
make wasm TinyGo → core.wasm Phase 5
|
||||
make ts-protos Connect-ES + Protobuf-ES gen Phase 5
|
||||
make fbs-ts FlatBuffers TS bindings via flatc Phase 8
|
||||
make gomobile gomobile bind → ios + android Phase 32+
|
||||
make desktop-mac Wails build for darwin Phase 31
|
||||
make desktop-win Wails build for windows Phase 31
|
||||
make desktop-linux Wails build for linux Phase 31
|
||||
make ios Capacitor + xcodebuild Phase 32+
|
||||
make android Capacitor + gradle Phase 32+
|
||||
make web Vite production build
|
||||
make wasm TinyGo → core.wasm
|
||||
make ts-protos Connect-ES + Protobuf-ES gen
|
||||
make fbs-ts FlatBuffers TS bindings via flatc
|
||||
make gomobile gomobile bind → ios + android (planned — see ROADMAP.md)
|
||||
make desktop-mac Wails build for darwin (planned — see ROADMAP.md)
|
||||
make desktop-win Wails build for windows (planned — see ROADMAP.md)
|
||||
make desktop-linux Wails build for linux (planned — see ROADMAP.md)
|
||||
make ios Capacitor + xcodebuild (planned — see ROADMAP.md)
|
||||
make android Capacitor + gradle (planned — see ROADMAP.md)
|
||||
make all every target above
|
||||
```
|
||||
|
||||
@@ -159,18 +145,20 @@ hostnames — use the long-lived dev environment at
|
||||
[`../tools/dev-deploy/`](../tools/dev-deploy/README.md). It is
|
||||
redeployed by Gitea Actions on every merge into `development`.
|
||||
|
||||
## Per-phase docs
|
||||
## Topic docs
|
||||
|
||||
Topic docs live under `ui/docs/` and are added per phase as they're
|
||||
needed (testing tiers, WASM toolchain, navigation shell, renderer
|
||||
internals, sync protocol, auth flow, and so on). The staged plan in
|
||||
`PLAN.md` names the topic doc each phase produces.
|
||||
Topic docs live under `ui/docs/` (testing tiers, WASM toolchain,
|
||||
navigation shell, renderer internals, sync protocol, auth flow, and
|
||||
so on).
|
||||
|
||||
## Cross-references
|
||||
|
||||
- [`PLAN.md`](./PLAN.md) — staged implementation plan with goals,
|
||||
artifacts, dependencies, acceptance criteria, and targeted tests
|
||||
per phase.
|
||||
- [`PLAN.md`](./PLAN.md) — staged implementation plan (historical
|
||||
record of completed work).
|
||||
- [`ROADMAP.md`](./ROADMAP.md) — planned desktop / mobile / multi-turn
|
||||
projection features.
|
||||
- [`PLAN-finalize.md`](./PLAN-finalize.md) — PWA, accessibility,
|
||||
localisation, error UX finalization work.
|
||||
- [`../docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md) — platform
|
||||
architecture and the transport security model (§15) the client
|
||||
envelope contract derives from.
|
||||
@@ -178,4 +166,4 @@ internals, sync protocol, auth flow, and so on). The staged plan in
|
||||
stories that drive the UI flows.
|
||||
- [`../docs/TESTING.md`](../docs/TESTING.md) — project-wide testing
|
||||
layers; UI-specific test tiers (Vitest, Playwright) live in
|
||||
`ui/docs/testing.md` from Phase 2 onward.
|
||||
`ui/docs/testing.md`.
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# UI Client — Post-MVP Roadmap
|
||||
|
||||
The MVP web client (Phases 1–30 of [PLAN.md](PLAN.md)) is complete. The
|
||||
near-term web **finalization** pass — visual system, accessibility,
|
||||
localisation, error UX, PWA, docs — is tracked separately in
|
||||
[PLAN-finalize.md](PLAN-finalize.md).
|
||||
|
||||
This roadmap holds the work deliberately deferred until after the web
|
||||
client is finalized: the native platform wrappers, the realistic
|
||||
multi-turn projection (a feature, not polish), the cross-platform
|
||||
acceptance pass, and a set of non-blocking follow-ups. Items keep their
|
||||
original PLAN.md phase numbers for continuity; their full original specs
|
||||
live in the PLAN.md git history.
|
||||
|
||||
---
|
||||
|
||||
## Phase 31 — Wails Desktop Wrapper
|
||||
|
||||
Goal: native desktop app (macOS, Windows, Linux) running the same
|
||||
frontend bundle, with `ui/core` embedded as Go instead of WASM.
|
||||
|
||||
Key artifacts: `ui/desktop/` Wails entry + IPC bindings; per-OS secure
|
||||
storage (macOS Keychain, Windows DPAPI, Linux Secret Service with a
|
||||
`0600` file fallback); `modernc.org/sqlite` cache; `WailsCore` /
|
||||
`WailsKeyStore` / `WailsCache` adapters; OS icons; `ui/Makefile`
|
||||
`desktop-*` targets; `ui/docs/desktop-secure-storage.md`. Re-evaluate
|
||||
Wails v2 vs v3 at phase start.
|
||||
|
||||
Depends on: Phase 6 (KeyStore/Cache interfaces) and the web form of
|
||||
Phases 7–30. Acceptance: all three OS binaries launch, log in, and
|
||||
persist the keypair on a fresh profile from one codebase; Linux file
|
||||
fallback activates without libsecret and writes `0600`.
|
||||
|
||||
## Phase 32 — Capacitor Mobile Wrapper
|
||||
|
||||
Goal: native iOS and Android apps running the same frontend bundle,
|
||||
calling a gomobile-compiled `ui/core`.
|
||||
|
||||
Key artifacts: `ui/mobile-bridge/bridge.go` gomobile façade;
|
||||
`ui/Makefile` `gomobile`/`ios`/`android` targets; Capacitor project +
|
||||
custom `galaxy-core` plugin (Swift + Kotlin); `CapacitorCore` /
|
||||
`CapacitorKeyStore` / `CapacitorCache` adapters (secure-storage +
|
||||
sqlite community plugins); app icons + splash; `ui/docs/mobile-bridge.md`.
|
||||
|
||||
Depends on: Phase 6 and the web form of Phases 7–30. Acceptance: iOS
|
||||
Simulator and Android Emulator launch, log in, and persist the keypair
|
||||
across restarts (manual smoke); same `Core`/`Storage` TS interfaces as
|
||||
web/desktop. Full Appium automation lands in Phase 36.
|
||||
|
||||
## Phase 34 — Multi-Turn Projection (realistic planet forecast)
|
||||
|
||||
Goal: a realistic multi-turn planet projection, surfaced in the planet
|
||||
inspector and the calculator's planet area. (Reach circles already
|
||||
shipped in Phase 30; this phase no longer owns them.)
|
||||
|
||||
The Phase 30 planet area is single-turn (MAT-only). This phase makes it
|
||||
realistic and multi-turn by extracting the planet economy into `pkg/calc`
|
||||
(single-sourced, engine delegates): `PlanetProduction`,
|
||||
`ProducePopulation`, `UnpackColonists`, `UnpackCapital`, reusing
|
||||
`ProduceShipsInTurn`; plus a `ProjectPlanetBuild` projector ("K ships in
|
||||
M turns" under guaranteed per-turn supply) mirroring `MakeTurn` steps
|
||||
09/12/14/15. CAP and COL only affect future turns (post-production
|
||||
unpacking), so they become meaningful here and join MAT as supply inputs
|
||||
in the calculator's planet area.
|
||||
|
||||
Key artifacts: the `pkg/calc` economy extraction + bridges + `Core`
|
||||
typings; planet-inspector forecast section (next-turn population,
|
||||
industry, materials, progress); calculator planet area gains CAP/COL
|
||||
supply; `ui/docs/multi-turn-projection.md`.
|
||||
|
||||
Depends on: Phases 17, 18, 30. Acceptance: projector output byte-
|
||||
identical to running the engine's per-turn planet update over the same
|
||||
turns (Go parity); inspector + calculator readouts consistent with it.
|
||||
|
||||
## Phase 36 — Acceptance Pass
|
||||
|
||||
Goal: reconcile implementation, documentation, and regression coverage
|
||||
before declaring the client ready for technical beta.
|
||||
|
||||
Key artifacts: final cross-platform regression run on a release
|
||||
candidate; `ui/docs/release-checklist.md`; visual-regression baselines;
|
||||
Appium harness (iOS Simulator + Android Emulator) covering login, push,
|
||||
and a full turn loop, wired into a macOS-runner CI job as a pre-release
|
||||
gate; a docs/README/ARCHITECTURE/FUNCTIONAL drift sweep.
|
||||
|
||||
Depends on: Phases 1–35 (incl. the finalization plan). Acceptance:
|
||||
implementation matches every documented contract; the cross-cutting
|
||||
regression scenarios below pass on web, desktop, and mobile; Appium
|
||||
smoke passes on iOS and Android in CI.
|
||||
|
||||
### Cross-cutting regression scenarios
|
||||
|
||||
- A fresh device generates a keypair, completes email-code login, and
|
||||
signs a follow-up authenticated request on every platform.
|
||||
- A returning device resumes its session without re-login, preserves
|
||||
queued orders, and keeps receiving push events without gaps.
|
||||
- Server-side session revocation tears down the push stream and forces
|
||||
re-login on every platform within one second.
|
||||
- Tampering with `payload_bytes`, `payload_hash`, `request_id`,
|
||||
`message_type`, or any signature byte is rejected by `ui/core` with a
|
||||
stable error code.
|
||||
- Requests outside the freshness window are rejected before network; a
|
||||
clock-skew warning surfaces when the local clock disagrees beyond it.
|
||||
- The map renderer holds 60 fps with a 1000-primitive fixture on web
|
||||
(Chrome, Edge, Safari, Firefox), desktop (Wails on mac/win/linux),
|
||||
and mobile (latest iPhone, mid-range Android).
|
||||
- The sidebar preserves tool state across tab switches; the active view
|
||||
preserves state across view switches.
|
||||
- Order draft survives reloads, view switches, network drops, and
|
||||
history-mode entry/exit.
|
||||
- Orders queued offline flush in order on reconnect; a turn-cutoff
|
||||
conflict surfaces as a 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.
|
||||
- The calculator's math matches `pkg/calc/` byte-for-byte; drift fails
|
||||
CI.
|
||||
- Linux desktop without Secret Service completes login via the `0600`
|
||||
file fallback.
|
||||
- The web service worker invalidates on app update and never serves
|
||||
stale code on the first load after deploy.
|
||||
- Push-event signature verification is mandatory; any failure tears
|
||||
down the stream and reconnects with backoff.
|
||||
- Locale switch persists across reloads and applies to every visible
|
||||
string on every platform.
|
||||
|
||||
---
|
||||
|
||||
## Deferred follow-ups (non-blocking)
|
||||
|
||||
Explicit deferral decisions, to fold into the phase noted when it lands.
|
||||
(The "build core.wasm in CI / drop the committed artefact" follow-up
|
||||
moved to [PLAN-finalize.md](PLAN-finalize.md) F6, since it is timely.)
|
||||
|
||||
- **Restore `js.CopyBytesToGo`** when TinyGo fixes the
|
||||
`instanceof Uint8Array` check — the per-element loop in
|
||||
`ui/wasm/main.go::copyBytesFromJS` is a TinyGo 0.41 workaround. Track
|
||||
upstream and revert once a fixed release is pinned.
|
||||
- **Migrate TS codegen to the Connect-ES v2 BSR plugin** once published —
|
||||
`ui/buf.gen.yaml` runs `protoc-gen-es` v2 locally because
|
||||
`buf.build/connectrpc/es` still emits v1-incompatible imports.
|
||||
- **Rename `gateway/internal/grpcapi/` → `connectapi/`** — historical
|
||||
name; the package serves Connect/gRPC/gRPC-Web. Pure rename. (Fold
|
||||
into the next gateway change; not a UI task.)
|
||||
- **Rename `GATEWAY_AUTHENTICATED_GRPC_*` env vars** to drop the `GRPC`
|
||||
infix — they label the authenticated edge tier, not the wire protocol.
|
||||
(Coordinate with the rename above; not a UI task.)
|
||||
- **Add a Docker-stack Connect end-to-end integration test** —
|
||||
`integration/connect_call_test.go` exercising a unary + a
|
||||
server-streaming Connect call through `testenv.Bootstrap`.
|
||||
- **Battle viewer — push event `game.battle.new`** — emit a backend
|
||||
notification intent (idempotency
|
||||
`battle-new:<game_id>:<turn>:<battle_id>`) so the shell shows a toast
|
||||
deep-linking into the Battle Viewer. Needs an engine emit-side change.
|
||||
- **Battle viewer — richer ship-class visuals** — derive shape/scale
|
||||
from mass, armament, shields, and ship count instead of the MVP
|
||||
one-circle-plus-label per `(race, className)`.
|
||||
+24
-23
@@ -4,8 +4,9 @@
|
||||
Galaxy cross-platform UI client. It carries v1 transport-envelope
|
||||
canonical bytes, signature verification, and Ed25519 keypair
|
||||
helpers. Network I/O and persistent storage live elsewhere on
|
||||
purpose: this module compiles unchanged to WASM (Phase 5),
|
||||
gomobile (Phase 32), and Wails-embedded native (Phase 31).
|
||||
purpose: this module compiles unchanged to WASM (the web target today)
|
||||
and is designed to also compile to gomobile (mobile) and Wails-embedded
|
||||
native (desktop) — both planned, see [`../ROADMAP.md`](../ROADMAP.md).
|
||||
|
||||
The authoritative byte contract is defined in
|
||||
[`docs/ARCHITECTURE.md` §15](../../docs/ARCHITECTURE.md). The gateway
|
||||
@@ -39,8 +40,11 @@ parity and round-trip sign/verify are exercised by
|
||||
```text
|
||||
ui/core/
|
||||
├── go.mod module galaxy/core (Go 1.26.0)
|
||||
├── calc/ ship-math wrappers over `pkg/calc/ship.go`
|
||||
│ └── ship.go Phase 18 designer preview bridge
|
||||
├── calc/ thin wrappers over `pkg/calc/` (no math here)
|
||||
│ ├── ship.go ship geometry, mass, speed, combat
|
||||
│ ├── planet.go ship build cost / per-turn production
|
||||
│ ├── solve.go goal-seek inverse solvers
|
||||
│ └── number.go display rounding (Ceil3)
|
||||
├── canon/ canonical-bytes builders and verifiers
|
||||
│ ├── canon.go length-prefix helpers
|
||||
│ ├── request.go galaxy-request-v1 fields and signing input
|
||||
@@ -92,20 +96,16 @@ ui/core/
|
||||
|
||||
### `galaxy/core/calc`
|
||||
|
||||
Thin Go bridge over `pkg/calc/ship.go`, surfaced via WASM to the
|
||||
Phase 18 ship-class designer preview. Each function is a one-line
|
||||
passthrough — no math lives here.
|
||||
Thin Go bridges over `pkg/calc/`, surfaced via WASM to the ship-class
|
||||
calculator and the ship-group inspector. Each function is a one-line
|
||||
passthrough — no math lives here. Coverage spans ship geometry / mass /
|
||||
speed / cargo, combat (attack, defence, bombing), block-upgrade and
|
||||
per-turn ship-build cost, the single-target goal-seek inverse solvers,
|
||||
and display rounding (`Ceil3`).
|
||||
|
||||
- `DriveEffective(drive, driveTech float64) float64`
|
||||
- `EmptyMass(drive, weapons float64, armament uint, shields, cargo float64) (float64, bool)`
|
||||
- `WeaponsBlockMass(weapons float64, armament uint) (float64, bool)`
|
||||
- `FullMass(emptyMass, carryingMass float64) float64`
|
||||
- `Speed(driveEffective, fullMass float64) float64`
|
||||
- `CargoCapacity(cargo, cargoTech float64) float64`
|
||||
- `CarryingMass(load, cargoTech float64) float64`
|
||||
|
||||
The full audit trail (which UI feature uses what, what is still
|
||||
deferred) lives in [`ui/docs/calc-bridge.md`](../docs/calc-bridge.md).
|
||||
The authoritative function surface (which UI feature uses what, parity
|
||||
rules, what is still deferred) lives in
|
||||
[`ui/docs/calc-bridge.md`](../docs/calc-bridge.md).
|
||||
|
||||
### `galaxy/core/types`
|
||||
|
||||
@@ -146,14 +146,15 @@ versa) is enforced from
|
||||
## What this module is **not**
|
||||
|
||||
- Not a network client. ConnectRPC over `@connectrpc/connect-web`
|
||||
on the TypeScript side is the only network surface (Phase 5+).
|
||||
- Not a key store. Per-platform secure storage lives in Phase 6.
|
||||
on the TypeScript side is the only network surface.
|
||||
- Not a key store. Per-platform secure storage lives in the platform
|
||||
`KeyStore` / `Cache` layer on the TypeScript side.
|
||||
- Not a freshness gate. Server-side `±5 min` freshness checks
|
||||
remain in `gateway/internal/grpcapi/freshness_replay.go`. The
|
||||
client is expected to stamp its own `timestamp_ms` accurately
|
||||
via `time.Now`, but does not enforce a window.
|
||||
- Not a FlatBuffers codec — that lands in a later phase, so the
|
||||
module today is small on purpose.
|
||||
- Not a FlatBuffers codec — report decoding lives on the TypeScript
|
||||
side, so this module stays small on purpose.
|
||||
|
||||
## Cross-references
|
||||
|
||||
@@ -161,5 +162,5 @@ versa) is enforced from
|
||||
authoritative byte contract.
|
||||
- [`../../gateway/authn`](../../gateway/authn) — server mirror of
|
||||
the same canonical bytes.
|
||||
- [`../PLAN.md`](../PLAN.md) Phase 3 — the staged plan that
|
||||
describes how this module fits into the wider client.
|
||||
- [`../PLAN.md`](../PLAN.md) — the staged plan that describes how this
|
||||
module fits into the wider client.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# UI client — topic docs
|
||||
|
||||
Deeper, topic-based documentation for the Galaxy web/cross-platform UI
|
||||
client, beyond what fits in [`../README.md`](../README.md). Each file
|
||||
describes how one area works (current state); the staged build history
|
||||
lives in [`../PLAN.md`](../PLAN.md), the active web finalization in
|
||||
[`../PLAN-finalize.md`](../PLAN-finalize.md), and deferred work in
|
||||
[`../ROADMAP.md`](../ROADMAP.md).
|
||||
|
||||
## Foundation & platform
|
||||
|
||||
- [navigation.md](navigation.md) — routes, the sidebar tabs, and the
|
||||
state-preservation rules across view/tab switches.
|
||||
- [storage.md](storage.md) — the `KeyStore` and `Cache` abstractions and
|
||||
their web (IndexedDB) implementations.
|
||||
- [game-state.md](game-state.md) — decoding the FlatBuffers report into
|
||||
`GameReport` and the `GameState` / rendered-report stores.
|
||||
- [sync-protocol.md](sync-protocol.md) — order-draft sync, turn cutoff,
|
||||
conflict handling, and auto-pause.
|
||||
- [events.md](events.md) — the signed push channel and event handling.
|
||||
- [calc-bridge.md](calc-bridge.md) — the `pkg/calc` → WASM → TypeScript
|
||||
bridge, with the live function surface and parity rules.
|
||||
- [wasm-toolchain.md](wasm-toolchain.md) — building `ui/core` to
|
||||
`core.wasm` with TinyGo.
|
||||
- [testing.md](testing.md) — the UI test layers (Vitest + Playwright).
|
||||
|
||||
## Auth & lobby
|
||||
|
||||
- [auth-flow.md](auth-flow.md) — device keypair, email-code login, and
|
||||
request signing on the client.
|
||||
- [lobby.md](lobby.md) — the lobby/game-list UI and membership flows.
|
||||
|
||||
## Map & active views
|
||||
|
||||
- [renderer.md](renderer.md) — the PixiJS map renderer contract (world
|
||||
model, hit-test, torus / no-wrap).
|
||||
- [order-composer.md](order-composer.md) — the order tab and the
|
||||
optimistic order overlay.
|
||||
- [report-view.md](report-view.md) — the Reports view.
|
||||
|
||||
## Tools & inspectors
|
||||
|
||||
- [calculator-ux.md](calculator-ux.md) — the ship-class calculator
|
||||
(design + goal-seek + planet build + reach circles + modernization).
|
||||
- [science-designer-ux.md](science-designer-ux.md) — the science
|
||||
designer.
|
||||
- [ship-group-actions.md](ship-group-actions.md) — ship-group inspector
|
||||
actions (move, send, upgrade, …).
|
||||
- [cargo-routes-ux.md](cargo-routes-ux.md) — cargo-route composition and
|
||||
reach filtering.
|
||||
|
||||
## Combat & comms
|
||||
|
||||
- [battle-viewer-ux.md](battle-viewer-ux.md) — the battle viewer.
|
||||
- [diplomail-ui.md](diplomail-ui.md) — the diplomatic-mail view.
|
||||
|
||||
## Localisation
|
||||
|
||||
- [i18n.md](i18n.md) — the localisation mechanism and translation
|
||||
bundles.
|
||||
+16
-17
@@ -96,7 +96,7 @@ The keypair lives next to the id in the same database (object
|
||||
store `keypair`, key `device`). Clearing site data wipes both;
|
||||
the next load generates a fresh keypair and the user must log in
|
||||
again. This is the documented re-login path — there is no paired
|
||||
"reissue device session" flow in Phase 7.
|
||||
"reissue device session" flow.
|
||||
|
||||
## Browser support
|
||||
|
||||
@@ -105,25 +105,23 @@ Chrome ≥ 137, Firefox ≥ 130, Safari ≥ 17.4 (see
|
||||
[`storage.md`](storage.md) for the rationale). On boot the layout
|
||||
runs a sanity probe (`crypto.subtle.generateKey` for `Ed25519`); if
|
||||
it rejects, the layout switches to a `browser not supported` page
|
||||
instead of rendering `/login`. Phase 7 deliberately does not ship a
|
||||
JavaScript Ed25519 fallback — see Phase 6's "modern-browser baseline,
|
||||
no JS Ed25519 fallback" decision.
|
||||
instead of rendering `/login`. The client deliberately does not ship a
|
||||
JavaScript Ed25519 fallback — the design decision is modern-browser
|
||||
baseline only.
|
||||
|
||||
## Revocation
|
||||
|
||||
The lobby layout opens a long-running `SubscribeEvents` stream as
|
||||
soon as `status` becomes `authenticated`. The watcher does not
|
||||
process individual events in Phase 7 — that arrives in Phase 24.
|
||||
Its only contract is liveness: any non-aborted termination of the
|
||||
stream is treated as a server-side session revocation, the watcher
|
||||
calls `session.signOut("revoked")`, and the layout effect redirects
|
||||
to `/login`.
|
||||
soon as `status` becomes `authenticated`. Its only contract is
|
||||
liveness: any non-aborted termination of the stream is treated as
|
||||
a server-side session revocation, the watcher calls
|
||||
`session.signOut("revoked")`, and the layout effect redirects to
|
||||
`/login`.
|
||||
|
||||
This satisfies the Phase 7 acceptance bar of "session revocation
|
||||
closes the active client within one second": the gateway closes
|
||||
the stream the moment it observes a `session_invalidation` push
|
||||
event from backend, and the watcher reacts on the next event-loop
|
||||
tick.
|
||||
Session revocation closes the active client within one second: the
|
||||
gateway closes the stream the moment it observes a
|
||||
`session_invalidation` push event from backend, and the watcher
|
||||
reacts on the next event-loop tick.
|
||||
|
||||
## Localisation
|
||||
|
||||
@@ -140,8 +138,9 @@ drops JS-set `Accept-Language` headers. See
|
||||
adding a new language.
|
||||
|
||||
The locale is **not** persisted between page reloads; detection
|
||||
runs again on every visit. Phase 35's full polish pass will
|
||||
revisit persistence and add message-format pluralisation.
|
||||
runs again on every visit. Persistence and message-format
|
||||
pluralisation are deferred to the finalization plan
|
||||
(../Plan-finalize.md).
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Battle Viewer UX
|
||||
|
||||
Phase 27 ships a dedicated viewer for battles (`/games/<id>/battle/<battleId>`).
|
||||
Bombings stay where they were in Phase 23 — a static table in the
|
||||
Reports view (`section-bombings.svelte`). The two domains are
|
||||
deliberately not mixed in any visual surface or click target.
|
||||
The battle viewer is a dedicated view for battles
|
||||
(`/games/<id>/battle/<battleId>`). Bombings are a separate static
|
||||
table in the Reports view (`section-bombings.svelte`). The two
|
||||
domains are deliberately not mixed in any visual surface or click
|
||||
target.
|
||||
|
||||
## Data shape
|
||||
|
||||
@@ -114,9 +115,8 @@ Below the scene the viewer renders a static `<ol>` text protocol —
|
||||
one line per action, formatted from `BattleReportGroup.race` and
|
||||
`BattleReportGroup.className`. The line for the current frame is
|
||||
highlighted so a non-visual reader can follow along by scrolling
|
||||
the log instead of watching the SVG. The list is always present
|
||||
and never hidden, satisfying the original Phase 27 acceptance "the
|
||||
same data is accessible as a static text log".
|
||||
the log instead of watching the SVG. The list is always present and never hidden; the same data is
|
||||
accessible as a static text log.
|
||||
|
||||
Each log row is also a `<button>`: a click or Enter/Space jumps
|
||||
playback to that shot (pauses and seeks). The list auto-scrolls
|
||||
|
||||
+59
-73
@@ -8,17 +8,16 @@ in Go under `pkg/calc/` and are surfaced to the UI through a
|
||||
Go → WASM → TypeScript bridge mounted under `ui/core/calc/` and a
|
||||
matching TS adapter in `ui/frontend/src/platform/core/`.
|
||||
|
||||
Phase 18 lands the **ship-math slice** of the bridge — everything
|
||||
the ship-class designer needs to render its preview pane. Phase 20
|
||||
extends it with `BlockUpgradeCost` so the ship-group inspector can
|
||||
preview modernize cost. Phase 30 extends it with the **combat,
|
||||
The bridge covers the **ship-math slice** (everything the ship-class
|
||||
designer needs to render its preview pane), `BlockUpgradeCost` (for
|
||||
the ship-group inspector's modernize-cost preview), and the **combat,
|
||||
planet-build, and goal-seek slice** for the ship-class calculator:
|
||||
`EffectiveAttack`, `EffectiveDefence`, `BombingPower`, `ShipBuildCost`,
|
||||
`ProduceShipsInTurn`, and the inverse solvers from `pkg/calc/solve.go`.
|
||||
Other slices (production/science forecast, the realistic multi-turn
|
||||
planet projection) remain deferred to dedicated future phases. This
|
||||
document is the running audit trail of what is live, what is missing,
|
||||
and how each function maps to its `pkg/calc/` source.
|
||||
planet projection) remain deferred. This document is the running audit
|
||||
trail of what is live, what is missing, and how each function maps to
|
||||
its `pkg/calc/` source.
|
||||
|
||||
## Live bridge surface
|
||||
|
||||
@@ -52,10 +51,10 @@ on the JS-side `globalThis.galaxyCore` (registered in
|
||||
| `ceil3` | `calc.Ceil3(value)` (`pkg/calc/number.go`) | `number` | calculator display rounding (round up to 3 dp) |
|
||||
|
||||
`BombingPower` and the per-turn build loop are no longer engine-only:
|
||||
Phase 30 extracted `BombingPower` from
|
||||
`game/internal/model/game/group.go` and the per-iteration build math
|
||||
from `controller.ProduceShip` into `pkg/calc` (`ProduceShipsInTurn`),
|
||||
and the engine now delegates to both — a true refactor, not a mirror.
|
||||
`BombingPower` was extracted from `game/internal/model/game/group.go`
|
||||
and the per-iteration build math from `controller.ProduceShip` into
|
||||
`pkg/calc` (`ProduceShipsInTurn`); the engine now delegates to both —
|
||||
a true refactor, not a mirror.
|
||||
The inverse solvers (`pkg/calc/solve.go`) invert the forward formulas
|
||||
for single-target goal-seek and return `null` when infeasible;
|
||||
`shieldsForDefence` uses bisection, the rest are analytic. Parity and
|
||||
@@ -77,12 +76,12 @@ same inputs and asserts byte-equal outputs.
|
||||
|
||||
## Still-deferred slices
|
||||
|
||||
Phase 18's Go-side bridge is intentionally narrow: it covers ship
|
||||
math and nothing else. Production forecasts, science, ship-build
|
||||
progress, and reach (`FligthDistance`) still depend on either
|
||||
inline TS arithmetic or the engine-shipped fields on `GameReport`.
|
||||
See the table further down for what is missing and the per-feature
|
||||
waivers below for the rationale on each deferral.
|
||||
The Go-side bridge is intentionally narrow: it covers ship math and
|
||||
the combat/planet-build/goal-seek slice. Production forecasts, science,
|
||||
and reach (`FligthDistance`) still depend on either inline TS arithmetic
|
||||
or the engine-shipped fields on `GameReport`. See the table further down
|
||||
for what is missing and the per-feature waivers below for the rationale
|
||||
on each deferral.
|
||||
|
||||
## Current `pkg/calc/` exports
|
||||
|
||||
@@ -91,7 +90,7 @@ waivers below for the rationale on each deferral.
|
||||
| `ShipProductionCost(shipEmptyMass float64) float64` | Production units required per unit of ship empty mass (×10). |
|
||||
| `PlanetProduceShipMass(L, Mat, Res float64) float64` | Ship mass produced per turn given free production `L`, material stockpile `Mat`, resources `Res`.|
|
||||
| `DriveEffective`, `Speed`, `EmptyMass`, `FullMass`, … | Ship-level derivations (`pkg/calc/ship.go`). |
|
||||
| `BlockUpgradeCost(blockMass, currentTech, target)` | Production cost of upgrading a single ship block (Phase 20 migrated this from `controller`). |
|
||||
| `BlockUpgradeCost(blockMass, currentTech, target)` | Production cost of upgrading a single ship block (migrated from `controller`). |
|
||||
| `FligthDistance(driveTech)`, `VisibilityDistance(...)` | Race-level reach formulas (`pkg/calc/race.go`). |
|
||||
| `ValidateShipTypeValues`, `CheckShipTypeValueDWSC` | Ship-design validators (`pkg/calc/validator.go`). |
|
||||
|
||||
@@ -105,83 +104,70 @@ never been exported.
|
||||
The table below tracks what UI features need from the bridge and
|
||||
whether the underlying Go function exists.
|
||||
|
||||
| UI feature | Go formula | In `pkg/calc/`? | Surfaced to TS? |
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: |
|
||||
| Ship-class designer preview (Phase 18) | `EmptyMass`, `FullMass`, `Speed`, `DriveEffective`, `CargoCapacity`, `CarryingMass`, `WeaponsBlockMass` (`pkg/calc/ship.go`) | yes | yes |
|
||||
| Ship-group modernize cost preview (Phase 20) | `BlockUpgradeCost` (`pkg/calc/ship.go`, migrated from `game/internal/controller/ship_group_upgrade.go`) | yes | yes |
|
||||
| Ship calculator combat (Phase 30) | `EffectiveAttack`, `EffectiveDefence`, `BombingPower` (`pkg/calc/ship.go`; `BombingPower` extracted from `model/game/group.go`) | yes | yes |
|
||||
| Ship calculator goal-seek (Phase 30) | inverse solvers in `pkg/calc/solve.go` | yes | yes |
|
||||
| Free production potential (`freeIndustry`) | `Planet.ProductionCapacity` → `industry*0.75 + population*0.25` (`game/internal/model/game/planet.go`) | no | no |
|
||||
| Industry production output per turn | `Planet.ProduceIndustry(freeProduction)` (`planet.go`); `freeProduction/5` modulo material constraint | no | no |
|
||||
| Materials production output per turn | `Planet.ProduceMaterial(freeProduction)` (`planet.go`); `freeProduction * resources` | no | no |
|
||||
| Per-tech research progress (DRIVE/WEAPONS/…) | `ResearchTech` (`game/internal/model/game/science.go`); `freeProduction / 5000` per tech level | no | no |
|
||||
| Custom-science progress | weighted form of `ResearchTech` driven by `Race.Sciences[].(Drive\|Weapons\|Shields\|Cargo)` (`science.go`) | no | no |
|
||||
| Ship build progress / planet build rate (Phase 30)| `ProduceShipsInTurn(L, Mat, Res, mass)` (`pkg/calc/planet.go`, extracted from `controller.ProduceShip`); `ShipBuildCost` | yes | yes |
|
||||
| UI feature | Go formula | In `pkg/calc/`? | Surfaced to TS? |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: |
|
||||
| Ship-class designer preview | `EmptyMass`, `FullMass`, `Speed`, `DriveEffective`, `CargoCapacity`, `CarryingMass`, `WeaponsBlockMass` (`pkg/calc/ship.go`) | yes | yes |
|
||||
| Ship-group modernize cost preview | `BlockUpgradeCost` (`pkg/calc/ship.go`, migrated from `game/internal/controller/ship_group_upgrade.go`) | yes | yes |
|
||||
| Ship calculator combat | `EffectiveAttack`, `EffectiveDefence`, `BombingPower` (`pkg/calc/ship.go`; `BombingPower` extracted from `model/game/group.go`) | yes | yes |
|
||||
| Ship calculator goal-seek | inverse solvers in `pkg/calc/solve.go` | yes | yes |
|
||||
| Free production potential (`freeIndustry`) | `Planet.ProductionCapacity` → `industry*0.75 + population*0.25` (`game/internal/model/game/planet.go`) | no | no |
|
||||
| Industry production output per turn | `Planet.ProduceIndustry(freeProduction)` (`planet.go`); `freeProduction/5` modulo material constraint | no | no |
|
||||
| Materials production output per turn | `Planet.ProduceMaterial(freeProduction)` (`planet.go`); `freeProduction * resources` | no | no |
|
||||
| Per-tech research progress (DRIVE/WEAPONS/…) | `ResearchTech` (`game/internal/model/game/science.go`); `freeProduction / 5000` per tech level | no | no |
|
||||
| Custom-science progress | weighted form of `ResearchTech` driven by `Race.Sciences[].(Drive\|Weapons\|Shields\|Cargo)` (`science.go`) | no | no |
|
||||
| Ship build progress / planet build rate | `ProduceShipsInTurn(L, Mat, Res, mass)` (`pkg/calc/planet.go`, extracted from `controller.ProduceShip`); `ShipBuildCost` | yes | yes |
|
||||
|
||||
`partial` means the Go primitives exist in `pkg/calc/` but the
|
||||
composition (and the conversion of TS-side `ReportPlanet`/
|
||||
`ShipClass` to the formula inputs) is not implemented anywhere.
|
||||
|
||||
## Phase 15 waiver
|
||||
## Production forecast waiver
|
||||
|
||||
Phase 15 ships the inspector's planet production controls
|
||||
(segmented control + sub-pickers + collapse-by-`planetNumber`
|
||||
order command) but **deliberately does not surface the per-type
|
||||
forecast number**. The planning gate explicitly raised the gap as
|
||||
a blocker per the plan's audit clause ("if any are missing in
|
||||
`pkg/calc/`, raise as blocker") and the project owner approved
|
||||
deferring the forecast to a dedicated future bridge phase. The
|
||||
inspector still renders the existing `freeIndustry` row (free
|
||||
production potential) — that number is computed engine-side and
|
||||
ships in the report payload, so no calc-bridge access is required
|
||||
for it today.
|
||||
The inspector's planet production controls (segmented control +
|
||||
sub-pickers + collapse-by-`planetNumber` order command) do **not**
|
||||
surface the per-type forecast number. The inspector renders the
|
||||
existing `freeIndustry` row (free production potential) — that number
|
||||
is computed engine-side and ships in the report payload, so no
|
||||
calc-bridge access is required for it. The per-type forecast number
|
||||
is deferred pending promotion of the relevant formulas into
|
||||
`pkg/calc/`.
|
||||
|
||||
Acceptance criterion 3 of Phase 15 ("forecast output number
|
||||
reflects the chosen production type and matches `pkg/calc/`
|
||||
outputs") is therefore intentionally not satisfied; the rewritten
|
||||
Phase 15 stage text records this decision and points back at this
|
||||
document.
|
||||
## Reach formula waiver
|
||||
|
||||
## Phase 16 waiver
|
||||
|
||||
Phase 16 introduces ship-reach filtering for the cargo-route
|
||||
destination picker. The engine formula is trivial:
|
||||
Ship-reach filtering for the cargo-route destination picker uses
|
||||
a trivial engine formula:
|
||||
|
||||
```
|
||||
flightDistance = driveTech * 40
|
||||
```
|
||||
|
||||
The Go-side reference now lives in
|
||||
The Go-side reference lives in
|
||||
[`pkg/calc/race.go`](../../pkg/calc/race.go) as
|
||||
`FligthDistance(driveTech) float64` (alongside the matching
|
||||
`VisibilityDistance` for in-space group reports — used in later
|
||||
phases). The engine call sites
|
||||
(`game/internal/model/game/race.go.FlightDistance`,
|
||||
`VisibilityDistance` for in-space group reports). The engine call
|
||||
sites (`game/internal/model/game/race.go.FlightDistance`,
|
||||
`game/internal/controller/route.go.PlanetRouteSet`) still wrap the
|
||||
Go formula directly; promoting them to call `pkg/calc/` is a
|
||||
follow-up cleanup outside Phase 16's scope.
|
||||
pending cleanup.
|
||||
|
||||
The original Phase 16 stage text described surfacing this through
|
||||
`pkg/calc/` and `ui/core/calc/`; with the calc-bridge phase still
|
||||
deferred, implementing the WASM glue for one constant-time
|
||||
multiplication would be premature scaffolding. The picker
|
||||
therefore computes reach inline in TypeScript using
|
||||
`torusShortestDelta(planet.x, candidate.x, mapWidth)` and
|
||||
`Math.hypot` against `40 * report.localPlayerDrive`, where
|
||||
`localPlayerDrive` is decoded from the report's `Player` block by
|
||||
matching `Player.name` to `report.race`
|
||||
Implementing the WASM glue for one constant-time multiplication
|
||||
would be premature scaffolding, so the picker computes reach inline
|
||||
in TypeScript using `torusShortestDelta(planet.x, candidate.x,
|
||||
mapWidth)` and `Math.hypot` against `40 * report.localPlayerDrive`,
|
||||
where `localPlayerDrive` is decoded from the report's `Player` block
|
||||
by matching `Player.name` to `report.race`
|
||||
(`api/game-state.ts.findLocalPlayerDrive`).
|
||||
|
||||
When the calc-bridge phase ships, the inline formula is replaced
|
||||
with a single call into the bridge — `calc.FligthDistance(driveTech)`
|
||||
becomes the source of truth for both the picker and the
|
||||
cargo-route auto-removal at turn cutoff. Until then, the UI
|
||||
duplicates `flightDistance` knowingly — same precedent as the
|
||||
production forecast deferral above.
|
||||
When the remaining bridge work ships, the inline formula will be
|
||||
replaced with a single call into the bridge —
|
||||
`calc.FligthDistance(driveTech)` becomes the source of truth for
|
||||
both the picker and the cargo-route auto-removal at turn cutoff.
|
||||
Until then, the UI duplicates `flightDistance` knowingly — same
|
||||
precedent as the production forecast deferral above.
|
||||
|
||||
## Planned bridge growth (follow-up phases)
|
||||
## Planned bridge growth
|
||||
|
||||
Phase 18 set up the canonical bridge layout (Go subpackage + WASM
|
||||
The canonical bridge layout is established (Go subpackage + WASM
|
||||
registration + typed `Core` interface + parity tests). Future calc
|
||||
work follows the same shape:
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Ship Class Calculator — UX
|
||||
|
||||
Phase 30 fuses the ship-class designer and a calculator into one sidebar
|
||||
tool (`lib/sidebar/calculator-tab.svelte`). It replaced the standalone
|
||||
designer view/route from Phases 17/18. All numeric math lives in
|
||||
The ship-class designer and calculator are fused into one sidebar
|
||||
tool (`lib/sidebar/calculator-tab.svelte`). The standalone designer
|
||||
view/route was replaced by this combined tool. All numeric math lives in
|
||||
`pkg/calc` and is reached through the `Core` WASM bridge; the calculator
|
||||
holds input state and orchestrates, it never computes.
|
||||
|
||||
@@ -36,7 +36,7 @@ in as a per-ship result rather than a separate mode.
|
||||
3. **Planet area** — when an own planet is selected on the map, shows
|
||||
its MAT (overridable) and the single-turn build rate (ships per turn,
|
||||
turns per ship). The realistic multi-turn forecast with CAP/COL
|
||||
supply is Phase 34.
|
||||
supply is planned (see ../ROADMAP.md).
|
||||
|
||||
## Locks and goal-seek
|
||||
|
||||
|
||||
+15
-16
@@ -1,13 +1,12 @@
|
||||
# Cargo routes UX
|
||||
|
||||
This document covers the cargo-route surface added in Phase 16: the
|
||||
four-slot inspector subsection, the map-driven destination pick, and
|
||||
the optimistic overlay that keeps the inspector and the map in lock-
|
||||
step with the local order draft. The user-visible spec lives in
|
||||
[`../PLAN.md`](../PLAN.md) Phase 16; the engine semantics are quoted
|
||||
from [`game/rules.txt`](../../game/rules.txt) section "Грузовые
|
||||
маршруты" (lines 808–843); this file is the source of truth for how
|
||||
the UI surfaces those rules.
|
||||
This document covers the cargo-route surface: the four-slot
|
||||
inspector subsection, the map-driven destination pick, and the
|
||||
optimistic overlay that keeps the inspector and the map in lock-step
|
||||
with the local order draft. The engine semantics are quoted from
|
||||
[`game/rules.txt`](../../game/rules.txt) section "Грузовые маршруты"
|
||||
(lines 808–843); this file is the source of truth for how the UI
|
||||
surfaces those rules.
|
||||
|
||||
## Engine semantics in one paragraph
|
||||
|
||||
@@ -44,7 +43,7 @@ section). `Remove` emits a `removeCargoRoute` command. The collapse
|
||||
rule on the order draft store ensures only one entry per
|
||||
`(source, loadType)` slot survives in the draft at any time, so a
|
||||
sequence of `Add → Edit → Remove` collapses to the latest verb only
|
||||
(matching the production-controls pattern from Phase 15).
|
||||
(matching the production-controls pattern).
|
||||
|
||||
Disabled state: every button is disabled when the
|
||||
`OrderDraftStore` or `MapPickService` context is missing (the
|
||||
@@ -98,8 +97,8 @@ centre + zoom before each remount and restores them when the game
|
||||
id is unchanged, so adding a route mid-pan does not jolt the view.
|
||||
|
||||
Arrows are drawn as a shaft plus two short arrowhead wings. Per-type
|
||||
styling (placeholder Phase 16 colours; final values land in Phase
|
||||
35 polish):
|
||||
styling (visual refinements are deferred to the finalization plan
|
||||
(../PLAN-finalize.md)):
|
||||
|
||||
| Load type | Stroke colour | Notes |
|
||||
| --------- | ------------- | ------------------------ |
|
||||
@@ -132,11 +131,11 @@ ownership of the *origin*). The picker mirrors that contract: the
|
||||
`reachableSet()` in `cargo-routes.svelte` filters out only the
|
||||
source planet itself.
|
||||
|
||||
Why inline rather than via a Go calc bridge? See the Phase 15 / 16
|
||||
deferral note in [`calc-bridge.md`](./calc-bridge.md). The formula
|
||||
is trivial (`tech × 40`) and the WASM glue would be premature
|
||||
infrastructure; when the calc bridge phase lands the shared
|
||||
`pkg/calc.FligthDistance` will replace this implementation.
|
||||
Why inline rather than via a Go calc bridge? See the deferral note
|
||||
in [`calc-bridge.md`](./calc-bridge.md). The formula is trivial
|
||||
(`tech × 40`) and the WASM glue would be premature infrastructure;
|
||||
when the calc bridge lands the shared `pkg/calc.FligthDistance` will
|
||||
replace this implementation.
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# In-game diplomatic mail UI
|
||||
|
||||
Phase 28 wires the in-game mail view that consumes the `diplomail`
|
||||
subsystem in the backend. The route lives at `/games/:id/mail`
|
||||
(registered in Phase 10) and replaces the active view when the user
|
||||
opens the "diplomatic mail" entry in the header menu.
|
||||
The in-game mail view consumes the `diplomail` subsystem in the
|
||||
backend. The route lives at `/games/:id/mail` and replaces the
|
||||
active view when the user opens the "diplomatic mail" entry in the
|
||||
header menu.
|
||||
|
||||
## Wire surface
|
||||
|
||||
@@ -29,8 +29,8 @@ the gateway translation lives in
|
||||
## Recipient by race name
|
||||
|
||||
The compose flow does **not** consult a memberships listing. The
|
||||
recipient picker reads `gameState.report.races[].name` (the Phase 22
|
||||
projection of `report.player[]`), and the send request carries the
|
||||
recipient picker reads `gameState.report.races[].name` (projected
|
||||
from `report.player[]`), and the send request carries the
|
||||
chosen race name as `recipient_race_name`. The backend resolves it
|
||||
against `Memberships.ListMembers(gameID, "active")` and rejects with
|
||||
`forbidden` if the matching member is no longer active. This keeps
|
||||
@@ -55,8 +55,8 @@ projects the union of inbox and sent into:
|
||||
|
||||
`read_at` and `deleted_at` are not surfaced to the user in any pane
|
||||
— they only drive the badge counter and the optimistic mark-read
|
||||
state. This is intentional (per Phase 28 decisions): the user-facing
|
||||
spec for diplomatic mail does not promise read receipts.
|
||||
state. This is intentional: the user-facing spec for diplomatic mail
|
||||
does not promise read receipts.
|
||||
|
||||
## Translation toggle
|
||||
|
||||
|
||||
+6
-8
@@ -9,9 +9,8 @@ stops it on sign-out.
|
||||
|
||||
## Why a single consumer
|
||||
|
||||
Before Phase 24, the watcher in `lib/revocation-watcher.ts` opened a
|
||||
parallel stream just to observe session revocation. Phase 24 folds
|
||||
that watcher into `EventStream` so that:
|
||||
The `EventStream` singleton consolidates what was previously a
|
||||
separate revocation watcher in `lib/revocation-watcher.ts` so that:
|
||||
|
||||
- there is only **one** SubscribeEvents connection per session
|
||||
(avoids doubling the gateway hub load);
|
||||
@@ -19,7 +18,7 @@ that watcher into `EventStream` so that:
|
||||
`Unauthenticated` ConnectError funnel through one
|
||||
`session.signOut("revoked")` call site;
|
||||
- per-event-type dispatch (turn-ready toasts, lobby/mail/battle
|
||||
notifications later) shares the same verification path.
|
||||
notifications) shares the same verification path.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
@@ -67,10 +66,9 @@ exponential backoff (base 1 s, ceiling 30 s, unbounded retries).
|
||||
- `reconnecting` — transient failure, backoff in flight.
|
||||
- `offline` — `navigator.onLine === false` at the moment of failure.
|
||||
|
||||
The header connection-state indicator planned in `PLAN.md`
|
||||
cross-cutting shell reads this rune; it is not part of Phase 24 but
|
||||
the rune is wired now so a later phase can add the dot without
|
||||
touching this module.
|
||||
The header connection-state indicator reads this rune; the rune is
|
||||
wired so a future change can add the indicator dot without touching
|
||||
this module.
|
||||
|
||||
## Revocation semantics
|
||||
|
||||
|
||||
+50
-58
@@ -1,10 +1,8 @@
|
||||
# Per-game state store
|
||||
|
||||
This document describes the per-game state owned by the in-game shell
|
||||
layout. Phase 11 introduces the store and uses it for two consumers
|
||||
(the header turn counter and the map view); later phases plug
|
||||
inspector tabs, the order composer, and the calculator on top of the
|
||||
same instance.
|
||||
layout. The store serves the header turn counter, the map view,
|
||||
inspector tabs, the order composer, and the calculator.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
@@ -23,10 +21,9 @@ gameId })`. `init`:
|
||||
2. calls `setGame(gameId)`, which:
|
||||
- reads the per-game wrap-mode preference from `Cache`
|
||||
(`game-prefs / <gameId>/wrap-mode`, default `torus`);
|
||||
- calls `lobby.my.games.list` and finds the game record (the
|
||||
Phase 11 wire schema extension on `GameSummary` adds
|
||||
`current_turn`); if the user is not a member, the store flips
|
||||
to `error`;
|
||||
- calls `lobby.my.games.list` and finds the game record
|
||||
(`GameSummary` carries `current_turn`); if the user is not a
|
||||
member, the store flips to `error`;
|
||||
- calls `user.games.report` for the discovered turn and decodes
|
||||
the FlatBuffers response into a TS-friendly `GameReport` shape.
|
||||
|
||||
@@ -44,24 +41,21 @@ The store exposes:
|
||||
| `wrapMode` | `torus / no-wrap` | per-game preference, persisted via `Cache` |
|
||||
| `error` | `string \| null` | localised error message when `status === "error"` |
|
||||
|
||||
## Phase boundaries
|
||||
## Store extensions
|
||||
|
||||
- Phase 11 surfaces only the planet subset of the report. Later
|
||||
phases extend `GameReport` and `decodeReport` as their slice of
|
||||
the wire lands (ships, fleets, sciences, routes, battles, mail).
|
||||
- Phase 26 splits `currentTurn` from the turn whose snapshot is
|
||||
displayed (`viewedTurn`) and adds `viewTurn(turn)` /
|
||||
`returnToCurrent()` for history navigation. The derived
|
||||
`historyMode` rune flips automatically when `viewedTurn <
|
||||
currentTurn`; the layout passes it to Phase 12's sidebar /
|
||||
bottom-tabs wiring (which hides the order tab) and to
|
||||
`OrderDraftStore.bindClient` (which gates `add` / `remove` /
|
||||
`move`). See "History mode" below for the cache and refresh
|
||||
rules.
|
||||
- Phase 24 replaces the tab-focus refresh with push-event-driven
|
||||
refreshes; the visibility listener stays as a fallback for
|
||||
background tabs that miss a push.
|
||||
- Phase 29 wires the wrap-mode toggle UI on top of `setWrapMode`.
|
||||
`GameReport` and `decodeReport` are extended as each slice of the
|
||||
wire lands (ships, fleets, sciences, routes, battles, mail).
|
||||
`currentTurn` is split from `viewedTurn`, and `viewTurn(turn)` /
|
||||
`returnToCurrent()` handle history navigation. The derived
|
||||
`historyMode` rune flips automatically when `viewedTurn <
|
||||
currentTurn`; the layout passes it to the sidebar / bottom-tabs
|
||||
wiring (which hides the order tab) and to
|
||||
`OrderDraftStore.bindClient` (which gates `add` / `remove` / `move`).
|
||||
See "History mode" below for the cache and refresh rules.
|
||||
Tab-focus refreshes are supplemented by push-event-driven refreshes;
|
||||
the visibility listener stays as a fallback for background tabs that
|
||||
miss a push. The wrap-mode toggle UI is wired on top of
|
||||
`setWrapMode`.
|
||||
|
||||
## Why `current_turn` lives on `GameSummary`
|
||||
|
||||
@@ -79,20 +73,19 @@ Extending `GameSummary` reuses the existing lobby pipeline; the
|
||||
backend already tracks `current_turn` in its runtime projection
|
||||
(`backend/internal/server/handlers_user_lobby_helpers.go`
|
||||
`gameSummaryToWire` reads it from `g.RuntimeSnapshot.CurrentTurn`).
|
||||
|
||||
The wire change touches Phase 8's already-shipped catalogue, but the
|
||||
`current_turn` field defaults to zero on the FB side, so existing
|
||||
The `current_turn` field defaults to zero on the FB side, so existing
|
||||
tests and the dev sandbox flow continue to work unchanged.
|
||||
|
||||
## State binding
|
||||
|
||||
`map/state-binding.ts::reportToWorld(report)` translates a
|
||||
`GameReport` into a renderer-ready `World`. Phase 11 emits one Point
|
||||
`GameReport` into a renderer-ready `World`. It emits one Point
|
||||
primitive per planet across all four kinds (local / other /
|
||||
uninhabited / unidentified). Each kind gets a distinct fill colour,
|
||||
fill alpha, and point radius so the four classes are
|
||||
visually-distinguishable at a glance; later phases will refine the
|
||||
colour palette as the visual language stabilises (Phase 35 polish).
|
||||
visually-distinguishable at a glance; colour-palette refinement is
|
||||
deferred to the finalization plan
|
||||
([../PLAN-finalize.md](../PLAN-finalize.md)).
|
||||
|
||||
The planet engine number is reused as the primitive id so a hit-test
|
||||
result can resolve back to a planet without an extra lookup table.
|
||||
@@ -108,9 +101,9 @@ unchanged), so a no-op refresh does not flicker the canvas.
|
||||
|
||||
In history mode `refresh()` is a no-op — forcing a reload would
|
||||
silently bump the user back onto the current turn while they are
|
||||
intentionally viewing a past one. Push events (Phase 24) still
|
||||
deliver new-turn notifications asynchronously while the user
|
||||
explores history, so the pending-turn toast continues to work.
|
||||
intentionally viewing a past one. Push events still deliver
|
||||
new-turn notifications asynchronously while the user explores
|
||||
history, so the pending-turn toast continues to work.
|
||||
|
||||
`setWrapMode(mode)` writes to `Cache` and updates the rune; the
|
||||
map view's effect picks the change up and re-mounts the renderer
|
||||
@@ -118,20 +111,19 @@ with the new mode.
|
||||
|
||||
## Map visibility toggles
|
||||
|
||||
Phase 29 adds a `mapToggles: MapToggles` rune that drives the
|
||||
gear popover in the map view. Every flag defaults to `true` —
|
||||
including `unreachablePlanets` (showing every planet by default)
|
||||
and `visibleHyperspace` (the fog overlay on by default). The
|
||||
exhaustive shape lives in `src/lib/game-state.svelte.ts`; the
|
||||
gear popover (`src/lib/active-view/map-toggles.svelte`) is a
|
||||
thin view of the rune.
|
||||
A `mapToggles: MapToggles` rune drives the gear popover in the map
|
||||
view. Every flag defaults to `true` — including `unreachablePlanets`
|
||||
(showing every planet by default) and `visibleHyperspace` (the fog
|
||||
overlay on by default). The exhaustive shape lives in
|
||||
`src/lib/game-state.svelte.ts`; the gear popover
|
||||
(`src/lib/active-view/map-toggles.svelte`) is a thin view of the
|
||||
rune.
|
||||
|
||||
`setMapToggle(key, value)` flips one entry in place and
|
||||
persists the whole blob to `Cache` under the
|
||||
`game-map-toggles/{gameId}` key. The blob carries a companion
|
||||
`lastResetTurn` number — the turn at which the toggles were last
|
||||
reset to defaults — so the new-turn reset path (below) can detect
|
||||
a stale blob even across a cross-session gap.
|
||||
`setMapToggle(key, value)` flips one entry in place and persists
|
||||
the whole blob to `Cache` under the `game-map-toggles/{gameId}` key.
|
||||
The blob carries a companion `lastResetTurn` number — the turn at
|
||||
which the toggles were last reset to defaults — so the new-turn reset
|
||||
path (below) can detect a stale blob even across a cross-session gap.
|
||||
|
||||
### New-turn reset
|
||||
|
||||
@@ -156,7 +148,7 @@ The cache namespace and blob shape are documented in
|
||||
|
||||
## History mode
|
||||
|
||||
Phase 26 lets the user step backward through the report timeline
|
||||
The store lets the user step backward through the report timeline
|
||||
without losing the live snapshot. The store keeps two turn runes:
|
||||
|
||||
- `currentTurn` — the server's authoritative latest. Only
|
||||
@@ -170,7 +162,7 @@ The derived `historyMode` rune (`status === "ready" && viewedTurn
|
||||
< currentTurn`) drives every history-aware consumer:
|
||||
|
||||
- the layout passes it to `Sidebar` / `BottomTabs` so the order
|
||||
tab vanishes (Phase 12 prop wiring);
|
||||
tab vanishes;
|
||||
- the layout passes a `getHistoryMode` getter to
|
||||
`OrderDraftStore.bindClient` so `add` / `remove` / `move` are
|
||||
no-ops while the user is looking at a past turn;
|
||||
@@ -179,17 +171,17 @@ The derived `historyMode` rune (`status === "ready" && viewedTurn
|
||||
- the new `HistoryBanner` component renders the sticky "Viewing
|
||||
turn N · read-only" strip when the flag is true.
|
||||
|
||||
`last-viewed-turn` semantics keep their Phase 11 meaning: "the
|
||||
latest turn the user was caught up on". `loadTurn` only writes the
|
||||
cache row when called with `isCurrent === true` (i.e. when the
|
||||
load matches `currentTurn`). Historical excursions are therefore
|
||||
ephemeral: closing the tab and reopening the game resumes on the
|
||||
last caught-up turn, not on the last clicked one.
|
||||
`last-viewed-turn` means "the latest turn the user was caught up
|
||||
on". `loadTurn` only writes the cache row when called with
|
||||
`isCurrent === true` (i.e. when the load matches `currentTurn`).
|
||||
Historical excursions are therefore ephemeral: closing the tab and
|
||||
reopening the game resumes on the last caught-up turn, not on the
|
||||
last clicked one.
|
||||
|
||||
Past-turn reports are cached in the `game-history` namespace
|
||||
(`{gameId}/turn/{N}` → `GameReport`). The cache is written by
|
||||
`loadTurn` on every successful historical fetch and read first by
|
||||
`viewTurn(N)` before falling back to the network. Past turns are
|
||||
immutable, so the cache has no TTL and no eviction in Phase 26.
|
||||
The current-turn snapshot is deliberately *not* cached — it is
|
||||
mutable until the next engine tick.
|
||||
immutable, so the cache has no TTL and no eviction. The current-turn
|
||||
snapshot is deliberately *not* cached — it is mutable until the next
|
||||
engine tick.
|
||||
|
||||
+13
-11
@@ -1,14 +1,14 @@
|
||||
# i18n (UI)
|
||||
|
||||
The UI client ships with a minimal locale primitive used by the
|
||||
phase-7 login form, the root layout, and the lobby placeholder. The
|
||||
goal is just enough infrastructure to translate user-visible
|
||||
strings, switch the active language at runtime, and forward the
|
||||
caller's choice to the gateway. Phase 35 will swap this primitive
|
||||
for a fuller solution once message-format pluralisation, lazy
|
||||
loading, and translator workflows become necessary; until then,
|
||||
the surface here covers every authenticated and unauthenticated
|
||||
screen the client renders.
|
||||
login form, the root layout, and the lobby. The goal is just
|
||||
enough infrastructure to translate user-visible strings, switch
|
||||
the active language at runtime, and forward the caller's choice to
|
||||
the gateway. Swapping this primitive for a fuller solution with
|
||||
message-format pluralisation, lazy loading, and translator
|
||||
workflows is deferred to the finalization plan
|
||||
(../Plan-finalize.md); until then, the surface here covers every
|
||||
authenticated and unauthenticated screen the client renders.
|
||||
|
||||
## Surface
|
||||
|
||||
@@ -79,13 +79,15 @@ any preference, or `DEFAULT_LOCALE` (English) when nothing matches.
|
||||
The web target calls it without arguments, in which case the helper
|
||||
reads `navigator.languages` (or `navigator.language` as fallback).
|
||||
Native wrappers (Wails, Capacitor) will pass their system locale
|
||||
once Phase 31/32 lands; the helper is platform-agnostic by design.
|
||||
once the desktop/mobile targets land (see ../ROADMAP.md); the
|
||||
helper is platform-agnostic by design.
|
||||
|
||||
The detection runs once at module load — there is no asynchronous
|
||||
init step. Callers that mutate the locale (e.g. the language picker
|
||||
on `/login`) call `i18n.setLocale(next)` directly. The choice is
|
||||
**not** persisted between page reloads in Phase 7; the next visit
|
||||
re-runs detection. Persistence is a phase-35 concern.
|
||||
**not** persisted between page reloads; the next visit re-runs
|
||||
detection. Persistence is deferred to the finalization plan
|
||||
(../Plan-finalize.md).
|
||||
|
||||
## Forwarding the locale to the gateway
|
||||
|
||||
|
||||
+15
-19
@@ -2,11 +2,9 @@
|
||||
|
||||
The lobby is the first authenticated view; the user lands here after
|
||||
the email-code login completes (see
|
||||
[`docs/auth-flow.md`](auth-flow.md)). Phase 8 introduced the live
|
||||
lobby with five sections, the create-game form, and the TS-side
|
||||
FlatBuffers integration the rest of the client builds on. This doc
|
||||
captures the sections, the application / invite lifecycle the user
|
||||
sees, and the defaults baked into the create-game form.
|
||||
[`docs/auth-flow.md`](auth-flow.md)). This doc captures the
|
||||
sections, the application / invite lifecycle the user sees, and
|
||||
the defaults baked into the create-game form.
|
||||
|
||||
## Sections
|
||||
|
||||
@@ -23,15 +21,15 @@ width.
|
||||
| `my applications` | `no applications` | `lobby.my.applications.list` | Status badge (`pending` / `approved` / `rejected`) |
|
||||
| `public games` | `no public games` | `lobby.public.games.list` | Submit application via inline race-name form (`lobby.application.submit`) |
|
||||
|
||||
The header preserves the device-session-id `<code>` block from the
|
||||
Phase 7 placeholder (kept as a debug affordance) plus a greeting if
|
||||
the gateway returns a `display_name` for the caller.
|
||||
The header preserves the device-session-id `<code>` block (kept as
|
||||
a debug affordance) plus a greeting if the gateway returns a
|
||||
`display_name` for the caller.
|
||||
|
||||
`GameSummary` carries an extra `current_turn` field (Phase 11
|
||||
extension) that the lobby UI does not display directly — the in-game
|
||||
shell reads it from the same payload to load the matching
|
||||
`user.games.report` for the map view without an additional gateway
|
||||
call. See [`game-state.md`](game-state.md) for the consumer's view.
|
||||
`GameSummary` carries a `current_turn` field that the lobby UI does
|
||||
not display directly — the in-game shell reads it from the same
|
||||
payload to load the matching `user.games.report` for the map view
|
||||
without an additional gateway call. See
|
||||
[`game-state.md`](game-state.md) for the consumer's view.
|
||||
|
||||
## Application lifecycle
|
||||
|
||||
@@ -104,7 +102,7 @@ and falls back to the gateway-supplied message via
|
||||
|
||||
The gateway encodes lobby payloads through `pkg/transcoder/lobby.go`
|
||||
into FlatBuffers bytes; the browser must decode them with the same
|
||||
schema. Phase 8 ships:
|
||||
schema. The TS integration ships:
|
||||
|
||||
- `flatbuffers` runtime dependency in `ui/frontend/package.json`;
|
||||
- `make -C ui fbs-ts` driving `flatc --ts` to regenerate the bindings
|
||||
@@ -112,8 +110,6 @@ schema. Phase 8 ships:
|
||||
- a Vitest round-trip suite (`tests/lobby-fbs.test.ts`) that catches
|
||||
binding drift in CI.
|
||||
|
||||
Phase 7's `user.account.get` decode previously used
|
||||
`JSON.parse(TextDecoder)`; that path was rewritten in Phase 8 to use
|
||||
the same generated `AccountResponse` table, so the lobby greeting now
|
||||
works against a real local stack as well as the mocked Playwright
|
||||
fixtures.
|
||||
`user.account.get` decodes through the generated `AccountResponse`
|
||||
table, so the lobby greeting works against a real local stack as well
|
||||
as the mocked Playwright fixtures.
|
||||
|
||||
+56
-65
@@ -18,50 +18,45 @@ two-line wrapper that mounts the matching content component from
|
||||
the plan is the file system plus those wrappers — there is no
|
||||
separate dispatch component.
|
||||
|
||||
| URL | Active view component | Phase that fills it |
|
||||
| ------------------------------------- | ---------------------------------------------- | ----------------------- |
|
||||
| URL | Active view component | Phase that fills it |
|
||||
| ------------------------------------------ | ---------------------------------------------- | ----------------------- |
|
||||
| `/games/:id/map` | `lib/active-view/map.svelte` | Phase 11 |
|
||||
| `/games/:id/table/:entity` | `lib/active-view/table.svelte` | Phase 11 / 17 / 19 / 22 |
|
||||
| `/games/:id/report` | `lib/active-view/report.svelte` (see [report-view.md](report-view.md)) | Phase 23 |
|
||||
| `/games/:id/battle/:battleId?` | `lib/active-view/battle.svelte` | Phase 27 |
|
||||
| `/games/:id/mail` | `lib/active-view/mail.svelte` | Phase 28 |
|
||||
| `/games/:id/designer/science/:scienceId?` | `lib/active-view/designer-science.svelte` | Phase 21 |
|
||||
| URL | Active view component |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| `/games/:id/map` | `lib/active-view/map.svelte` |
|
||||
| `/games/:id/table/:entity` | `lib/active-view/table.svelte` |
|
||||
| `/games/:id/report` | `lib/active-view/report.svelte` (see [report-view.md](report-view.md)) |
|
||||
| `/games/:id/battle/:battleId?` | `lib/active-view/battle.svelte` |
|
||||
| `/games/:id/mail` | `lib/active-view/mail.svelte` |
|
||||
| `/games/:id/designer/science/:scienceId?` | `lib/active-view/designer-science.svelte` |
|
||||
|
||||
`/games/:id` (no trailing view) redirects to `/games/:id/map`. The
|
||||
optional `:scienceId?` segment on the science designer route matches
|
||||
SvelteKit's `[[scienceId]]` syntax — `/designer/science` opens the
|
||||
empty new-science form, `/designer/science/{name}` opens the named
|
||||
science. Phase 17/18 originally added a parallel ship-class designer
|
||||
route; Phase 30 removed it and folded ship-class design into the
|
||||
sidebar ship-class calculator (`lib/sidebar/calculator-tab.svelte`,
|
||||
see [calculator-ux.md](calculator-ux.md)), reached from the
|
||||
ship-classes table and the view/bottom menus.
|
||||
science. Ship-class design is folded into the sidebar ship-class
|
||||
calculator (`lib/sidebar/calculator-tab.svelte`, see
|
||||
[calculator-ux.md](calculator-ux.md)), reached from the ship-classes
|
||||
table and the view/bottom menus.
|
||||
|
||||
The `entity` slug on the table route is kebab-case (`planets`,
|
||||
`ship-classes`, `ship-groups`, `fleets`, `sciences`, `races`).
|
||||
`table.svelte` is the active-view router: it dispatches by slug to
|
||||
the per-entity component (`ship-classes` → `table-ship-classes.svelte`
|
||||
in Phase 17; the others fall back to the Phase 10 stub copy until
|
||||
their respective phases land).
|
||||
the per-entity component (`ship-classes` → `table-ship-classes.svelte`;
|
||||
other entities dispatch to their respective components).
|
||||
|
||||
## Sidebar tools and state preservation
|
||||
|
||||
The desktop sidebar hosts three tools:
|
||||
|
||||
| Tool | Component | Phase that fills it |
|
||||
| ---------- | -------------------------------------- | -------------------- |
|
||||
| Calculator | `lib/sidebar/calculator-tab.svelte` | Phase 30 |
|
||||
| Inspector | `lib/sidebar/inspector-tab.svelte` | Phase 13 / 19 |
|
||||
| Order | `lib/sidebar/order-tab.svelte` | Phase 12 / 14 |
|
||||
| Tool | Component |
|
||||
| ---------- | ----------------------------------- |
|
||||
| Calculator | `lib/sidebar/calculator-tab.svelte` |
|
||||
| Inspector | `lib/sidebar/inspector-tab.svelte` |
|
||||
| Order | `lib/sidebar/order-tab.svelte` |
|
||||
|
||||
The selected-tab state is a `$state` rune in
|
||||
`routes/games/[id]/+layout.svelte`, bound into
|
||||
`lib/sidebar/sidebar.svelte` via `$bindable()`. The layout owns the
|
||||
rune so external events — Phase 13's planet click, future similar
|
||||
flows — can drive the active tab from outside the sidebar without
|
||||
plumbing callbacks. The component is mounted by the layout, and
|
||||
rune so external events — such as a planet click — can drive the
|
||||
active tab from outside the sidebar without plumbing callbacks. The component is mounted by the layout, and
|
||||
SvelteKit keeps that layout instance alive while the user navigates
|
||||
between child routes (`/games/:id/map` → `/games/:id/report` → …),
|
||||
so the rune survives every active-view switch automatically with no
|
||||
@@ -70,23 +65,21 @@ described below still live inside the sidebar — they mutate the
|
||||
bindable in place; the layout sees the change through the binding.
|
||||
|
||||
A `?sidebar=calc|calculator|inspector|order` URL param is read once
|
||||
on mount and seeds the initial tab. Later phases that want to land
|
||||
the user on a particular tool (for example, Phase 14's first
|
||||
end-to-end command flow) can set it on navigation.
|
||||
on mount and seeds the initial tab. Navigation flows that want to
|
||||
land the user on a particular tool can set this param on navigation.
|
||||
|
||||
The Order entry is hidden when the layout's `historyMode` flag is
|
||||
true. Phase 12 plumbs the flag end-to-end as a prop —
|
||||
`+layout.svelte` forwards a derived value to `Sidebar`, which
|
||||
true. `+layout.svelte` forwards a derived value to `Sidebar`, which
|
||||
forwards `hideOrder` to its `TabBar`; the same flag goes to
|
||||
`BottomTabs` so the mobile `Order` button is also suppressed. A
|
||||
`?sidebar=order` URL seed that arrives while the flag is true falls
|
||||
back to `inspector`, and an `$effect` on the sidebar resets
|
||||
`activeTab` away from `order` if the flag flips on mid-session.
|
||||
|
||||
Phase 26 wires the flag to the live history signal owned by
|
||||
`GameStateStore`. The derivation lives directly in `+layout.svelte`
|
||||
The `historyMode` flag is derived from the live history signal owned
|
||||
by `GameStateStore`. The derivation lives directly in `+layout.svelte`
|
||||
(`const historyMode = $derived(gameState.historyMode)`) — no
|
||||
separate `lib/history-mode.ts` module ships, because the layout is
|
||||
separate `lib/history-mode.ts` module exists, because the layout is
|
||||
the single consumer and the project's compactness rule rejects a
|
||||
one-line indirection. The order draft survives the toggle because
|
||||
`OrderDraftStore` lives one level above the sidebar in the layout
|
||||
@@ -100,8 +93,7 @@ for the draft-store side of the flow and
|
||||
|
||||
## Header turn navigator and history banner
|
||||
|
||||
The header replaces the Phase 11 inline `turn N` text with a
|
||||
`← Turn N →` triplet (`lib/header/turn-navigator.svelte`). The
|
||||
The header shows a `← Turn N →` triplet (`lib/header/turn-navigator.svelte`). The
|
||||
arrows step `viewedTurn` by ±1 (disabled at boundaries `0` and
|
||||
`currentTurn`); clicking the middle button opens an absolute
|
||||
popover (desktop) or a fixed full-width drawer (mobile, ≤ 767.98
|
||||
@@ -129,9 +121,8 @@ Three discrete CSS modes matched to the IA section diagrams:
|
||||
toggle in the header right corner. Tapping the toggle slides the
|
||||
sidebar in as a fixed overlay above the active view; a close
|
||||
button on the sidebar dismisses it. The full swipe-from-right
|
||||
gesture in the IA section is deferred to Phase 35 polish — the
|
||||
click toggle satisfies the "layout switches at 768 px" acceptance
|
||||
criterion on Phase 10.
|
||||
gesture is deferred to the finalization plan
|
||||
([../PLAN-finalize.md](../PLAN-finalize.md)).
|
||||
- **< 768 px (mobile)** — the sidebar is hidden entirely and the
|
||||
bottom-tabs row appears at the bottom of the viewport. The
|
||||
view-menu trigger swaps to a hamburger icon (☰) that opens the
|
||||
@@ -139,7 +130,7 @@ Three discrete CSS modes matched to the IA section diagrams:
|
||||
|
||||
On mobile the bottom tab row does not include `Inspector`. The
|
||||
inspector content is reached by tapping a map object instead, which
|
||||
raises a bottom-sheet — see [Planet selection](#planet-selection-phase-13).
|
||||
raises a bottom-sheet — see [Planet selection](#planet-selection).
|
||||
|
||||
## Mobile bottom-tabs and tool overlay
|
||||
|
||||
@@ -157,27 +148,27 @@ The next time the user taps a Calc or Order bottom-tab, the
|
||||
navigation re-routes them to `/map` and re-applies the overlay.
|
||||
|
||||
The `More` button opens a drawer that mirrors the header view-menu
|
||||
content. The IA section's narrower "More" list (Mail, Battle log,
|
||||
Tables, History, Settings, Logout) is the polish target for Phase 35
|
||||
— Phase 10 keeps a single source of truth for destinations.
|
||||
content. A narrower "More" list (Mail, Battle log, Tables, History,
|
||||
Settings, Logout) is deferred to the finalization plan
|
||||
([../PLAN-finalize.md](../PLAN-finalize.md)); the current drawer keeps
|
||||
a single source of truth for destinations.
|
||||
|
||||
## Transient map overlays
|
||||
|
||||
Some views can push a transient overlay onto `/map` with a back
|
||||
affordance. (Phase 30's calculator reach circles are a simpler,
|
||||
always-on map extra rather than a back-stacked overlay; the transient
|
||||
back-stack mechanism itself is still a Phase 34 concept.) A transient
|
||||
overlay clears when the user navigates to any other view via the header
|
||||
or the bottom-tabs.
|
||||
affordance. (The calculator reach circles are a simpler, always-on
|
||||
map extra rather than a back-stacked overlay; the transient
|
||||
back-stack mechanism is planned — see
|
||||
[../ROADMAP.md](../ROADMAP.md).) A transient overlay clears when the
|
||||
user navigates to any other view via the header or the bottom-tabs.
|
||||
|
||||
Phase 10 documents this concept but does not implement the
|
||||
back-stack mechanism. Phase 34 lands the back-stack alongside its
|
||||
first user (multi-turn projection, range circles in the ship-class
|
||||
designer).
|
||||
The back-stack mechanism is not yet implemented; it is planned
|
||||
alongside its first user (multi-turn projection, range circles in the
|
||||
ship-class designer) in [../ROADMAP.md](../ROADMAP.md).
|
||||
|
||||
## Planet selection (Phase 13)
|
||||
## Planet selection
|
||||
|
||||
The map view turns into the entry point for the inspector by
|
||||
The map view is the entry point for the inspector by
|
||||
translating a renderer click into a planet selection. The flow:
|
||||
|
||||
1. The renderer (`src/map/render.ts`) exposes `onClick(cb)` next to
|
||||
@@ -190,9 +181,9 @@ translating a renderer click into a planet selection. The flow:
|
||||
`GameStateStore.report`, and calls `SelectionStore.selectPlanet(number)`.
|
||||
3. `SelectionStore` (`lib/selection.svelte.ts`) is a runes store
|
||||
instantiated by the layout and exposed via Svelte context under
|
||||
`SELECTION_CONTEXT_KEY`. It carries a discriminated union — Phase
|
||||
13 only models `{ kind: "planet"; id: number }`; Phase 19 widens
|
||||
it for ship groups. Selection is in-memory only: it survives the
|
||||
`SELECTION_CONTEXT_KEY`. It carries a discriminated union —
|
||||
`{ kind: "planet"; id: number }` for planets and widened for
|
||||
ship groups. Selection is in-memory only: it survives the
|
||||
layout's lifetime (active-view switches inside `/games/:id/*`)
|
||||
but does not persist across reloads — that contrast with the
|
||||
order draft is intentional.
|
||||
@@ -211,11 +202,11 @@ translating a renderer click into a planet selection. The flow:
|
||||
|
||||
The mobile bottom-sheet is mounted alongside `<BottomTabs />` in the
|
||||
layout. Its visibility is conditional on `effectiveTool === "map"` so
|
||||
it does not stack on top of the calc / order overlays. Phase 13 ships
|
||||
the minimal dismissal surface: a close button (`✕`) that calls
|
||||
`SelectionStore.clear()`. Tap-outside and swipe-down dismissal from
|
||||
the IA section are deferred to Phase 35 polish. A click that lands on
|
||||
empty space is a no-op — selection is mutated only by an explicit
|
||||
it does not stack on top of the calc / order overlays. The dismissal
|
||||
surface is a close button (`✕`) that calls `SelectionStore.clear()`.
|
||||
Tap-outside and swipe-down dismissal are deferred to the finalization
|
||||
plan ([../PLAN-finalize.md](../PLAN-finalize.md)). A click that lands
|
||||
on empty space is a no-op — selection is mutated only by an explicit
|
||||
planet click or by the close button.
|
||||
|
||||
The planet inspector itself is a presentational component: it takes
|
||||
@@ -227,9 +218,9 @@ field the FBS schema carries (`industryStockpile` for `capital`,
|
||||
Fields the FBS table does not project for a given kind read as `null`
|
||||
and the inspector simply omits the row.
|
||||
|
||||
The selected-planet visual on the map (a ring or halo) is **not**
|
||||
shipped in Phase 13. It rolls into Phase 35 polish together with the
|
||||
sheet's swipe-to-dismiss gesture.
|
||||
The selected-planet visual on the map (a ring or halo) is deferred
|
||||
to the finalization plan ([../PLAN-finalize.md](../PLAN-finalize.md))
|
||||
together with the sheet's swipe-to-dismiss gesture.
|
||||
|
||||
## Auth gate
|
||||
|
||||
|
||||
+64
-80
@@ -25,29 +25,28 @@ during a connectivity hiccup keeps every line the player typed. A
|
||||
remote-first composer that reflects the gateway's pending-orders
|
||||
queue would force a sync on every keystroke.
|
||||
|
||||
Phase 14 lands the submit pipeline with batch semantics: every
|
||||
entry the user has marked `valid` is collected into one signed
|
||||
`user.games.order` request. The engine validates and stores the
|
||||
order, returning per-command `cmdApplied` / `cmdErrorCode` in the
|
||||
response body. The gateway re-encodes that JSON into the FBS
|
||||
`UserGamesOrderResponse` envelope (with `commands: [CommandItem]`
|
||||
populated), and `submitOrder` rejoins the verdict to each draft
|
||||
entry by `cmdId`. Successfully applied entries stay visible in
|
||||
the draft (the player keeps composing until turn cutoff);
|
||||
rejected entries stay until the player edits or removes them.
|
||||
The submit pipeline uses batch semantics: every entry the user has
|
||||
marked `valid` is collected into one signed `user.games.order`
|
||||
request. The engine validates and stores the order, returning
|
||||
per-command `cmdApplied` / `cmdErrorCode` in the response body. The
|
||||
gateway re-encodes that JSON into the FBS `UserGamesOrderResponse`
|
||||
envelope (with `commands: [CommandItem]` populated), and `submitOrder`
|
||||
rejoins the verdict to each draft entry by `cmdId`. Successfully
|
||||
applied entries stay visible in the draft (the player keeps composing
|
||||
until turn cutoff); rejected entries stay until the player edits or
|
||||
removes them.
|
||||
|
||||
Phase 25 layers a transport-level policy on top of this baseline
|
||||
without changing the batch semantics. The submit pipeline now
|
||||
goes through `OrderQueue` (see
|
||||
[`sync-protocol.md`](sync-protocol.md)): the queue holds the
|
||||
submit while the browser is offline, classifies
|
||||
`turn_already_closed` and `game_paused` server replies into
|
||||
matching banners on the order tab, and exits the loop on the
|
||||
sticky states so a stream of mutations does not re-elicit the
|
||||
same gateway reply. Recovery from a `conflict` or `paused`
|
||||
banner happens on the next `game.turn.ready` push frame via
|
||||
`OrderDraftStore.resetForNewTurn`, which clears the local draft
|
||||
and re-hydrates from the server for the new turn.
|
||||
A transport-level policy layers on top of the batch baseline without
|
||||
changing the batch semantics. The submit pipeline goes through
|
||||
`OrderQueue` (see [`sync-protocol.md`](sync-protocol.md)): the queue
|
||||
holds the submit while the browser is offline, classifies
|
||||
`turn_already_closed` and `game_paused` server replies into matching
|
||||
banners on the order tab, and exits the loop on the sticky states so
|
||||
a stream of mutations does not re-elicit the same gateway reply.
|
||||
Recovery from a `conflict` or `paused` banner happens on the next
|
||||
`game.turn.ready` push frame via `OrderDraftStore.resetForNewTurn`,
|
||||
which clears the local draft and re-hydrates from the server for the
|
||||
new turn.
|
||||
|
||||
## Local-validation invariant
|
||||
|
||||
@@ -58,10 +57,9 @@ pipeline refuses to drain a draft that contains any `invalid`
|
||||
entries. The validation step is per-command and pure — it consults
|
||||
the current `GameStateStore` snapshot only, never the network.
|
||||
|
||||
Phase 14's `planetRename` is the first variant that exercises the
|
||||
`draft → valid | invalid` transition. The validator
|
||||
(`lib/util/entity-name.ts`) is a TS port of
|
||||
`pkg/util/string.go.ValidateTypeName`, exercised on every render
|
||||
The `planetRename` variant exercises the `draft → valid | invalid`
|
||||
transition. The validator (`lib/util/entity-name.ts`) is a TS port
|
||||
of `pkg/util/string.go.ValidateTypeName`, exercised on every render
|
||||
in the inline editor and re-run by the store on every `add`. The
|
||||
submit pipeline filters the draft to `valid` entries only — any
|
||||
`invalid` row blocks the Submit button.
|
||||
@@ -79,13 +77,12 @@ draft ──validate──▶ valid ──submit──▶ submitting ──ack
|
||||
Transitions:
|
||||
|
||||
- **`draft → valid` / `draft → invalid`**: local validation. May
|
||||
re-run when the underlying `GameStateStore` snapshot changes
|
||||
(Phase 14+).
|
||||
re-run when the underlying `GameStateStore` snapshot changes.
|
||||
- **`valid → submitting`**: the submit pipeline picks the entry off
|
||||
the draft and sends it to the gateway.
|
||||
- **`submitting → applied` / `submitting → rejected`**: the gateway
|
||||
responded; the entry is no longer in flight.
|
||||
- **`submitting → conflict`** (Phase 25): the gateway returned
|
||||
- **`submitting → conflict`**: the gateway returned
|
||||
`resultCode = "turn_already_closed"`. The order tab surfaces a
|
||||
banner above the command list. Any subsequent mutation
|
||||
re-validates the conflict row back to `valid` / `invalid`; a
|
||||
@@ -94,10 +91,10 @@ Transitions:
|
||||
[`sync-protocol.md`](sync-protocol.md) for the full state
|
||||
table and recovery paths.
|
||||
|
||||
Phase 14 lands the local validators (`draft → valid | invalid`),
|
||||
the submit pipeline (`valid → submitting → applied | rejected`),
|
||||
and the optimistic overlay that shows the player's intent on the
|
||||
map and inspector while the order is in flight.
|
||||
Local validators (`draft → valid | invalid`), the submit pipeline
|
||||
(`valid → submitting → applied | rejected`), and the optimistic
|
||||
overlay that shows the player's intent on the map and inspector while
|
||||
the order is in flight are all implemented.
|
||||
|
||||
Statuses are runtime-only — they are not persisted alongside the
|
||||
commands themselves. On every `init` the store re-runs
|
||||
@@ -110,9 +107,7 @@ stored value).
|
||||
|
||||
## Discriminated union shape
|
||||
|
||||
`OrderCommand` is a discriminated union on the `kind` field. Phase
|
||||
12 shipped the skeleton with a single content-free variant; Phase
|
||||
14 added the first real one and Phase 15 added the second:
|
||||
`OrderCommand` is a discriminated union on the `kind` field:
|
||||
|
||||
```ts
|
||||
interface PlaceholderCommand {
|
||||
@@ -148,9 +143,9 @@ The `id` field is the canonical identifier the store uses for
|
||||
remove and reorder; later variants must keep `id: string` so the
|
||||
store API stays uniform. The whole draft round-trips through
|
||||
IndexedDB structured clone, so every variant must use only
|
||||
JSON-friendly value types. Phase 14 lands `planetRename` together
|
||||
with the inline editor in `lib/inspectors/planet.svelte`, the
|
||||
local validator (`lib/util/entity-name.ts`, parity with
|
||||
JSON-friendly value types. `planetRename` ships with the inline
|
||||
editor in `lib/inspectors/planet.svelte`, the local validator
|
||||
(`lib/util/entity-name.ts`, parity with
|
||||
`pkg/util/string.go.ValidateTypeName`), and the submit pipeline.
|
||||
|
||||
`setProductionType` is the wire-mirror of the engine's
|
||||
@@ -164,10 +159,10 @@ optimistic overlay rewrites `planet.production` using
|
||||
mirrors the engine's `Cache.PlanetProductionDisplayName` so the
|
||||
overlay stays byte-equal with the next server report.
|
||||
|
||||
### Collapse-by-target rule (Phase 15)
|
||||
### Collapse-by-target rule
|
||||
|
||||
`setProductionType` is the first variant to carry a
|
||||
collapse-by-target rule. `OrderDraftStore.add` enforces it:
|
||||
`setProductionType` carries a collapse-by-target rule.
|
||||
`OrderDraftStore.add` enforces it:
|
||||
when the incoming command's `kind` is `"setProductionType"` it
|
||||
drops every prior `setProductionType` entry with the same
|
||||
`planetNumber` (and the matching keys from `statuses`) before
|
||||
@@ -187,9 +182,7 @@ coexist — the rules apply within a `kind`, not across.
|
||||
`OrderDraftStore` lives in
|
||||
[`../frontend/src/sync/order-draft.svelte.ts`](../frontend/src/sync/order-draft.svelte.ts).
|
||||
The class is a Svelte 5 runes store, so the file extension is
|
||||
`.svelte.ts` (the original PLAN.md artifact line listed `.ts` —
|
||||
the deviation is documented inline in `PLAN.md`'s Phase 12
|
||||
"Decisions" subsection).
|
||||
`.svelte.ts`.
|
||||
|
||||
Lifecycle:
|
||||
|
||||
@@ -212,9 +205,8 @@ Layout integration mirrors `GameStateStore`:
|
||||
- Exposed through the `ORDER_DRAFT_CONTEXT_KEY` Svelte context.
|
||||
- Disposed in the layout's `onDestroy`.
|
||||
|
||||
The order tab consumes the store via
|
||||
`getContext(ORDER_DRAFT_CONTEXT_KEY)`; Phase 14's planet inspector
|
||||
will use the same key to push a new command.
|
||||
The order tab and the planet inspector both consume the store via
|
||||
`getContext(ORDER_DRAFT_CONTEXT_KEY)` to push new commands.
|
||||
|
||||
## Submit pipeline
|
||||
|
||||
@@ -224,9 +216,9 @@ will use the same key to push a new command.
|
||||
`markSubmitting(ids)` so each row reads `submitting`, then
|
||||
posts the snapshot through `submitOrder`.
|
||||
2. `submitOrder` builds the FBS `UserGamesOrder` request (game_id,
|
||||
`updatedAt = 0` in Phase 14, every command encoded as a
|
||||
`CommandItem` with the typed payload union) and signs it via
|
||||
the existing `executeCommand` orchestration.
|
||||
`updatedAt`, every command encoded as a `CommandItem` with the
|
||||
typed payload union) and signs it via the existing
|
||||
`executeCommand` orchestration.
|
||||
3. The engine validates, stores, and answers `202 Accepted` with
|
||||
the stored order body — `game_id`, `updatedAt`, plus each
|
||||
command echoed with `cmdApplied` and (on rejection)
|
||||
@@ -252,8 +244,7 @@ in-flight entries back to `valid` so the operator can retry.
|
||||
`applyOrderOverlay(report, commands, statuses)` (in
|
||||
`api/game-state.ts`) returns a copy of the server `GameReport`
|
||||
with every command in `applied` or `submitting` status projected
|
||||
on top. Phase 14 understands `planetRename` only; future phases
|
||||
extend the switch.
|
||||
on top.
|
||||
|
||||
The overlay has its own context (`RENDERED_REPORT_CONTEXT_KEY`,
|
||||
`lib/rendered-report.svelte.ts`) — the in-game shell layout owns
|
||||
@@ -288,9 +279,7 @@ Cache row layout:
|
||||
| -------------- | ------------------ | ---------------- |
|
||||
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` |
|
||||
|
||||
The store writes the full draft on every mutation. Phase 25 may
|
||||
profile the submit pipeline and batch into a microtask if write
|
||||
amplification becomes a problem; until then the deterministic
|
||||
The store writes the full draft on every mutation. The deterministic
|
||||
write-on-every-mutation model is what tests assert and what the
|
||||
layout relies on for crash safety.
|
||||
|
||||
@@ -300,14 +289,12 @@ order composer uses the namespace.
|
||||
|
||||
## History mode wiring
|
||||
|
||||
Phase 26 implements history mode: the user can step back through
|
||||
past turns and see the report as it was. The IA section specifies
|
||||
that the Order tab is hidden when history mode is active — the
|
||||
player is browsing an immutable snapshot, and composing commands
|
||||
History mode lets the user step back through past turns and see the
|
||||
report as it was. The Order tab is hidden when history mode is active
|
||||
— the player is browsing an immutable snapshot, and composing commands
|
||||
against it would be confusing.
|
||||
|
||||
Phase 12 wires the flag end-to-end as a prop. The layout owns the
|
||||
flag and passes it to:
|
||||
The layout owns the `historyMode` flag and passes it to:
|
||||
|
||||
- `Sidebar` as `historyMode`. The sidebar forwards it to its
|
||||
`TabBar` as `hideOrder`. The Order entry is filtered out of the
|
||||
@@ -318,17 +305,16 @@ flag and passes it to:
|
||||
- `BottomTabs` as `hideOrder`. The mobile bottom-tab `Order`
|
||||
button is suppressed when true.
|
||||
|
||||
Phase 26 turns the constant into a derived value driven by
|
||||
`GameStateStore.historyMode` (`viewedTurn < currentTurn` while
|
||||
`status === "ready"`). The same getter is also passed into
|
||||
`OrderDraftStore.bindClient` as `getHistoryMode`, which short-
|
||||
circuits the `add` / `remove` / `move` mutations to a no-op while
|
||||
the flag is true. This makes every Phase 14–22 inspector affordance
|
||||
that calls `orderDraft.add(...)` inert in history mode without
|
||||
per-component edits — the gate lives in the one chokepoint that
|
||||
all callers go through. The conflict / paused banners and the
|
||||
in-flight sync pipeline are untouched: they describe state that
|
||||
exists independently of the user's current view.
|
||||
`historyMode` is a derived value driven by `GameStateStore.historyMode`
|
||||
(`viewedTurn < currentTurn` while `status === "ready"`). The same
|
||||
getter is also passed into `OrderDraftStore.bindClient` as
|
||||
`getHistoryMode`, which short-circuits the `add` / `remove` / `move`
|
||||
mutations to a no-op while the flag is true. This makes every
|
||||
inspector affordance that calls `orderDraft.add(...)` inert in history
|
||||
mode without per-component edits — the gate lives in the one
|
||||
chokepoint that all callers go through. The conflict / paused banners
|
||||
and the in-flight sync pipeline are untouched: they describe state
|
||||
that exists independently of the user's current view.
|
||||
|
||||
The store itself stays alive across history-mode round-trips so
|
||||
the draft survives the toggle. The `RenderedReportSource` overlay
|
||||
@@ -346,13 +332,11 @@ the chrome.
|
||||
|
||||
## Testing
|
||||
|
||||
Phase 12 + Phase 14 test artifacts:
|
||||
|
||||
- [`../frontend/tests/order-draft.test.ts`](../frontend/tests/order-draft.test.ts)
|
||||
— Vitest unit tests for the store. Drives `OrderDraftStore`
|
||||
directly with `IDBCache` over `fake-indexeddb`. Covers init,
|
||||
add, remove, move, per-game isolation, mutations-before-init,
|
||||
dispose hygiene, the Phase 14 status machine
|
||||
dispose hygiene, the status machine
|
||||
(`validate` / `markSubmitting` / `applyResults` /
|
||||
`revertSubmittingToValid`), and the
|
||||
`hydrateFromServer` cache-miss fallback.
|
||||
@@ -375,12 +359,12 @@ Phase 12 + Phase 14 test artifacts:
|
||||
— Vitest component tests for the rename action and the inline
|
||||
editor's local validation.
|
||||
- [`../frontend/tests/e2e/order-composer.spec.ts`](../frontend/tests/e2e/order-composer.spec.ts)
|
||||
— Playwright spec for the Phase 12 skeleton (seed three
|
||||
— Playwright spec for the order composer skeleton (seed three
|
||||
commands, reload, persistence).
|
||||
- [`../frontend/tests/e2e/rename-planet.spec.ts`](../frontend/tests/e2e/rename-planet.spec.ts)
|
||||
— Phase 14 end-to-end: select a planet, rename, submit, observe
|
||||
the overlay-applied name on the inspector + map, reload, and
|
||||
see the rename hydrated from `user.games.order.get`.
|
||||
— End-to-end: select a planet, rename, submit, observe the
|
||||
overlay-applied name on the inspector + map, reload, and see the
|
||||
rename hydrated from `user.games.order.get`.
|
||||
|
||||
The `__galaxyDebug.seedOrderDraft(gameId, commands)` and
|
||||
`__galaxyDebug.clearOrderDraft(gameId)` helpers in
|
||||
|
||||
+22
-23
@@ -25,8 +25,7 @@ UI sits on top of. It must:
|
||||
2. Support pan and zoom over a toroidal world (`'torus'` mode) and
|
||||
over a bounded plane (`'no-wrap'` mode), both first-class.
|
||||
3. Run the same algorithm on web, Wails, Capacitor, and PWA
|
||||
targets — only the browser is supported in Phase 9, but no API
|
||||
in this module assumes the platform.
|
||||
targets — no API in this module assumes the platform.
|
||||
4. Provide deterministic hit-test for cursor-to-primitive mapping,
|
||||
with results that are unit-testable independently of Pixi.
|
||||
|
||||
@@ -76,11 +75,11 @@ overrides them.
|
||||
|
||||
## Theme
|
||||
|
||||
A single dark theme ships in Phase 9. The theme is a record of
|
||||
default colours; primitives whose `style` omits a colour fall back
|
||||
to the theme. Runtime theme switching is not implemented — Phase
|
||||
35 introduces light/dark and the materialise-on-theme-change
|
||||
cycle.
|
||||
A single dark theme is implemented. The theme is a record of default
|
||||
colours; primitives whose `style` omits a colour fall back to the
|
||||
theme. Runtime theme switching is not implemented — light/dark and
|
||||
the materialise-on-theme-change cycle are deferred to the
|
||||
finalization plan ([../PLAN-finalize.md](../PLAN-finalize.md)).
|
||||
|
||||
## Hit-test
|
||||
|
||||
@@ -134,7 +133,7 @@ Per-primitive distance:
|
||||
representation: from `(x1, y1)` to `(x1 + dx, y1 + dy)` where
|
||||
`(dx, dy)` is the torus-shortest delta from end-1 to end-2.
|
||||
|
||||
The brute-force `O(N)` walk is fine for the Phase 9 target of
|
||||
The brute-force `O(N)` walk is fine for the current target of
|
||||
~1000 primitives on every pointer event. Spatial indexing is
|
||||
deferred until profiling proves it necessary; PixiJS' culling and
|
||||
batching handle the draw side without help.
|
||||
@@ -260,10 +259,10 @@ average frame time over a scripted drag.
|
||||
|
||||
## Pick mode
|
||||
|
||||
Phase 16 introduced a generic *map-driven destination pick* the
|
||||
inspector uses for cargo routes and that ship-group dispatch
|
||||
(Phase 19/20) will reuse. The renderer owns the visual lifecycle;
|
||||
the Svelte side wraps it in a promise-shaped service.
|
||||
The renderer provides a generic *map-driven destination pick* that
|
||||
the inspector uses for cargo routes and ship-group dispatch. The
|
||||
renderer owns the visual lifecycle; the Svelte side wraps it in a
|
||||
promise-shaped service.
|
||||
|
||||
Lifecycle (`RendererHandle.setPickMode(opts)`):
|
||||
|
||||
@@ -321,17 +320,17 @@ freshly-pushed extras layer (cargo-route overlay, pending-Send
|
||||
tracks) does not silently un-hide a primitive whose id is in the
|
||||
current set.
|
||||
|
||||
The Phase 29 map view (`src/lib/active-view/map.svelte`) computes
|
||||
the set from the per-game `MapToggles` rune + the planet-cascade
|
||||
rule and pushes it on every effect run; toggling a checkbox
|
||||
flips visibility within one frame without a Pixi remount.
|
||||
The map view (`src/lib/active-view/map.svelte`) computes the set
|
||||
from the per-game `MapToggles` rune + the planet-cascade rule and
|
||||
pushes it on every effect run; toggling a checkbox flips visibility
|
||||
within one frame without a Pixi remount.
|
||||
|
||||
## Visible-hyperspace overlay (the "fog")
|
||||
|
||||
`RendererHandle.setVisibilityFog(circles)` draws (or removes) the
|
||||
Phase 29 fog overlay used to highlight the player's visible
|
||||
hyperspace. Each entry describes a circle around a LOCAL planet
|
||||
where the player has scanner / visibility coverage:
|
||||
fog overlay that highlights the player's visible hyperspace. Each
|
||||
entry describes a circle around a LOCAL planet where the player has
|
||||
scanner / visibility coverage:
|
||||
|
||||
- An empty list destroys the existing fog rectangles and mask.
|
||||
- A non-empty list rebuilds a single viewport-level `fogLayer` (a
|
||||
@@ -381,14 +380,14 @@ pixels:
|
||||
- `getMapPrimitives()` returns a snapshot of every primitive in
|
||||
the active world: id, kind, priority, current alpha
|
||||
(post-overlay), the explicit fill / stroke colour from its
|
||||
`Style` (no theme fallback), and the Phase 29 `visible` flag
|
||||
mirroring the renderer's hide set.
|
||||
`Style` (no theme fallback), and the `visible` flag mirroring the
|
||||
renderer's hide set.
|
||||
- `getMapPickState()` returns `{ active, sourcePlanetNumber,
|
||||
reachableIds, hoveredId }` — the renderer's view of the
|
||||
current pick session.
|
||||
- `getMapCamera()` returns the current camera + viewport +
|
||||
canvas-origin snapshot, used by Phase 29 e2e specs to assert
|
||||
camera preservation across wrap-mode flips.
|
||||
canvas-origin snapshot, used by e2e specs to assert camera
|
||||
preservation across wrap-mode flips.
|
||||
- `getMapFog()` returns the most recent fog input
|
||||
(the list of circles last passed to `setVisibilityFog`).
|
||||
Empty when the `visibleHyperspace` toggle is off.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Report view — Phase 23
|
||||
# Report view
|
||||
|
||||
The Phase 23 in-game "turn report" view is a single scrollable
|
||||
layout with twenty sections, one per array on the FBS `Report`
|
||||
table. The route file is the standard two-line wrapper; the
|
||||
orchestrator and the per-section components live under
|
||||
The in-game "turn report" view is a single scrollable layout with
|
||||
twenty sections, one per array on the FBS `Report` table. The route
|
||||
file is the standard two-line wrapper; the orchestrator and the
|
||||
per-section components live under
|
||||
`ui/frontend/src/lib/active-view/report/`.
|
||||
|
||||
## Component layout
|
||||
@@ -140,7 +140,7 @@ highlight consistent without a second source of truth.
|
||||
|
||||
## i18n namespace
|
||||
|
||||
All Phase 23 strings live under `game.report.*`:
|
||||
All strings live under `game.report.*`:
|
||||
- `game.report.loading` — section loading placeholder.
|
||||
- `game.report.back_to_map`, `game.report.toc.title`,
|
||||
`game.report.toc.mobile_label` — shell-level strings.
|
||||
|
||||
@@ -6,9 +6,9 @@ planet's production is set to a science, the planet's industry
|
||||
output for that turn is split between the four tech research tracks
|
||||
in those proportions
|
||||
(`game/internal/controller/planet/production.go.runScienceResearch`).
|
||||
Phase 21 lights up the CRUD list, the designer, and the
|
||||
production-picker integration. The wire and the engine validation
|
||||
are unchanged from earlier phases — only the UI is new.
|
||||
The CRUD list, the designer, and the production-picker integration
|
||||
are provided by the UI; the wire and engine validation are handled
|
||||
by the backend.
|
||||
|
||||
## Engine semantics in one paragraph
|
||||
|
||||
@@ -41,12 +41,12 @@ from `100`, and the form's Save button stays disabled until the sum
|
||||
matches. A live readout under the inputs displays the running total
|
||||
so the player can chase it down without trial-and-error guessing.
|
||||
|
||||
The strict-sum gate is a Phase 21 decision (alternatives —
|
||||
auto-rebalance, raw-parts-with-engine-normalisation — were
|
||||
considered and rejected): keeping the input model close to "what
|
||||
gets sent on the wire" minimises surprises when the engine returns
|
||||
the science exactly as typed. See `lib/util/science-validation.ts`
|
||||
for the validator and the conversion helper.
|
||||
The strict-sum gate was chosen over alternatives — auto-rebalance
|
||||
and raw-parts-with-engine-normalisation — because keeping the input
|
||||
model close to "what gets sent on the wire" minimises surprises when
|
||||
the engine returns the science exactly as typed. See
|
||||
`lib/util/science-validation.ts` for the validator and the
|
||||
conversion helper.
|
||||
|
||||
## Name validation
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Ship-group inspector actions
|
||||
|
||||
Phase 20 turns the read-only ship-group inspector
|
||||
(`ui/frontend/src/lib/inspectors/ship-group.svelte`) into an
|
||||
The ship-group inspector
|
||||
(`ui/frontend/src/lib/inspectors/ship-group.svelte`) is an
|
||||
interactive command source for the player's own groups in orbit.
|
||||
This document is the running spec for the actions panel
|
||||
(`ui/frontend/src/lib/inspectors/ship-group/actions.svelte`):
|
||||
@@ -151,9 +151,8 @@ count. Block masses come from the player's
|
||||
- Drive / shields / cargo block mass = the corresponding ship-
|
||||
class field (raw value).
|
||||
- Weapons block mass = `core.weaponsBlockMass({ weapons,
|
||||
armament })` (Phase 18 bridge); returns null on the invalid
|
||||
weapons/armament pairing, in which case the row contributes
|
||||
zero.
|
||||
armament })`; returns null on the invalid weapons/armament
|
||||
pairing, in which case the row contributes zero.
|
||||
|
||||
For `tech === "ALL"` every block whose mass is non-zero
|
||||
contributes against the player's race tech as the target. For
|
||||
@@ -182,21 +181,18 @@ Per-action additional fields are documented on the
|
||||
`ui/frontend/src/sync/order-types.ts` next to the JSDoc for each
|
||||
variant.
|
||||
|
||||
## Decisions baked into Phase 20
|
||||
## Design notes
|
||||
|
||||
- **`BlockUpgradeCost` migrated to `pkg/calc`**. The cost
|
||||
formula previously lived in
|
||||
`game/internal/controller/ship_group_upgrade.go`. To keep the
|
||||
`ui/core/calc` bridge a wrapper around pure `pkg/calc/`
|
||||
formulas, the function moved to `pkg/calc/ship.go` and the
|
||||
controller now imports it (`controller/ship_group_upgrade.go`).
|
||||
- **`BlockUpgradeCost` lives in `pkg/calc`**. The cost formula
|
||||
lives in `pkg/calc/ship.go`; the `ui/core/calc` bridge wraps
|
||||
pure `pkg/calc/` formulas, and the controller imports it
|
||||
(`controller/ship_group_upgrade.go`).
|
||||
- **`GameReport.otherRaces`**. The transfer-to-race picker reads
|
||||
from a new `GameReport.otherRaces: string[]` field, populated
|
||||
by walking `report.player[]` and excluding the local race plus
|
||||
every `extinct` entry. Phase 22 (Races View) reuses the same
|
||||
field.
|
||||
from `GameReport.otherRaces: string[]`, populated by walking
|
||||
`report.player[]` and excluding the local race plus every
|
||||
`extinct` entry. The Races View reuses the same field.
|
||||
- **Stationed-ship rows are clickable**. The map deliberately
|
||||
hides on-planet groups; the planet inspector's stationed-ship
|
||||
rows now pivot the selection to the corresponding ship-group
|
||||
rows pivot the selection to the corresponding ship-group
|
||||
variant so the actions panel is reachable from the standard
|
||||
click flow.
|
||||
|
||||
+29
-28
@@ -15,8 +15,9 @@ namespace.
|
||||
This topic doc covers the web implementation only. The platform-
|
||||
agnostic `KeyStore` and `Cache` interfaces in
|
||||
`src/platform/store/index.ts` are what the rest of the client codes
|
||||
against; later phases bring `WailsStore` and `CapacitorStore` adapters
|
||||
that satisfy the same contracts.
|
||||
against; `WailsStore` and `CapacitorStore` adapters will satisfy the
|
||||
same contracts on their respective platforms (see
|
||||
[../ROADMAP.md](../ROADMAP.md)).
|
||||
|
||||
Source-of-truth for the cross-service contract is
|
||||
[`../../docs/ARCHITECTURE.md` §15 "Transport security model"](../../docs/ARCHITECTURE.md);
|
||||
@@ -38,13 +39,12 @@ recently:
|
||||
|
||||
Browsers older than the baseline above will fail at the first
|
||||
`generateKey({ name: 'Ed25519' }, ...)` call with a
|
||||
`NotSupportedError`. Phase 6 deliberately does not ship a JavaScript
|
||||
fallback (e.g. `@noble/ed25519`) — keeping the keystore on WebCrypto
|
||||
is what gives us non-extractable storage on every supported engine.
|
||||
The Phase 7 root layout runs a one-time probe on boot and switches
|
||||
to a "browser not supported" page (described in
|
||||
[`auth-flow.md`](auth-flow.md)) when the probe rejects, instead of
|
||||
attempting the keystore generate.
|
||||
`NotSupportedError`. No JavaScript fallback (e.g. `@noble/ed25519`)
|
||||
is shipped — keeping the keystore on WebCrypto is what gives
|
||||
non-extractable storage on every supported engine. The root layout
|
||||
runs a one-time probe on boot and switches to a "browser not
|
||||
supported" page (described in [`auth-flow.md`](auth-flow.md)) when
|
||||
the probe rejects, instead of attempting the keystore generate.
|
||||
|
||||
### WebKit non-determinism note
|
||||
|
||||
@@ -60,8 +60,9 @@ is missing.
|
||||
|
||||
Tests that assert "the same key signs the same message identically"
|
||||
must either pin to the Vitest path (Node WebCrypto) or be replaced
|
||||
with verify-after-sign assertions. The Phase 6 Playwright spec uses
|
||||
the verify path, which works on every engine in the baseline.
|
||||
with verify-after-sign assertions. The Playwright spec for the
|
||||
keystore uses the verify path, which works on every engine in the
|
||||
baseline.
|
||||
|
||||
## IndexedDB schema
|
||||
|
||||
@@ -112,14 +113,14 @@ wipes every namespace.
|
||||
|
||||
Namespaces in current use:
|
||||
|
||||
| Namespace | Key | Value type | Owner |
|
||||
|--------------------|--------------------------------|-----------------------------------------------|------------------------------------|
|
||||
| `session` | `device-session-id` | `string` | Phase 7+ |
|
||||
| `game-prefs` | `{gameId}/wrap-mode` | `WrapMode` | Phase 11+ (`game-state.md`) |
|
||||
| `game-prefs` | `{gameId}/last-viewed-turn` | `number` | Phase 11+ (`game-state.md`) |
|
||||
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` | Phase 12+ (`order-composer.md`) |
|
||||
| `game-history` | `{gameId}/turn/{N}` | `GameReport` | Phase 26+ (`game-state.md`) |
|
||||
| `game-map-toggles` | `{gameId}` | `{toggles: MapToggles; lastResetTurn: number}` | Phase 29+ (`game-state.md`) |
|
||||
| Namespace | Key | Value type | Owner |
|
||||
|--------------------|--------------------------------|------------------------------------------------|--------------------------|
|
||||
| `session` | `device-session-id` | `string` | `auth-flow.md` |
|
||||
| `game-prefs` | `{gameId}/wrap-mode` | `WrapMode` | `game-state.md` |
|
||||
| `game-prefs` | `{gameId}/last-viewed-turn` | `number` | `game-state.md` |
|
||||
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` | `order-composer.md` |
|
||||
| `game-history` | `{gameId}/turn/{N}` | `GameReport` | `game-state.md` |
|
||||
| `game-map-toggles` | `{gameId}` | `{toggles: MapToggles; lastResetTurn: number}` | `game-state.md` |
|
||||
|
||||
The `game-map-toggles` blob stores the gear popover's per-game
|
||||
visibility state plus a `lastResetTurn` companion field. Reading
|
||||
@@ -131,10 +132,10 @@ whenever `lastResetTurn < currentTurn`, so a fresh server turn
|
||||
always greets the player with every map category visible (see
|
||||
`game-state.md` for the new-turn-reset contract).
|
||||
|
||||
Later phases will add more per-feature namespaces (fixtures, lobby
|
||||
snapshot, etc.). The contract is namespace-strings stay scoped to
|
||||
one feature; cross-feature reads through the cache are by convention
|
||||
disallowed.
|
||||
Additional per-feature namespaces will be added as needed (fixtures,
|
||||
lobby snapshot, etc.). The contract is namespace-strings stay scoped
|
||||
to one feature; cross-feature reads through the cache are by
|
||||
convention disallowed.
|
||||
|
||||
## Keystore lifecycle
|
||||
|
||||
@@ -176,9 +177,8 @@ Thin orchestration layer over `KeyStore` + `Cache`:
|
||||
- `loadDeviceSession(keyStore, cache)` returns
|
||||
`{ keypair, deviceSessionId }`. The `keypair` field is always
|
||||
populated (loaded if present, freshly generated if not). The
|
||||
`deviceSessionId` field is `null` until Phase 7's
|
||||
`confirm-email-code` handler stores the gateway-issued id via
|
||||
`setDeviceSessionId`.
|
||||
`deviceSessionId` field is `null` until the `confirm-email-code`
|
||||
handler stores the gateway-issued id via `setDeviceSessionId`.
|
||||
- `setDeviceSessionId(cache, id)` writes the id to the `session`
|
||||
namespace.
|
||||
- `clearDeviceSession(keyStore, cache)` wipes both the keypair and
|
||||
@@ -186,7 +186,7 @@ Thin orchestration layer over `KeyStore` + `Cache`:
|
||||
push-event-driven revocation path.
|
||||
|
||||
A `null` `deviceSessionId` is the signal that the session is
|
||||
unauthenticated — Phase 7 routes such users to `/login`.
|
||||
unauthenticated — the root layout routes such users to `/login`.
|
||||
|
||||
## Test layout
|
||||
|
||||
@@ -211,7 +211,8 @@ the debug entry point never attaches `window.__galaxyDebug`.
|
||||
|
||||
## Future: native targets
|
||||
|
||||
Phase 31 (Wails) and Phase 32 (Capacitor) bring native keystores —
|
||||
Native desktop and mobile targets (planned in
|
||||
[../ROADMAP.md](../ROADMAP.md)) will bring native keystores —
|
||||
Keychain on macOS / iOS, DPAPI/Credential Locker on Windows,
|
||||
libsecret on Linux, Android Keystore on Android — behind the same
|
||||
`KeyStore` interface, plus SQLite-backed `Cache` adapters. The web
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# UI sync protocol
|
||||
|
||||
Phase 25 wires the transport-level policy that keeps the local
|
||||
order draft consistent with the gateway across two failure modes
|
||||
that Phase 14 punted on: transient network outages and turn
|
||||
cutoffs the player did not anticipate. The wiring also reacts to
|
||||
admin-initiated game pauses signalled by the new `game.paused`
|
||||
push event.
|
||||
The transport-level policy keeps the local order draft consistent
|
||||
with the gateway across two failure modes: transient network
|
||||
outages and turn cutoffs the player did not anticipate. The wiring
|
||||
also reacts to admin-initiated game pauses signalled by the
|
||||
`game.paused` push event.
|
||||
|
||||
The contract lives at three layers:
|
||||
|
||||
@@ -210,8 +209,7 @@ state machine deterministically.
|
||||
status flip on the lobby side without an explicit push event.
|
||||
The UI relies on the next `game.turn.ready` for recovery; a
|
||||
dedicated `game.resumed` event would let the banner clear
|
||||
immediately without waiting for the next cron tick. Not part
|
||||
of this phase.
|
||||
immediately without waiting for the next cron tick.
|
||||
- The conflict banner shows the player-facing template
|
||||
unmodified; a future revision may interpolate the explicit
|
||||
cutoff timestamp once the server adds it to the error body.
|
||||
|
||||
+9
-10
@@ -2,8 +2,7 @@
|
||||
|
||||
UI client test toolchain. Project-wide testing layers (service /
|
||||
inter-service / system) live in [`../../docs/TESTING.md`](../../docs/TESTING.md);
|
||||
this doc only covers the UI-specific tiers added in Phase 2 of
|
||||
[`../PLAN.md`](../PLAN.md).
|
||||
this doc covers the UI-specific tiers.
|
||||
|
||||
## Tier 1 — per-PR
|
||||
|
||||
@@ -45,17 +44,18 @@ as Gitea Actions artefacts (`playwright-report` and `playwright-traces`,
|
||||
|
||||
Triggered by `.gitea/workflows/ui-release.yaml` on tag push (`v*`).
|
||||
Currently mirrors the Tier 1 step set; the dedicated release-only
|
||||
checks land in later phases:
|
||||
checks are deferred:
|
||||
|
||||
- **Visual regression baseline check** — Phase 33. Snapshots live in
|
||||
- **Visual regression baseline check** — deferred to the
|
||||
finalization plan (../Plan-finalize.md). Snapshots will live in
|
||||
`ui/frontend/tests/__snapshots__/` until the project shifts to
|
||||
Argos or another visual-diff service.
|
||||
- **iOS smoke (Capacitor + Appium)** — Phase 32. Runs on a `macos-13`
|
||||
runner once the Capacitor mobile wrapper exists.
|
||||
- **iOS smoke (Capacitor + Appium)** — planned (see ../ROADMAP.md).
|
||||
Runs on a `macos-13` runner once the Capacitor mobile wrapper
|
||||
exists.
|
||||
|
||||
Both blocks are present as commented sections in
|
||||
`.gitea/workflows/ui-release.yaml` with the phase number that
|
||||
re-enables them.
|
||||
`.gitea/workflows/ui-release.yaml`.
|
||||
|
||||
## Local execution
|
||||
|
||||
@@ -150,8 +150,7 @@ In synthetic mode:
|
||||
same synthetic id afterwards redirects to /lobby. Re-load the JSON
|
||||
to reseed.
|
||||
|
||||
The synthetic-report parity rule (see [`../PLAN.md`](../PLAN.md) §
|
||||
Assumptions and Defaults) requires every UI phase that extends
|
||||
The synthetic-report parity rule requires every change that extends
|
||||
`decodeReport` to also extend the legacy parser in lockstep, or to
|
||||
record in the parser's `README.md` that the new field cannot be
|
||||
derived from legacy text. This keeps the synthetic-mode coverage in
|
||||
|
||||
@@ -13,10 +13,10 @@ keystore — see [`storage.md`](storage.md) for the web implementation
|
||||
|
||||
Two viable Go-to-WASM toolchains exist:
|
||||
|
||||
| Toolchain | Bundle size (Phase 5) | Notes |
|
||||
| Toolchain | Bundle size | Notes |
|
||||
|---------------|------------------------------------|--------------------------------------------|
|
||||
| **TinyGo** | ~903 KB (under 1 MB acceptance bar) | LLVM-based, no full GC, fast cold-start |
|
||||
| Standard Go | ~2 MB (`GOOS=js GOARCH=wasm`) | Drops in without extra tooling |
|
||||
| **TinyGo** | ~903 KB (under 1 MB target) | LLVM-based, no full GC, fast cold-start |
|
||||
| Standard Go | ~2 MB (`GOOS=js GOARCH=wasm`) | Drops in without extra tooling |
|
||||
|
||||
`ui/core` was written under the TinyGo invariants documented in
|
||||
`ui/core/README.md` (no `crypto/x509`, no `encoding/pem`, no
|
||||
@@ -108,7 +108,7 @@ TinyGo being installed in every environment.
|
||||
|
||||
| Build | Date | Size |
|
||||
|--------------|------------|-------|
|
||||
| Phase 5 land | 2026-05-07 | 903 KB |
|
||||
| Initial land | 2026-05-07 | 903 KB |
|
||||
|
||||
If the artefact ever crosses the 1 MB target, profile via
|
||||
`tinygo build -size full` and trim before committing.
|
||||
|
||||
Reference in New Issue
Block a user