Files
languard-servers-manager/FRONTEND.md
Tran G. (Revernomad) Khoa bf09a6ed1c fix: fix Arma 3 log discovery and improve config editor UX
- Fix logfiles_router and thread_registry to resolve .rpt log files
  from Path(server["exe_path"]).parent/server/ instead of the languard
  data dir, which never contained log files — log list and live tail
  both now work correctly
- Rewrite get_ui_schema() in config_generator to cover all ~80 fields
  across all 5 sections (server/basic/profile/launch/rcon) with proper
  toggle/select/number/password/tag-list/hidden widgets and labels;
  missions field is hidden (managed by Missions tab)
- Add formatSelectDisplay() to ConfigEditor so select fields show
  descriptive text (e.g. "0 - Never") instead of raw numbers in view mode
- Add ToggleDisplay for boolean fields (Enabled/Disabled with indicator dot)
- Add section tab labels and descriptions to ConfigEditor
- Add MissionList UX hints and dynamic Add/In Rotation button labels
- Add "hidden" to FieldSchema widget union type
- Update API.md, ARCHITECTURE.md, CLAUDE.md, FRONTEND.md, MODULES.md,
  THREADING.md to document log path fix and schema coverage
2026-04-18 15:56:04 +07:00

19 KiB
Raw Blame History

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 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, display_name, workshop_id }display_name/workshop_id from mod.cpp/meta.cpp
  • 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: /apihttp://localhost:8000, /wsws://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