- Revamp architecture for modular game server support (Arma 3 first, extensible) - Merge ConfigSchema into ConfigGenerator per council decision (8→7 protocols) - Add has_capability() method to GameAdapter protocol for explicit capability probing - Add FRONTEND.md: production-grade dark neumorphism design with amber/orange palette - Update all docs (ARCHITECTURE, MODULES, DATABASE, API, IMPLEMENTATION_PLAN, THREADING) to reflect protocol merge and multi-game adapter patterns
25 KiB
Languard Servers Manager — Implementation Plan
Prerequisites
Before starting, ensure the following are available:
- Python 3.11+
- A working Arma 3 dedicated server installation (for testing the first adapter)
- Node.js 18+ (for frontend dev server)
- The reference docs: ARCHITECTURE.md, DATABASE.md, API.md, MODULES.md, THREADING.md
Phase 0 — Adapter Framework (New)
Goal: Build the adapter protocol + registry system before any other code. This is the foundation that makes every subsequent phase modular.
Step 0.1 — Adapter protocols, exceptions, and registry
- Create
backend/adapters/__init__.py— auto-imports built-in adapters - Create
backend/adapters/protocols.py— all capability Protocol definitions:ConfigGenerator(merged: schema + generation),ProcessConfig,LogParserRemoteAdmin,RemoteAdminClientMissionManager,ModManager,BanManagerGameAdapter(composite protocol withhas_capability()method)ConfigGeneratorincludesget_sections(),get_sensitive_fields(section),get_config_version()RemoteAdminincludesget_player_data_schema() -> type[BaseModel] | NoneMissionManagerincludesget_mission_data_schema() -> type[BaseModel] | NoneModManagerincludesget_mod_data_schema() -> type[BaseModel] | NoneBanManagerincludesget_ban_data_schema() -> type[BaseModel] | None
- Create
backend/adapters/exceptions.py— typed adapter exceptions:AdapterError(base)ConfigWriteError— atomic write failed (tmp file cleanup done)ConfigValidationError— adapter Pydantic validation failedLaunchArgsError— invalid launch argumentsRemoteAdminError— admin protocol communication failedExeNotAllowedError— executable not in adapter allowlist
- Create
backend/adapters/registry.py—GameAdapterRegistrysingleton - Add
has_capability(name) -> boolmethod toGameAdapterprotocol — core uses explicit capability probes instead of scatteredNonechecks - Write unit tests: register adapter, get adapter, list game types, missing adapter raises error, exceptions are catchable by type, has_capability returns correct bools
Step 0.2 — Arma 3 adapter skeleton
- Create
backend/adapters/arma3/__init__.py— exports and registersARMA3_ADAPTER - Create
backend/adapters/arma3/adapter.py—Arma3Adapterclass (all methods return stubs initially) - Create
backend/adapters/arma3/process_config.py—Arma3ProcessConfig(full implementation) - Create
backend/adapters/arma3/config_generator.py— Pydantic models (ServerConfig, BasicConfig, ProfileConfig, LaunchConfig, RConConfig) +Arma3ConfigGenerator(schema + generation merged) - Third-party adapter loading: add
languard.adaptersentry_point group topyproject.toml:Core scans entry_points at startup via[project.entry-points."languard.adapters"] arma3 = "adapters.arma3:ARMA3_ADAPTER"importlib.metadatain addition to built-in imports. - Write unit tests: adapter registers, protocols satisfied, config schema produces valid JSON Schema
Test: Import adapters module → GameAdapterRegistry.get("arma3") returns a valid adapter. GameAdapterRegistry.list_game_types() returns [{"game_type": "arma3", "display_name": "Arma 3", ...}].
Phase 1 — Foundation
Goal: Running FastAPI server with DB, auth, and basic server CRUD using the adapter framework.
Step 1.1 — Project scaffold
mkdir backend
cd backend
python -m venv venv
venv/Scripts/activate
pip install fastapi uvicorn[standard] sqlalchemy python-jose[cryptography] passlib[bcrypt] cryptography psutil apscheduler python-multipart slowapi pytest pytest-asyncio httpx
pip freeze > requirements.txt
Create:
backend/config.py— Settings classbackend/main.py— FastAPI app factory, startup/shutdown hooksbackend/conftest.py— pytest fixtures (in-memory SQLite, test client).env.example— All env vars documented
Step 1.2 — Database + Migrations
- Create
backend/core/migrations/001_initial_schema.sql— all core tables:schema_migrations,users,servers(withgame_type),game_configsmods(withgame_type,game_data),server_modsmissions,mission_rotation(withgame_data)players(withslot_idTEXT,game_data),player_historybans(withgame_data),logs,metrics,server_events- Include all CHECK constraints and indexes
- Include
PRAGMA busy_timeout=5000in engine setup
- Create
backend/core/dal/event_repository.py - Create
backend/database.py:get_engine()with WAL + FK pragmarun_migrations()get_db()— FastAPI dependencyget_thread_db()— thread-local session factory
- Call
run_migrations()inmain.py:on_startup()
Test: Start app, confirm languard.db created with all tables. Run pytest with in-memory SQLite.
Step 1.3 — Auth module
backend/core/auth/utils.py—hash_password,verify_password,create_access_token,decode_access_tokenbackend/core/auth/schemas.py—LoginRequest,TokenResponse,UserResponsebackend/core/auth/service.py—AuthServicebackend/core/auth/router.py— login, me, users CRUDbackend/dependencies.py—get_current_user,require_admin,get_adapter_for_servermain.py— seed default admin user on first startup (random password printed to stdout)- Add rate limiting to
POST /auth/login(5 attempts/minute per IP via slowapi)
Test: POST /api/auth/login returns JWT. GET /api/auth/me with token returns user. Rate limiting returns 429 after 5 failed attempts.
Step 1.4 — Server CRUD (no process management yet)
backend/core/dal/server_repository.pybackend/core/dal/config_repository.py— managesgame_configstablebackend/core/servers/schemas.py—CreateServerRequest(includesgame_type)backend/core/servers/router.py— GET, POST, PUT, DELETE /serversbackend/core/servers/service.py— CRUD methods +create_serverseeds config sections from adapter defaultsbackend/core/utils/file_utils.py—ensure_server_dirs()(uses adapter'sget_server_dir_layout())backend/core/utils/port_checker.py—is_port_in_use(),check_server_ports_available()- Full cross-game port checking: query ALL running servers, resolve each adapter, get port conventions for each, check the full derived port set
- Example: Arma 3 uses game port + 1 (Steam query), BattlEye RCon port; another game may use different conventions — all checked
Test: Create server via API with game_type: "arma3" → confirm DB row + game_configs rows + directory created. Create a second server with a port that conflicts with derived ports of the first → confirm 409 error.
Step 1.5 — Game type discovery endpoints
backend/core/games/router.py—GET /games,GET /games/{type},GET /games/{type}/config-schema,GET /games/{type}/defaults
Test: GET /api/games returns [{"game_type": "arma3", ...}]. GET /api/games/arma3/config-schema returns JSON Schema for all 5 Arma 3 config sections.
Step 1.6 — Migration script for existing Arma 3 data
If upgrading from the single-game schema, create a migration script:
- Create
backend/core/migrations/002_migrate_arma3_config.py - Column type map:
max_playersINT→JSONmaxPlayers,hostnameTEXT→JSONhostname, etc. migrate_config_table(): read old Arma 3 config table rows → buildgame_configsJSON blobs → insert into new table → delete old rowsmigrate_player_data(): convertplayer_numINTEGER →slot_idTEXT- Transaction + rollback: all migration runs inside a single DB transaction; on failure, full rollback
- Row count verification: after migration, assert row counts match between old and new tables
- Idempotent: safe to run multiple times (checks if migration already applied)
Test: Create test DB with old single-game schema + sample data → run migration script → verify all data in new tables → verify old tables dropped.
Phase 2 — Arma 3 Adapter Implementation
Goal: Complete the Arma 3 adapter with config generation and process management. This phase proves the adapter architecture works end-to-end with the primary game.
Step 2.1 — Config Generator (Arma 3 adapter)
backend/adapters/arma3/config_generator.py—Arma3ConfigGenerator- Use a structured builder (NOT f-strings) — escape double quotes and newlines in all user-supplied string values
- Write
server.cfgcovering all params from config schema, including mission rotation asclass Missions {}block - Write
basic.cfg - Write
server.Arma3Profile— written toservers/{id}/server/server.Arma3Profile - Write
beserver.cfg— createsbattleye/directory, writes RCon config build_launch_args()— assembles full CLI arg list including-bepath=./battleyepreview_config()— renders all files without writing to disk, returnsdict[str, str]of label→content (filenames for file-based, variable names for env-var, argument names for CLI)- Set file permissions 0600 on config files containing passwords
- Atomic write pattern: all config files written to
.tmpfiles first, thenos.replace()for atomic rename. On any write failure, all.tmpfiles are cleaned up and original files remain untouched. RaisesConfigWriteErroron failure.
Test: Arma3ConfigGenerator.write_configs(server_id, dir, config) → inspect all generated files. Test config injection prevention: set hostname to X"; passwordAdmin = "pwned"; // — verify generated server.cfg does NOT contain the injected directive. Test atomic write: mock os.replace() to raise OSError → confirm .tmp files are cleaned up and original files are untouched.
Step 2.2 — Process Manager (core)
backend/core/servers/process_manager.py—ProcessManagersingleton (game-agnostic)start(server_id, exe_path, args, cwd=servers/{id}/)stop(server_id, timeout=30)— on Windows:terminate()= hard killkill(),is_running(),get_pid()recover_on_startup()— verify PID is alive AND process name matches adapter allowlist (prevents PID reuse)- Wire
ServerService.start()andServerService.stop()— both delegate to adapter for exe validation and config generation - Add
POST /servers/{id}/start,POST /servers/{id}/stop,POST /servers/{id}/killendpoints - Typed exception handling in start flow: catch and map adapter exceptions to HTTP responses:
ConfigWriteError→ 500 (atomic write failed, tmp cleaned)ConfigValidationError→ 422 (invalid config values)LaunchArgsError→ 400 (invalid launch arguments)ExeNotAllowedError→ 403 (executable not in adapter allowlist)
Test: Start a server via API → confirm process appears in Task Manager. Stop it → confirm process ends. Test error paths: set invalid exe path → confirm 403 ExeNotAllowedError response.
Step 2.3 — Config endpoints (core + adapter validation)
GET /servers/{id}/config— reads all sections fromgame_configsGET /servers/{id}/config/{section}— reads single section, response includes_metawithconfig_versionandschema_versionPUT /servers/{id}/config/{section}— validates against adapter's Pydantic model, encrypts sensitive fields viaadapter.get_sensitive_fields(section), stores ingame_configs- Optimistic locking: client must send
config_versionin request body; if it doesn't match the current row'sconfig_version, return 409 Conflict withCONFIG_VERSION_CONFLICTerror code - On successful write, increment
config_versionin the row
- Optimistic locking: client must send
GET /servers/{id}/config/preview— delegates to adapter'spreview_config(), returnsdict[str, str]of label→contentGET /servers/{id}/config/download/{filename}— filename validated against adapter allowlist
Test: Update hostname via API → regenerate and start server → confirm new hostname appears in server browser. Test optimistic locking: two concurrent PUT requests with same config_version → one succeeds (200), one fails (409 Conflict).
Phase 3 — Background Threads (Core + Adapter)
Goal: Live monitoring — process crash detection, log tailing, metrics.
Step 3.1 — Thread infrastructure
backend/core/threads/base_thread.py—BaseServerThreadbackend/core/threads/thread_registry.py—ThreadRegistry(adapter-aware)- Wire
start_server_threads()/stop_server_threads()intoServerService.start()/ServerService.stop()
Step 3.2 — Process Monitor Thread (core)
backend/core/threads/process_monitor.py- Crash detection + status update in DB
- Auto-restart with exponential backoff (daemon cleanup thread pattern)
Test: Start server → kill process manually → confirm DB status changes to 'crashed'. Test: Enable auto_restart → kill → confirm server restarts automatically.
Step 3.3 — Log Parser (Arma 3 adapter) + Log Tail Thread (core)
backend/adapters/arma3/log_parser.py—RPTParserimplementingLogParserprotocolbackend/core/threads/log_tail.py—LogTailThread(generic, takes adapter'sLogParser)backend/core/dal/log_repository.pybackend/core/logs/service.pybackend/core/logs/router.py—GET /servers/{id}/logs
Test: Start server → GET /api/servers/{id}/logs returns recent RPT lines.
Step 3.4 — Metrics Collector Thread (core)
backend/core/metrics/service.pybackend/core/dal/metrics_repository.pybackend/core/threads/metrics_collector.pybackend/core/metrics/router.py—GET /servers/{id}/metrics
Test: Running server → query metrics endpoint → see CPU/RAM data points.
Phase 4 — Remote Admin (Arma 3: BattlEye RCon)
Goal: Real-time player list, in-game admin commands via the adapter's RemoteAdmin protocol.
Step 4.1 — RCon Client (Arma 3 adapter)
backend/adapters/arma3/rcon_client.py—BERConClient- Implement BE RCon UDP protocol:
- Packet structure:
'BE'+ CRC32 (little-endian) + type byte + payload - Login: type
0x00, payload = password - Command: type
0x01, payload = sequence byte + command string - Keepalive: type
0x02, payload = empty
- Packet structure:
- Request multiplexer: track pending requests by sequence byte, route responses to correct caller via
threading.Eventper request parse_players_response()— parseplayerscommand output- Handle unsolicited server messages (type 0x02)
Test: Connect BERConClient to a running server with BattlEye → successfully login → send players → receive response.
Step 4.2 — RCon Service (Arma 3 adapter) + Remote Admin Poller Thread (core)
backend/adapters/arma3/rcon_service.py—Arma3RConServiceimplementingRemoteAdminprotocolbackend/core/threads/remote_admin_poller.py—RemoteAdminPollerThread(generic, takes adapter'sRemoteAdmin)backend/core/dal/player_repository.pybackend/core/players/service.pybackend/core/players/router.py—GET /servers/{id}/players
Test: Players join server → GET /players returns them with pings.
Step 4.3 — Admin Actions via Remote Admin
POST /servers/{id}/players/{slot_id}/kick— delegates to adapter'sremote_admin.kick_player()POST /servers/{id}/players/{slot_id}/ban— delegates to adapter'sremote_admin.ban_player()POST /servers/{id}/remote-admin/command— delegates to adapter'sremote_admin.send_command()POST /servers/{id}/remote-admin/say— delegates to adapter'sremote_admin.say_all()backend/core/dal/ban_repository.pyGET/POST/DELETE /servers/{id}/bans
Step 4.4 — Ban Manager (Arma 3 adapter)
backend/adapters/arma3/ban_manager.py—Arma3BanManagerimplementingBanManagerprotocol- ban.txt bidirectional sync: on ban add/delete via API, also write to
battleye/ban.txt; on startup, readban.txtand upsert into DB
Test: Kick a player via API → confirm player disconnected from server.
Phase 5 — WebSocket Real-Time
Goal: Live updates to React frontend without polling. Fully game-agnostic.
Step 5.1 — Broadcast infrastructure
backend/core/websocket/broadcaster.py—BroadcastThread+enqueue()backend/core/websocket/manager.py—ConnectionManager- Store event loop reference in
main.py:on_startup() - Start
BroadcastThreadinon_startup() - Wire
BroadcastThread.enqueue()calls into all background threads
Step 5.2 — WebSocket endpoint
backend/core/websocket/router.py- JWT validation from query param
- Subscribe/unsubscribe message handling
- Ping/pong keepalive
Test: Connect to ws://localhost:8000/ws/1?token=... → see live log lines stream in terminal.
Step 5.3 — Integrate all event sources
Wire BroadcastThread.enqueue() into:
ProcessMonitorThread→ status updates, crash eventsLogTailThread→ log linesMetricsCollectorThread→ metrics snapshotsRemoteAdminPollerThread→ player list updatesServerService.start/stop→ status transitions
Test: React frontend connects to WS → server starts → see status, logs, metrics all update in real time.
Phase 6 — Mission & Mod Management (Arma 3 Adapter)
Step 6.1 — Missions
backend/adapters/arma3/mission_manager.py—Arma3MissionManagerimplementingMissionManagerprotocolbackend/core/missions/router.py— generic endpoints (delegate to adapter if capability supported)- Upload file validation (extension from adapter's
MissionManager.file_extension) - Mission rotation CRUD
Test: Upload a .pbo → appears in GET /missions → set as rotation → start server → mission available.
Step 6.2 — Mods
backend/adapters/arma3/mod_manager.py—Arma3ModManagerimplementingModManagerprotocolbackend/core/mods/router.py— generic endpoints (delegate to adapter if capability supported)build_mod_args()— assemble-mod=and-serverMod=args- Wire mod args into
Arma3ConfigGenerator.build_launch_args()
Test: Register @CBA_A3 → enable on server → start → server loads mod.
Phase 7 — Polish & Production
Step 7.1 — APScheduler jobs
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
scheduler.add_job(log_service.cleanup_old_logs, 'cron', hour=3)
scheduler.add_job(metrics_service.cleanup_old_metrics, 'cron', hour=3, minute=30)
scheduler.add_job(player_service.cleanup_old_history, 'cron', hour=4)
scheduler.start()
Step 7.2 — Startup recovery
In on_startup() → ProcessManager.recover_on_startup():
- Query DB for servers with
status='running' - Check if PID still alive (
psutil.pid_exists(pid)) - Validate process name against adapter's
get_allowed_executables() - If alive: re-attach threads (skip process start, just start monitoring threads)
- If dead: mark as
crashed, clear players
Step 7.3 — Events log
backend/core/dal/event_repository.py- Insert events for: start, stop, crash, kick, ban, config change, mission change
GET /servers/{id}/eventsendpoint
Step 7.4 — Security hardening
- Encrypt sensitive DB fields in
game_configsJSON (passwords, rcon_password)backend/core/utils/crypto.pywith FernetLANGUARD_ENCRYPTION_KEYmust be a Fernet base64 key- Adapter declares sensitive fields:
adapter.get_sensitive_fields(section) -> list[str] - ConfigRepository handles Fernet encrypt/decrypt transparently: encrypts declared fields on write, decrypts on read
- Content-Security-Policy headers for frontend
- Penetration testing and security audit
Step 7.5 — Frontend integration checklist
Verify React app can:
- Login and store JWT
- See list of supported game types
- Create server with game type selection
- List servers with live status (any game type)
- Start/stop server and see status update via WebSocket
- View streaming log output (parsed by adapter)
- See player list update (via adapter's remote admin)
- See CPU/RAM charts update
- Edit config sections (dynamic form from adapter's JSON Schema)
- Upload a mission file (if adapter supports missions)
- Manage mods (if adapter supports mods)
- Kick/ban a player (if adapter supports remote admin)
- Send a message to all players (if adapter supports remote admin)
Phase 8 — Second Adapter (Validation)
Goal: Prove the architecture works by adding a second game adapter. This validates that new games require zero core changes.
Choose a second game (examples):
- Minecraft Java Edition — Has RCON (Source protocol), server.properties config, JAR executable, world/ directory, plugins/ mods
- Rust — Has RCON (websocket-based), server.cfg, RustDedicated.exe, oxide/mods
- Valheim — Has no RCON, start_server.sh config, valheim_server.exe, mods via BepInEx
Steps for a new adapter:
- Create
backend/adapters/<game_type>/directory (built-in) or separate Python package (third-party) - Implement required protocols:
ConfigGenerator(schema + generation),ProcessConfig,LogParser - Implement optional protocols as needed:
RemoteAdmin,MissionManager,ModManager,BanManager - Create adapter class implementing
GameAdapter - Register adapter:
- Built-in: add to
backend/adapters/<game_type>/__init__.pyand auto-import inadapters/__init__.py - Third-party: add
languard.adaptersentry_point inpyproject.toml:Core discovers these via[project.entry-points."languard.adapters"] mygame = "my_package.adapters:MYGAME_ADAPTER"importlib.metadataat startup.
- Built-in: add to
- No core code changes needed
- No DB migrations needed
- Test: create a server with the new game_type, start it, monitor it
Testing Strategy
Unit tests (pytest)
GameAdapterRegistry— register, get, list, missing adapterArma3ConfigGenerator— Pydantic model validation for each section (merged schema + generation)Arma3ConfigGenerator.write_server_cfg()— compare output against expected string; test config injection preventionArma3ConfigGenerator._escape_config_string()— test double-quote and newline escapingRPTParser.parse_line()— test all log formatsBERConClient.parse_players_response()— test with sample outputAuthService.login()— correct/wrong password / rate limiting- Repository methods — use in-memory SQLite (
:memory:) check_server_ports_available()— test derived port validation (via adapter conventions)sanitize_filename()— test path traversal prevention- Protocol conformance — verify Arma3Adapter satisfies all GameAdapter protocol methods
Integration tests
- Full start/stop cycle with a real arma3server.exe (manual — requires licensed Arma 3)
- WebSocket message delivery (can be automated with httpx test client)
- RCon command round-trip (manual — requires running server with BattlEye)
- Adapter resolution: create server with game_type, verify correct adapter is used throughout
Adapter contract tests
- Template test suite that any new adapter should pass
- Tests: ConfigGenerator produces valid sections and valid config files, ProcessConfig returns allowed executables, LogParser parses sample lines
Load notes
- SQLite with WAL handles concurrent reads from 4 threads per server well
- For >10 simultaneous servers, consider connection pool size tuning
- WebSocket broadcast scales to ~100 concurrent connections without issue
Environment Setup (Developer)
# 1. Clone repo
git clone <repo>
cd languard-servers-manager
# 2. Backend
cd backend
python -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install -r requirements.txt
# 3. Environment
cp .env.example .env
# Edit .env: set game-specific paths (LANGUARD_ARMA3_DEFAULT_EXE, etc.)
# 4. Run backend
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 5. Frontend (separate)
cd ../frontend
npm install
npm run dev
Backend auto-creates languard.db, seeds an admin user on first run, and registers the Arma 3 adapter automatically.
Phase Summary
| Phase | Deliverable | Key Change from Single-Game |
|---|---|---|
| 0 | Adapter framework (protocols + exceptions + registry) | NEW — foundation for modularity |
| 1 | Foundation (auth + server CRUD + game discovery + migration) | Core tables, game_type field, game_configs JSON, migration from old schema |
| 2 | Arma 3 adapter: config gen + process mgmt | Config generation in adapter, atomic writes, typed exceptions, optimistic locking |
| 3 | Background threads (core + adapter injection) | Generic threads + adapter parsers/clients, per-server lock for RemoteAdmin |
| 4 | Remote admin (Arma 3: BattlEye RCon) | RCon in adapter, generic poller in core |
| 5 | WebSocket real-time | No change — fully game-agnostic |
| 6 | Mission + mod management (Arma 3 adapter) | In adapter, generic endpoints in core |
| 7 | Polish, security, recovery | Adapter-declared sensitive fields, Fernet encryption |
| 8 | Second game adapter | NEW — validates zero core changes, entry_points for third-party |
Implement phases in order — each phase builds on the previous and is independently testable. Phase 0 must come first as it defines the contract that all subsequent code depends on.