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