Skip to content

Admin UI

Status: 🚧 Partial β€” Admin-UI page/component inventory, live-status dashboard, universe editor, and the regional governance tabs are real. (Re-verified 2026-09-03 vs Sectorwars2102 feat tip 489e31851 β‰₯ 40849442b.) Tip-delta: #1452 MERGED (baseline @ 40849442b); #1453/#1454/#1455 now MERGED (not open). tip-PRESENT β‰  Soft-HOLD unlock β€” LEG-273 KEEP. Prior tip stamps (96c968ad / df11cf380 / densify merge trains through #778/#792/#794/MessageModeration Soft-HOLD #1114/#1115 CLOSED) remain tip-ancestors of 489e31851 β€” body tip-honesty bullets below still apply; do not read Soft-HOLD unlocks from this re-stamp.

The administrative web app at services/admin-ui/. Used by operators to manage the universe, players, economy, security, multi-regional deployment, and the AI dialogue subsystem.


1. Tech stack

From services/admin-ui/package.json on tip 96c968ad:

Layer Choice
Framework React 19.2 + TypeScript 7.0
Build Vite 8.2
Routing react-router-dom 7.18
HTTP axios 1.19
WebSockets socket.io-client 4.8
Charting chart.js 4.4 + react-chartjs-2; d3 7.8 for custom viz
i18n i18next 26.3 + react-i18next 17.0 + browser-languagedetector + http-backend
Tests Vitest 4.1 (npm test); Playwright 1.62 (E2E)
Lint ESLint 10 + @typescript-eslint 8

State management: React Context (no external store).

Auth: JWT, integrated via contexts/AuthContext.tsx and utils/auth.ts.

Default ports: admin-ui on 3001, gameserver on 8080.


2. Project layout

services/admin-ui/src/
β”œβ”€β”€ App.tsx, main.tsx, i18n.ts
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ admin/        Player asset / detail editors, intervention panels, bulk ops
β”‚   β”œβ”€β”€ ai/           Market prediction, route optimization, behavior analytics
β”‚   β”œβ”€β”€ analytics/    Custom report builder, performance metrics, predictive
β”‚   β”œβ”€β”€ auth/         LoginForm, MFASetup, MFAVerification, ProtectedRoute, UserProfile
β”‚   β”œβ”€β”€ charts/       Combat activity, fleet health, market health, price, team strength
β”‚   β”œβ”€β”€ colonization/ Colony overview, genesis tracking, planetary mgmt, production
β”‚   β”œβ”€β”€ combat/       CombatFeed, DisputePanel
β”‚   β”œβ”€β”€ common/       LanguageSwitcher, PageLoader
β”‚   β”œβ”€β”€ first-login/  ConversationDetailModal, ConversationFilters, ConversationTable
β”‚   β”œβ”€β”€ layouts/      AppLayout, Sidebar
β”‚   β”œβ”€β”€ pages/        top-level page components incl. ScopesManager (see Section 3)
β”‚   β”œβ”€β”€ security/     AuditLogViewer
β”‚   β”œβ”€β”€ fleet/        FleetOperationsTab
β”‚   β”œβ”€β”€ ui/           PageHeader, SystemHealthStatus
β”‚   β”œβ”€β”€ universe/     SectorEditModal, PlanetDetail(Modal), StationDetail, ColonyDetailModal, UniverseEditor
β”œβ”€β”€ contexts/         AdminContext, AuthContext, WebSocketContext
β”œβ”€β”€ services/         bangGalaxyApi.ts, resourceCatalog.ts, websocket.ts
β”œβ”€β”€ styles/, types/, utils/

utils/ holds auth.ts (plus auth.test.ts) and βœ… tip-shipped adminApiError.ts (plus adminApiError.test.ts) via #760 / board #1113 (mergeCommit d82b7990; live tip 96c968ad). There is no player-client-style services/api.ts. On Sectorwars2102 origin/feat 96c968ad, src/services/ has three files (bangGalaxyApi.ts, resourceCatalog.ts, websocket.ts) β€” not websocket-only. Most REST calls go through the shared axios instance exported as api from utils/auth.ts. Residual raw import axios on origin/feat 96c968ad (non-test; git grep -n 'import axios' <tip> -- services/admin-ui/src | grep -v test):

services/admin-ui/src/contexts/AuthContext.tsx:2:import axios from 'axios';
services/admin-ui/src/utils/auth.ts:5:import axios from 'axios';

