Files
languard-servers-manager/ARCHITECTURE.md
Tran G. (Revernomad) Khoa 6511353b55 feat: implement full backend + frontend server detail, settings, and create server pages
Backend:
- Complete FastAPI backend with 42+ REST endpoints (auth, servers, config,
  players, bans, missions, mods, games, system)
- Game adapter architecture with Arma 3 as first-class adapter
- WebSocket real-time events for status, metrics, logs, players
- Background thread system (process monitor, metrics, log tail, RCon poller)
- Fernet encryption for sensitive config fields at rest
- JWT auth with admin/viewer roles, bcrypt password hashing
- SQLite with WAL mode, parameterized queries, migration system
- APScheduler cleanup jobs for logs, metrics, events

Frontend:
- Server Detail page with 7 tabs (overview, config, players, bans,
  missions, mods, logs)
- Settings page with password change and admin user management
- Create Server wizard (4-step; known bug: silent validation failure)
- New hooks: useServerDetail, useAuth, useGames
- New components: ServerHeader, ConfigEditor, PlayerTable, BanTable,
  MissionList, ModList, LogViewer, PasswordChange, UserManager
- WebSocket onEvent callback for real-time log accumulation
- 120 unit tests passing (Vitest + React Testing Library)

Docs:
- Added .gitignore, CLAUDE.md, README.md
- Updated FRONTEND.md, ARCHITECTURE.md with current implementation state
- Added .env.example for backend configuration

Known issues:
- Create Server form: "Next" buttons don't validate before advancing,
  causing silent submit failure when fields are invalid
- Config sub-tabs need UX redesign for non-technical users
2026-04-17 11:58:34 +07:00

40 KiB

Languard Server Manager -- System Architecture

Overview

Languard is a multi-game web-based management panel for dedicated game servers. It uses a game adapter architecture where a game-agnostic core handles server lifecycle, monitoring, and real-time communication, while game-specific behavior (config formats, remote admin protocols, log parsing, mission/mod handling) is encapsulated in pluggable adapters.

Arma 3 is the first-class, built-in adapter. Adding a new game server type requires only a new adapter package -- no core code changes.


Technology Stack

Layer Technology Rationale
Backend framework FastAPI (Python 3.11+) Async-native, built-in WebSocket, OpenAPI docs
Database SQLite via SQLAlchemy (sync, raw SQL via text()) Zero-config, file-based; WAL mode for concurrent reads; all DB access is synchronous
Process management subprocess.Popen + threading + psutil Wrap server executables, recover PIDs after restart; on Windows terminate() is a hard kill
Real-time comms WebSocket via FastAPI Push log lines, player lists, server status to React
Game adapters Python Protocol classes (duck typing) + GameAdapterRegistry Each game implements capability protocols; core resolves adapter at runtime from server.game_type
Scheduling APScheduler BackgroundScheduler Log/metrics/event cleanup jobs (sync DB ops run in thread pool)
Auth JWT (python-jose, HS256) + bcrypt Secure API; React stores token in localStorage
Encryption Fernet (AES-256 via cryptography library) Encrypt sensitive config fields (RCON passwords, admin passwords) at rest
Rate limiting slowapi (installed, no per-route limits yet) Infrastructure present, not yet applied to individual routes
Frontend React 19 + TypeScript 6 + Vite 8 + Tailwind CSS SPA with dark neumorphic design system

Architecture Diagram

+--------------------------------------------------------------+
|                     React Frontend (SPA)                      |
|  Dashboard | Login | Server List | Server Detail (planned)   |
+------------------------------+-------------------------------+
                               |  HTTP REST + WebSocket (/ws)
                               v
