feat(admin-console): server-rendered operator console at /_gm #87

Merged
developer merged 6 commits from feature/admin-console into development 2026-05-31 19:07:48 +00:00
Owner

Adds a server-rendered operator console at /_gm, exposed through the gateway behind the existing admin_accounts Basic Auth. The backend already shipped a complete admin JSON API (/api/v1/admin/*) but had no UI and no public path; this PR adds the operator-facing console and routes it through the gateway's edge protections.

What's included

  • gateway: new admin anti-abuse route class (per-IP rate limit + body/method limits) and a reverse proxy for /_gm/* to the backend, preserving Host, relaying the backend 401 Basic Auth challenge, and answering 502 when the backend is down. Authentication is delegated to the backend.
  • backend: server-rendered console (html/template) under the existing admin Basic Auth, with a stateless HMAC CSRF guard + same-origin check. Domains: dashboard/monitoring (opsstatus, built on go-jet), users (block / entitlement / soft-delete), games + runtime + engine versions, operators (admin accounts), and mail + notifications + multi-game broadcast.
  • dev-deploy: Caddy route /_gm/*, a bootstrap operator + stable CSRF key, and the Prometheus /metrics exporters enabled (forward-compat for a future Prometheus/Grafana stack).

Verification

  • Unit tests on both layers (gateway proxy + classifier; backend render / CSRF / per-domain with fakes), a testcontainer test for opsstatus, and an integration test driving /_gm end to end through the real gateway to the backend.
  • Previewed on the dev environment: unauthenticated -> 401 + Basic challenge; authenticated -> dashboard renders; assets served.

Notes

  • Unblock is intentionally absent (the admin API exposes no remove-sanction endpoint).
  • The dashboard reports backend-visible state; cross-service metrics arrive with the future Prometheus/Grafana task.

Docs updated: ARCHITECTURE 14.1/16, FUNCTIONAL 10.2.1 (+ru mirror), backend & gateway READMEs, and backend/docs/admin-console.md.

Implemented in 6 CI-green stages on feature/admin-console.

Adds a server-rendered operator console at `/_gm`, exposed through the gateway behind the existing `admin_accounts` Basic Auth. The backend already shipped a complete admin JSON API (`/api/v1/admin/*`) but had no UI and no public path; this PR adds the operator-facing console and routes it through the gateway's edge protections. ## What's included - **gateway**: new `admin` anti-abuse route class (per-IP rate limit + body/method limits) and a reverse proxy for `/_gm/*` to the backend, preserving `Host`, relaying the backend 401 Basic Auth challenge, and answering 502 when the backend is down. Authentication is delegated to the backend. - **backend**: server-rendered console (`html/template`) under the existing admin Basic Auth, with a stateless HMAC CSRF guard + same-origin check. Domains: dashboard/monitoring (`opsstatus`, built on go-jet), users (block / entitlement / soft-delete), games + runtime + engine versions, operators (admin accounts), and mail + notifications + multi-game broadcast. - **dev-deploy**: Caddy route `/_gm/*`, a bootstrap operator + stable CSRF key, and the Prometheus `/metrics` exporters enabled (forward-compat for a future Prometheus/Grafana stack). ## Verification - Unit tests on both layers (gateway proxy + classifier; backend render / CSRF / per-domain with fakes), a testcontainer test for `opsstatus`, and an integration test driving `/_gm` end to end through the real gateway to the backend. - Previewed on the dev environment: unauthenticated -> 401 + Basic challenge; authenticated -> dashboard renders; assets served. ## Notes - Unblock is intentionally absent (the admin API exposes no remove-sanction endpoint). - The dashboard reports backend-visible state; cross-service metrics arrive with the future Prometheus/Grafana task. Docs updated: ARCHITECTURE 14.1/16, FUNCTIONAL 10.2.1 (+ru mirror), backend & gateway READMEs, and `backend/docs/admin-console.md`. Implemented in 6 CI-green stages on `feature/admin-console`.
developer added 6 commits 2026-05-31 19:02:01 +00:00
Add the server-rendered operator console at /_gm, exposed publicly through
the gateway behind the existing admin_accounts Basic Auth.

Backend:
- new internal/adminconsole package (html/template Renderer, stateless HMAC
  CSRF signer, embedded stylesheet)
- /_gm route group reusing basicauth.Middleware(admin.Service) + a CSRF guard
  (per-operator token + same-origin check); dashboard landing page
- BACKEND_ADMIN_CONSOLE_CSRF_KEY config (per-process random fallback)

Gateway:
- new "admin" public route class (per-IP rate limit, body + GET/HEAD/POST
  method limits) classifying /_gm traffic
- reverse proxy to the backend /_gm surface, preserving Host and relaying the
  backend 401 Basic Auth challenge; 502 when the backend is unreachable
- GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_ADMIN_* config

dev-deploy:
- Caddy routes /_gm/* to the gateway
- bootstrap admin + stable CSRF key; enable Prometheus /metrics exporters on
  backend and gateway (forward-compat for a future Prometheus/Grafana stack)

Docs: ARCHITECTURE 14.1/16, FUNCTIONAL 10.2.1 (+ru mirror), backend and
gateway READMEs, new backend/docs/admin-console.md.

Tests: renderer + CSRF unit tests; backend router auth/render/asset/CSRF;
gateway classifier, proxy forwarding/Host/401/405/413/429/502.
feat(admin-console): Stage 2 — dashboard monitoring
Tests · Go / test (push) Successful in 1m58s
985e51d25e
Turn the console landing page into an operational dashboard.

- new internal/opsstatus: read-only Postgres projection via go-jet — ping +
  per-status COUNT/GROUP BY on runtime_records, mail_deliveries,
  notification_routes, and a malformed-intent count; degrades per-probe into
  Snapshot.Errors rather than failing the page
- dashboard renders backend readiness, database health, the three status
  tables, the malformed count, and any collection errors; falls back to a
  "monitoring not wired" note when no reader is injected
- AdminConsoleHandlers now takes an AdminConsoleDeps struct (Monitor + Ready
  added) so later stages add service refs without churning the signature

Tests: opsstatus store test against a Postgres testcontainer (empty schema +
one enqueued delivery); dashboard render tests with a fake reader (with and
without monitoring).

Docs: ARCHITECTURE 14.1 + FUNCTIONAL 10.2.1 (+ru) describe the dashboard.
(Prometheus /metrics exporters were already enabled in dev-deploy in Stage 1.)
feat(admin-console): Stage 3 — users domain
Tests · Go / test (push) Successful in 1m56s
cf34710b4f
Add the operator console's user-administration pages over the existing
*user.Service (no new business logic).

- GET  /_gm/users            paginated account list
- GET  /_gm/users/{id}       account detail: profile, entitlement, sanctions
- POST /_gm/users/{id}/block        apply permanent_block (reason required)
- POST /_gm/users/{id}/entitlement  set the entitlement tier
- POST /_gm/users/{id}/soft-delete  soft-delete the account (cascades)

The console depends on a UserAdmin interface (satisfied by *user.Service) so the
pages render in tests without a database. All writes flow through the CSRF
guard, carry the operator as the audit actor, and answer with a 303 redirect;
a generic message page handles not-found, validation, and failure notices.
Unblock is intentionally absent — the admin API exposes no remove-sanction
endpoint.

Tests: list/detail render, not-found, block (with actor/scope/reason
assertions), missing-reason 400, bad-CSRF 403, entitlement, soft-delete
redirect, and the service-unavailable path.

Docs: backend/docs/admin-console.md gains the page inventory.
feat(admin-console): Stage 4 — games & runtimes domain
Tests · Go / test (push) Successful in 1m58s
ecfb2d3351
Add the games, runtime, and engine-version pages over the existing lobby,
runtime, and engine-version services (no new business logic).

- GET/POST /_gm/games                         list + create public game
- GET      /_gm/games/{id}                    detail incl. runtime snapshot
- POST     /_gm/games/{id}/force-start|stop    game state actions
- POST     /_gm/games/{id}/ban-member          ban a member (uuid + reason)
- POST     /_gm/games/{id}/runtime/restart|patch|force-next-turn
- GET/POST /_gm/engine-versions               registry + register
- POST     /_gm/engine-versions/{ver}/disable disable a version

Console depends on GameAdmin / RuntimeAdmin / EngineVersionAdmin interfaces
(satisfied by the concrete services) so the pages render in tests without a
database. Collection-mutating POSTs are mounted on the collection path to avoid
a static-vs-param route conflict in gin. Writes flow through the CSRF guard and
redirect back; the create form parses datetime-local as UTC.

Tests: list/detail (with and without a runtime), create (visibility/owner/time
assertions), force-start (+ bad-CSRF), ban-member (+ bad uuid), runtime patch
(+ missing version), engine-version list/register/disable, and unavailable.

Docs: backend/docs/admin-console.md page inventory extended.
feat(admin-console): Stage 5 — operators (admin accounts)
Tests · Go / test (push) Successful in 1m59s
87a272166b
Add the operator-management page over *admin.Service (no new business logic).

- GET/POST /_gm/operators                       list + create operator
- POST     /_gm/operators/{user}/disable|enable  toggle access
- POST     /_gm/operators/{user}/reset-password  set a new password

Console depends on an OperatorAdmin interface (satisfied by *admin.Service) so
the page renders in tests without a database. Create POST is mounted on the
collection path; per-row disable/enable/reset are guarded by the CSRF middleware
and redirect back. Passwords are never logged.

Tests: list render, create (+ username/password assertions), username-taken
conflict, disable/enable, reset (+ password assertion), missing-password 400,
bad-CSRF 403, and unavailable 503.

Docs: backend/docs/admin-console.md page inventory extended.
feat(admin-console): Stage 6 — mail & notifications domain
Tests · Go / test (push) Successful in 2m2s
Tests · Go / test (pull_request) Successful in 1m59s
Tests · Integration / integration (pull_request) Successful in 1m43s
7cac910de4
Add the mail, notifications, and broadcast pages over the mail, notification,
and diplomail services (no new business logic), completing the operator console.

- GET  /_gm/mail                         deliveries (paginated) + dead-letters
- GET  /_gm/mail/deliveries/{id}         delivery detail + attempts
- POST /_gm/mail/deliveries/{id}/resend  re-enqueue a non-sent delivery
- GET  /_gm/notifications                notifications + dead-letters + malformed
- GET/POST /_gm/broadcast                multi-game admin diplomatic broadcast

Console depends on MailAdmin / NotificationAdmin / DiplomailAdmin interfaces
(satisfied by the concrete services); pages render in tests without a database.
Delivery detail and dead-letters live under /_gm/mail/deliveries/* and
/_gm/mail/... static segments to avoid a param/static route conflict. Resend
and broadcast flow through the CSRF guard.

Tests: mail page, delivery detail (+ not-found), resend (+ bad-CSRF),
notifications overview, broadcast form + send (input assertions) + bad game
ids, and unavailable. Plus an integration test that drives /_gm end to end
through the real gateway → backend (401 challenge + authenticated dashboard).

Docs: backend/docs/admin-console.md page inventory completed.
owner approved these changes 2026-05-31 19:06:56 +00:00
developer merged commit 26f1e62924 into development 2026-05-31 19:07:48 +00:00
developer deleted branch feature/admin-console 2026-05-31 19:07:48 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/galaxy-game#87