Files
languard-servers-manager/FRONTEND.md
Tran G. (Revernomad) Khoa d45345a094 feat: fix mods tab, add client/server split, and scaffold server dirs
Mods tab bug fixes:
- mod_manager: fix wrong kwargs in set_enabled_mods, fix scan dir to use
  mods/ subdir instead of server root, migrate old string-list format to
  dict format on read
- service: replace dead server_mods SQL JOIN with get_enabled_mods()
  call through the mod_manager capability; pass is_server_mod to
  build_mod_args
- mods_router: accept list[EnabledModEntry] objects (name + is_server_mod)
  instead of bare strings

Client/server mod split:
- Mods now stored as list[{"name": str, "is_server_mod": bool}]; old
  string-list format auto-migrated on read
- is_server_mod=true routes to -serverMod= arg; false to -mod= arg
- ModList UI: amber Client/Server badge in selected pane; toggle button
  in split-pane selector

Directory scaffold:
- process_config: adds "mods" to dir layout; provides get_dir_readme()
  with per-directory README.txt content
- file_utils: ensure_server_dirs() gains readme_provider kwarg; writes
  README.txt idempotently if absent
- service.create_server: passes readme_provider via hasattr probe
- main.py startup: backfills all existing servers with correct subdirs
  and README files (idempotent)

Docs: API.md and FRONTEND.md updated for new mod schema and types
Test __init__.py files added for pytest discovery
2026-04-20 10:54:56 +07:00