+--------------------------------------------------------------+
|                  FastAPI Application (main.py)                |
|                                                                |
|  Lifespan: init DB -> register adapters -> create WSManager   |
|            -> start BroadcastThread -> create ThreadRegistry   |
|            -> recover processes -> seed admin -> start scheduler|
|                                                                |
|  +-----------+  +-----------+  +-----------+  +-----------+   |
|  | Auth      |  | Servers   |  | Games     |  | System    |   |
|  | Router    |  | Router    |  | Router    |  | Router    |   |
|  +-----------+  +-----------+  +-----------+  +-----------+   |
|  +-----------+  +-----------+  +-----------+  +-----------+   |
|  | Players   |  | Bans      |  | Missions  |  | Mods      |   |
|  | Router    |  | Router    |  | Router    |  | Router    |   |
|  +-----------+  +-----------+  +-----------+  +-----------+   |
|  +-----------+                                                 |
|  | WebSocket |  /ws endpoint, JWT via query param             |
|  | Router    |  BroadcastThread bridges Queue -> asyncio      |
|  +-----------+                                                 |
|                                                                |
|  +----------------------------------------------------------+ |
|  |                   Service Layer                          | |
|  |  AuthService | ServerService | (delegating to adapters)  | |
|  +----------------------------------------------------------+ |
|                                                                |
|  +----------------------------------------------------------+ |
|  |                   Data Access Layer                       | |
|  |  BaseRepository -> ServerRepository | ConfigRepository   | |
|  |  PlayerRepository | LogRepository  | MetricsRepository  | |
|  |  EventRepository  | BanRepository                        | |
|  +----------------------------------------------------------+ |
|                                                                |
|  +----------------------------------------------------------+ |
|  |              Game Adapter System                         | |
|  |  GameAdapterRegistry (class-level singleton)             | |
|  |  -> Arma3Adapter (implements all 7 capabilities)        | |
|  |  -> third-party adapters via entry_points                | |
|  +----------------------------------------------------------+ |
|                                                                |
|  +----------------------------------------------------------+ |
|  |              Background Thread System                    | |
|  |  ThreadRegistry -> per-server thread bundles:            | |
|  |    ProcessMonitorThread | MetricsCollectorThread         | |
|  |    LogTailThread (conditional) | RemoteAdminPollerThread | |
|  +----------------------------------------------------------+ |
|                                                                |
|  +----------------------------------------------------------+ |
|  |              Scheduled Jobs (APScheduler)                | |
|  |  cleanup_old_logs (daily 03:00)                          | |
|  |  cleanup_old_metrics (every 6h)                          | |
|  |  cleanup_old_events (weekly Sunday 04:00)                | |
|  +----------------------------------------------------------+ |
+--------------------------------------------------------------+
                               |
                               v
+--------------------------------------------------------------+
|                      SQLite Database                          |
|  users | servers | game_configs | mods | server_mods          |
|  missions | mission_rotation | players | player_history       |
|  bans | logs | metrics | server_events | schema_migrations   |
+--------------------------------------------------------------+

Backend Architecture

Application Bootstrap (main.py)

The app uses a lifespan async context manager on the FastAPI instance. The startup sequence, in order:

  1. Initialize DB -- get_engine() creates the SQLAlchemy engine with WAL mode, foreign keys, and busy timeout pragmas. run_migrations() applies all pending SQL files from core/migrations/.
  2. Register adapters -- initialize_adapters() imports built-in adapters (Arma 3) which self-register via GameAdapterRegistry.register(), then scans importlib.metadata entry_points under "languard.adapters" for third-party adapters.
  3. Create WebSocketManager -- Instantiated and stored on app.state.ws_manager.
  4. Create broadcast queue and BroadcastThread -- A queue.Queue(maxsize=1000) bridges background threads to the asyncio event loop. BroadcastThread is a daemon thread that calls asyncio.run_coroutine_threadsafe() to schedule ws_manager.broadcast() on the event loop.
  5. Create ThreadRegistry -- Initialized with the ProcessManager singleton, GameAdapterRegistry, and the global broadcast queue. Stored on app.state.thread_registry.
  6. Recover processes -- ProcessManager.recover_on_startup() checks DB for servers marked "running", verifies PIDs via psutil against the adapter's executable allowlist, and re-attaches monitoring threads for valid PIDs. Crashed servers are marked "crashed".
  7. Reattach threads for running servers -- Iterates running servers and calls ThreadRegistry.reattach_server_threads().
  8. Seed default admin -- AuthService.seed_admin_if_empty() creates an "admin" user with a random 16-char password if no users exist. The password is logged to stdout at startup.
  9. Start APScheduler -- Registers cleanup jobs and starts the BackgroundScheduler.

Shutdown stops all threads, the broadcast thread, and the scheduler.

Layered Architecture

The backend follows a strict layered pattern:

Routers -> Services -> Repositories -> Database
  • Routers handle HTTP concerns (request parsing, response formatting, auth guards). They return standardized {"success": True, "data": ..., "error": None} envelopes.
  • Services contain business logic. ServerService orchestrates lifecycle operations, delegating game-specific work to adapters.
  • Repositories encapsulate SQL. All extend BaseRepository, which provides _execute(), _fetchone(), _fetchall(), and _lastrowid() helpers that wrap sqlalchemy.text().
  • Database is SQLite with raw SQL (no ORM models). Migrations are numbered SQL files applied in order.

Global Exception Handler

All unhandled exceptions are caught by a FastAPI @app.exception_handler(Exception) that returns:

{
  "success": false,
  "data": null,
  "error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}
}

CORS Middleware

Configured via LANGUARD_CORS_ORIGINS env var (default: ["http://localhost:5173"]). Allows all methods and headers with credentials.


Game Adapter System

