Files
ArmA-3-web-server/CODEBASE.md
2026-07-06 14:14:58 +02:00

9.7 KiB

Codebase Analysis: arma3-web-server

Identity

  • Language / runtime: Go 1.25 (backend), TypeScript 6 + React 19 (frontend)
  • Framework(s): Gin v1.12 (HTTP router), gorilla/websocket (WS), TanStack Query v5 (data fetching), Tailwind CSS 4 (styling), Vite 8 (bundler)
  • Build system: Go toolchain (go build), Vite/Rollup for frontend, multi-stage Docker build
  • Test framework: None detected

Directory map

arma3-web-server/
├── backend/                         # Go backend
│   ├── cmd/server/main.go           # Entry point, env parsing, dir creation, wiring
│   ├── internal/
│   │   ├── api/                     # Gin HTTP handlers + WebSocket endpoints
│   │   │   ├── 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 + check/download-missing/update-all
│   │   │   └── logs.go              # WS streaming (server, steamcmd, rpt) + log file listing
│   │   ├── models/                  # Data structs
│   │   │   ├── settings.go          # ServerSettings (singleton)
│   │   │   └── 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
│   │       ├── server_process.go    # Process lifecycle, arg builder, mod path resolver
│   │       ├── steamcmd.go          # SteamCMD manager (UpdateGame, DownloadMod, DownloadMods)
│   │       └── log_streamer.go      # Pub/sub fan-out for stdout/stderr via channels
│   ├── go.mod / go.sum
│   └── serverfiles/                 # Default SERVERFILE_DIR (created at startup)
├── frontend/                        # React SPA
│   ├── 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, ServerPaths
│   │   ├── pages/                   # 7 route-level page components
│   │   │   ├── Dashboard.tsx        # Overview cards (status, configs, modlists)
│   │   │   ├── Settings.tsx         # Main settings tab + userconfig tabs + SteamCMD section
│   │   │   ├── 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
│   │   │   └── Logs.tsx             # Tabbed LiveTerminal (Server Console / RPT / SteamCMD) + file browser
│   │   └── components/
│   │       ├── ConfigEditor.tsx      # Monaco editor wrapper
│   │       ├── LiveTerminal.tsx      # xterm.js + auto-reconnect WebSocket
│   │       ├── Layout.tsx            # Sidebar navigation shell
│   │       └── ui/                   # Empty
│   ├── package.json
│   ├── vite.config.ts               # Proxy /api + /ws to :8080
│   └── tsconfig*.json
├── data/                            # Runtime data (mounted volume in Docker)
│   ├── presets/                     # (unused)
│   └── servers/                     # (unused)
├── Dockerfile                       # Multi-stage: Go build -> npm build -> alpine runtime
├── docker-compose.yml               # Single service with volume mounts + env vars
└── Makefile                         # Convenience targets: backend, frontend, build, run, dev, clean

Dependencies

Backend (Go)

Module Type Version Purpose
github.com/gin-gonic/gin framework v1.12.0 HTTP routing, middleware, request binding
github.com/gorilla/websocket library v1.5.3 WebSocket upgrade + message I/O
github.com/google/uuid utility v1.6.0 UUID generation for modlists
modernc.org/sqlite database v1.53.0 Declared but not used (file-based chosen)
golang.org/x/net stdlib v0.51.0 HTML parser for modlist import

Frontend (npm)

Package Type Version Purpose
react / react-dom framework ^19.2.7 UI rendering
react-router-dom routing ^7.18.1 Client-side routing (7 pages)
@tanstack/react-query data fetching ^5.101.2 Server state, caching, polling, mutations
tailwindcss styling ^4.3.2 Utility-first CSS
@monaco-editor/react editor ^4.7.0 Monaco code editor for .cfg files
@xterm/xterm / @xterm/addon-fit terminal ^6.0.0 / ^0.11.0 In-browser xterm.js terminal
zustand state ^5.0.14 Declared but unused (no stores exist)
vite bundler ^8.1.1 Dev server + production build
@vitejs/plugin-react tooling ^6.0.3 React fast-refresh / JSX transform
typescript language ~6.0.2 Type checking
oxlint linter ^1.71.0 Linting

