2ffd7527a6c8cb5608a2a85c8df1b7aba963220d
513 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2ffd7527a6 |
test(ui): align rejected-submit e2e specs with per-command sync semantics
The `rename-planet` and `ship-classes` rejected-submit specs broke on the previous commit because: 1. `tests/e2e/fixtures/order-fbs.ts` builds the FBS response without `forceDefaults(true)`, and flatbuffers@25's TS codegen now elides `cmd_applied=false` against its int8 default of 0. The encoded payload no longer carried the rejection, so the UI decoded the row as `applied` and the assertions on the `rejected` status text failed first. The production Go transcoder already force-slots the field; mirror that behaviour in the e2e fixture. 2. The specs themselves still asserted the old blanket `data-sync-status="error"` on per-command rejection. After the previous commit's behaviour change the bar stays `synced` for per-command rejection (only genuine transport failures keep the red banner + Retry), so the assertions now read the row's inline reason text instead. `tests/e2e/fixtures/order-fbs.ts` also gains the `cmdErrorMessage` field so future fixtures can mirror the engine's rejection reason through the round trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
723885e74e |
fix(order): surface rejection reason, keep sync green, hydrate verdicts
Tests · UI / test (push) Has been cancelled
Tests · Go / test (push) Successful in 2m3s
Tests · Go / test (pull_request) Successful in 2m5s
Tests · Integration / integration (pull_request) Successful in 1m44s
Tests · UI / test (pull_request) Failing after 4m28s
Three issues surfaced once the per-command rejection from the previous commit actually reached the UI: 1. Sync banner falsely red. `OrderDraftStore.runSync` flipped `syncStatus = "error"` whenever any command was rejected and advertised a Retry button. A per-command rejection is a player-correctable state — the round trip succeeded, the engine just refused that command — so the retry can't help. Keep `syncStatus = "synced"` on `success`; the red row highlight is the visible cue. 2. Rejection reason missing. Add `cmd_error_message: string` to `CommandItem` in `pkg/schema/fbs/order.fbs` (appended last to preserve existing slot offsets) and regenerate the Go + TS stubs for that one type. Plumb the message through `CommandMeta`, `Controller.applyCommand`'s `m.Result(code, message)` call, the Go transcoder, the UI decoders in `submit.ts` / `order-load.ts`, and the `OrderDraftStore.errorMessages` map. `order-tab.svelte` renders it as an italic danger-coloured line under rejected commands, with new CSS for `.error-reason`. 3. Verdict lost on navigation. `order-load.ts.decodeCommand` never read `cmdApplied`/`cmdErrorCode`, so `hydrateFromServer` fell back to a blanket "applied" status — a previously-rejected command came back green after a lobby → game round trip. Extend the fetch decoder to populate `statuses`/`errorCodes`/ `errorMessages` maps and have `hydrateFromServer` use them. Engine-side persistence already records the verdict on disk — verified against the live `0000/order/<id>.json`. `flatbuffers@25` elides default-int8/int64 fields on write; the Go transcoder force-slots `cmd_applied=false` / `cmd_error_code=0` already, the new test fixtures flip `builder.forceDefaults(true)` to mirror that behaviour so the round trip survives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e038ea6154 |
fix(dev-deploy): recycle engine containers on galaxy-engine:dev SHA drift
`backend`'s reconciler adopts pre-existing `galaxy-game-*` containers without comparing their image SHA against the freshly-built `galaxy-engine:dev`, so a long-lived sandbox would otherwise keep serving the previous engine code after a redeploy. Issue #59 surfaced this: after the per-command-rejection fix was deployed via `workflow_dispatch`, the running sandbox container was still on the old image SHA and the browser kept seeing the 503/unavailable response. Adds a `Recycle engine containers on image drift` step right before `Reap stray dev-deploy containers`. The step compares the new `galaxy-engine:dev` SHA against every running `galaxy-game-*` container and, on drift, stops the backend, removes the container, wipes the bind-mounted per-game state directory (Engine.Init() writes turn-0 over any pre-existing `turn-N` files — silent state corruption otherwise), and cascade-deletes the lobby `games` row. The `dev-sandbox` bootstrap on the next backend boot finds no live sandbox and provisions a fresh one on the new engine image. When the engine sources are unchanged, the BuildKit cache hits and the SHA stays the same — the recycle step is a no-op and the running games keep their state across the deploy. Verified end-to-end against the live dev environment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
af30846091 |
fix(game): #59 — per-command rejection on PUT /api/v1/order
Validation of a player's order now applies every command against a transient game-state snapshot and records the per-command outcome (cmdApplied, cmdErrorCode) in each command's meta. The order is persisted even when some commands are rejected, and the response is 202 + UserGamesOrder so clients can surface the partial failure without the chain collapsing into "downstream service is unavailable". Pkg/error consts are reshelved onto three explicit ranges with a package doc and helpers (IsInternalCode/IsInputCode/IsGameStateCode): 1xxx internal/server (500/501), 2xxx structural input (400), 3xxx game-state per-command rejection (400 when escaping HTTP, otherwise recorded as cmdErrorCode). Two pre-existing typos fixed mechanically (ErrBeakGroupNumberNotEnough -> ErrBreakGroupNumberNotEnough, ErrRaceExinct -> ErrRaceExtinct) along with all callsites. Engine errorResponse maps *GenericError by shelf rather than mapping everything to 500. The Quit-not-last structural check in Controller.ValidateOrder is preserved and its type assertion fixed (was a value assertion against a pointer-typed command, so the check silently never fired). Backend, gateway and UI are unchanged — they were already correct on the 202 path; only the engine collapsing per-command rejection into 500 was needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ce1dc19a29 |
Merge pull request 'feat(ui): F8-12 — map polish (#55)' (#70) from feature/issue-55-map-polish into development
Closes #55. |
||
|
|
a37b784452 |
perf(ui): F8-12 — toggle responsiveness on 700-planet legacy reports (#55)
Profiling KNNTS041 (700 planets, 1283 primitives, 29 LOCAL fog circles) flushed three independent costs out of the toggle path: * `setVisibilityFog` rebuilt the inverse mask + 29 × 9 paint ops on every effect run, even when the input was identical. Caches a fingerprint of the circles + wrap mode and bails on a no-op call — knocks ~1 ms off every flip, more on heavier maps. * `paintLabelEntry` was split into `paintLabelLayout` (hit-area / line positions / frame geometry — runs on every content change) and `paintLabelSelection` (text fills + frame visibility — runs only when the selection identity actually flips). The incremental path now skips the 6300 redundant `Text.style.fill = ...` writes it used to perform on every `planetNames` flip, which is what forced Pixi to invalidate the underlying text textures. * `applyLabelContent` no longer blanks `nameText.text` when the toggle hides the name — it just flips `visible`. The cached text texture survives, so the next paint frame skips ~700 texture rebuilds. Also enables Pixi-side culling on every per-copy primitive / outline / label container. With 9 torus copies × ~700 planets the scene graph holds thousands of nodes, most of which sit outside the visible viewport at any moment — the cullable flag lets Pixi skip them in the per-frame traversal. The legacy `KNNTS041` probe (chromium-desktop, headless) shows `applyVisibilityState` collapsing from ~24 ms to ~5 ms after a cache-warm flip; `app.render` drops from ~46 ms to ~22 ms. Reading the toggle delay end-to-end inside the browser still measures ~460 ms in headless, which is consistent with the runner's RAF cadence — owner can confirm on the real machine where the previous ~1 s delay was reported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f4670c1831 |
perf+fix(ui): F8-12 — max-zoom clamp + planet-names toggle responsiveness (#55)
* Max-zoom clamp: `MIN_VISIBLE_WORLD_AT_MAX_ZOOM = 5` world units on the longest viewport axis. Tuned against the owner's debug-overlay readings — mobile longest ≈ 412 px clamps at scale ≈ 82, desktop longest ≈ 1200 px clamps at scale ≈ 240. Same formula adapts to both shapes automatically; no separate mobile / desktop branch. * Planet-names toggle no longer rebuilds every Pixi.Text on a flip. When `setPlanetLabels` sees the same planet set (which is the common case — only the `name` lines toggling on / off), it walks the live label containers and just retunes text content + visibility instead of destroying and recreating 9 × N Text instances. A 500-planet map flips the toggle inside a frame now. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4d729c1f50 |
feat(ui): F8-12 — smooth planet discs + ?debug=1 overlay (#55)
* Planet discs (and every other circle the renderer draws — outlines, picker hover ring, reach / bombing rings, etc.) trace a fixed 32-segment polygon instead of leaning on Pixi's adaptive bezier subdivision. PixiJS v8 picks the segment count from the world-space radius, which collapsed to 6-8 segments once the parent container's scale climbed — so the planet read as a visible polygon at high zoom. The custom path stays cheap (~64 floats per disc) and gives a perceptually round silhouette at every zoom level. * Opt-in dev overlay activated by `?debug=1` in the URL. A small bottom-left panel shows the current `scale`, the "whole world fits" reference scale, the current zoom ratio (scale / scale_ref), and the world-units rectangle visible in the viewport — so the owner can decide what `maxScale` to clamp to on the next iteration without guessing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
24d75564bb |
fix(ui): F8-12 — owner feedback round 3 (#55)
* Hit-test: a click inside a planet's visible disc always picks the planet, regardless of overlapping route shafts or battle X-crosses with higher base `priority`. Closes the #1, #2, #4 reports (picker hover would only catch the circumference, planet+routes swallowed disc clicks, label click on a battled planet routed to the battle viewer). Slop-only hits (cursor near a line but not on any disc) still use the existing priority order. * Labels and planet outlines render in all nine torus copies again so they follow the player into wrap tiles — closes #3 (labels vanished on the wrong half of the viewport whenever the camera was panned past the wrap seam). The fingerprint guard keeps the per-toggle / per-selection rebuild cheap. * Pixi.Text gets a few px of `padding` so the rasteriser no longer clips the last letter on a half-pixel measurement — closes #5. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
eb5018342e |
feat(ui): F8-12 — owner feedback round 2 (#55)
* Bug fix: theme flip no longer leaves planets oversized. The camera-preserving remount now calls a new `RendererHandle.refreshCameraDerivedDraws` explicitly after the manual moveCenter/setZoom pair so the post-mount geometry tracks `viewport.scaled` even if pixi-viewport's `'zoomed'` listener races the next Ticker tick. * Doc #3: clicks on a planet label route through the same hit-test path as a click on the disc. The label `Container` now has a pointer hit area sized to the text + frame padding; pointertap simulates a click at the planet centre, so selection and pick-mode resolution behave identically. * Doc #4: battle X-crosses + cargo arrowhead wings grow sub-linearly with zoom (PLANET_SIZE_ZOOM_ALPHA). New `Style.softLengthAnchor` ('center' / 'start') makes the renderer treat the recorded endpoints as the geometry "at the reference scale" and rescale around the midpoint (X-cross) or the start endpoint (arrow wings). Arrowhead base length is halved from 6 to 3 world units to match the owner's "in half" request. * Doc #5: picker overlay loses the anchor ring at the source, the cursor line drops to a cargo-route-thin 0.6 px stroke, and the hover ring around the destination is replaced by a planet-style outline (visible disc + 1 px padding) in the `pickHighlight` accent — so candidate destinations read like selection in warm yellow. * Doc #6: regression test pins the in-disc hit zone. * Perf #1: camera-driven redraws are throttled onto the next Ticker tick. A rapid wheel / pinch burst now coalesces into at most one `clear() + redraw` pass per painted frame, which keeps the 500-planet map responsive on zoom and toggle flips. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6c3cd25476 |
add converted legacy report
Tests · UI / test (pull_request) Successful in 3m25s
|
||
|
|
6996a79286 |
perf(ui): F8-12 — pixel-space planet sizing + single-copy label/outline layers (#55)
* Planet size formula moves to pixel-space: `pointRadiusBasePx = 2 + 2 * cbrt(size / SIZE_NORMALIZER)`. The on-screen disc now reads ~4-7 px at the reference zoom regardless of how large the world rectangle is — the previous `world-units` formulation blew up on small maps and made Source-class planets swallow their neighbours. * Labels + outlines live in the origin copy only. The 9× replication across torus copies was the dominant cost on a 100+ planet map (Pixi.Text creation + Graphics rebuilds on every zoom step); the origin-copy layout is what the camera-wrap listener guarantees the user actually sees. * `setPlanetLabels` and `setPlanetOutlines` skip Pixi-object rebuilds when the input fingerprint is unchanged — toggle flips and selection changes now keep the existing Text / Graphics instances alive and only repaint the affected pieces. * `renderer.md` updated to the new contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
75a4211373 |
fix(ui): F8-12 — settle e2e regressions from the polish PR (#55)
* state-binding.ts: normalise planet size by the engine's typical mid-range (`SIZE_NORMALIZER = 100`) so legacy fixtures recording Size in the hundreds do not blow up the world-unit disc and start overlapping neighbouring planets. The cube-root growth stays; Size-800 reads twice as big as Size-100. * cargo-routes.spec.ts: retire the selection-ring CirclePrim from the expected primitive count (4 planets + 3 cargo arrow lines = 7). * map-toggles.spec.ts: bombing-rings → planet outlines (the high-bit 0xc… range is permanently empty); planet-names persist test waits for the renderer's debug providers and for the IndexedDB write to flush before reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
680ebac919 |
feat(ui): F8-12 — map polish (zoom invariance, labels, selection, soft radius) (#55)
* Honest pixel-space sizing for `pointRadiusPx` / `strokeWidthPx`: the renderer divides by the current camera scale on every `viewport.zoomed` so thin lines / small markers stay the same on-screen size at any zoom. * Known-size planets switch to `pointRadiusWorld`, softened against the reference scale by `PLANET_SIZE_ZOOM_ALPHA = 0.33`; unidentified planets pin to a 3-px disc. * New planet label layer renders a two-line `name / #N` legend under each planet (`#N` only for unidentified or when the new `planetNames` toggle is off). Selection now paints an inverse-fill frame around the selected planet's label plus an outline on the disc; the old selection-ring primitive is retired. * Bombing markers swap the separate CirclePrim for a planet-outline overlay (damaged / wiped colour); the report deep-link moves to a "view bombing report" link in the planet inspector. * Docs + tests follow: `renderer.md` reflects the new sizing contract + label / outline layers, vitest covers the sizing math, label formatting, and the new toggle, and the map-toggles e2e adds a persistence case for `planetNames`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ba93a9092e | Merge pull request 'feat(ui): F8-11 — battles table under table submenu (#54)' (#69) from feature/issue-54-battles-table into development | ||
|
|
209f8508cd |
feat(ui): F8-11 — battles table under table submenu (#54)
Adds a sortable battles list as a new entity under the existing `view → table` submenu (entity slug `battles`), replacing the standalone top-level `battle log` shortcut which always opened a "battle not found" placeholder. The single-battle viewer stays put and is reached only by clicking a row (or a battle marker on the map), identical to the existing `section-battles.svelte` flow. Columns are planet (via the shared `planetLabel` helper) and shots (the per-battle action count carried by `BattleSummary`), sortable both ways with shots-desc default. No backend / FBS / map changes: the wire payload is unchanged. Participants / observers / total mass require the full BattleReport and were intentionally dropped to avoid N round trips per menu open. The top-level `battle log` item is removed from `header/view-menu` and `sidebar/bottom-tabs` (and their stale comment blocks updated); the now-orphan `game.view.battle` i18n key is dropped from both locales. |
||
|
|
e4fbb6644c | Merge pull request 'feat(ui): F8-10 — tables planets / ship-groups / fleets, ship-classes delete guard (#53)' (#68) from feature/issue-53-table-planets-groups-fleets into development | ||
|
|
8e552f556d |
fix(ui): F8-10 owner-feedback — persistent filters, camera, disabled visual, dropdown narrowing (#53)
Polish pass after the first F8-10 walkthrough:
- table-planets: moved the `foreign` chip to the end of the row and
hid the race dropdown until `foreign` is on (it never made sense
to pick a race while the bucket itself was off).
- persistent per-table filter / sort state — extracted to
`table-{planets,ship-groups,fleets}-state.svelte.ts` singletons so
a row click → map → back to the table restores the prior chip /
dropdown / sort state. Held in memory only; an F5 still resets.
- table-ship-groups: the planet and class dropdowns now narrow to
the slice surviving the owner checkboxes, so toggling `foreign`
off removes planets / classes touched only by foreign rows.
- map.svelte: camera (centre + zoom) is captured on every dispose
path into a new `GameStateStore.lastCamera` and consumed on the
next mount, so leaving the map for any other active view and
coming back restores the prior pan / zoom. A pending focus from
the tables still wins for the centre point.
- table-ship-classes: `:disabled` now reads as disabled (muted
colour, no hover ring, not-allowed cursor) — the click was already
a no-op, only the visual was lying.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
80ed11e3b6 |
feat(ui): F8-10 — tables planets / ship-groups / fleets, ship-classes delete guard (#53)
Lights up three previously-stubbed table active views and tightens the
existing one:
- table-planets: 4 kind checkboxes (own / foreign / uninhabited /
unknown) + race dropdown that filters the foreign slice; row click
selects + centres the planet on the map.
- table-ship-groups: local + foreign groups in one grid, owner
checkboxes, planet dropdown (destination OR origin), class
dropdown; on-planet click focuses the destination planet, in-space
click focuses the ship group itself (camera follows interpolated
position).
- table-fleets: own fleets only with the shared planet dropdown;
on-planet click focuses the planet, in-space click centres the
camera on the interpolated fleet position without altering the
selection (no fleet variant in Selected).
- table-ship-classes: per-row Delete is disabled with a count tooltip
while at least one local ship group references the class. The
engine refuses the removal anyway; the UI pre-empts the surface.
Wires the click → map flow through a transient `SelectionStore.focus`
/ `focusPoint` channel that `map.svelte` consumes once on mount —
in-memory only, so an F5 does not re-centre.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ef4cecb4b2 | Merge pull request 'feat(ui): F8-09 — turn report sticky icon-popup section menu' (#67) from feature/issue-52-report-toc-popup into development | ||
|
|
1b2c13ecd6 |
fix(ui): F8-09 owner-feedback — fixed TOC trigger + heading offset (#52)
Owner-reported regressions in Firefox + Safari on desktop after the
initial F8-09 patch landed:
1. The TOC trigger rode up with the page during scroll instead of
staying pinned to the viewport (mobile worked, desktop did not).
2. Clicking a popover item scrolled the matching section so its
heading went up under the chrome — only the table body was visible.
Root cause for (1): the in-game shell declares `overflow-y: auto`
on `.active-view-host` so mobile (where `.game-shell` is fixed at
`inset: 0`) has an internal scroll region. On desktop the host
grows with content, no overflow ever engages, and the document
body becomes the actual scroll container. Per CSS spec the host
remains the "scrollport" for any `position: sticky` descendant, so
the trigger inside the report column never sees the scroll event
and rides up with the body content.
Fix:
- Swap the trigger from `position: sticky` to `position: fixed`.
The component is mounted only while the report active view is on
screen, so the fixed element is naturally tied to the view's
lifetime. Anchor at `top: 4rem` (below the in-game header), and
on `min-width: 1024px` shift `right` by 18 rem to clear the
always-on sidebar; below 1024 px the sidebar is an overlay so
the default `right: 1.25rem` matches the report's right padding.
- Add `padding-top: 4.5rem` to `.report-view` (4rem mobile) so the
first section heading does not land under the trigger at scroll
position 0.
- Add `scroll-margin-top: 7.5rem` to every `<section
id="report-…">` so `scrollIntoView({ block: "start" })` lands
the heading below the trigger after a popover-driven jump.
- Sync `ui/docs/report-view.md` §"Table of contents and active
highlight" with the new positioning rationale.
Tests: `pnpm check`, `pnpm test` (821), `pnpm test:e2e
report-sections` (4 projects) all green.
Refs: #52 (#43 umbrella).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
cfbe052242 |
feat(ui): F8-09 — turn report sticky icon-popup section menu (#52)
- Replace the 14 rem sticky sidebar (and its mobile <select> twin)
with a single sticky icon-popup trigger pinned to the top-right
corner of the report column. Trigger shows `≡` followed by the
currently active section title (CSS-clamped with text-overflow:
ellipsis so long RU titles cannot bloat the button). Click opens
an anchored popover on desktop and a fixed bottom-sheet on
<768.98 px (mirrors lib/active-view/map-toggles.svelte).
- Each menuitem closes the popover and scrolls the matching
`<section id="report-<slug>">` into view. The scroll is deferred
one animation frame so the surface unmount + restoreFocus's
focus restoration on the (sticky) trigger commit first; otherwise
the focus call could cancel the just-started smooth/instant
scroll under desktop Chromium and WebKit.
- Drop the in-report "Back to map" button — the same affordance
lives in the app-shell view menu (tests/e2e/game-shell.spec.ts
covers it).
- Tighten the report grid to a single flex column so the section
body now occupies the full container width.
- i18n: remove game.report.back_to_map and
game.report.toc.mobile_label; add game.report.toc.open and
game.report.toc.close (mirrors game.map.toggles.open/close).
- Tests: Vitest report-toc.test.ts rewritten for the new icon-popup
contract; Playwright report-sections.spec.ts switches the anchor
loop to trigger → menuitem and adds a mobile bottom-sheet
assertion; game-shell-stubs.test.ts no longer asserts the
back-to-map button on the report orchestrator.
- Docs: ui/docs/report-view.md (TOC + i18n + test seams) and
docs/FUNCTIONAL{,_ru}.md §6.4 updated. The stale SvelteKit
Snapshot reference (the route file was removed by the single-URL
app-shell) is dropped at the same time.
Refs: #52 (#43 umbrella).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
147c7d0a6a | Merge pull request 'F8-05 — game-mode chrome cleanup + inspector compact rows' (#66) from feature/issue-48-game-chrome-and-inspector-compact-rows into development | ||
|
|
24c68e9846 |
feat(model+ui): F8-05 — race on OtherGroup, real attribution + N×M label
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m6s
Tests · Integration / integration (pull_request) Successful in 1m51s
Tests · UI / test (pull_request) Successful in 3m53s
Issue #48 п.32 ("Stationed ship groups") shipped with a fragile race fallback: when a foreign group sat on a non-`other`-kind planet the inspector printed a generic "foreign" label, which collapsed the race dropdown to a single uninformative bucket. The engine FBS contract did not carry per-group race either, so live games hit the same gap. This patch carries race authoritatively from the engine through every layer down to the inspector. Wire format & engine - `pkg/schema/fbs/report.fbs`: add `race:string` to `OtherGroup` and `LocalGroup` (additive — old clients ignore). - `pkg/schema/fbs/report/`: regenerated Go bindings. - `ui/frontend/src/proto/galaxy/fbs/report/`: regenerated TS bindings. - `pkg/model/report.OtherGroup.Race`: new field; carried through `LocalGroup` via the embedded `OtherGroup`. - `pkg/transcoder/report.go`: encode + decode `race` on both `LocalGroup` and `OtherGroup`. - `game/internal/controller/report.go.otherGroup`: set `v.Race` from `c.g.Race[c.RaceIndex(sg.OwnerID)].Name` so every emitted group — own or foreign — carries the resolved race name. Legacy parser - `tools/local-dev/legacy-report/parser.go`: capture the `<Race> Groups` header into `pendingOtherGroup.race`, fill local group `Race` from `p.rep.Race`, propagate both into the `report.OtherGroup` rows. - Tests + smoke counts updated; regenerated `KNNTS{039,041}.json` fixtures so the synthetic loader carries the new field. UI - `ui/frontend/src/api/`: `ReportShipGroupBase.race` field; synthetic loader + FBS decoder populate it. - `ui/frontend/src/lib/inspectors/planet/ship-groups.svelte`: the stationed-groups inspector picks race directly from `group.race` (own falls back to `localRace`, both finally to the `race.unknown` placeholder). The planet-owner / "foreign" heuristic is gone. - Row label changes from "N ships mass M" to a compact `<class>` | `<N ×>` | `<mass>` three-column layout: the count cell is right-aligned tabular, the mass cell is right-aligned monospace + tabular, matching the inspector / calculator number conventions. Stale i18n keys removed (`ship_groups.row.count`, `.row.mass`, `.race.foreign`). - All affected unit tests (8 files) carry the new `race` field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
cc4bc3c2b7 |
feat(ui+legacy): F8-05 owner-feedback round 1 — inspector tweaks + parser
Owner-reported polish on top of #48, plus a legacy-parser gap that prevented verifying stationed ship groups against a real .REP fixture. UI: - Production: drop the empty `(production)` placeholder option. Owned planets always produce something, so the primary select now opens on `industry` by default when `planet.production` is null/unknown, keeping the row inside the four real production kinds at all times. - Production: lock the row to a single line (no flex-wrap) and strip border + padding from the ✓/✗ buttons so the apply/cancel icons read as glyphs and the row no longer breaks into two visual rows for Research / Ship contexts where both selects are present. - Cargo routes: the placeholder option is now an `<option disabled>` styled like a section header (greyed, italic) and reads "manage routes" instead of "cargo routes". The wording shifts the intent from a section label to an action prompt. Legacy parser: - F8-05 (#48 п.32) "Stationed ship groups" couldn't be verified against the dg fixture because the legacy `<Race> Groups` blocks (outside battles) and the `Unidentified Groups` block were dropped by the parser — both are now wired up. Foreign group rows parse the `# T D W S C T Q D P M` columns and resolve the destination against the parsed planet tables (rows with an invisible destination drop, matching the existing local-group convention). The legacy row carries no origin / range columns, so foreign groups surface as stationed at the destination. - Smoke tests on every fixture extended with `otherGroups` and `unidentifiedGroups` counts. New focused unit test `TestParseOtherAndUnidentifiedGroups` covers the column layout, the drop-on-unknown-destination rule, and the `X Y`-only unidentified rows. - `tools/local-dev/reports/dg/KNNTS039.json` and `tools/local-dev/reports/dg/KNNTS041.json` regenerated so the synthetic-loader fixtures carry the new arrays. - README updated: the two sections move out of "Skipped sections" into a "Foreign and unidentified groups" block; package doc-comment reflects the broader scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
aee5f39a7e |
docs(ui): F8-05 — sync inspector topic docs with the new compact rows
Tests · UI / test (pull_request) Successful in 11m1s
- order-composer.md describes the production row's apply-gate (two selects + ✓/✗) and the click-to-edit entry point for planetRename. - cargo-routes-ux.md replaces the four-slot grid description with the new single-row dropdown + contextual actions and notes the "stays on the picked type" UX rule. - science-designer-ux.md updates the production-picker integration description to the dropdown pair and refreshes the e2e walkthrough step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4a23c357e5 |
feat(ui): F8-05 — game-mode chrome cleanup + inspector compact rows (#48)
Drains six F8 polish items (parent #43) in one feature: а) Chrome cleanup - п.6 — remove the AccountMenu (settings/sessions/theme/language/logout ∼ rudimentary in-game) and replace it with a single icon-button light/dark theme toggle. The toggle flips an in-memory `theme.override`; game-shell unmount calls `theme.clearOverride()` so the lobby (and any re-entry) re-projects the persisted lobby choice. - п.8 — remove the wrap-scrolling radio from the map gear popover. The per-game `wrapMode` store and the renderer's no-wrap path stay in place for a future engine-side topology feature; only the UI surface is dropped (wrap is a server-side concept, not a per-session UI affordance). б) Inspector compact rows (single idiom: select + ✓ apply / ✗ cancel, or contextual edit/remove/add) - п.13 — planet name is now click-to-edit: clicking the name opens an inline `<input>` + ✓ confirm icon; Escape cancels; the explicit Rename action button and Cancel button are gone. - п.14 — production becomes one row: primary `<select>` picks industry/materials/research/ship, conditional secondary `<select>` picks the target (tech / science / ship class) for research and ship contexts. Apply is gated until row state differs from the planet's current effective production; auto-submit-on-click is replaced by the apply-gate. - п.16 — cargo routes collapse to one row: a single dropdown (COL/CAP/MAT/EMP plus a placeholder that absorbs the old section title) and contextual action buttons (add / edit + remove) to the right. After a successful pick or remove the dropdown stays on the type the user just acted on. - п.32 — stationed ship groups hoist the race column into a dropdown above the table. The dropdown seeds with the player's own race when local groups are stationed here, otherwise the first race alphabetically; rendered only when more than one race is in orbit. The race column is dropped in both single- and multi-race modes — the dropdown's value already names the active race. Tests: unit and Playwright e2e updated for every changed test-id and flow; new coverage added for `theme.override`, the in-game toggle, the apply-gate behaviour, and the stationed-race dropdown. i18n keys for the removed menu items, the wrap radios, the cargo title, and the explicit `rename.cancel` are dropped from both locales; new `game.shell.theme_toggle.*`, `production.main/target.*`, `production.apply/cancel`, `cargo.placeholder`, and `ship_groups.race_filter.aria` keys land. Docs synced: `docs/FUNCTIONAL.md` §6.7 + `docs/FUNCTIONAL_ru.md` mirror drop the torus / no-wrap radio mention; `ui/docs/design-system.md` documents the lobby-owned persisted picker + the in-game ephemeral override channel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
2901ecb21b | Merge pull request 'fix(ui): F8-08 unified number format — mono, fixed 3-decimal, no separators' (#65) from feature/issue-51-number-format into development | ||
|
|
ed4e2f58a1 |
test(ui): F8-08 e2e — match new 1-dec percent + 3-dec float formatting
sciences.spec.ts: `sciences-cell-drive` now reads "25.0" (was "25") because formatPercent always emits one fractional digit. ship-classes.spec.ts: `ship-classes-cell-drive` now reads "1.000" (was "1") because formatFloat always emits three fractional digits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b31d9f4c45 |
fix(ui): F8-08 unified number format — mono, fixed 3-decimal, no separators
Engine emits Floats at Fixed3 quantisation; UI now renders them as 3-decimal fixed-point strings without thousand separators, monospaced via var(--font-mono) on .numeric cells, and right-aligned in tables so columns line up on the decimal point. Integer counts render with 0 decimals and no separators; science fractions render as 1-decimal percent (matches the engine's third decimal of precision). Bug fixes from #51 (umbrella #43): - Player Status drive/weapons/shields/cargo: were tech LEVELS rendered through formatPercent (x100) — now use formatFloat (raw level). - Races table: same bug, same fix. Style/UX cleanups: - Inspector field labels lose "stockpile" word ($ / M suffix carries it). - Coordinates drop the parentheses (just "x, y"). - Inspector + report tables unify font sizes with calculator-tab (values 0.85rem mono, labels 0.8rem). Files: - new util: ui/frontend/src/lib/util/number-format.ts - report/format.ts becomes a thin re-export to keep section imports compact - inspector planet / ship-group / actions: drop inline formatNumber, mark numeric <dd> with class="numeric" - table-races (+ bug fix), table-sciences, table-ship-classes, designer-science: drop inline formatters, switch to util, add class="numeric" on numeric <th>/<td> - 17 report section files: class="numeric" on numeric th/td + scoped CSS rule for mono+right-align - i18n en/ru: drop "stockpile" word, drop "%" from tech-level column headers in races + player_status (the "%" was the misleading bit from the bug) - tests/inspector-planet + tests/table-races: update assertions to match the new format Verification: pnpm test (814 passed), pnpm check (0 errors/warnings), pnpm build clean. Refs: #51 (#43 umbrella). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
208d30073b | Merge pull request 'fix(ui): F8-04b lobby — auto-expand first available games sub + hide empty invitations' (#64) from feature/lobby-default-expand-and-empty-invitations into development | ||
|
|
6fbab5417f |
fix(ui): F8-04b lobby — auto-expand first available games sub + hide empty invitations
Two follow-up nits on the F8-04b sidebar: 1. The bare-`lobby` resolver (lobby-screen.svelte) redirected to `games-recruitment` unconditionally on mount. With games already in the player's roster the sidebar then highlighted the wrong sub-page. The resolver now awaits the lobby fan-out + account fetch, then hands off to the same `firstVisibleGamesScreen` helper the sidebar uses — so a fresh entry with games lands on `active-past`, the canonical-order fallback stays `recruitment`. 2. `games-invitations` was unconditionally visible in the sidebar. Now it follows the `active-past` rule: hidden until the pending-invites list reports >=1. The lobby shell's auto-kick effect treats it symmetrically — accepting / declining the last invite moves the player to the next visible sub-page once the fan-out has resolved. Acceptance order in games-invitations-screen.acceptInvite was also swapped to setMyGames-before-removeInvitation: both mutations land in the same microtask, so the new auto-kick sees the freshly added game in `myGames` when invitations drop to zero and routes the player to `active-past` instead of bouncing through `recruitment`. The visibility predicates and canonical order live in the new `src/lib/lobby-nav.ts` pure helper, shared between the sidebar and the resolver so they cannot disagree. Unit tests cover every combination of (hasMyGames, hasInvitations, isPaidOrDev). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8e8b34d112 | Merge pull request 'fix(ui): F8-07 cargo-route picker — torus-wrap overlay + thinner arrows' (#63) from feature/issue-50-cargo-picker-torus into development | ||
|
|
175bf25794 |
docs(ui): F8-07 update renderer.md pick-mode lifecycle for per-copy overlay
Tests · UI / test (pull_request) Successful in 2m55s
Spec described the overlay as a single Graphics in the origin tile, which was both the bug source and out of date after the F8-07 fix. Updates the Open / Tick steps to describe the nine-copy replication and the torus-shortest line endpoint contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3d8aa91973 |
fix(ui): F8-07 cargo-route picker — torus-wrap overlay + thinner arrows
Tests · UI / test (push) Successful in 3m4s
Pick overlay (anchor ring, cursor line, hover outline) was drawn into a single Pixi container — copies[ORIGIN_COPY_INDEX] — so any view of a wrap copy lost it: picker from A1/A2 to the right (across the seam) showed no hover highlight on A3's wrap copy, and the picker on A3 (x≈1.44, near the left edge) put its anchor far left of the viewport. Fix replicates the overlay across all nine torus copies (matching how primitives and fog already render) and switches the cursor-line endpoint to torus-shortest geometry via torusShortestDelta. Anchor and hover-outline coordinates stay canonical; the per-copy replication renders them under the user's view in whatever tile is on screen. Also reduces cargo-route arrow strokes: COL/CAP/MAT 2->0.6 wu and EMP 1->0.4 wu (~3 / ~2 screen px at typical zoom) per the owner's request. Tests cover the new torus path: source near the left edge with cursor on the wrap copy across the seam (x axis), source near the top edge with cursor across the y seam, and a guard that anchor / hover-outline coords stay canonical regardless of the world argument. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3153a95292 | Merge pull request 'feat(lobby): F8-04b hierarchical sidebar + paid-tier gate for create-game' (#62) from feature/f8-04b-lobby-restructure into development | ||
|
|
f42ab87233 |
test(ui): F8-04b mobile-safe assertion for free-tier private-games entry
The desktop submenu (.desktop-only) is CSS-hidden on mobile viewports — the mobile sidebar tucks the same sub-panel entries behind a dropdown popover. Assert `toBeAttached()` instead of `toBeVisible()` so the dev-bundle smoke check works on every viewport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cff7cc3859 |
fix(ui): F8-04b e2e — viewport-agnostic nav + refresh after create
Tests · UI / test (push) Failing after 3m8s
- lobby-create-screen: call lobbyData.refresh() after a successful POST so the new game shows up in the private-games panel immediately. The shared lobby-data store is otherwise lazy (ensure-on-first-mount), which rendered a stale list across the post-create navigation in the e2e suite. - e2e tests that move between lobby sub-panels now go through `window.__galaxyNav.go(...)` rather than clicking the sidebar items. The mobile sidebar tucks the submenu behind a dropdown, so testid-based clicks fail on the mobile-iphone-13 / pixel-5 viewports — the dev nav surface bypasses that UX (which has its own coverage in `lobby-tier-gate` / future submenu specs). - game-shell-map missing-membership test: assert `lobby-account-name` instead of `lobby-create-button` on drop-back-to-lobby (the button moved into the paid-only private-games sub-panel; the identity strip is the constant lobby chrome). - inspector-ship-group + ship-group-send synthetic loader specs: jump straight to the dev-only `synthetic-reports` top-level screen via the dev nav surface before looking for the file input (the loader moved off Overview in F8-04b). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
058c4fcf69 |
test(ui): update profile-screen e2e for F8-04b sidebar rename
Tests · UI / test (push) Failing after 11m56s
`lobby-nav-overview` is replaced by `lobby-nav-games` (the new parent), and the empty-games active-past sub-panel is hidden entirely so the landing testid becomes `lobby-recruitment-empty` (the always-visible sub-panel for a no-games session). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
009ea560f9 |
feat(lobby): F8-04b hierarchical sidebar + paid-tier gate for create-game
Reshape the lobby UI from a single Overview into a two-level sidebar (games · profile · DEV synthetic-reports) with four games sub-panels (active-past · recruitment · invitations · private-games). Move the `create new game` button into the private-games panel, merge the applications section into recruitment cards as status chips, and add DEV-only synthetic-report loader as a top-level screen. Add a paid-tier gate at backend `lobby.game.create`: free callers get `403 forbidden` before the lobby service is invoked. The UI hides the private-games sub-panel + create button on free tier (DEV affordances flag overrides). Update every integration test that creates a game to use a new `testenv.PromoteToPaid` helper; add a new `TestLobbyFlow_FreeUserCreateGameForbidden`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
98d1fe6cae |
feat(ui): lobby site-style sidebar + profile screen (#47)
Closes #47 Round 2 polish: time-zone dropdown, save-stay on profile, shared account store removing header flicker. |
||
|
|
a679d9cdcb |
fix(ui): F8-04 profile polish — IANA timezone picker, save-stay, shared identity cache
PR-feedback round on #60: - Time-zone field is now a continent-grouped <select> populated from `Intl.supportedValuesOf("timeZone")`, with the browser-detected zone pre-selected when no value is stored. A stored zone the runtime no longer advertises is preserved as an "Other" entry. - Saving the profile no longer kicks the user back to the lobby: the form stays put and shows a transient `saved` notice, cleared on the next edit. Only `cancel` returns to the lobby. - New `lib/account-store.svelte.ts` caches `user.account.get` for the session; lobby + profile share it through `account.ensure()`, so navigating Overview ⇄ Profile no longer flashes the "loading account…" placeholder or fires a second gateway call. Profile save writes through to the store so the shell identity strip picks up the new display name without refetching. Cleared on logout to prevent identity bleed between accounts. - e2e: existing 4 cases adjusted for save-stay; added two new ones for the timezone dropdown and identity-strip stability across navigation. - Docs: `ui/docs/lobby.md` updated to describe the shared cache, the new timezone picker shape, and the save-stay behaviour. |
||
|
|
2ecdecad1e |
feat(ui): lobby site-style sidebar + profile screen (#47)
- Wrap lobby and profile in a shared `lobby-shell.svelte` chrome: page-list sidebar (Overview/Profile) and a top "Player-xxxx" identity strip mirroring the project site's monospace look. - Strip the legacy `lobby.title`, device-session-id `<code>`, and `lobby.greeting` paragraph; the identity strip both names the user and opens the profile editor. - Add a top-level `profile` AppScreen with a three-field form (`display_name`, `preferred_language`, `time_zone`) backed by a new `src/api/account.ts` wrapper around `user.account.get`, `user.profile.update`, and `user.settings.update`. Saving switches the active i18n locale in-place when the new preferred language is one the UI ships translations for. - Update e2e fixture + auth-flow / lobby-flow specs to use the new `lobby-account-name` testid and wait for the loaded identity before releasing pending `SubscribeEvents` (webkit revocation race). New `profile-screen.spec.ts` covers navigation, edit-save, and cancel. - Sync `ui/docs/lobby.md` and `ui/docs/navigation.md` to the new layout. Closes #47 |
||
|
|
b03993fcb1 | Merge pull request 'fix(ui): F8-06 calculator polish — input steps, lock idiom, tech floor, speed-lock fix' (#61) from feature/issue-49-calculator-polish into development | ||
|
|
b01a60e42b |
fix(ui): F8-06 calculator polish — drop delete-class button, reserve lock slot
- Remove the `delete <ship_class_name>` button (and `deleteClass`, `canDelete`, `.delete` CSS, `game.calculator.action.delete` i18n key) from the calculator. Delete-class lives in the ship-classes table — the broader rework will land under #53. - Bombing and cargo-capacity rows now reserve a hidden lock-slot placeholder so their value column lines up vertically with the mass/speed/attack/defence rows (which carry a lock button). |
||
|
|
cc4727a32e |
fix(ui): F8-06 calculator polish — always 3-decimal display, mono font, input cap
Owner feedback round 2 on PR #61: - Pad every read-only calculator value to three decimals: tech labels, derived results (mass, speed, attack, defence, bombing, cargo capacity), planet MAT, planet build-rate, modernization cost, and the full-cargo capacity label all read as "1.000" instead of "1", matching the goal-seek back-solved input and the report. Drops thousands grouping so the same `fmt()` string also embeds cleanly in the read-only `<input type="number">` cell. - Switch label and input styling onto the existing `--font-mono` token (right-aligned, tabular-nums) so columns line up vertically across rows like a financial table. - Refuse a fourth decimal as the user types in every calculator number input (DWSC blocks, tech, MAT, custom load, lock value, modernization target tech): the `oninput` truncates the input text past three decimal digits and explicitly writes the truncated value back through `bind:value`, so Svelte's later reactive flush cannot undo the cap. - Doc + tests follow the rule (five new vitest cases covering the 3-decimal label format, the input cap on each input class, and the integer-padding rule for derived results). |
||
|
|
cbf7f65916 |
fix(ui): F8-06 calculator polish — unified spinner UX, lock-infeasible on (0, 1), dropdown reset-changes
Owner review on PR #61: - п.9 (option B). Hide the native spinner on EVERY numeric input in the calculator (DWSC blocks, armament, tech, planet MAT, custom load, lock value, modernization target tech) and drive every step through ArrowUp / ArrowDown. The column widths stay stable and the inputs read consistently across the whole row. The ship blocks keep the smart (0 ↔ 1) jump on ArrowUp/ArrowDown; armament steps ±1 with a JS handler instead of relying on the native spinner. Other inputs step by their natural grain (±0.001 for tech / lock, ±0.01 for MAT / load). - п.10. Tech-level labels (`tech-val`) and the planet MAT label (`mat-val`) now read through the same `Ceil3` formatter as the derived results, so plain-text numeric values share the report's 3-decimal tabular formatting. The design-area component receives `formatNumber` as a prop; the resolved (goal-seek) cell uses the same formatter, so the read-only computed value matches the rest of the row. - п.12. `computeCalculator` now validates the back-solved block against the same DWSC rule the live validator enforces (`0` or `≥ 1`). When the solver lands in the `(0, 1)` gap (e.g. attack 0.5 / weaponsTech 1.5 → weapons 0.333…) the lock is flagged infeasible — the lock input flips red and the claimed block is NOT back-solved into the invalid range, so the design preview keeps reading the user's own typed values instead of silently showing a sub-1 block. - new. Selecting an existing ship class from the name datalist now loads it immediately. `change` fires only on blur in Firefox, which is why the previous behaviour looked delayed; switching the load to `oninput` with an `InputEvent.inputType` check makes the load synchronous everywhere (datalist replacement carries `"insertReplacementText"` in Chromium / WebKit, `undefined` in Firefox; keyboard typing always carries a typing `inputType`). Before loading we compare the live blocks to the previously loaded class (or to the empty defaults) and, if they differ, ask through a `window.confirm`. On decline we revert the name field and leave the design untouched. Tests: calculator-tab and calc-model gain six cases (armament step, tech/MAT formatter labels, lock infeasible on (0, 1) for both attack→weapons and emptyMass→cargo, lock-value Arrow step, dropdown immediate load + confirm-blocks-load + confirm-allows-load), all 779 vitest tests green. docs/calculator-ux.md follows the new behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
e9b904332e |
fix(ui): calculator polish — smart input steps, unified tech/MAT lock idiom, tech floor, speed-lock ceiling fix
- pkg/calc: DriveForSpeed treats restMass==0 as a valid ceiling-only case (every positive drive solves it), so locking the displayed speed of a D=1, W=A=S=C=0 ship is no longer a phantom "infeasible". - ship-design-area: drive/weapons/shields/cargo inputs use a JS-driven smart step on ArrowUp/ArrowDown (0↔1 jump, otherwise ±0.1) and hide the native spinner so it cannot produce invalid (0, 1) values; armament keeps its native step 1. - Tech and planet MAT cells follow the same lock idiom as goal-seek locks: open padlock (🔓) over the inherited value → click to open an input with a closed padlock (🔒). The padlock slot is always reserved, so the column width is stable. - Tech overrides (design area and modernization target) are floored at the player's current tech on this turn — a lower value is flagged as invalid. |
||
|
|
793b709d8f |
feat(ui): readable order card — status as background tint, corner ✕ (#58)
Closes #46 F8-03 — повышаем читаемость карточки приказа: перенос длинного текста, статус в фоне карточки через --color-{success,danger,warning}-subtle, маленький угловой ✕ всегда видимый. |
||
|
|
2294d8b3d9 |
fix(ui): tighter order card — calculator-scale font, corner-flush ✕;
stabilise report-sections e2e Owner review on PR #58: - shrink the order-card body to 0.8rem (matching the calculator's body text scale) so the order list reads as part of the sidebar's density, not its own larger surface; - shrink the delete ✕ to 0.95rem and glue it flush to the card's top-right corner (no offset, sized to fit the corner padding-space); - tighten the card padding to match the smaller text. Independently — the same review asked to fix `report-sections › every TOC anchor lands its section in view`, which had been a long-standing e2e flake (run #366 on `development` already failed it twice before passing on retry; my PR's run #367 simply exhausted all five retries). The root cause is the smooth `scrollIntoView` settling slower than Playwright's 5 s viewport wait under heavy CI load. The production TOC already honours `prefers-reduced-motion: reduce` and swaps to an instant scroll there; switching the Playwright config to that media mode makes every spec deterministic without touching production code. |