Brings in ANALYSIS.md, HOW_IT_WORKS.md, and CHERRY_PICK.md generated from deep analysis of the arma-server-web-admin benchmark project. These docs inform the Arma 3 UX enhancement plan (.claude/plan/arma3-ux-enhancement.md) and provide context for implementing agents without needing to re-read the source project.
248 lines
11 KiB
Markdown
248 lines
11 KiB
Markdown
# Cherry-Pick Candidates for languard-servers-manager
|
||
|
||
This document lists modules and functions from `arma-server-web-admin` that are worth adapting into `languard-servers-manager`. Each entry explains what the code does, why it is valuable, and a concrete adapter strategy.
|
||
|
||
Target project: `E:\TestScript\languard-servers-manager` (FastAPI + React stack).
|
||
|
||
---
|
||
|
||
## 1. Manager Pattern — Multi-Server Lifecycle Registry
|
||
|
||
**Source:** `lib/manager.js`
|
||
**Key functions:** `load()`, `save()`, `addServer()`, `removeServer()`, `getServer()`, `getServers()`
|
||
|
||
**What it does:**
|
||
Maintains an in-memory registry of Server instances (both as an ordered array and a hash for O(1) lookup), flushes state to `servers.json` on every mutation, and emits change events so the Socket.IO layer can broadcast diffs.
|
||
|
||
**Why it is valuable:**
|
||
languard already has a similar concept (servers stored in SQLite), but the EventEmitter pattern that automatically triggers broadcasts on every mutation is clean and decoupled. The dual array+hash storage pattern is also worth copying for cache performance.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Create: backend/core/servers/manager.py
|
||
- ServerManager class with in-memory cache (dict + list)
|
||
- on_change callback / asyncio.Event instead of EventEmitter
|
||
- load() reads from SQLite via existing ServerRepository
|
||
- save() writes back through repository
|
||
- Emit WebSocket broadcasts via FastAPI WebSocket manager on every mutation
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Process Lifecycle — Start / Stop / Kill with Graceful Fallback
|
||
|
||
**Source:** `lib/server.js` — `start()`, `stop()` methods
|
||
|
||
**What it does:**
|
||
Spawns a child process for the game server, captures its PID, starts a status-poll interval, pipes stdout/stderr to a log file, and on stop sends SIGTERM with a 5-second SIGKILL fallback. Headless clients are launched/killed alongside the main process.
|
||
|
||
**Why it is valuable:**
|
||
languard's backend already launches processes, but the graceful stop with timed SIGKILL fallback and the headless client co-lifecycle are patterns not yet present.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Adapt: backend/core/servers/process_manager.py
|
||
- async def start_server(config) -> asyncio.subprocess.Process
|
||
- async def stop_server(proc, timeout=5) -> SIGTERM then SIGKILL
|
||
- Capture PID, store in DB server record
|
||
- Pipe stdout/stderr to dated log file (matches existing log path logic)
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Status Polling — Gamedig-style Periodic Query
|
||
|
||
**Source:** `lib/server.js` — `queryStatus()` + `setInterval` pattern
|
||
|
||
**What it does:**
|
||
Every 5 seconds, queries the running game server via the game's UDP query protocol (Gamedig). Stores the response (`players`, `mission`, `state`) in the server's in-memory state and emits it to connected clients.
|
||
|
||
**Why it is valuable:**
|
||
languard's frontend relies on WebSocket events pushed from process stdout. A periodic external query would give accurate player counts and current mission data independent of log output.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Create: backend/core/servers/status_poller.py
|
||
- asyncio.Task per running server (cancel on stop)
|
||
- Use python-a2s or opengsq-python to query UDP game port
|
||
- QueryAdapter interface: arma3, dayz, etc. as subclasses
|
||
- On result: update server record in DB, push via WebSocket manager
|
||
- Poll interval configurable (default 5 s)
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Mod Discovery — Parallel Metadata Resolution
|
||
|
||
**Source:** `lib/mods/index.js`, `lib/mods/modFile.js`, `lib/mods/steamMeta.js`, `lib/mods/folderSize.js`
|
||
|
||
**What it does:**
|
||
Finds all mod directories via glob, then for each mod concurrently: parses `mod.cpp` for display name, parses `meta.cpp` for Steam Workshop ID, and recursively calculates folder size with symlink-aware deduplication.
|
||
|
||
**Why it is valuable:**
|
||
languard's mod tab lists paths but does not extract Steam IDs, human-readable names, or folder sizes. This pattern handles all three in parallel efficiently.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Create: backend/core/mods/scanner.py
|
||
- async def scan_mods(game_path: str) -> list[ModMeta]
|
||
- Use pathlib.glob for discovery
|
||
- asyncio.gather for parallel metadata:
|
||
parse_mod_file(path) -> ModFileAdapter (reads mod.cpp)
|
||
parse_steam_meta(path) -> SteamMetaAdapter (reads meta.cpp)
|
||
folder_size(path) -> recursive os.scandir sum
|
||
- ModMeta dataclass: { path, name, steam_id, size_bytes }
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Log File Management — Platform Paths + Auto-Cleanup
|
||
|
||
**Source:** `lib/logs.js` — `logsPath()`, `logFiles()`, `cleanupOldLogFiles()`
|
||
|
||
**What it does:**
|
||
Resolves the game log directory based on platform (windows / linux / wine). Lists `.rpt` files with metadata, auto-deletes the oldest files beyond a configurable retention limit (default 20).
|
||
|
||
**Why it is valuable:**
|
||
languard streams real-time logs via WebSocket but has no endpoint to browse historical `.rpt` files on disk. Discovery and cleanup logic is directly reusable.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Adapt: backend/core/logs/log_manager.py
|
||
- def logs_path(platform: str, game_path: str) -> Path
|
||
- def list_logs(logs_dir: Path) -> list[LogMeta] (stat each .rpt)
|
||
- def cleanup_old_logs(logs_dir: Path, keep: int = 20)
|
||
- Expose via:
|
||
GET /api/servers/{id}/logfiles
|
||
GET /api/servers/{id}/logfiles/{name}/download
|
||
DELETE /api/servers/{id}/logfiles/{name}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Mission Multi-File Upload + Filename Parsing
|
||
|
||
**Source:** `lib/missions.js` — `updateMissions()` + `routes/missions.js` upload handler
|
||
|
||
**What it does:**
|
||
Accepts multipart upload of up to 64 `.pbo` files simultaneously (parallel move with limit 8), scans the `mpmissions/` directory, and extracts `{ name, world }` from the Arma filename convention `missionname.worldname.pbo`.
|
||
|
||
**Why it is valuable:**
|
||
languard's missions tab lists files but does not support drag-and-drop multi-file upload or Steam Workshop download. The `.pbo` filename parsing convention is Arma-specific and worth encoding explicitly.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Adapt: backend/routers/missions.py
|
||
- POST /api/servers/{id}/missions -> UploadFile[] via FastAPI
|
||
- Validate .pbo extension server-side (reject others)
|
||
- asyncio.gather with semaphore (limit 8): move to mpmissions/
|
||
- Parse filename: "name.world.pbo" -> { name, world }
|
||
- Trigger rescan -> return updated list
|
||
- Optional: POST /api/missions/workshop { id } -> delegate to steamcmd
|
||
```
|
||
|
||
---
|
||
|
||
## 7. EventEmitter → WebSocket Broadcast Bridge
|
||
|
||
**Source:** `app.js` lines 49–66 (event bridge block)
|
||
|
||
**What it does:**
|
||
Listens on EventEmitter events from Manager, Missions, and Mods, then calls `io.emit()` to broadcast to all connected Socket.IO clients. New connections receive a full state snapshot immediately on connect.
|
||
|
||
**Why it is valuable:**
|
||
languard's WebSocket layer pushes real-time log lines but does not broadcast server state changes to all connected tabs/clients simultaneously. The "snapshot on connect + push diffs on change" pattern is directly applicable.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Adapt: backend/core/websocket/broadcast_manager.py
|
||
- WebSocketManager class (already partially exists in languard)
|
||
- Add: publish(event: str, payload) -> broadcast_all()
|
||
- On new connection: send snapshot of all servers, missions, mods
|
||
- On server state change: publish("servers", get_all_servers())
|
||
- Same for missions and mods events
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Mission Rotation Table — Per-Mission Difficulty
|
||
|
||
**Source:** `public/js/app/views/servers/missions/rotation/` (list + item views)
|
||
|
||
**What it does:**
|
||
Renders a table of missions in the server's active rotation. Each row has a difficulty dropdown. Rows can be added from the discovered mission list or removed individually. The full rotation array saves with the server config.
|
||
|
||
**Why it is valuable:**
|
||
languard's missions tab shows available missions but has no drag-to-add rotation table with per-mission difficulty. This is a key Arma workflow.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Adapt: frontend/src/pages/ServerDetailPage.tsx (Missions tab)
|
||
- MissionRotationTable component
|
||
- Row type: { name: string, difficulty: '' | 'Recruit' | 'Regular' | 'Veteran' }
|
||
- Add row: select from available missions dropdown
|
||
- Remove row: delete button per row
|
||
- Save: PUT /api/servers/{id} { missions: [...] }
|
||
- Backend: MissionRotationItem Pydantic schema as array field on Server
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Mod Assignment UI — Split-Pane Available / Selected
|
||
|
||
**Source:** `public/js/app/views/servers/mods/` (available list + selected list views)
|
||
|
||
**What it does:**
|
||
Two side-by-side lists: all discovered mods on the left, server-assigned mods on the right. Clicking a mod moves it between lists. Each list has a search filter. Selection saves when the settings form submits.
|
||
|
||
**Why it is valuable:**
|
||
languard's mods tab uses a flat checkbox list. The split-pane pattern with instant visual feedback is significantly more usable when mod counts are large (50+).
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Adapt: frontend/src/pages/ServerDetailPage.tsx (Mods tab)
|
||
- <AvailableModsList> and <SelectedModsList> side by side
|
||
- Shared state: selectedMods: string[] (mod paths)
|
||
- On click: immutable transfer between lists
|
||
- Search input on each list (client-side filter)
|
||
- On save: PUT /api/servers/{id} { mods: selectedMods }
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Startup Parameter Editor — Dynamic String List
|
||
|
||
**Source:** `public/js/app/views/servers/parameters/` (list + item views)
|
||
|
||
**What it does:**
|
||
Renders a dynamic list of startup parameter inputs (e.g., `-limitFPS=100`). Rows can be added or removed. The list persists with the server config.
|
||
|
||
**Why it is valuable:**
|
||
languard's Create Server wizard captures some fixed parameters but has no UI for arbitrary additional startup flags, which power users need.
|
||
|
||
**Adapter strategy:**
|
||
```
|
||
Create: frontend/src/components/ParameterEditor.tsx
|
||
- Props: value: string[], onChange: (v: string[]) => void
|
||
- Renders list of text inputs with remove buttons
|
||
- Add button appends empty string
|
||
- Used in: CreateServerPage wizard (step 3) and ServerDetailPage settings tab
|
||
- Save: include in PUT /api/servers/{id} { parameters: [...] }
|
||
```
|
||
|
||
---
|
||
|
||
## Priority Ranking
|
||
|
||
| Priority | Candidate | Effort | Value |
|
||
|----------|-----------|--------|-------|
|
||
| High | Status Polling (asyncio task per server) | Medium | Accurate live player counts |
|
||
| High | Mission Rotation Table UI | Medium | Key missing workflow |
|
||
| High | Mission Multi-File Upload | Low | Missing feature |
|
||
| High | Mod Discovery with Parallel Metadata | Medium | Rich mod metadata |
|
||
| Medium | Startup Parameter Editor UI | Low | Power-user feature |
|
||
| Medium | Mod Split-Pane Selection UI | Medium | UX improvement for large mod lists |
|
||
| Medium | Log File Discovery + Cleanup | Low | Historical log access |
|
||
| Low | EventEmitter Broadcast Bridge | Low | Already partially implemented |
|
||
| Low | Config-Driven Optional Auth | Low | Dev convenience only |
|