Files
ArmA-3-web-server/PLAN.md
T
MrFastwind d55200886b docs: rewrite PLAN.md and update CODEBASE.md, README.md
- Full rewrite of PLAN.md to reflect current architecture (27 REST + 3 WS endpoints,
  embedded frontend, automation, scheduler, health check, GoReleaser CI)
- Added health.go, mods.go, scheduler.go, robfig/cron dep to CODEBASE.md
- Added Gitea Actions CI/CD section to CODEBASE.md
- Added conventional commits to code style section
- Added missing API routes and embed/ to README.md
2026-07-23 20:14:56 +02:00

14 KiB

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

{
  "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

{
  "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 pathsSERVERFILE_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/<id> 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 recoveryWasRunning 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.