Architecture

Pattern: Layered (API -> Service -> Model), file-based persistence

HTTP/WS                    Gin Router             Services              Filesystem
---------                  ----------             --------              ----------
Browser -- REST/WS -->  api.Handler -->  SettingsManager    -->  data/settings.json
                             |              ConfigManager     -->  $CFG_DIR/*.cfg
                             |              ModlistManager    -->  data/modlists/*.json
                             |              ProcessManager    -->  spawn arma3server_x64
                             |              SteamCmdManager   -->  spawn steamcmd
                             |              LogStreamer       -->  pub/sub (channels)
                             +--->  WebSocket clients (xterm.js)

Key abstractions

  • Handler (api package) -- stateless; receives service pointers via constructor injection. All handlers are methods on Handler.
  • *Manager (services package) -- stateful structs for each concern. Hold their own sync.Mutex for thread safety.
  • LogStreamer -- pub/sub per named stream ("server", "steamcmd"). Subscribe returns a chan string; Stream reads from an io.Reader (process stdout/stderr) and broadcasts to all subscribers of that key.
  • ProcessManager -- manages the Arma 3 server child process lifecycle. Builds the command line from settings, resolves mod paths, monitors process exit.

Notable design decisions

  1. No database -- everything is files: settings.json, *.cfg in $CFG_DIR, modlist JSON files. Backup means copying directories.
  2. Single server instance -- ProcessManager allows only one running process at a time.
  3. %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 (-config=, -mod=, -profiles=, -port=) appended. Enables Wine wrappers.
  4. Two-tier mod resolution -- resolveModPath checks $MODS_DIR/@name first (manual/symlinked), then falls back to $SERVERFILE_DIR/steamapps/workshop/content/107410/<id> for workshop downloads.
  5. SteamCMD async with live streaming -- all steamcmd operations run in background goroutines, output broadcast to "steamcmd" key.
  6. Env-defined paths -- all dirs set via env vars with hardcoded defaults, resolved to absolute paths at startup.

Data flow (example: Start server)

User clicks "Start" (Settings.tsx)
  --> handleSaveThen() -- auto-saves if dirty
  --> POST /api/server/start
  --> Handler.StartServer() (settings.go)
    --> process.Start() (server_process.go)
      --> Load settings.json
      --> If %command% in ServerParameters:
          --> Replace %command% with binPath
          --> Split into command + args
          --> Append buildAutoArgs(s): -config=, -mod=, -profiles=, -port=
          --> exec.CommandContext(ctx, parts[0], append(parts[1:], autoArgs...)...)
        Else:
          --> buildArgs(s): split parameters + append auto-args
          --> exec.CommandContext(ctx, binPath, args...)
      --> cmd.Dir = serverfileDir
      --> Pipe stdout/stderr
      --> cmd.Start()
      --> Stream stdout/stderr to LogStreamer("server")
      --> Monitor exit, broadcast [SERVER_PROCESS_EXITED]
  --> Returns {"status": "starting"}
  --> Frontend polls GET /api/server/status every 3s
  --> Shows "Running" badge

CI/CD

  • No CI pipeline -- no .github/ or .gitlab-ci.yml found
  • No tests -- zero test files anywhere in the codebase
  • Docker build: multi-stage Dockerfile at root -- builds Go binary, builds frontend dist, assembles Alpine image with steamcmd, exposes :8080, mounts /data and /servers volumes
  • Deployment: single docker-compose.yml with one service, volume mounts for persistence
  • Local dev: Makefile targets -- make run starts backend, make dev starts both backend + Vite dev server with hot reload