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-08-21 vs Sectorwars2102 origin/feat 96c968ad.) Tip delta from prior 46bce720 stamp is Admin #794 MedalAdmin Player collection β€” #794 β‰  Admin Soft-HOLD unlock for LanguageSwitcher / Beacon / Admin-UI bulk-moderate / acceptΒ·redactΒ·block callers / Fibril Soft-ORDER #1561/#1563/#1564. Prior Admin #604 TradeDockAdmin read-only v1 remains tip-PRESENT (Soft-HOLD #1146 / LEG-71) β€” #604 β‰  Soft-ORDER #1561 force-cancel unlock. βœ… tip-PRESENT: #604 TradeDockAdmin.tsx App mount + /tradedocks route (read-only v1); #789 SectorDetail EditableFields description / radiation_level / resource_regeneration; #788 StationsManager / PlanetDetailModal / FleetManagement; #793 ReEngagementQueuePanel formatAdminApiError + Sidebar /review-queue; #794 MedalAdmin Player collection tab + GET …/medals/admin/players/{id}/collection. OPEN Soft-ORDER tip-pending (not tip-shipped): #778 AI secondary + LanguageSwitcher 403/429; #791 Station fuel/luxury qty + Planet residual PATCH (Fibril #1564/#1563); #792 TradeDockAdmin force-cancel + formatAdminApiError reads (Fibril #1561/#1711); #795 FleetManagement emergency repair/refuel + Playwright /tradedocks+//medals smoke β€” claim only when tip ancestor of land SHA. Fibril Soft-ORDER wires #1561 TradeDockAdmin force-cancel / #1563 PlanetDetail residual PATCH / #1564 StationDetail fuel/luxury editors stay tip-pending. Fibril Soft-ORDER #1758 Admin UI POST …/ships/registry/backfill consumer + #1759 DroneOperationsTab PATCH+DELETE stay tip-pending (gap MET on tip) β€” claim only when tip ancestor. Prior Admin #786 StationDetail/PlanetDetail PATCH honesty + Admin #787 Soft-ORDER create-station/planet + GS #784 force-cancel + Admin #775 universe detail 403/429 + GS #756 combat/history + Admin #783 EconomyDashboard alerts/summary honesty + GS #710 medal collection GET remain tip-true. GS #711 bulk-moderate route remains tip-PRESENT; Admin UI callers still Soft-HOLD. βœ… GS canon moderation routes tip-PRESENT via #709 β€” do not claim Admin-UI accept/redact/block buttons tip-shipped. Found and corrected an overstated claim about the Culture/Diplomacy tabs (see Β§ below β€” they're labeled-limited and navigable, not literally disabled; the "no mock data" substance held up).

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 @ 96c968ad β€” App lazy import + /tradedocks route + Vitest (read-only v1; no force-cancel write path). Soft-ORDER #1561 force-cancel Admin-UI consumer stays 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; Fibril #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 Fibril #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. Do not treat Fibril READY product wires #1563/#1564 (residual PlanetDetail PATCH / StationDetail fuelΒ·luxury editors) as unlocked β€” those stay tip-pending. 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 / Fibril 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 / Fibril READY #1561/#1563/#1564. Do not claim #778 tip-shipped (still OPEN).

Tip honesty (#604 MERGED / tip 96c968ad; Soft-HOLD #1146 / LEG-71): βœ… tip-PRESENT β€” TradeDockAdmin.tsx + App lazy mount + /tradedocks route + tests (read-only v1; no force-cancel write path). Soft-ORDER #1561 force-cancel Admin-UI consumer stays tip-pending. Soft-HOLD residuals above unchanged for LanguageSwitcher / Beacon / MessageModeration Admin callers / #1563/#1564.

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 / Fibril READY #1561/#1563/#1564. Do not claim #795/#778 tip-shipped (still OPEN).

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 / Fibril READY #1561/#1563/#1564. Do not claim #795/#778/#791/#792 tip-shipped (still OPEN). Fibril Soft-ORDER #1758/#1759 stay tip-pending.

Tip honesty (OPEN Soft-ORDER PRs, tip-pending): #791 Station fuel/luxury + Planet residual PATCH (Fibril #1564/#1563); #792 TradeDockAdmin force-cancel + formatAdminApiError reads (Fibril #1561/#1711); #795 FleetManagement emergency repair/refuel + Playwright /tradedocks+//medals smoke; keep #778 tip-pending. Fibril Soft-ORDER #1758 Admin UI POST …/ships/registry/backfill (FleetManagement; gap MET: zero registry/backfill under admin-ui) + #1759 DroneOperationsTab PATCH+DELETE (gap MET: tip get/post force-recall/restore only) stay tip-pending. Claim only when tip ancestor of land SHA.


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 is tip-PRESENT via #711 / LEG-266 (admin_messages.py:100; tip 96c968ad). βœ… GS canon accept / redact / block tip-PRESENT via #709 / LEG-263 (admin_moderation_messages.py). Do not claim Admin-UI bulk-moderate callers or Admin-UI accept/redact/block buttons as shipped (#1114/#1115 Soft-HOLD β€” Admin UI still tip-absent; MessageModeration.tsx remains delete|unflag only).

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-08-20 vs Sectorwars2102 HEAD 96c968ad.) 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.

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; Fibril #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 / Fibril #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 empty progress catch (see Β§ i18n / Fibril #1336), and 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; Fibril 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 read-only via #604 / Fibril #1146 @ 96c968ad (force-cancel Soft-ORDER #1561 still 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 + Admin-UI accept/redact/block buttons stay Soft-HOLD unshipped (#1114/#1115 β€” zero Admin UI tip matches). Admin UI Soft-HOLD for BeaconSectorCapEditor stays on PR #764 / message-beacons.md β€” not claimed tip-shipped here.

ARIA per-player security-ops UI (UIADM-SECURITY-PLAYER-OPS) β€” πŸ“ Design-only, gated behind the Wave-12 RBAC scope rollout (ADR-0093 item 10; Max overrode the build-now recommendation). The per-player ARIA security-ops surface β€” reviewing a specific player's ARIASecurityLog history, trust score, and violation count, and acting on it (block / unblock / reset trust) β€” is born scope-gated under an admin.aria.* scope family, never shipped flat-admin the way the legacy /admin/security/player/{id}/* endpoints in ./aria.md are today. It sequences behind the same 19-scope rollout as item 19.

Orphaned GC admin methods (manual grant/revoke) β€” πŸ“ Design-only, gated behind the same scope rollout (ADR-0093 item 21). Manual Galactic-Citizen grant/revoke β€” support tooling for comping or clawing back GC status outside the normal PayPal subscription flow β€” wires as admin.subscriptions.*-scoped admin routes, sequenced behind item 19's scope rollout. See ./monetization.md Β§ Manual GC grant/revoke (admin support tooling) for the player-state side.

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. Residual (tip 96c968ad): progress GET uses an empty catch that keeps the static 100% fallback on any failure β€” including 403 scope denial and 429 admin rate-limit β€” so RBAC/rate-limit errors can masquerade as a green completion bar (LanguageSwitcher.tsx ~68–70). That is not tip-shipped progress RBAC honesty. Product land path: Admin Fibril #1336 / LEG-1265. Shared adminApiError.ts is tip-shipped (Β§ Authorization model) but this switcher does not use it.


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.