# Arma 3 Web Server — Architecture Plan ## Overview A web-based control panel to configure, update, install, and execute a **single** Arma 3 dedicated server instance. Supports both Windows and Linux server binaries via SteamCMD. --- ## Tech Stack | Layer | Choice | Rationale | |-------|--------|-----------| | **Backend** | Go 1.25 (`gin`, `gorilla/websocket`) | Single binary, cross-compiles, goroutines for process streaming | | **Storage** | File-based (JSON on disk) | No database dependency; settings, configs, and modlists are files | | **Scheduling** | `robfig/cron/v3` | Cron-based scheduled updates (game + mods) | | **Frontend** | React 19 + TypeScript 6 + Vite 8 | Fast iteration, rich ecosystem | | **Data Fetching** | TanStack Query v5 | Server state, caching, polling, mutations | | **UI Kit** | Tailwind CSS 4 | Dark-theme UI, utility-first styling | | **Code Editor** | Monaco (VS Code) | Syntax-highlighted config editing | | **Terminal** | xterm.js | Live log display from server stdout | --- ## Project Structure ``` arma3-web-server/ ├── backend/ │ ├── cmd/server/main.go # Entry point, env parsing, dir creation, startup auto-tasks │ ├── embed/ │ │ ├── embed.go # //go:embed dist — embeds frontend into Go binary │ │ └── dist/ # Pre-built frontend SPA assets │ ├── internal/ │ │ ├── api/ # HTTP handlers (Gin routes) │ │ │ ├── router.go # Route registration, Handler struct + New() │ │ │ ├── settings.go # Settings CRUD + server start/stop/restart + steamcmd │ │ │ ├── configs.go # .cfg file CRUD handlers │ │ │ ├── modlists.go # Modlist CRUD + import + export + check/download-missing/update-all │ │ │ ├── mods.go # Mod listing, deletion, bulk cleanup │ │ │ ├── health.go # Comprehensive health check endpoint │ │ │ └── logs.go # WS streaming (server, steamcmd, rpt) + log file listing │ │ ├── models/ # Data structs │ │ │ ├── settings.go # ServerSettings (singleton, 15 fields) │ │ │ └── modlist.go # Modlist + ModEntry + ModlistListItem │ │ └── services/ # Business logic layer │ │ ├── settings.go # JSON load/save from data/settings.json │ │ ├── config_manager.go # .cfg file I/O in $CFG_DIR │ │ ├── modlist_manager.go # Modlist CRUD against data/modlists/*.json │ │ ├── modlist_parser.go # HTML Arma Launcher preset parser + renderer │ │ ├── mod_manager.go # Workshop + local mod discovery + usage map │ │ ├── server_process.go # Process lifecycle, arg builder, mod path resolver │ │ ├── steamcmd.go # SteamCMD manager (UpdateGame, DownloadMod, DownloadMods) │ │ ├── scheduler.go # Cron-based scheduled updates │ │ └── log_streamer.go # Pub/sub fan-out for stdout/stderr via channels │ ├── go.mod │ └── go.sum ├── frontend/ │ ├── src/ │ │ ├── App.tsx # Router setup (React Router v7) │ │ ├── main.tsx # Entry point (QueryClient + BrowserRouter) │ │ ├── index.css # Tailwind v4 imports │ │ ├── api/ │ │ │ └── client.ts # Typed fetch wrappers + WS URL builders │ │ ├── types/ │ │ │ └── index.ts # ServerSettings, Modlist, ModEntry, ConfigInfo, ModInfo, ServerHealth │ │ ├── pages/ │ │ │ ├── Dashboard.tsx # Overview cards (status, configs, modlists) │ │ │ ├── Settings.tsx # Main settings tab + userconfig tabs + SteamCMD + automation │ │ │ ├── Configs.tsx # List/create/duplicate/delete configs │ │ │ ├── ConfigEditor.tsx # Full-page Monaco editor for a single config │ │ │ ├── Modlists.tsx # List/create/duplicate/delete + HTML import │ │ │ ├── ModlistEditor.tsx # Mod list reorder + enable/disable + check/download-missing/update-all │ │ │ ├── Mods.tsx # Installed mods table + search + delete + cleanup │ │ │ ├── Logs.tsx # Tabbed LiveTerminal (Server Console / RPT / SteamCMD) + file browser │ │ │ └── Status.tsx # Health check / deploy status dashboard │ │ └── components/ │ │ ├── ConfigEditor.tsx # Monaco editor wrapper │ │ ├── LiveTerminal.tsx # xterm.js + auto-reconnect WebSocket │ │ ├── Layout.tsx # Sidebar nav shell │ │ └── ui/ # (empty — reserved for future shared UI primitives) │ ├── package.json │ └── vite.config.ts # Proxy /api + /ws to :8080 ├── data/ # Runtime data (mounted volume in Docker) │ ├── settings.json # Singleton server settings │ └── modlists/ # Modlist JSON files (*.json) ├── dev-deploy/ # Local development runtime data (git-ignored) ├── .gitea/workflows/ │ ├── ci.yml # Build-only CI (Go + frontend) │ └── release.yml # GoReleaser-based release on tag push ├── docker-compose.yml ├── Dockerfile # Multi-stage: Go build -> npm build -> alpine runtime + SteamCMD ├── Dockerfile.goreleaser # Single-stage for GoReleaser (pre-built artifacts injected) ├── .goreleaser.yaml # GoReleaser v2 config (Gitea release target) └── Makefile # Convenience targets: backend, frontend, build, run, dev, clean ``` --- ## Environment Variables | Env | Purpose | Default | |-----|---------|---------| | `SERVERFILE_DIR` | Base dir where `arma3server_x64` binary + `userconfig/` live | `./serverfiles` | | `MODS_DIR` | Base dir where `@modname` folders are stored | `$SERVERFILE_DIR/mods` | | `CFG_DIR` | Base dir where `.cfg` config files are stored | `$SERVERFILE_DIR/cfg` | | `PROFILES_DIR` | Server profile/save/log path | `$SERVERFILE_DIR/profiles` | | `DATA_DIR` | Internal data (settings.json, modlists/) | `./data` | | `LISTEN` | HTTP listen address | `:8080` | > Note: `FRONTEND_DIR` was removed — the frontend is now embedded into the Go binary via `//go:embed`. --- ## Data Model ### Server Settings (singleton) — `data/settings.json` ```json { "ip_port": "0.0.0.0:2302", "server_parameters": "-server -world=empty -loadMissionToMemory -noPause", "steam_branch": "stable", "steam_user": "anonymous", "platform": "linux", "cba_settings": "", "ai_level_presets": "", "difficulty_presets": "", "active_config": "server", "active_modlist": "uuid-of-modlist", "auto_update_on_startup": false, "auto_start_on_startup": false, "auto_update_mods_on_startup": false, "was_running": false, "scheduled_update": "", "updated_at": "2026-01-01T00:00:00Z" } ``` ### Configs — `$CFG_DIR/*.cfg` Each config is a plain `.cfg` file stored in the env-defined `CFG_DIR`. CRUD operations create/rename/delete these files directly on disk. ### Modlists — `data/modlists/{uuid}.json` ```json { "id": "uuid", "name": "My Modlist", "mods": [ { "id": "450814997", "name": "CBA_A3", "enabled": true }, { "id": "463939057", "name": "ACE", "enabled": false } ], "created_at": "...", "updated_at": "..." } ``` --- ## Server Configuration The three userconfig files are **singletons** — only one version can exist on disk at a time: | File | Disk location | Managed in | |------|--------------|------------| | `cba_settings.sqf` | `$SERVERFILE_DIR/userconfig/cba_settings.sqf` | Settings page (textarea) | | `CfgAILevelPresets.sqf` | `$SERVERFILE_DIR/userconfig/CfgAILevelPresets.sqf` | Settings page (textarea) | | `CfgDifficultyPresets.sqf` | `$SERVERFILE_DIR/userconfig/CfgDifficultyPresets.sqf` | Settings page (textarea) | These are written to disk immediately when settings are saved. --- ## API Routes ``` # Server Settings (singleton) GET /api/server/settings PUT /api/server/settings # also writes userconfig files to disk POST /api/server/start POST /api/server/stop POST /api/server/restart GET /api/server/status GET /api/server/paths # returns configured directory paths GET /api/server/health # comprehensive health check (binary, paths, disk, mods, steamcmd, frontend) # SteamCMD GET /api/server/steamcmd # returns current branch/user/platform POST /api/server/steamcmd/update-game POST /api/server/steamcmd/download-mod GET /api/server/steamcmd/status # Logs GET /api/server/logs # list log files (.log, .rpt) GET /api/server/logs/:file # read log file content # Configs (.cfg files in $CFG_DIR) GET /api/configs POST /api/configs GET /api/configs/:name PUT /api/configs/:name DELETE /api/configs/:name POST /api/configs/:name/duplicate # Modlists GET /api/modlists POST /api/modlists GET /api/modlists/:id PUT /api/modlists/:id DELETE /api/modlists/:id POST /api/modlists/:id/duplicate POST /api/modlists/import (multipart form: file=*.html — parses Arma Launcher HTML preset) GET /api/modlists/:id/export (returns downloadable Arma Launcher HTML preset) GET /api/modlists/:id/check (returns mod entries with downloaded: bool) POST /api/modlists/:id/download-missing POST /api/modlists/:id/update-all # Mods (installed on disk) GET /api/mods # combined workshop + local mods with usage info DELETE /api/mods # delete mod by path (validates path prefix) POST /api/mods/cleanup # bulk-delete all mods not referenced by any modlist # WebSocket Endpoints GET /ws/server/logs # live server process stdout/stderr GET /ws/steamcmd/logs # live steamcmd output GET /ws/server/rpt # tail latest .rpt crash dump (250ms polling) ``` **Total: 27 REST endpoints + 3 WebSocket endpoints** --- ## Process Start Flow ``` User clicks Start → Settings loaded from data/settings.json → WasRunning set to true, saved to disk → If ServerParameters contains %command%: → Replace %command% with binary path (enables Wine wrappers) → Split into command + args → Append auto-args: -config=, -mod=, -profiles=, -port= → exec.CommandContext(ctx, parts[0], parts[1:]..., autoArgs...) Else: → Split server_parameters into args → Append auto-args → exec.CommandContext(ctx, binPath, args...) → cmd.Dir = serverfileDir → stdout/stderr piped to LogStreamer fan-out → cmd.Start() → WebSocket clients receive live output → On exit, process reference cleaned up, [SERVER_PROCESS_EXITED] broadcast → Frontend polls GET /api/server/status every 3-5s → shows "Running" badge ``` --- ## Startup Auto-Tasks Configured via the Automation section of the Settings UI. All run asynchronously at boot: | Setting | Behavior | |---------|----------| | **Auto-update server on startup** | Runs `steamcmd +app_update` if `steam_user` is set | | **Auto-update mods on startup** | Downloads workshop updates for every enabled mod in the active modlist | | **Auto-start server on startup** | Restarts the game server if `was_running` was true when the service last stopped | ### Scheduled Updates A cron expression in the `scheduled_update` field runs game + mod updates on a schedule (e.g. `"0 4 * * *"` for daily at 4 AM). Uses `robfig/cron/v3`. The scheduled update runs even if the game server is currently running. --- ## Key Design Decisions 1. **File-based storage** — No database. Settings, configs, and modlists are files on disk. Portable, backup-able with simple file copy. 2. **Env-defined paths** — `SERVERFILE_DIR`, `MODS_DIR`, `CFG_DIR`, `PROFILES_DIR` are environment variables, not stored in the UI. 3. **Embedded frontend** — The built React SPA is embedded into the Go binary via `//go:embed`, creating a single self-contained deployment artifact. The `NoRoute` handler serves `index.html` for client-side routing. 4. **Configs are real .cfg files** — Stored in `$CFG_DIR`, directly usable by the Arma 3 server's `-config=` parameter. 5. **Modlist = mod references + enabled state** — The `-mod=` parameter is built at start time by resolving enabled mods. Two-tier resolution: `$MODS_DIR/@name` first (manually placed or symlinked); fallback to `$SERVERFILE_DIR/steamapps/workshop/content/107410/` for workshop downloads. 6. **`%command%` substitution** — If `ServerParameters` contains `%command%`, the entire textarea is treated as a shell template with `%command%` replaced by the binary path and auto-args appended. Enables Wine wrappers. 7. **Single server instance** — Only one server at a time. No multi-instance support. 8. **No auth for v1** — JWT auth can be added later without breaking the API design. 9. **Docker-first deployment** — Single docker-compose.yml bundles SteamCMD, backend, and frontend serving. Also supports GoReleaser for automated releases to Gitea. 10. **Crash recovery** — `WasRunning` flag persists across restarts. If `auto_start_on_startup` is enabled, the server restarts automatically. 11. **SteamCMD async with live streaming** — All steamcmd operations run in background goroutines, output broadcast to `"steamcmd"` key via LogStreamer pub/sub. 12. **Health endpoint** — `/api/server/health` checks binary existence, directory writability, disk usage, mod counts, SteamCMD status, and frontend serving.