From 5d009d50d1f1d068c1cfa334cc5e375d29b1f1cb Mon Sep 17 00:00:00 2001 From: "Khoa (Revenovich) Tran Gia" Date: Fri, 17 Apr 2026 14:53:37 +0700 Subject: [PATCH] docs: add Arma 3 UX enhancement implementation plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-references arma-server-web-admin benchmark, classifies cherry-pick candidates (must-have/good-to-have/optional), and provides a 6-phase implementation plan covering config UI schema, mission rotation, mod display names, player kick/ban, log file browser, and server card quick actions. Plan is self-contained — no need to re-read the benchmark project to execute. --- .claude/plan/arma3-ux-enhancement.md | 891 +++++++++++++++++++++++++++ 1 file changed, 891 insertions(+) create mode 100644 .claude/plan/arma3-ux-enhancement.md diff --git a/.claude/plan/arma3-ux-enhancement.md b/.claude/plan/arma3-ux-enhancement.md new file mode 100644 index 0000000..6a23325 --- /dev/null +++ b/.claude/plan/arma3-ux-enhancement.md @@ -0,0 +1,891 @@ +# Plan: Arma 3 Adapter UX Enhancement + +**Status:** APPROVED — Ready to implement +**Branch:** main +**Estimated effort:** ~20h total (6 phases) + +--- + +## Background & Context + +### What was analyzed + +`arma-server-web-admin` (Node.js/Express + Backbone.js) was deep-analyzed as a UX benchmark. Full documentation was written to `E:\TestScript\arma-server-web-admin\docs\`: +- `ANALYSIS.md` — feature inventory, tech stack, directory structure +- `HOW_IT_WORKS.md` — internal flows (server start/stop, mission upload, mod discovery, Socket.IO bridge) +- `CHERRY_PICK.md` — adapter candidates with file paths and adapter strategies + +**You do NOT need to re-read arma-server-web-admin.** All relevant patterns are self-contained below. + +### Problem statement + +languard-servers-manager has a **complete backend** (42+ endpoints, Arma3ConfigGenerator, Arma3MissionManager, Arma3ModManager, Arma3BanManager, Arma3RemoteAdmin) but the **frontend has gaps** that make daily Arma 3 server administration painful: + +| Problem | Root cause | +|---------|-----------| +| All config fields render as text boxes | ConfigEditor is generic; no per-field widget hints | +| Can't build a mission rotation | Backend config supports `missions[]` array; no UI | +| Mods show `@CBA_A3` not "Community Base Addons" | `mod.cpp` not parsed; no `display_name` field | +| Can't kick a player from the UI | `Arma3RemoteAdmin.kick_player()` exists; endpoint missing | +| Can't browse/download historical log files | Only real-time WebSocket stream; no file browser | +| Must navigate to detail page to start/stop | No quick-action buttons on ServerCard | + +--- + +## Cross-Reference: Cherry-Pick Classification + +### MUST HAVE + +| Feature | Gap in languard | +|---------|----------------| +| Config field UI widgets (textarea/dropdown/toggle/tag-list) | Generic `` for everything | +| Kick player from Players tab | `Arma3RemoteAdmin.kick_player(slot_id, reason)` exists but no HTTP endpoint | +| Mission rotation table + per-mission difficulty | Backend: `Arma3MissionManager.get_rotation_config()` exists. Frontend: nothing | +| Multi-file mission upload | `useUploadMission` accepts single `File` only | + +### GOOD TO HAVE + +| Feature | Gap | +|---------|-----| +| Mod display names (mod.cpp parsing) | `list_available_mods()` returns path only, no `display_name` | +| Split-pane mod assignment UI | Flat checkbox list | +| Server card start/stop quick actions | Must navigate to ServerDetailPage | +| Log file browser + download | WebSocket stream only | +| Admin UIDs tag list in config | Hidden in raw JSON config editor | +| Ban from player list quick action | User must go to Bans tab manually | + +### OPTIONAL + +| Feature | Note | +|---------|------| +| Steam Workshop mission download | Requires external `steamcmd` binary | +| Mod Steam Workshop ID (meta.cpp) | Informational only | +| Log viewer level filter | Pure client-side | +| Headless client count in UI | Niche advanced feature | + +--- + +## Current Codebase — Critical File Inventory + +### Backend (FastAPI + Python) + +``` +backend/ +├── adapters/arma3/ +│ ├── adapter.py # Arma3Adapter — registers capabilities, returns sub-managers +│ ├── config_generator.py # Arma3ConfigGenerator +│ │ SECTIONS: ServerSection, BasicSection, ProfileSection, +│ │ LaunchSection, RconSection +│ │ KEY FIELDS IN ServerSection: +│ │ hostname (str), max_players (int), password (str), +│ │ admin_password (str), motd_lines (list[str]), +│ │ forced_difficulty (str), battle_eye (bool), von (bool), +│ │ admin_uids (list[str]) +│ │ KEY FIELDS IN LaunchSection: +│ │ additional_args (list[str]) +│ │ METHODS: get_sections(), write_configs(), build_launch_args(), +│ │ get_sensitive_fields(), get_defaults() +│ │ ADD: get_ui_schema() -> dict +│ ├── mission_manager.py # Arma3MissionManager +│ │ list_missions() -> [{name, filename, size_bytes}] +│ │ parse_mission_filename(fn) -> {mission_name, terrain, filename} +│ │ get_rotation_config(entries) -> Arma3 missions config block string +│ │ UPDATE: list_missions() add terrain field +│ ├── mod_manager.py # Arma3ModManager +│ │ list_available_mods() -> [{name, path, size_bytes, enabled}] +│ │ get_enabled_mods(), set_enabled_mods(), build_mod_args() +│ │ UPDATE: add display_name, workshop_id to list_available_mods() +│ ├── log_parser.py # RPTParser +│ │ parse_line(), get_log_file_resolver() +│ │ ADD: list_log_files(server_dir), get_log_file_path(server_dir, filename) +│ ├── remote_admin.py # Arma3RemoteAdmin (DO NOT MODIFY — stable) +│ │ kick_player(slot_id, reason) -> bool ← USE THIS +│ │ ban_player(uid, duration_minutes, reason) -> bool +│ │ get_players() -> list[dict] +│ └── ban_manager.py # Arma3BanManager — bans.txt sync (stable) +│ +├── core/servers/ +│ ├── router.py # Main server endpoints +│ │ ADD: GET /api/servers/{id}/config/schema +│ ├── service.py # ServerService +│ │ ADD: get_config_schema(), kick_player(), ban_from_player() +│ ├── missions_router.py # GET/POST/DELETE /api/servers/{id}/missions/* +│ │ ADD: GET /api/servers/{id}/missions/rotation +│ │ ADD: PUT /api/servers/{id}/missions/rotation +│ ├── players_router.py # GET /api/servers/{id}/players +│ │ ADD: POST /api/servers/{id}/players/{slot_id}/kick +│ │ ADD: POST /api/servers/{id}/players/{slot_id}/ban +│ └── [NEW] logfiles_router.py +│ GET /api/servers/{id}/logfiles +│ GET /api/servers/{id}/logfiles/{filename}/download +│ DELETE /api/servers/{id}/logfiles/{filename} +│ +└── main.py # ADD: include_router(logfiles_router) +``` + +### Frontend (React 19 + TypeScript + TanStack Query) + +``` +frontend/src/ +├── hooks/ +│ └── useServerDetail.ts # All query/mutation hooks for server detail +│ ADD: useServerConfigSchema() +│ ADD: useServerMissionRotation(), useUpdateMissionRotation() +│ ADD: useKickPlayer(), useBanPlayer() +│ ADD: useServerLogFiles(), useDeleteLogFile() +│ UPDATE: useUploadMission(File[]) — was single File +│ UPDATE: Mod type — add display_name, workshop_id +│ UPDATE: Mission type — add terrain field +│ +├── components/servers/ +│ ├── ConfigEditor.tsx # UPDATE: consume schema, render per-widget type +│ ├── MissionList.tsx # REDESIGN: Available section + Rotation section +│ ├── ModList.tsx # REDESIGN: split pane (Available vs Selected) +│ ├── PlayerTable.tsx # UPDATE: add Actions column (Kick/Ban buttons, admin only) +│ ├── LogViewer.tsx # UPDATE: level filter + Log Files browser section +│ └── ServerCard.tsx # UPDATE: Start/Stop quick-action buttons +│ +└── components/ui/ + └── [NEW] TagListEditor.tsx # Dynamic string-list editor (reused in ConfigEditor) +``` + +--- + +## Execution Order + +| Phase | Description | Priority | Est. | +|-------|-------------|----------|------| +| 1 | Config UI Schema (widget hints per field) | MUST | ~4h | +| 4 | Player Kick/Ban | MUST | ~3h | +| 2 | Mission Rotation + Multi-file upload | MUST | ~5h | +| 3 | Mod Display Names + Split Pane | GOOD | ~4h | +| 6 | Server Card Quick Actions | GOOD | ~1h | +| 5 | Log File Browser + Level Filter | GOOD | ~3h | + +--- + +## Phase 1 — Config UI Schema System (MUST HAVE, ~4h) + +**Goal:** Each Arma 3 config field renders with the right UI widget instead of a generic text box. + +### 1.1 `backend/adapters/arma3/config_generator.py` — add `get_ui_schema()` + +Add this method to `Arma3ConfigGenerator`: + +```python +def get_ui_schema(self) -> dict: + return { + "server": { + "hostname": {"widget": "text", "label": "Server Hostname"}, + "max_players": {"widget": "number", "label": "Max Players", "min": 1, "max": 256}, + "password": {"widget": "password", "label": "Player Password"}, + "admin_password": {"widget": "password", "label": "Admin Password"}, + "motd_lines": {"widget": "textarea", "label": "Message of the Day (one line per row)"}, + "forced_difficulty": {"widget": "select", "label": "Difficulty Preset", + "options": ["", "Recruit", "Regular", "Veteran", "Custom"]}, + "battle_eye": {"widget": "toggle", "label": "BattleEye Anti-Cheat"}, + "von": {"widget": "toggle", "label": "Voice over Net (VoN)"}, + "verify_signatures": {"widget": "toggle", "label": "Verify Addon Signatures"}, + "persistent": {"widget": "toggle", "label": "Persistent (keep running when empty)"}, + "admin_uids": {"widget": "tag-list", "label": "Admin Steam UIDs", + "placeholder": "76561198000000000"}, + }, + "basic": { + "max_packet_size": {"widget": "number", "label": "Max Packet Size"}, + "max_custom_file_size": {"widget": "number", "label": "Max Custom File Size (bytes)"}, + }, + "launch": { + "additional_args": {"widget": "tag-list", "label": "Additional Startup Parameters", + "placeholder": "-limitFPS=100"}, + }, + "rcon": { + "password": {"widget": "password", "label": "RCon Password"}, + "port": {"widget": "number", "label": "RCon Port"}, + }, + } +``` + +### 1.2 `backend/core/servers/service.py` — add `get_config_schema()` + +```python +async def get_config_schema(self, server_id: int, db: Session) -> dict: + server = self.server_repo.get_by_id(server_id, db) + adapter = self.adapter_registry.get(server.game_type) + config_gen = adapter.get_config_generator() + if hasattr(config_gen, "get_ui_schema"): + return config_gen.get_ui_schema() + return {} +``` + +### 1.3 `backend/core/servers/router.py` — new endpoint + +Add after existing config routes: +```python +@router.get("/{server_id}/config/schema") +async def get_config_schema( + server_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + schema = await server_service.get_config_schema(server_id, db) + return {"success": True, "data": schema} +``` + +### 1.4 `frontend/src/hooks/useServerDetail.ts` — add schema types + hook + +```typescript +export interface FieldSchema { + widget: "text" | "number" | "password" | "textarea" | "select" | "toggle" | "tag-list"; + label?: string; + placeholder?: string; + min?: number; + max?: number; + options?: string[]; +} + +export interface ConfigSchema { + [section: string]: { [field: string]: FieldSchema }; +} + +export function useServerConfigSchema(serverId: number) { + return useQuery({ + queryKey: ["servers", serverId, "config", "schema"], + queryFn: async () => { + const res = await apiClient.get<{ success: boolean; data: ConfigSchema }>( + `/api/servers/${serverId}/config/schema`, + ); + return res.data.data; + }, + enabled: serverId > 0, + }); +} +``` + +### 1.5 `frontend/src/components/ui/TagListEditor.tsx` — NEW component + +```typescript +interface TagListEditorProps { + value: string[]; + onChange: (v: string[]) => void; + placeholder?: string; + disabled?: boolean; +} + +export function TagListEditor({ value, onChange, placeholder, disabled }: TagListEditorProps) { + const update = (idx: number, val: string) => + onChange(value.map((v, i) => (i === idx ? val : v))); + const remove = (idx: number) => onChange(value.filter((_, i) => i !== idx)); + const add = () => onChange([...value, ""]); + + return ( +
+ {value.map((item, idx) => ( +
+ update(idx, e.target.value)} + /> + +
+ ))} + +
+ ); +} +``` + +### 1.6 `frontend/src/components/servers/ConfigEditor.tsx` — consume schema + +- Import `useServerConfigSchema` and `TagListEditor` +- Call `useServerConfigSchema(serverId)` alongside existing config queries +- For each field in a config section, look up `schema?.[sectionName]?.[fieldName]` +- Render based on `widget`: + - `"text"` → `` + - `"number"` → `` + - `"password"` → `` + - `"textarea"` → `