Initial commit
This commit is contained in:
156
CODEBASE.md
Normal file
156
CODEBASE.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user