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>
ui — Galaxy Cross-Platform Client
ui/ hosts the new cross-platform Galaxy client. A single
TypeScript + Svelte source tree builds to five targets: web,
web-mobile, standalone PC (mac/win/linux), iOS, and Android. A
shared Go module (ui/core) carries envelope cryptography, the
FlatBuffers codec, keypair management, and a thin bridge over
pkg/calc/ for UI-side game math; it is compiled to WASM for the
web targets, gomobile native libraries for mobile, and embedded
directly in Wails on desktop. All network I/O lives on the
TypeScript side via ConnectRPC, so the Go module is a pure compute
boundary on every platform.
The web target is a finalized, installable PWA: a shared design-token system with light/dark themes, WCAG 2.2 AA accessibility, en/ru localisation with a persisted choice, a central error surface, and offline tolerance — see the topic docs below.
The legacy Fyne client under client/ is reference-only.
Nothing in ui/ imports from it.
The strategic rationale (why Svelte, why PixiJS, why Go-as-WASM, why
Wails+Capacitor) lives outside the repo at
~/.claude/plans/buzzing-questing-fountain.md. This README is a
quick orientation; deeper design notes live under ui/docs/.
Targets
| Target | Wrapper | Toolchain | Status |
|---|---|---|---|
| web | browser tab | Vite + WASM | implemented |
| web-mobile | mobile browser | Vite + WASM | implemented |
| desktop (mac) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
| desktop (win) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
| desktop (linux) | Wails v2 | Go + Wails CLI | planned (see ROADMAP.md) |
| iOS | Capacitor | gomobile + Xcode | planned (see ROADMAP.md) |
| Android | Capacitor | gomobile + Gradle | planned (see ROADMAP.md) |
Layered architecture
- TypeScript + Svelte 5 frontend, shared across all five targets, scaffolded with SvelteKit + Vite.
- PixiJS v8 with dual WebGPU/WebGL backend for the world map renderer.
- Go module
ui/core/as a compute-only library (canonical bytes, Ed25519 sign/verify, FlatBuffers codec, keypair, thin bridge topkg/calc/) compiled to WASM, gomobile, and Wails-embedded native. - TypeScript-side
Coreinterface with three adapters (WasmCore,WailsCore,CapacitorCore) selected at build time. GalaxyClienton top ofCoreperforms all network I/O via ConnectRPC (@connectrpc/connect-web) on every platform.- Per-platform storage: WebCrypto + IndexedDB on web, OS keychain
- SQLite on desktop, iOS Keychain / Android Keystore + SQLite on
mobile, all behind a single
KeyStoreandCacheTypeScript interface.
- SQLite on desktop, iOS Keychain / Android Keystore + SQLite on
mobile, all behind a single
- Single-URL app-shell navigation: the game UI is one route served
at
/game/; the screen (login / lobby / game) and the in-game view are in-memory state (lib/app-nav.svelte.ts), not URLs, so the address bar never changes. Browser Back/Forward move between screens via shallow routing without touching the URL — a model that also suits the bundled standalone targets (Wails / Capacitor) that have no URLs. One active view occupies the main area at a time; the sidebar holds a single tool (calculator, inspector, or order) with persistent state on switch. Seedocs/navigation.md.
Repository layout
ui/
├── PLAN.md staged implementation plan
├── ROADMAP.md planned desktop / mobile / multi-turn features
├── PLAN-finalize.md PWA, accessibility, localisation, error UX
├── Makefile wasm / ts-protos / web / mobile / desktop targets
├── README.md this file
├── buf.gen.yaml local-plugin TS Protobuf-ES generator
├── docs/ topic-based design notes
│ ├── auth-flow.md email-code login, session store, revocation
│ ├── i18n.md translation primitive, native-name picker, extensibility
│ ├── order-composer.md order draft model, persistence, history-mode wiring
│ ├── storage.md web KeyStore/Cache, IDB schema, baseline
│ ├── testing.md per-PR / release test tiers
│ └── wasm-toolchain.md TinyGo build, JSDOM loading, bundle budget
├── core/ ui/core Go module (canonical bytes, keypair)
├── wasm/ TinyGo entry point exposing Core to JS
├── mobile-bridge/ gomobile bindings (planned — see ROADMAP.md)
├── desktop/ Wails project (planned — see ROADMAP.md)
├── mobile/ Capacitor project (planned — see ROADMAP.md)
└── frontend/ SvelteKit / Vite source
├── src/api/ GalaxyClient + typed Connect client + auth + session
├── src/lib/ app-shell nav + screens + game shell, env config, session store, stores
├── src/platform/core/ Core interface + WasmCore adapter
├── src/platform/store/ KeyStore/Cache interfaces + web adapter
├── src/proto/ generated Protobuf-ES + Connect descriptors + FlatBuffers TS bindings
├── src/routes/ single-URL app-shell: `/game/` dispatcher (+page.svelte) + `/__debug/*`
└── static/ core.wasm + wasm_exec.js (built by `make wasm` / CI; gitignored)
Linked topic docs:
docs/navigation.md— single-URL app-shell, screens and views as in-memory state, screen history, sidebar tools.docs/auth-flow.md— email-code login, session store state machine, revocation watcher.docs/lobby.md— lobby UI sections, application / invite lifecycle, create-game form defaults.docs/i18n.md— translation primitive, native-name language picker, recipe for adding a new locale.docs/storage.md— web KeyStore/Cache, IndexedDB schema, browser baseline.docs/order-composer.md— local order draft store, persistence, history-mode wiring.docs/wasm-toolchain.md— TinyGo build, loading recipe, bundle size budget.docs/design-system.md— design tokens, light/dark theming, component conventions.docs/a11y.md— WCAG 2.2 AA approach, axe + keyboard gates, shared a11y primitives.docs/error-state-ux.md— central error surface, shared loading/empty/error states, selection marker, sheet dismissal.docs/pwa-strategy.md— installable, offline PWA: service worker, manifest, icons.docs/testing.md— Tier 1 per-PR + Tier 2 release test tiers.
Build pipeline
Every cross-target build flows through make at this level.
Native targets are placeholders until their platform work lands;
running make with no arguments prints the current placeholder map.
make web Vite production build
make wasm TinyGo → core.wasm
make ts-protos Connect-ES + Protobuf-ES gen
make fbs-ts FlatBuffers TS bindings via flatc
make gomobile gomobile bind → ios + android (planned — see ROADMAP.md)
make desktop-mac Wails build for darwin (planned — see ROADMAP.md)
make desktop-win Wails build for windows (planned — see ROADMAP.md)
make desktop-linux Wails build for linux (planned — see ROADMAP.md)
make ios Capacitor + xcodebuild (planned — see ROADMAP.md)
make android Capacitor + gradle (planned — see ROADMAP.md)
make all every target above
Local development
For UI work against a real stack, the tools/local-dev/ docker
compose brings up postgres + redis + mailpit + backend + gateway in
one command, and ui/frontend/.env.development is already wired to
talk to it:
make -C tools/local-dev up # build + start, wait for healthy
pnpm -C ui/frontend dev # Vite on the host
# UI: http://localhost:5173
# Mailpit: http://localhost:8025
The stack accepts a fixed dev code (123456) in addition to the
real Mailpit-delivered one. Full runbook in
../tools/local-dev/README.md.
For testing the production-shaped surface — Caddy in front of the
gateway, statically served UI bundle, real https://*.galaxy.lan
hostnames — use the long-lived dev environment at
../tools/dev-deploy/. It is
redeployed by Gitea Actions on every merge into development.
Topic docs
Topic docs live under ui/docs/ (testing tiers, WASM toolchain,
navigation shell, renderer internals, sync protocol, auth flow, and
so on).
Cross-references
PLAN.md— staged implementation plan (historical record of completed work).ROADMAP.md— planned desktop / mobile / multi-turn projection features.PLAN-finalize.md— PWA, accessibility, localisation, error UX finalization work.../docs/ARCHITECTURE.md— platform architecture and the transport security model (§15) the client envelope contract derives from.../docs/FUNCTIONAL.md— per-domain user stories that drive the UI flows.../docs/TESTING.md— project-wide testing layers; UI-specific test tiers (Vitest, Playwright) live inui/docs/testing.md.