Commit Graph

359 Commits

Author SHA1 Message Date
Ilia Denisov 070fdc0ee5 update gitattributes
ui-test / test (push) Failing after 38s
2026-05-11 22:18:16 +02:00
Ilia Denisov e98e6bda73 ui/phase-25: mark stage done after local-ci run 5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:07:03 +02:00
Ilia Denisov 2ca47eb4df ui/phase-25: backend turn-cutoff guard + auto-pause + UI sync protocol
Backend now owns the turn-cutoff and pause guards the order tab
relies on: the scheduler flips runtime_status between
generation_in_progress and running around every engine tick, a
failed tick auto-pauses the game through OnRuntimeSnapshot, and a
new game.paused notification kind fans out alongside
game.turn.ready. The user-games handlers reject submits with
HTTP 409 turn_already_closed or game_paused depending on the
runtime state.

UI delegates auto-sync to a new OrderQueue: offline detection,
single retry on reconnect, conflict / paused classification.
OrderDraftStore surfaces conflictBanner / pausedBanner runes,
clears them on local mutation or on a game.turn.ready push via
resetForNewTurn. The order tab renders the matching banners and
the new conflict per-row badge; i18n bundles cover en + ru.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:00:16 +02:00
Ilia Denisov bbdcc36e05 ui/phase-24: declare game.turn.ready as JSON-friendly catalog kind
ui-test / test (push) Failing after 40s
TestBuildClientPushEventCoversCatalog required every catalog kind to
encode through a FlatBuffers `preMarshaledEvent`. game.turn.ready
intentionally rides on the JSON fallback because its payload is just
`{game_id, turn}` and the only consumer (Phase 24 UI handler) parses
JSON inline. Make the policy explicit through a jsonFriendlyKinds
allow-list so the test still asserts each kind is covered and a future
producer that picks the wrong encoding fails loudly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 17:27:29 +02:00
Ilia Denisov 5b07bb4e14 ui/phase-24: push events, turn-ready toast, single SubscribeEvents consumer
Wires the gateway's signed SubscribeEvents stream end-to-end:

- backend: emit game.turn.ready from lobby.OnRuntimeSnapshot on every
  current_turn advance, addressed to every active membership, push-only
  channel, idempotency key turn-ready:<game_id>:<turn>;
- ui: single EventStream singleton replaces revocation-watcher.ts and
  carries both per-event dispatch and revocation detection; toast
  primitive (store + host) lives in lib/; GameStateStore gains
  pendingTurn/markPendingTurn/advanceToPending and a persisted
  lastViewedTurn so a return after multiple turns surfaces the same
  "view now" affordance as a live push event;
- mandatory event-signature verification through ui/core
  (verifyPayloadHash + verifyEvent), full-jitter exponential backoff
  1s -> 30s on transient failure, signOut("revoked") on
  Unauthenticated or clean end-of-stream;
- catalog and migration accept the new kind; tests cover producer
  (testcontainers + capturing publisher), consumer (Vitest event
  stream, toast, game-state extensions), and a Playwright e2e
  delivering a signed frame to the live UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 16:16:31 +02:00
Ilia Denisov 5a2a977dc6 ui/phase-23: mark stage done after local-ci run 2
ui-test / test (push) Failing after 2m11s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:41:35 +02:00
Ilia Denisov c58027c034 ui/phase-23: turn-report view with twenty sections and TOC
Replaces the Phase 10 report stub with a scrollable orchestrator that
renders every FBS array as a dedicated section (galaxy summary, votes,
player status, my/foreign sciences, my/foreign ship classes, battles,
bombings, approaching groups, my/foreign/uninhabited/unknown planets,
ships in production, cargo routes, my fleets, my/foreign/unidentified
ship groups). A sticky table of contents (a <select> on mobile),
"back to map" affordance, IntersectionObserver-driven active-section
highlight, and SvelteKit Snapshot-based scroll save/restore round out
the view.

GameReport gains six new fields (players, otherScience, otherShipClass,
battleIds, bombings, shipProductions); decodeReport, the synthetic-
report loader, the e2e fixture builder, and EMPTY_SHIP_GROUPS extend
in lockstep. ~90 new i18n keys land in en + ru together.

