- ARCHITECTURE.md: fix diagram (Server Detail was "planned", now complete), expand frontend directory listing with all 5 pages, all server components, all hooks - FRONTEND.md: note planned terrain/display_name/workshop_id fields on Mission/Mod types; add planned hooks table for Phases 1-5 of UX enhancement plan - MODULES.md: annotate PlayerRepository with get_by_slot() and ThreadRegistry with get_rcon_client() as planned additions in Phase 4 - API.md: add "Upcoming Endpoints" section documenting all planned routes for Phases 1-5 of the UX enhancement plan - README.md: update unit test count (69 → ~120), update frontend structure comment to list all current pages/components/hooks
17 KiB
Frontend Architecture
Tech Stack
| Package | Version | Purpose |
|---|---|---|
| React | 19.2 | UI framework |
| TypeScript | 6.0 | Type safety |
| Vite | 8.0 | Build tool and dev server |
| TanStack React Query | 5.99 | Server state management, caching, mutations |
| Zustand | 5.0 | Client state (auth, UI) |
| Axios | 1.15 | HTTP client with interceptors |
| React Router | 7.14 | Client-side routing |
| React Hook Form | 7.72 | Form state management |
| Zod | 4.3 | Schema validation |
| Tailwind CSS | 3.4 | Utility-first CSS framework |
| Lucide React | 1.8 | Icon library |
| clsx | 2.1 | Conditional class merging |
Dev Dependencies
| Package | Purpose |
|---|---|
| Vitest 4.1 + jsdom | Unit testing |
| @testing-library/react 16 | Component testing |
| @testing-library/jest-dom 6 | DOM matchers |
| @testing-library/user-event 14 | User interaction simulation |
| Playwright 1.59 | E2E testing |
| ESLint 9 | Linting |
| TypeScript ESLint 8 | TypeScript lint rules |
Project Structure
frontend/src/
├── main.tsx # Entry point, renders <App />
├── App.tsx # Router, QueryClientProvider, auth guard
├── index.css # Tailwind directives, neumorphic classes
├── vite-env.d.ts # Vite env type declarations
│
├── lib/
│ └── api.ts # Axios client, auth interceptors
│
├── store/
│ ├── auth.store.ts # Auth state (token, user, isAuthenticated)
│ └── ui.store.ts # UI state (sidebar, notifications)
│
├── hooks/
│ ├── useServers.ts # TanStack Query hooks for server CRUD + lifecycle
│ ├── useServerDetail.ts # TanStack Query hooks for config, players, bans, missions, mods, RCon
│ ├── useAuth.ts # Auth management hooks (users, password, logout)
│ ├── useGames.ts # Game type hooks (list, detail, schema, defaults)
│ └── useWebSocket.ts # WebSocket connection with backoff + onEvent callback
│
├── pages/
│ ├── LoginPage.tsx # Login form with Zod validation
│ ├── DashboardPage.tsx # Server grid with lifecycle controls
│ ├── ServerDetailPage.tsx # Server detail with 7 tabs (overview, config, players, bans, missions, mods, logs)
│ ├── CreateServerPage.tsx # 4-step server creation wizard (admin only)
│ └── SettingsPage.tsx # Account (password change) + Users (admin user management)
│
├── components/
│ ├── layout/
│ │ └── Sidebar.tsx # Left nav with server list
│ ├── servers/
│ │ ├── ServerCard.tsx # Server card with actions
│ │ ├── ServerHeader.tsx # Server name, status, stats grid, lifecycle buttons
│ │ ├── ConfigEditor.tsx # Tabbed config section editor with optimistic locking
│ │ ├── PlayerTable.tsx # Current players + history with search
│ │ ├── BanTable.tsx # Ban list + create/revoke form
│ │ ├── MissionList.tsx # Mission list + upload/delete .pbo
│ │ ├── ModList.tsx # Mod list with enable/disable checkboxes
│ │ └── LogViewer.tsx # Log display with level filter (receives logs as props)
│ ├── settings/
│ │ ├── PasswordChange.tsx # Password change form
│ │ └── UserManager.tsx # User CRUD table (admin only)
│ └── ui/
│ └── StatusLed.tsx # Colored status indicator dot
│
└── __tests__/
├── api.test.ts # Axios interceptor tests
├── auth.store.test.ts # Auth store tests
├── ui.store.test.ts # UI store tests
├── StatusLed.test.tsx # StatusLed component tests
├── LoginPage.test.tsx # Login page tests
├── DashboardPage.test.tsx # Dashboard page tests
├── ServerCard.test.tsx # Server card rendering tests
├── ServerCard.handlers.test.tsx # Server card interaction tests
├── Sidebar.test.tsx # Sidebar tests
├── useWebSocket.test.tsx # WebSocket hook tests
└── useServers.test.tsx # Server hooks tests
Routes
| Path | Component | Auth | Status |
|---|---|---|---|
/login |
LoginPage |
Public | Implemented |
/ |
DashboardPage |
Protected | Implemented |
/servers/:serverId |
ServerDetailPage |
Protected | Implemented (7 tabs) |
/servers/new |
CreateServerPage |
Protected (admin only) | Implemented (4-step wizard) |
/settings |
SettingsPage |
Protected | Implemented (Account + Users tabs) |
ProtectedLayout wraps all non-login routes. When isAuthenticated is false, it redirects to /login.
Component Tree
App
├── QueryClientProvider
│ ├── BrowserRouter
│ │ ├── /login → LoginPage
│ │ └── /* → ProtectedLayout
│ │ ├── Sidebar
│ │ │ ├── Languard branding
│ │ │ ├── Dashboard link
│ │ │ ├── Server list (from useServers)
│ │ │ └── Settings link
│ │ └── main content area
│ │ ├── / → DashboardPage
│ │ │ ├── "2 servers configured" heading
│ │ │ ├── Add Server button
│ │ │ └── Server grid
│ │ │ └── ServerCard × N
│ │ │ ├── StatusLed + name + game type
│ │ │ ├── Stats: Players, Port, Restarts
│ │ │ └── Action buttons: Start/Stop/Restart
│ │ ├── /servers/:id → ServerDetailPage
│ │ │ ├── ServerHeader (status, stats, lifecycle buttons)
│ │ │ ├── Tab bar (Overview, Config, Players, Bans, Missions, Mods, Logs)
│ │ │ ├── OverviewTab (stats grid, executable path)
│ │ │ ├── ConfigEditor (section tabs, edit form, optimistic locking)
│ │ │ ├── PlayerTable (current + history with search)
│ │ │ ├── BanTable (ban list + create/revoke)
│ │ │ ├── MissionList (upload .pbo, delete)
│ │ │ ├── ModList (enable/disable checkboxes)
│ │ │ └── LogViewer (level filter, real-time via WebSocket onEvent)
│ │ ├── /servers/new → CreateServerPage
│ │ │ └── 4-step wizard (Game Type → Info → Options → Review)
│ │ └── /settings → SettingsPage
│ │ ├── Account tab → PasswordChange
│ │ └── Users tab → UserManager (admin only)
│ └── ReactQueryDevtools
State Management
Server State (TanStack Query)
All server data flows through TanStack Query hooks:
Server CRUD (useServers.ts):
| Hook | Type | Endpoint | Cache Key |
|---|---|---|---|
useServers() |
Query | GET /api/servers |
["servers"] (refetch every 30s) |
useServer(id) |
Query | GET /api/servers/:id |
["servers", id] |
useStartServer() |
Mutation | POST /api/servers/:id/start |
Invalidates ["servers", id], ["servers"] |
useStopServer() |
Mutation | POST /api/servers/:id/stop |
Invalidates both caches |
useRestartServer() |
Mutation | POST /api/servers/:id/restart |
Invalidates ["servers", id] |
useCreateServer() |
Mutation | POST /api/servers |
Invalidates ["servers"] |
useDeleteServer() |
Mutation | DELETE /api/servers/:id |
Invalidates ["servers"] |
useUpdateServer(id) |
Mutation | PUT /api/servers/:id |
Invalidates ["servers", id], ["servers"] |
useKillServer() |
Mutation | POST /api/servers/:id/kill |
Invalidates ["servers", id], ["servers"] |
Server Detail (useServerDetail.ts):
| Hook | Type | Endpoint | Cache Key |
|---|---|---|---|
useServerConfig(id) |
Query | GET /api/servers/:id/config |
["servers", id, "config"] |
useServerConfigSection(id, section) |
Query | GET /api/servers/:id/config/:section |
["servers", id, "config", section] |
useServerConfigPreview(id) |
Query | GET /api/servers/:id/config/preview |
["servers", id, "config", "preview"] |
useServerPlayers(id) |
Query | GET /api/servers/:id/players |
["players", id] |
useServerPlayerHistory(id, opts?) |
Query | GET /api/servers/:id/players/history |
["players", id, "history", opts] |
useServerBans(id) |
Query | GET /api/servers/:id/bans |
["bans", id] |
useServerMissions(id) |
Query | GET /api/servers/:id/missions |
["missions", id] |
useServerMods(id) |
Query | GET /api/servers/:id/mods |
["mods", id] |
useUpdateConfigSection(id, section) |
Mutation | PUT /api/servers/:id/config/:section |
Invalidates config keys |
useCreateBan(id) |
Mutation | POST /api/servers/:id/bans |
Invalidates ["bans", id] |
useRevokeBan(id) |
Mutation | DELETE /api/servers/:id/bans/:banId |
Invalidates ["bans", id] |
useUploadMission(id) |
Mutation | POST /api/servers/:id/missions (multipart) |
Invalidates ["missions", id] |
useDeleteMission(id) |
Mutation | DELETE /api/servers/:id/missions/:filename |
Invalidates ["missions", id] |
useSetEnabledMods(id) |
Mutation | PUT /api/servers/:id/mods/enabled |
Invalidates ["mods", id] |
useSendCommand(id) |
Mutation | POST /api/servers/:id/rcon/command |
No invalidation |
Auth (useAuth.ts):
| Hook | Type | Endpoint |
|---|---|---|
useCurrentUser() |
Query | GET /api/auth/me |
useUsers() |
Query | GET /api/auth/users |
useChangePassword() |
Mutation | PUT /api/auth/password |
useCreateUser() |
Mutation | POST /api/auth/users |
useDeleteUser() |
Mutation | DELETE /api/auth/users/:id |
useLogout() |
Mutation | POST /api/auth/logout + clear auth state |
Games (useGames.ts):
| Hook | Type | Endpoint |
|---|---|---|
useGamesList() |
Query | GET /api/games |
useGameDetail(gameType) |
Query | GET /api/games/:gameType |
useGameConfigSchema(gameType) |
Query | GET /api/games/:gameType/config-schema |
useGameDefaults(gameType) |
Query | GET /api/games/:gameType/defaults |
Key type notes:
Servertype inuseServers.tsusesgame_port,current_players,max_players(matches enriched API response)Missiontype:{ name, filename, size_bytes }—terrainfield planned (Phase 2 UX enhancement)Modtype:{ name, path, size_bytes, enabled }—display_name,workshop_idfields planned (Phase 3 UX enhancement)Bantype:{ id, server_id, guid, name, reason, banned_by, banned_at, expires_at, is_active, game_data }(matches API)- There is no REST endpoint for logs — logs are only pushed via WebSocket events
Planned hooks (UX Enhancement Plan):
| Hook | Phase | Endpoint |
|---|---|---|
useConfigUISchema(serverId) |
Phase 1 | GET /api/servers/:id/config/ui-schema |
useMissionRotation(id) |
Phase 2 | GET /api/servers/:id/missions/rotation |
useUpdateMissionRotation(id) |
Phase 2 | PUT /api/servers/:id/missions/rotation |
useKickPlayer(id) |
Phase 4 | POST /api/servers/:id/players/:slot_id/kick |
useBanPlayer(id) |
Phase 4 | POST /api/servers/:id/players/:slot_id/ban |
useLogFiles(id) |
Phase 5 | GET /api/servers/:id/logfiles |
QueryClient defaults: staleTime: 10s, retry: 2, refetchOnWindowFocus: false.
Client State (Zustand)
auth.store.ts — Persisted to localStorage under languard-auth:
token: string | null— JWT access tokenuser: { id, username, role } | null— Current userisAuthenticated: boolean— Derived on rehydrationsetAuth(token, user)— Sets token, user, and writeslanguard_tokento localStorageclearAuth()— Clears all state and localStorage keys
ui.store.ts — In-memory only:
sidebarOpen: boolean— Sidebar collapse stateactiveServerId: number | null— Highlighted server in sidebarnotifications: Notification[]— Toast notifications (auto-remove after 5s)addNotification(type, message)— Add toast with auto-dismissremoveNotification(id)— Manual dismiss
API Client
src/lib/api.ts configures Axios with:
- Base URL:
VITE_API_URLenv var, defaults tohttp://localhost:8000 - Timeout: 30 seconds
- Request interceptor: Reads
languard_tokenfrom localStorage, addsAuthorization: Bearer <token>header - Response interceptor: On 401, checks if URL starts with
/api/auth/— if NOT an auth endpoint, clears token and redirects to/login. Auth endpoint 401s are left for the calling component to handle.
Standard response envelope: { success: boolean, data: T | null, error?: string }
WebSocket
useWebSocket.ts connects to VITE_WS_URL (default ws://localhost:8000):
- Accepts
UseWebSocketOptions(ornumber[]for backward compat):{ serverIds?, onEvent? } - Passes JWT token as query parameter:
/ws?token=...&server_id=... - Exponential backoff reconnect: starts 2s, doubles up to 30s max
- Close code 4001 = explicit disconnect (no reconnect)
onEventcallback: receives raw WebSocket events for custom handling (e.g., log accumulation in ServerDetailPage)- Event handlers invalidate TanStack Query caches:
server_status→["servers", id]+["servers"]metrics→["metrics", id]log→ no cache key (no REST endpoint for logs; use onEvent callback instead)players→["players", id]
Important: There is no REST endpoint for server logs. Logs are only available via WebSocket push events. Components that need log data must use the onEvent callback to accumulate log entries in local state, then pass them to LogViewer as props.
Design System
Dark neumorphic theme defined in tailwind.config.js:
Colors:
- Surface: base
#1a1a2e, raised#1e1e35, recessed#16162a, overlay#22223a - Accent: amber
#f59e0bwith bright/dim/glow variants - Status: running green, stopped gray, crashed red, starting amber, restarting blue
- Text: primary
#e2e8f0, secondary#94a3b8, muted#475569
Neumorphic classes: neu-card, neu-input, btn-primary, btn-danger, btn-ghost, status-led-*
Custom shadows: neu-raised, neu-raised-lg, neu-recessed, plus glow variants
Fonts: Inter (sans), JetBrains Mono (mono)
Testing
Unit Tests (120 tests, Vitest + React Testing Library)
| Test File | Tests | Coverage |
|---|---|---|
api.test.ts |
4 | Interceptors: token header, 401 redirect (non-auth), 401 no-redirect (auth) |
auth.store.test.ts |
3 | Init state, setAuth, clearAuth, localStorage sync |
ui.store.test.ts |
5 | Init state, toggleSidebar, setActiveServer, add/remove notifications |
StatusLed.test.tsx |
8 | Status classes, showLabel, sizes |
LoginPage.test.tsx |
4 | Form render, validation, API call, error display |
DashboardPage.test.tsx |
5 | Loading/error/empty states, card rendering |
ServerCard.test.tsx |
10 | Card rendering, button visibility, disabled states, actions |
ServerCard.handlers.test.tsx |
9 | Start/stop/restart success/failure notifications |
Sidebar.test.tsx |
6 | Branding, links, loading state, server list, active highlight |
useWebSocket.test.tsx |
5 | No-connect without token, connect, token in URL, invalidation, cleanup |
useServers.test.tsx |
10 | Server CRUD + lifecycle hooks, cache invalidation |
useServerDetail.test.tsx |
20+ | Config, players, bans, missions, mods, mutations, cache invalidation |
useAuth.test.tsx |
7 | Current user, users, change password, create/delete user, logout |
useGames.test.tsx |
5 | Games list, detail, config schema, defaults |
E2E Tests (23 tests, Playwright)
Login Flow (6 tests):
- Display login form, branding, validation errors
- Error on invalid credentials (mocked 401)
- Navigation to dashboard on successful login
- Loading state ("Signing in...")
Dashboard (12 tests):
- Header, server count, server cards, server names
- Add Server button, sidebar with server list
- Stop button for running server, Start button for stopped
- Player count display, server detail navigation
- Empty state, error state
Full Stack Integration (5 tests):
- Login + see A3Master on dashboard (real backend)
- A3Master server details in card (real backend)
- Server detail navigation (real backend)
- Unauthenticated redirect to login
- API response shape validation
Configuration
Vite (vite.config.ts)
- Path alias
@→./src - Dev server on port 5173
- Proxy:
/api→http://localhost:8000,/ws→ws://localhost:8000
Vitest (vitest.config.ts)
jsdomenvironment- Global test APIs
- Setup:
src/__tests__/setup.ts(imports jest-dom matchers) - Excludes
tests-e2edirectory
Playwright (playwright.config.ts)
- Chromium only
baseURL: http://localhost:5173- Traces on first retry, screenshots on failure, video on failure
- Dev server auto-started via
npm run dev - CI: retries=2, single worker,
forbidOnly