359 lines
19 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.
# 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; per-field widgets via useServerConfigSchema
│ │ ├── PlayerTable.tsx # Current players + history with search
│ │ ├── BanTable.tsx # Ban list + create/revoke form
│ │ ├── MissionList.tsx # Available missions + Mission Rotation sections; multi-file upload
│ │ ├── ModList.tsx # Split-pane mod selector (Available vs Selected); Apply Selection button
│ │ └── LogViewer.tsx # Log display with level filter + collapsible Log Files browser (download/delete)
│ ├── settings/
│ │ ├── PasswordChange.tsx # Password change form
│ │ └── UserManager.tsx # User CRUD table (admin only)
│ └── ui/
│ ├── StatusLed.tsx # Colored status indicator dot
│ └── TagListEditor.tsx # Dynamic string-list editor (add/remove items)
└── __tests__/
├── api.test.ts # Axios interceptor tests
├── auth.store.test.ts # Auth store tests
├── ui.store.test.ts # UI store tests
├── logger.test.ts # Logger level-filtering 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 (useServers, useServer, lifecycle, useUpdateServer, useKillServer)
├── useServerDetail.test.tsx # Server detail hooks (config, players, bans, missions, mods, RCon, logfiles)
├── useAuth.test.tsx # Auth hooks tests
├── useGames.test.tsx # Games hooks tests
└── CreateServerPage.test.tsx # Create server wizard (steps, validation, submit, edge cases)
```
## 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, per-field widgets from schema, optimistic locking)
│ │ │ ├── PlayerTable (current + history with search)
│ │ │ ├── BanTable (ban list + create/revoke)
│ │ │ ├── MissionList (Available + Rotation sections, multi-file upload)
│ │ │ ├── ModList (split-pane: Available | Selected; Apply Selection)
│ │ │ └── LogViewer (level filter, real-time stream + Log Files browser)
│ │ ├── /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]` |
| `useServerConfigSchema(id)` | Query | `GET /api/servers/:id/config/schema` (per-field widget hints for ~80 fields: text, toggle, select, number, password, tag-list, hidden, textarea) | `["servers", id, "config", "schema"]` |
| `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]` |
| `useServerMissionRotation(id)` | Query | `GET /api/servers/:id/missions/rotation` | `["missions", id, "rotation"]` |
| `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, `File[]`) | Invalidates `["missions", id]` |
| `useUpdateMissionRotation(id)` | Mutation | `PUT /api/servers/:id/missions/rotation` | Invalidates rotation + server config |
| `useDeleteMission(id)` | Mutation | `DELETE /api/servers/:id/missions/:filename` | Invalidates `["missions", id]` |
| `useSetEnabledMods(id)` | Mutation | `PUT /api/servers/:id/mods/enabled` body: `EnabledModEntry[]` | Invalidates `["mods", id]` |
| `useSendCommand(id)` | Mutation | `POST /api/servers/:id/rcon/command` | No invalidation |
| `useKickPlayer(id)` | Mutation | `POST /api/servers/:id/players/:slot_id/kick` | Invalidates `["players", id]` |
| `useBanPlayer(id)` | Mutation | `POST /api/servers/:id/players/:slot_id/ban` | Invalidates players + bans |
| `useServerLogFiles(id)` | Query | `GET /api/servers/:id/logfiles` | `["servers", id, "logfiles"]` (refetch 30s) |
| `useDeleteLogFile(id)` | Mutation | `DELETE /api/servers/:id/logfiles/:filename` | Invalidates logfiles |
**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**:
- `Server` type in `useServers.ts` uses `game_port`, `current_players`, `max_players` (matches enriched API response)
- `Mission` type: `{ name, filename, size_bytes, terrain }` — terrain parsed from filename
- `Mod` type: `{ name, path, size_bytes, enabled, is_server_mod, display_name, workshop_id }``display_name`/`workshop_id` from mod.cpp/meta.cpp; `is_server_mod` controls `-serverMod=` vs `-mod=`
- `EnabledModEntry` type: `{ name: string, is_server_mod: boolean }` — used as `useSetEnabledMods` mutation input
- `Ban` type: `{ 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
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 token
- `user: { id, username, role } | null` — Current user
- `isAuthenticated: boolean` — Derived on rehydration
- `setAuth(token, user)` — Sets token, user, and writes `languard_token` to localStorage
- `clearAuth()` — Clears all state and localStorage keys
**ui.store.ts** — In-memory only:
- `sidebarOpen: boolean` — Sidebar collapse state
- `activeServerId: number | null` — Highlighted server in sidebar
- `notifications: Notification[]` — Toast notifications (auto-remove after 5s)
- `addNotification(type, message)` — Add toast with auto-dismiss
- `removeNotification(id)` — Manual dismiss
## API Client
`src/lib/api.ts` configures Axios with:
- **Base URL**: `VITE_API_URL` env var, defaults to `http://localhost:8000`
- **Timeout**: 30 seconds
- **Request interceptor**: Reads `languard_token` from localStorage, adds `Authorization: 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` (or `number[]` 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)
- `onEvent` callback: 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 `#f59e0b` with 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 (167 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` | 8 | Init state, setAuth, clearAuth, localStorage sync, rehydration, partialize |
| `ui.store.test.ts` | 5 | Init state, toggleSidebar, setActiveServer, add/remove notifications |
| `logger.test.ts` | 10 | All 4 log methods, level filtering (debug/warn/error), message format |
| `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` | 12 | Server CRUD + lifecycle hooks, useUpdateServer, useKillServer |
| `useServerDetail.test.tsx` | 20+ | Config, players, bans, missions, mods, mutations, logfiles, 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 |
| `CreateServerPage.test.tsx` | 14 | All 4 wizard steps, validation, submit, non-admin gate, API error handling |
### E2E Tests (38 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
**Server Detail — 5 UX phases** (15 tests, fully mocked):
- Overview: server name/status, all 6 tabs visible
- Config: field labels rendered (Hostname, BattlEye)
- Missions: mission names, terrain names, Upload button
- Mods: display names, enabled/disabled state
- Players: player list, ping values, Kick buttons
- Logs: collapsible Log Files section, Download buttons, live log viewer area
**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`)
- `jsdom` environment
- Global test APIs
- Setup: `src/__tests__/setup.ts` (imports jest-dom matchers)
- Excludes `tests-e2e` directory
### 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`