The legacy-report parser is extended to populate the new sections from
the dg/gplus text formats (Your Sciences, <Race> Sciences, <Race> Ship
Types, Bombings, Ships In Production). Ships-in-production prod_used
is derived through a new pkg/calc.ShipBuildCost helper; the engine's
controller.ProduceShip refactors to call the same helper without any
behaviour change (engine tests stay unchanged and green). Battles
remain in the parser's Skipped list — the legacy text carries no
stable per-battle UUID.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:33:56 +02:00
Ilia Denisov 81d8be08b2 phase 22 2026-05-11 11:38:40 +02:00
Ilia Denisov e2a4790f6c ui/phase-22: skip the no-op stance click in the races table
Clicking the already-active WAR/PEACE button still appended a
\`setDiplomaticStance\` whose \`relation\` matched the row's current
value. The engine would accept the duplicate harmlessly, but the
order tab inflates with rows that say nothing and every auto-sync
re-ships the redundant payload. Compare against the overlayed
stance (so a queued-but-not-applied change suppresses a re-click
that matches the *intended* state, not just the server snapshot)
and short-circuit when they agree. Mirrors the vote picker, which
already had the same guard.

vitest.config.ts: \`mergeConfig\` refuses callback-form base
configs, so resolve \`vite.config.ts\`'s callback with the test
context first and merge the plain object. Surfaced after the
\`loadEnv\` migration switched the root config to the callback
form.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 11:19:57 +02:00
Ilia Denisov c0382117b8 ui: read dev-server config from .env files and add VITE_DEV_HOST opt-in
`vite.config.ts` read `VITE_DEV_PROXY_TARGET` /
`VITE_DEV_GRPC_PROXY_TARGET` straight from `process.env`, so the
gateway-override knob only worked when the variable was exported in
the shell that ran `pnpm dev`. Per-developer `.env.development.local`
files (the documented way to override) were silently ignored by the
config: Vite auto-populates `import.meta.env` for client code from
those files, but the config itself runs in Node and has to call
`loadEnv` explicitly.

Switch the config to the function-form + `loadEnv` so every
`VITE_*` entry in any `.env*` file reaches both client code and the
config. Now adding `VITE_DEV_PROXY_TARGET=http://localhost:18080` to
`.env.development.local` actually retargets the proxy, no shell
gymnastics required.

While there, introduce `VITE_DEV_HOST` as an opt-in for wider
listener binding: unset (default) keeps Vite's loopback-only
behaviour; `true`/`1`/`yes` flips to "all interfaces" (`0.0.0.0` +
IPv6); any other string is passed through verbatim to pin a
specific LAN address. Useful when reaching the dev server through
SSH port forwarding, a VM, or a container needs a non-loopback
bind, and intentionally opt-in so an unattended `pnpm dev` on a
laptop never exposes the unauthenticated dev surface to the LAN by
accident.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:46:08 +02:00
Ilia Denisov 5867afd168 local-dev: parameterize host-port mappings via LOCAL_DEV_*_PORT
The compose stack hard-coded host ports (postgres 5433, redis 6380,
mailpit 8025, gateway REST 8080, gateway gRPC 9090) — fine for a
clean dev machine, painful when those ports collide with other
services on the same host (e.g. a `crowdsec` sitting on
127.0.0.1:8080 or a Prometheus instance on :9090).

Every host-port mapping is now `${LOCAL_DEV_*_PORT:-<old-default>}`,
so the defaults match prior behaviour for everyone and a per-host
override is a single environment variable away. `.env` carries the
overrides as commented-out lines so the customisation surface is
discoverable without grepping the compose file. README's
"Port 8080 already in use" troubleshooting entry now points at the
new variables and the optional `docker-compose.override.yml`
workflow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:23:42 +02:00
Ilia Denisov 9111dd955a ui/phase-22: races table with stance toggle and vote slot
Adds the Races View in the in-game shell. The table lists every
non-extinct other race with tech levels (percent), totals,
planets, votes received, and a per-row WAR | PEACE segmented
control. A single vote-recipient slot above the table queues a
`CommandRaceVote`; per-row buttons queue `CommandRaceRelation`.
Both commands flow through the existing order draft store with
collapse-by-acceptor (stance) and singleton (vote) rules.

`GameReport` widens with `races`, `myVotes`, `myVoteFor`; the
decoder walks `report.player[]` once for the richer projection.
The optimistic overlay flips stance and vote target immediately;
`votesReceived`, `myVotes`, and the alliance summary stay
server-authoritative — alliance grouping and the 2/3 victory
check are tallied on the server at turn cutoff and explicitly
not surfaced client-side (`rules.txt` keeps foreign races'
outgoing vote targets private).

Includes Vitest component coverage of stance and vote
collapse rules + a Playwright e2e that drives both commands
through the dispatcher route and verifies the gateway saw the
expected `CommandRaceRelation` / `CommandRaceVote` payloads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 01:52:23 +02:00
Ilia Denisov 7a7f2e4b98 chore: claude settings 2026-05-11 01:10:32 +02:00
Ilia Denisov 9c29f03d66 ui/phase-21: make MapView's mounted flag reactive
The renderer-mount effect in `lib/active-view/map.svelte` reads
`mounted` to gate the runSerializedMount call, but the variable was
declared as a plain `let`, not `$state`. On the first navigation to
/map this is benign: the effect's first pass returns early (gameState
still hydrating, `report` null), and once `report` arrives the
effect re-fires — by which point `onMount` has already flipped
`mounted = true`.

