Files
ArmA-3-web-server/CODEBASE.md
T
MrFastwind 41c1390972 docs: update PLAN, CODEBASE, README with env vars, stubs, tests, atomic state
PLAN.md:
- Add SERVER_BINARY, SERVER_PARAMS, STEAMCMD_PATH to env vars table
- Add testdata/ to project structure
- Add testing section (backend tests, stubs, frontend tests, make targets)
- Update process start flow with env var overrides

CODEBASE.md:
- Add testdata/ stubs to directory map
- Replace zustand with vitest/testing-library/jsdom in frontend deps
- Update CI to include test steps
- Update Makefile targets description
- Add atomic state machine to ProcessManager description
- Add env overrides and atomic state to design decisions
- Update data flow with env var overrides

README.md:
- Add SERVER_BINARY, SERVER_PARAMS, STEAMCMD_PATH to env vars table
- Remove FRONTEND_DIR (embedded frontend)
- Update Development section with make targets
2026-07-23 22:29:22 +02:00

13 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: Go testing + Vitest + React Testing Library

Code Style

  • Commits: Conventional Commits — format: type(scope): description
    • Types: feat, fix, docs, style, refactor, test, chore, ci, perf
    • Scopes: backend, frontend, ci, docker, or omit for project-wide changes
    • Examples: feat(backend): add mod cleanup endpoint, fix(frontend): resolve race in modlist editor, docs: update PLAN.md to reflect current architecture

Directory map

arma3-web-server/
├── backend/                         # Go backend
│   ├── cmd/server/main.go           # Entry point, env parsing, dir creation, startup auto-tasks, wiring
│   ├── embed/
│   │   ├── embed.go                 # //go:embed dist — embeds frontend into Go binary
│   │   └── dist/                    # Pre-built frontend SPA assets
│   ├── 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 + 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)
│   │   │   └── 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 (atomic state machine)
│   │       ├── 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
│   │       └── testdata/            # Stub binaries for testing
│   │           ├── arma3server_x64  # Server stub: logs args, heartbeat, -t N exit, RPT writes
│   │           └── steamcmd         # SteamCMD stub: fake downloads, creates mod dirs
│   ├── 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 + 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 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)
├── 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
├── Dockerfile                       # Multi-stage: Go build -> npm build -> alpine runtime
├── Dockerfile.goreleaser            # Single-stage for GoReleaser (pre-built artifacts injected)
├── .goreleaser.yaml                 # GoReleaser v2 config (Gitea release target)
├── 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
github.com/robfig/cron/v3 scheduler v3.0.1 Cron expression parsing and scheduled task execution
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
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
vitest test runner ^4.1.10 Frontend unit/integration tests
@testing-library/react test utility ^16.3.0 React component testing
@testing-library/jest-dom test matcher ^6.6.3 DOM assertion matchers
jsdom test env ^26.1.0 Browser environment for tests

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. Uses atomic.Int32 state machine (idle/starting/running/stopping) with CompareAndSwap for lock-free state transitions. 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. SERVER_BINARY, SERVER_PARAMS, STEAMCMD_PATH override defaults at runtime.
  7. Atomic state machine -- ProcessManager and SteamCmdManager use atomic.Int32/atomic.Bool with CompareAndSwap for lock-free state transitions, eliminating race conditions between concurrent Start/Stop calls.

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
      --> SERVER_PARAMS env overrides ServerParameters if set
      --> SERVER_BINARY env overrides platform default binary name if set
      --> 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

Gitea Actions (/.gitea/workflows/)

  • ci.yml — Triggered on push to any branch or PR. Runs go build ./..., npm ci && npm run build, go test ./..., and npm test. Build + test CI.
  • release.yml — Triggered on v* tag push. Runs GoReleaser v2 with release --clean, which builds Go binaries (linux/amd64 + windows/amd64), builds frontend via before.hooks, creates archives, builds + pushes a Docker image using Dockerfile.goreleaser, and publishes a Gitea release.

Docker

  • Dockerfile — Multi-stage build: Go compile → npm build → Debian slim runtime + SteamCMD. Single self-contained image.
  • Dockerfile.goreleaser — Single-stage assembly-only Dockerfile for GoReleaser CI. Expects pre-built artifacts injected by GoReleaser.

Local dev

  • Makefile targetsmake run starts backend, make dev starts both backend + Vite dev server with hot reload (copies test stubs to dev-deploy, passes SERVER_BINARY/SERVER_PARAMS/STEAMCMD_PATH env vars). make test runs both backend and frontend tests.