utils/auth.ts is the transport root (intentionally raw). AuthContext.tsx remains raw for auth bootstrap. AdminContext.tsx and bangGalaxyApi.ts no longer import axios on tip 96c968ad β€” they use the shared api from utils/auth. Dashboard.tsx remains on the shared api (b28bf981 / LEG-233 / #674 is a tip ancestor). #760 (Admin LEG-1044 / board #1113) is MERGED β€” tip ancestor; do not treat older WO-branch HEADs (e.g. 31b90026 / local wo/LEG-917) as the tip. Never stamp a WO-branch HEAD as the tip.

Comparison note: prior docs falsely claimed AdminContext/bangGalaxyApi were axios-free on c2522953 by confusing a WO-branch tip with origin/feat, then later Soft-HOLD-stamped them as still raw on 3cfb1d5c. On current tip 96c968ad those two are migrated; only AuthContext + auth.ts remain raw. components/users/ is absent (UsersManager lives under components/pages/).


3. Pages

App.tsx on tip 96c968ad lazy-imports 32 routed page modules under components/pages/ β€” not 26 files each routed. Additional page-level files not their own routes include EconomyLeversPanel.tsx (embedded in EconomyDashboard) and, on tip 1f6acc0e, RankingLeaderboardPanel.tsx + βœ… tip-shipped ReEngagementQueuePanel.tsx (both embedded in PlayerAnalytics; land #772 / mergeCommit 3fbb6073). βœ… TradeDockAdmin.tsx tip-PRESENT via #604 / Soft-HOLD #1146 β€” App lazy import + /tradedocks route + Vitest; force-cancel tip-PRESENT via #792 on tip 63a2cabb1 (TradeDockAdmin.tsx:225 + LEG-2974 densify). Soft-ORDER #1561 no longer tip-pending. GS admin_construction remains tip-shipped / #598. The routed list:

Page Domain
LoginPage.tsx Auth
Dashboard.tsx Landing / overview
UniverseManager.tsx, BangGalaxyPage.tsx Universe editor + Bang-engine generation
SectorsManager.tsx, StationsManager.tsx, PlanetsManager.tsx, WarpTunnelsManager.tsx Universe drill-downs
UsersManager.tsx, PlayerAnalytics.tsx Users / players
AdvancedAnalytics.tsx Analytics
EconomyDashboard.tsx Economy
CombatOverview.tsx, FleetManagement.tsx Combat / fleets
ContractDisputeArbitration.tsx Contracts (/contract-disputes)
ColonizationManagement.tsx Planetary
TeamManagement.tsx Teams
EventManagement.tsx Game events
FactionManagement.tsx Factions
MedalAdmin.tsx Medals (/medals)
TradeDockAdmin.tsx TradeDock / construction (/tradedocks) β€” read-only v1
MessageModeration.tsx Messaging / moderation
MultiAccountReview.tsx Multi-account (/multi-account)
CentralNexusManager.tsx, RegionalGovernorDashboard.tsx Multi-regional
AITradingDashboard.tsx, FirstLoginConversations.tsx AI ops
SecurityDashboard.tsx, ScopesManager.tsx, AdminActionLogPage.tsx Security / admin scopes (RBAC) / audit (/audit)
TranslationManagement.tsx Internationalization
NotFound.tsx Catch-all (*)

Tip honesty (#769 MERGED, still true on 96c968ad): MultiAccountReview retires the stale β€œdetection sweep not shipped” banner β€” the hourly detection sweep is tip-shipped; an empty queue means no open clusters awaiting review. The same land added inline 403/429 (scope / admin rate-limit) copy on colonization panels (GenesisDeviceTracking, PlanetaryManagement, ProductionMonitoring), DisputePanel, BalanceAnalytics, Dashboard audit feed, and PerformanceMetrics. Shared adminApiError.ts coverage landed later via #760 β€” see Β§ Authorization model.

Tip honesty (#774 MERGED, still true on 96c968ad): additional inline 403/429 honesty on AI MarketPrediction / PlayerBehavior / RouteOptimization, PlayerDetailEditor / PlayerBountyPanel / PlayerAssetManager, AdvancedAnalytics generate/export, AuditLogViewer, and FleetHealthReport. Shared helper tip-shipped separately via #760 / #1113 β€” residuals in Β§ Authorization model.

Tip honesty (#783 MERGED / tip 96c968ad; #1446 DONE): EconomyDashboard.tsx β€” alertsRes (price-alerts) and summaryRes (dashboard-summary) settled rejections now push formatAdminApiError into the page error strip (same helper market/metrics already used). Empty alerts array / null summary only on honest empty payloads β€” not on 403/429 silent clears.

Tip honesty (#756 MERGED / tip 96c968ad; GS LEG-862): βœ… GS GET /api/v1/combat/history tip-PRESENT (player_combat.py:215 β€” authenticated player CombatLog list). Do not claim an Admin-UI consumer of this route as tip-shipped β€” #756 is gameserver-only; Soft-HOLD residuals above unchanged.

Tip honesty (#775 MERGED / tip 96c968ad; Admin LEG-1213/1214 + LEG-1233): βœ… universe detail editors + SystemHealthStatus β€” RBAC/rate-limit failures no longer collapse to bare Failed / console-only Offline. Shared helper utils/universeAdminError.ts tip-PRESENT. Soft-HOLD residuals above unchanged.

Tip honesty (#784 MERGED / tip 96c968ad; GS LEG-339): βœ… GS POST /api/v1/admin/construction/reservations/{id}/force-cancel tip-PRESENT. Do not claim any Admin-UI force-cancel consumer as tip-shipped β€” Soft-ORDER #1561 owns that residual. (TradeDockAdmin panel is tip-PRESENT read-only via #604; that is not the force-cancel write path.)

Tip honesty (#787 MERGED / tip 96c968ad; Admin LEG-1451/1452/1453): Soft-ORDER create-station/planet payload honesty on Admin universe create flows. Soft-HOLD residuals above unchanged β€” #787 β‰  unlock LanguageSwitcher / Beacon / bulk-moderate callers / Soft-ORDER #1561 force-cancel.

Tip honesty (#786 MERGED / tip 96c968ad; Admin LEG-1373/1443/1444/1445): Soft-ORDER StationDetail/PlanetDetail PATCH honesty tip-PRESENT. βœ… Soft-ORDER #1563/#1564 tip-PRESENT on tip 8b1546cf7 (PlanetDetail residual PATCH + StationDetail fuelΒ·luxury EditableFields via #791). Soft-HOLD residuals above unchanged β€” #786 β‰  unlock LanguageSwitcher / Beacon / bulk-moderate / acceptΒ·redactΒ·block callers / Soft-ORDER #1561 force-cancel.

Tip honesty (#788 MERGED / tip 96c968ad; Admin Soft-ORDER): βœ… tip-PRESENT β€” StationsManager.tsx (station_class POST), PlanetDetailModal.tsx, FleetManagement.tsx (+ tests) on tip. Soft-HOLD residuals above unchanged β€” #788 β‰  unlock LanguageSwitcher / Beacon / bulk-moderate / acceptΒ·redactΒ·block callers / READY #1561/#1563/#1564.

Tip honesty (#789 MERGED / tip 96c968ad; Admin Soft-ORDER LEG-1488): βœ… tip-PRESENT β€” SectorDetail.tsx EditableFields description / radiation_level / resource_regeneration (+ createPayload tests) on tip. Soft-HOLD residuals above unchanged β€” #789 β‰  unlock LanguageSwitcher / Beacon / bulk-moderate / acceptΒ·redactΒ·block callers / READY #1561/#1563/#1564. βœ… #778 LanguageSwitcher progress 403/429 honesty tip-PRESENT on feat tip 63a2cabb1 (mergeCommit 97709009a).

Tip honesty (#604 MERGED / tip 96c968ad; Soft-HOLD #1146 / LEG-71): βœ… tip-PRESENT β€” TradeDockAdmin.tsx + App lazy mount + /tradedocks route + tests. βœ… force-cancel tip-PRESENT on tip 63a2cabb1 via #792 (mergeCommit 05628624a): POST …/construction/reservations/{id}/force-cancel with ADR-0093 gate + formatAdminApiError 403/429 + TypeError densify Vitest (TradeDockAdmin.tsx:225+; TradeDockAdmin.test.tsx LEG-2974). Soft-ORDER #1561 force-cancel consumer is no longer tip-pending. βœ… LanguageSwitcher changeLanguage honesty tip-PRESENT via #1186 MERGED (see Β§ i18n). Beacon Soft-HOLD #1219 + MessageModeration Soft-HOLD #1114/#1115 + Soft-ORDER #1563/#1564 are tip-PRESENT CLOSED on 8b1546cf7 β€” see status banner.

Tip honesty (#710 MERGED / tip 96c968ad; GS LEG-264): βœ… GS GET /api/v1/medals/admin/players/{player_id}/collection tip-PRESENT β€” gameserver route. Admin UI collection consumer tip-PRESENT separately via #794. Soft-HOLD residuals unchanged β€” #710 β‰  Admin Soft-HOLD unlock.

Tip honesty (#793 MERGED / tip 96c968ad; Admin Soft-ORDER LEG-1639/#1715 + LEG-1640/#1716): βœ… tip-PRESENT β€” ReEngagementQueuePanel.tsx imports+uses formatAdminApiError on load/update catches; Sidebar.tsx { to: '/review-queue', label: 'Review Queue' } tip-PRESENT (canonical surface remains /audit?tab=review via navigate). Soft-HOLD residuals above unchanged β€” #793 β‰  unlock LanguageSwitcher / Beacon / bulk-moderate / acceptΒ·redactΒ·block callers / READY #1561/#1563/#1564. βœ… #795 tip-PRESENT (MERGED; ancestor of 8b1546cf7). βœ… #778 LanguageSwitcher progress honesty tip-PRESENT.

Tip honesty (#794 MERGED / tip 96c968ad; Admin Soft-ORDER LEG-1643/#1719): βœ… tip-PRESENT β€” MedalAdmin.tsx Player collection tab (TabId includes collection) + GET /api/v1/medals/admin/players/{player_id}/collection caller + Vitest 403/429 paths. Soft-HOLD residuals above unchanged β€” #794 β‰  Soft-HOLD unlock LanguageSwitcher / Beacon / bulk-moderate / acceptΒ·redactΒ·block callers / READY #1561/#1563/#1564. βœ… #795/#791 tip-PRESENT (MERGED; ancestors of 8b1546cf7). βœ… #792 TradeDockAdmin force-cancel tip-PRESENT. βœ… #778 LanguageSwitcher progress honesty tip-PRESENT. βœ… Soft-ORDER #1758 registry/backfill tip-PRESENT; βœ… #1759 DroneOperationsTab PATCH+DELETE tip-PRESENT on 8b1546cf7.

Tip honesty (Soft-ORDER PRs on tip 8b1546cf7): βœ… #791 Station fuel/luxury + Planet residual PATCH tip-PRESENT (#1564/#1563). βœ… #795 FleetManagement emergency repair/refuel tip-PRESENT. βœ… #778 LanguageSwitcher progress honesty tip-PRESENT. βœ… #792 TradeDockAdmin force-cancel tip-PRESENT. βœ… Soft-ORDER #1758 registry/backfill tip-PRESENT (FleetManagement.tsx). βœ… Soft-ORDER #1759 DroneOperationsTab PATCH+DELETE tip-PRESENT (DroneOperationsTab.tsx on 8b1546cf7) β€” do not remint.


4. Key features

Universe editor

UniverseManager.tsx (plus BangGalaxyPage.tsx for Bang-engine generation and the universe/ modal components) offers galaxy generation (configurable parameters, region distribution sliders, sector-by-sector edits) and clickable drill-downs to planets, ports, and warp tunnels. Backed by /api/v1/admin/galaxy/* and /api/v1/admin/sectors/* endpoints.

Bang wipe / add-region error honesty (tip 96c968ad): BangGalaxyPage.tsx wipe/add-region catches call formatAdminApiError (scopeHint admin.universe.manage; landed with #760). βœ… Context-layer MET: AdminContext.wipeGalaxy also uses formatAdminApiError with the same fallback/scopeHint (LEG-1315 / #780 / mergeCommit 1f6acc0e; still true on tip 96c968ad) β€” do not read wipeGalaxy as still collapsing to a bare Failed to wipe galaxy string. See Β§ Authorization model.

Dashboard with live status

SystemHealthStatus.tsx (in components/ui/) renders three live cards β€” Database, AI providers (OpenAI / Anthropic), Game Server β€” pulling from /api/v1/status/database, /api/v1/status/ai/providers, /api/v1/status/. Auto-refreshes; shows pool metrics, response times, configuration vs reachability state. βœ… Tip honesty (#775): probe catch paths surface 403/429 to operators via role=alert instead of console-only Offline.

Messaging moderation

MessageModeration.tsx is the flagged-message / flagged-beacon review queue (page-table row above). On HEAD 96c968ad it live-refreshes when a player flags a message: the page subscribes to WebSocket event flagged:message:alert (GS type flagged_message_alert) and reloads the queue after a debounce, with a toast (MessageModeration.tsx ~8, 224–246). Landed f36ff5f1 / LEG-414 / #705 β€” not #706 (player FLAG UI, LEG-412).

GS path: player POST /api/v1/messages/{message_id}/flag (messages.py flag_message) β†’ message_service.py ~426–444 builds {type: "flagged_message_alert", ...} and await manager.broadcast_to_admins(...). Operators do not need a manual refresh for new flags. Player FLAG flow: ../FEATURES/gameplay/messaging.md Β§ Player-facing affordances. βœ… GS POST /admin/messages/bulk-moderate tip-PRESENT via #711 / LEG-266 (admin_messages.py:100). βœ… GS canon accept / redact / block tip-PRESENT via #709 / LEG-263 (admin_moderation_messages.py). βœ… Admin-UI callers tip-PRESENT on tip 7012c53e5 β€” MessageModeration.tsx ships Accept/Redact/Block canon buttons (~384–428), POST /api/v1/admin/messages/bulk-moderate (~502), and block-count badges (LEG-1579/2690); Vitest MessageModeration.test.tsx. (re-verified 2026-08-31 vs 7012c53e5 MessageModeration.tsx:384–829.)

AI ops

  • AITradingDashboard.tsx β€” monitors AI trading interactions.
  • FirstLoginConversations.tsx + first-login/ modals/filters/table β€” review the AI-driven first-login dialogues, useful for tuning the guard personality and ship-outcome thresholds (recent migrations ec92f8afd44a, c5e32c313020, 6b1d95a38c98, 2e78250f47bc, 6acc65ee7a72 all touched this area).
  • ai/ components (MarketPredictionInterface, RouteOptimizationDisplay, PlayerBehaviorAnalytics) surface backend ML output.

Analytics

PlayerAnalytics.tsx (large) and AdvancedAnalytics.tsx, plus analytics/ (CustomReportBuilder, PerformanceMetrics, PredictiveAnalytics) and the charts/ widgets. (Re-verified 2026-09-01 vs Sectorwars2102 feat tip 2762ea5c6.) Predictive analytics tip honesty (LEG-3609 / LEG-3612 / LEG-3613): βœ… GS GET /api/v1/admin/analytics/predictions tip-PRESENT on feat tip 2762ea5c6 (LEG-3609 / admin_comprehensive.py:1621; PLAYERS_VIEW scope). βœ… Admin UI consumer tip-PRESENT β€” PredictiveAnalytics.tsx fetches /api/v1/admin/analytics/predictions via api.get + formatAdminApiError (LEG-3612 / merged #1304; mergeCommit f7f5078e). Mounted on AdvancedAnalytics.tsx Predictive tab; Vitest PredictiveAnalytics.test.tsx asserts live fetch path. The Player Metrics block on PlayerAnalytics surfaces two shipped stat cards (also present on origin/feat 48d014ee):

  • 7-day retention rate β€” data-testid="player-metrics-retention-rate"; description '7-day retention'; landed 6f25007f / LEG-376 / #690. Methodology: retention.md Β§ Retention rate. When the value is null the card shows an em-dash plus an unavailable empty state β€” not a stub slogan. βœ… Re-engagement OPEN queue β€” ReEngagementQueuePanel.tsx mounted on PlayerAnalytics (import + render ~287); tip-shipped on 1f6acc0e via #772 / mergeCommit 3fbb6073 (LEG-880). See retention.md Β§ Retention rate.
  • Average session time (hours) β€” data-testid="player-metrics-session-time"; renders average_session_time as hours; landed 118cae0f / LEG-386 / #717. Methodology: retention.md Β§ Session metrics. Same honest empty-state pattern when null.

Do not invent formulas in this page β€” those live in retention.md.

Admin Reports / Analytics API (backend contract)

βœ… Shipped β€” five read-only endpoints in services/gameserver/src/api/routes/admin_reports.py (router prefix /admin, mounted under /api/v1). All gated by admin.audit.view (AUDIT_VIEW). Consumed by the admin-ui Advanced Analytics page. Expensive generate/export calls are the intended load class for the Reports / exports tier in Β§ Admin REST rate limits below (5 / hour via classify_admin_tier β†’ ADMIN_TIER_REPORTS, enforced by RateLimitingMiddleware in services/gameserver/src/api/middleware/security.py).

Method Path Purpose
GET /api/v1/admin/reports/metrics Metric catalog (schema-driven; no DB reads).
GET /api/v1/admin/reports/templates Built-in report templates.
POST /api/v1/admin/reports/generate Run a custom report: compute live DB aggregates for each requested metric id.
GET /api/v1/admin/analytics/export Bulk dataset export (json or csv).
GET /api/v1/admin/performance/metrics System / DB / application performance snapshot.

βœ… Separate GS route (not in admin_reports.py): GET /api/v1/admin/analytics/predictions β€” market price predictions in admin_comprehensive.py:1621 (LEG-3609; PLAYERS_VIEW scope). Admin UI consumer tip-PRESENT via LEG-3612 / merged #1304; see Predictive analytics tip honesty in Β§ Analytics above.

Pydantic shapes (must stay aligned with admin-ui TypeScript interfaces):

  • ReportFilter β€” { id, field, operator, value }
  • ReportTemplate β€” { id, name, description, metrics[], filters[], groupBy[], sortBy[], visualization, chartType?, schedule? }
  • ReportResult β€” { id, name, generatedAt (ISO-8601), data (metric_id β†’ value), template }

Metric catalog categories (ids are stable; values computed by _compute_metric):

Category Metric ids
Players player_total_count, player_active_count (7d login), player_avg_credits, player_total_credits, player_avg_turns
Economy market_total_transactions, market_total_volume, market_avg_profit_margin
Galaxy sector_total_count
Combat combat_total_encounters
Fleet ship_total_count
Teams team_total_count

Player-facing aggregates exclude soft-deleted users (User.deleted == false), matching the players export filter.

Built-in templates: tpl-economy-overview, tpl-player-health, tpl-galaxy-status (each is a ReportTemplate with a fixed metric list and visualization: "table").

POST /reports/generate: body = ReportTemplate; requires 1–50 metric ids; returns ReportResult with a fresh report-{12-hex} id. Unknown metric ids resolve to 0 (never fabricated narrative data).

GET /analytics/export:

Query Values
dataset players Β· economy Β· combat Β· teams Β· ships Β· performance
format json (default) Β· csv β€” excel/pdf rejected with 400 (deps not installed)

Row cap 10β€―000 per dataset. performance returns a pointer row directing clients to GET /performance/metrics rather than duplicating that payload. Attachment filenames: {dataset}-export.{json\|csv}.

GET /performance/metrics: query timeRange ∈ 1h · 6h · 24h (default) · 7d. Response blocks:

  • system β€” activeConnections + Postgres uptime % are real; CPU/memory/disk/network/errorRate are 0.0 without psutil / in-band error tracking; requestsPerSecond is a market-txn-last-minute proxy.
  • database β€” pg_stat_activity pool breakdown, slow-query count (>1s), pg_statio_all_tables cache hit rate; queryTime stays 0 without pg_stat_statements.
  • application β€” throughput from MarketTransaction over the window; p50/p95/p99 response times are 0 (no in-band latency).
  • historical β€” 12 evenly spaced buckets of transaction volume over the window (load/error series padded with zeros).
  • suggestions β€” static, stack-specific recommendations (not live ML).

(Drafted 2026-08-07 from live admin_reports.py for WO-CANON-DRAFT-ADMIN-REPORTS-ANALYTICS-API-CONTRACT.)

Combat analytics & intervention

CombatAnalyticsService (services/gameserver/src/services/combat_analytics_service.py) backs the /admin/combat/* routes (admin_combat.py) with a live combat dashboard, gated by PLAYERS_VIEW (reads) / COMBAT_INTERVENE (writes):

  • GET /admin/combat/live β€” recent/active CombatLog rows plus active FleetBattle rows merged into one feed, with participant names, sector, damage/drone stats, and a needs_intervention flag (heuristics: >100 rounds, a >20-round stalemate under 100 damage each side, or a >50-round one-sided fight at >95%/<5% damage split).
  • POST /admin/combat/{combat_id}/intervene β€” admin interventions on an in-progress combat: stop_combat, adjust_damage (multiplier), restore_shields (tip-shipped #763 β€” mutates live Ship.combat JSONB shields to shield_percent of max_shields; not CombatLog-only), declare_winner. Every intervention (success or failure) is written to the audit log via AuditService.
  • βœ… Admin UI tip-PRESENT (#782 / tip 96c968ad; #1422–#1425 DONE): CombatOverview.tsx posts restore_shields with target + shield_percent, plus adjust_damage / declare_winner, against that intervene API. Restore is a live shield write, not audit-only UI. Pause/Reset remain unavailable on the page.
  • GET /admin/combat/balance β€” win-rate/balance analytics over completed combats, grouped by ship_type / player_level / combat_type / overall, with a 0–100 balance score (variance from an ideal 50% win rate) and outlier detection (>70% win rate = overpowered, <30% = underpowered, min 10-sample floor) feeding generated buff/nerf recommendations.
  • GET /admin/combat/disputes β€” heuristic dispute surfacing: completed combats with a >10:1 damage ratio in the last 24h, plus "repeat combat" pattern detection (>5 combats between the same attacker/defender pair within an hour, flagged as potential farming/harassment).
  • GET /admin/combat/stats, GET /admin/combat/logs, POST /admin/combat/{combat_id}/resolve, GET /admin/combat/dashboard-summary β€” ported-forward legacy endpoints (aggregate stats by time window, paginated combat-log history, manual dispute resolution, and a combined dashboard payload merging live feed + balance + disputes).

Multi-regional

CentralNexusManager.tsx and RegionalGovernorDashboard.tsx expose the Central Nexus generation + regional governance APIs (/api/v1/nexus/*, /api/v1/regions/*). The latter is one of the larger pages in the app β€” 7-tab interface (Overview, Governance, Economy, Policies, Elections, Diplomacy, Culture).

Governance write scope and honest-disabled tabs (canon, blessed by Max 2026-06-14). Governance writes are keyed by region id β€” an operator acts on a named region's governance state, not a global one β€” gated by admin.regions.* scopes. The Culture and Diplomacy tabs are honest, not faked β€” not literally disabled/unclickable (both are navigable, labeled "Diplomacy β€” limited" / "Culture β€” read-only" in the tab bar), but they render only real data (e.g. real treaties rows) with no fabricated write actions: an explicit inline note states the write endpoints don't exist rather than showing an Actions bar that does nothing. (Re-verified 2026-08-20 vs Sectorwars2102 HEAD 96c968ad.) against RegionalGovernorDashboard.tsx β€” corrected the "visibly inert" wording, which overstated how disabled the tabs actually are; the "never wired to mock data" claim held up exactly as described. Diplomacy in particular is intended to graduate into a real gameplay feature (inter-region relations), not a permanent admin-only panel β€” so its tab stays limited rather than stubbed until that feature lands.

Security & audit

SecurityDashboard.tsx for monitoring (live metrics over the audit-log feed); security/AuditLogViewer.tsx for the audit trail. MFA setup and verification components live in auth/ (MFASetup.tsx, MFAVerification.tsx). Gameserver TOTP/MFA routes: ../ARCHITECTURE/auth.md Β§ Multi-factor authentication (TOTP).

Authorization model

(Re-verified 2026-08-21 vs Sectorwars2102 HEAD 96c968ad.)

βœ… Shipped β€” the scope-based gate described below is live, not just the Release target. User.is_admin is Phase-C3 grant-derived (a correlated EXISTS expression against AdminScopeGrant), superseding the flat User.is_admin == true gate from ADR-0027 β€” see ../DATA_MODELS/admin.md for the full schema/status detail.

Per ADR-0058 A-F2 (supersedes ADR-0027), greenlit as the RBAC foundation chain by ADR-0093 item 19 (2026-07-10): admin capabilities live as a scope set, each scope assignable per-admin via AdminScopeGrant (user_id, scope, granted_by, granted_at) β€” table + 4 migrations shipped, per-endpoint scope checks wired across 32 route files.

Scope families: admin.players.* (view, suspend, adjust_rep, transfer_assets), admin.subscriptions.* (view, modify, refund), admin.webhooks.* (view, replay), admin.regions.* (view, create, terminate, transfer_ownership), admin.aria.audit, admin.multi_account.review, admin.bang.regenerate, admin.scopes.* (grant, revoke), admin.audit.view. Full list in ADR-0058.

UI surfaces (βœ… Shipped unless noted):

  • /scopes β€” scope-grant management via ScopesManager.tsx, gated by admin.scopes.grant / admin.scopes.revoke. Wired to /api/v1/admin/scopes/holders, /catalog, /grant, /revoke against AdminScopeGrant. Legacy /permissions route redirects here. The old scaffolding (PermissionsDashboard.tsx, permissions/PermissionMatrix.tsx, permissions/RoleManagement.tsx) was deleted after the live scopes API landed β€” do not document those filenames as live.
  • /audit β€” βœ… Shipped β€” append-only AdminActionLog ledger and the HIGH_IMPACT retrospective review queue, both in AdminActionLogPage.tsx (App.tsx route audit). Ledger tab: GET /api/v1/admin/audit (admin.audit.view). Review tab (?tab=review): GET /api/v1/admin/audit/review-queue + acknowledge via the audit review POST (admin.audit.review) writing reviewed_by / reviewed_at. Gameserver: services/gameserver/src/api/routes/audit.py. No pre-action two-person gate β€” accountability through scope-precision + audit + review.
  • Canonical path residual: older prose named this surface /admin/review-queue (or bare /review-queue). The Admin UI now mounts Route path="review-queue" β†’ Navigate to="/audit?tab=review" (LEG-103 / PR #659 / SHA edfdc35c). Primary surface remains /audit?tab=review β€” do not invent a duplicate review page.

A sweep-test alarm fires on the review tab (AdminActionLogPage.tsx, /audit?tab=review) when the count of 30-day-old unacknowledged HIGH_IMPACT rows meets REVIEW_QUEUE_STALE_ALARM_THRESHOLD (provisional N=3 in Admin UI PR #621 / SHA 99b10a81 β€” not a ratified canon number; code comment keeps N ≀ 5). Target remains: review acknowledged within 7 days. βœ… Shipped β€” persistent review-tab banner (role="alert", data-testid="review-queue-sweep-alarm") + Vitest on/off coverage.

Scope-gated routes will return 403 with the missing scope name in the response body β€” clients render "you need scope X" rather than a generic permission error.

Shared client helper (services/admin-ui/src/utils/adminApiError.ts) β€” βœ… tip-shipped on origin/feat 96c968ad via #760 / board #1113 (mergeCommit d82b7990; live tip 96c968ad). Helper + Vitest land with tip callers (formatAdminApiError / import of adminApiError) including PlayerDetailEditor, ColonyOverview, MessageModeration, BangGalaxyPage wipe/add-region, AdminContext.wipeGalaxy (LEG-1315 / #780 / 1f6acc0e), CombatOverview intervene paths (#782), EconomyDashboard alerts/summary (#783 / #1446), and many pages inventory surfaces. Do not claim universal coverage: residuals include other AdminContext generic Failed to … setError strings (wipeGalaxy is not among them), LanguageSwitcher progress 403/429 honesty tip-PRESENT via #778 (progressError / LanguageSwitcher.tsx:75-88,117-120) and βœ… changeLanguage honesty tip-PRESENT via #1186 MERGED / mergeCommit 0987726c3 on tip 8b1546cf7 (changeLanguageError role="alert" β€” see Β§ i18n); pages still without the helper (e.g. PlayerAnalytics.tsx generic catch β€” Admin LEG-962). UniverseManager.tsx is not a page-local formatAdmin residual on tip 96c968ad: it has no local API catch β€” it only surfaces useAdmin().error from AdminContext (Soft-ORDER / context-layer honesty owns remaining AdminContext generic strings; Soft-ORDER #1325 CLAIMED). Tip-PRESENT ReEngagementQueuePanel.tsx (mounted on PlayerAnalytics via #772 / mergeCommit 3fbb6073) now uses formatAdminApiError on load/update catches via #793 / tip 96c968ad β€” do not read #772 alone as full 403/429 coverage; #793 closed that residual. βœ… TradeDockAdmin.tsx tip-PRESENT via #604 / #1146; force-cancel tip-PRESENT via #792 on tip 63a2cabb1 (TradeDockAdmin.tsx:225 + Vitest LEG-2974) β€” Soft-ORDER #1561 no longer tip-pending. βœ… GS POST /admin/messages/bulk-moderate tip-PRESENT (#711 / LEG-266). βœ… GS canon accept / redact / block tip-PRESENT (#709 / LEG-263). βœ… Admin-UI bulk-moderate callers + accept/redact/block buttons tip-PRESENT on tip 8b1546cf7 (MessageModeration.tsx; Soft-HOLD #1114/#1115 CLOSED β€” see Tip honesty Β§ MessageModeration). βœ… Admin beacon-sector-cap UI tip-PRESENT via RegionalGovernorDashboard.tsx on tip 8b1546cf7 (Soft-HOLD #1219 CLOSED; see message-beacons.md).

ARIA per-player security-ops UI (UIADM-SECURITY-PLAYER-OPS) β€” βœ… tip-PRESENT (LEG-3607 / #1297 / Sectorwars2102 tip 4824f5a3d) β€” AriaPlayerSecurityOpsPanel.tsx mounted on SecurityDashboard.tsx:339. Consumes GET /api/v1/admin/security/player/{id}/risk, GET …/status, GET …/logs, and POST …/action; scope gate admin.aria.audit remains real (AriaPlayerSecurityOpsPanel.tsx:8). Vitest AriaPlayerSecurityOpsPanel.test.tsx (LEG-3607 log history, LEG-3544 Network Error densify). Do not read this as claiming the legacy flat-admin /admin/security/player/{id}/* endpoints in ./aria.md are scope-gated β€” this panel is the scope-gated Admin UI surface for per-player ARIA security ops.

Orphaned GC admin methods (manual grant/revoke) β€” βœ… tip-PRESENT (LEG-3617 / #1304 / tip f7f5078ea) β€” GalacticCitizenAdminPanel.tsx on EconomyDashboard.tsx (~:676); POST /api/v1/admin/players/{player_id}/galactic-citizen/grant|revoke with required reason, gated by admin.subscriptions.modify; Vitest GalacticCitizenAdminPanel.test.tsx + GalacticCitizenAdminPanel.typeErrorHonesty.test.tsx. GS routes tip-PRESENT per LEG-3616 (#1302). See ./monetization.md Β§ Manual GC grant/revoke (admin support tooling). (tip-stamped 2026-09-01.)

Admin REST rate limits

βœ… Shipped β€” admin endpoints are rate-limited per authenticated admin user (separate from the player-facing limits enforced on the realtime bus) via classify_admin_tier + ADMIN_TIER_LIMITS in services/gameserver/src/api/middleware/security.py, mounted as RateLimitingMiddleware:

Endpoint class Limit Classifier
Read (GET / HEAD / OPTIONS on /admin) 100 / minute default non-mutating admin path
Write (POST / PUT / PATCH / DELETE) 30 / minute mutating admin path (non-bulk, non-report)
Bulk operations (mutating + /bulk / /batch / /mass) 10 / minute _ADMIN_BULK_MARKERS
Reports / exports (any method + /report(s) / /export(s)) 5 / hour _ADMIN_REPORT_MARKERS β†’ ADMIN_TIER_REPORTS

Bulk operations include market interventions (price cap / floor / supply injection), combat resolution overrides, mass message moderation, and asset transfers β€” actions that touch many rows in one call. The 5-per-hour reports cap covers expensive analytics queries that scan denormalized aggregates.

i18n

common/LanguageSwitcher.tsx with full i18next integration. Backed by six-language translation files (English / Spanish / French / Portuguese / Chinese launch-complete; German Partial β€” see ./i18n.md). On tip 96c968ad the switcher seeds from static SUPPORTED_LANGUAGES filtered by LAUNCH_COMPLETE_CODES (en/es/fr/zh/pt β†’ static completionPercentage 100%, never the old non-en 0% fallback) and then refreshes each row from GET /api/v1/i18n/admin/progress/{code} when the API returns a finite percent (LanguageSwitcher.tsx:24-74; PR #720 / LEG-488 landed). German stays Partial in canon and is not in the launch-complete active set. Do not read this page as claiming Complete locales still show 0% in the Admin picker. (Re-verified 2026-09-01 vs Sectorwars2102 feat tip 8b1546cf7 / #1186 MERGED.) βœ… tip-PRESENT progress honesty via #778 (mergeCommit 97709009a): progress GET 403/401 surfaces progressError access-denied copy; 429 surfaces rate-limit copy (LanguageSwitcher.tsx:75-88,117-120). Transport/non-HTTP failures still keep the static 100% launch-complete fallback (intentional β€” not a green-bar masquerade for RBAC). βœ… tip-PRESENT changeLanguage honesty via #1186 MERGED (mergeCommit 0987726c3; Admin LEG-3236 Soft-ORDER): handleLanguageChange / i18n.changeLanguage catch sets changeLanguageError with TypeError densify + generic recoverable copy and renders a role="alert" (LanguageSwitcher.tsx:102-133 on tip 8b1546cf7) β€” operator-visible, not console-only (console.error may still log alongside). Shared adminApiError.ts is tip-shipped (Β§ Authorization model) but this switcher does not use it for either path.


5. Build / test

# from services/admin-ui/
npm run dev          # vite dev
npm run dev:watch    # with live-reload watcher
npm run build        # tsc + vite build
npm run lint         # eslint
npm test             # vitest run
npm run test:e2e     # playwright via scripts/simple-run-tests.js

E2E specs live in repo-root e2e_tests/admin/ui/ β€” 13 *.spec.ts files on tip 96c968ad: the original six (login, dashboard, universe-generation, sector-editing, sector-editing-authenticated, user-management) plus deep-link-basename, economy-dashboard-mutations, economy-levers-panel, faction-mutations, player-detail-emergency, rbac-d-scopes-audit, and translation-key-edit.