On every subsequent return to /map the report is already loaded by
the long-lived gameState in the layout. The effect therefore makes
exactly one pass on the freshly-mounted component, gates on
`mounted === false` (the brand-new instance has not run `onMount`
yet), and never wakes up again because no tracked state changes
afterwards. Symptom: black canvas — fresh DOM, no mount-error
overlay, but Pixi never rebuilt the world on the new canvas.

Convert `mounted` to `$state(false)` so flipping it true inside
`onMount` triggers the effect's second pass, which now finds all
preconditions satisfied and proceeds to `runSerializedMount`. The
detailed lifecycle reasoning is preserved as a code comment so the
next reader can see why this one variable must be reactive.

Add tests/e2e/map-roundtrip.spec.ts: navigates /map → {report,
ship-class designer, science designer, mail} → /map for each
non-map view, then asserts the renderer republished primitives onto
the DEV `__galaxyDebug.getMapPrimitives()` surface. The pre-fix
build failed every variant; the patch lands all four green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:58:32 +02:00
Ilia Denisov 85ea6f413e local-dev: thread pkg/calc into the dockerfile build context
Commit 408097e ('feat: move func to calc package') moved a helper
into pkg/calc and made pkg/util/map.go import galaxy/calc, but the
local-dev backend / gateway Dockerfiles never picked up the new
module. The synthesised go.work has no replace directive for
galaxy/calc and the build context never copies pkg/calc, so any
backend / gateway image rebuild fails with

    galaxy/calc@v0.0.0: malformed module path "galaxy/calc": missing
    dot in first path element

Add the missing COPY, the matching `use ./pkg/calc` line, and the
`galaxy/calc v0.0.0 => ./pkg/calc` replace to both local-dev
Dockerfiles. The local-dev stack now rebuilds cleanly and the
auto-heal flow (prune-broken-engines + pre-bootstrap reconciler
tick) finishes by spawning a fresh engine container for the new
sandbox game.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:45:54 +02:00
Ilia Denisov ff53cc0ad3 local-dev: prune broken engines on rebuild + document one-time bake
`make rebuild` runs `compose build --no-cache backend gateway` plus
a fresh `up -d --wait`. It must therefore also reap any engine
container whose bind-mount source went away during host downtime,
otherwise the new backend image boots into a stack with the same
orphan that triggered the heal flow in the first place.

Also extend the troubleshooting note: pulling the heal-cycle fix
requires one explicit `make rebuild` so the backend image picks up
the pre-bootstrap reconciler tick. Without that, `make up` runs
the new Makefile target but the legacy backend cannot follow
through, and the developer is left staring at a `cancelled`
sandbox with no running replacement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:40:27 +02:00
Ilia Denisov edc9709bd6 local-dev: auto-recreate engine containers when bind-mount disappears
After a host reboot macOS clears /private/tmp, so the per-game
bind-mount source under /tmp/galaxy-game-state/<uuid> vanishes and
Docker refuses to restart the long-lived engine container under
`restart: unless-stopped`. The container then sits in `exited` state
and the dev sandbox is unreachable until the developer manually rms
it and runs `make up` twice.

Fix `make -C tools/local-dev up` to heal this in one cycle:

1. `prune-broken-engines` (new make target wired into `up`) walks
   every container labelled `galaxy-game-engine` and removes the ones
   not in `running` / `restarting` state. Healthy long-lived
   containers survive normal up/down cycles untouched.
2. The backend now runs a single reconciliation pass before the
   dev-sandbox bootstrap (`Reconciler().Tick(ctx)` in main.go).
   Without it, bootstrap would reuse the soon-to-be-cancelled game
   that the periodic ticker is about to mark `removed`. The pre-tick
   cascades the orphan runtime row through markRemoved → lobby
   cancel before bootstrap purges terminal sandbox games and creates
   a fresh one — so a single `make up` lands a working sandbox with
   a brand new state directory.

README troubleshooting section documents the symptom and the
recovery so the bind-mount-source error message is greppable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:27:31 +02:00
Ilia Denisov 5a3bec5acd ui/phase-21: bump done marker to local-ci run 30
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:10:01 +02:00
Ilia Denisov e55355a2cf ui/phase-21: harden applyOrderOverlay against HMR-stale localScience
Fixes a black-canvas regression on /map after creating a science in
DEV: when Vite hot-reloads the decoder bump that adds the
`localScience` field, the live in-memory `gameState.report` keeps
its older shape with no such field, so the overlay's
`[...report.localScience]` throws inside the reactive getter and
silently aborts the map view's `$effect`. The fix wraps the spread
and the final return in `?? []` defaults — and matches the
ship-class branches for symmetry — so the overlay stays well-defined
for any partial report shape upstream consumers may carry across an
HMR boundary.

