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

Server-rendered admin console in the backend at /_gm (internal/adminconsole),
fronted on the gateway's public listener by Basic-Auth + a verbatim reverse proxy
(mounted on the edge mux below the h2c wrap). A same-origin check guards its POSTs;
no operator identity is tracked. This supersedes the Stage 6 gateway-fronts-
/api/v1/admin model: GATEWAY_ADMIN_ADDR and the backend /api/v1/admin ping are
dropped and gateway/internal/admin is repurposed to the verbatim proxy.

- Complaints: migration 00008 (+ jetgen) adds disposition/resolution_note/
  resolved_at/applied_in_version + the deferred status CHECK; resolution feeds a
  query-derived pending dictionary-change pipeline (marked applied after a reload).
- Dictionary hot-reload: per-version subdir BACKEND_DICT_DIR/<version>/ via the new
  Registry.LoadAvailable; engine.OpenWithVersions restores resident versions on
  restart. Partially addresses TODO-2.
- Broadcasts: a backend Telegram-connector client (internal/connector,
  BACKEND_CONNECTOR_ADDR) for SendToUser / SendToGameChannel (discharges the Stage 9
  forward-note).
- Admin reads: account.ListAccounts/CountAccounts/Identities and
  game.ListGames/CountGames/GameByID/ListComplaints/GetComplaint/CountComplaints/
  ResolveComplaint/DictionaryChanges/MarkChangesApplied.
- Tests: adminconsole render, engine reload, same-origin guard, gateway verbatim
  proxy + h2c console mount, inttest complaint pipeline + list/count + /_gm console.
- Docs: PLAN (Stage 10 done + refinements + TODO-2), ARCHITECTURE §1/§5/§6/§12/§13,
  FUNCTIONAL (+_ru), TESTING, backend/gateway READMEs.
This commit is contained in:
Ilia Denisov
2026-06-04 09:24:59 +02:00
parent 4c4beace85
commit aafdd46a4b
49 changed files with 2548 additions and 200 deletions
+58
View File
@@ -67,6 +67,16 @@ type Account struct {
UpdatedAt time.Time
}
// Identity is one of an account's platform/email identities, surfaced on the
// admin account-detail view. ExternalID is the platform user id (or the email
// address for an email identity); Confirmed tracks the email confirm-code flow.
type Identity struct {
Kind string
ExternalID string
Confirmed bool
CreatedAt time.Time
}
// Store is the Postgres-backed query surface for accounts and identities.
type Store struct {
db *sql.DB
@@ -187,6 +197,54 @@ func (s *Store) IdentityExternalID(ctx context.Context, accountID uuid.UUID, kin
return row.ExternalID, nil
}
// Identities returns the account's platform/email identities, oldest first, for
// the admin account-detail view.
func (s *Store) Identities(ctx context.Context, accountID uuid.UUID) ([]Identity, error) {
stmt := postgres.SELECT(table.Identities.AllColumns).
FROM(table.Identities).
WHERE(table.Identities.AccountID.EQ(postgres.UUID(accountID))).
ORDER_BY(table.Identities.CreatedAt.ASC())
var rows []model.Identities
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
return nil, fmt.Errorf("account: list identities %s: %w", accountID, err)
}
out := make([]Identity, 0, len(rows))
for _, r := range rows {
out = append(out, Identity{Kind: r.Kind, ExternalID: r.ExternalID, Confirmed: r.Confirmed, CreatedAt: r.CreatedAt})
}
return out, nil
}
// ListAccounts returns accounts for the admin user list, newest first, paginated
// by limit and offset.
func (s *Store) ListAccounts(ctx context.Context, limit, offset int) ([]Account, error) {
stmt := postgres.SELECT(table.Accounts.AllColumns).
FROM(table.Accounts).
ORDER_BY(table.Accounts.CreatedAt.DESC()).
LIMIT(int64(limit)).
OFFSET(int64(offset))
var rows []model.Accounts
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
return nil, fmt.Errorf("account: list accounts: %w", err)
}
out := make([]Account, 0, len(rows))
for _, r := range rows {
out = append(out, modelToAccount(r))
}
return out, nil
}
// CountAccounts returns the total number of accounts, for admin-list pagination.
func (s *Store) CountAccounts(ctx context.Context) (int, error) {
stmt := postgres.SELECT(postgres.COUNT(table.Accounts.AccountID).AS("count")).
FROM(table.Accounts)
var dest struct{ Count int64 }
if err := stmt.QueryContext(ctx, s.db, &dest); err != nil {
return 0, fmt.Errorf("account: count accounts: %w", err)
}
return int(dest.Count), nil
}
// findByIdentity joins identities to accounts and returns the matching account,
// or ErrNotFound.
func (s *Store) findByIdentity(ctx context.Context, kind, externalID string) (Account, error) {