Phase 28: diplomatic mail UI (work in progress) #11
Reference in New Issue
Block a user
Delete Branch "feat/ui-stage-28"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 28 (diplomatic mail view) — work in progress.
This PR will be built up commit-by-commit on the feature branch as the stage progresses through the per-step plan in
ui/PLAN.md§28.Step 1 (this commit): backend support for race-name addressed mail. Adds
diplomail_messages.sender_race_namesnapshot (player senders) and an alternativerecipient_race_namefield on the user-mail send endpoints. The in-game UI consumes these to group personal mail into per-race threads without an extra membership-listing RPC.Follow-up steps (FBS schemas, gateway translators, UI store + view, push handler, badge, i18n, docs, tests, CI gate) will land as additional commits on this branch.
Adds the gateway-side translation layer that maps the eight new ConnectRPC mail commands onto backend's `/api/v1/user/games/{game_id}/mail/*` REST endpoints. - `gateway/internal/backendclient/mail_commands.go` defines `ExecuteMailCommand` and one helper per command (inbox, sent, message.get, send, broadcast, admin, read, delete). Each helper decodes the FlatBuffers request envelope, issues the REST call via the existing `*RESTClient.do`, decodes the JSON body, and re-encodes a typed FlatBuffers response. Recipient identifiers travel through unchanged so the new `recipient_race_name` shortcut introduced in Step 1 reaches backend untouched. - `routes.go` exposes a `MailRoutes` constructor and a matching `mailCommandClient` implementing `downstream.Client`. - `cmd/gateway/main.go` registers the new routes alongside the existing user / lobby / game-engine routes. - `mail_commands_test.go` covers the inbox, send-by-race-name, and read-state paths end-to-end against an `httptest.Server`, asserting request shapes (path, body, X-User-ID) and the decoded FlatBuffers response. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Adds `src/lib/mail-store.svelte.ts` — the reactive store that coordinates the in-game mail view. Responsibilities: - holds the inbox and sent listings for the current game and fires the initial parallel fetch (`fetchInbox` + `fetchSent`) on `setGame`; - exposes a `entries` derived rune that builds the unified list pane: per-race threads merged from incoming + outgoing personal messages, plus stand-alone items for system / admin / own paid-tier broadcasts. Thread messages are sorted oldest → newest for chat-style rendering; the list itself sorts newest-first by the most-recent entry timestamp; - derives `unreadCount` from `readAt === null` rows for the header view-menu badge; - imperative `markRead` / `softDelete` actions with optimistic state flips and roll-back on RPC failure; - compose actions for personal / paid-tier broadcast / owner-admin sends; - `applyPushEvent(gameId)` hook called by the layout when a `diplomail.message.received` push frame arrives; refetches the inbox without trusting the preview payload; - persists the most recent message id under `cache.diplomail/${gameId}/last-seen` so a returning session can pre-paint the badge without a network round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Step 7 — header view-menu badge. `view-menu.svelte` reads `mailStore.unreadCount` and renders an inline pill next to the "diplomatic mail" entry whenever the counter is non-zero. The badge styling matches the per-row dot in `thread-list.svelte` so the two surfaces feel consistent. Step 8 — push event handler + MailStore init in the in-game layout. `routes/games/[id]/+layout.svelte`: - registers a `diplomail.message.received` handler alongside the existing `game.turn.ready` / `game.paused` ones, parses the signed payload, calls `mailStore.applyPushEvent` to refresh the inbox for the matching game, and raises a toast with a "view" deep-link that navigates to `/games/:id/mail`; - adds `mailStore.init({ client, cache, gameId })` to the boot `Promise.all` so the inbox + sent lists are warm by the time the view mounts, and the badge counter is populated before any user interaction; - disposes the new subscription in the `onDestroy` block so a game switch does not leak handlers across navigations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>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>Two problems showed up while trying to log into the long-lived dev environment with the dev-fixed code `123456`: 1. `ConfirmEmailCode` checked the per-challenge attempts ceiling *before* the dev-fixed-code override. A developer who burned past `ChallengeMaxAttempts` on an existing un-consumed challenge (easy to trigger when the throttle reuses one challenge_id) hit `ErrTooManyAttempts` and the UI rendered "code expired or already used" even though the fixed code was correct. Reorder so the dev-fixed-code branch runs first and bypasses both the bcrypt verify and the attempts gate. Production stays unaffected because production loaders refuse to set `DevFixedCode`. 2. `dev-deploy.yaml` only fires on push to `development`, so the matching docker-compose default change for `BACKEND_AUTH_DEV_FIXED_CODE` could not reach the running stack before this PR merged. Add `workflow_dispatch: {}` so a developer can deploy any branch — typically a feature branch under review — from the Gitea Actions UI without waiting for the merge. Covered by a new `TestConfirmEmailCodeDevFixedCodeBypassesAttemptsCeiling` integration test that burns through the ceiling with wrong codes then proves the dev-fixed code still produces a session. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Two issues surfaced once the long-lived dev environment finally reached the diplomail view: 1. `/sent` returns one row per recipient for broadcast and admin fan-outs (so the admin tooling can render the materialised audience). The list pane fed all rows into the stand-alone bucket, so the `{#each entries as e (entryKey(e))}` key in `thread-list.svelte` collapsed to the same `standalone:${id}` for every recipient and Svelte 5 aborted the render with `each_key_duplicate`. Dedupe stand-alones by `message_id` in `buildEntries`. 2. The compose dialog exposed an `admin` kind toggle gated on "owner of game". That was a Phase 28 plan decision, but admin compose is an operator tool (server admin), not an in-game action — every game owner should not be able to broadcast admin notifications. Drop the admin option, the audience sub-toggles, and the admin path through `submit`. The `MailStore.composeAdmin` wrapper and the backend RPC stay so the future admin UI can call them. Vitest covers the fan-out dedup with three rows sharing one `message_id` collapsing to a single stand-alone entry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>