Also adds order-overlay regression tests covering the createScience /
removeScience branches plus the explicit HMR-stale shape, and a
Playwright e2e (sciences-map-regress.spec.ts) replaying the
user-reported flow: /map → /designer/science → save → /map, asserting
no map-mount-error overlay and no console errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:00:03 +02:00
Ilia Denisov f674c86e4b ui/phase-21: mark stage as done after local-ci run 29
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 21:42:29 +02:00
Ilia Denisov 7bea22b0b5 ui/phase-21: sciences CRUD list, designer, and production-picker integration
Lights up the player-defined sciences feature: a table view with sort
and filter, a designer with four percent inputs and a strict
sum-equals-100 gate, and a Research-sub-row integration so the
planet production picker lists the user's sciences alongside the
four tech buttons. Phase 21 decisions are baked back into ui/PLAN.md
(no UpdateScience on the wire — write-once via createScience +
removeScience; percentages instead of fractions; sciences live under
the existing Research segment).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 21:32:37 +02:00
Ilia Denisov 0509f2cde2 ui/phase-20: bump done marker to local-ci run 28
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:06:20 +02:00
Ilia Denisov 54733bfb14 ui/phase-20: lock after Send + dashed tracks for in-flight & pending sends
Send joins Modernize / Dismantle / Transfer as a lockable command:
once any of the four lands in the draft for a group, every action
button on its inspector is disabled with a "command pending"
tooltip and the banner names the queued kind. Load / Unload /
Split / Join Fleet stay non-locking — they stack legitimately on
the engine side.

Two dashed overlays now run alongside the cargo-route arrows:

- Yellow dashed track for own in-space groups, drawn from the
  origin planet to the destination (matches the in-space point
  colour so eye reads both as one entity).
- Green dashed track for every wire-valid sendShipGroup command
  in the order draft, drawn from the source group's orbit planet
  to the chosen destination. Disappears when the command is
  removed from the order tab, when the engine rejects it, or
  when the group has left orbit (in-space track replaces it).

Both tracks are wrap-aware via torusShortestDelta and never
participate in hit-test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:55:43 +02:00
Ilia Denisov 2d201537ee ui/phase-20: bump done marker to local-ci run 27
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:30:46 +02:00
Ilia Denisov ac14eaff10 ui/phase-20: pick-first Send + lock after Modernize/Dismantle/Transfer
Send no longer carries a destination control inside the form: a
click on the action drops the inspector straight into map-pick
mode, and the form (ship count + confirm) only mounts after the
player chooses a destination. Cancelling the picker leaves no
form behind.

A queued Modernize / Dismantle / Transfer for a given group
locks every action button on its inspector and surfaces a banner
that points the player at the order list. Cancelling the queued
entry from the order tab releases the lock on the next render —
the derivation watches draft.commands directly. Send / Load /
Unload / Split / Join Fleet do not lock; Send is naturally
followed by an out-of-orbit state at turn cutoff, the rest can
stack legitimately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:20:48 +02:00
Ilia Denisov de824dfc9a ui/phase-20: mark stage as done after local-ci run 26
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:38:16 +02:00
Ilia Denisov 3626998a33 ui/phase-20: ship-group inspector actions
Eight ship-group operations land on the inspector behind a single
inline-form panel: split, send, load, unload, modernize, dismantle,
transfer, join fleet. Each action either appends a typed command to
the local order draft or surfaces a tooltip explaining the
disabled state. Partial-ship operations emit an implicit
breakShipGroup command before the targeted action so the engine
sees a clean (Break, Action) pair on the wire.

`pkg/calc.BlockUpgradeCost` migrates from
`game/internal/controller/ship_group_upgrade.go` so the calc
bridge can wrap a pure pkg/calc formula; the controller now
imports it. The bridge surfaces the function as
`core.blockUpgradeCost`, which the inspector calls once per ship
block to render the modernize cost preview.

`GameReport.otherRaces` is decoded from the report's player block
(non-extinct, ≠ self) and feeds the transfer-to-race picker. The
planet inspector's stationed-ship rows become clickable for own
groups so the actions panel is reachable from the standard click
flow (the renderer continues to hide on-planet groups).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:27:55 +02:00
Ilia Denisov f7109af55c ui/phase-19: torus-aware incoming track + on-planet groups in inspector
Two follow-up fixes after the initial Phase 19 landing:

  1. The IncomingGroup dashed trajectory was drawn between raw
     (x1, y1) and (x2, y2) world coordinates. On torus wrap mode
     this took the long way around when origin and destination
     sat near opposite seams. The line now picks endpoints via
     `torusShortestDelta` so the segment crosses the seam when
     that's the shorter visual path. The interpolated clickable
     point follows the same unwrapped vector. The same helper
     fixes the in-hyperspace position for local / foreign groups.
  2. On-planet local and foreign groups previously rendered as
     small offset points next to every populated planet, which
     turned the canvas into noise as soon as a player held more
     than a handful of planets. The map no longer renders any
     in-orbit group; the planet inspector grows a compact
     "stationed ship groups" subsection
     (`lib/inspectors/planet/ship-groups.svelte`) that lists
     each in-orbit group as a row of `<race> · <class> · <count>
     ships · <mass>`. Race attribution: LocalGroup → the player's
     race, OtherGroup on a foreign-owned planet → the planet's
     owner, OtherGroup elsewhere → "foreign" placeholder. Rows
     are non-interactive in Phase 19; Phase 21+ will deep-link
     into the ship-groups table view with a (planet, race) filter.