Design

Adapters use Python Protocol classes (runtime-checkable duck typing). The core never imports adapter internals -- it only imports from adapters.protocols and resolves adapters via GameAdapterRegistry.

Capability Protocols

Seven capability protocols are defined in adapters/protocols.py:

Protocol Purpose Key Methods
ConfigGenerator Config schema, defaults, file writing, launch args get_sections(), get_defaults(), get_sensitive_fields(), get_config_version(), migrate_config(), write_configs(), build_launch_args(), preview_config()
ProcessConfig Executable allowlist, port conventions, directory layout get_allowed_executables(), get_port_conventions(), get_default_game_port(), get_default_rcon_port(), get_server_dir_layout()
LogParser Parse game-specific log lines parse_line(), get_log_file_resolver()
RemoteAdmin Factory for remote admin clients (RCon) create_client(), get_startup_delay(), get_poll_interval(), get_player_data_schema()
MissionManager Mission file handling and rotation parse_mission_filename(), get_rotation_config(), get_missions_dir(), get_mission_data_schema()
ModManager Mod folder conventions and CLI args get_mod_folder_pattern(), build_mod_args(), validate_mod_folder(), get_mod_data_schema()
BanManager Bidirectional ban file sync get_ban_file_path(), sync_bans_to_file(), read_bans_from_file(), get_ban_data_schema()

The composite GameAdapter protocol requires all adapters to implement has_capability(name) which returns whether a given capability is available. Optional capabilities (RemoteAdmin, MissionManager, ModManager, BanManager) return None from their factory methods when not supported.

GameAdapterRegistry

A class-level singleton mapping game_type string to adapter instance. Provides:

  • register(adapter) -- Called at import time by each adapter package
  • get(game_type) -- Raises KeyError if not found
  • all() -- Returns all registered adapters
  • list_game_types() -- Returns metadata list for the /api/games endpoint

Arma3Adapter

