- 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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
`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>
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>
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.
- 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
- 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).
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).
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>
- 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.
Closes#46
F8-03 — повышаем читаемость карточки приказа: перенос длинного текста, статус в фоне карточки через --color-{success,danger,warning}-subtle, маленький угловой ✕ всегда видимый.
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.
The order-tab row now wraps long labels (`overflow-wrap: anywhere`),
encodes status into the card background via the design-token subtle
palette (applied → success-subtle, invalid/rejected/conflict →
danger-subtle, draft/valid/submitting → warning-subtle), and exposes
a small framed `✕` delete button absolutely positioned in the
card's top-right corner — always visible, labelled by
`game.sidebar.order.command_delete` for assistive tech. The textual
status name remains in the DOM as an `.sr-only` node so screen
readers and the existing testids still observe it.
Refs #46
Clicking the current-turn row in the header turn navigator while
already viewing it routed through returnToCurrent() →
viewTurn(currentTurn), which re-fetches the live report and flips the
view through `loading`. At turn 0 the only row is the live turn, so
the dropdown always fired a pointless backend round-trip and redraw.
Guard goToTurn() against re-selecting the on-screen turn
(turn === viewedTurn): just close the popover and stop. Leaving
history is unaffected — there the viewed turn differs from the target.
Closes#45
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two-step e-mail login now drops the cursor on each step's primary
field as it mounts — the e-mail field on load, the code field once the
e-mail step advances — via a small `use:` action. Focusing fires each
input's onfocus, which clears the readonly autofill guard, so the field
is editable straight away.
The code input now requests `autocomplete="one-time-code"` instead of
`new-password`. The latter is a password-manager hint and does not stop
Firefox saving the typed code to form history (it was offering the
previous code back in a dropdown). `one-time-code` is the semantic token
for a verification code; Firefox honours it specifically to keep the
value out of form history (Mozilla bug 1547294). The e-mail field keeps
`new-password` to fend off saved-login autofill.
Tests: new Vitest cases assert autofocus on both steps and the code
field's `one-time-code` token; a new Playwright case covers the same in
Chromium and WebKit (Safari engine). Firefox form history is owner
manual-QA — there is no Firefox project in the e2e matrix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The map view now selects a DARK_THEME or LIGHT_THEME palette from the
resolved app theme and threads it through every primitive builder, so
the canvas, planets, ship groups, cargo routes, battle/bombing markers,
fog, reach + selection rings, pending-Send tracks, and the pick overlay
all switch with the rest of the chrome. A theme flip remounts the
renderer preserving the camera — Pixi bakes the background at init and
every primitive bakes its colour at build, so a live re-tint is not
possible on the same instance.
This also fixes the reported bug: the gear-popover trigger and the
loading overlay hardcoded a dark navy background, so in light theme the
gear was invisible (dark icon on dark chip) until hover flipped it to a
white chip. Both now use the --color-surface-overlay token and read
correctly in both themes.
The light palette mirrors the dark one role-for-role, darkened /
saturated for contrast on a light background while keeping the incoming,
battle, and bombing accents vivid. The values are a first pass meant to
be refined during the F8 manual-QA loop.
Removes the now-dead "Phase 35" references from the code and lifts the
map-recoloring prohibition from the design-system / renderer docs; the
battle scene stays a fixed-palette data-viz surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ARCHITECTURE.md §15 "Verification order" specifies signature verification
(step 4) before payload_hash (step 5), but the authenticated-edge
decorator chain wrapped the payload-hash gate outside the signature gate,
so the hash was checked first. gateway/README.md and gateway/docs/flows.md
had drifted to match the code (hash-first), leaving ARCHITECTURE.md as the
lone source describing the intended order.
Swap the two decorators in server.go so the signature gate runs first, and
align README + flows.md to ARCHITECTURE.md. Signature-first is the
cryptographically sound order: the signature covers the payload_hash field,
so the request is authenticated before any of its content is processed.
Observable side effect: a request carrying a tampered payload_hash whose
signature was computed over the original hash is now rejected at the
signature gate (UNAUTHENTICATED "invalid request signature") instead of the
hash gate (INVALID_ARGUMENT). Security is unchanged — both refusals happen
before the payload is handled. The four payload-hash unit tests re-sign
over the tampered hash so they keep exercising the hash gate; the
cross-service integration test signs over the overridden hash and already
accepts both codes.
Refs #39
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The single-URL restore replays the saved screen/view on an in-place
refresh only. Re-entering a game from the lobby resets activeView to the
map (lobby calls activeView.reset() before appScreen.go("game")), and
browser Back / the return-to-lobby control exit to the lobby. Spell this
out so the refresh-restore is not mistaken for a per-re-enter restore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The app-shell migration surfaced a mobile-only e2e failure: taps on the
bottom-tab bar, the map-toggles menu, and the planet sheet were
intercepted by sibling elements despite the targets being on top.
Root cause: `.game-shell` used `min-height: 100vh`, so sub-pixel content
overflowed the viewport and made the document scrollable. On mobile that
scroll toggles the browser's dynamic toolbar, which resizes the viewport
and every `position: fixed` overlay (their sizes derive from `100vh`)
mid-gesture — defeating Playwright's actionability hit-test, and making
the real controls jittery to tap.
Pin the shell with `position: fixed; inset: 0` on the mobile breakpoint
so it leaves document flow: the document can no longer scroll, the
toolbar stays put, the viewport and overlays stay stable, and the
active-view area remains the single internal scroll region. Desktop is
unchanged (the rule is scoped to max-width: 767.98px).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Unit: repoint moved screen imports (lib/screens, lib/game), mock
$lib/app-nav (appScreen/activeView) instead of $app/navigation, drop the
removed gameId props, assert screen/view selection.
- e2e: add a dev-only window.__galaxyNav affordance; specs enter a game via
enterGame(...) instead of a /games/:id URL; URL assertions become content
assertions (the URL stays /game/); reload uses waitUntil:"commit" (shallow
routing) and mocks /rpc on game entry.
- Remove the obsolete report scroll-restore test (it relied on a SvelteKit
route Snapshot that no longer exists); update the missing-membership test
to the new lobby-redirect+toast behaviour. Fix a stale report.svelte
docstring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- A restored game that no longer exists (cancelled/removed/revoked) drops to
the lobby with a toast instead of the in-game error state: game-state
exposes a `notFound` flag and the shell redirects via appScreen.go("lobby").
- Add a visible "return to lobby" control to the in-game header.
- Push/toast deep-links use activeView.select(...) (no URL); fix a latent
visibility-listener double-install on in-place game switches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the screen into browser history via SvelteKit shallow routing
(pushState/replaceState with page.state) so Back/Forward move between
screens while the URL stays at /game/. Overlays (game, lobby-create) push;
lobby/login replace. A popstate→page.state effect syncs the store back
without re-pushing (no loop); the boot stamp puts a restored overlay above
the load entry so Back falls through to lobby. In-game view switches never
touch history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse the game UI to one route (`/`): a screen dispatcher renders
login/lobby/lobby-create/game from `appScreen`/`activeView` state instead of
URL routes. Move screen components to lib/screens & lib/game; the game shell
reads the game id from `appScreen.gameId` and re-inits per-game stores via an
$effect; in-game views render from `activeView`. Flip ~23 goto/href nav sites
to store mutations; drop the `?sidebar=` URL coupling. Auth gate is now
state-based. WIP: browser-history (Back→lobby), restore-validation, the
return-to-lobby button, push deep-links, and the test migration are follow-ups
on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `appScreen` + `activeView` rune singletons with a shared sessionStorage
snapshot — the in-memory source of truth that replaces URL-based screen/view
routing for the single-URL app-shell. Not wired in yet (additive).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VitePress is a Vue SPA; a same-origin link to /game/ (a separate app, not
a VitePress page) was intercepted by its client router and rendered
VitePress's own 404 until a manual reload. Mark the game links (both
home pages and the nav item) target="_self" so the click is a real
browser navigation that the edge Caddy serves from the game bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- The app root ("/", i.e. /game/) rendered a dev "workspace skeleton"
stub, and the layout guard only redirected anonymous users off it, so
an authenticated visitor stayed on the stub. Redirect "/" to /lobby
(authenticated) and /login (anonymous), and replace the stub with a
minimal loading placeholder. Drop the obsolete landing-stub unit test
(root redirect is covered by the auth-flow e2e).
- Ship a tombstone /service-worker.js on the project site so any old
root-scoped PWA worker (from when the game lived at the origin root)
unregisters itself instead of serving a stale cached page at the
site origin. The game now registers its worker only under /game/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>