Administration
Overview
Server administration via CLI or web dashboard.
Admin Capabilities
Time Control
| Action | Description |
|---|---|
| Set tick rate | Change ticks per second (0.1 - 100.0 Hz) |
| Set time scale | Change game speed multiplier (0.1 - 100.0x) |
| Toggle time sync | Enable/disable game time synchronization to wall-clock UTC |
| Pause | Stop tick processing immediately |
| Resume | Continue tick processing |
Time Sync
When enabled (default), the tick engine adjusts effective time scale by up to ±5% to converge game time to wall-clock UTC. Only active when admin time_scale ≈ 1.0. See Game Time Synchronization for the correction formula.
Setting tick rate to 0 is not allowed — use pause instead. Invalid rates return an error.
Pause Behavior
- Pause takes effect immediately, even during catch-up
- Tick counter freezes at current value
- Player connections remain open, but no state updates sent
- When resumed, catch-up continues from where it left off
Registration Control
Admins can enable or disable new player registrations at runtime. When disabled, the registration endpoint returns error E020 (“Registration is currently closed”). Existing players can still log in.
| Action | Endpoint | Description |
|---|---|---|
| Enable registrations | POST /api/admin/registrations/enable |
Allow new player registrations |
| Disable registrations | POST /api/admin/registrations/disable |
Block new player registrations |
| Get status | GET /api/admin/registrations |
Check current registration status |
| Toggle (legacy) | POST /api/admin/registrations |
Set via {"open": true/false} body |
Redis flag: game:registration_open (default "true" when absent)
Behavior:
- Registrations are open by default (key absent = open)
- Only the
/api/auth/registerendpoint is affected; login is never blocked - The registration status is included in
GET /api/admin/statusandGET /api/statusresponses asregistration_open
Server Management
| Action | Description |
|---|---|
| Start | Start the game server |
| Stop | Gracefully stop the server |
| Status | View server health and metrics |
Game State
| Action | Description |
|---|---|
| Reset | Factory reset — wipe all game state (see below) |
| Snapshot | Create state backup |
| Restore | Restore from snapshot |
Admin Reset vs Player Reset
| Aspect | Player Reset | Admin Reset |
|---|---|---|
| Scope | Single ship | Entire game |
| Ships | Returns to LEO, fuel refilled | All ships removed from Redis |
| Celestial bodies | Unchanged | Reset to ephemeris for current UTC |
| Tick counter | Unchanged | Reset to 0 |
| Game time | Unchanged | Reset to current UTC |
| Player accounts | Unchanged | All deleted (including admin) |
| Automation rules | Unchanged | All deleted |
| Maneuver state | Unchanged | All deleted (Redis + PostgreSQL) |
| Game config | Unchanged | All deleted from PostgreSQL |
| Facilities | Unchanged | All deleted (Redis + PostgreSQL) |
| Snapshots | Unchanged | All deleted |
| total_spawns counter | Incremented | Reset to 0 |
| Backup | N/A | Created before destruction |
| Service restart | N/A | Required after reset |
Admin reset is a full factory reset — all player accounts, ships, facilities, automation, maneuvers, and game config are wiped. A backup is created in the reset_backups table before destruction. Services should be restarted after reset for clean in-memory state. The bootstrap admin account is automatically re-created by the reset endpoint immediately after the gRPC call succeeds, using GALAXY_ADMIN_USERNAME/GALAXY_ADMIN_PASSWORD env vars — no api-gateway restart is needed for admin access.
Admin Reset Flow for Connected Players
When admin triggers a factory reset, connected players experience the following:
| Step | Action |
|---|---|
| 1 | Admin calls POST /api/admin/game/reset |
| 2 | api-gateway calls tick-engine.ResetGame() via gRPC (120s timeout) |
| 3 | tick-engine pauses tick processing |
| 4 | tick-engine creates backup (Redis BGSAVE + PostgreSQL data → reset_backups table) |
| 5 | tick-engine calls physics.ClearAllShips() via gRPC |
| 6 | physics deletes all ship:* keys, resets game:total_spawns to 0 |
| 7 | physics publishes ship.removed events for each deleted ship |
| 8 | tick-engine calls physics.ClearAllStations() via gRPC |
| 9 | tick-engine calls physics.ClearAllJumpGates() via gRPC |
| 10 | tick-engine deletes player Redis keys (player:*, automation:*, maneuver:*, maneuver_log:*, maneuver_history:*, connections:online, game:registration_open, game:maneuver_logging) |
| 11 | tick-engine deletes facility Redis keys (facility:*, player:*:facilities) |
| 12 | tick-engine resets game state keys, re-initializes (tick=0, time=current UTC) |
| 13 | tick-engine deletes PostgreSQL data (maneuver_events, facilities, ships, players, game_config, snapshots) |
| 14 | tick-engine re-initializes physics (bodies from ephemeris) |
| 15 | tick-engine resumes tick processing |
| 16 | api-gateway re-bootstraps admin account from env vars (immediate, no restart needed) |
| 17 | api-gateway returns backup_id and restart_required: true |
| 18 | CLI/dashboard restarts remaining game deployments via kubectl rollout restart |
Impact on connected players:
| Aspect | Behavior |
|---|---|
| Connections | Remain open (not disconnected) |
| Ship state | Deleted; next control command returns E006 |
| Error shown | “Ship not found” (E006) |
| Recovery | Player must re-register (account was deleted) |
| Game time | Resets to current UTC in next state update |
Client experience:
- Player sees game time reset to current UTC
- Player’s HUD shows no ship data (or stale data)
- Player attempts any control → receives E006 error
- Services restart → WebSocket connections close
- Player must re-register (account was deleted in reset)
- Normal gameplay resumes
Use admin reset for:
- Starting a fresh game
- Recovering from corrupted state
- Testing/development
Snapshot Restore Behavior
When admin restores from a snapshot:
| Step | Action |
|---|---|
| 1 | Admin calls POST /api/admin/snapshots/{id}/restore |
| 2 | tick-engine pauses tick processing |
| 3 | tick-engine loads snapshot from PostgreSQL |
| 4 | tick-engine replaces all Redis state (bodies, ships, game:tick, etc.) |
| 5 | tick-engine resumes tick processing |
| 6 | Next tick broadcasts new state to all connected clients |
Impact on connected players:
| Aspect | Behavior |
|---|---|
| Connections | Remain open (not disconnected) |
| Ship positions | May jump to snapshot position |
| Notification | No special message; next state update shows new positions |
| Controls | Continue working normally |
| Ships not in snapshot | Removed from Redis; player sees E006 on next control |
Edge cases:
| Scenario | Behavior |
|---|---|
| Player registered after snapshot | Ship not in snapshot; removed on restore; player must reset |
| Player deleted after snapshot | Ship restored; player can’t control (no account) |
| Mid-tick restore | Current tick completes, restore happens before next tick |
Recovery for affected players:
- Players whose ships were removed can use the “reset” service to spawn a new ship
- Players see their ship at the snapshot position on next state update
Interfaces
CLI (admin-cli)
galaxy-admin status
galaxy-admin tick-rate set 2.0
galaxy-admin pause
galaxy-admin resume
galaxy-admin snapshot create
galaxy-admin snapshot restore <snapshot-id>
galaxy-admin players list
galaxy-admin players delete <player-id>
CLI Authentication
The admin CLI uses the same REST API as the dashboard.
Credential sources (in priority order):
| Priority | Source | Description |
|---|---|---|
| 1 | Cached token | ~/.galaxy/admin-token |
| 2 | Environment variables | GALAXY_ADMIN_USER, GALAXY_ADMIN_PASSWORD |
| 3 | Interactive prompt | Prompts for username/password |
Authentication flow:
- Check for cached token in
~/.galaxy/admin-token - If token exists and not expired, use it
- If token expired or missing:
- Check
GALAXY_ADMIN_USERandGALAXY_ADMIN_PASSWORDenvironment variables - If not set, prompt interactively for username and password
- Check
- Call
POST /api/admin/auth/loginwith credentials - Cache new token to
~/.galaxy/admin-token - Token cached with 600 file permissions (owner read/write only)
Token expiry handling:
- Token expires after 24 hours
- CLI detects 401 response and re-authenticates automatically
- Re-authentication uses same priority order (env vars, then prompt)
Non-interactive mode:
For scripts and automation, set environment variables:
export GALAXY_ADMIN_USER=admin
export GALAXY_ADMIN_PASSWORD=secret
galaxy-admin status
Web Dashboard (retired — now integrated into web-client)
The standalone admin-dashboard service has been retired. All admin functionality is now available through the web client’s built-in admin view (View > Admin View).
- Real-time server metrics (tick rate, time scale, energy drift, momentum drift)
- Visual controls for time/state management
- Player management with promote/demote
- Infrastructure management (stations, jump gates)
- Maneuver debugging
Integration Health Metrics
The Game Status card displays energy drift (dE/E0) and momentum drift (dL/L0) with color coding:
| Color | Drift Level | Meaning |
|---|---|---|
| Green | < 1e-6 | Normal |
| Yellow | 1e-6 to 1e-3 | Elevated |
| Red | >= 1e-3 | Concerning |
Logging
All game events logged for analytics and debugging.
Log Format
JSON structured logs:
{
"timestamp": "2025-01-15T10:30:00Z",
"level": "info",
"event": "player.login",
"player_id": "uuid",
"details": {}
}
Events Logged
| Event | Level | Details |
|---|---|---|
| player.registered | info | username |
| player.login | info | player_id |
| player.logout | info | player_id |
| ship.spawned | info | ship_id, player_id, position |
| ship.service.fuel | info | ship_id |
| ship.service.reset | info | ship_id |
| tick.completed | debug | tick_number, duration_ms |
| tick.behind | warn | ticks_behind |
| error.validation | warn | player_id, message |
| error.internal | error | message, stack |
Log Storage
- Stdout in JSON format (container best practice)
- Aggregated via Kubernetes logging (e.g., Loki, ELK)
- Retention: 30 days (configurable)
Metrics
Format and Export
- Format: Prometheus
- Endpoint:
/metricson each service (HTTP) - Scrape interval: 15 seconds (configurable in Prometheus)
| Service | Metrics Endpoint |
|---|---|
| api-gateway | http://api-gateway:8000/metrics |
| tick-engine | http://tick-engine:8001/metrics |
| physics | http://physics:8002/metrics |
| players | http://players:8003/metrics |
| galaxy | http://galaxy:8004/metrics |
Prometheus Metric Names
Metrics follow Prometheus naming conventions (galaxy_ prefix):
Tick Processing
| Metric | Type | Description |
|---|---|---|
galaxy_tick_current |
Gauge | Current tick number |
galaxy_tick_rate_configured |
Gauge | Configured ticks per second |
galaxy_tick_rate_actual |
Gauge | Actual ticks per second achieved |
galaxy_tick_duration_seconds |
Histogram | Processing time per tick |
galaxy_tick_behind_total |
Gauge | Number of ticks behind schedule (0 = healthy) |
API Gateway
| Metric | Type | Labels | Description |
|---|---|---|---|
galaxy_api_requests_total |
Counter | method, path | Total HTTP requests (path uses route template) |
galaxy_connections_active |
Gauge | — | WebSocket connections |
Game State
| Metric | Type | Description |
|---|---|---|
galaxy_celestial_bodies_total |
Gauge | Number of bodies in n-body simulation |
galaxy_ships_total |
Gauge | Number of active ships |
galaxy_players_registered_total |
Counter | Total registered players |
System Health
| Metric | Type | Description |
|---|---|---|
galaxy_redis_up |
Gauge | Redis connection health (1 = up, 0 = down) |
galaxy_postgres_up |
Gauge | PostgreSQL connection health (1 = up, 0 = down) |
Kubernetes Integration
# ServiceMonitor for Prometheus Operator
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: galaxy-services
spec:
selector:
matchLabels:
app.kubernetes.io/part-of: galaxy
endpoints:
- port: metrics
interval: 15s
Authentication
Unified Auth Model
Admin and player identities share the players table. An is_admin boolean flag distinguishes admin accounts from regular players. This replaces the previous separate admins table.
Key properties:
- Admin accounts are rows in
playerswithis_admin = TRUE - Admins may have
ship_id = NULL(ship-less observation mode) - Regular player registration always creates a ship (
is_admin = FALSE) - A player can be promoted to admin (or demoted) by toggling
is_admin - JWT tokens include an
is_adminclaim for authorization checks
Token claims (unified):
| Claim | Type | Description |
|---|---|---|
sub |
string | Player ID (UUID) |
username |
string | Username |
ship_id |
string | Ship UUID, or empty string for ship-less admins |
is_admin |
bool | True if admin |
iat |
int | Issued at (Unix timestamp) |
exp |
int | Expiry (Unix timestamp, 24 hours) |
The previous type: "admin" claim is no longer used. Admin authorization checks is_admin instead.
Ship-less Admin Mode
Admins with ship_id = NULL can connect via WebSocket for observation:
- Welcome message includes full game config and
is_admin: true - State broadcasts include all bodies, ships, and stations (but
shipfield is null) - Ship-dependent messages (
control,service,attitude_*,automation_*,rendezvous_*,target_select,ship_rename) return error E030 “No ship assigned” - Non-ship messages (ping/pong, chat) work normally
Constant-Time Authentication
Admin authentication must be constant-time to prevent timing-based username enumeration. When a username is not found in the database, the server must still perform a bcrypt verification against a dummy hash to ensure the response time is indistinguishable from a valid-username-wrong-password attempt.
Implementation:
- Generate a dummy bcrypt hash at module load time:
_DUMMY_HASH = bcrypt.hashpw(b"dummy", bcrypt.gensalt()).decode() - When the database query returns no row for the given username, call
verify_password(password, _DUMMY_HASH)before returningNone - This ensures both code paths (user found, user not found) execute bcrypt verification
Initial Admin Setup
- On first startup, api-gateway checks for existing admin accounts in
playerstable (WHERE is_admin = TRUE) - If none exist, creates bootstrap admin from Kubernetes Secret:
GALAXY_ADMIN_USERNAME(default: “admin”)GALAXY_ADMIN_PASSWORD(required, no default)- Inserts into
playerswithis_admin = TRUE,ship_id = NULL
- Bootstrap admin can create additional admins via dashboard or in-game admin view
- Bootstrap admin should change password after first login
Missing password behavior:
If GALAXY_ADMIN_PASSWORD is not set and no admin accounts exist:
- Server logs fatal error: “GALAXY_ADMIN_PASSWORD required for initial admin setup”
- Server exits with non-zero status code (prevents startup)
- Kubernetes will show pod in CrashLoopBackOff until secret is configured
This prevents running without admin access and avoids security vulnerabilities from empty/default passwords.
Admin Account Management
| Action | Access | Endpoint |
|---|---|---|
| Create admin | Existing admin via CreateAdminAccount gRPC or REST endpoint |
— |
| Promote player to admin | Existing admin via admin view or REST | PUT /api/admin/players/{player_id}/role |
| Demote admin to player | Existing admin via admin view or REST (cannot demote self) | PUT /api/admin/players/{player_id}/role |
| Change password | Self or other admin | — |
Promote/Demote endpoint: PUT /api/admin/players/{player_id}/role
| Field | Type | Description |
|---|---|---|
is_admin |
bool | true to promote, false to demote |
Constraints:
- Self-demotion is blocked — admins cannot change their own role (returns 400 “Cannot change own admin role”)
- Promoted players keep their existing ship
- Demoted admins with no ship cannot log in normally until a ship is assigned
Note: “Deleting” an admin now means setting is_admin = FALSE (demotion), not deleting the player row. If the demoted admin had no ship, they cannot log in normally until a ship is assigned.
Player Account Management
| Action | Access |
|---|---|
| Reset player password | Admin via dashboard or admin view |
| Delete player account | Admin via dashboard or admin view |
| View player list | Admin via dashboard or admin view |
Password reset generates a temporary password that admin provides to player out-of-band.
Deleting Connected Players
When admin deletes a player who is currently connected:
| Step | Action |
|---|---|
| 1 | Admin initiates delete via dashboard |
| 2 | players service deletes account from PostgreSQL |
| 3 | players service calls physics.RemoveShip |
| 4 | physics service removes ship from Redis |
| 5 | ship.removed event published to Redis Stream |
| 6 | api-gateway receives event, finds active connection |
| 7 | api-gateway sends E014 “Account deleted” to client |
| 8 | api-gateway closes WebSocket connection |
| 9 | Client displays “Your account has been deleted” |
The deletion is immediate — no grace period or confirmation to the connected player.
Edge cases:
| Scenario | Behavior |
|---|---|
| In-flight commands | Any commands queued at api-gateway are discarded after WebSocket closes |
| Ship removal timing | Immediate — removed from Redis before ship.removed event published |
| Cached JWT reconnect | api-gateway validates token, calls players.GetPlayer, receives “not found”, returns E005 |
| Mid-tick deletion | If physics is processing a tick, ship may appear in that tick’s broadcast but not subsequent ones |
| Concurrent control input | ApplyControl calls after deletion return E006 “Ship not found” |
In-Game Admin View
The web client includes a full admin dashboard accessible to admin users. The standalone admin-dashboard service has been retired; all admin functionality is now in the web client.
Access
- Visible only to users with
is_admin = TRUEin their JWT - Menu item: View > Admin View (hidden for non-admins)
- Ship-less admins default to admin view on login
- Admins with ships can toggle between cockpit/map/admin views
Layout
Full-screen overlay (below menu bar, above canvas) with a centered card grid:
| Panel | Width | Description |
|---|---|---|
| Game Status | 1 col | Tick, rates, drift, pause state, registration |
| Tick Rate | 1 col | Input + set button |
| Time Sync | 1 col | Toggle, drift, manual scale input |
| Snapshots | 1 col | Create + scrollable list (last 10) |
| Danger Zone | 1 col | Factory reset button (red border) |
| Infrastructure | 1 col | Station/jumpgate spawn forms + lists |
| Maneuver Debug | 1 col | Logging level, per-ship debug toggle |
| NPC Management | 1 col | Placeholder — “Coming soon” |
| Players | full width | Table: username, status, ship, created, actions |
Floating Admin Windows
In addition to the full-screen admin view, admin panels are available as individual draggable floating windows that overlay the cockpit. This allows admins to monitor and control the game while playing.
Admin Menu
A top-level “Admin” menu appears between Ship and Help in the menu bar (hidden for non-admins). Menu items:
| Item | Action |
|---|---|
| Game Control | Toggle game control floating window |
| State Management | Toggle state management floating window |
| Player Management | Toggle player management floating window |
| Infrastructure | Toggle infrastructure floating window |
| Diagnostics | Toggle diagnostics floating window |
| Show All | Show all 5 floating windows |
| Hide All | Hide all 5 floating windows |
Window Definitions
| Window ID | Panels Merged | Default Width |
|---|---|---|
admin-game-control-window |
Game Status + Tick Rate + Time Sync | 320px |
admin-state-mgmt-window |
Snapshots + Danger Zone | 340px |
admin-players-window |
Players table | 600px |
admin-infrastructure-window |
Stations + Jump Gates | 380px |
admin-diagnostics-window |
Maneuver Debug | 340px |
Dual-Mode Behavior
- Full-screen admin view and floating windows coexist independently
- Rendering functions are shared — both modes call the same render logic with different DOM element sets
- Floating window elements use
fw-admin-prefixed IDs to avoid conflicts with full-screen elements - Buttons and inputs in floating windows also use
fw-prefixed IDs
Polling
Polling is unified across both modes:
- Polling starts when either full-screen admin view activates OR any floating window becomes visible
- Polling stops only when full-screen admin view is deactivated AND no floating windows are visible
- Both modes receive the same data from the same polling intervals
Position Persistence
Floating window positions are saved to settings.windowPositions (localStorage) and restored on init. Draggable only — no resize.
Polling Intervals
Data is polled via REST endpoints while admin view or any admin floating window is active:
| Data | Interval | Endpoint |
|---|---|---|
| Game status | 2 seconds | GET /api/admin/status |
| Players | 10 seconds | GET /api/admin/players |
| Snapshots | 30 seconds | GET /api/admin/snapshots |
| Stations | 30 seconds | GET /api/admin/stations |
| Jump gates | 30 seconds | GET /api/admin/jumpgates |
Polling starts on view activation and stops on deactivation.
Authentication
Uses the existing state.token (JWT with is_admin claim). No separate admin login flow. 401 responses are handled gracefully (token expired).
Keyboard Input Handling
Input fields within the admin view stop keyboard event propagation to prevent cockpit shortcuts (M, P, H, etc.) from firing while typing. This is done via stopPropagation() on keydown events for admin input elements.
Confirmation Dialogs
Destructive operations require browser confirm() / prompt() dialogs:
| Action | Confirmation |
|---|---|
| Time scale change | confirm() — changing time scale can destabilize physics |
| Restore snapshot | confirm() — current state will be lost |
| Factory reset | Single confirm() — irreversible, deletes all accounts |
| Delete player | confirm() with username shown |
| Promote player | confirm() with username shown |
| Demote admin | confirm() with username shown |
| Remove station | confirm() with station name shown |
| Remove jump gate | confirm() with gate name shown |
| Reset password | prompt() for new password (min 8 chars) |