Tests:
  - `state-binding-groups.test.ts` swaps the on-planet rendering
    expectation for the new "no map primitive" rule, and adds a
    regression that asserts the incoming line crosses the torus
    seam via `torusShortestDelta`.
  - new `inspector-planet-ship-groups.test.ts` covers row
    composition, the destination-mismatch filter, the
    in-hyperspace exclusion, the foreign-planet owner fallback,
    and the empty-state collapse.
  - `inspector-planet.test.ts` and `inspector-ship-group.spec.ts`
    pick up the new prop chain (`localShipGroups`,
    `otherShipGroups`, `localRace`).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:08:41 +02:00
Ilia Denisov d63fe44618 pkg/calc: fix Deltas wrap on rectangular maps + add signed ShortestDelta
The pre-existing `Deltas` helper used the height to wrap the x-axis,
which silently produced wrong values on any rectangular galaxy
(`w != h`). Square galaxies — the only configuration the engine
ships today — masked the bug, so it stayed in tree.

`Deltas` is now a thin wrapper around the new `ShortestDelta(a, b,
size)`, which returns the signed per-axis shortest delta on a 1-D
circle (range `(-size/2, size/2]`). The signed flavour is what the
Phase 19 ship-group renderer needs to draw an IncomingGroup
trajectory across the torus seam; `Deltas` continues to return the
pair of absolute deltas for distance computation.

Adds `pkg/calc/map_test.go` with table-driven coverage for both
helpers, including a regression that exercises the rectangular
case the bug was hiding behind, and the half-circumference
tie-break.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:08:16 +02:00
Ilia Denisov 408097e3aa feat: move func to calc package 2026-05-10 14:55:14 +02:00
Ilia Denisov 92413575f3 ui/phase-19: mark stage as done after local-ci run 24
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:48:30 +02:00
Ilia Denisov 3694847792 ui/phase-19: seed an authenticated session in the synthetic-report e2e
The root +layout.svelte redirects anonymous traffic to /login, so a
fresh CI browser context never gets to render the lobby's
DEV-gated synthetic-report section — the previous spec relied on
leftover session state in the local browser and silently broke on
clean runners (local-ci run 23).

Bootstrap the session through /__debug/store before navigating to
/lobby: load a device keypair, set a deterministic device session
id. The synthetic flow itself still bypasses the gateway entirely;
the seed only ensures `session.status === "authenticated"` so the
layout guard lets the lobby through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:39:08 +02:00
Ilia Denisov 86e77efe39 ui/phase-19: read-only ship-group inspector + sheet + tab dispatch
Closes Phase 19's UI surface. The inspector dispatches on the
selection variant: local / other groups render class, count, the
four tech levels, mass, cargo (type + amount when loaded),
location (planet name on-orbit, from/to/distance in hyperspace),
and — for local groups only — fleet membership + state. Incoming
groups surface origin / destination / distance / speed and the
inline ETA = ceil(distance / speed); zero speed collapses to the
designer's existing "—" placeholder. Unidentified groups render
just the (x, y) coordinates and the no-data hint, mirroring the
unidentified planet treatment.

Layout / inspector-tab plumbing:
  - inspector-tab.svelte derives selectedShipGroup against the
    rendered report and mounts <ShipGroup /> when the planet
    branch doesn't match. Stale refs (an index that no longer
    resolves after a turn refresh) collapse cleanly to the empty
    state.
  - +layout.svelte mounts <ShipGroupSheet /> alongside the
    existing planet sheet on mobile; both share the
    `effectiveTool === "map"` guard and clear-on-close.

i18n: en + ru both grow ~30 keys under
`game.inspector.ship_group.*`. Adding a key to one without the
other is a TS error (TranslationKey is `keyof typeof en`), so the
Russian mirror stays mandatory.

Tests:
  - inspector-ship-group.test.ts exercises every variant —
    on-planet local, in-hyperspace local, cargo-loaded local,
    foreign, incoming with ETA, incoming with zero speed,
    unidentified, plus the missing-planet `#NN` fallback.
  - tests/e2e/inspector-ship-group.spec.ts is a smoke spec that
    drives the DEV-only synthetic-report loader from /lobby
    through navigation to /games/synthetic-XXX/map.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:24:17 +02:00
