Files
languard-servers-manager/docs/CHERRY_PICK.md
Khoa (Revenovich) Tran Gia 4ba199dd62 docs: add arma-server-web-admin analysis reference docs
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.
2026-04-17 14:55:59 +07:00

248 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 4966 (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 |