- ARCHITECTURE.md: fix diagram (Server Detail was "planned", now complete), expand frontend directory listing with all 5 pages, all server components, all hooks - FRONTEND.md: note planned terrain/display_name/workshop_id fields on Mission/Mod types; add planned hooks table for Phases 1-5 of UX enhancement plan - MODULES.md: annotate PlayerRepository with get_by_slot() and ThreadRegistry with get_rcon_client() as planned additions in Phase 4 - API.md: add "Upcoming Endpoints" section documenting all planned routes for Phases 1-5 of the UX enhancement plan - README.md: update unit test count (69 → ~120), update frontend structure comment to list all current pages/components/hooks
41 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) |
| Login | Dashboard | Server Detail (7 tabs) | Create Server |
| Settings | (all routes complete) |
+------------------------------+-------------------------------+
| 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:
- Initialize DB --
get_engine()creates the SQLAlchemy engine with WAL mode, foreign keys, and busy timeout pragmas.run_migrations()applies all pending SQL files fromcore/migrations/. - Register adapters --
initialize_adapters()imports built-in adapters (Arma 3) which self-register viaGameAdapterRegistry.register(), then scansimportlib.metadataentry_points under"languard.adapters"for third-party adapters. - Create WebSocketManager -- Instantiated and stored on
app.state.ws_manager. - Create broadcast queue and BroadcastThread -- A
queue.Queue(maxsize=1000)bridges background threads to the asyncio event loop.BroadcastThreadis a daemon thread that callsasyncio.run_coroutine_threadsafe()to schedulews_manager.broadcast()on the event loop. - Create ThreadRegistry -- Initialized with the
ProcessManagersingleton,GameAdapterRegistry, and the global broadcast queue. Stored onapp.state.thread_registry. - 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". - Reattach threads for running servers -- Iterates running servers and calls
ThreadRegistry.reattach_server_threads(). - 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. - 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.
ServerServiceorchestrates lifecycle operations, delegating game-specific work to adapters. - Repositories encapsulate SQL. All extend
BaseRepository, which provides_execute(),_fetchone(),_fetchall(), and_lastrowid()helpers that wrapsqlalchemy.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 packageget(game_type)-- RaisesKeyErrorif not foundall()-- Returns all registered adapterslist_game_types()-- Returns metadata list for the/api/gamesendpoint
Arma3Adapter
Implements all 7 capabilities:
- ConfigGenerator: Defines
server,rcon,missionsections using Pydantic models. Writesserver.cfgusing atomic write pattern (.tmpthenos.replace()). Builds launch args from config + mod list. - ProcessConfig: Allows
arma3server_x64.exeandarma3server.exe. Derives 4 ports fromgame_port(game, steam_query, von, steam_auth). Default game port 2302, default RCon port game+4. - LogParser: Parses Arma 3
.rptlog files. Resolves log path from server config or defaults toserver_dir/server/*.rpt. - RemoteAdmin: Implements BattlEye RCon protocol via
Arma3RemoteAdminFactory. Supports login, command sending, player listing, kick, ban, say-all, and shutdown. - MissionManager: Handles
.pbomission 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 failedConfigValidationError-- Pydantic model rejected configConfigMigrationError-- Schema migration failedLaunchArgsError-- Launch args construction failedRemoteAdminError-- RCon connection/command failure (withrecoverableflag)ExeNotAllowedError-- Executable not in adapter allowlist
Adding a New Game Adapter
- Create
adapters/<game>/package with an adapter class implementingGameAdapterprotocol - Create capability modules for supported protocols
- Export a module-level adapter instance (e.g.,
MYGAME_ADAPTER) - Import it in
adapters/__init__.py'sload_builtin_adapters()-- or register viapyproject.tomlentry_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_KEYenv var (required)
Password Hashing
- bcrypt via the
bcryptPython package hash_password()andverify_password()incore/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 dictrequire_admin()-- Wrapsget_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_KEYenv var (Fernet base64 key, required) - Format: encrypted values are stored as
encrypted:<base64-token>in the database ConfigRepositoryhandles encryption/decryption transparently -- reads decrypt, writes encrypt- The
get_sensitive_fields()method on eachConfigGeneratordeclares 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, usesCREATE_NO_WINDOWflag 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.Lockserializes 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'sget_allowed_executables(). Invalid PIDs cause the server to be marked "crashed". Valid PIDs get a_PsutilProcessWrapperattached 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:
- Client connects with
tokenand optionalserver_idparams - Server validates JWT, closes with code
4001if invalid - Server sends
{"type": "connected", "data": {"user": ..., "subscriptions": ...}} - Server pushes events for subscribed server IDs
WebSocketManager (asyncio-side)
connect(ws, server_ids)-- Accept connection, register subscriptionsdisconnect(ws)-- Remove connectionbroadcast(server_id, message)-- Send to all clients subscribed toserver_idor toNone(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
- Creation: When a server is created,
ConfigRepository.upsert_section()is called for each section with adapter defaults. Sensitive fields are encrypted. - Read:
get_section()andget_all_sections()decrypt sensitive fields transparently. - Update:
upsert_section()supports optimistic locking viaexpected_config_version. On conflict, raisesValueErrorwith"CONFIG_VERSION_CONFLICT"prefix, which the service layer translates to a 409 response with the current config data. - Write to disk:
ConfigGenerator.write_configs()uses an atomic write pattern (write to.tmp, thenos.replace()). On failure, temp files are cleaned up andConfigWriteErroris raised. - 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 fieldsis_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
persistmiddleware (localStorage key:languard-auth) - State:
token,user(id, username, role),isAuthenticated - Actions:
setAuth(token, user),clearAuth() - On rehydration, sets
isAuthenticated = trueif 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_URLenv 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 secondsuseServer(id)-- Gets single serveruseStartServer(),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)
- Component tests:
-
E2E tests (3 specs): Playwright
auth/login.spec.ts-- Login flowdashboard/dashboard.spec.ts-- Dashboard interactionsintegration/fullstack.spec.ts-- Full-stack integration
Security Measures
- JWT Authentication: All API routes except
/api/auth/loginand/api/games/*and/api/system/healthrequire a Bearer token - Role-based Authorization:
adminandviewerroles enforced via dependency injection - Fernet Encryption: Sensitive config fields (RCon passwords, admin passwords) encrypted at rest with AES-256
- Exe Allowlist:
ProcessConfig.get_allowed_executables()prevents path traversal by validating executable names - Filename Sanitization:
sanitize_filename()infile_utils.pystrips path separators, null bytes, control characters, and collapses consecutive dots - Atomic Config Writes: Config files written to
.tmpfirst, thenos.replace()for atomic rename - Optimistic Locking: Config updates require
config_versionto prevent lost updates - Port Conflict Detection:
check_ports_against_running_servers()prevents port collisions between servers - CORS: Configurable origins via
LANGUARD_CORS_ORIGINS - Password Hashing: bcrypt for all user passwords
- 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
ServerDetailPage.tsx # 7-tab detail page (overview, config, players, bans, missions, mods, logs)
CreateServerPage.tsx # 4-step wizard (admin only)
SettingsPage.tsx # Password change + admin user management
components/
layout/
Sidebar.tsx # Sidebar navigation
servers/
ServerCard.tsx # Server status card with lifecycle buttons
ServerHeader.tsx # Server name, status, stats, lifecycle buttons
ConfigEditor.tsx # Tabbed config editor with optimistic locking
PlayerTable.tsx # Current players + history with search
BanTable.tsx # Ban list + create/revoke form
MissionList.tsx # Mission list + .pbo upload/delete
ModList.tsx # Mod list with enable/disable checkboxes
LogViewer.tsx # Log display with level filter (props-driven)
settings/
PasswordChange.tsx # Password change form
UserManager.tsx # User CRUD table (admin only)
ui/
StatusLed.tsx # Status LED indicator component
hooks/
useServers.ts # TanStack Query hooks for server CRUD + lifecycle
useServerDetail.ts # Config, players, bans, missions, mods, RCon hooks
useAuth.ts # Auth management hooks (users, password, logout)
useGames.ts # Game type hooks (list, detail, schema, defaults)
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__/ # 14 unit test files (~120 tests, 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
-
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. TheBaseRepositoryprovides thin helpers (_execute,_fetchone,_fetchall,_lastrowid). -
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.
-
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.
-
Queue-based broadcast: Background threads write to a
queue.Queue, and aBroadcastThreadbridges to the asyncio event loop viaasyncio.run_coroutine_threadsafe(). This avoids thread-safety issues with the WebSocket manager. -
Adapter Protocol system: Using
Protocolclasses with@runtime_checkableallows duck-typing withisinstance()checks while maintaining type safety. Adapters are resolved bygame_typestring at runtime. -
Optimistic locking for configs: The
config_versioncolumn ingame_configsprevents lost updates when multiple users edit configs concurrently. On version conflict, the API returns 409 with the current config data. -
Fernet encryption for secrets: Sensitive fields are encrypted transparently in
ConfigRepository. Theencrypted:prefix distinguishes encrypted values from plaintext, allowing gradual migration. -
Atomic config file writes: Config files are written to temporary
.tmpfiles first, thenos.replace()renames them to the final path. This prevents partial writes on crash. -
PID recovery on restart:
ProcessManager.recover_on_startup()usespsutilto 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.