docs(ui): finalize MVP plan structure + de-archaeologize topic docs #25
@@ -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
|
network I/O lives on the TypeScript side via ConnectRPC, so the Go
|
||||||
module is a pure compute boundary on every platform.
|
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
|
The existing Fyne client in `client/` is deprecated and is not modified
|
||||||
or imported by the new code. The strategy and rationale behind these
|
or imported by the new code. The architectural overview is mirrored into
|
||||||
choices live in the plan file at
|
`ui/README.md`.
|
||||||
`/Users/id/.claude/plans/buzzing-questing-fountain.md`; the architectural
|
|
||||||
overview is mirrored into `ui/README.md` as part of Phase 1.
|
|
||||||
|
|
||||||
Each phase ends with a runnable artifact. The visual progression is:
|
Each phase ends with a runnable artifact. The visual progression is:
|
||||||
empty page → navigation skeleton → stubbed views → live data → real
|
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
|
## 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
|
phase has a single primary goal, clear deliverables, explicit
|
||||||
dependencies, acceptance criteria, and focused tests. Tests live
|
dependencies, acceptance criteria, and focused tests. Tests live
|
||||||
alongside the code added in the phase; a phase is not closed until its
|
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
|
(`make wasm`, needs TinyGo) for the new bridge functions to be present
|
||||||
at runtime and in the Playwright suite; Vitest injects a fake `Core`
|
at runtime and in the Playwright suite; Vitest injects a fake `Core`
|
||||||
and does not need the rebuild.
|
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.
|
The legacy Fyne client under `client/` is reference-only.
|
||||||
Nothing in `ui/` imports from it.
|
Nothing in `ui/` imports from it.
|
||||||
|
|
||||||
The full staged implementation plan lives in `PLAN.md`. The
|
The strategic rationale (why Svelte, why PixiJS, why Go-as-WASM, why
|
||||||
strategic rationale (why Svelte, why PixiJS, why Go-as-WASM, why
|
|
||||||
Wails+Capacitor) lives outside the repo at
|
Wails+Capacitor) lives outside the repo at
|
||||||
`~/.claude/plans/buzzing-questing-fountain.md`. This README is a
|
`~/.claude/plans/buzzing-questing-fountain.md`. This README is a
|
||||||
quick orientation; deeper per-phase design notes earn their place
|
quick orientation; deeper design notes live under `ui/docs/`.
|
||||||
under `ui/docs/` as they are introduced.
|
|
||||||
|
|
||||||
## Targets
|
## Targets
|
||||||
|
|
||||||
| Target | Wrapper | Toolchain | Phase |
|
| Target | Wrapper | Toolchain | Status |
|
||||||
| --------------- | ---------------- | ----------------------- | -------- |
|
| --------------- | ---------------- | ----------------------- | ------------------------------ |
|
||||||
| web | browser tab | Vite + WASM | 5+ |
|
| web | browser tab | Vite + WASM | implemented |
|
||||||
| web-mobile | mobile browser | Vite + WASM | 5+ |
|
| web-mobile | mobile browser | Vite + WASM | implemented |
|
||||||
| desktop (mac) | Wails v2 | Go + Wails CLI | 31 |
|
| desktop (mac) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
|
||||||
| desktop (win) | Wails v2 | Go + Wails CLI | 31 |
|
| desktop (win) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
|
||||||
| desktop (linux) | Wails v2 | Go + Wails CLI | 31 |
|
| desktop (linux) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
|
||||||
| iOS | Capacitor | gomobile + Xcode | 32+ |
|
| iOS | Capacitor | gomobile + Xcode | planned (see ROADMAP.md) |
|
||||||
| Android | Capacitor | gomobile + Gradle | 32+ |
|
| Android | Capacitor | gomobile + Gradle | planned (see ROADMAP.md) |
|
||||||
|
|
||||||
## Layered architecture
|
## Layered architecture
|
||||||
|
|
||||||
@@ -58,7 +56,9 @@ under `ui/docs/` as they are introduced.
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
ui/
|
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
|
├── Makefile wasm / ts-protos / web / mobile / desktop targets
|
||||||
├── README.md this file
|
├── README.md this file
|
||||||
├── buf.gen.yaml local-plugin TS Protobuf-ES generator
|
├── buf.gen.yaml local-plugin TS Protobuf-ES generator
|
||||||
@@ -71,6 +71,9 @@ ui/
|
|||||||
│ └── wasm-toolchain.md TinyGo build, JSDOM loading, bundle budget
|
│ └── wasm-toolchain.md TinyGo build, JSDOM loading, bundle budget
|
||||||
├── core/ ui/core Go module (canonical bytes, keypair)
|
├── core/ ui/core Go module (canonical bytes, keypair)
|
||||||
├── wasm/ TinyGo entry point exposing Core to JS
|
├── 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
|
└── frontend/ SvelteKit / Vite source
|
||||||
├── src/api/ GalaxyClient + typed Connect client + auth + session
|
├── src/api/ GalaxyClient + typed Connect client + auth + session
|
||||||
├── src/lib/ env config, session store, revocation watcher
|
├── 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
|
- [`docs/testing.md`](docs/testing.md) — Tier 1 per-PR + Tier 2
|
||||||
release test tiers.
|
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
|
## Build pipeline
|
||||||
|
|
||||||
Every cross-target build flows through `make` at this level. All
|
Every cross-target build flows through `make` at this level.
|
||||||
named targets are placeholders until the named phase lands; running
|
Native targets are placeholders until their platform work lands;
|
||||||
`make` with no arguments prints the current placeholder map.
|
running `make` with no arguments prints the current placeholder map.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
make web Vite production build Phase 5+
|
make web Vite production build
|
||||||
make wasm TinyGo → core.wasm Phase 5
|
make wasm TinyGo → core.wasm
|
||||||
make ts-protos Connect-ES + Protobuf-ES gen Phase 5
|
make ts-protos Connect-ES + Protobuf-ES gen
|
||||||
make fbs-ts FlatBuffers TS bindings via flatc Phase 8
|
make fbs-ts FlatBuffers TS bindings via flatc
|
||||||
make gomobile gomobile bind → ios + android Phase 32+
|
make gomobile gomobile bind → ios + android (planned — see ROADMAP.md)
|
||||||
make desktop-mac Wails build for darwin Phase 31
|
make desktop-mac Wails build for darwin (planned — see ROADMAP.md)
|
||||||
make desktop-win Wails build for windows Phase 31
|
make desktop-win Wails build for windows (planned — see ROADMAP.md)
|
||||||
make desktop-linux Wails build for linux Phase 31
|
make desktop-linux Wails build for linux (planned — see ROADMAP.md)
|
||||||
make ios Capacitor + xcodebuild Phase 32+
|
make ios Capacitor + xcodebuild (planned — see ROADMAP.md)
|
||||||
make android Capacitor + gradle Phase 32+
|
make android Capacitor + gradle (planned — see ROADMAP.md)
|
||||||
make all every target above
|
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
|
[`../tools/dev-deploy/`](../tools/dev-deploy/README.md). It is
|
||||||
redeployed by Gitea Actions on every merge into `development`.
|
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
|
Topic docs live under `ui/docs/` (testing tiers, WASM toolchain,
|
||||||
needed (testing tiers, WASM toolchain, navigation shell, renderer
|
navigation shell, renderer internals, sync protocol, auth flow, and
|
||||||
internals, sync protocol, auth flow, and so on). The staged plan in
|
so on).
|
||||||
`PLAN.md` names the topic doc each phase produces.
|
|
||||||
|
|
||||||
## Cross-references
|
## Cross-references
|
||||||
|
|
||||||
- [`PLAN.md`](./PLAN.md) — staged implementation plan with goals,
|
- [`PLAN.md`](./PLAN.md) — staged implementation plan (historical
|
||||||
artifacts, dependencies, acceptance criteria, and targeted tests
|
record of completed work).
|
||||||
per phase.
|
- [`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
|
- [`../docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md) — platform
|
||||||
architecture and the transport security model (§15) the client
|
architecture and the transport security model (§15) the client
|
||||||
envelope contract derives from.
|
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.
|
stories that drive the UI flows.
|
||||||
- [`../docs/TESTING.md`](../docs/TESTING.md) — project-wide testing
|
- [`../docs/TESTING.md`](../docs/TESTING.md) — project-wide testing
|
||||||
layers; UI-specific test tiers (Vitest, Playwright) live in
|
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
|
Galaxy cross-platform UI client. It carries v1 transport-envelope
|
||||||
canonical bytes, signature verification, and Ed25519 keypair
|
canonical bytes, signature verification, and Ed25519 keypair
|
||||||
helpers. Network I/O and persistent storage live elsewhere on
|
helpers. Network I/O and persistent storage live elsewhere on
|
||||||
purpose: this module compiles unchanged to WASM (Phase 5),
|
purpose: this module compiles unchanged to WASM (the web target today)
|
||||||
gomobile (Phase 32), and Wails-embedded native (Phase 31).
|
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
|
The authoritative byte contract is defined in
|
||||||
[`docs/ARCHITECTURE.md` §15](../../docs/ARCHITECTURE.md). The gateway
|
[`docs/ARCHITECTURE.md` §15](../../docs/ARCHITECTURE.md). The gateway
|
||||||
@@ -39,8 +40,11 @@ parity and round-trip sign/verify are exercised by
|
|||||||
```text
|
```text
|
||||||
ui/core/
|
ui/core/
|
||||||
├── go.mod module galaxy/core (Go 1.26.0)
|
├── go.mod module galaxy/core (Go 1.26.0)
|
||||||
├── calc/ ship-math wrappers over `pkg/calc/ship.go`
|
├── calc/ thin wrappers over `pkg/calc/` (no math here)
|
||||||
│ └── ship.go Phase 18 designer preview bridge
|
│ ├── 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/ canonical-bytes builders and verifiers
|
||||||
│ ├── canon.go length-prefix helpers
|
│ ├── canon.go length-prefix helpers
|
||||||
│ ├── request.go galaxy-request-v1 fields and signing input
|
│ ├── request.go galaxy-request-v1 fields and signing input
|
||||||
@@ -92,20 +96,16 @@ ui/core/
|
|||||||
|
|
||||||
### `galaxy/core/calc`
|
### `galaxy/core/calc`
|
||||||
|
|
||||||
Thin Go bridge over `pkg/calc/ship.go`, surfaced via WASM to the
|
Thin Go bridges over `pkg/calc/`, surfaced via WASM to the ship-class
|
||||||
Phase 18 ship-class designer preview. Each function is a one-line
|
calculator and the ship-group inspector. Each function is a one-line
|
||||||
passthrough — no math lives here.
|
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`
|
The authoritative function surface (which UI feature uses what, parity
|
||||||
- `EmptyMass(drive, weapons float64, armament uint, shields, cargo float64) (float64, bool)`
|
rules, what is still deferred) lives in
|
||||||
- `WeaponsBlockMass(weapons float64, armament uint) (float64, bool)`
|
[`ui/docs/calc-bridge.md`](../docs/calc-bridge.md).
|
||||||
- `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).
|
|
||||||
|
|
||||||
### `galaxy/core/types`
|
### `galaxy/core/types`
|
||||||
|
|
||||||
@@ -146,14 +146,15 @@ versa) is enforced from
|
|||||||
## What this module is **not**
|
## What this module is **not**
|
||||||
|
|
||||||
- Not a network client. ConnectRPC over `@connectrpc/connect-web`
|
- Not a network client. ConnectRPC over `@connectrpc/connect-web`
|
||||||
on the TypeScript side is the only network surface (Phase 5+).
|
on the TypeScript side is the only network surface.
|
||||||
- Not a key store. Per-platform secure storage lives in Phase 6.
|
- 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
|
- Not a freshness gate. Server-side `±5 min` freshness checks
|
||||||
remain in `gateway/internal/grpcapi/freshness_replay.go`. The
|
remain in `gateway/internal/grpcapi/freshness_replay.go`. The
|
||||||
client is expected to stamp its own `timestamp_ms` accurately
|
client is expected to stamp its own `timestamp_ms` accurately
|
||||||
via `time.Now`, but does not enforce a window.
|
via `time.Now`, but does not enforce a window.
|
||||||
- Not a FlatBuffers codec — that lands in a later phase, so the
|
- Not a FlatBuffers codec — report decoding lives on the TypeScript
|
||||||
module today is small on purpose.
|
side, so this module stays small on purpose.
|
||||||
|
|
||||||
## Cross-references
|
## Cross-references
|
||||||
|
|
||||||
@@ -161,5 +162,5 @@ versa) is enforced from
|
|||||||
authoritative byte contract.
|
authoritative byte contract.
|
||||||
- [`../../gateway/authn`](../../gateway/authn) — server mirror of
|
- [`../../gateway/authn`](../../gateway/authn) — server mirror of
|
||||||
the same canonical bytes.
|
the same canonical bytes.
|
||||||
- [`../PLAN.md`](../PLAN.md) Phase 3 — the staged plan that
|
- [`../PLAN.md`](../PLAN.md) — the staged plan that describes how this
|
||||||
describes how this module fits into the wider client.
|
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;
|
store `keypair`, key `device`). Clearing site data wipes both;
|
||||||
the next load generates a fresh keypair and the user must log in
|
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
|
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
|
## 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
|
[`storage.md`](storage.md) for the rationale). On boot the layout
|
||||||
runs a sanity probe (`crypto.subtle.generateKey` for `Ed25519`); if
|
runs a sanity probe (`crypto.subtle.generateKey` for `Ed25519`); if
|
||||||
it rejects, the layout switches to a `browser not supported` page
|
it rejects, the layout switches to a `browser not supported` page
|
||||||
instead of rendering `/login`. Phase 7 deliberately does not ship a
|
instead of rendering `/login`. The client deliberately does not ship a
|
||||||
JavaScript Ed25519 fallback — see Phase 6's "modern-browser baseline,
|
JavaScript Ed25519 fallback — the design decision is modern-browser
|
||||||
no JS Ed25519 fallback" decision.
|
baseline only.
|
||||||
|
|
||||||
## Revocation
|
## Revocation
|
||||||
|
|
||||||
The lobby layout opens a long-running `SubscribeEvents` stream as
|
The lobby layout opens a long-running `SubscribeEvents` stream as
|
||||||
soon as `status` becomes `authenticated`. The watcher does not
|
soon as `status` becomes `authenticated`. Its only contract is
|
||||||
process individual events in Phase 7 — that arrives in Phase 24.
|
liveness: any non-aborted termination of the stream is treated as
|
||||||
Its only contract is liveness: any non-aborted termination of the
|
a server-side session revocation, the watcher calls
|
||||||
stream is treated as a server-side session revocation, the watcher
|
`session.signOut("revoked")`, and the layout effect redirects to
|
||||||
calls `session.signOut("revoked")`, and the layout effect redirects
|
`/login`.
|
||||||
to `/login`.
|
|
||||||
|
|
||||||
This satisfies the Phase 7 acceptance bar of "session revocation
|
Session revocation closes the active client within one second: the
|
||||||
closes the active client within one second": the gateway closes
|
gateway closes the stream the moment it observes a
|
||||||
the stream the moment it observes a `session_invalidation` push
|
`session_invalidation` push event from backend, and the watcher
|
||||||
event from backend, and the watcher reacts on the next event-loop
|
reacts on the next event-loop tick.
|
||||||
tick.
|
|
||||||
|
|
||||||
## Localisation
|
## Localisation
|
||||||
|
|
||||||
@@ -140,8 +138,9 @@ drops JS-set `Accept-Language` headers. See
|
|||||||
adding a new language.
|
adding a new language.
|
||||||
|
|
||||||
The locale is **not** persisted between page reloads; detection
|
The locale is **not** persisted between page reloads; detection
|
||||||
runs again on every visit. Phase 35's full polish pass will
|
runs again on every visit. Persistence and message-format
|
||||||
revisit persistence and add message-format pluralisation.
|
pluralisation are deferred to the finalization plan
|
||||||
|
(../Plan-finalize.md).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Battle Viewer UX
|
# Battle Viewer UX
|
||||||
|
|
||||||
Phase 27 ships a dedicated viewer for battles (`/games/<id>/battle/<battleId>`).
|
The battle viewer is a dedicated view for battles
|
||||||
Bombings stay where they were in Phase 23 — a static table in the
|
(`/games/<id>/battle/<battleId>`). Bombings are a separate static
|
||||||
Reports view (`section-bombings.svelte`). The two domains are
|
table in the Reports view (`section-bombings.svelte`). The two
|
||||||
deliberately not mixed in any visual surface or click target.
|
domains are deliberately not mixed in any visual surface or click
|
||||||
|
target.
|
||||||
|
|
||||||
## Data shape
|
## 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
|
one line per action, formatted from `BattleReportGroup.race` and
|
||||||
`BattleReportGroup.className`. The line for the current frame is
|
`BattleReportGroup.className`. The line for the current frame is
|
||||||
highlighted so a non-visual reader can follow along by scrolling
|
highlighted so a non-visual reader can follow along by scrolling
|
||||||
the log instead of watching the SVG. The list is always present
|
the log instead of watching the SVG. The list is always present and never hidden; the same data is
|
||||||
and never hidden, satisfying the original Phase 27 acceptance "the
|
accessible as a static text log.
|
||||||
same data is accessible as a static text log".
|
|
||||||
|
|
||||||
Each log row is also a `<button>`: a click or Enter/Space jumps
|
Each log row is also a `<button>`: a click or Enter/Space jumps
|
||||||
playback to that shot (pauses and seeks). The list auto-scrolls
|
playback to that shot (pauses and seeks). The list auto-scrolls
|
||||||
|
|||||||
+53
-67
@@ -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
|
Go → WASM → TypeScript bridge mounted under `ui/core/calc/` and a
|
||||||
matching TS adapter in `ui/frontend/src/platform/core/`.
|
matching TS adapter in `ui/frontend/src/platform/core/`.
|
||||||
|
|
||||||
Phase 18 lands the **ship-math slice** of the bridge — everything
|
The bridge covers the **ship-math slice** (everything the ship-class
|
||||||
the ship-class designer needs to render its preview pane. Phase 20
|
designer needs to render its preview pane), `BlockUpgradeCost` (for
|
||||||
extends it with `BlockUpgradeCost` so the ship-group inspector can
|
the ship-group inspector's modernize-cost preview), and the **combat,
|
||||||
preview modernize cost. Phase 30 extends it with the **combat,
|
|
||||||
planet-build, and goal-seek slice** for the ship-class calculator:
|
planet-build, and goal-seek slice** for the ship-class calculator:
|
||||||
`EffectiveAttack`, `EffectiveDefence`, `BombingPower`, `ShipBuildCost`,
|
`EffectiveAttack`, `EffectiveDefence`, `BombingPower`, `ShipBuildCost`,
|
||||||
`ProduceShipsInTurn`, and the inverse solvers from `pkg/calc/solve.go`.
|
`ProduceShipsInTurn`, and the inverse solvers from `pkg/calc/solve.go`.
|
||||||
Other slices (production/science forecast, the realistic multi-turn
|
Other slices (production/science forecast, the realistic multi-turn
|
||||||
planet projection) remain deferred to dedicated future phases. This
|
planet projection) remain deferred. This document is the running audit
|
||||||
document is the running audit trail of what is live, what is missing,
|
trail of what is live, what is missing, and how each function maps to
|
||||||
and how each function maps to its `pkg/calc/` source.
|
its `pkg/calc/` source.
|
||||||
|
|
||||||
## Live bridge surface
|
## 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) |
|
| `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:
|
`BombingPower` and the per-turn build loop are no longer engine-only:
|
||||||
Phase 30 extracted `BombingPower` from
|
`BombingPower` was extracted from `game/internal/model/game/group.go`
|
||||||
`game/internal/model/game/group.go` and the per-iteration build math
|
and the per-iteration build math from `controller.ProduceShip` into
|
||||||
from `controller.ProduceShip` into `pkg/calc` (`ProduceShipsInTurn`),
|
`pkg/calc` (`ProduceShipsInTurn`); the engine now delegates to both —
|
||||||
and the engine now delegates to both — a true refactor, not a mirror.
|
a true refactor, not a mirror.
|
||||||
The inverse solvers (`pkg/calc/solve.go`) invert the forward formulas
|
The inverse solvers (`pkg/calc/solve.go`) invert the forward formulas
|
||||||
for single-target goal-seek and return `null` when infeasible;
|
for single-target goal-seek and return `null` when infeasible;
|
||||||
`shieldsForDefence` uses bisection, the rest are analytic. Parity and
|
`shieldsForDefence` uses bisection, the rest are analytic. Parity and
|
||||||
@@ -77,12 +76,12 @@ same inputs and asserts byte-equal outputs.
|
|||||||
|
|
||||||
## Still-deferred slices
|
## Still-deferred slices
|
||||||
|
|
||||||
Phase 18's Go-side bridge is intentionally narrow: it covers ship
|
The Go-side bridge is intentionally narrow: it covers ship math and
|
||||||
math and nothing else. Production forecasts, science, ship-build
|
the combat/planet-build/goal-seek slice. Production forecasts, science,
|
||||||
progress, and reach (`FligthDistance`) still depend on either
|
and reach (`FligthDistance`) still depend on either inline TS arithmetic
|
||||||
inline TS arithmetic or the engine-shipped fields on `GameReport`.
|
or the engine-shipped fields on `GameReport`. See the table further down
|
||||||
See the table further down for what is missing and the per-feature
|
for what is missing and the per-feature waivers below for the rationale
|
||||||
waivers below for the rationale on each deferral.
|
on each deferral.
|
||||||
|
|
||||||
## Current `pkg/calc/` exports
|
## 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). |
|
| `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`.|
|
| `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`). |
|
| `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`). |
|
| `FligthDistance(driveTech)`, `VisibilityDistance(...)` | Race-level reach formulas (`pkg/calc/race.go`). |
|
||||||
| `ValidateShipTypeValues`, `CheckShipTypeValueDWSC` | Ship-design validators (`pkg/calc/validator.go`). |
|
| `ValidateShipTypeValues`, `CheckShipTypeValueDWSC` | Ship-design validators (`pkg/calc/validator.go`). |
|
||||||
|
|
||||||
@@ -106,82 +105,69 @@ The table below tracks what UI features need from the bridge and
|
|||||||
whether the underlying Go function exists.
|
whether the underlying Go function exists.
|
||||||
|
|
||||||
| UI feature | Go formula | In `pkg/calc/`? | Surfaced to TS? |
|
| 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-class designer preview | `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-group modernize cost preview | `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 combat | `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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
`partial` means the Go primitives exist in `pkg/calc/` but the
|
||||||
composition (and the conversion of TS-side `ReportPlanet`/
|
composition (and the conversion of TS-side `ReportPlanet`/
|
||||||
`ShipClass` to the formula inputs) is not implemented anywhere.
|
`ShipClass` to the formula inputs) is not implemented anywhere.
|
||||||
|
|
||||||
## Phase 15 waiver
|
## Production forecast waiver
|
||||||
|
|
||||||
Phase 15 ships the inspector's planet production controls
|
The inspector's planet production controls (segmented control +
|
||||||
(segmented control + sub-pickers + collapse-by-`planetNumber`
|
sub-pickers + collapse-by-`planetNumber` order command) do **not**
|
||||||
order command) but **deliberately does not surface the per-type
|
surface the per-type forecast number. The inspector renders the
|
||||||
forecast number**. The planning gate explicitly raised the gap as
|
existing `freeIndustry` row (free production potential) — that number
|
||||||
a blocker per the plan's audit clause ("if any are missing in
|
is computed engine-side and ships in the report payload, so no
|
||||||
`pkg/calc/`, raise as blocker") and the project owner approved
|
calc-bridge access is required for it. The per-type forecast number
|
||||||
deferring the forecast to a dedicated future bridge phase. The
|
is deferred pending promotion of the relevant formulas into
|
||||||
inspector still renders the existing `freeIndustry` row (free
|
`pkg/calc/`.
|
||||||
production potential) — that number is computed engine-side and
|
|
||||||
ships in the report payload, so no calc-bridge access is required
|
|
||||||
for it today.
|
|
||||||
|
|
||||||
Acceptance criterion 3 of Phase 15 ("forecast output number
|
## Reach formula waiver
|
||||||
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.
|
|
||||||
|
|
||||||
## Phase 16 waiver
|
Ship-reach filtering for the cargo-route destination picker uses
|
||||||
|
a trivial engine formula:
|
||||||
Phase 16 introduces ship-reach filtering for the cargo-route
|
|
||||||
destination picker. The engine formula is trivial:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
flightDistance = driveTech * 40
|
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
|
[`pkg/calc/race.go`](../../pkg/calc/race.go) as
|
||||||
`FligthDistance(driveTech) float64` (alongside the matching
|
`FligthDistance(driveTech) float64` (alongside the matching
|
||||||
`VisibilityDistance` for in-space group reports — used in later
|
`VisibilityDistance` for in-space group reports). The engine call
|
||||||
phases). The engine call sites
|
sites (`game/internal/model/game/race.go.FlightDistance`,
|
||||||
(`game/internal/model/game/race.go.FlightDistance`,
|
|
||||||
`game/internal/controller/route.go.PlanetRouteSet`) still wrap the
|
`game/internal/controller/route.go.PlanetRouteSet`) still wrap the
|
||||||
Go formula directly; promoting them to call `pkg/calc/` is a
|
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
|
Implementing the WASM glue for one constant-time multiplication
|
||||||
`pkg/calc/` and `ui/core/calc/`; with the calc-bridge phase still
|
would be premature scaffolding, so the picker computes reach inline
|
||||||
deferred, implementing the WASM glue for one constant-time
|
in TypeScript using `torusShortestDelta(planet.x, candidate.x,
|
||||||
multiplication would be premature scaffolding. The picker
|
mapWidth)` and `Math.hypot` against `40 * report.localPlayerDrive`,
|
||||||
therefore computes reach inline in TypeScript using
|
where `localPlayerDrive` is decoded from the report's `Player` block
|
||||||
`torusShortestDelta(planet.x, candidate.x, mapWidth)` and
|
by matching `Player.name` to `report.race`
|
||||||
`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`).
|
(`api/game-state.ts.findLocalPlayerDrive`).
|
||||||
|
|
||||||
When the calc-bridge phase ships, the inline formula is replaced
|
When the remaining bridge work ships, the inline formula will be
|
||||||
with a single call into the bridge — `calc.FligthDistance(driveTech)`
|
replaced with a single call into the bridge —
|
||||||
becomes the source of truth for both the picker and the
|
`calc.FligthDistance(driveTech)` becomes the source of truth for
|
||||||
cargo-route auto-removal at turn cutoff. Until then, the UI
|
both the picker and the cargo-route auto-removal at turn cutoff.
|
||||||
duplicates `flightDistance` knowingly — same precedent as the
|
Until then, the UI duplicates `flightDistance` knowingly — same
|
||||||
production forecast deferral above.
|
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
|
registration + typed `Core` interface + parity tests). Future calc
|
||||||
work follows the same shape:
|
work follows the same shape:
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Ship Class Calculator — UX
|
# Ship Class Calculator — UX
|
||||||
|
|
||||||
Phase 30 fuses the ship-class designer and a calculator into one sidebar
|
The ship-class designer and calculator are fused into one sidebar
|
||||||
tool (`lib/sidebar/calculator-tab.svelte`). It replaced the standalone
|
tool (`lib/sidebar/calculator-tab.svelte`). The standalone designer
|
||||||
designer view/route from Phases 17/18. All numeric math lives in
|
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
|
`pkg/calc` and is reached through the `Core` WASM bridge; the calculator
|
||||||
holds input state and orchestrates, it never computes.
|
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
|
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,
|
its MAT (overridable) and the single-turn build rate (ships per turn,
|
||||||
turns per ship). The realistic multi-turn forecast with CAP/COL
|
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
|
## Locks and goal-seek
|
||||||
|
|
||||||
|
|||||||
+15
-16
@@ -1,13 +1,12 @@
|
|||||||
# Cargo routes UX
|
# Cargo routes UX
|
||||||
|
|
||||||
This document covers the cargo-route surface added in Phase 16: the
|
This document covers the cargo-route surface: the four-slot
|
||||||
four-slot inspector subsection, the map-driven destination pick, and
|
inspector subsection, the map-driven destination pick, and the
|
||||||
the optimistic overlay that keeps the inspector and the map in lock-
|
optimistic overlay that keeps the inspector and the map in lock-step
|
||||||
step with the local order draft. The user-visible spec lives in
|
with the local order draft. The engine semantics are quoted from
|
||||||
[`../PLAN.md`](../PLAN.md) Phase 16; the engine semantics are quoted
|
[`game/rules.txt`](../../game/rules.txt) section "Грузовые маршруты"
|
||||||
from [`game/rules.txt`](../../game/rules.txt) section "Грузовые
|
(lines 808–843); this file is the source of truth for how the UI
|
||||||
маршруты" (lines 808–843); this file is the source of truth for how
|
surfaces those rules.
|
||||||
the UI surfaces those rules.
|
|
||||||
|
|
||||||
## Engine semantics in one paragraph
|
## 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
|
rule on the order draft store ensures only one entry per
|
||||||
`(source, loadType)` slot survives in the draft at any time, so a
|
`(source, loadType)` slot survives in the draft at any time, so a
|
||||||
sequence of `Add → Edit → Remove` collapses to the latest verb only
|
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
|
Disabled state: every button is disabled when the
|
||||||
`OrderDraftStore` or `MapPickService` context is missing (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.
|
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
|
Arrows are drawn as a shaft plus two short arrowhead wings. Per-type
|
||||||
styling (placeholder Phase 16 colours; final values land in Phase
|
styling (visual refinements are deferred to the finalization plan
|
||||||
35 polish):
|
(../PLAN-finalize.md)):
|
||||||
|
|
||||||
| Load type | Stroke colour | Notes |
|
| 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
|
`reachableSet()` in `cargo-routes.svelte` filters out only the
|
||||||
source planet itself.
|
source planet itself.
|
||||||
|
|
||||||
Why inline rather than via a Go calc bridge? See the Phase 15 / 16
|
Why inline rather than via a Go calc bridge? See the deferral note
|
||||||
deferral note in [`calc-bridge.md`](./calc-bridge.md). The formula
|
in [`calc-bridge.md`](./calc-bridge.md). The formula is trivial
|
||||||
is trivial (`tech × 40`) and the WASM glue would be premature
|
(`tech × 40`) and the WASM glue would be premature infrastructure;
|
||||||
infrastructure; when the calc bridge phase lands the shared
|
when the calc bridge lands the shared `pkg/calc.FligthDistance` will
|
||||||
`pkg/calc.FligthDistance` will replace this implementation.
|
replace this implementation.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# In-game diplomatic mail UI
|
# In-game diplomatic mail UI
|
||||||
|
|
||||||
Phase 28 wires the in-game mail view that consumes the `diplomail`
|
The in-game mail view consumes the `diplomail` subsystem in the
|
||||||
subsystem in the backend. The route lives at `/games/:id/mail`
|
backend. The route lives at `/games/:id/mail` and replaces the
|
||||||
(registered in Phase 10) and replaces the active view when the user
|
active view when the user opens the "diplomatic mail" entry in the
|
||||||
opens the "diplomatic mail" entry in the header menu.
|
header menu.
|
||||||
|
|
||||||
## Wire surface
|
## Wire surface
|
||||||
|
|
||||||
@@ -29,8 +29,8 @@ the gateway translation lives in
|
|||||||
## Recipient by race name
|
## Recipient by race name
|
||||||
|
|
||||||
The compose flow does **not** consult a memberships listing. The
|
The compose flow does **not** consult a memberships listing. The
|
||||||
recipient picker reads `gameState.report.races[].name` (the Phase 22
|
recipient picker reads `gameState.report.races[].name` (projected
|
||||||
projection of `report.player[]`), and the send request carries the
|
from `report.player[]`), and the send request carries the
|
||||||
chosen race name as `recipient_race_name`. The backend resolves it
|
chosen race name as `recipient_race_name`. The backend resolves it
|
||||||
against `Memberships.ListMembers(gameID, "active")` and rejects with
|
against `Memberships.ListMembers(gameID, "active")` and rejects with
|
||||||
`forbidden` if the matching member is no longer active. This keeps
|
`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
|
`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
|
— they only drive the badge counter and the optimistic mark-read
|
||||||
state. This is intentional (per Phase 28 decisions): the user-facing
|
state. This is intentional: the user-facing spec for diplomatic mail
|
||||||
spec for diplomatic mail does not promise read receipts.
|
does not promise read receipts.
|
||||||
|
|
||||||
## Translation toggle
|
## Translation toggle
|
||||||
|
|
||||||
|
|||||||
+6
-8
@@ -9,9 +9,8 @@ stops it on sign-out.
|
|||||||
|
|
||||||
## Why a single consumer
|
## Why a single consumer
|
||||||
|
|
||||||
Before Phase 24, the watcher in `lib/revocation-watcher.ts` opened a
|
The `EventStream` singleton consolidates what was previously a
|
||||||
parallel stream just to observe session revocation. Phase 24 folds
|
separate revocation watcher in `lib/revocation-watcher.ts` so that:
|
||||||
that watcher into `EventStream` so that:
|
|
||||||
|
|
||||||
- there is only **one** SubscribeEvents connection per session
|
- there is only **one** SubscribeEvents connection per session
|
||||||
(avoids doubling the gateway hub load);
|
(avoids doubling the gateway hub load);
|
||||||
@@ -19,7 +18,7 @@ that watcher into `EventStream` so that:
|
|||||||
`Unauthenticated` ConnectError funnel through one
|
`Unauthenticated` ConnectError funnel through one
|
||||||
`session.signOut("revoked")` call site;
|
`session.signOut("revoked")` call site;
|
||||||
- per-event-type dispatch (turn-ready toasts, lobby/mail/battle
|
- per-event-type dispatch (turn-ready toasts, lobby/mail/battle
|
||||||
notifications later) shares the same verification path.
|
notifications) shares the same verification path.
|
||||||
|
|
||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
||||||
@@ -67,10 +66,9 @@ exponential backoff (base 1 s, ceiling 30 s, unbounded retries).
|
|||||||
- `reconnecting` — transient failure, backoff in flight.
|
- `reconnecting` — transient failure, backoff in flight.
|
||||||
- `offline` — `navigator.onLine === false` at the moment of failure.
|
- `offline` — `navigator.onLine === false` at the moment of failure.
|
||||||
|
|
||||||
The header connection-state indicator planned in `PLAN.md`
|
The header connection-state indicator reads this rune; the rune is
|
||||||
cross-cutting shell reads this rune; it is not part of Phase 24 but
|
wired so a future change can add the indicator dot without touching
|
||||||
the rune is wired now so a later phase can add the dot without
|
this module.
|
||||||
touching this module.
|
|
||||||
|
|
||||||
## Revocation semantics
|
## Revocation semantics
|
||||||
|
|
||||||
|
|||||||
+49
-57
@@ -1,10 +1,8 @@
|
|||||||
# Per-game state store
|
# Per-game state store
|
||||||
|
|
||||||
This document describes the per-game state owned by the in-game shell
|
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
|
layout. The store serves the header turn counter, the map view,
|
||||||
(the header turn counter and the map view); later phases plug
|
inspector tabs, the order composer, and the calculator.
|
||||||
inspector tabs, the order composer, and the calculator on top of the
|
|
||||||
same instance.
|
|
||||||
|
|
||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
||||||
@@ -23,10 +21,9 @@ gameId })`. `init`:
|
|||||||
2. calls `setGame(gameId)`, which:
|
2. calls `setGame(gameId)`, which:
|
||||||
- reads the per-game wrap-mode preference from `Cache`
|
- reads the per-game wrap-mode preference from `Cache`
|
||||||
(`game-prefs / <gameId>/wrap-mode`, default `torus`);
|
(`game-prefs / <gameId>/wrap-mode`, default `torus`);
|
||||||
- calls `lobby.my.games.list` and finds the game record (the
|
- calls `lobby.my.games.list` and finds the game record
|
||||||
Phase 11 wire schema extension on `GameSummary` adds
|
(`GameSummary` carries `current_turn`); if the user is not a
|
||||||
`current_turn`); if the user is not a member, the store flips
|
member, the store flips to `error`;
|
||||||
to `error`;
|
|
||||||
- calls `user.games.report` for the discovered turn and decodes
|
- calls `user.games.report` for the discovered turn and decodes
|
||||||
the FlatBuffers response into a TS-friendly `GameReport` shape.
|
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` |
|
| `wrapMode` | `torus / no-wrap` | per-game preference, persisted via `Cache` |
|
||||||
| `error` | `string \| null` | localised error message when `status === "error"` |
|
| `error` | `string \| null` | localised error message when `status === "error"` |
|
||||||
|
|
||||||
## Phase boundaries
|
## Store extensions
|
||||||
|
|
||||||
- Phase 11 surfaces only the planet subset of the report. Later
|
`GameReport` and `decodeReport` are extended as each slice of the
|
||||||
phases extend `GameReport` and `decodeReport` as their slice of
|
wire lands (ships, fleets, sciences, routes, battles, mail).
|
||||||
the wire lands (ships, fleets, sciences, routes, battles, mail).
|
`currentTurn` is split from `viewedTurn`, and `viewTurn(turn)` /
|
||||||
- Phase 26 splits `currentTurn` from the turn whose snapshot is
|
`returnToCurrent()` handle history navigation. The derived
|
||||||
displayed (`viewedTurn`) and adds `viewTurn(turn)` /
|
|
||||||
`returnToCurrent()` for history navigation. The derived
|
|
||||||
`historyMode` rune flips automatically when `viewedTurn <
|
`historyMode` rune flips automatically when `viewedTurn <
|
||||||
currentTurn`; the layout passes it to Phase 12's sidebar /
|
currentTurn`; the layout passes it to the sidebar / bottom-tabs
|
||||||
bottom-tabs wiring (which hides the order tab) and to
|
wiring (which hides the order tab) and to
|
||||||
`OrderDraftStore.bindClient` (which gates `add` / `remove` /
|
`OrderDraftStore.bindClient` (which gates `add` / `remove` / `move`).
|
||||||
`move`). See "History mode" below for the cache and refresh
|
See "History mode" below for the cache and refresh rules.
|
||||||
rules.
|
Tab-focus refreshes are supplemented by push-event-driven refreshes;
|
||||||
- Phase 24 replaces the tab-focus refresh with push-event-driven
|
the visibility listener stays as a fallback for background tabs that
|
||||||
refreshes; the visibility listener stays as a fallback for
|
miss a push. The wrap-mode toggle UI is wired on top of
|
||||||
background tabs that miss a push.
|
`setWrapMode`.
|
||||||
- Phase 29 wires the wrap-mode toggle UI on top of `setWrapMode`.
|
|
||||||
|
|
||||||
## Why `current_turn` lives on `GameSummary`
|
## 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 already tracks `current_turn` in its runtime projection
|
||||||
(`backend/internal/server/handlers_user_lobby_helpers.go`
|
(`backend/internal/server/handlers_user_lobby_helpers.go`
|
||||||
`gameSummaryToWire` reads it from `g.RuntimeSnapshot.CurrentTurn`).
|
`gameSummaryToWire` reads it from `g.RuntimeSnapshot.CurrentTurn`).
|
||||||
|
The `current_turn` field defaults to zero on the FB side, so existing
|
||||||
The wire change touches Phase 8's already-shipped catalogue, but the
|
|
||||||
`current_turn` field defaults to zero on the FB side, so existing
|
|
||||||
tests and the dev sandbox flow continue to work unchanged.
|
tests and the dev sandbox flow continue to work unchanged.
|
||||||
|
|
||||||
## State binding
|
## State binding
|
||||||
|
|
||||||
`map/state-binding.ts::reportToWorld(report)` translates a
|
`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 /
|
primitive per planet across all four kinds (local / other /
|
||||||
uninhabited / unidentified). Each kind gets a distinct fill colour,
|
uninhabited / unidentified). Each kind gets a distinct fill colour,
|
||||||
fill alpha, and point radius so the four classes are
|
fill alpha, and point radius so the four classes are
|
||||||
visually-distinguishable at a glance; later phases will refine the
|
visually-distinguishable at a glance; colour-palette refinement is
|
||||||
colour palette as the visual language stabilises (Phase 35 polish).
|
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
|
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.
|
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
|
In history mode `refresh()` is a no-op — forcing a reload would
|
||||||
silently bump the user back onto the current turn while they are
|
silently bump the user back onto the current turn while they are
|
||||||
intentionally viewing a past one. Push events (Phase 24) still
|
intentionally viewing a past one. Push events still deliver
|
||||||
deliver new-turn notifications asynchronously while the user
|
new-turn notifications asynchronously while the user explores
|
||||||
explores history, so the pending-turn toast continues to work.
|
history, so the pending-turn toast continues to work.
|
||||||
|
|
||||||
`setWrapMode(mode)` writes to `Cache` and updates the rune; the
|
`setWrapMode(mode)` writes to `Cache` and updates the rune; the
|
||||||
map view's effect picks the change up and re-mounts the renderer
|
map view's effect picks the change up and re-mounts the renderer
|
||||||
@@ -118,20 +111,19 @@ with the new mode.
|
|||||||
|
|
||||||
## Map visibility toggles
|
## Map visibility toggles
|
||||||
|
|
||||||
Phase 29 adds a `mapToggles: MapToggles` rune that drives the
|
A `mapToggles: MapToggles` rune drives the gear popover in the map
|
||||||
gear popover in the map view. Every flag defaults to `true` —
|
view. Every flag defaults to `true` — including `unreachablePlanets`
|
||||||
including `unreachablePlanets` (showing every planet by default)
|
(showing every planet by default) and `visibleHyperspace` (the fog
|
||||||
and `visibleHyperspace` (the fog overlay on by default). The
|
overlay on by default). The exhaustive shape lives in
|
||||||
exhaustive shape lives in `src/lib/game-state.svelte.ts`; the
|
`src/lib/game-state.svelte.ts`; the gear popover
|
||||||
gear popover (`src/lib/active-view/map-toggles.svelte`) is a
|
(`src/lib/active-view/map-toggles.svelte`) is a thin view of the
|
||||||
thin view of the rune.
|
rune.
|
||||||
|
|
||||||
`setMapToggle(key, value)` flips one entry in place and
|
`setMapToggle(key, value)` flips one entry in place and persists
|
||||||
persists the whole blob to `Cache` under the
|
the whole blob to `Cache` under the `game-map-toggles/{gameId}` key.
|
||||||
`game-map-toggles/{gameId}` key. The blob carries a companion
|
The blob carries a companion `lastResetTurn` number — the turn at
|
||||||
`lastResetTurn` number — the turn at which the toggles were last
|
which the toggles were last reset to defaults — so the new-turn reset
|
||||||
reset to defaults — so the new-turn reset path (below) can detect
|
path (below) can detect a stale blob even across a cross-session gap.
|
||||||
a stale blob even across a cross-session gap.
|
|
||||||
|
|
||||||
### New-turn reset
|
### New-turn reset
|
||||||
|
|
||||||
@@ -156,7 +148,7 @@ The cache namespace and blob shape are documented in
|
|||||||
|
|
||||||
## History mode
|
## 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:
|
without losing the live snapshot. The store keeps two turn runes:
|
||||||
|
|
||||||
- `currentTurn` — the server's authoritative latest. Only
|
- `currentTurn` — the server's authoritative latest. Only
|
||||||
@@ -170,7 +162,7 @@ The derived `historyMode` rune (`status === "ready" && viewedTurn
|
|||||||
< currentTurn`) drives every history-aware consumer:
|
< currentTurn`) drives every history-aware consumer:
|
||||||
|
|
||||||
- the layout passes it to `Sidebar` / `BottomTabs` so the order
|
- 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
|
- the layout passes a `getHistoryMode` getter to
|
||||||
`OrderDraftStore.bindClient` so `add` / `remove` / `move` are
|
`OrderDraftStore.bindClient` so `add` / `remove` / `move` are
|
||||||
no-ops while the user is looking at a past turn;
|
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
|
- the new `HistoryBanner` component renders the sticky "Viewing
|
||||||
turn N · read-only" strip when the flag is true.
|
turn N · read-only" strip when the flag is true.
|
||||||
|
|
||||||
`last-viewed-turn` semantics keep their Phase 11 meaning: "the
|
`last-viewed-turn` means "the latest turn the user was caught up
|
||||||
latest turn the user was caught up on". `loadTurn` only writes the
|
on". `loadTurn` only writes the cache row when called with
|
||||||
cache row when called with `isCurrent === true` (i.e. when the
|
`isCurrent === true` (i.e. when the load matches `currentTurn`).
|
||||||
load matches `currentTurn`). Historical excursions are therefore
|
Historical excursions are therefore ephemeral: closing the tab and
|
||||||
ephemeral: closing the tab and reopening the game resumes on the
|
reopening the game resumes on the last caught-up turn, not on the
|
||||||
last caught-up turn, not on the last clicked one.
|
last clicked one.
|
||||||
|
|
||||||
Past-turn reports are cached in the `game-history` namespace
|
Past-turn reports are cached in the `game-history` namespace
|
||||||
(`{gameId}/turn/{N}` → `GameReport`). The cache is written by
|
(`{gameId}/turn/{N}` → `GameReport`). The cache is written by
|
||||||
`loadTurn` on every successful historical fetch and read first by
|
`loadTurn` on every successful historical fetch and read first by
|
||||||
`viewTurn(N)` before falling back to the network. Past turns are
|
`viewTurn(N)` before falling back to the network. Past turns are
|
||||||
immutable, so the cache has no TTL and no eviction in Phase 26.
|
immutable, so the cache has no TTL and no eviction. The current-turn
|
||||||
The current-turn snapshot is deliberately *not* cached — it is
|
snapshot is deliberately *not* cached — it is mutable until the next
|
||||||
mutable until the next engine tick.
|
engine tick.
|
||||||
|
|||||||
+13
-11
@@ -1,14 +1,14 @@
|
|||||||
# i18n (UI)
|
# i18n (UI)
|
||||||
|
|
||||||
The UI client ships with a minimal locale primitive used by the
|
The UI client ships with a minimal locale primitive used by the
|
||||||
phase-7 login form, the root layout, and the lobby placeholder. The
|
login form, the root layout, and the lobby. The goal is just
|
||||||
goal is just enough infrastructure to translate user-visible
|
enough infrastructure to translate user-visible strings, switch
|
||||||
strings, switch the active language at runtime, and forward the
|
the active language at runtime, and forward the caller's choice to
|
||||||
caller's choice to the gateway. Phase 35 will swap this primitive
|
the gateway. Swapping this primitive for a fuller solution with
|
||||||
for a fuller solution once message-format pluralisation, lazy
|
message-format pluralisation, lazy loading, and translator
|
||||||
loading, and translator workflows become necessary; until then,
|
workflows is deferred to the finalization plan
|
||||||
the surface here covers every authenticated and unauthenticated
|
(../Plan-finalize.md); until then, the surface here covers every
|
||||||
screen the client renders.
|
authenticated and unauthenticated screen the client renders.
|
||||||
|
|
||||||
## Surface
|
## 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
|
The web target calls it without arguments, in which case the helper
|
||||||
reads `navigator.languages` (or `navigator.language` as fallback).
|
reads `navigator.languages` (or `navigator.language` as fallback).
|
||||||
Native wrappers (Wails, Capacitor) will pass their system locale
|
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
|
The detection runs once at module load — there is no asynchronous
|
||||||
init step. Callers that mutate the locale (e.g. the language picker
|
init step. Callers that mutate the locale (e.g. the language picker
|
||||||
on `/login`) call `i18n.setLocale(next)` directly. The choice is
|
on `/login`) call `i18n.setLocale(next)` directly. The choice is
|
||||||
**not** persisted between page reloads in Phase 7; the next visit
|
**not** persisted between page reloads; the next visit re-runs
|
||||||
re-runs detection. Persistence is a phase-35 concern.
|
detection. Persistence is deferred to the finalization plan
|
||||||
|
(../Plan-finalize.md).
|
||||||
|
|
||||||
## Forwarding the locale to the gateway
|
## 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 lobby is the first authenticated view; the user lands here after
|
||||||
the email-code login completes (see
|
the email-code login completes (see
|
||||||
[`docs/auth-flow.md`](auth-flow.md)). Phase 8 introduced the live
|
[`docs/auth-flow.md`](auth-flow.md)). This doc captures the
|
||||||
lobby with five sections, the create-game form, and the TS-side
|
sections, the application / invite lifecycle the user sees, and
|
||||||
FlatBuffers integration the rest of the client builds on. This doc
|
the defaults baked into the create-game form.
|
||||||
captures the sections, the application / invite lifecycle the user
|
|
||||||
sees, and the defaults baked into the create-game form.
|
|
||||||
|
|
||||||
## Sections
|
## Sections
|
||||||
|
|
||||||
@@ -23,15 +21,15 @@ width.
|
|||||||
| `my applications` | `no applications` | `lobby.my.applications.list` | Status badge (`pending` / `approved` / `rejected`) |
|
| `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`) |
|
| `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
|
The header preserves the device-session-id `<code>` block (kept as
|
||||||
Phase 7 placeholder (kept as a debug affordance) plus a greeting if
|
a debug affordance) plus a greeting if the gateway returns a
|
||||||
the gateway returns a `display_name` for the caller.
|
`display_name` for the caller.
|
||||||
|
|
||||||
`GameSummary` carries an extra `current_turn` field (Phase 11
|
`GameSummary` carries a `current_turn` field that the lobby UI does
|
||||||
extension) that the lobby UI does not display directly — the in-game
|
not display directly — the in-game shell reads it from the same
|
||||||
shell reads it from the same payload to load the matching
|
payload to load the matching `user.games.report` for the map view
|
||||||
`user.games.report` for the map view without an additional gateway
|
without an additional gateway call. See
|
||||||
call. See [`game-state.md`](game-state.md) for the consumer's view.
|
[`game-state.md`](game-state.md) for the consumer's view.
|
||||||
|
|
||||||
## Application lifecycle
|
## 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`
|
The gateway encodes lobby payloads through `pkg/transcoder/lobby.go`
|
||||||
into FlatBuffers bytes; the browser must decode them with the same
|
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`;
|
- `flatbuffers` runtime dependency in `ui/frontend/package.json`;
|
||||||
- `make -C ui fbs-ts` driving `flatc --ts` to regenerate the bindings
|
- `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
|
- a Vitest round-trip suite (`tests/lobby-fbs.test.ts`) that catches
|
||||||
binding drift in CI.
|
binding drift in CI.
|
||||||
|
|
||||||
Phase 7's `user.account.get` decode previously used
|
`user.account.get` decodes through the generated `AccountResponse`
|
||||||
`JSON.parse(TextDecoder)`; that path was rewritten in Phase 8 to use
|
table, so the lobby greeting works against a real local stack as well
|
||||||
the same generated `AccountResponse` table, so the lobby greeting now
|
as the mocked Playwright fixtures.
|
||||||
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
|
the plan is the file system plus those wrappers — there is no
|
||||||
separate dispatch component.
|
separate dispatch component.
|
||||||
|
|
||||||
| URL | Active view component | Phase that fills it |
|
| URL | Active view component |
|
||||||
| ------------------------------------- | ---------------------------------------------- | ----------------------- |
|
| ------------------------------------------ | ---------------------------------------------------------------------- |
|
||||||
| URL | Active view component | Phase that fills it |
|
| `/games/:id/map` | `lib/active-view/map.svelte` |
|
||||||
| ------------------------------------------ | ---------------------------------------------- | ----------------------- |
|
| `/games/:id/table/:entity` | `lib/active-view/table.svelte` |
|
||||||
| `/games/:id/map` | `lib/active-view/map.svelte` | Phase 11 |
|
| `/games/:id/report` | `lib/active-view/report.svelte` (see [report-view.md](report-view.md)) |
|
||||||
| `/games/:id/table/:entity` | `lib/active-view/table.svelte` | Phase 11 / 17 / 19 / 22 |
|
| `/games/:id/battle/:battleId?` | `lib/active-view/battle.svelte` |
|
||||||
| `/games/:id/report` | `lib/active-view/report.svelte` (see [report-view.md](report-view.md)) | Phase 23 |
|
| `/games/:id/mail` | `lib/active-view/mail.svelte` |
|
||||||
| `/games/:id/battle/:battleId?` | `lib/active-view/battle.svelte` | Phase 27 |
|
| `/games/:id/designer/science/:scienceId?` | `lib/active-view/designer-science.svelte` |
|
||||||
| `/games/:id/mail` | `lib/active-view/mail.svelte` | Phase 28 |
|
|
||||||
| `/games/:id/designer/science/:scienceId?` | `lib/active-view/designer-science.svelte` | Phase 21 |
|
|
||||||
|
|
||||||
`/games/:id` (no trailing view) redirects to `/games/:id/map`. The
|
`/games/:id` (no trailing view) redirects to `/games/:id/map`. The
|
||||||
optional `:scienceId?` segment on the science designer route matches
|
optional `:scienceId?` segment on the science designer route matches
|
||||||
SvelteKit's `[[scienceId]]` syntax — `/designer/science` opens the
|
SvelteKit's `[[scienceId]]` syntax — `/designer/science` opens the
|
||||||
empty new-science form, `/designer/science/{name}` opens the named
|
empty new-science form, `/designer/science/{name}` opens the named
|
||||||
science. Phase 17/18 originally added a parallel ship-class designer
|
science. Ship-class design is folded into the sidebar ship-class
|
||||||
route; Phase 30 removed it and folded ship-class design into the
|
calculator (`lib/sidebar/calculator-tab.svelte`, see
|
||||||
sidebar ship-class calculator (`lib/sidebar/calculator-tab.svelte`,
|
[calculator-ux.md](calculator-ux.md)), reached from the ship-classes
|
||||||
see [calculator-ux.md](calculator-ux.md)), reached from the
|
table and the view/bottom menus.
|
||||||
ship-classes table and the view/bottom menus.
|
|
||||||
|
|
||||||
The `entity` slug on the table route is kebab-case (`planets`,
|
The `entity` slug on the table route is kebab-case (`planets`,
|
||||||
`ship-classes`, `ship-groups`, `fleets`, `sciences`, `races`).
|
`ship-classes`, `ship-groups`, `fleets`, `sciences`, `races`).
|
||||||
`table.svelte` is the active-view router: it dispatches by slug to
|
`table.svelte` is the active-view router: it dispatches by slug to
|
||||||
the per-entity component (`ship-classes` → `table-ship-classes.svelte`
|
the per-entity component (`ship-classes` → `table-ship-classes.svelte`;
|
||||||
in Phase 17; the others fall back to the Phase 10 stub copy until
|
other entities dispatch to their respective components).
|
||||||
their respective phases land).
|
|
||||||
|
|
||||||
## Sidebar tools and state preservation
|
## Sidebar tools and state preservation
|
||||||
|
|
||||||
The desktop sidebar hosts three tools:
|
The desktop sidebar hosts three tools:
|
||||||
|
|
||||||
| Tool | Component | Phase that fills it |
|
| Tool | Component |
|
||||||
| ---------- | -------------------------------------- | -------------------- |
|
| ---------- | ----------------------------------- |
|
||||||
| Calculator | `lib/sidebar/calculator-tab.svelte` | Phase 30 |
|
| Calculator | `lib/sidebar/calculator-tab.svelte` |
|
||||||
| Inspector | `lib/sidebar/inspector-tab.svelte` | Phase 13 / 19 |
|
| Inspector | `lib/sidebar/inspector-tab.svelte` |
|
||||||
| Order | `lib/sidebar/order-tab.svelte` | Phase 12 / 14 |
|
| Order | `lib/sidebar/order-tab.svelte` |
|
||||||
|
|
||||||
The selected-tab state is a `$state` rune in
|
The selected-tab state is a `$state` rune in
|
||||||
`routes/games/[id]/+layout.svelte`, bound into
|
`routes/games/[id]/+layout.svelte`, bound into
|
||||||
`lib/sidebar/sidebar.svelte` via `$bindable()`. The layout owns the
|
`lib/sidebar/sidebar.svelte` via `$bindable()`. The layout owns the
|
||||||
rune so external events — Phase 13's planet click, future similar
|
rune so external events — such as a planet click — can drive the
|
||||||
flows — can drive the active tab from outside the sidebar without
|
active tab from outside the sidebar without plumbing callbacks. The component is mounted by the layout, and
|
||||||
plumbing callbacks. The component is mounted by the layout, and
|
|
||||||
SvelteKit keeps that layout instance alive while the user navigates
|
SvelteKit keeps that layout instance alive while the user navigates
|
||||||
between child routes (`/games/:id/map` → `/games/:id/report` → …),
|
between child routes (`/games/:id/map` → `/games/:id/report` → …),
|
||||||
so the rune survives every active-view switch automatically with no
|
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.
|
bindable in place; the layout sees the change through the binding.
|
||||||
|
|
||||||
A `?sidebar=calc|calculator|inspector|order` URL param is read once
|
A `?sidebar=calc|calculator|inspector|order` URL param is read once
|
||||||
on mount and seeds the initial tab. Later phases that want to land
|
on mount and seeds the initial tab. Navigation flows that want to
|
||||||
the user on a particular tool (for example, Phase 14's first
|
land the user on a particular tool can set this param on navigation.
|
||||||
end-to-end command flow) can set it on navigation.
|
|
||||||
|
|
||||||
The Order entry is hidden when the layout's `historyMode` flag is
|
The Order entry is hidden when the layout's `historyMode` flag is
|
||||||
true. Phase 12 plumbs the flag end-to-end as a prop —
|
true. `+layout.svelte` forwards a derived value to `Sidebar`, which
|
||||||
`+layout.svelte` forwards a derived value to `Sidebar`, which
|
|
||||||
forwards `hideOrder` to its `TabBar`; the same flag goes to
|
forwards `hideOrder` to its `TabBar`; the same flag goes to
|
||||||
`BottomTabs` so the mobile `Order` button is also suppressed. A
|
`BottomTabs` so the mobile `Order` button is also suppressed. A
|
||||||
`?sidebar=order` URL seed that arrives while the flag is true falls
|
`?sidebar=order` URL seed that arrives while the flag is true falls
|
||||||
back to `inspector`, and an `$effect` on the sidebar resets
|
back to `inspector`, and an `$effect` on the sidebar resets
|
||||||
`activeTab` away from `order` if the flag flips on mid-session.
|
`activeTab` away from `order` if the flag flips on mid-session.
|
||||||
|
|
||||||
Phase 26 wires the flag to the live history signal owned by
|
The `historyMode` flag is derived from the live history signal owned
|
||||||
`GameStateStore`. The derivation lives directly in `+layout.svelte`
|
by `GameStateStore`. The derivation lives directly in `+layout.svelte`
|
||||||
(`const historyMode = $derived(gameState.historyMode)`) — no
|
(`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
|
the single consumer and the project's compactness rule rejects a
|
||||||
one-line indirection. The order draft survives the toggle because
|
one-line indirection. The order draft survives the toggle because
|
||||||
`OrderDraftStore` lives one level above the sidebar in the layout
|
`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
|
## Header turn navigator and history banner
|
||||||
|
|
||||||
The header replaces the Phase 11 inline `turn N` text with a
|
The header shows a `← Turn N →` triplet (`lib/header/turn-navigator.svelte`). The
|
||||||
`← Turn N →` triplet (`lib/header/turn-navigator.svelte`). The
|
|
||||||
arrows step `viewedTurn` by ±1 (disabled at boundaries `0` and
|
arrows step `viewedTurn` by ±1 (disabled at boundaries `0` and
|
||||||
`currentTurn`); clicking the middle button opens an absolute
|
`currentTurn`); clicking the middle button opens an absolute
|
||||||
popover (desktop) or a fixed full-width drawer (mobile, ≤ 767.98
|
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
|
toggle in the header right corner. Tapping the toggle slides the
|
||||||
sidebar in as a fixed overlay above the active view; a close
|
sidebar in as a fixed overlay above the active view; a close
|
||||||
button on the sidebar dismisses it. The full swipe-from-right
|
button on the sidebar dismisses it. The full swipe-from-right
|
||||||
gesture in the IA section is deferred to Phase 35 polish — the
|
gesture is deferred to the finalization plan
|
||||||
click toggle satisfies the "layout switches at 768 px" acceptance
|
([../PLAN-finalize.md](../PLAN-finalize.md)).
|
||||||
criterion on Phase 10.
|
|
||||||
- **< 768 px (mobile)** — the sidebar is hidden entirely and the
|
- **< 768 px (mobile)** — the sidebar is hidden entirely and the
|
||||||
bottom-tabs row appears at the bottom of the viewport. The
|
bottom-tabs row appears at the bottom of the viewport. The
|
||||||
view-menu trigger swaps to a hamburger icon (☰) that opens 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
|
On mobile the bottom tab row does not include `Inspector`. The
|
||||||
inspector content is reached by tapping a map object instead, which
|
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
|
## 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.
|
navigation re-routes them to `/map` and re-applies the overlay.
|
||||||
|
|
||||||
The `More` button opens a drawer that mirrors the header view-menu
|
The `More` button opens a drawer that mirrors the header view-menu
|
||||||
content. The IA section's narrower "More" list (Mail, Battle log,
|
content. A narrower "More" list (Mail, Battle log, Tables, History,
|
||||||
Tables, History, Settings, Logout) is the polish target for Phase 35
|
Settings, Logout) is deferred to the finalization plan
|
||||||
— Phase 10 keeps a single source of truth for destinations.
|
([../PLAN-finalize.md](../PLAN-finalize.md)); the current drawer keeps
|
||||||
|
a single source of truth for destinations.
|
||||||
|
|
||||||
## Transient map overlays
|
## Transient map overlays
|
||||||
|
|
||||||
Some views can push a transient overlay onto `/map` with a back
|
Some views can push a transient overlay onto `/map` with a back
|
||||||
affordance. (Phase 30's calculator reach circles are a simpler,
|
affordance. (The calculator reach circles are a simpler, always-on
|
||||||
always-on map extra rather than a back-stacked overlay; the transient
|
map extra rather than a back-stacked overlay; the transient
|
||||||
back-stack mechanism itself is still a Phase 34 concept.) A transient
|
back-stack mechanism is planned — see
|
||||||
overlay clears when the user navigates to any other view via the header
|
[../ROADMAP.md](../ROADMAP.md).) A transient overlay clears when the
|
||||||
or the bottom-tabs.
|
user navigates to any other view via the header or the bottom-tabs.
|
||||||
|
|
||||||
Phase 10 documents this concept but does not implement the
|
The back-stack mechanism is not yet implemented; it is planned
|
||||||
back-stack mechanism. Phase 34 lands the back-stack alongside its
|
alongside its first user (multi-turn projection, range circles in the
|
||||||
first user (multi-turn projection, range circles in the ship-class
|
ship-class designer) in [../ROADMAP.md](../ROADMAP.md).
|
||||||
designer).
|
|
||||||
|
|
||||||
## 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:
|
translating a renderer click into a planet selection. The flow:
|
||||||
|
|
||||||
1. The renderer (`src/map/render.ts`) exposes `onClick(cb)` next to
|
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)`.
|
`GameStateStore.report`, and calls `SelectionStore.selectPlanet(number)`.
|
||||||
3. `SelectionStore` (`lib/selection.svelte.ts`) is a runes store
|
3. `SelectionStore` (`lib/selection.svelte.ts`) is a runes store
|
||||||
instantiated by the layout and exposed via Svelte context under
|
instantiated by the layout and exposed via Svelte context under
|
||||||
`SELECTION_CONTEXT_KEY`. It carries a discriminated union — Phase
|
`SELECTION_CONTEXT_KEY`. It carries a discriminated union —
|
||||||
13 only models `{ kind: "planet"; id: number }`; Phase 19 widens
|
`{ kind: "planet"; id: number }` for planets and widened for
|
||||||
it for ship groups. Selection is in-memory only: it survives the
|
ship groups. Selection is in-memory only: it survives the
|
||||||
layout's lifetime (active-view switches inside `/games/:id/*`)
|
layout's lifetime (active-view switches inside `/games/:id/*`)
|
||||||
but does not persist across reloads — that contrast with the
|
but does not persist across reloads — that contrast with the
|
||||||
order draft is intentional.
|
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
|
The mobile bottom-sheet is mounted alongside `<BottomTabs />` in the
|
||||||
layout. Its visibility is conditional on `effectiveTool === "map"` so
|
layout. Its visibility is conditional on `effectiveTool === "map"` so
|
||||||
it does not stack on top of the calc / order overlays. Phase 13 ships
|
it does not stack on top of the calc / order overlays. The dismissal
|
||||||
the minimal dismissal surface: a close button (`✕`) that calls
|
surface is a close button (`✕`) that calls `SelectionStore.clear()`.
|
||||||
`SelectionStore.clear()`. Tap-outside and swipe-down dismissal from
|
Tap-outside and swipe-down dismissal are deferred to the finalization
|
||||||
the IA section are deferred to Phase 35 polish. A click that lands on
|
plan ([../PLAN-finalize.md](../PLAN-finalize.md)). A click that lands
|
||||||
empty space is a no-op — selection is mutated only by an explicit
|
on empty space is a no-op — selection is mutated only by an explicit
|
||||||
planet click or by the close button.
|
planet click or by the close button.
|
||||||
|
|
||||||
The planet inspector itself is a presentational component: it takes
|
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`
|
Fields the FBS table does not project for a given kind read as `null`
|
||||||
and the inspector simply omits the row.
|
and the inspector simply omits the row.
|
||||||
|
|
||||||
The selected-planet visual on the map (a ring or halo) is **not**
|
The selected-planet visual on the map (a ring or halo) is deferred
|
||||||
shipped in Phase 13. It rolls into Phase 35 polish together with the
|
to the finalization plan ([../PLAN-finalize.md](../PLAN-finalize.md))
|
||||||
sheet's swipe-to-dismiss gesture.
|
together with the sheet's swipe-to-dismiss gesture.
|
||||||
|
|
||||||
## Auth gate
|
## 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
|
remote-first composer that reflects the gateway's pending-orders
|
||||||
queue would force a sync on every keystroke.
|
queue would force a sync on every keystroke.
|
||||||
|
|
||||||
Phase 14 lands the submit pipeline with batch semantics: every
|
The submit pipeline uses batch semantics: every entry the user has
|
||||||
entry the user has marked `valid` is collected into one signed
|
marked `valid` is collected into one signed `user.games.order`
|
||||||
`user.games.order` request. The engine validates and stores the
|
request. The engine validates and stores the order, returning
|
||||||
order, returning per-command `cmdApplied` / `cmdErrorCode` in the
|
per-command `cmdApplied` / `cmdErrorCode` in the response body. The
|
||||||
response body. The gateway re-encodes that JSON into the FBS
|
gateway re-encodes that JSON into the FBS `UserGamesOrderResponse`
|
||||||
`UserGamesOrderResponse` envelope (with `commands: [CommandItem]`
|
envelope (with `commands: [CommandItem]` populated), and `submitOrder`
|
||||||
populated), and `submitOrder` rejoins the verdict to each draft
|
rejoins the verdict to each draft entry by `cmdId`. Successfully
|
||||||
entry by `cmdId`. Successfully applied entries stay visible in
|
applied entries stay visible in the draft (the player keeps composing
|
||||||
the draft (the player keeps composing until turn cutoff);
|
until turn cutoff); rejected entries stay until the player edits or
|
||||||
rejected entries stay until the player edits or removes them.
|
removes them.
|
||||||
|
|
||||||
Phase 25 layers a transport-level policy on top of this baseline
|
A transport-level policy layers on top of the batch baseline without
|
||||||
without changing the batch semantics. The submit pipeline now
|
changing the batch semantics. The submit pipeline goes through
|
||||||
goes through `OrderQueue` (see
|
`OrderQueue` (see [`sync-protocol.md`](sync-protocol.md)): the queue
|
||||||
[`sync-protocol.md`](sync-protocol.md)): the queue holds the
|
holds the submit while the browser is offline, classifies
|
||||||
submit while the browser is offline, classifies
|
`turn_already_closed` and `game_paused` server replies into matching
|
||||||
`turn_already_closed` and `game_paused` server replies into
|
banners on the order tab, and exits the loop on the sticky states so
|
||||||
matching banners on the order tab, and exits the loop on the
|
a stream of mutations does not re-elicit the same gateway reply.
|
||||||
sticky states so a stream of mutations does not re-elicit the
|
Recovery from a `conflict` or `paused` banner happens on the next
|
||||||
same gateway reply. Recovery from a `conflict` or `paused`
|
`game.turn.ready` push frame via `OrderDraftStore.resetForNewTurn`,
|
||||||
banner happens on the next `game.turn.ready` push frame via
|
which clears the local draft and re-hydrates from the server for the
|
||||||
`OrderDraftStore.resetForNewTurn`, which clears the local draft
|
new turn.
|
||||||
and re-hydrates from the server for the new turn.
|
|
||||||
|
|
||||||
## Local-validation invariant
|
## 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
|
entries. The validation step is per-command and pure — it consults
|
||||||
the current `GameStateStore` snapshot only, never the network.
|
the current `GameStateStore` snapshot only, never the network.
|
||||||
|
|
||||||
Phase 14's `planetRename` is the first variant that exercises the
|
The `planetRename` variant exercises the `draft → valid | invalid`
|
||||||
`draft → valid | invalid` transition. The validator
|
transition. The validator (`lib/util/entity-name.ts`) is a TS port
|
||||||
(`lib/util/entity-name.ts`) is a TS port of
|
of `pkg/util/string.go.ValidateTypeName`, exercised on every render
|
||||||
`pkg/util/string.go.ValidateTypeName`, exercised on every render
|
|
||||||
in the inline editor and re-run by the store on every `add`. The
|
in the inline editor and re-run by the store on every `add`. The
|
||||||
submit pipeline filters the draft to `valid` entries only — any
|
submit pipeline filters the draft to `valid` entries only — any
|
||||||
`invalid` row blocks the Submit button.
|
`invalid` row blocks the Submit button.
|
||||||
@@ -79,13 +77,12 @@ draft ──validate──▶ valid ──submit──▶ submitting ──ack
|
|||||||
Transitions:
|
Transitions:
|
||||||
|
|
||||||
- **`draft → valid` / `draft → invalid`**: local validation. May
|
- **`draft → valid` / `draft → invalid`**: local validation. May
|
||||||
re-run when the underlying `GameStateStore` snapshot changes
|
re-run when the underlying `GameStateStore` snapshot changes.
|
||||||
(Phase 14+).
|
|
||||||
- **`valid → submitting`**: the submit pipeline picks the entry off
|
- **`valid → submitting`**: the submit pipeline picks the entry off
|
||||||
the draft and sends it to the gateway.
|
the draft and sends it to the gateway.
|
||||||
- **`submitting → applied` / `submitting → rejected`**: the gateway
|
- **`submitting → applied` / `submitting → rejected`**: the gateway
|
||||||
responded; the entry is no longer in flight.
|
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
|
`resultCode = "turn_already_closed"`. The order tab surfaces a
|
||||||
banner above the command list. Any subsequent mutation
|
banner above the command list. Any subsequent mutation
|
||||||
re-validates the conflict row back to `valid` / `invalid`; a
|
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
|
[`sync-protocol.md`](sync-protocol.md) for the full state
|
||||||
table and recovery paths.
|
table and recovery paths.
|
||||||
|
|
||||||
Phase 14 lands the local validators (`draft → valid | invalid`),
|
Local validators (`draft → valid | invalid`), the submit pipeline
|
||||||
the submit pipeline (`valid → submitting → applied | rejected`),
|
(`valid → submitting → applied | rejected`), and the optimistic
|
||||||
and the optimistic overlay that shows the player's intent on the
|
overlay that shows the player's intent on the map and inspector while
|
||||||
map and inspector while the order is in flight.
|
the order is in flight are all implemented.
|
||||||
|
|
||||||
Statuses are runtime-only — they are not persisted alongside the
|
Statuses are runtime-only — they are not persisted alongside the
|
||||||
commands themselves. On every `init` the store re-runs
|
commands themselves. On every `init` the store re-runs
|
||||||
@@ -110,9 +107,7 @@ stored value).
|
|||||||
|
|
||||||
## Discriminated union shape
|
## Discriminated union shape
|
||||||
|
|
||||||
`OrderCommand` is a discriminated union on the `kind` field. Phase
|
`OrderCommand` is a discriminated union on the `kind` field:
|
||||||
12 shipped the skeleton with a single content-free variant; Phase
|
|
||||||
14 added the first real one and Phase 15 added the second:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface PlaceholderCommand {
|
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
|
remove and reorder; later variants must keep `id: string` so the
|
||||||
store API stays uniform. The whole draft round-trips through
|
store API stays uniform. The whole draft round-trips through
|
||||||
IndexedDB structured clone, so every variant must use only
|
IndexedDB structured clone, so every variant must use only
|
||||||
JSON-friendly value types. Phase 14 lands `planetRename` together
|
JSON-friendly value types. `planetRename` ships with the inline
|
||||||
with the inline editor in `lib/inspectors/planet.svelte`, the
|
editor in `lib/inspectors/planet.svelte`, the local validator
|
||||||
local validator (`lib/util/entity-name.ts`, parity with
|
(`lib/util/entity-name.ts`, parity with
|
||||||
`pkg/util/string.go.ValidateTypeName`), and the submit pipeline.
|
`pkg/util/string.go.ValidateTypeName`), and the submit pipeline.
|
||||||
|
|
||||||
`setProductionType` is the wire-mirror of the engine's
|
`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
|
mirrors the engine's `Cache.PlanetProductionDisplayName` so the
|
||||||
overlay stays byte-equal with the next server report.
|
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
|
`setProductionType` carries a collapse-by-target rule.
|
||||||
collapse-by-target rule. `OrderDraftStore.add` enforces it:
|
`OrderDraftStore.add` enforces it:
|
||||||
when the incoming command's `kind` is `"setProductionType"` it
|
when the incoming command's `kind` is `"setProductionType"` it
|
||||||
drops every prior `setProductionType` entry with the same
|
drops every prior `setProductionType` entry with the same
|
||||||
`planetNumber` (and the matching keys from `statuses`) before
|
`planetNumber` (and the matching keys from `statuses`) before
|
||||||
@@ -187,9 +182,7 @@ coexist — the rules apply within a `kind`, not across.
|
|||||||
`OrderDraftStore` lives in
|
`OrderDraftStore` lives in
|
||||||
[`../frontend/src/sync/order-draft.svelte.ts`](../frontend/src/sync/order-draft.svelte.ts).
|
[`../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
|
The class is a Svelte 5 runes store, so the file extension is
|
||||||
`.svelte.ts` (the original PLAN.md artifact line listed `.ts` —
|
`.svelte.ts`.
|
||||||
the deviation is documented inline in `PLAN.md`'s Phase 12
|
|
||||||
"Decisions" subsection).
|
|
||||||
|
|
||||||
Lifecycle:
|
Lifecycle:
|
||||||
|
|
||||||
@@ -212,9 +205,8 @@ Layout integration mirrors `GameStateStore`:
|
|||||||
- Exposed through the `ORDER_DRAFT_CONTEXT_KEY` Svelte context.
|
- Exposed through the `ORDER_DRAFT_CONTEXT_KEY` Svelte context.
|
||||||
- Disposed in the layout's `onDestroy`.
|
- Disposed in the layout's `onDestroy`.
|
||||||
|
|
||||||
The order tab consumes the store via
|
The order tab and the planet inspector both consume the store via
|
||||||
`getContext(ORDER_DRAFT_CONTEXT_KEY)`; Phase 14's planet inspector
|
`getContext(ORDER_DRAFT_CONTEXT_KEY)` to push new commands.
|
||||||
will use the same key to push a new command.
|
|
||||||
|
|
||||||
## Submit pipeline
|
## Submit pipeline
|
||||||
|
|
||||||
@@ -224,9 +216,9 @@ will use the same key to push a new command.
|
|||||||
`markSubmitting(ids)` so each row reads `submitting`, then
|
`markSubmitting(ids)` so each row reads `submitting`, then
|
||||||
posts the snapshot through `submitOrder`.
|
posts the snapshot through `submitOrder`.
|
||||||
2. `submitOrder` builds the FBS `UserGamesOrder` request (game_id,
|
2. `submitOrder` builds the FBS `UserGamesOrder` request (game_id,
|
||||||
`updatedAt = 0` in Phase 14, every command encoded as a
|
`updatedAt`, every command encoded as a `CommandItem` with the
|
||||||
`CommandItem` with the typed payload union) and signs it via
|
typed payload union) and signs it via the existing
|
||||||
the existing `executeCommand` orchestration.
|
`executeCommand` orchestration.
|
||||||
3. The engine validates, stores, and answers `202 Accepted` with
|
3. The engine validates, stores, and answers `202 Accepted` with
|
||||||
the stored order body — `game_id`, `updatedAt`, plus each
|
the stored order body — `game_id`, `updatedAt`, plus each
|
||||||
command echoed with `cmdApplied` and (on rejection)
|
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
|
`applyOrderOverlay(report, commands, statuses)` (in
|
||||||
`api/game-state.ts`) returns a copy of the server `GameReport`
|
`api/game-state.ts`) returns a copy of the server `GameReport`
|
||||||
with every command in `applied` or `submitting` status projected
|
with every command in `applied` or `submitting` status projected
|
||||||
on top. Phase 14 understands `planetRename` only; future phases
|
on top.
|
||||||
extend the switch.
|
|
||||||
|
|
||||||
The overlay has its own context (`RENDERED_REPORT_CONTEXT_KEY`,
|
The overlay has its own context (`RENDERED_REPORT_CONTEXT_KEY`,
|
||||||
`lib/rendered-report.svelte.ts`) — the in-game shell layout owns
|
`lib/rendered-report.svelte.ts`) — the in-game shell layout owns
|
||||||
@@ -288,9 +279,7 @@ Cache row layout:
|
|||||||
| -------------- | ------------------ | ---------------- |
|
| -------------- | ------------------ | ---------------- |
|
||||||
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` |
|
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` |
|
||||||
|
|
||||||
The store writes the full draft on every mutation. Phase 25 may
|
The store writes the full draft on every mutation. The deterministic
|
||||||
profile the submit pipeline and batch into a microtask if write
|
|
||||||
amplification becomes a problem; until then the deterministic
|
|
||||||
write-on-every-mutation model is what tests assert and what the
|
write-on-every-mutation model is what tests assert and what the
|
||||||
layout relies on for crash safety.
|
layout relies on for crash safety.
|
||||||
|
|
||||||
@@ -300,14 +289,12 @@ order composer uses the namespace.
|
|||||||
|
|
||||||
## History mode wiring
|
## History mode wiring
|
||||||
|
|
||||||
Phase 26 implements history mode: the user can step back through
|
History mode lets the user step back through past turns and see the
|
||||||
past turns and see the report as it was. The IA section specifies
|
report as it was. The Order tab is hidden when history mode is active
|
||||||
that the Order tab is hidden when history mode is active — the
|
— the player is browsing an immutable snapshot, and composing commands
|
||||||
player is browsing an immutable snapshot, and composing commands
|
|
||||||
against it would be confusing.
|
against it would be confusing.
|
||||||
|
|
||||||
Phase 12 wires the flag end-to-end as a prop. The layout owns the
|
The layout owns the `historyMode` flag and passes it to:
|
||||||
flag and passes it to:
|
|
||||||
|
|
||||||
- `Sidebar` as `historyMode`. The sidebar forwards it to its
|
- `Sidebar` as `historyMode`. The sidebar forwards it to its
|
||||||
`TabBar` as `hideOrder`. The Order entry is filtered out of the
|
`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`
|
- `BottomTabs` as `hideOrder`. The mobile bottom-tab `Order`
|
||||||
button is suppressed when true.
|
button is suppressed when true.
|
||||||
|
|
||||||
Phase 26 turns the constant into a derived value driven by
|
`historyMode` is a derived value driven by `GameStateStore.historyMode`
|
||||||
`GameStateStore.historyMode` (`viewedTurn < currentTurn` while
|
(`viewedTurn < currentTurn` while `status === "ready"`). The same
|
||||||
`status === "ready"`). The same getter is also passed into
|
getter is also passed into `OrderDraftStore.bindClient` as
|
||||||
`OrderDraftStore.bindClient` as `getHistoryMode`, which short-
|
`getHistoryMode`, which short-circuits the `add` / `remove` / `move`
|
||||||
circuits the `add` / `remove` / `move` mutations to a no-op while
|
mutations to a no-op while the flag is true. This makes every
|
||||||
the flag is true. This makes every Phase 14–22 inspector affordance
|
inspector affordance that calls `orderDraft.add(...)` inert in history
|
||||||
that calls `orderDraft.add(...)` inert in history mode without
|
mode without per-component edits — the gate lives in the one
|
||||||
per-component edits — the gate lives in the one chokepoint that
|
chokepoint that all callers go through. The conflict / paused banners
|
||||||
all callers go through. The conflict / paused banners and the
|
and the in-flight sync pipeline are untouched: they describe state
|
||||||
in-flight sync pipeline are untouched: they describe state that
|
that exists independently of the user's current view.
|
||||||
exists independently of the user's current view.
|
|
||||||
|
|
||||||
The store itself stays alive across history-mode round-trips so
|
The store itself stays alive across history-mode round-trips so
|
||||||
the draft survives the toggle. The `RenderedReportSource` overlay
|
the draft survives the toggle. The `RenderedReportSource` overlay
|
||||||
@@ -346,13 +332,11 @@ the chrome.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Phase 12 + Phase 14 test artifacts:
|
|
||||||
|
|
||||||
- [`../frontend/tests/order-draft.test.ts`](../frontend/tests/order-draft.test.ts)
|
- [`../frontend/tests/order-draft.test.ts`](../frontend/tests/order-draft.test.ts)
|
||||||
— Vitest unit tests for the store. Drives `OrderDraftStore`
|
— Vitest unit tests for the store. Drives `OrderDraftStore`
|
||||||
directly with `IDBCache` over `fake-indexeddb`. Covers init,
|
directly with `IDBCache` over `fake-indexeddb`. Covers init,
|
||||||
add, remove, move, per-game isolation, mutations-before-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` /
|
(`validate` / `markSubmitting` / `applyResults` /
|
||||||
`revertSubmittingToValid`), and the
|
`revertSubmittingToValid`), and the
|
||||||
`hydrateFromServer` cache-miss fallback.
|
`hydrateFromServer` cache-miss fallback.
|
||||||
@@ -375,12 +359,12 @@ Phase 12 + Phase 14 test artifacts:
|
|||||||
— Vitest component tests for the rename action and the inline
|
— Vitest component tests for the rename action and the inline
|
||||||
editor's local validation.
|
editor's local validation.
|
||||||
- [`../frontend/tests/e2e/order-composer.spec.ts`](../frontend/tests/e2e/order-composer.spec.ts)
|
- [`../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).
|
commands, reload, persistence).
|
||||||
- [`../frontend/tests/e2e/rename-planet.spec.ts`](../frontend/tests/e2e/rename-planet.spec.ts)
|
- [`../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
|
— End-to-end: select a planet, rename, submit, observe the
|
||||||
the overlay-applied name on the inspector + map, reload, and
|
overlay-applied name on the inspector + map, reload, and see the
|
||||||
see the rename hydrated from `user.games.order.get`.
|
rename hydrated from `user.games.order.get`.
|
||||||
|
|
||||||
The `__galaxyDebug.seedOrderDraft(gameId, commands)` and
|
The `__galaxyDebug.seedOrderDraft(gameId, commands)` and
|
||||||
`__galaxyDebug.clearOrderDraft(gameId)` helpers in
|
`__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
|
2. Support pan and zoom over a toroidal world (`'torus'` mode) and
|
||||||
over a bounded plane (`'no-wrap'` mode), both first-class.
|
over a bounded plane (`'no-wrap'` mode), both first-class.
|
||||||
3. Run the same algorithm on web, Wails, Capacitor, and PWA
|
3. Run the same algorithm on web, Wails, Capacitor, and PWA
|
||||||
targets — only the browser is supported in Phase 9, but no API
|
targets — no API in this module assumes the platform.
|
||||||
in this module assumes the platform.
|
|
||||||
4. Provide deterministic hit-test for cursor-to-primitive mapping,
|
4. Provide deterministic hit-test for cursor-to-primitive mapping,
|
||||||
with results that are unit-testable independently of Pixi.
|
with results that are unit-testable independently of Pixi.
|
||||||
|
|
||||||
@@ -76,11 +75,11 @@ overrides them.
|
|||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
A single dark theme ships in Phase 9. The theme is a record of
|
A single dark theme is implemented. The theme is a record of default
|
||||||
default colours; primitives whose `style` omits a colour fall back
|
colours; primitives whose `style` omits a colour fall back to the
|
||||||
to the theme. Runtime theme switching is not implemented — Phase
|
theme. Runtime theme switching is not implemented — light/dark and
|
||||||
35 introduces light/dark and the materialise-on-theme-change
|
the materialise-on-theme-change cycle are deferred to the
|
||||||
cycle.
|
finalization plan ([../PLAN-finalize.md](../PLAN-finalize.md)).
|
||||||
|
|
||||||
## Hit-test
|
## Hit-test
|
||||||
|
|
||||||
@@ -134,7 +133,7 @@ Per-primitive distance:
|
|||||||
representation: from `(x1, y1)` to `(x1 + dx, y1 + dy)` where
|
representation: from `(x1, y1)` to `(x1 + dx, y1 + dy)` where
|
||||||
`(dx, dy)` is the torus-shortest delta from end-1 to end-2.
|
`(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
|
~1000 primitives on every pointer event. Spatial indexing is
|
||||||
deferred until profiling proves it necessary; PixiJS' culling and
|
deferred until profiling proves it necessary; PixiJS' culling and
|
||||||
batching handle the draw side without help.
|
batching handle the draw side without help.
|
||||||
@@ -260,10 +259,10 @@ average frame time over a scripted drag.
|
|||||||
|
|
||||||
## Pick mode
|
## Pick mode
|
||||||
|
|
||||||
Phase 16 introduced a generic *map-driven destination pick* the
|
The renderer provides a generic *map-driven destination pick* that
|
||||||
inspector uses for cargo routes and that ship-group dispatch
|
the inspector uses for cargo routes and ship-group dispatch. The
|
||||||
(Phase 19/20) will reuse. The renderer owns the visual lifecycle;
|
renderer owns the visual lifecycle; the Svelte side wraps it in a
|
||||||
the Svelte side wraps it in a promise-shaped service.
|
promise-shaped service.
|
||||||
|
|
||||||
Lifecycle (`RendererHandle.setPickMode(opts)`):
|
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
|
tracks) does not silently un-hide a primitive whose id is in the
|
||||||
current set.
|
current set.
|
||||||
|
|
||||||
The Phase 29 map view (`src/lib/active-view/map.svelte`) computes
|
The map view (`src/lib/active-view/map.svelte`) computes the set
|
||||||
the set from the per-game `MapToggles` rune + the planet-cascade
|
from the per-game `MapToggles` rune + the planet-cascade rule and
|
||||||
rule and pushes it on every effect run; toggling a checkbox
|
pushes it on every effect run; toggling a checkbox flips visibility
|
||||||
flips visibility within one frame without a Pixi remount.
|
within one frame without a Pixi remount.
|
||||||
|
|
||||||
## Visible-hyperspace overlay (the "fog")
|
## Visible-hyperspace overlay (the "fog")
|
||||||
|
|
||||||
`RendererHandle.setVisibilityFog(circles)` draws (or removes) the
|
`RendererHandle.setVisibilityFog(circles)` draws (or removes) the
|
||||||
Phase 29 fog overlay used to highlight the player's visible
|
fog overlay that highlights the player's visible hyperspace. Each
|
||||||
hyperspace. Each entry describes a circle around a LOCAL planet
|
entry describes a circle around a LOCAL planet where the player has
|
||||||
where the player has scanner / visibility coverage:
|
scanner / visibility coverage:
|
||||||
|
|
||||||
- An empty list destroys the existing fog rectangles and mask.
|
- An empty list destroys the existing fog rectangles and mask.
|
||||||
- A non-empty list rebuilds a single viewport-level `fogLayer` (a
|
- A non-empty list rebuilds a single viewport-level `fogLayer` (a
|
||||||
@@ -381,14 +380,14 @@ pixels:
|
|||||||
- `getMapPrimitives()` returns a snapshot of every primitive in
|
- `getMapPrimitives()` returns a snapshot of every primitive in
|
||||||
the active world: id, kind, priority, current alpha
|
the active world: id, kind, priority, current alpha
|
||||||
(post-overlay), the explicit fill / stroke colour from its
|
(post-overlay), the explicit fill / stroke colour from its
|
||||||
`Style` (no theme fallback), and the Phase 29 `visible` flag
|
`Style` (no theme fallback), and the `visible` flag mirroring the
|
||||||
mirroring the renderer's hide set.
|
renderer's hide set.
|
||||||
- `getMapPickState()` returns `{ active, sourcePlanetNumber,
|
- `getMapPickState()` returns `{ active, sourcePlanetNumber,
|
||||||
reachableIds, hoveredId }` — the renderer's view of the
|
reachableIds, hoveredId }` — the renderer's view of the
|
||||||
current pick session.
|
current pick session.
|
||||||
- `getMapCamera()` returns the current camera + viewport +
|
- `getMapCamera()` returns the current camera + viewport +
|
||||||
canvas-origin snapshot, used by Phase 29 e2e specs to assert
|
canvas-origin snapshot, used by e2e specs to assert camera
|
||||||
camera preservation across wrap-mode flips.
|
preservation across wrap-mode flips.
|
||||||
- `getMapFog()` returns the most recent fog input
|
- `getMapFog()` returns the most recent fog input
|
||||||
(the list of circles last passed to `setVisibilityFog`).
|
(the list of circles last passed to `setVisibilityFog`).
|
||||||
Empty when the `visibleHyperspace` toggle is off.
|
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
|
The in-game "turn report" view is a single scrollable layout with
|
||||||
layout with twenty sections, one per array on the FBS `Report`
|
twenty sections, one per array on the FBS `Report` table. The route
|
||||||
table. The route file is the standard two-line wrapper; the
|
file is the standard two-line wrapper; the orchestrator and the
|
||||||
orchestrator and the per-section components live under
|
per-section components live under
|
||||||
`ui/frontend/src/lib/active-view/report/`.
|
`ui/frontend/src/lib/active-view/report/`.
|
||||||
|
|
||||||
## Component layout
|
## Component layout
|
||||||
@@ -140,7 +140,7 @@ highlight consistent without a second source of truth.
|
|||||||
|
|
||||||
## i18n namespace
|
## i18n namespace
|
||||||
|
|
||||||
All Phase 23 strings live under `game.report.*`:
|
All strings live under `game.report.*`:
|
||||||
- `game.report.loading` — section loading placeholder.
|
- `game.report.loading` — section loading placeholder.
|
||||||
- `game.report.back_to_map`, `game.report.toc.title`,
|
- `game.report.back_to_map`, `game.report.toc.title`,
|
||||||
`game.report.toc.mobile_label` — shell-level strings.
|
`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
|
output for that turn is split between the four tech research tracks
|
||||||
in those proportions
|
in those proportions
|
||||||
(`game/internal/controller/planet/production.go.runScienceResearch`).
|
(`game/internal/controller/planet/production.go.runScienceResearch`).
|
||||||
Phase 21 lights up the CRUD list, the designer, and the
|
The CRUD list, the designer, and the production-picker integration
|
||||||
production-picker integration. The wire and the engine validation
|
are provided by the UI; the wire and engine validation are handled
|
||||||
are unchanged from earlier phases — only the UI is new.
|
by the backend.
|
||||||
|
|
||||||
## Engine semantics in one paragraph
|
## 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
|
matches. A live readout under the inputs displays the running total
|
||||||
so the player can chase it down without trial-and-error guessing.
|
so the player can chase it down without trial-and-error guessing.
|
||||||
|
|
||||||
The strict-sum gate is a Phase 21 decision (alternatives —
|
The strict-sum gate was chosen over alternatives — auto-rebalance
|
||||||
auto-rebalance, raw-parts-with-engine-normalisation — were
|
and raw-parts-with-engine-normalisation — because keeping the input
|
||||||
considered and rejected): keeping the input model close to "what
|
model close to "what gets sent on the wire" minimises surprises when
|
||||||
gets sent on the wire" minimises surprises when the engine returns
|
the engine returns the science exactly as typed. See
|
||||||
the science exactly as typed. See `lib/util/science-validation.ts`
|
`lib/util/science-validation.ts` for the validator and the
|
||||||
for the validator and the conversion helper.
|
conversion helper.
|
||||||
|
|
||||||
## Name validation
|
## Name validation
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Ship-group inspector actions
|
# Ship-group inspector actions
|
||||||
|
|
||||||
Phase 20 turns the read-only ship-group inspector
|
The ship-group inspector
|
||||||
(`ui/frontend/src/lib/inspectors/ship-group.svelte`) into an
|
(`ui/frontend/src/lib/inspectors/ship-group.svelte`) is an
|
||||||
interactive command source for the player's own groups in orbit.
|
interactive command source for the player's own groups in orbit.
|
||||||
This document is the running spec for the actions panel
|
This document is the running spec for the actions panel
|
||||||
(`ui/frontend/src/lib/inspectors/ship-group/actions.svelte`):
|
(`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-
|
- Drive / shields / cargo block mass = the corresponding ship-
|
||||||
class field (raw value).
|
class field (raw value).
|
||||||
- Weapons block mass = `core.weaponsBlockMass({ weapons,
|
- Weapons block mass = `core.weaponsBlockMass({ weapons,
|
||||||
armament })` (Phase 18 bridge); returns null on the invalid
|
armament })`; returns null on the invalid weapons/armament
|
||||||
weapons/armament pairing, in which case the row contributes
|
pairing, in which case the row contributes zero.
|
||||||
zero.
|
|
||||||
|
|
||||||
For `tech === "ALL"` every block whose mass is non-zero
|
For `tech === "ALL"` every block whose mass is non-zero
|
||||||
contributes against the player's race tech as the target. For
|
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
|
`ui/frontend/src/sync/order-types.ts` next to the JSDoc for each
|
||||||
variant.
|
variant.
|
||||||
|
|
||||||
## Decisions baked into Phase 20
|
## Design notes
|
||||||
|
|
||||||
- **`BlockUpgradeCost` migrated to `pkg/calc`**. The cost
|
- **`BlockUpgradeCost` lives in `pkg/calc`**. The cost formula
|
||||||
formula previously lived in
|
lives in `pkg/calc/ship.go`; the `ui/core/calc` bridge wraps
|
||||||
`game/internal/controller/ship_group_upgrade.go`. To keep the
|
pure `pkg/calc/` formulas, and the controller imports it
|
||||||
`ui/core/calc` bridge a wrapper around pure `pkg/calc/`
|
(`controller/ship_group_upgrade.go`).
|
||||||
formulas, the function moved to `pkg/calc/ship.go` and the
|
|
||||||
controller now imports it (`controller/ship_group_upgrade.go`).
|
|
||||||
- **`GameReport.otherRaces`**. The transfer-to-race picker reads
|
- **`GameReport.otherRaces`**. The transfer-to-race picker reads
|
||||||
from a new `GameReport.otherRaces: string[]` field, populated
|
from `GameReport.otherRaces: string[]`, populated by walking
|
||||||
by walking `report.player[]` and excluding the local race plus
|
`report.player[]` and excluding the local race plus every
|
||||||
every `extinct` entry. Phase 22 (Races View) reuses the same
|
`extinct` entry. The Races View reuses the same field.
|
||||||
field.
|
|
||||||
- **Stationed-ship rows are clickable**. The map deliberately
|
- **Stationed-ship rows are clickable**. The map deliberately
|
||||||
hides on-planet groups; the planet inspector's stationed-ship
|
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
|
variant so the actions panel is reachable from the standard
|
||||||
click flow.
|
click flow.
|
||||||
|
|||||||
+28
-27
@@ -15,8 +15,9 @@ namespace.
|
|||||||
This topic doc covers the web implementation only. The platform-
|
This topic doc covers the web implementation only. The platform-
|
||||||
agnostic `KeyStore` and `Cache` interfaces in
|
agnostic `KeyStore` and `Cache` interfaces in
|
||||||
`src/platform/store/index.ts` are what the rest of the client codes
|
`src/platform/store/index.ts` are what the rest of the client codes
|
||||||
against; later phases bring `WailsStore` and `CapacitorStore` adapters
|
against; `WailsStore` and `CapacitorStore` adapters will satisfy the
|
||||||
that satisfy the same contracts.
|
same contracts on their respective platforms (see
|
||||||
|
[../ROADMAP.md](../ROADMAP.md)).
|
||||||
|
|
||||||
Source-of-truth for the cross-service contract is
|
Source-of-truth for the cross-service contract is
|
||||||
[`../../docs/ARCHITECTURE.md` §15 "Transport security model"](../../docs/ARCHITECTURE.md);
|
[`../../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
|
Browsers older than the baseline above will fail at the first
|
||||||
`generateKey({ name: 'Ed25519' }, ...)` call with a
|
`generateKey({ name: 'Ed25519' }, ...)` call with a
|
||||||
`NotSupportedError`. Phase 6 deliberately does not ship a JavaScript
|
`NotSupportedError`. No JavaScript fallback (e.g. `@noble/ed25519`)
|
||||||
fallback (e.g. `@noble/ed25519`) — keeping the keystore on WebCrypto
|
is shipped — keeping the keystore on WebCrypto is what gives
|
||||||
is what gives us non-extractable storage on every supported engine.
|
non-extractable storage on every supported engine. The root layout
|
||||||
The Phase 7 root layout runs a one-time probe on boot and switches
|
runs a one-time probe on boot and switches to a "browser not
|
||||||
to a "browser not supported" page (described in
|
supported" page (described in [`auth-flow.md`](auth-flow.md)) when
|
||||||
[`auth-flow.md`](auth-flow.md)) when the probe rejects, instead of
|
the probe rejects, instead of attempting the keystore generate.
|
||||||
attempting the keystore generate.
|
|
||||||
|
|
||||||
### WebKit non-determinism note
|
### WebKit non-determinism note
|
||||||
|
|
||||||
@@ -60,8 +60,9 @@ is missing.
|
|||||||
|
|
||||||
Tests that assert "the same key signs the same message identically"
|
Tests that assert "the same key signs the same message identically"
|
||||||
must either pin to the Vitest path (Node WebCrypto) or be replaced
|
must either pin to the Vitest path (Node WebCrypto) or be replaced
|
||||||
with verify-after-sign assertions. The Phase 6 Playwright spec uses
|
with verify-after-sign assertions. The Playwright spec for the
|
||||||
the verify path, which works on every engine in the baseline.
|
keystore uses the verify path, which works on every engine in the
|
||||||
|
baseline.
|
||||||
|
|
||||||
## IndexedDB schema
|
## IndexedDB schema
|
||||||
|
|
||||||
@@ -113,13 +114,13 @@ wipes every namespace.
|
|||||||
Namespaces in current use:
|
Namespaces in current use:
|
||||||
|
|
||||||
| Namespace | Key | Value type | Owner |
|
| Namespace | Key | Value type | Owner |
|
||||||
|--------------------|--------------------------------|-----------------------------------------------|------------------------------------|
|
|--------------------|--------------------------------|------------------------------------------------|--------------------------|
|
||||||
| `session` | `device-session-id` | `string` | Phase 7+ |
|
| `session` | `device-session-id` | `string` | `auth-flow.md` |
|
||||||
| `game-prefs` | `{gameId}/wrap-mode` | `WrapMode` | Phase 11+ (`game-state.md`) |
|
| `game-prefs` | `{gameId}/wrap-mode` | `WrapMode` | `game-state.md` |
|
||||||
| `game-prefs` | `{gameId}/last-viewed-turn` | `number` | Phase 11+ (`game-state.md`) |
|
| `game-prefs` | `{gameId}/last-viewed-turn` | `number` | `game-state.md` |
|
||||||
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` | Phase 12+ (`order-composer.md`) |
|
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` | `order-composer.md` |
|
||||||
| `game-history` | `{gameId}/turn/{N}` | `GameReport` | Phase 26+ (`game-state.md`) |
|
| `game-history` | `{gameId}/turn/{N}` | `GameReport` | `game-state.md` |
|
||||||
| `game-map-toggles` | `{gameId}` | `{toggles: MapToggles; lastResetTurn: number}` | Phase 29+ (`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
|
The `game-map-toggles` blob stores the gear popover's per-game
|
||||||
visibility state plus a `lastResetTurn` companion field. Reading
|
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
|
always greets the player with every map category visible (see
|
||||||
`game-state.md` for the new-turn-reset contract).
|
`game-state.md` for the new-turn-reset contract).
|
||||||
|
|
||||||
Later phases will add more per-feature namespaces (fixtures, lobby
|
Additional per-feature namespaces will be added as needed (fixtures,
|
||||||
snapshot, etc.). The contract is namespace-strings stay scoped to
|
lobby snapshot, etc.). The contract is namespace-strings stay scoped
|
||||||
one feature; cross-feature reads through the cache are by convention
|
to one feature; cross-feature reads through the cache are by
|
||||||
disallowed.
|
convention disallowed.
|
||||||
|
|
||||||
## Keystore lifecycle
|
## Keystore lifecycle
|
||||||
|
|
||||||
@@ -176,9 +177,8 @@ Thin orchestration layer over `KeyStore` + `Cache`:
|
|||||||
- `loadDeviceSession(keyStore, cache)` returns
|
- `loadDeviceSession(keyStore, cache)` returns
|
||||||
`{ keypair, deviceSessionId }`. The `keypair` field is always
|
`{ keypair, deviceSessionId }`. The `keypair` field is always
|
||||||
populated (loaded if present, freshly generated if not). The
|
populated (loaded if present, freshly generated if not). The
|
||||||
`deviceSessionId` field is `null` until Phase 7's
|
`deviceSessionId` field is `null` until the `confirm-email-code`
|
||||||
`confirm-email-code` handler stores the gateway-issued id via
|
handler stores the gateway-issued id via `setDeviceSessionId`.
|
||||||
`setDeviceSessionId`.
|
|
||||||
- `setDeviceSessionId(cache, id)` writes the id to the `session`
|
- `setDeviceSessionId(cache, id)` writes the id to the `session`
|
||||||
namespace.
|
namespace.
|
||||||
- `clearDeviceSession(keyStore, cache)` wipes both the keypair and
|
- `clearDeviceSession(keyStore, cache)` wipes both the keypair and
|
||||||
@@ -186,7 +186,7 @@ Thin orchestration layer over `KeyStore` + `Cache`:
|
|||||||
push-event-driven revocation path.
|
push-event-driven revocation path.
|
||||||
|
|
||||||
A `null` `deviceSessionId` is the signal that the session is
|
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
|
## Test layout
|
||||||
|
|
||||||
@@ -211,7 +211,8 @@ the debug entry point never attaches `window.__galaxyDebug`.
|
|||||||
|
|
||||||
## Future: native targets
|
## 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,
|
Keychain on macOS / iOS, DPAPI/Credential Locker on Windows,
|
||||||
libsecret on Linux, Android Keystore on Android — behind the same
|
libsecret on Linux, Android Keystore on Android — behind the same
|
||||||
`KeyStore` interface, plus SQLite-backed `Cache` adapters. The web
|
`KeyStore` interface, plus SQLite-backed `Cache` adapters. The web
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# UI sync protocol
|
# UI sync protocol
|
||||||
|
|
||||||
Phase 25 wires the transport-level policy that keeps the local
|
The transport-level policy keeps the local order draft consistent
|
||||||
order draft consistent with the gateway across two failure modes
|
with the gateway across two failure modes: transient network
|
||||||
that Phase 14 punted on: transient network outages and turn
|
outages and turn cutoffs the player did not anticipate. The wiring
|
||||||
cutoffs the player did not anticipate. The wiring also reacts to
|
also reacts to admin-initiated game pauses signalled by the
|
||||||
admin-initiated game pauses signalled by the new `game.paused`
|
`game.paused` push event.
|
||||||
push event.
|
|
||||||
|
|
||||||
The contract lives at three layers:
|
The contract lives at three layers:
|
||||||
|
|
||||||
@@ -210,8 +209,7 @@ state machine deterministically.
|
|||||||
status flip on the lobby side without an explicit push event.
|
status flip on the lobby side without an explicit push event.
|
||||||
The UI relies on the next `game.turn.ready` for recovery; a
|
The UI relies on the next `game.turn.ready` for recovery; a
|
||||||
dedicated `game.resumed` event would let the banner clear
|
dedicated `game.resumed` event would let the banner clear
|
||||||
immediately without waiting for the next cron tick. Not part
|
immediately without waiting for the next cron tick.
|
||||||
of this phase.
|
|
||||||
- The conflict banner shows the player-facing template
|
- The conflict banner shows the player-facing template
|
||||||
unmodified; a future revision may interpolate the explicit
|
unmodified; a future revision may interpolate the explicit
|
||||||
cutoff timestamp once the server adds it to the error body.
|
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 /
|
UI client test toolchain. Project-wide testing layers (service /
|
||||||
inter-service / system) live in [`../../docs/TESTING.md`](../../docs/TESTING.md);
|
inter-service / system) live in [`../../docs/TESTING.md`](../../docs/TESTING.md);
|
||||||
this doc only covers the UI-specific tiers added in Phase 2 of
|
this doc covers the UI-specific tiers.
|
||||||
[`../PLAN.md`](../PLAN.md).
|
|
||||||
|
|
||||||
## Tier 1 — per-PR
|
## 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*`).
|
Triggered by `.gitea/workflows/ui-release.yaml` on tag push (`v*`).
|
||||||
Currently mirrors the Tier 1 step set; the dedicated release-only
|
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
|
`ui/frontend/tests/__snapshots__/` until the project shifts to
|
||||||
Argos or another visual-diff service.
|
Argos or another visual-diff service.
|
||||||
- **iOS smoke (Capacitor + Appium)** — Phase 32. Runs on a `macos-13`
|
- **iOS smoke (Capacitor + Appium)** — planned (see ../ROADMAP.md).
|
||||||
runner once the Capacitor mobile wrapper exists.
|
Runs on a `macos-13` runner once the Capacitor mobile wrapper
|
||||||
|
exists.
|
||||||
|
|
||||||
Both blocks are present as commented sections in
|
Both blocks are present as commented sections in
|
||||||
`.gitea/workflows/ui-release.yaml` with the phase number that
|
`.gitea/workflows/ui-release.yaml`.
|
||||||
re-enables them.
|
|
||||||
|
|
||||||
## Local execution
|
## Local execution
|
||||||
|
|
||||||
@@ -150,8 +150,7 @@ In synthetic mode:
|
|||||||
same synthetic id afterwards redirects to /lobby. Re-load the JSON
|
same synthetic id afterwards redirects to /lobby. Re-load the JSON
|
||||||
to reseed.
|
to reseed.
|
||||||
|
|
||||||
The synthetic-report parity rule (see [`../PLAN.md`](../PLAN.md) §
|
The synthetic-report parity rule requires every change that extends
|
||||||
Assumptions and Defaults) requires every UI phase that extends
|
|
||||||
`decodeReport` to also extend the legacy parser in lockstep, or to
|
`decodeReport` to also extend the legacy parser in lockstep, or to
|
||||||
record in the parser's `README.md` that the new field cannot be
|
record in the parser's `README.md` that the new field cannot be
|
||||||
derived from legacy text. This keeps the synthetic-mode coverage in
|
derived from legacy text. This keeps the synthetic-mode coverage in
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ keystore — see [`storage.md`](storage.md) for the web implementation
|
|||||||
|
|
||||||
Two viable Go-to-WASM toolchains exist:
|
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 |
|
| **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 |
|
| 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` was written under the TinyGo invariants documented in
|
||||||
@@ -108,7 +108,7 @@ TinyGo being installed in every environment.
|
|||||||
|
|
||||||
| Build | Date | Size |
|
| 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
|
If the artefact ever crosses the 1 MB target, profile via
|
||||||
`tinygo build -size full` and trim before committing.
|
`tinygo build -size full` and trim before committing.
|
||||||
|
|||||||
Reference in New Issue
Block a user