Ilia Denisov 676556db4e ui/phase-19: ship-group decoder + map binding + selection store
Wires Phase 19's data and rendering layers without yet adding the
inspector UI:

  - game-state.ts grows ReportLocalShipGroup / ReportOtherShipGroup
    / ReportIncomingShipGroup / ReportUnidentifiedShipGroup /
    ReportLocalFleet types and walks the matching FlatBuffers
    vectors (LocalGroup, OtherGroup, IncomingGroup,
    UnidentifiedGroup, LocalFleet) inside decodeReport. The Tech
    map is folded into the fixed-shape ShipGroupTech struct;
    cargo strings normalise to the closed CargoLoadType | "NONE"
    union; UUIDs come back as canonical 36-char strings.
  - synthetic-report.ts mirrors the new fields so the DEV-only
    lobby loader can feed JSON produced by legacy-report-to-json
    straight into the live UI surface.
  - selection.svelte.ts widens its discriminated union with a
    `kind: "shipGroup"` branch carrying a ShipGroupRef
    (local UUID / other / incoming / unidentified by index).
  - world.ts adds Style.strokeDashPx and render.ts.drawLine
    honours it via manual segmentation (PixiJS v8 has no native
    dash API). Ignored on points and circles.
  - state-binding.ts now returns { world, hitLookup }: the
    hit-lookup map keys every primitive id back to a concrete
    HitTarget so the click handler can dispatch to selectPlanet
    or selectShipGroup. Ship-group primitives live in a separate
    ship-groups.ts that emits one point per local / other /
    unidentified group, plus a dashed origin→destination line +
    clickable point per incoming group. Position is interpolated
    along the trajectory for in-hyperspace groups.
  - map.svelte threads the hitLookup into handleMapClick.

Vitest:
  - tests/helpers/empty-ship-groups.ts exposes EMPTY_SHIP_GROUPS
    so existing fixtures can spread the new five empty arrays
    without enumerating every field.
  - state-binding-groups.test.ts covers each group variant's
    primitive geometry and lookup correctness.
  - All previously-existing fixture builders pick up the spread
    so GameReport stays a complete object.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:23:56 +02:00
Ilia Denisov 8839f46c25 ui/phase-19: legacy parser learns Your Groups / Your Fleets / Incoming Groups
The parity rule from ui/PLAN.md says every UI phase that decodes a
new Report field must extend the legacy converter in lockstep.
Phase 19 brings ship groups (LocalGroup / OtherGroup /
UnidentifiedGroup / IncomingGroup) and LocalFleet onto the wire-
compatible UI surface; this commit teaches
tools/local-dev/legacy-report to populate the three sections that
exist in the legacy text format:

  - "Your Groups" → []LocalGroup. Cargo type, load, fleet name,
    state, on-planet vs hyperspace position (origin / range) all
    decoded; LocalGroup.ID is synthesised deterministically from
    the per-report group index so re-running the converter
    produces byte-identical JSON. Speed is left zero — the legacy
    table doesn't expose it.
  - "Your Fleets" → []LocalFleet. Origin / range / state mirror
    the row layout used by Killer / Tancordia variants; gplus's
    state-less rows still resolve.
  - "Incoming Groups" → []IncomingGroup. Origin / destination
    names — and `#NN` by-id references — resolve against the
    parsed planet tables. Because the section can land before
    "Your Planets" in some engines, group / fleet / incoming rows
    are buffered and resolved in `parser.finish` after every
    planet is known.

Battles, OtherGroup (only ever in battle rosters), and
UnidentifiedGroup stay out of scope — README.md spells out what
remains not-derivable.

Adds Killer031–033 / TSERCON_Z032–033 / Tancordia036–039 fixtures
to the dg directory and exercises three of them through new
TestParseDg{Killer031,Tancordia037,KNNTS041} smoke tests, plus
inline tests for each new section parser. Drops the stale
KNNTS039.json artefact left over from Phase 18 development.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:23:17 +02:00
Ilia Denisov 132ed4e0db feat: load legary reports 2026-05-10 12:16:08 +02:00
Ilia Denisov f5ac9fac59 ui/synthetic-report: PLAN parity rule + testing doc
Locks in the synthetic-report parity rule as a global "Assumptions
and Defaults" entry in ui/PLAN.md: every phase that extends the
server->UI report contract must also extend the legacy parser in
the same PR (or document in tools/local-dev/legacy-report/README.md
why the new field cannot be derived from legacy text). The Go side
already enforces shape compatibility via the pkg/model/report
import; this rule extends that mechanical guard to "did we remember
to wire the new field through".

