40d6ba6ba40466b2eefc4705a63ec19d5f5595d9
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
601970b028 |
refactor(game): lock-free storage, remove /command, flatten engine wrapper
Three-stage refactor of the game-engine plumbing (game logic untouched): Stage 1 — lock-free persistence + admin serialisation. Remove the file lock from repo/fs (the .lock file, the Read/Write-vs-*Safe duality and the dead ReadSafe polling) and replace the two-step rename with a single atomic rename so concurrent reads are torn-free without a lock. Serialise the state-mutating admin writers (init/turn/banish) with one shared router LimitMiddleware, rewritten to block on the request context instead of a racy shared 100ms timer. Stage 2 — remove the obsolete immediate-command path end to end. Players submit through PUT /api/v1/order; the legacy PUT /api/v1/command path is deleted across game (route, handler, 24 command factories, Ctrl), backend (Commands handler/route, engineclient.ExecuteCommands), gateway (dispatch + executeUserGamesCommand + routing entry), the FlatBuffers/model contract (UserGamesCommand[Response]) and transcoder, plus every affected OpenAPI/README/FUNCTIONAL/ARCHITECTURE doc. The integration proxy test is converted to the order path. Stage 3 — flatten the REST->engine wrapper. Replace the executor adapter, the controller package functions and RepoController with one concrete controller.Service; drop the single-implementation Repo and Storage interfaces (repo.Repo / fs.FS are now concrete). Handlers depend on a thin handler.Engine seam and own the domain->REST projection; storage is resolved once at startup instead of per request. Co-Authored-By: Claude Opus 4.8 (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> |
||
|
|
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> |
||
|
|
8565942392 |
feat(deploy): single-origin path-based deployment + project site
Serve the whole stack behind one host: site at /, game UI at /game/, gateway REST at /api + /healthz, Connect at /rpc (prefix stripped by the edge Caddy). The built artifact is domain-agnostic — the UI talks to the gateway same-origin via relative URLs, so the same bundle runs under any host with no rebuild and with CORS disabled. - Rename the Connect proto service galaxy.gateway.v1.EdgeGateway -> edge.v1.Gateway; regenerate Go + TS; public path /rpc/edge.v1.Gateway. - Move the game UI under base path /game (env BASE_PATH); make the manifest, service-worker scope, WASM loader, and all navigation base-aware via a withBase helper. - Relative API + /rpc Connect prefix; Vite dev proxy mirrors the strip. - Rewrite the edge Caddy (dev + prod) for path-based routing; empty CORS allow-lists (same-origin); single host. - New VitePress project site (site/): i18n en/ru with switcher, LaTeX math, minimal monospace theme; built and served at /. - dev-deploy compose/Makefile + CI (dev-deploy, prod-build, new site-build) build and seed the site; probes hit /, /game/, /healthz. - Sync docs (ARCHITECTURE, gateway README/openapi, dev-deploy & local-dev READMEs, CLAUDE.md, ui/PLAN). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ebd156ece2 |
battle-fetch: migrate to user.games.battle ConnectRPC command
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m7s
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · UI / test (pull_request) Failing after 3m42s
The Phase 27 BattleViewer was the last UI surface still issuing raw
fetch() against the backend REST contract (`/api/v1/user/games/...
/battles/...`). The dev-deploy gateway never proxied that path, so
the viewer worked only in tools/local-dev/. Move it onto the signed
ConnectRPC channel every other authenticated surface already uses.
Wire pieces:
- FBS GameBattleRequest in pkg/schema/fbs/battle.fbs, regenerated
Go + TS bindings.
- MessageTypeUserGamesBattle constant + GameBattleRequest struct in
pkg/model/report/messages.go.
- pkg/transcoder/battle.go gains GameBattleRequestToPayload and
PayloadToGameBattleRequest helpers.
- gateway games_commands.go switches on the new message type and
GETs /api/v1/user/games/{id}/battles/{turn}/{battle_id}; the JSON
response is re-encoded as a FlatBuffers BattleReport before being
returned. 404 from backend surfaces as the canonical `not_found`
gateway error.
- ui/frontend/src/api/battle-fetch.ts now builds the FBS request,
calls GalaxyClient.executeCommand, and decodes the FBS response
into the existing UI shape (Record<string,string> race/ship maps,
string-form UUID). BattleFetchError carries an HTTP-style status
derived from the result code so the active-view's not_found branch
keeps working.
- battle.svelte pulls the GalaxyClient from the in-game shell
context. While the layout's boot Promise.all is in flight the
effect stays in `loading` until the client handle becomes
non-null.
- ui/Makefile FBS_INPUTS gains battle.fbs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
fed282f2d2 |
Phase 28 (Step 2): FBS schemas + message-type constants for mail
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m4s
Tests · Go / test (push) Successful in 2m5s
Tests · Integration / integration (pull_request) Successful in 1m54s
Tests · UI / test (pull_request) Successful in 2m50s
Adds the wire schema for the eight `user.games.mail.*` ConnectRPC commands together with the shared payload types (`MailMessage`, `MailRecipientState`, `MailBroadcastReceipt`). Send-request tables carry the optional `recipient_race_name` introduced in Step 1. Drops: - `pkg/schema/fbs/diplomail.fbs` — schema sources; - `pkg/schema/fbs/diplomail/*.go` — generated Go bindings (flatc `--go --go-module-name galaxy/schema/fbs`); - `pkg/model/diplomail/diplomail.go` — message-type catalog used by the gateway router; - `ui/frontend/src/proto/galaxy/fbs/diplomail/*.ts` — generated TS bindings consumed by the upcoming UI client wrapper; - `ui/Makefile` `FBS_INPUTS` extended to pick the new schema up on the next `make -C ui fbs-ts` run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
969c0480ba |
ui/phase-27: battle viewer (radial scene, playback, map markers)
Engine wire change: Report.battle switched from []uuid.UUID to
[]BattleSummary{id, planet, shots} so the map can place battle
markers without N extra fetches. FBS schema + generated Go/TS
regenerated; transcoder + report controller updated; openapi
adds the BattleSummary schema with a freeze test.
Backend gateway forwards engine GET /api/v1/battle/:turn/:uuid as
/api/v1/user/games/{game_id}/battles/{turn}/{battle_id} (handler
plus engineclient.FetchBattle, contract test stub, openapi spec).
UI:
- BattleViewer (lib/battle-player/) is a logically isolated SVG
radial scene that consumes a BattleReport prop. Planet at the
centre, races on the outer ring at equal angular spacing, race
clusters by (race, className) with <class>:<numLeft> labels;
observer groups (inBattle: false) are not drawn; eliminated
races drop out and survivors re-distribute on the next frame.
- Shot line per frame: red on destroyed, green otherwise; erased
on the next frame. Playback controls: play/pause + step ± +
rewind + 1x/2x/4x speed (400/200/100 ms per frame).
- Page wrapper (lib/active-view/battle.svelte) loads BattleReport
via api/battle-fetch.ts; synthetic-gameId prefix routes to a
fixture loader, otherwise REST through the gateway. Always-
visible <ol> text protocol satisfies the accessibility ask.
- section-battles.svelte links every battle UUID into the viewer.
- map/battle-markers.ts: yellow X cross of 2 LinePrim through the
corners of the planet's circumscribed square (stroke width
clamps from 1 px at 1 shot to 5 px at 100+ shots); bombing
marker is a stroke-only ring (yellow when damaged, red when
wiped). Wired into state-binding.ts; click handler dispatches
battle clicks to the viewer and bombing clicks to the matching
Reports row.
- i18n keys for the viewer in en + ru.
Docs: ui/docs/battle-viewer-ux.md, FUNCTIONAL.md §6.5 + ru
mirror, ui/PLAN.md Phase 27 decisions + deferred TODOs (push
event, richer class visuals, animated re-distribution).
Tests: Vitest unit (radial layout + timeline frame builder +
marker stroke formula + marker primitives), Playwright e2e for
the viewer (Reports link → viewer, playback step, not-found),
backend engineclient FetchBattle (200 / 404 / bad input), engine
openapi freezes (BattleReport, BattleReportGroup,
BattleActionReport, BattleSummary, Report.battle items).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f80c623a74 |
ui/phase-14: rename planet end-to-end + order read-back
Wires the first end-to-end command through the full pipeline:
inspector rename action → local order draft → user.games.order
submit → optimistic overlay on map / inspector → server hydration
on cache miss via the new user.games.order.get message type.
Backend: GET /api/v1/user/games/{id}/orders forwards to engine
GET /api/v1/order. Gateway parses the engine PUT response into the
extended UserGamesOrderResponse FBS envelope and adds
executeUserGamesOrderGet for the read-back path. Frontend ports
ValidateTypeName to TS, lands the inline rename editor + Submit
button, and exposes a renderedReport context so consumers see the
overlay-applied snapshot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ce7a66b3e6 |
ui/phase-11: map wired to live game state
Replaces the Phase 10 map stub with live planet rendering driven by `user.games.report`, and wires the header turn counter to the same data. Phase 11's frontend sits on a per-game `GameStateStore` that lives in `lib/game-state.svelte.ts`: the in-game shell layout instantiates one per game, exposes it through Svelte context, and disposes it on remount. The store discovers the game's current turn through `lobby.my.games.list`, fetches the matching report, and exposes a TS-friendly snapshot to the header turn counter, the map view, and the inspector / order / calculator tabs that later phases will plug onto the same instance. The pipeline forced one cross-stage decision: the user surface needs the current turn number to know which report to fetch, but `GameSummary` did not expose it. Phase 11 extends the lobby catalogue (FB schema, transcoder, Go model, backend gameSummaryWire, gateway decoders, openapi, TS bindings, api/lobby.ts) with `current_turn:int32`. The data was already tracked in backend's `RuntimeSnapshot.CurrentTurn`; surfacing it is a wire change only. Two alternatives were rejected: a brand-new `user.games.state` message (full wire-flow for one field) and hard-coding `turn=0` (works for the dev sandbox, which never advances past zero, but renders the initial state for any real game). The change crosses Phase 8's already-shipped catalogue per the project's "decisions baked back into the live plan" rule — existing tests and fixtures are updated in the same patch. The state binding lives in `map/state-binding.ts::reportToWorld`: one Point primitive per planet across all four kinds (local / other / uninhabited / unidentified) with distinct fill colours, fill alphas, and point radii so the user can tell them apart at a glance. The planet engine number is reused as the primitive id so a hit-test result resolves directly to a planet without an extra lookup table. Zero-planet reports yield a well-formed empty world; malformed dimensions fall back to 1×1 so a bad report cannot crash the renderer. The map view's mount effect creates the renderer once and skips re-mount on no-op refreshes (same turn, same wrap mode); a turn change or wrap-mode flip disposes and recreates it. The renderer's external API does not yet expose `setWorld`; Phase 24 / 34 will extract it once high-frequency updates land. The store installs a `visibilitychange` listener that calls `refresh()` when the tab regains focus. Wrap-mode preference uses `Cache` namespace `game-prefs`, key `<gameId>/wrap-mode`, default `torus`. Phase 11 reads through `store.wrapMode`; Phase 29 wires the toggle UI on top of `setWrapMode`. Tests: Vitest unit coverage for `reportToWorld` (every kind, ids, styling, empty / zero-dimension edges, priority order) and for the store lifecycle (init success, missing-membership error, forbidden-result error, `setTurn`, wrap-mode persistence across instances, `failBootstrap`). Playwright e2e mocks the gateway for `lobby.my.games.list` and `user.games.report` and asserts the live data path: turn counter shows the reported turn, `active-view-map` flips to `data-status="ready"`, and `data-planet-count` matches the fixture count. The zero-planet regression and the missing-membership error path are covered. Phase 11 status stays `pending` in `ui/PLAN.md` until the local-ci run lands green; flipping to `done` follows in the next commit per the per-stage CI gate in `CLAUDE.md`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f57a290432 |
phase 8: lobby UI + cross-stack lobby command catalog + TS FlatBuffers
- Extend pkg/model/lobby and pkg/schema/fbs/lobby.fbs with public-games
list, my-applications/invites lists, game-create, application-submit,
invite-redeem/decline. Mirror the matching transcoder pairs and Go
fixture round-trip tests.
- Wire the seven new lobby message types through
gateway/internal/backendclient/{routes,lobby_commands}.go with
per-command REST helpers, JSON-tolerant decoding of backend wire
shapes, and httptest-based unit coverage for success / 4xx / 5xx /
503 across each command.
- Introduce TS-side FlatBuffers via the `flatbuffers` runtime dep, a
`make fbs-ts` target driving flatc, and the generated bindings under
ui/frontend/src/proto/galaxy/fbs. Phase 7's `user.account.get` decode
now uses these bindings as well, closing the JSON.parse vs
FlatBuffers gap that would have failed against a real local stack.
- Replace the placeholder lobby with five sections (my games, pending
invitations, my applications, public games, create new game) and the
/lobby/create form. Submit-application uses an inline race-name
form on the public-game card; create-game keeps name / description /
turn_schedule / enrollment_ends_at always visible and the rest under
an Advanced toggle with TS-side defaults.
- Update lobby/+page.svelte to throw LobbyError on non-ok result codes;
GalaxyClient.executeCommand now returns { resultCode, payloadBytes }.
- Vitest binding round-trips, lobby.ts wrapper unit tests, lobby-page
+ lobby-create component tests, Playwright lobby-flow.spec covering
create / submit / accept across all four projects. Phase 7 e2e was
migrated to the FlatBuffers fixtures and to click+fill against the
Safari-autofill readonly inputs.
- Mark Phase 8 done in ui/PLAN.md, mirror the wire-format note into
Phase 7, append the new lobby commands to gateway/README.md and
docs/ARCHITECTURE.md, add ui/docs/lobby.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
fbc0260720 |
phase 5: wasm core, GalaxyClient skeleton, Connect-Web stubs
Compile `ui/core` to WebAssembly via TinyGo (903 KB) and expose four canonical-bytes / signature-verification functions on `globalThis.galaxyCore` from `ui/wasm/main.go`. The TypeScript-side `Core` interface plus a `WasmCore` adapter (browser + JSDOM loader) bridge those into a typed shape, and a `GalaxyClient` skeleton wires `Core.signRequest` → injected `Signer` → typed Connect client → `Core.verifyPayloadHash` / `verifyResponse`. Wire `ui/buf.gen.yaml` against the local `@bufbuild/protoc-gen-es` v2 binary (devDependency) so the codegen step does not depend on the buf.build BSR. Vitest covers the bridge end-to-end: per-method WasmCore tests under JSDOM, byte-for-byte canon parity against the gateway fixtures committed in Phase 3, and a `GalaxyClient` orchestration test using `createRouterTransport`. The committed `core.wasm` snapshot tracks TinyGo output so contributors run `make wasm` only when `ui/core/` changes; CI consumes the snapshot directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |