# Plan: Arma 3 Adapter UX Enhancement
**Status:** APPROVED — Ready to implement
**Branch:** main
**Estimated effort:** ~20h total (5 active phases)
---
## Progress Tracker
> **IMPLEMENTING AGENT:** Update this section at the start and end of each session. Mark each phase `[x]` when ALL its checklist items pass. This is the only reliable way for the next session to know where to pick up.
| Phase | Status | Last session note |
|-------|--------|------------------|
| 1 — Config UI Schema | `[x] done` | TagListEditor, useServerConfigSchema, ConfigEditor widget routing, backend get_ui_schema + endpoint |
| 2 — Mission Rotation | `[x] done` | terrain in list_missions, rotation GET/PUT endpoints, MissionRotationEntry type, useServerMissionRotation/useUpdateMissionRotation hooks, multi-file upload, MissionList redesigned with Available + Rotation sections |
| 3 — Mod Display Names + Split Pane | `[x] done` | _parse_mod_cpp/_parse_meta_cpp in mod_manager, ModList split-pane redesign |
| 4 — Player Kick/Ban | `[x] done` | get_by_slot in PlayerRepository, get_rcon_client in ThreadRegistry, kick/ban endpoints, PlayerTable modals |
| 5 — Log File Browser | `[x] done` | list_log_files/get_log_file_path in RPTParser, logfiles_router (GET/download/DELETE), LogViewer file browser section |
**How to resume:** Read this table first. Find the first phase that is not `[x] done`. Read only that phase section — do not re-read earlier phases. Run `cd frontend && npx tsc --noEmit` to confirm the build is clean before making any changes.
---
## Background & Context
### What was analyzed
`arma-server-web-admin` (Node.js/Express + Backbone.js) was deep-analyzed as a UX benchmark. Reference documentation is in `docs/` (this repo):
- `docs/ANALYSIS.md` — feature inventory, tech stack, directory structure
- `docs/HOW_IT_WORKS.md` — internal flows (server start/stop, mission upload, mod discovery, Socket.IO bridge)
- `docs/CHERRY_PICK.md` — adapter candidates with file paths and adapter strategies
**You do NOT need to read the original arma-server-web-admin source.** All relevant patterns are documented in `docs/` and 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 |
| 5 | Log File Browser + Level Filter | GOOD | ~3h |
| 6 | Server Card Quick Actions | ~~GOOD~~ | **DONE** |
---
## 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": {
# Field names MUST match Arma3ConfigGenerator.ServerConfig exactly
"hostname": {"widget": "text", "label": "Server Hostname"},
"max_players": {"widget": "number", "label": "Max Players", "min": 1, "max": 1000},
"password": {"widget": "password", "label": "Player Password"},
"password_admin": {"widget": "password", "label": "Admin Password"}, # NOT 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"]},
"battleye": {"widget": "toggle", "label": "BattleEye Anti-Cheat"}, # NOT battle_eye
"disable_von": {"widget": "toggle", "label": "Disable Voice over Net (VoN)"}, # NOT von — and it's inverted
"verify_signatures": {"widget": "number", "label": "Verify Signatures (0=off, 1=on, 2=strict)", "min": 0, "max": 2},
"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()`
Follow the existing pattern: `ServerService.__init__` already stores `self._server_repo` and `self._config_repo`. No `db` param needed:
```python
def get_config_schema(self, server_id: int) -> dict:
server = self.get_server(server_id) # raises 404 if not found, uses self._server_repo
adapter = GameAdapterRegistry.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, following the `ServerService(db)` inline pattern:
```python
@router.get("/{server_id}/config/schema")
def get_config_schema(
server_id: int,
db: Annotated[Connection, Depends(get_db)],
_user: Annotated[dict, Depends(get_current_user)],
) -> dict:
schema = ServerService(db).get_config_schema(server_id)
return {"success": True, "data": schema, "error": None}
```
### 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"` → ``
- `"select"` → `