ui/docs/testing.md grows a "Synthetic reports for visual testing"
section with the full conversion -> load -> compose loop and the
two operational gotchas (no network on synthetic ids, page reload
clears the in-memory map).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:08:13 +02:00
Ilia Denisov 8f320010c6 ui/synthetic-report: dev-only legacy report loader on lobby
Adds api/synthetic-report.ts, an in-memory registry + JSON->GameReport
decoder for synthetic-mode game sessions. The lobby grows a
import.meta.env.DEV-gated "Synthetic test reports" section with a
JSON file picker; loading a file registers the decoded report under
a synthetic-<uuid> id and navigates to /games/<id>/map.

The in-game shell layout detects the synthetic id range, takes the
report straight from the registry via gameState.initSynthetic, and
deliberately skips both galaxyClient.set and orderDraft.bindClient.
Order auto-sync stays silent: scheduleSync already short-circuits on
non-UUID game ids, and without a bound client the network path is
unreachable. applyOrderOverlay continues to project locally-valid
draft commands onto the rendered report so renames / production
choices / route edits are visible immediately.

A page reload loses the in-memory entry and redirects to /lobby —
synthetic mode is a debug affordance, not a session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:08:05 +02:00
Ilia Denisov 99962b295f tools/local-dev: legacy-report-to-json CLI for synthetic UI testing
A Go module under tools/local-dev/legacy-report that converts the
"dg" / "gplus" engine .REP files in tools/local-dev/reports/ into the
JSON shape of pkg/model/report.Report. The output drives a DEV-only
synthetic-mode loader on the UI lobby so the map, inspectors, and
order-overlay can be exercised against rich game states without
playing many turns end-to-end.

Scope is intentionally narrow: only the fields the UI client decodes
today (planets, players, own ship classes, header). Importing
pkg/model/report keeps the parser and the typed contract in lockstep
— any backwards-incompatible schema change breaks the tool's
compilation before it ships. The README spells out the parity rule
for extending the parser alongside future UI decoders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:07:50 +02:00
Ilia Denisov e0e0f00daf chore: legacy reports 2026-05-10 10:41:59 +02:00
Ilia Denisov e4dc0ce029 ui/phase-18: ship-class calc bridge with live designer preview
Wires pkg/calc/ship.go into the WASM Core boundary as seven thin
wrappers (DriveEffective, EmptyMass, WeaponsBlockMass, FullMass,
Speed, CargoCapacity, CarryingMass). The ship-class designer reads
Core through a new CORE_CONTEXT_KEY populated by the in-game layout
and renders a five-row preview pane (mass, full-load mass, max
speed, range at full load, cargo capacity) that updates reactively
on every form edit and on the player's localPlayer{Drive,Weapons,
Shields,Cargo} tech levels — three of which are now decoded from
the report's Player block alongside the existing localPlayerDrive.

CarryingMass is the seventh wrapper added to the original six-function
list so that "full-load mass" composes through pkg/calc/ functions
without putting math in TypeScript.
2026-05-09 23:14:40 +02:00
Ilia Denisov 721fa2172d ui/phase-17: mark stage as done after local-ci run 20
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:01:30 +02:00
Ilia Denisov c8332bb122 ui/phase-17: clamp ship-class name input to validator's 30-rune limit
Mirrors the validateEntityName MAX_LENGTH on the form input so the
field stops accepting characters once the limit is hit. The
validator still runs and surfaces the localised reason if a paste
overshoots; the maxlength is purely a typing-time guardrail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 21:52:24 +02:00
Ilia Denisov 0068e065ea ui/phase-17: spell out overlay invariant in plan acceptance criteria
Adds an explicit acceptance line documenting that pending Save /
Delete actions reflect on the table immediately via
applyOrderOverlay (already exercised by the new Vitest + Playwright
suites). Touches a watched path so the local-ci runner picks up the
retrigger after the previous flaky run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 21:50:35 +02:00
Ilia Denisov 2cfe427ce9 ui/phase-17: retrigger local-ci after flaky pkg/util test
Run 19 failed on a known flake in pkg/util.TestRandomSuffixGenerator
(line 289 — generator can produce the same 4-char suffix in two
consecutive draws out of 10000). Empty commit retriggers the
runner; flake is unrelated to Phase 17.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 21:49:11 +02:00
Ilia Denisov 785c3483f8 ui/phase-17: ship-class CRUD without calc
Phase 17 lights up the ship-class table and designer active views,
extends the order-draft pipeline with createShipClass and
removeShipClass commands, and projects pending Save/Delete actions
through applyOrderOverlay so the table reflects the player's
intent before auto-sync lands. The plan is corrected in the same
patch: per game/rules.txt, ship classes are designed once and
cannot be edited — the engine has no Update command, so the UI
exposes only Create + Delete.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 21:44:21 +02:00
Ilia Denisov 8a236bef14 ui/phase-16: pick any planet in reach + stronger pick-mode dim
The cargo-route picker filtered out unidentified planets, so an
early-game player who had spotted but not surveyed a destination
could not configure a route to it — the engine has no such
restriction (`game/internal/controller/route.go.PlanetRouteSet`
only checks ownership of the origin and `util.ShortDistance(...) <=
FligthDistance`). Drop the unidentified guard and document the
contract in `cargo-routes-ux.md` plus a comment over `reachableSet()`.

