From 4ba199dd62f624419a49709ff2e44958c3d55646 Mon Sep 17 00:00:00 2001 From: "Khoa (Revenovich) Tran Gia" Date: Fri, 17 Apr 2026 14:55:59 +0700 Subject: [PATCH] docs: add arma-server-web-admin analysis reference docs Brings in ANALYSIS.md, HOW_IT_WORKS.md, and CHERRY_PICK.md generated from deep analysis of the arma-server-web-admin benchmark project. These docs inform the Arma 3 UX enhancement plan (.claude/plan/arma3-ux-enhancement.md) and provide context for implementing agents without needing to re-read the source project. --- docs/ANALYSIS.md | 155 +++++++++++++++++++++ docs/CHERRY_PICK.md | 247 ++++++++++++++++++++++++++++++++++ docs/HOW_IT_WORKS.md | 314 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 716 insertions(+) create mode 100644 docs/ANALYSIS.md create mode 100644 docs/CHERRY_PICK.md create mode 100644 docs/HOW_IT_WORKS.md diff --git a/docs/ANALYSIS.md b/docs/ANALYSIS.md new file mode 100644 index 0000000..ebf7556 --- /dev/null +++ b/docs/ANALYSIS.md @@ -0,0 +1,155 @@ +# Arma Server Web Admin — Full Analysis + +## Project Overview + +**Arma Server Web Admin** is a Node.js/Express web application providing a browser-based administration panel for managing one or more Arma game server instances (Arma 1/2/2OA/3, CWA, OFP). + +It consolidates all day-to-day server management tasks into a single UI: +- Launch and stop game server processes +- Monitor live player counts, current mission, and server state +- Upload and rotate missions (.pbo files, including Steam Workshop) +- Discover and assign mods per server +- Browse and download server log files +- Configure every server setting (ports, passwords, difficulty, mods, MOTD, etc.) +- Deploy headless clients automatically alongside the server process + +--- + +## Technology Stack + +| Layer | Technology | +|-------|-----------| +| HTTP server | Node.js + Express.js | +| Real-time | Socket.IO 2.x | +| Game process | arma-server (Node wrapper) | +| Game query | Gamedig | +| Steam Workshop | steam-workshop | +| File helpers | fs.extra, glob, multer | +| Async control | async 2.x | +| Frontend SPA | Backbone.js + Marionette.js | +| Templating | Underscore.js | +| UI framework | Bootstrap 3 | +| Build | Webpack v1 | +| Auth | express-basic-auth (optional) | + +--- + +## Directory Structure + +``` +arma-server-web-admin/ +├── app.js # Entry point: Express + Socket.IO + route wiring +├── config.js.example # All configuration options with defaults +├── config.docker.js # Docker-specific config overrides +├── webpack.config.js # Frontend bundling +├── package.json +│ +├── lib/ # Backend business logic +│ ├── manager.js # Multi-server lifecycle manager (EventEmitter) +│ ├── server.js # Single server wrapper (start/stop/query/persist) +│ ├── missions.js # Mission file discovery, upload, Workshop download +│ ├── logs.js # Log file discovery, cleanup, platform paths +│ ├── settings.js # Config accessor (public subset for client) +│ ├── setup-basic-auth.js # Optional HTTP Basic Auth middleware +│ └── mods/ +│ ├── index.js # Mod discovery + parallel metadata resolution +│ ├── folderSize.js # Recursive folder size calculator +│ ├── modFile.js # Parses mod.cpp for display name +│ └── steamMeta.js # Parses meta.cpp for Steam Workshop ID +│ +├── routes/ # Express routers (each returns a router factory) +│ ├── servers.js # /api/servers/* (CRUD + start/stop) +│ ├── missions.js # /api/missions/* (list/upload/download/delete) +│ ├── mods.js # /api/mods/* (list/delete) +│ ├── logs.js # /api/logs/* (list/view/download/delete) +│ └── settings.js # /api/settings (GET public settings) +│ +└── public/ # Single-page frontend (built by Webpack) + ├── index.html + ├── css/styles.css + └── js/ + ├── app.js # RequireJS bootstrap + Socket.IO init + └── app/ + ├── router.js # Backbone Router (5 routes) + ├── models/ # Backbone Models + ├── collections/ # Backbone Collections + └── views/ # Marionette views +``` + +--- + +## Feature Inventory + +### Server Lifecycle +- Create / edit / delete server definitions persisted to `servers.json` +- Start / stop server processes (Windows `.exe`, Linux binary, Wine) +- Auto-start servers on application launch (`auto_start` flag) +- Process ID tracking +- 5-second polling via Gamedig for live state (players, mission, status) + +### Configuration +- Title, port, max players +- Player password, admin password +- Message of the Day (MOTD, multi-line) +- BattleEye, VoN, signature verification, file patching, persistent mode +- Difficulty override (Recruit / Regular / Veteran / Custom) +- Per-server additional `server.cfg` text (freeform) +- Per-server startup parameters (e.g. `-limitFPS=100`) +- Per-server mod selection +- Per-server mission rotation with per-mission difficulty + +### Mission Management +- List `.pbo` files from `mpmissions/` +- Upload up to 64 `.pbo` files at once (multipart) +- Download from Steam Workshop by ID +- Download mission files to browser +- Delete mission files +- View name, world name, file size, timestamps + +### Mod Management +- Auto-discover mods via glob patterns (`@*`, `csla`, `ef`, etc.) +- Extract display name from `mod.cpp` +- Extract Steam Workshop ID from `meta.cpp` +- Calculate folder sizes recursively (symlink-aware) +- Delete mod folders +- Assign mods per server (split-pane UI) + +### Log Management +- Discover `.rpt` log files from platform-appropriate paths: + - **Windows:** `AppData\Local\[Game]\` + - **Linux:** `[game_path]/logs/` + - **Wine:** `.wine/drive_c/users/.../Application Data/[Game]/` +- View log contents inline +- Download logs +- Delete logs +- Auto-cleanup: retain only the 20 newest log files + +### Headless Clients +- Configure number of headless clients per server +- Auto-launch when server starts, auto-kill when server stops +- Connect to `127.0.0.1:[server_port]` + +### Real-Time Updates +- Socket.IO push for: server state, mission list, mod list, settings +- All connected clients receive updates simultaneously + +### Authentication +- Optional HTTP Basic Auth (single or multiple users) +- Credentials in `config.js`; no database + +### Platform Support +- Windows, Linux, Wine, Docker + +--- + +## Data Persistence + +| Data | Storage | +|------|---------| +| Server definitions | `servers.json` (plain JSON, in-memory on load) | +| Mission files | `[game_path]/mpmissions/` (filesystem) | +| Mod files | `[game_path]/[mod_dirs]/` (filesystem) | +| Log files | Platform-specific log directory | +| App config | `config.js` (static, not written at runtime) | + +No database. All runtime state lives in the `Manager` class and is flushed to `servers.json` on every mutation. diff --git a/docs/CHERRY_PICK.md b/docs/CHERRY_PICK.md new file mode 100644 index 0000000..40f2078 --- /dev/null +++ b/docs/CHERRY_PICK.md @@ -0,0 +1,247 @@ +# Cherry-Pick Candidates for languard-servers-manager + +This document lists modules and functions from `arma-server-web-admin` that are worth adapting into `languard-servers-manager`. Each entry explains what the code does, why it is valuable, and a concrete adapter strategy. + +Target project: `E:\TestScript\languard-servers-manager` (FastAPI + React stack). + +--- + +## 1. Manager Pattern — Multi-Server Lifecycle Registry + +**Source:** `lib/manager.js` +**Key functions:** `load()`, `save()`, `addServer()`, `removeServer()`, `getServer()`, `getServers()` + +**What it does:** +Maintains an in-memory registry of Server instances (both as an ordered array and a hash for O(1) lookup), flushes state to `servers.json` on every mutation, and emits change events so the Socket.IO layer can broadcast diffs. + +**Why it is valuable:** +languard already has a similar concept (servers stored in SQLite), but the EventEmitter pattern that automatically triggers broadcasts on every mutation is clean and decoupled. The dual array+hash storage pattern is also worth copying for cache performance. + +**Adapter strategy:** +``` +Create: backend/core/servers/manager.py + - ServerManager class with in-memory cache (dict + list) + - on_change callback / asyncio.Event instead of EventEmitter + - load() reads from SQLite via existing ServerRepository + - save() writes back through repository + - Emit WebSocket broadcasts via FastAPI WebSocket manager on every mutation +``` + +--- + +## 2. Process Lifecycle — Start / Stop / Kill with Graceful Fallback + +**Source:** `lib/server.js` — `start()`, `stop()` methods + +**What it does:** +Spawns a child process for the game server, captures its PID, starts a status-poll interval, pipes stdout/stderr to a log file, and on stop sends SIGTERM with a 5-second SIGKILL fallback. Headless clients are launched/killed alongside the main process. + +**Why it is valuable:** +languard's backend already launches processes, but the graceful stop with timed SIGKILL fallback and the headless client co-lifecycle are patterns not yet present. + +**Adapter strategy:** +``` +Adapt: backend/core/servers/process_manager.py + - async def start_server(config) -> asyncio.subprocess.Process + - async def stop_server(proc, timeout=5) -> SIGTERM then SIGKILL + - Capture PID, store in DB server record + - Pipe stdout/stderr to dated log file (matches existing log path logic) +``` + +--- + +## 3. Status Polling — Gamedig-style Periodic Query + +**Source:** `lib/server.js` — `queryStatus()` + `setInterval` pattern + +**What it does:** +Every 5 seconds, queries the running game server via the game's UDP query protocol (Gamedig). Stores the response (`players`, `mission`, `state`) in the server's in-memory state and emits it to connected clients. + +**Why it is valuable:** +languard's frontend relies on WebSocket events pushed from process stdout. A periodic external query would give accurate player counts and current mission data independent of log output. + +**Adapter strategy:** +``` +Create: backend/core/servers/status_poller.py + - asyncio.Task per running server (cancel on stop) + - Use python-a2s or opengsq-python to query UDP game port + - QueryAdapter interface: arma3, dayz, etc. as subclasses + - On result: update server record in DB, push via WebSocket manager + - Poll interval configurable (default 5 s) +``` + +--- + +## 4. Mod Discovery — Parallel Metadata Resolution + +**Source:** `lib/mods/index.js`, `lib/mods/modFile.js`, `lib/mods/steamMeta.js`, `lib/mods/folderSize.js` + +**What it does:** +Finds all mod directories via glob, then for each mod concurrently: parses `mod.cpp` for display name, parses `meta.cpp` for Steam Workshop ID, and recursively calculates folder size with symlink-aware deduplication. + +**Why it is valuable:** +languard's mod tab lists paths but does not extract Steam IDs, human-readable names, or folder sizes. This pattern handles all three in parallel efficiently. + +**Adapter strategy:** +``` +Create: backend/core/mods/scanner.py + - async def scan_mods(game_path: str) -> list[ModMeta] + - Use pathlib.glob for discovery + - asyncio.gather for parallel metadata: + parse_mod_file(path) -> ModFileAdapter (reads mod.cpp) + parse_steam_meta(path) -> SteamMetaAdapter (reads meta.cpp) + folder_size(path) -> recursive os.scandir sum + - ModMeta dataclass: { path, name, steam_id, size_bytes } +``` + +--- + +## 5. Log File Management — Platform Paths + Auto-Cleanup + +**Source:** `lib/logs.js` — `logsPath()`, `logFiles()`, `cleanupOldLogFiles()` + +**What it does:** +Resolves the game log directory based on platform (windows / linux / wine). Lists `.rpt` files with metadata, auto-deletes the oldest files beyond a configurable retention limit (default 20). + +**Why it is valuable:** +languard streams real-time logs via WebSocket but has no endpoint to browse historical `.rpt` files on disk. Discovery and cleanup logic is directly reusable. + +**Adapter strategy:** +``` +Adapt: backend/core/logs/log_manager.py + - def logs_path(platform: str, game_path: str) -> Path + - def list_logs(logs_dir: Path) -> list[LogMeta] (stat each .rpt) + - def cleanup_old_logs(logs_dir: Path, keep: int = 20) + - Expose via: + GET /api/servers/{id}/logfiles + GET /api/servers/{id}/logfiles/{name}/download + DELETE /api/servers/{id}/logfiles/{name} +``` + +--- + +## 6. Mission Multi-File Upload + Filename Parsing + +**Source:** `lib/missions.js` — `updateMissions()` + `routes/missions.js` upload handler + +**What it does:** +Accepts multipart upload of up to 64 `.pbo` files simultaneously (parallel move with limit 8), scans the `mpmissions/` directory, and extracts `{ name, world }` from the Arma filename convention `missionname.worldname.pbo`. + +**Why it is valuable:** +languard's missions tab lists files but does not support drag-and-drop multi-file upload or Steam Workshop download. The `.pbo` filename parsing convention is Arma-specific and worth encoding explicitly. + +**Adapter strategy:** +``` +Adapt: backend/routers/missions.py + - POST /api/servers/{id}/missions -> UploadFile[] via FastAPI + - Validate .pbo extension server-side (reject others) + - asyncio.gather with semaphore (limit 8): move to mpmissions/ + - Parse filename: "name.world.pbo" -> { name, world } + - Trigger rescan -> return updated list + - Optional: POST /api/missions/workshop { id } -> delegate to steamcmd +``` + +--- + +## 7. EventEmitter → WebSocket Broadcast Bridge + +**Source:** `app.js` lines 49–66 (event bridge block) + +**What it does:** +Listens on EventEmitter events from Manager, Missions, and Mods, then calls `io.emit()` to broadcast to all connected Socket.IO clients. New connections receive a full state snapshot immediately on connect. + +**Why it is valuable:** +languard's WebSocket layer pushes real-time log lines but does not broadcast server state changes to all connected tabs/clients simultaneously. The "snapshot on connect + push diffs on change" pattern is directly applicable. + +**Adapter strategy:** +``` +Adapt: backend/core/websocket/broadcast_manager.py + - WebSocketManager class (already partially exists in languard) + - Add: publish(event: str, payload) -> broadcast_all() + - On new connection: send snapshot of all servers, missions, mods + - On server state change: publish("servers", get_all_servers()) + - Same for missions and mods events +``` + +--- + +## 8. Mission Rotation Table — Per-Mission Difficulty + +**Source:** `public/js/app/views/servers/missions/rotation/` (list + item views) + +**What it does:** +Renders a table of missions in the server's active rotation. Each row has a difficulty dropdown. Rows can be added from the discovered mission list or removed individually. The full rotation array saves with the server config. + +**Why it is valuable:** +languard's missions tab shows available missions but has no drag-to-add rotation table with per-mission difficulty. This is a key Arma workflow. + +**Adapter strategy:** +``` +Adapt: frontend/src/pages/ServerDetailPage.tsx (Missions tab) + - MissionRotationTable component + - Row type: { name: string, difficulty: '' | 'Recruit' | 'Regular' | 'Veteran' } + - Add row: select from available missions dropdown + - Remove row: delete button per row + - Save: PUT /api/servers/{id} { missions: [...] } + - Backend: MissionRotationItem Pydantic schema as array field on Server +``` + +--- + +## 9. Mod Assignment UI — Split-Pane Available / Selected + +**Source:** `public/js/app/views/servers/mods/` (available list + selected list views) + +**What it does:** +Two side-by-side lists: all discovered mods on the left, server-assigned mods on the right. Clicking a mod moves it between lists. Each list has a search filter. Selection saves when the settings form submits. + +**Why it is valuable:** +languard's mods tab uses a flat checkbox list. The split-pane pattern with instant visual feedback is significantly more usable when mod counts are large (50+). + +**Adapter strategy:** +``` +Adapt: frontend/src/pages/ServerDetailPage.tsx (Mods tab) + - and side by side + - Shared state: selectedMods: string[] (mod paths) + - On click: immutable transfer between lists + - Search input on each list (client-side filter) + - On save: PUT /api/servers/{id} { mods: selectedMods } +``` + +--- + +## 10. Startup Parameter Editor — Dynamic String List + +**Source:** `public/js/app/views/servers/parameters/` (list + item views) + +**What it does:** +Renders a dynamic list of startup parameter inputs (e.g., `-limitFPS=100`). Rows can be added or removed. The list persists with the server config. + +**Why it is valuable:** +languard's Create Server wizard captures some fixed parameters but has no UI for arbitrary additional startup flags, which power users need. + +**Adapter strategy:** +``` +Create: frontend/src/components/ParameterEditor.tsx + - Props: value: string[], onChange: (v: string[]) => void + - Renders list of text inputs with remove buttons + - Add button appends empty string + - Used in: CreateServerPage wizard (step 3) and ServerDetailPage settings tab + - Save: include in PUT /api/servers/{id} { parameters: [...] } +``` + +--- + +## Priority Ranking + +| Priority | Candidate | Effort | Value | +|----------|-----------|--------|-------| +| High | Status Polling (asyncio task per server) | Medium | Accurate live player counts | +| High | Mission Rotation Table UI | Medium | Key missing workflow | +| High | Mission Multi-File Upload | Low | Missing feature | +| High | Mod Discovery with Parallel Metadata | Medium | Rich mod metadata | +| Medium | Startup Parameter Editor UI | Low | Power-user feature | +| Medium | Mod Split-Pane Selection UI | Medium | UX improvement for large mod lists | +| Medium | Log File Discovery + Cleanup | Low | Historical log access | +| Low | EventEmitter Broadcast Bridge | Low | Already partially implemented | +| Low | Config-Driven Optional Auth | Low | Dev convenience only | diff --git a/docs/HOW_IT_WORKS.md b/docs/HOW_IT_WORKS.md new file mode 100644 index 0000000..d213e29 --- /dev/null +++ b/docs/HOW_IT_WORKS.md @@ -0,0 +1,314 @@ +# How Arma Server Web Admin Works + +## Application Boot Sequence + +``` +node app.js + ├── Load config.js + ├── Create Express app + HTTP server + ├── Attach Socket.IO to HTTP server + ├── Instantiate: Settings, Missions, Mods, Manager + ├── Manager.load() → read servers.json, restore Server instances + ├── Register event bridges (manager/missions/mods → io.emit) + ├── Mount routes (/api/*) + ├── Serve public/ (SPA) + ├── Optional: setup-basic-auth middleware + └── http.listen(config.port) +``` + +--- + +## Core Flow: Server Lifecycle + +### Start a Server + +``` +POST /api/servers/:id/start + → routes/servers.js → manager.startServer(id) + → lib/server.js Server.start() + ├── Instantiate ArmaServer.Server with merged config + ├── Write server.cfg to filesystem (via arma-server lib) + ├── Spawn child process: + │ Windows → arma3server.exe [params] + │ Linux → ./arma3server [params] + │ Wine → wine arma3server.exe [params] + ├── On Linux: pipe stdout/stderr to dated .rpt log file + ├── Start queryStatusInterval every 5 s (Gamedig) + ├── If number_of_headless_clients > 0 → startHeadlessClients() + └── Emit 'state' event → manager bubbles → io.emit('servers', ...) +``` + +### Stop a Server + +``` +POST /api/servers/:id/stop + → Server.stop() + ├── instance.kill() (SIGTERM) + ├── setTimeout(5000) → instance.kill() if still alive (SIGKILL) + ├── stopHeadlessClients() + ├── clearInterval(queryStatusInterval) + └── On 'close' → emit 'state' +``` + +### Status Polling (every 5 seconds) + +``` +setInterval → Server.queryStatus() + → Gamedig.query({ type: 'arma3', host: '127.0.0.1', port }) + → On success: store { players, mission, status } in server.state + → On failure: set state = 'stopped' if instance.exitCode set + → Emit 'state' → manager → io.emit('servers', getServers()) +``` + +### Persistence + +``` +Any mutation (add/edit/delete/start/stop) + → Manager.save() + → JSON.stringify(serversArr.map(s => s.toJSON())) + → Write to servers.json + → Emit 'servers' event → io.emit('servers', ...) +``` + +--- + +## Core Flow: Mission Management + +### List Missions + +``` +GET /api/missions/ + → missions.missions (pre-loaded array) + → [ { name, world, filename, size, created, modified }, ... ] +``` + +### Upload Missions + +``` +POST /api/missions (multipart/form-data, field: "missions") + → multer stores files to temp dir + → Filter: only .pbo extension allowed + → async.parallelLimit(8): fs.move(temp → mpmissions/filename) + → missions.updateMissions() + → fs.readdir(mpmissions/) + → stat each file → build metadata object + → update this.missions array + → io.emit('missions', missions.missions) +``` + +### Steam Workshop Download + +``` +POST /api/missions/workshop { id: "workshop_id" } + → steamWorkshop.downloadFile(id, mpmissionsDir) + → missions.updateMissions() + → io.emit('missions', ...) +``` + +--- + +## Core Flow: Mod Management + +### Discovery Pipeline + +``` +Mods.updateMods() + → glob('**/{@*,csla,ef,...}/addons', gamePath) + → For each modDir, async.map → resolveModData(modDir) + ├── async.parallel: + │ folderSize(modDir) → recursive sum of file sizes (symlink-aware) + │ modFile(modDir) → parse mod.cpp → { name } + │ steamMeta(modDir) → parse meta.cpp → { id, name } + └── Merge results into: + { name: relative_path, size, formattedSize, modFile, steamMeta } + → this.mods = result array + → Emit 'mods' +``` + +### Assign Mods to Server + +``` +Client UI: drag/click mod from "Available" → "Selected" + → Backbone model update: server.mods = [ 'path/to/@mod', ... ] + → PUT /api/servers/:id { mods: [...] } + → manager.saveServer(id, body) + → Manager.save() → servers.json +``` + +--- + +## Core Flow: Log Management + +### Locate Log Files + +``` +Logs.logsPath() + → if config.type === 'windows' → AppData/Local/[GameName]/ + → if config.type === 'linux' → config.path/logs/ + → if config.type === 'wine' → .wine/drive_c/users/.../AppData/[GameName]/ + +Logs.logFiles() + → fs.readdir(logsPath) + → filter: /\.rpt$/ + → stat each → { name, size, created, modified } + → sort by modified desc +``` + +### Auto-Cleanup + +``` +After any delete or Linux log write: + Logs.cleanupOldLogFiles() + → logFiles() → sort by modified + → if count > 20: delete oldest (count - 20) files +``` + +### Linux Real-Time Logging + +``` +Server.start() (Linux) + → logStream = fs.createWriteStream(logPath, { flags: 'a' }) + → process.stdout.pipe(logStream) + → process.stderr.pipe(logStream) +``` + +--- + +## Real-Time Architecture (Socket.IO) + +``` +Backend (EventEmitter chain) + Manager/Missions/Mods emit events via Node EventEmitter + app.js bridges each to Socket.IO: + manager.on('servers', () => io.emit('servers', manager.getServers())) + missions.on('missions', (m) => io.emit('missions', m)) + mods.on('mods', (m) => io.emit('mods', m)) + +On new client connection: + socket.emit('missions', missions.missions) // push initial snapshot + socket.emit('mods', mods.mods) + socket.emit('servers', manager.getServers()) + socket.emit('settings', settings.getPublicSettings()) + +Frontend (Backbone + Socket.IO) + socket.on('servers', (servers) → serversCollection.set(servers)) + socket.on('missions', (m) → missionsCollection.set(m)) + socket.on('mods', (m) → modsCollection.set(m)) + → Backbone triggers 'change'/'add'/'remove' → Marionette re-renders views +``` + +--- + +## Frontend SPA Architecture + +### Routing + +Five Backbone routes drive the entire SPA: + +| Route | Handler | View | +|-------|---------|------| +| `` (home) | `home()` | `ServersListView` — server grid | +| `logs` | `logs()` | `LogsListView` — log file browser | +| `missions` | `missions()` | `MissionsView` — upload + list | +| `mods` | `mods()` | `ModsView` — mod browser | +| `servers/:id` | `server(id)` | `ServerView` — tabbed detail page | + +### View Hierarchy + +``` +LayoutView (root, persists across route changes) +├── Region: navigation → NavigationView (server list sidebar) +└── Region: content → (swapped per route) + ├── ServersListView + │ └── ServerItemView (per server card) + ├── LogsListView + │ └── LogItemView (per log file) + ├── MissionsView + │ ├── UploadView (file input + drag-and-drop) + │ ├── WorkshopView (Steam ID input) + │ └── MissionsListView → MissionItemView + ├── ModsView + │ ├── AvailableModsListView → ModItemView + │ └── SelectedModsListView → SelectedModItemView + └── ServerView (tabbed LayoutView) + ├── Tab: Info → InfoView (status, start/stop, PID, players) + ├── Tab: Mods → ServerModsView (split pane) + ├── Tab: Missions → MissionRotationView (add/remove rotation rows) + ├── Tab: Parameters → ParametersView (startup param editor) + ├── Tab: Players → PlayersView (live player table) + └── Tab: Settings → FormView (full server config form) +``` + +### Settings Form Save Flow + +``` +FormView.save() + → Collect: jQuery serializeArray() + checkbox state + → Validate: title required + → AJAX PUT /api/servers/:id { ...formData } + → On 200: server model updated, navigate to /servers/:newId + → On error: SweetAlert error dialog +``` + +--- + +## Configuration System + +### Global Config (`config.js`) + +```javascript +{ + game: 'arma3', // Game variant + path: '/opt/arma3', // Game install path + port: 3000, // Web UI port + host: '0.0.0.0', + type: 'linux', // 'windows' | 'linux' | 'wine' + parameters: [], // Global startup params (all servers) + serverMods: [], // Server-side mods (all servers) + admins: [], // Steam IDs auto-granted admin + auth: { username, password }, // Optional Basic Auth + prefix: '', // Prepended to all server hostnames + suffix: '', + additionalConfigurationOptions: '' // Appended to all server.cfg +} +``` + +### Per-Server Config (stored in `servers.json`) + +```javascript +{ + id: 'my-server', // URL-safe slug of title + title: 'My Server', + port: 2302, + max_players: 32, + password: '', + admin_password: '', + motd: '', + auto_start: false, + battle_eye: true, + persistent: false, + von: true, + verify_signatures: false, + file_patching: false, + allowed_file_patching: 0, + forcedDifficulty: '', + number_of_headless_clients: 0, + parameters: [], + additionalConfigurationOptions: '', + missions: [{ name: 'mission.world', difficulty: '' }], + mods: ['@CBA_A3', '@ACE'] +} +``` + +--- + +## Authentication Flow + +``` +If config.auth defined: + setupBasicAuth(app, config.auth) + → app.use(expressBasicAuth({ users: { username: password } })) + → All routes require valid Authorization: Basic ... header + → req.auth.user available in all route handlers + → Morgan logs include authenticated username +```