Stage 10: admin console & dictionary ops (complaint review, hot-reload, broadcasts) (#11)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 13s

This commit was merged in pull request #11.
This commit is contained in:
2026-06-04 07:27:49 +00:00
parent 4c4beace85
commit 3a640a17a4
49 changed files with 2548 additions and 200 deletions
+49 -2
View File
@@ -43,7 +43,7 @@ independent (see ARCHITECTURE §9.1).
| 7 | UI — playable slice + UX polish (Svelte+Vite, board, lobby, chat, hint/word-check, i18n) | **done** |
| 8 | UI — social/account/history (friends, blocks, invitations, profile edit, stats, history/GCG) | **done** |
| 9 | Telegram integration (bot side-service, deep-link, push) | **done** |
| 10 | Admin & dictionary ops (complaint review, version reload) | todo |
| 10 | Admin & dictionary ops (complaint review, version reload) | **done** |
| 11 | Account linking & merge | todo |
| 12 | Polish (observability, perf with evidence, deploy) | todo |
@@ -693,6 +693,51 @@ Open details: deployment target/host; dashboards; load expectations.
the sweeper skip when CI ran with `now-1h` inside it) was made deterministic by
clearing the test account's away window.
- **Stage 10** (interview + implementation):
- **Admin console = backend-rendered `/_gm`, gateway Basic-Auth** (interview, two
rounds): the owner chose a dedicated web console but, pointing at `../galaxy-game`
and asking to keep it simple, the deliverable is **server-rendered Go
`html/template` + one embedded CSS** (`backend/internal/adminconsole`: a
framework-agnostic renderer + page view-models, `//go:embed` templates/assets, zero
JS, no build step), **not** a SPA. It lives **in the backend** on its own route
`/_gm/*`; the **gateway** (the project's built-in reverse proxy) gates `/_gm/*` with
the existing `GATEWAY_ADMIN_USER/PASSWORD` Basic-Auth on its **public** listener and
proxies **verbatim** to backend `/_gm/*` (mounted on the edge mux below the h2c wrap
so Connect keeps working). This **supersedes Stage 6's** gateway-fronts-
`/api/v1/admin` model: the separate admin port `GATEWAY_ADMIN_ADDR` is dropped (only
the port — user/password stay), the backend `/api/v1/admin` group + `ping` are
removed, and `gateway/internal/admin` is repurposed to the verbatim proxy. The
backend keeps **no operator identity** and no `admin_accounts` table; CSRF on the
console's POSTs is a **same-origin** check (`Origin`/`Referer` vs `Host`, the gateway
preserves the inbound Host) — no token. Discharges Stage 1's "admin bootstrap" (it is
config, not a DB seed).
- **Complaint resolution + dictionary pipeline** (interview): migration **00008**
(+ jetgen) adds `disposition`/`resolution_note`/`resolved_at`/`applied_in_version`
to `complaints` and the deferred `status` CHECK (`open|resolved`) — **discharges
Stage 3's** deferral (no `resolved_by`: operator identity is not tracked). Resolution
sets a disposition (`reject`/`accept_add`/`accept_remove`); accepted complaints are
**derived by query** into a pending dictionary-change list (no new table), stamped
`applied_in_version` once a rebuilt version is loaded. New `game` reads
`ListComplaints`/`GetComplaint`/`CountComplaints`/`ResolveComplaint`/
`DictionaryChanges`/`MarkChangesApplied`; admin list/count reads
`account.ListAccounts/CountAccounts/Identities` and `game.ListGames/CountGames/
GameByID`.
- **Dictionary hot-reload = per-version subdir** (interview): the launch version stays
in the flat `BACKEND_DICT_DIR` (CI/dev untouched); a reloaded version `X` loads from
`BACKEND_DICT_DIR/X/` via the new `Registry.LoadAvailable` (present variants only),
and boot re-loads every subdirectory via `engine.OpenWithVersions` so reloaded
versions survive a restart. **Partially addresses TODO-2** (the runtime reload
contract; the offline DAWG generator stays future work).
- **Operator broadcasts** (discharges Stage 9's forward-note): the backend gains its
own connector gRPC client (`backend/internal/connector`, `BACKEND_CONNECTOR_ADDR`,
nil when unset) over the existing `pkg/proto/telegram/v1`; the console messages a
user by `account_id` (backend resolves the Telegram `external_id`) and posts to the
game channel via `SendToUser`/`SendToGameChannel`.
- **Config/CI**: backend adds `BACKEND_CONNECTOR_ADDR`; gateway drops
`GATEWAY_ADMIN_ADDR` (keeps user/password). No new module and no fbs/proto/UI codegen
(the console is server-rendered Go). The Go workflows already span
`./backend/... ./gateway/... ./pkg/...`; integration stays `./backend/...`.
## Deferred TODOs (cross-stage)
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
@@ -710,7 +755,9 @@ Open details: deployment target/host; dashboards; load expectations.
git submodule (the ~0.50.7 MB DAWGs are regenerated wholesale and bloat git
history); pin by tag/hash for a reproducible startup set. A submodule/LFS pull
is a **deploy-time** way to populate the directory, **not** the runtime
dynamic-reload mechanism (Stage 10) — keep the `BACKEND_DICT_DIR` directory as
dynamic-reload mechanism (**implemented in Stage 10**: a per-version subdirectory
`BACKEND_DICT_DIR/<version>/` loaded via `Registry.LoadAvailable`, restart-restored by
`engine.OpenWithVersions`) — keep the `BACKEND_DICT_DIR` directory as
the runtime contract: a new `.dawg` appears in it and is loaded with
`dawg.Load`.
- **TODO-3 — garbage-collect abandoned guest accounts.** Stage 6 makes a guest a