Pick-mode dim now drops both alpha and tint on out-of-reach
planets so bright shapes (`STYLE_LOCAL` is `0x6dd2ff`) collapse
into a single muted gray. The single-channel `dimAlpha=0.3` was too
gentle against the dark theme — the user reported the dim wasn't
visible. Tighten to `dimAlpha=0.35 + dimTint=0x303841`; restore
both on tear-down.

Also threads through the user's `pkg/calc/race.go.FligthDistance`
addition: `calc-bridge.md` records the new Go-side reference (the
engine's `Race.FlightDistance()` already wraps it), and the picker
comment points at the canonical formula location.

Tests:
- `inspector-planet-cargo-routes.test.ts` adds two cases — a
  reach-spans-every-kind case (own + foreign + uninhabited +
  unidentified all picked when in range) and a successful pick to
  an unidentified destination.
- All 356 vitest cases + chromium-desktop / webkit-desktop e2e
  cargo-routes pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 20:48:42 +02:00
Ilia Denisov 3442dc94f7 ui/phase-16: mark stage as done after local-ci run 17
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 20:10:56 +02:00
Ilia Denisov 7c8b5aeb23 ui/phase-16: cargo routes inspector + map pick foundation
Add per-planet cargo routes (COL/CAP/MAT/EMP) to the inspector with
a renderer-driven destination picker (faded out-of-reach planets,
cursor-line anchor, hover-highlight) and per-route arrows on the
map. The pick-mode primitives are exposed via `MapPickService` so
ship-group dispatch in Phase 19/20 can reuse the same surface.

Pass A — generic map foundation:
- hit-test now sizes the click zone to `pointRadiusPx + slopPx` so
  the visible disc is always part of the target.
- `RendererHandle` gains `onPointerMove`, `onHoverChange`,
  `setPickMode`, `getPickState`, `getPrimitiveAlpha`,
  `setExtraPrimitives`, `getPrimitives`. The click dispatcher is
  centralised: pick-mode swallows clicks atomically so the standard
  selection consumers do not race against teardown.
- `MapPickService` (`lib/map-pick.svelte.ts`) wraps the renderer
  contract in a promise-shaped `pick(...)`. The in-game shell
  layout owns the service so sidebar and bottom-sheet inspectors
  see the same instance.
- Debug-surface registry exposes `getMapPrimitives`,
  `getMapPickState`, `getMapCamera` to e2e specs without spawning a
  separate debug page after navigation.

Pass B — cargo-route feature:
- `CargoLoadType`, `setCargoRoute`, `removeCargoRoute` typed
  variants with `(source, loadType)` collapse rule on the order
  draft; round-trip through the FBS encoder/decoder.
- `GameReport` decodes `routes` and the local player's drive tech
  for the inline reach formula (40 × drive). `applyOrderOverlay`
  upserts/drops route entries for valid/submitting/applied
  commands.
- `lib/inspectors/planet/cargo-routes.svelte` renders the
  four-slot section. `Add` / `Edit` call `MapPickService.pick`,
  `Remove` emits `removeCargoRoute`.
- `map/cargo-routes.ts` builds shaft + arrowhead primitives per
  cargo type; the map view pushes them through
  `setExtraPrimitives` so the renderer never re-inits Pixi on
  route mutations (Pixi 8 doesn't support that on a reused
  canvas).

Docs:
- `docs/cargo-routes-ux.md` covers engine semantics + UI map.
- `docs/renderer.md` documents pick mode and the debug surface.
- `docs/calc-bridge.md` records the Phase 16 reach waiver.
- `PLAN.md` rewrites Phase 16 to reflect the foundation + feature
  split and the decisions baked in (map-driven picker, inline
  reach, optimistic overlay via `setExtraPrimitives`).

Tests:
- `tests/map-pick-mode.test.ts` — pure overlay-spec helper.
- `tests/map-cargo-routes.test.ts` — `buildCargoRouteLines`.
- `tests/inspector-planet-cargo-routes.test.ts` — slot rendering,
  picker invocation, collapse, cancel, remove.
- Extensions to `order-draft`, `submit`, `order-load`,
  `order-overlay`, `state-binding`, `inspector-planet`,
  `inspector-overlay`, `game-shell-sidebar`, `game-shell-header`.
- `tests/e2e/cargo-routes.spec.ts` — Playwright happy path: add
  COL, add CAP, remove COL, asserting both the inspector and the
  arrow count via `__galaxyDebug.getMapPrimitives()`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 20:01:34 +02:00
Ilia Denisov 5fd67ed958 ui/phase-15: mark stage as done after local-ci run 16 2026-05-09 16:15:20 +02:00