# Languard Servers Manager — Database Design ## Engine - **SQLite** via `SQLAlchemy Core` (sync for all access — routes and threads) - File: `languard.db` at project root (configurable via `LANGUARD_DB_PATH`) - WAL mode enabled: `PRAGMA journal_mode=WAL` — allows concurrent reads during writes - Foreign keys enabled: `PRAGMA foreign_keys=ON` - Busy timeout: `PRAGMA busy_timeout=5000` — prevents "database is locked" errors under concurrent thread writes --- ## Schema ### Table: `users` Stores web UI admin accounts. ```sql CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, -- bcrypt hash role TEXT NOT NULL DEFAULT 'viewer', -- 'admin' | 'viewer' CHECK (role IN ('admin', 'viewer')), created_at TEXT NOT NULL DEFAULT (datetime('now')), last_login TEXT ); ``` --- ### Table: `servers` One row per managed Arma 3 server instance. ```sql CREATE TABLE servers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- display name in UI description TEXT, status TEXT NOT NULL DEFAULT 'stopped', -- status values: 'stopped' | 'starting' | 'running' | 'stopping' | 'crashed' | 'error' CHECK (status IN ('stopped', 'starting', 'running', 'stopping', 'crashed', 'error')), CHECK (game_port BETWEEN 1024 AND 65535), CHECK (rcon_port BETWEEN 1024 AND 65535), -- Process info pid INTEGER, -- OS process ID when running exe_path TEXT NOT NULL, -- path to arma3server_x64.exe started_at TEXT, -- ISO datetime stopped_at TEXT, -- Network game_port INTEGER NOT NULL DEFAULT 2302, rcon_port INTEGER NOT NULL DEFAULT 2306, -- user-configurable; written to battleye/beserver.cfg steam_query_port INTEGER GENERATED ALWAYS AS (game_port + 1) VIRTUAL, -- convention, not enforced by engine -- Auto-management auto_restart INTEGER NOT NULL DEFAULT 0, -- 1 = restart on crash max_restarts INTEGER NOT NULL DEFAULT 3, -- within restart_window_seconds restart_window_seconds INTEGER NOT NULL DEFAULT 300, restart_count INTEGER NOT NULL DEFAULT 0, last_restart_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_servers_status ON servers(status); CREATE INDEX idx_servers_game_port ON servers(game_port); CREATE INDEX idx_servers_rcon_port ON servers(rcon_port); ``` --- ### Table: `server_configs` Stores all parameters for generating `server.cfg`. One row per server. ```sql CREATE TABLE server_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL UNIQUE REFERENCES servers(id) ON DELETE CASCADE, -- Basic identity hostname TEXT NOT NULL DEFAULT 'My Arma 3 Server', password TEXT, -- join password (encrypted at app layer via Fernet) password_admin TEXT NOT NULL, -- encrypted (no default — must be set on creation) server_command_password TEXT, -- encrypted -- Players max_players INTEGER NOT NULL DEFAULT 40, kick_duplicate INTEGER NOT NULL DEFAULT 1, persistent INTEGER NOT NULL DEFAULT 1, -- Voting vote_threshold REAL NOT NULL DEFAULT 0.33, vote_mission_players INTEGER NOT NULL DEFAULT 1, vote_timeout INTEGER NOT NULL DEFAULT 60, -- seconds role_timeout INTEGER NOT NULL DEFAULT 90, briefing_timeout INTEGER NOT NULL DEFAULT 60, debriefing_timeout INTEGER NOT NULL DEFAULT 45, lobby_idle_timeout INTEGER NOT NULL DEFAULT 300, -- Voice disable_von INTEGER NOT NULL DEFAULT 0, von_codec INTEGER NOT NULL DEFAULT 1, -- 1 = OPUS CHECK (von_codec IN (0, 1)), von_codec_quality INTEGER NOT NULL DEFAULT 20, -- 1-30 -- Network quality kick thresholds max_ping INTEGER NOT NULL DEFAULT 250, max_packet_loss INTEGER NOT NULL DEFAULT 50, max_desync INTEGER NOT NULL DEFAULT 200, disconnect_timeout INTEGER NOT NULL DEFAULT 15, kick_on_ping INTEGER NOT NULL DEFAULT 1, kick_on_packet_loss INTEGER NOT NULL DEFAULT 1, kick_on_desync INTEGER NOT NULL DEFAULT 1, kick_on_timeout INTEGER NOT NULL DEFAULT 1, -- Security battleye INTEGER NOT NULL DEFAULT 1, verify_signatures INTEGER NOT NULL DEFAULT 2, -- 0 | 1 | 2 (1 = check but don't kick) allowed_file_patching INTEGER NOT NULL DEFAULT 0, -- 0 | 1 | 2 -- Difficulty forced_difficulty TEXT NOT NULL DEFAULT 'Regular', -- Recruit | Regular | Veteran | Custom -- Misc timestamp_format TEXT NOT NULL DEFAULT 'short', -- none | short | full auto_select_mission INTEGER NOT NULL DEFAULT 0, random_mission_order INTEGER NOT NULL DEFAULT 0, missions_to_restart INTEGER NOT NULL DEFAULT 0, missions_to_shutdown INTEGER NOT NULL DEFAULT 0, log_file TEXT NOT NULL DEFAULT 'server_console.log', skip_lobby INTEGER NOT NULL DEFAULT 0, drawing_in_map INTEGER NOT NULL DEFAULT 1, upnp INTEGER NOT NULL DEFAULT 0, loopback INTEGER NOT NULL DEFAULT 0, statistics_enabled INTEGER NOT NULL DEFAULT 1, force_rotor_lib INTEGER NOT NULL DEFAULT 0, -- 0=player, 1=AFM, 2=SFM CHECK (force_rotor_lib IN (0, 1, 2)), required_build INTEGER NOT NULL DEFAULT 0, steam_protocol_max_data_size INTEGER NOT NULL DEFAULT 1024, -- MOTD motd_lines TEXT NOT NULL DEFAULT '[]', -- JSON array of strings motd_interval REAL NOT NULL DEFAULT 5.0, -- Event scripts on_user_connected TEXT DEFAULT '', on_user_disconnected TEXT DEFAULT '', on_unsigned_data TEXT DEFAULT 'kick (_this select 0)', on_hacked_data TEXT DEFAULT 'kick (_this select 0)', double_id_detected TEXT DEFAULT '', -- Headless clients (JSON arrays) headless_clients TEXT NOT NULL DEFAULT '[]', -- e.g. '["127.0.0.1"]' local_clients TEXT NOT NULL DEFAULT '[]', -- Admin UIDs whitelist admin_uids TEXT NOT NULL DEFAULT '[]', -- JSON array of Steam UIDs -- File extension whitelists (JSON arrays) allowed_load_extensions TEXT NOT NULL DEFAULT '["hpp","sqs","sqf","fsm","cpp","paa","txt","xml","inc","ext","sqm","ods","fxy","lip","csv","kb","bik","bikb","html","htm","biedi"]', allowed_preprocess_extensions TEXT NOT NULL DEFAULT '["hpp","sqs","sqf","fsm","cpp","paa","txt","xml","inc","ext","sqm","ods","fxy","lip","csv","kb","bik","bikb","html","htm","biedi"]', allowed_html_extensions TEXT NOT NULL DEFAULT '["htm","html","xml","txt"]', updated_at TEXT NOT NULL DEFAULT (datetime('now')), CHECK (verify_signatures IN (0, 1, 2)), CHECK (allowed_file_patching IN (0, 1, 2)), CHECK (von_codec_quality BETWEEN 1 AND 30), CHECK (forced_difficulty IN ('Recruit', 'Regular', 'Veteran', 'Custom')), CHECK (vote_threshold >= 0.0 AND vote_threshold <= 1.0), CHECK (max_players > 0) ); ``` --- ### Table: `basic_configs` Stores `basic.cfg` (bandwidth) settings. One row per server. ```sql CREATE TABLE basic_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL UNIQUE REFERENCES servers(id) ON DELETE CASCADE, min_bandwidth INTEGER NOT NULL DEFAULT 800000, max_bandwidth INTEGER NOT NULL DEFAULT 25000000, max_msg_send INTEGER NOT NULL DEFAULT 384, -- default 128; higher = desync risk max_size_guaranteed INTEGER NOT NULL DEFAULT 512, max_size_non_guaranteed INTEGER NOT NULL DEFAULT 256, min_error_to_send REAL NOT NULL DEFAULT 0.003, max_custom_file_size INTEGER NOT NULL DEFAULT 100000, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` --- ### Table: `server_profiles` Stores `server.Arma3Profile` difficulty settings. One row per server. ```sql CREATE TABLE server_profiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL UNIQUE REFERENCES servers(id) ON DELETE CASCADE, -- Custom difficulty options (all 0/1 or 0/1/2) reduced_damage INTEGER NOT NULL DEFAULT 0, group_indicators INTEGER NOT NULL DEFAULT 0, friendly_tags INTEGER NOT NULL DEFAULT 0, enemy_tags INTEGER NOT NULL DEFAULT 0, detected_mines INTEGER NOT NULL DEFAULT 0, commands INTEGER NOT NULL DEFAULT 1, waypoints INTEGER NOT NULL DEFAULT 1, tactical_ping INTEGER NOT NULL DEFAULT 0, weapon_info INTEGER NOT NULL DEFAULT 2, stance_indicator INTEGER NOT NULL DEFAULT 2, stamina_bar INTEGER NOT NULL DEFAULT 0, weapon_crosshair INTEGER NOT NULL DEFAULT 0, vision_aid INTEGER NOT NULL DEFAULT 0, third_person_view INTEGER NOT NULL DEFAULT 0, camera_shake INTEGER NOT NULL DEFAULT 1, score_table INTEGER NOT NULL DEFAULT 1, death_messages INTEGER NOT NULL DEFAULT 1, von_id INTEGER NOT NULL DEFAULT 1, map_content_friendly INTEGER NOT NULL DEFAULT 0, map_content_enemy INTEGER NOT NULL DEFAULT 0, map_content_mines INTEGER NOT NULL DEFAULT 0, auto_report INTEGER NOT NULL DEFAULT 0, multiple_saves INTEGER NOT NULL DEFAULT 0, -- AI level ai_level_preset INTEGER NOT NULL DEFAULT 3, -- 0=Low,1=Normal,2=High,3=Custom skill_ai REAL NOT NULL DEFAULT 0.5, precision_ai REAL NOT NULL DEFAULT 0.5, CHECK (ai_level_preset BETWEEN 0 AND 3), CHECK (skill_ai BETWEEN 0.0 AND 1.0), CHECK (precision_ai BETWEEN 0.0 AND 1.0), CHECK (group_indicators BETWEEN 0 AND 2), CHECK (weapon_info BETWEEN 0 AND 2), CHECK (stance_indicator BETWEEN 0 AND 2), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` --- ### Table: `launch_params` Extra command-line parameters added to the server launch command. One row per server. ```sql CREATE TABLE launch_params ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL UNIQUE REFERENCES servers(id) ON DELETE CASCADE, world TEXT NOT NULL DEFAULT 'empty', extra_params TEXT NOT NULL DEFAULT '', -- raw extra params string limit_fps INTEGER NOT NULL DEFAULT 50, auto_init INTEGER NOT NULL DEFAULT 0, load_mission_to_memory INTEGER NOT NULL DEFAULT 0, bandwidth_alg INTEGER, -- NULL | 2 CHECK (bandwidth_alg IS NULL OR bandwidth_alg = 2), enable_ht INTEGER NOT NULL DEFAULT 0, huge_pages INTEGER NOT NULL DEFAULT 0, cpu_count INTEGER, -- NULL = auto ex_threads INTEGER NOT NULL DEFAULT 7, max_mem INTEGER, -- NULL = auto no_logs INTEGER NOT NULL DEFAULT 0, netlog INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` --- ### Table: `mods` Registered mods. Many-to-many with servers. ```sql CREATE TABLE mods ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, folder_path TEXT NOT NULL UNIQUE, -- absolute or relative path workshop_id TEXT, -- Steam Workshop ID if applicable description TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE server_mods ( server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, mod_id INTEGER NOT NULL REFERENCES mods(id) ON DELETE CASCADE, is_server_mod INTEGER NOT NULL DEFAULT 0, -- 1 = -serverMod (not broadcast to clients) sort_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (server_id, mod_id) ); CREATE INDEX idx_server_mods_server ON server_mods(server_id); ``` --- ### Table: `missions` Mission PBO files tracked per server. ```sql CREATE TABLE missions ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, filename TEXT NOT NULL, -- e.g. "MyMission.Altis.pbo" mission_name TEXT NOT NULL, -- e.g. "MyMission.Altis" terrain TEXT NOT NULL, -- e.g. "Altis" file_size INTEGER, -- bytes uploaded_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE (server_id, filename) ); CREATE INDEX idx_missions_server ON missions(server_id); ``` --- ### Table: `mission_rotation` Ordered mission cycle for a server. ```sql CREATE TABLE mission_rotation ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, mission_id INTEGER NOT NULL REFERENCES missions(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL DEFAULT 0, difficulty TEXT NOT NULL DEFAULT 'Regular', CHECK (difficulty IN ('Recruit', 'Regular', 'Veteran', 'Custom')), params_json TEXT NOT NULL DEFAULT '{}', -- mission params override as JSON UNIQUE (server_id, sort_order) ); CREATE INDEX idx_mission_rotation_server ON mission_rotation(server_id); ``` --- ### Table: `players` Currently connected players (live state, refreshed by RConPollerThread). ```sql CREATE TABLE players ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, player_num INTEGER NOT NULL, -- BE player# (slot number) name TEXT NOT NULL, guid TEXT, -- BattlEye GUID steam_uid TEXT, ip TEXT, ping INTEGER, verified INTEGER NOT NULL DEFAULT 0, -- 1 = signature verified joined_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE (server_id, player_num) ); CREATE INDEX idx_players_server ON players(server_id); ``` --- ### Table: `player_history` Historical record of connections. Inserted when player disconnects. ```sql CREATE TABLE player_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, name TEXT NOT NULL, guid TEXT, steam_uid TEXT, ip TEXT, joined_at TEXT NOT NULL, left_at TEXT NOT NULL DEFAULT (datetime('now')), session_duration_seconds INTEGER ); CREATE INDEX idx_player_history_server ON player_history(server_id); CREATE INDEX idx_player_history_steam ON player_history(steam_uid); ``` ### Player History Retention Cleanup (run daily via APScheduler, keep 90 days) ```sql DELETE FROM player_history WHERE left_at < datetime('now', '-90 days'); ``` --- ### Table: `bans` Local ban records (source of truth for the UI). **Must sync bidirectionally with `battleye/ban.txt`** — BattlEye reads only from ban.txt. On API ban add/delete: also write to ban.txt. On startup: read ban.txt and upsert into DB. **ban.txt format** (one entry per line): ``` GUID|IP timestamp reason ``` Example: `a1b2c3d4e5f6|192.168.1.1 1713260000 Cheating` **Sync caveats:** ban.txt does not store `banned_by`, `expires_at`, or `is_active`. Timed bans are represented by a future timestamp (not minutes); permanent bans have timestamp `0`. On startup, `banned_by` is set to `'ban.txt'` for entries read from file. Deactivated bans (`is_active=0`) are not written to ban.txt. ```sql CREATE TABLE bans ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, guid TEXT, steam_uid TEXT, name TEXT, reason TEXT, banned_by TEXT, -- admin username banned_at TEXT NOT NULL DEFAULT (datetime('now')), expires_at TEXT, -- NULL = permanent is_active INTEGER NOT NULL DEFAULT 1, CHECK (is_active IN (0, 1)) ); CREATE INDEX idx_bans_server ON bans(server_id); CREATE INDEX idx_bans_guid ON bans(guid); CREATE INDEX idx_bans_steam_uid ON bans(steam_uid); CREATE INDEX idx_bans_active ON bans(is_active); ``` --- ### Table: `logs` Parsed RPT log lines (rolling retention, default 7 days). ```sql CREATE TABLE logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, timestamp TEXT NOT NULL, level TEXT NOT NULL DEFAULT 'info', -- 'info' | 'warning' | 'error' CHECK (level IN ('info', 'warning', 'error')), message TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_logs_server_ts ON logs(server_id, timestamp); CREATE INDEX idx_logs_level ON logs(level); -- for ?level= filter CREATE INDEX idx_logs_created ON logs(created_at); -- for retention cleanup ``` --- ### Table: `metrics` Time-series CPU/RAM/player count snapshots. ```sql CREATE TABLE metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, timestamp TEXT NOT NULL DEFAULT (datetime('now')), cpu_percent REAL, ram_mb REAL, player_count INTEGER ); CREATE INDEX idx_metrics_server_ts ON metrics(server_id, timestamp); ``` --- ### Table: `server_events` Audit trail of all significant events (start, stop, crash, restart, admin actions). ```sql CREATE TABLE server_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE, event_type TEXT NOT NULL, -- event_type values: -- 'started' | 'stopped' | 'crashed' | 'restarted' | 'config_updated' -- 'player_kicked' | 'player_banned' | 'mission_changed' | 'admin_login' -- 'rcon_command' | 'auto_restarted' actor TEXT, -- username or 'system' detail TEXT, -- JSON with event-specific data created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_events_server ON server_events(server_id, created_at); ``` --- ### Table: `rcon_configs` BattlEye RCon credentials per server. ```sql CREATE TABLE rcon_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER NOT NULL UNIQUE REFERENCES servers(id) ON DELETE CASCADE, rcon_password TEXT NOT NULL, -- encrypted at app layer max_ping INTEGER NOT NULL DEFAULT 200, enabled INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` --- ## Relationships Diagram ``` users (1) ──────────────────────────────────── (many) server_events.actor servers (1) ──┬── (1) server_configs ├── (1) basic_configs ├── (1) server_profiles ├── (1) launch_params ├── (1) rcon_configs ├── (many) server_mods ──── (many) mods ├── (many) missions ├── (many) mission_rotation → missions ├── (many) players ├── (many) player_history ├── (many) bans ├── (many) logs ├── (many) metrics └── (many) server_events ``` --- ## Maintenance Queries ### Log Retention Cleanup (run daily via APScheduler) ```sql DELETE FROM logs WHERE created_at < datetime('now', '-7 days'); ``` ### Metrics Retention Cleanup (keep 30 days) ```sql DELETE FROM metrics WHERE timestamp < datetime('now', '-30 days'); ``` ### Clear disconnected players on server stop ```sql DELETE FROM players WHERE server_id = ?; ``` ### Vacuum (run weekly) ```sql VACUUM; ``` --- ## Migration Strategy - Migrations are plain `.sql` files in `backend/migrations/` - Naming: `001_initial_schema.sql`, `002_add_bans.sql`, etc. - Tracked in a `schema_migrations` table: ```sql CREATE TABLE schema_migrations ( version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` - Applied automatically at app startup by `database.py:run_migrations()`