Implements all 7 capabilities:

  • ConfigGenerator: Defines server, rcon, mission sections using Pydantic models. Writes server.cfg using atomic write pattern (.tmp then os.replace()). Builds launch args from config + mod list.
  • ProcessConfig: Allows arma3server_x64.exe and arma3server.exe. Derives 4 ports from game_port (game, steam_query, von, steam_auth). Default game port 2302, default RCon port game+4.
  • LogParser: Parses Arma 3 .rpt log files. Resolves log path from server config or defaults to server_dir/server/*.rpt.
  • RemoteAdmin: Implements BattlEye RCon protocol via Arma3RemoteAdminFactory. Supports login, command sending, player listing, kick, ban, say-all, and shutdown.
  • MissionManager: Handles .pbo mission files and mission rotation config.
  • ModManager: Builds -mod= and -serverMod= CLI arguments from mod list.
  • BanManager: Syncs bans between the DB and battleye/bans.txt.

Adapter Error Hierarchy

Typed exceptions in adapters/exceptions.py:

  • AdapterError (base)
    • ConfigWriteError -- Atomic file write failed
    • ConfigValidationError -- Pydantic model rejected config
    • ConfigMigrationError -- Schema migration failed
    • LaunchArgsError -- Launch args construction failed
    • RemoteAdminError -- RCon connection/command failure (with recoverable flag)
    • ExeNotAllowedError -- Executable not in adapter allowlist

Adding a New Game Adapter

  1. Create adapters/<game>/ package with an adapter class implementing GameAdapter protocol
  2. Create capability modules for supported protocols
  3. Export a module-level adapter instance (e.g., MYGAME_ADAPTER)
  4. Import it in adapters/__init__.py's load_builtin_adapters() -- or register via pyproject.toml entry_points under "languard.adapters"

No core code changes are required.


Authentication & Authorization

JWT Authentication

  • Algorithm: HS256 via python-jose
  • Token payload: {sub: user_id, username, role, exp}
  • Expiry: configurable via LANGUARD_JWT_EXPIRE_HOURS (default 24h)
  • Secret: LANGUARD_SECRET_KEY env var (required)

Password Hashing

  • bcrypt via the bcrypt Python package
  • hash_password() and verify_password() in core/auth/utils.py

Roles

Two roles: admin and viewer.

  • admin: Full access -- server lifecycle, config editing, user management, RCon commands
  • viewer: Read-only access -- can view servers, logs, metrics, players

Authorization is enforced via FastAPI dependency injection:

  • get_current_user() -- Decodes JWT, validates user exists in DB, returns user dict
  • require_admin() -- Wraps get_current_user(), raises 403 if role is not "admin"

Default Admin Seeding

On first run, if no users exist, a default admin user is created with username "admin" and a randomly generated 16-char password (secrets.token_urlsafe(16)). The password is printed to the server log at startup.


Encryption

Sensitive config fields (RCON passwords, admin passwords) are encrypted at rest using Fernet (AES-256-CBC with HMAC) from the cryptography library.

  • Encryption key: LANGUARD_ENCRYPTION_KEY env var (Fernet base64 key, required)
  • Format: encrypted values are stored as encrypted:<base64-token> in the database
  • ConfigRepository handles encryption/decryption transparently -- reads decrypt, writes encrypt
  • The get_sensitive_fields() method on each ConfigGenerator declares which field names in each section need encryption

Process Management

ProcessManager (Singleton)

Owns all subprocess.Popen handles. Key behaviors:

  • Start: Validates server is not already running, launches with subprocess.Popen. On Windows, uses CREATE_NO_WINDOW flag to suppress console windows.
  • Stop: Sends terminate(), waits up to 30 seconds, then force-kills if needed.
  • Kill: Immediate force-kill via proc.kill().
  • Operation locks: Per-server threading.Lock serializes start/stop/restart for the same server ID.
  • PID recovery: On startup, checks DB for servers marked "running". Uses psutil.Process(pid) to verify the PID is still alive and the process name matches the adapter's get_allowed_executables(). Invalid PIDs cause the server to be marked "crashed". Valid PIDs get a _PsutilProcessWrapper attached so the process can be monitored.

Exe Allowlist

ProcessConfig.get_allowed_executables() returns a list of allowed executable names. This is checked during server creation and startup to prevent path traversal attacks. The check compares only the filename component (via Path(exe_path).name), not the full path.


Thread Architecture

ThreadRegistry

Manages per-server background thread bundles. Created at app startup and stored on app.state.thread_registry. Also provides class-level convenience methods (start_server_threads, stop_server_threads, etc.) that delegate to the singleton instance.

Each server gets up to 4 threads:

Thread Started Purpose
ProcessMonitorThread Always Watches Popen.poll() and detects crashes; updates DB status; broadcasts status changes
MetricsCollectorThread Always Periodically collects CPU/RAM via psutil.Process and stores in metrics table
LogTailThread If adapter has log_parser capability Tails the game log file, parses lines via adapter, stores in logs table, broadcasts to WebSocket
RemoteAdminPollerThread If adapter has remote_admin capability Periodically polls player list via RCon, stores in players table, broadcasts to WebSocket

All threads inherit from BaseServerThread (a threading.Thread subclass with stop_and_join() and stop_event).

BroadcastThread

A daemon thread that bridges queue.Queue (written by background threads) to WebSocketManager.broadcast() on the asyncio event loop. Uses asyncio.run_coroutine_threadsafe() to schedule broadcasts. If the event loop is closed, events are silently dropped (with periodic logging of drop count).


WebSocket System

Endpoint

/ws -- JWT authentication via token query parameter (browser WebSocket API does not support custom headers). Optional server_id query parameters for subscription filtering.

Connection flow:

  1. Client connects with token and optional server_id params
  2. Server validates JWT, closes with code 4001 if invalid
  3. Server sends {"type": "connected", "data": {"user": ..., "subscriptions": ...}}
  4. Server pushes events for subscribed server IDs

WebSocketManager (asyncio-side)

  • connect(ws, server_ids) -- Accept connection, register subscriptions
  • disconnect(ws) -- Remove connection
  • broadcast(server_id, message) -- Send to all clients subscribed to server_id or to None (global)
  • send_to_connection(ws, message) -- Send to a single connection
  • Automatically removes disconnected clients

Message Format

{
  "type": "server_status" | "metrics" | "log" | "players",
  "server_id": 1,
  "data": { ... }
}

Database

SQLite Configuration

  • Engine: sqlite:///<db_path> (default: ./languard.db)
  • WAL mode enabled via PRAGMA journal_mode=WAL
  • Foreign keys enforced via PRAGMA foreign_keys=ON
  • Busy timeout: 5000ms via PRAGMA busy_timeout=5000
  • All queries use sqlalchemy.text() with parameterized inputs (no ORM)

Migration System

Migrations are SQL files in core/migrations/ named with a numeric prefix (e.g., 001_initial_schema.sql). Applied in order. A schema_migrations table tracks which versions have been applied.

Schema (Tables)

Table Purpose
users Auth -- id, username, password_hash, role (admin/viewer), timestamps
servers Server instances -- id, name, game_type, status, pid, exe_path, ports, auto_restart settings
game_configs Per-server config sections -- server_id, game_type, section, config_json, schema_version, config_version (optimistic locking)
mods Mod registry -- game_type, name, folder_path, workshop_id, game_data
server_mods Many-to-many server-mod with sort_order, is_server_mod flag
missions Mission files -- server_id, filename, mission_name, terrain
mission_rotation Ordered mission rotation for a server
players Live player list per server -- slot_id, name, guid, ip, ping
player_history Historical player sessions with join/leave timestamps
bans Ban records -- guid, name, reason, banned_by, expires_at, is_active
logs Parsed log entries -- server_id, timestamp, level, message
metrics CPU/RAM/player_count time series -- server_id, timestamp, cpu_percent, ram_mb, player_count
server_events Audit log -- server_id, event_type, actor, detail (JSON)

Thread-Local DB Connections

Background threads use database.get_thread_db() which returns a thread-local Connection. Each thread gets its own connection (SQLite requirement). Connections must be closed in thread teardown.


Configuration Management

Config Sections

Each game adapter declares config sections via ConfigGenerator.get_sections(), returning a dict of {section_name: PydanticModelClass}. The Arma 3 adapter defines server, rcon, and mission sections.

Config Lifecycle

  1. Creation: When a server is created, ConfigRepository.upsert_section() is called for each section with adapter defaults. Sensitive fields are encrypted.
  2. Read: get_section() and get_all_sections() decrypt sensitive fields transparently.
  3. Update: upsert_section() supports optimistic locking via expected_config_version. On conflict, raises ValueError with "CONFIG_VERSION_CONFLICT" prefix, which the service layer translates to a 409 response with the current config data.
  4. Write to disk: ConfigGenerator.write_configs() uses an atomic write pattern (write to .tmp, then os.replace()). On failure, temp files are cleaned up and ConfigWriteError is raised.
  5. Preview: ConfigGenerator.preview_config() renders configs as strings without writing to disk.

Sensitive Field Encryption

The ConfigRepository handles encryption/decryption transparently:

  • _encrypt_sensitive(): Encrypts specified fields using Fernet, prefixes with "encrypted:"
  • _decrypt_sensitive(): Decrypts "encrypted:"-prefixed fields
  • is_encrypted(): Checks if a value starts with "encrypted:"
  • Non-encrypted values pass through decryption unchanged

Scheduled Cleanup Jobs

APScheduler BackgroundScheduler with a ThreadPoolExecutor(max_workers=2) runs three cleanup jobs:

Job Schedule Retention Action
cleanup_old_logs Daily at 03:00 7 days Delete log entries older than retention
cleanup_old_metrics Every 6 hours 1 day Delete metrics older than retention
cleanup_old_events Weekly Sunday 04:00 30 days Delete server events older than retention

Each job creates a thread-local DB connection, runs the cleanup, commits, and closes the connection.


API Structure

All routes are prefixed with /api (except WebSocket at /ws).

Authentication (/api/auth)

Method Path Auth Description
POST /auth/login None Login, returns JWT
POST /auth/logout Bearer Client-side token deletion
GET /auth/me Bearer Get current user
PUT /auth/password Bearer Change password
GET /auth/users Admin List all users
POST /auth/users Admin Create user
DELETE /auth/users/{id} Admin Delete user

Servers (/api/servers)

Method Path Auth Description
GET /servers Bearer List servers (optional game_type filter)
POST /servers Admin Create server
GET /servers/{id} Bearer Get server details
PUT /servers/{id} Admin Update server
DELETE /servers/{id} Admin Delete server (must be stopped)
POST /servers/{id}/start Admin Start server
POST /servers/{id}/stop Admin Stop server (optional force)
POST /servers/{id}/restart Admin Restart server
POST /servers/{id}/kill Admin Force-kill server
GET /servers/{id}/config Bearer Get all config sections (sensitive fields masked)
GET /servers/{id}/config/preview Admin Preview config files without writing
GET /servers/{id}/config/{section} Bearer Get single config section
PUT /servers/{id}/config/{section} Admin Update config section (optimistic locking)
POST /servers/{id}/rcon/command Admin Send RCon command

Players, Bans, Missions, Mods (/api/players, /api/bans, /api/missions, /api/mods)

Sub-routers under /api for their respective CRUD operations. All require Bearer auth; write operations require admin role.

Games (/api/games)

Method Path Auth Description
GET /games None List all registered game types with capabilities
GET /games/{game_type} None Get game type details, config sections, allowed executables
GET /games/{game_type}/config-schema None Get Pydantic JSON schemas for config sections
GET /games/{game_type}/defaults None Get default config values

System (/api/system)

Method Path Auth Description
GET /system/health None Health check
GET /system/status Bearer System status (running/total servers, supported games)

WebSocket (/ws)

Path Auth Description
/ws?token=JWT JWT query param Connect to real-time event stream
/ws?token=JWT&server_id=1&server_id=2 JWT query param Subscribe to specific servers

Frontend Architecture

Tech Stack

  • React 19 with TypeScript 6
  • Vite 8 for bundling and dev server
  • Tailwind CSS 3 for styling with a custom dark neumorphic design system
  • Zustand 5 for client state (auth, UI)
  • TanStack Query 5 for server state
  • React Router 7 for routing
  • Axios for HTTP client
  • React Hook Form + Zod for form validation (login page)
  • Lucide React for icons

Design System

Dark neumorphic theme defined in tailwind.config.js:

  • Surface colors: surface-base (#1a1a2e), surface-raised (#1e1e35), surface-recessed (#16162a), surface-overlay (#22223a)
  • Accent: Amber (#f59e0b) with bright, dim, and glow variants
  • Status colors: running (green), stopped (gray), crashed (red), starting (amber), restarting (blue)
  • Neumorphic shadows: shadow-neu-raised, shadow-neu-raised-lg, shadow-neu-recessed
  • Glow shadows: shadow-glow-green, shadow-glow-amber, shadow-glow-red, shadow-glow-blue
  • Animations: pulse-slow (3s), glow-pulse (2s opacity fade)
  • Font: Inter (UI) + JetBrains Mono (code)
  • Component classes: .neu-card, .neu-input, .btn-primary, .btn-ghost, .btn-danger, .status-led

Application Structure

App.tsx
  BrowserRouter
    QueryClientProvider (staleTime: 10s, retry: 2, no refetchOnWindowFocus)
      ReactQueryDevtools
      Routes:
        /login -> LoginPage
        /* -> ProtectedLayout (requires auth)
               Sidebar + main content area
               / -> DashboardPage
               /servers/:serverId -> ServerDetailPage (7 tabs: overview, config, players, bans, missions, mods, logs)
               /servers/new -> CreateServerPage (4-step wizard, admin only)
               /settings -> SettingsPage (Account + Users tabs)

State Management

Auth Store (store/auth.store.ts):

  • Zustand store with persist middleware (localStorage key: languard-auth)
  • State: token, user (id, username, role), isAuthenticated
  • Actions: setAuth(token, user), clearAuth()
  • On rehydration, sets isAuthenticated = true if token exists

UI Store (store/ui.store.ts):

  • Zustand store (in-memory, not persisted)
  • State: sidebarOpen, activeServerId, notifications[]
  • Actions: toggleSidebar(), setActiveServer(), addNotification(), removeNotification()
  • Notifications auto-dismiss after 5 seconds

API Client (lib/api.ts)

Axios instance with:

  • Base URL from VITE_API_URL env var (default: http://localhost:8000)
  • 30-second timeout
  • Request interceptor: adds Authorization: Bearer <token> header from localStorage
  • Response interceptor: on 401, clears token and redirects to /login (except for /api/auth/ endpoints)
  • Type: ApiResponse<T> = { success: boolean; data: T; error?: string }

WebSocket Hook (hooks/useWebSocket.ts)

  • Connects to VITE_WS_URL/ws?token=<jwt>[&server_id=<id>]
  • Exponential backoff reconnect: starts at 2 seconds, doubles up to 30 seconds max
  • On message, invalidates relevant TanStack Query cache keys based on event type
  • On close with code 4001 (auth failure), does not reconnect
  • Cleanup on unmount: closes WebSocket, clears reconnect timer

Server Data Hooks (hooks/useServers.ts)

TanStack Query hooks wrapping the API client:

  • useServers() -- Lists all servers, refetches every 30 seconds
  • useServer(id) -- Gets single server
  • useStartServer(), useStopServer(), useRestartServer(), useCreateServer(), useDeleteServer() -- Mutation hooks that invalidate relevant query caches on success

Vite Dev Proxy

In development, Vite proxies:

  • /api -> http://localhost:8000
  • /ws -> ws://localhost:8000 (WebSocket upgrade)

Tests

  • Unit tests (12 files): Vitest + React Testing Library + jsdom

    • Component tests: DashboardPage, LoginPage, ServerCard, Sidebar, StatusLed
    • Hook tests: useServers, useWebSocket
    • Store tests: auth.store, ui.store
    • Lib tests: api (Axios interceptors)
  • E2E tests (3 specs): Playwright

    • auth/login.spec.ts -- Login flow
    • dashboard/dashboard.spec.ts -- Dashboard interactions
    • integration/fullstack.spec.ts -- Full-stack integration

Security Measures

  1. JWT Authentication: All API routes except /api/auth/login and /api/games/* and /api/system/health require a Bearer token
  2. Role-based Authorization: admin and viewer roles enforced via dependency injection
  3. Fernet Encryption: Sensitive config fields (RCon passwords, admin passwords) encrypted at rest with AES-256
  4. Exe Allowlist: ProcessConfig.get_allowed_executables() prevents path traversal by validating executable names
  5. Filename Sanitization: sanitize_filename() in file_utils.py strips path separators, null bytes, control characters, and collapses consecutive dots
  6. Atomic Config Writes: Config files written to .tmp first, then os.replace() for atomic rename
  7. Optimistic Locking: Config updates require config_version to prevent lost updates
  8. Port Conflict Detection: check_ports_against_running_servers() prevents port collisions between servers
  9. CORS: Configurable origins via LANGUARD_CORS_ORIGINS
  10. Password Hashing: bcrypt for all user passwords
  11. WebSocket Auth: JWT via query parameter (browser WebSocket limitation); invalid tokens close with code 4001

Environment Variables

All prefixed with LANGUARD_:

Variable Required Default Description
LANGUARD_SECRET_KEY Yes -- JWT signing key
LANGUARD_ENCRYPTION_KEY Yes -- Fernet base64 key for config field encryption
LANGUARD_DB_PATH No ./languard.db SQLite database file path
LANGUARD_SERVERS_DIR No ./servers Base directory for server instances
LANGUARD_HOST No 0.0.0.0 Server bind address
LANGUARD_PORT No 8000 Server bind port
LANGUARD_CORS_ORIGINS No ["http://localhost:5173"] JSON array of allowed origins
LANGUARD_LOG_RETENTION_DAYS No 7 Days to retain log entries
LANGUARD_METRICS_RETENTION_DAYS No 30 Days to retain metrics
LANGUARD_PLAYER_HISTORY_RETENTION_DAYS No 90 Days to retain player history
LANGUARD_JWT_EXPIRE_HOURS No 24 JWT token lifetime
LANGUARD_LOGIN_RATE_LIMIT No 5/minute Rate limit for login endpoint
LANGUARD_ARMA3_DEFAULT_EXE No C:/Arma3Server/arma3server_x64.exe Default Arma 3 server executable path

Configuration uses pydantic-settings with SettingsConfigDict (env prefix LANGUARD_, .env file support).


Directory Layout

Backend

backend/
  main.py                    # FastAPI app factory, lifespan, CORS, routers
  config.py                  # Pydantic Settings class (env vars)
  database.py                # SQLAlchemy engine, get_db(), get_thread_db(), migrations
  dependencies.py             # FastAPI dependencies: get_current_user, require_admin, get_adapter_for_server
  requirements.txt            # Python dependencies
  adapters/
    __init__.py               # initialize_adapters() -- loads built-in + third-party
    protocols.py               # 7 capability Protocols + GameAdapter composite
    registry.py                # GameAdapterRegistry class-level singleton
    exceptions.py              # Typed adapter exceptions
    arma3/
      __init__.py              # Exports ARMA3_ADAPTER
      adapter.py               # Arma3Adapter (implements all 7 capabilities)
      config_generator.py      # Arma3ConfigGenerator (server.cfg, launch args)
      process_config.py        # Arma3ProcessConfig (exe allowlist, port conventions)
      log_parser.py            # RPTParser (Arma 3 .rpt log format)
      remote_admin.py          # Arma3RemoteAdminFactory (BattlEye RCon)
      rcon_client.py           # BEClient (BattlEye RCon protocol)
      rcon_service.py          # RCon command helpers
      mission_manager.py       # Arma3MissionManager (.pbo missions)
      mod_manager.py           # Arma3ModManager (mod CLI args)
      ban_manager.py           # Arma3BanManager (ban file sync)
      migrations/              # Game-specific migration files
  core/
    __init__.py
    auth/
      router.py                # Auth API routes
      service.py               # AuthService (login, create_user, change_password, seed_admin)
      utils.py                  # JWT create/decode, password hash/verify
      schemas.py                # Pydantic request models
    servers/
      router.py                # Server CRUD + lifecycle + config + RCon routes
      service.py                # ServerService (orchestrates all server operations)
      process_manager.py       # ProcessManager singleton (Popen lifecycle + PID recovery)
      schemas.py                # Pydantic request/response models
      players_router.py        # Players API routes
      bans_router.py           # Bans API routes
      missions_router.py       # Missions API routes
      mods_router.py           # Mods API routes
    games/
      router.py                # Game type info routes
    system/
      router.py                # Health and status routes
    dal/
      __init__.py
      base_repository.py       # BaseRepository with SQL helpers
      server_repository.py     # Server CRUD operations
      config_repository.py     # Config section CRUD with Fernet encryption
      player_repository.py     # Player operations
      log_repository.py        # Log operations
      metrics_repository.py    # Metrics operations + cleanup
      event_repository.py      # Server event operations + cleanup
      ban_repository.py         # Ban operations
    threads/
      thread_registry.py       # ThreadRegistry (manages per-server thread bundles)
      process_monitor.py       # ProcessMonitorThread
      metrics_collector.py    # MetricsCollectorThread
      log_tail.py              # LogTailThread
      remote_admin_poller.py   # RemoteAdminPollerThread
    websocket/
      __init__.py
      manager.py               # WebSocketManager (asyncio-side, subscription model)
      broadcast_thread.py      # BroadcastThread (Queue -> asyncio bridge)
      router.py                # /ws endpoint with JWT auth
    utils/
      __init__.py
      crypto.py                # Fernet encrypt/decrypt for config fields
      file_utils.py            # Server dir helpers, filename sanitization
      port_checker.py          # Port availability checking
    jobs/
      __init__.py
      scheduler.py             # APScheduler BackgroundScheduler setup
      cleanup_jobs.py          # Registered cleanup jobs (logs, metrics, events)
    migrations/
      001_initial_schema.sql   # Initial database schema
    logs/                       # Log parsing (not a module, handled by log_tail thread)
    metrics/                    # Metrics collection (not a module, handled by metrics thread)
    players/                    # Player tracking (not a module, handled by RCon poller)
    events/                     # Event tracking (not a module, handled by services)

Frontend

frontend/
  src/
    App.tsx                    # BrowserRouter + QueryClientProvider + ProtectedLayout
    main.tsx                   # React entry point
    index.css                  # Tailwind imports + neumorphic component classes
    vite-env.d.ts              # Vite type declarations
    pages/
      LoginPage.tsx            # Login form (react-hook-form + zod validation)
      DashboardPage.tsx        # Server dashboard with ServerCard grid
    components/
      layout/
        Sidebar.tsx             # Sidebar navigation
      servers/
        ServerCard.tsx          # Server status card with lifecycle buttons
      ui/
        StatusLed.tsx           # Status LED indicator component
    hooks/
      useServers.ts            # TanStack Query hooks for server CRUD + lifecycle
      useWebSocket.ts          # WebSocket hook with exponential backoff reconnect
    store/
      auth.store.ts            # Zustand auth store (persisted to localStorage)
      ui.store.ts              # Zustand UI store (sidebar, notifications)
    lib/
      api.ts                   # Axios client with auth interceptors
    __tests__/                  # 12 unit test files (Vitest)
  tests-e2e/
    auth/
      login.spec.ts            # Login E2E test
    dashboard/
      dashboard.spec.ts        # Dashboard E2E test
    integration/
      fullstack.spec.ts        # Full-stack integration test
    pages/
      LoginPage.ts              # Playwright page object
      DashboardPage.ts          # Playwright page object
  index.html                   # Vite HTML entry
  vite.config.ts               # Vite config with @/ alias and dev proxy
  tailwind.config.js           # Custom dark neumorphic design tokens
  package.json                 # Dependencies and scripts

Key Design Decisions

  1. Raw SQL over ORM: The project uses SQLAlchemy's text() for all queries instead of the ORM. This keeps queries explicit and avoids hidden N+1 problems. The BaseRepository provides thin helpers (_execute, _fetchone, _fetchall, _lastrowid).

  2. Synchronous DB access: All database operations are synchronous. This is appropriate for SQLite (single-writer) and avoids the complexity of async SQLAlchemy. FastAPI runs sync route handlers in a thread pool by default.

  3. Thread-per-server model: Each running server gets up to 4 background threads (monitor, metrics, log tail, RCon poller). This avoids the complexity of async subprocess management while keeping operations isolated per server.

  4. Queue-based broadcast: Background threads write to a queue.Queue, and a BroadcastThread bridges to the asyncio event loop via asyncio.run_coroutine_threadsafe(). This avoids thread-safety issues with the WebSocket manager.

  5. Adapter Protocol system: Using Protocol classes with @runtime_checkable allows duck-typing with isinstance() checks while maintaining type safety. Adapters are resolved by game_type string at runtime.

  6. Optimistic locking for configs: The config_version column in game_configs prevents lost updates when multiple users edit configs concurrently. On version conflict, the API returns 409 with the current config data.

  7. Fernet encryption for secrets: Sensitive fields are encrypted transparently in ConfigRepository. The encrypted: prefix distinguishes encrypted values from plaintext, allowing gradual migration.

  8. Atomic config file writes: Config files are written to temporary .tmp files first, then os.replace() renames them to the final path. This prevents partial writes on crash.

  9. PID recovery on restart: ProcessManager.recover_on_startup() uses psutil to check if a PID from a previous run is still alive and running an allowed executable. This handles the case where the Languard process restarts but game servers are still running.