Files
languard-servers-manager/FRONTEND.md
Tran G. (Revernomad) Khoa 624d7594e2 feat: multi-game adapter revamp, council protocol merge, and frontend design doc
- Revamp architecture for modular game server support (Arma 3 first, extensible)
- Merge ConfigSchema into ConfigGenerator per council decision (8→7 protocols)
- Add has_capability() method to GameAdapter protocol for explicit capability probing
- Add FRONTEND.md: production-grade dark neumorphism design with amber/orange palette
- Update all docs (ARCHITECTURE, MODULES, DATABASE, API, IMPLEMENTATION_PLAN, THREADING)
  to reflect protocol merge and multi-game adapter patterns
2026-04-16 17:05:04 +07:00

1343 lines
66 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.
# Languard Servers Manager — Frontend Design
## Purpose
A real-time game server management dashboard that gives admins instant visibility and control over their dedicated servers. The interface must feel like a mission control center — dense with live data, fast to react, and unambiguous in its state signals.
**Audience:** Server administrators managing game servers. Technical, task-oriented, frequently under time pressure (server crashed, player is cheating, need to restart now).
**Emotional tone:** Confident, precise, operational. Not playful, not corporate, not minimal-for-the-sake-of-it.
**Visual direction:** **Dark neumorphic command center** — near-black surfaces with soft extruded/inset shadows creating tactile depth, amber and orange accents cutting through like instrument panel lights, monospaced data glowing against dark backgrounds. The aesthetic of a physical control panel — buttons you can feel, displays that look back-lit, surfaces that have real mass.
**One thing the user should remember:** "I can see exactly what's happening and act on it immediately."
---
## Technology Stack
| Layer | Technology | Rationale |
|-------|-----------|-----------|
| Framework | **React 18** + **TypeScript 5** | Ecosystem, type safety, team familiarity |
| Build | **Vite 5** | Fast HMR, native ESM, minimal config |
| Routing | **React Router v6** | Standard, nested layouts |
| State (server) | **TanStack Query v5** | Server state cache, background refetch, optimistic updates |
| State (client) | **Zustand** | Minimal boilerplate, no providers, good for WS state |
| Forms | **React Hook Form** + **Zod** | Adapter-driven dynamic form generation from JSON Schema |
| Styling | **Tailwind CSS v3** + CSS variables | Utility-first with design tokens for theming |
| Charts | **Recharts** | Lightweight, responsive, sufficient for CPU/RAM/player time series |
| Icons | **Lucide React** | Consistent stroke style, tree-shakeable |
| HTTP | **Ky** (fetch wrapper) | Hooks, retry, typed responses |
| WS | Native WebSocket + custom hook | Minimal abstraction over browser API |
| Code quality | **ESLint** + **Prettier** + **tsc --noEmit** | Lint, format, type-check |
No UI component library (shadcn, MUI, etc.). Every component is purpose-built to the design system below.
---
## Design Tokens
### Color
Black, white, dark yellow, and orange. The palette mirrors a military-grade instrument panel — near-black surfaces, white text for readability, amber/yellow for data highlights and warnings, orange for primary actions and live indicators.
```css
:root {
/* ── Surfaces ──────────────────────────────────────────────
Dark neumorphism: surfaces are all near-black but subtly
differentiated by lightness. Neumorphic shadows (below)
create the illusion of physical depth — raised panels,
sunken inputs, extruded buttons.
*/
--color-base: #0d0d0d; /* deepest — page background */
--color-surface: #141414; /* panels, cards */
--color-elevated:#1a1a1a; /* raised panels, modals */
--color-hover: #1f1f1f; /* hover state on interactive surfaces */
/* ── Neumorphic shadow source ──────────────────────────────
Neumorphism requires two opposing shadows on the same
surface: a lighter shadow (simulating top-left light)
and a darker shadow (simulating bottom-right depth).
The source colors are derived from the surface itself.
*/
--neu-light: #1e1e1e; /* light shadow — 4-5% above surface */
--neu-dark: #0a0a0a; /* dark shadow — 4-5% below surface */
/* ── Text ───────────────────────────────────────────────── */
--color-text: #f5f5f5; /* primary — near-white */
--color-text-dim: #999999; /* secondary — timestamps, meta */
--color-text-muted: #555555; /* disabled, placeholders */
/* ── Accent — dark yellow / orange ──────────────────────── */
--color-accent: #d4940a; /* dark yellow — primary actions */
--color-accent-hover: #e5a61c; /* lighter on hover */
--color-accent-dim: #8b6210; /* muted accent for backgrounds */
--color-orange: #d45e0a; /* orange — secondary accent */
--color-orange-hover: #e56e1c; /* lighter on hover */
--color-orange-dim: #8b3e0a; /* muted orange for backgrounds */
/* ── Status ─────────────────────────────────────────────── */
--color-running: #d4940a; /* amber glow — server is live */
--color-stopped: #555555; /* gray — idle */
--color-starting: #d4940a; /* amber — transitioning (pulsing) */
--color-crashed: #cc3333; /* red — needs attention */
--color-error: #cc3333; /* red — error states */
/* ── Danger ─────────────────────────────────────────────── */
--color-danger: #cc3333; /* red — destructive actions */
--color-danger-hover: #dd4444;
/* ── Glow ────────────────────────────────────────────────
Status indicators use a subtle glow (box-shadow) to
simulate back-lit LEDs on a dark panel.
*/
--glow-amber: 0 0 8px 2px oklch(72% 0.15 80 / 0.4);
--glow-red: 0 0 8px 2px oklch(55% 0.20 25 / 0.4);
--glow-orange: 0 0 8px 2px oklch(65% 0.17 50 / 0.35);
/* ── Overlays ──────────────────────────────────────────── */
--color-overlay: oklch(8% 0 0 / 0.85); /* modal backdrop */
}
```
**Color rules:**
- **Black is the only background.** No white backgrounds anywhere. White is text only.
- **Amber (dark yellow) is the primary accent** — used for: primary buttons, active tabs, selected items, the "running" status dot, data highlight values.
- **Orange is the secondary accent** — used for: warning states, important metrics, secondary CTAs.
- **Red is reserved for danger** — crashed, error, destructive actions. Never decorative.
- **Gray is the neutral** — stopped status, disabled states, borders.
- No purple. No blue. No decorative gradients.
- Status dots glow via `box-shadow` — like back-lit LEDs on a physical panel.
### Neumorphic Surface Treatment
Dark neumorphism creates the illusion of physical depth through opposing light/dark shadows on near-black surfaces. Every interactive element signals its affordance through shadow direction:
```css
/* ── Raised (extruded) ────────────────────────────────────
Buttons, cards, metric tiles — things that sit above the surface.
Light shadow top-left, dark shadow bottom-right.
*/
.neu-raised {
background: var(--color-surface);
border-radius: var(--radius-md);
box-shadow:
4px 4px 8px var(--neu-dark),
-4px -4px 8px var(--neu-light);
}
/* ── Inset (sunken) ────────────────────────────────────────
Input fields, log viewer, search bars — things that go into the surface.
Dark shadow top-left, light shadow bottom-right.
*/
.neu-inset {
background: var(--color-base);
border-radius: var(--radius-sm);
box-shadow:
inset 3px 3px 6px var(--neu-dark),
inset -3px -3px 6px var(--neu-light);
}
/* ── Flat (flush) ──────────────────────────────────────────
Modal surfaces, overlays — flat on the surface, no shadow play.
*/
.neu-flat {
background: var(--color-elevated);
border-radius: var(--radius-lg);
box-shadow:
0 8px 32px oklch(0% 0 0 / 0.5);
}
/* ── Pressed ───────────────────────────────────────────────
Active/pressed button state — flips to inset.
*/
.neu-raised:active {
box-shadow:
inset 3px 3px 6px var(--neu-dark),
inset -3px -3px 6px var(--neu-light);
}
/* ── Accent raised ─────────────────────────────────────────
Primary action buttons with amber/orange fill.
Neumorphic shadows on a colored surface.
*/
.neu-raised-accent {
background: var(--color-accent);
color: #0d0d0d;
border-radius: var(--radius-md);
box-shadow:
4px 4px 8px var(--neu-dark),
-4px -4px 8px var(--neu-light),
0 0 12px oklch(72% 0.15 80 / 0.2); /* subtle amber glow */
}
/* ── Status LED ────────────────────────────────────────────
Status indicator dots with glow. Looks like a back-lit LED.
*/
.led-running {
background: var(--color-running);
border-radius: var(--radius-full);
box-shadow: var(--glow-amber);
}
.led-crashed {
background: var(--color-crashed);
border-radius: var(--radius-full);
box-shadow: var(--glow-red);
}
```
**Neumorphism rules for this project:**
- **Shadow intensity is proportional to interaction.** Buttons get full shadows. Decorative cards get lighter shadows. Flat info panels get none.
- **Never use borders with neumorphism** — shadows define edges, not lines. The only exception is focus rings for accessibility.
- **Inset for input, raised for output.** Form fields, log viewers, and search bars are sunken. Metric tiles, buttons, and cards are raised.
- **Pressed = inset.** When a raised button is clicked, it flips to inset shadows for tactile feedback.
- **Subtle, not extreme.** Shadow offsets are 3-4px, blur 6-8px. This is not the exaggerated neumorphism of 2020 — it's understated depth that makes the interface feel like physical hardware.
- **Glow replaces color fill for status.** Running servers don't just get a colored dot — they get a dot that glows, like a real LED indicator on a server rack.
### Typography
```css
:root {
/* Display — for page titles */
--font-display: "Space Grotesk", sans-serif;
/* Body — for most UI text */
--font-body: "Inter", sans-serif;
/* Mono — for logs, code, ports, PIDs, timestamps */
--font-mono: "JetBrains Mono", monospace;
/* Scale */
--text-xs: 0.75rem; /* 12px — badges, tags */
--text-sm: 0.8125rem; /* 13px — table cells, meta */
--text-base: 0.875rem; /* 14px — body, form labels */
--text-lg: 1rem; /* 16px — section headings */
--text-xl: 1.25rem; /* 20px — page titles */
--text-2xl: 1.5rem; /* 24px — hero metrics */
--text-3xl: 2rem; /* 32px — big status numbers */
/* Weight */
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
/* Line height */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75; /* for log blocks */
}
```
**Rules:**
- All data values (ports, PIDs, player counts, IPs) use `--font-mono`.
- Log viewer is entirely monospaced.
- Page titles use `--font-display`. Everything else uses `--font-body`.
- Never use font weight alone to differentiate — combine with size or color.
### Spacing
```css
:root {
--space-1: 0.25rem; /* 4px — tight internal gaps */
--space-2: 0.5rem; /* 8px — form field spacing */
--space-3: 0.75rem; /* 12px — compact padding */
--space-4: 1rem; /* 16px — standard padding */
--space-5: 1.5rem; /* 24px — section gaps */
--space-6: 2rem; /* 32px — page margins */
--space-8: 3rem; /* 48px — major separations */
}
```
**Rhythm:** 4px base unit. All spacing is a multiple of 4px. No arbitrary padding values.
### Borders, Shadows, Radii
```css
:root {
/* Radii — consistent, not excessive */
--radius-sm: 6px; /* inputs, badges, small buttons */
--radius-md: 10px; /* cards, panels */
--radius-lg: 14px; /* modals, overlays */
--radius-full: 9999px; /* status LEDs, pills */
/* Neumorphism handles edge definition — borders are rare.
Only use borders for: focus rings, table row separators,
and explicit dividers between sections. */
--border-subtle: 1px solid #222222;
--border-focus: 2px solid var(--color-accent);
}
```
**Note on neumorphic shadows + Tailwind:** The neumorphic shadow classes above cannot be expressed as single Tailwind utilities. Use CSS custom classes (`.neu-raised`, `.neu-inset`, etc.) defined in `globals.css` and reference them via `@apply` or direct class names in components. Tailwind utilities handle everything else (spacing, layout, typography, color).
### Motion
```css
:root {
--duration-fast: 100ms; /* hover states, toggles */
--duration-normal: 200ms; /* panel transitions, modals */
--duration-slow: 400ms; /* page transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.45, 0, 0.55, 1);
}
```
**Rules:**
- Motion is for state transitions, not decoration.
- Status changes (stopped→starting→running) use `--duration-normal` + `--ease-out`.
- No scroll-triggered animations. No parallax. No loading spinners with decorative motion.
- Log streaming has zero animation — lines appear instantly.
- Modals slide in from bottom or fade in. Never bounce, never spring.
---
## Layout Architecture
### Shell
```
┌─────────────────────────────────────────────────────────┐
│ LOGO Languard [user] [settings] │ ← 48px header
├────────┬────────────────────────────────────────────────┤
│ │ │
│ NAV │ CONTENT AREA │
│ │ │
│ ┌────┐ │ ┌──────────────────────────────────────────┐ │
│ │📊 │ │ │ │ │
│ ├────┤ │ │ │ │
│ │🖥️ │ │ │ Page Content │ │
│ ├────┤ │ │ │ │
│ │📋 │ │ │ │ │
│ ├────┤ │ │ │ │
│ │⚙️ │ │ │ │ │
│ ├────┤ │ └──────────────────────────────────────────┘ │
│ │🔧 │ │ │
│ └────┘ │ │
│ 56px │ │
├────────┴────────────────────────────────────────────────┤
│ Status bar: connected servers / WS status / version │ ← 28px footer
└─────────────────────────────────────────────────────────┘
```
- **Sidebar:** 56px collapsed (icons only), expands to 200px on hover/click. Icon + label for each section.
- **Header:** App name left, user dropdown right. Dark, fixed.
- **Footer:** Always visible — shows WebSocket connection status (connected/reconnecting/disconnected), number of running servers, app version. Critical for trust.
- **Content:** Scrollable main area. Min width 1024px.
### Navigation Structure
| Route | Icon | Label | Access |
|-------|------|-------|--------|
| `/` | LayoutDashboard | Dashboard | All |
| `/servers` | Server | Servers | All |
| `/servers/:id` | — | (Server Detail) | All |
| `/servers/:id/config` | — | (Server Config) | Admin |
| `/missions` | — | (via server detail) | Admin |
| `/mods` | PuzzlePiece | Mods | All (view), Admin (edit) |
| `/bans` | ShieldOff | Bans | All (view), Admin (edit) |
| `/users` | Users | Users | Admin |
| `/settings` | Settings | Settings | Admin |
### Responsive Breakpoints
| Breakpoint | Layout | Sidebar |
|-----------|--------|---------|
| ≥1440px | Full layout | Expanded by default |
| 10241439px | Full layout | Collapsed by default |
| <1024px | **Not supported** | — |
This is a server management tool, not a consumer app. Mobile is not a target. The minimum viewport is 1024px wide. Below that, show a "Please use a desktop browser" message.
---
## Page Designs
### 1. Login
Single centered card on dark background. No illustration, no hero, no marketing copy. The card itself is `neu-raised` — it appears to float above the base surface.
```
┌─────────────────────────────────┐
│ │
│ Languard │ ← display font, white
│ Server Manager │ ← dim, monospaced?
│ │
│ ┌─────────────────────────┐ │
│ │ Username │ │ ← neu-inset input
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ Password │ │ ← neu-inset input
│ └─────────────────────────┘ │
│ │
│ ╔═══════════════════════╗ │ ← neu-raised-accent
│ ║ Sign In ║ │ (amber fill)
│ ╚═══════════════════════╝ │
│ │
│ Error message area (red glow) │
│ │
└─────────────────────────────────┘
```
- No "remember me", no "forgot password" (single-host tool, admin manages users via CLI or initial setup).
- Failed login shows inline error with rate-limit countdown after 5 attempts.
- JWT stored in `localStorage`. On token expiry, redirect to login with a toast "Session expired".
### 2. Dashboard
A command-center overview. Not a marketing dashboard — dense, data-first. Neumorphic raised cards for metrics, inset panels for lists.
```
┌────────────────────────────────────────────────────────┐
│ Dashboard [Refresh] │
├────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │(raised) │ │(raised) │ │(raised) │ │(raised) │ │
│ │ 3 │ │ 2 │ │ 15 │ │ 34% │ │
│ │ Total │ │ Running │ │ Players │ │ Avg CPU │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SERVER STATUS (raised panel) │ │
│ │ │ │
│ │ 🟡 Main Server Arma 3 15/40 34%CPU │ │ ← amber glow LED
│ │ 🟡 Altis COOP Arma 3 0/32 12%CPU │ │
│ │ ⚫ Test Server Arma 3 — — │ │ ← gray, no glow
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ RECENT EVENTS (inset panel) │ │
│ │ │ │
│ │ 10:05 PlayerOne kicked (AFK) Main Server │ │
│ │ 10:02 Server started Altis COOP │ │
│ │ 09:58 Crashed (exit 1) 🔴 Test Server │ │ ← red glow LED
│ │ 09:45 Ban added: Cheater42 Main Server │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
```
- **Summary cards:** Neumorphic raised (`neu-raised`). Large amber number on top, white label below. Monospaced numbers. Running count glows amber. Crashed count glows red (if > 0).
- **Server status list:** Clickable rows → navigate to server detail. Status dot is an LED with glow (`led-running`). Player count in `mono`. CPU is color-coded: <50% white, 50-80% amber, >80% orange-red.
- **Recent events:** Inset panel (`neu-inset`). Last 10 events across all servers. Crash events show red LED.
### 3. Server List
Full CRUD list with filtering and bulk overview.
```
┌─────────────────────────────────────────────────────────────┐
│ Servers [+ New Server] [Game: All ▾] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Name │ Game │ Status │ Players │ CPU │ ⚙ │ │
│ ├───────────────┼────────┼─────────┼─────────┼─────┼───┤ │
│ │ Main Server │ Arma 3 │ 🟡 Run │ 15/40 │ 34% │ ⋮ │ │ ← amber LED
│ │ Altis COOP │ Arma 3 │ 🟡 Run │ 0/32 │ 12% │ ⋮ │ │
│ │ Test Server │ Arma 3 │ ⚫ Stop │ — │ — │ ⋮ │ │ ← gray, no glow
│ └───────────────┴────────┴─────────┴─────────┴─────┴───┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
- **Game filter dropdown:** Populated from `GET /games`. Inset input style (`neu-inset`).
- **[+ New Server]:** Raised accent button (`neu-raised-accent`). Amber fill, dark text.
- **Row actions (⋮ menu):** Start/Stop/Restart/Kill, Edit, Delete. Actions are context-aware — "Start" is disabled when running, "Stop" is disabled when stopped.
- **Sort:** Click column headers. Default sort: status (running first), then name.
- **Status column:** LED dot + short label. Amber glow = running, gray = stopped, red glow = crashed.
### 4. New Server Dialog
Modal overlay. Multi-step for clarity, not wizardry.
```
┌──────────────────────────────────────┐
│ New Server │
│ │
│ ── Step 1: Game Type ────────── │
│ │
│ ┌────────┐ ┌────────┐ │
│ │ Arma 3 │ │ + Add │ ← greyed │
│ │ ★ │ │ more │ if no │
│ └────────┘ └────────┘ adapters │
│ │
│ ── Step 2: Details ──────────── │
│ │
│ Server Name [ ]│
│ Description [ ]│
│ Executable [/path/to/exe ]│
│ Game Port [2302 ]│
│ RCon Port [2306 ]│
│ │
│ ── Step 3: Config ────────────── │
│ │
│ (Pre-filled from adapter defaults) │
│ Hostname [My Arma 3 Server ]│
│ Max Players [40 ]│
│ Admin Password [•••••• ]│
│ ... │
│ │
│ [Cancel] [Create Server] │
│ │
└──────────────────────────────────────┘
```
- **Game type selector:** Visual cards, not a dropdown. Each shows game name + icon. Only registered game types appear (from `GET /games`).
- **Config sections:** Dynamically rendered from `GET /games/{type}/config-schema`. Each section becomes a collapsible group. Fields generated from JSON Schema types (string → text input, integer → number input, boolean → toggle, enum → dropdown).
- **Sensitive fields:** Password inputs with show/hide toggle. Pre-generated values shown once in a dismissible callout after creation.
- **Port auto-fill:** Game port defaults from adapter's `get_default_game_port()`. RCon port from `get_default_rcon_port()`. User can override.
### 5. Server Detail
The primary operating surface. Tabbed layout with real-time data.
```
┌─────────────────────────────────────────────────────────────┐
│ ← Servers │ Main Server │
├─────────────────────────────────────────────────────────────┤
│ │
│ 🟡 RUNNING Arma 3 PID: 12345 Uptime: 2h 15m │ ← amber LED + glow
│ │
│ [Stop] [Restart] [Kill ⚠] │ ← action bar
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │(neu-raised)│ │(neu-raised)│ │(neu-raised)│ │
│ │ 15 │ │ 34.2% │ │ 1850 MB │ │
│ │ Players │ │ CPU │ │ RAM │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ [Overview] [Logs] [Players] [Config] [Missions] [Mods] │
│ ═════════ │
│ │
│ ── Tab Content Area ── │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Action bar:**
- `[Stop]` and `[Restart]` are `neu-raised` (default surface style).
- `[Kill ⚠]` is `neu-raised` with red text (not a red button — danger signals through color, not surface).
- Buttons press to `neu-inset` on click (tactile feedback).
- Action bar is fixed at top of content — always accessible.
**Metric cards:**
- Neumorphic raised (`neu-raised`). Updated in real-time via WebSocket (`type: "metrics"`).
- Player count shows `current/max` in mono font, accent color when > 0.
- CPU color-coded: <50% white, 50-80% amber, >80% orange.
**Tabs:** Rendered/hidden based on adapter capabilities:
| Tab | Condition | Source |
|-----|-----------|--------|
| Overview | Always | Server detail + recent events |
| Logs | Always | WebSocket `type: "log"` + `GET /servers/{id}/logs` |
| Players | `has_capability("remote_admin")` | WebSocket `type: "players"` + `GET /servers/{id}/players` |
| Config | Always (admin only) | `GET /servers/{id}/config` |
| Missions | `has_capability("mission_manager")` | `GET /servers/{id}/missions` |
| Mods | `has_capability("mod_manager")` | `GET /servers/{id}/mods` + `GET /mods` |
Tabs that the adapter doesn't support are simply not rendered. No disabled tabs, no "coming soon" badges.
### 5a. Server Detail — Logs Tab
```
┌─────────────────────────────────────────────────────────────┐
│ [Level: All ▾] [Search...] [Clear Logs ⚠] │
├─────────────────────────────────────────────────────────────┤
│ │
│ 10:05:23 INFO BattlEye Server: Initialized (v1.240) │
│ 10:05:24 INFO Player PlayerOne connected │
│ 10:05:30 WARN High ping detected: PlayerTwo (450ms) │
│ 10:06:01 ERROR BattlEye: RCon connection timeout │
│ 10:06:15 INFO Player PlayerOne disconnected │
│ │
│ ── streaming ── │
│ │
└─────────────────────────────────────────────────────────────┘
```
- **Entirely monospaced** (`--font-mono`). This is a log terminal, not a chat.
- **Container uses `neu-inset`** — the log area looks like a sunken display screen.
- **Level colors:** INFO = dim white, WARN = amber, ERROR = red. Color on the level tag only, not the whole line.
- **Streaming:** New lines prepend from top (newest-first) or append to bottom (oldest-first) — user toggle. Default: newest-first.
- **Virtualized list** for performance. Logs can grow to tens of thousands of lines.
- **Search** is client-side filter on loaded logs + server-side `?search=` for historical.
- **No auto-scroll lock** — if user scrolls up, stop auto-scroll. Resume when scrolled to bottom.
### 5b. Server Detail — Players Tab
```
┌─────────────────────────────────────────────────────────────┐
│ Players (15/40) [Say All] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┬──────────┬────────┬──────┬──────────────────┐│
│ │ Slot │ Name │ GUID │ Ping │ Actions ││
│ ├─────────┼──────────┼────────┼──────┼──────────────────┤│
│ │ 1 │ PlayerOne│ abc... │ 45ms │ [Kick] [Ban] ││
│ │ 2 │ PlayerTwo│ def... │ 450ms│ [Kick] [Ban] ││
│ │ ... │ │ │ │ ││
│ └─────────┴──────────┴────────┴──────┴──────────────────┘│
│ │
└─────────────────────────────────────────────────────────────┘
```
- **Real-time:** Table updates via WebSocket `type: "players"`. No manual refresh.
- **GUID** column shows truncated GUID (click to copy full).
- **Ping** column color-coded: <100 green, 100-300 default, >300 amber, >500 red.
- **Actions:** Kick opens a small popover for reason input. Ban opens a popover with reason + duration.
- **Say All button:** Opens a message input. Sends `POST /servers/{id}/remote-admin/say`.
- **Viewer role:** Sees the table, no action buttons.
### 5c. Server Detail — Config Tab
The most complex UI surface. Dynamic form generation from adapter's JSON Schema.
```
┌─────────────────────────────────────────────────────────────┐
│ Config [Preview Config] [Download ▾] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ▸ Server (modified*) │ │
│ │ ▾ Basic (neu-raised panel) │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ Hostname ╔═══════════════════════════════╗ │ │ │ ← neu-inset input
│ │ │ ║ My Arma 3 Server ║ │ │ │
│ │ │ ╚═══════════════════════════════╝ │ │ │
│ │ │ Max Players ╔════════════╗ Password ╔════╗ │ │ │
│ │ │ ║ 40 ║ ║••••║ │ │ │
│ │ │ ╚════════════╝ [👁] ╚════╝ │ │ │
│ │ │ BattlEye [● On ] Verify Sig [2 ▾] │ │ │ ← toggle = raised
│ │ └───────────────────────────────────────────────┘ │ │
│ │ ▸ Profile │ │
│ │ ▸ Launch │ │
│ │ ▸ RCon │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [Reset Section] ╔═══════════════╗ │
│ ║ Save Changes ║ ← neu-raised-accent │
│ ╚══════════════╝ (amber fill) │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Dynamic form generation:**
- Config sections are collapsible panels, one per adapter section.
- Each section's fields are rendered from `GET /games/{type}/config-schema` (JSON Schema).
- JSON Schema → form field mapping:
- `type: "string"` → text input (or textarea if `format: "multiline"`)
- `type: "integer"` → number input (with min/max from schema)
- `type: "number"` → number input (step=0.1)
- `type: "boolean"` → toggle switch
- `enum: [...]` → dropdown select
- `type: "array"` → repeatable field group (for motd_lines, etc.)
- Sensitive fields (from `get_sensitive_fields()`) → password input with show/hide
- Field descriptions from JSON Schema `description` → tooltip on hover.
**Optimistic locking:**
- Each section stores its `config_version` from the last read.
- On save (`PUT /servers/{id}/config/{section}`), sends the version.
- On 409 Conflict: shows a diff dialog with "Your changes" vs "Current server values", with options to override or merge.
**Dirty state:**
- Unsaved changes show `(modified*)` on the section header.
- Navigation away from dirty form triggers an unsaved-changes dialog.
- `Reset Section` reverts to the last saved state.
**Preview:**
- `Preview Config` opens a modal with rendered config from `GET /servers/{id}/config/preview`.
- Each entry in the `label→content` dict is shown as a labeled code block. Monospaced.
- `Download ▾` gives individual file downloads from `GET /servers/{id}/config/download/{filename}`.
### 5d. Server Detail — Missions Tab
```
┌─────────────────────────────────────────────────────────────┐
│ Missions [Upload .pbo] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ── Active Rotation ────────────────────────────────────── │
│ 1. MyMission.Altis (Regular) [↑] [↓] [✕] │
│ 2. ZeusOps.Altis (Veteran) [↑] [↓] [✕] │
│ │
│ ── Available Missions ─────────────────────────────────── │
│ ┌──────────────────────┬──────────┬────────┬──────────┐ │
│ │ Filename │ Terrain │ Size │ Actions │ │
│ ├──────────────────────┼──────────┼────────┼──────────┤ │
│ │ MyMission.Altis.pbo │ Altis │ 100 KB │ [+ Add] │ │
│ │ ZeusOps.Altis.pbo │ Altis │ 50 KB │ In rot. │ │
│ │ Training.Stratis.pbo│ Stratis │ 25 KB │ [+ Add] │ │
│ └──────────────────────┴──────────┴────────┴──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
- Only shown if `has_capability("mission_manager")`.
- **Upload:** Drag-and-drop zone + file picker. Extension validated from adapter's `MissionManager.file_extension`.
- **Rotation:** Ordered list. Reorder via drag-and-drop or arrow buttons. Difficulty dropdown per entry.
- **Add to rotation:** Button on each available mission. Moves it to rotation.
- **Remove from rotation:** Removes from rotation, mission stays on disk.
### 5e. Server Detail — Mods Tab
```
┌─────────────────────────────────────────────────────────────┐
│ Mods [Register Mod] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ── Active Mods ───────────────────────────────────────── │
│ ┌──────────┬─────────────────────┬────────────┬────────┐ │
│ │ Type │ Mod │ Workshop ID │ Remove │ │
│ ├──────────┼─────────────────────┼────────────┼────────┤ │
│ │ Client │ @CBA_A3 │ 450814997 │ [✕] │ │
│ │ Server │ @ACE_server │ — │ [✕] │ │
│ └──────────┴─────────────────────┴────────────┴────────┘ │
│ │
│ ── Available Mods ─────────────────────────────────────── │
│ ┌─────────────────────┬────────────┬──────────────────┐ │
│ │ @CBA_A3 │ 450814997 │ [+ Enable] │ │
│ │ @ACE │ 463289743 │ [+ Enable] │ │
│ └─────────────────────┴────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
- Only shown if `has_capability("mod_manager")`.
- **Client vs Server mod:** Toggle on each mod assignment. Determines `-mod=` vs `-serverMod=` in Arma 3.
- **Sort order:** Drag-and-drop reordering within each type. Affects load order.
- **Register Mod:** Opens a form to add a new mod folder path + metadata.
### 6. Bans Page
```
┌─────────────────────────────────────────────────────────────┐
│ Bans [Server: All ▾] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┬──────────┬───────────────┬────────┬──────────┐│
│ │ Server │ Name │ GUID │ Reason │ Expires ││
│ ├──────────┼──────────┼───────────────┼────────┼──────────┤│
│ │ Main │ Cheater42│ abc123... │ Hacking│ Perm ││
│ │ Altis │ Troll99 │ def456... │ Grief │ 2h left ││
│ └──────────┴──────────┴───────────────┴────────┴──────────┘│
│ │
│ [+ Add Ban Manually] │
│ │
└─────────────────────────────────────────────────────────────┘
```
- Cross-server view by default (all servers). Filterable by server.
- **Add Ban Manually:** Opens a form with GUID/Name/Reason/Duration/Server selector.
- **Unban:** Confirmation dialog → `DELETE /servers/{id}/bans/{ban_id}`.
- Adapter's `BanManager` syncs to game ban file automatically (no extra UI needed).
### 7. Users Page (Admin Only)
Simple CRUD table: username, role, created date. Add/delete users. Change role. No inline password editing (use `/auth/password`).
### 8. Settings Page (Admin Only)
- Change own password
- System info (version, uptime, supported games)
- API key management (future)
---
## Component Architecture
### Directory Structure
```
frontend/
├── index.html
├── vite.config.ts
├── tsconfig.json
├── package.json
├── tailwind.config.ts
├── postcss.config.js
├── public/
│ └── favicon.svg
└── src/
├── main.tsx # Mount point
├── App.tsx # Router + providers
├── vite-env.d.ts
├── styles/
│ ├── globals.css # CSS variables, resets, base styles
│ └── fonts.css # @font-face declarations
├── api/
│ ├── client.ts # Ky instance with base URL + auth interceptor
│ ├── auth.ts # Login, me, users
│ ├── servers.ts # Server CRUD, start/stop/kill
│ ├── config.ts # Config CRUD, preview, download
│ ├── players.ts # Players, kick, ban
│ ├── mods.ts # Mod registration, server mods
│ ├── missions.ts # Missions, upload, rotation
│ ├── bans.ts # Bans CRUD
│ ├── games.ts # Game type discovery, schemas, defaults
│ ├── logs.ts # Log queries
│ ├── metrics.ts # Metrics queries
│ └── events.ts # Event log queries
├── hooks/
│ ├── useWebSocket.ts # Connection management, reconnection, channel sub
│ ├── useAuth.ts # JWT state, login/logout, role check
│ ├── useServerStatus.ts # WS-driven status for a server (or all)
│ ├── useServerLogs.ts # WS-driven log stream
│ ├── useServerPlayers.ts # WS-driven player list
│ ├── useServerMetrics.ts # WS-driven metrics
│ ├── useConfigForm.ts # Dynamic form from JSON Schema + optimistic locking
│ ├── useCapability.ts # Check adapter.has_capability()
│ └── useConfirm.ts # Confirmation dialog hook
├── stores/
│ ├── authStore.ts # Zustand: token, user, role
│ └── wsStore.ts # Zustand: connection status, reconnect count
├── components/
│ ├── ui/ # Primitives (no business logic)
│ │ ├── Button.tsx # neu-raised / neu-raised-accent / neu-raised-danger
│ │ ├── Input.tsx # neu-inset
│ │ ├── Select.tsx # neu-inset + custom dropdown (neu-raised)
│ │ ├── Toggle.tsx # neu-raised (on) / neu-inset (off) with amber LED
│ │ ├── Badge.tsx
│ │ ├── Modal.tsx # neu-flat (drop shadow, no neumorphic play)
│ │ ├── Toast.tsx
│ │ ├── Tooltip.tsx
│ │ ├── ConfirmDialog.tsx
│ │ ├── EmptyState.tsx
│ │ ├── LoadingBar.tsx # amber bar, inset track
│ │ ├── CodeBlock.tsx # neu-inset
│ │ └── StatusLed.tsx # LED dot with glow (amber/red/gray)
│ │
│ ├── layout/
│ │ ├── AppShell.tsx # Header + sidebar + footer
│ │ ├── Sidebar.tsx
│ │ ├── Header.tsx
│ │ └── StatusBar.tsx # Footer: WS status, server count
│ │
│ ├── server/
│ │ ├── ServerCard.tsx # Dashboard summary card (neu-raised)
│ │ ├── ServerList.tsx # Table view (rows inside neu-raised container)
│ │ ├── ServerStatusDot.tsx # Status LED with glow
│ │ ├── ServerActionBar.tsx # Start/Stop/Restart/Kill buttons
│ │ ├── ServerMetricCard.tsx # Player/CPU/RAM card (neu-raised, amber numbers)
│ │ └── NewServerDialog.tsx # Multi-step creation modal (neu-flat)
│ │
│ ├── config/
│ │ ├── ConfigSection.tsx # Collapsible config panel (neu-raised)
│ │ ├── ConfigForm.tsx # Dynamic form from JSON Schema (neu-inset inputs)
│ │ ├── ConfigField.tsx # Single field renderer
│ │ ├── ConfigPreview.tsx # Modal with rendered config (neu-inset code blocks)
│ │ └── ConflictDialog.tsx # Optimistic locking 409 handler
│ │
│ ├── players/
│ │ ├── PlayerTable.tsx
│ │ ├── KickPopover.tsx
│ │ ├── BanPopover.tsx
│ │ └── SayAllDialog.tsx
│ │
│ ├── logs/
│ │ ├── LogViewer.tsx # Virtualized log stream
│ │ └── LogLine.tsx
│ │
│ ├── missions/
│ │ ├── MissionTable.tsx
│ │ ├── MissionUpload.tsx
│ │ ├── RotationList.tsx
│ │ └── RotationEntry.tsx
│ │
│ ├── mods/
│ │ ├── ModTable.tsx
│ │ ├── ServerModList.tsx
│ │ └── ModRegistrationDialog.tsx
│ │
│ ├── bans/
│ │ ├── BanTable.tsx
│ │ └── BanFormDialog.tsx
│ │
│ └── charts/
│ ├── MetricsChart.tsx # CPU + RAM time series
│ └── PlayerCountChart.tsx
├── pages/
│ ├── LoginPage.tsx
│ ├── DashboardPage.tsx
│ ├── ServerListPage.tsx
│ ├── ServerDetailPage.tsx
│ ├── ModsPage.tsx
│ ├── BansPage.tsx
│ ├── UsersPage.tsx
│ └── SettingsPage.tsx
└── lib/
├── jsonSchemaToFields.ts # JSON Schema → form field descriptors
├── formatUptime.ts # Seconds → "2h 15m"
├── formatBytes.ts # Bytes → human readable
├── timeAgo.ts # Timestamp → "5 minutes ago"
└── cn.ts # clsx + tailwind-merge utility
```
### Key Component Contracts
**`ConfigForm`** — the hardest component. Must handle:
- Dynamic rendering from JSON Schema (any game, any section)
- Sensitive field masking
- Dirty state tracking
- Optimistic locking (send `config_version` on save)
- 409 conflict resolution (show diff, allow override/merge)
- Validation errors from adapter (field-level, from Pydantic)
**`LogViewer`** — performance-critical:
- Virtualized rendering (react-window or similar)
- Newest-first or oldest-first toggle
- Level filter, text search
- Auto-scroll with manual-override detection
- Streams via WebSocket, paginated fallback via HTTP
**`ServerDetailPage`** — orchestrator:
- Resolves adapter from `server.game_type`
- Checks `has_capability()` to show/hide tabs
- Manages WS subscriptions for the server
- Handles status transitions in real-time (start → starting → running)
---
## State Management Strategy
### Server State (TanStack Query)
All API data uses TanStack Query with appropriate stale times:
| Query | staleTime | refetchInterval |
|-------|----------|-----------------|
| Server list | 30s | Background 30s (fallback for WS) |
| Server detail | 15s | — |
| Config sections | 5min | — (manual save) |
| Players | 0s | WS-driven |
| Logs | 0s | WS-driven |
| Metrics | 0s | WS-driven |
| Missions | 5min | — |
| Mods | 5min | — |
| Bans | 2min | — |
| Game types | 30min | — (rarely changes) |
| Config schema | 30min | — (tied to adapter version) |
**Optimistic updates** on:
- Server start/stop → immediately update status in cache, rollback on error
- Player kick/ban → immediately remove from player list cache
- Config save → immediately update config cache, rollback on 409 or error
### Client State (Zustand)
Only two stores — keep it minimal:
**`authStore`:**
```typescript
interface AuthState {
token: string | null;
user: { id: number; username: string; role: "admin" | "viewer" } | null;
setAuth: (token: string, user: User) => void;
logout: () => void;
isAdmin: () => boolean;
}
```
**`wsStore`:**
```typescript
interface WsState {
status: "connected" | "reconnecting" | "disconnected";
reconnectAttempts: number;
lastEventAt: number | null;
}
```
### URL State
Persisted in URL query params:
- Server list filters (`game_type`, `sort`)
- Log viewer filters (`level`, `search`)
- Ban list filters (`server`, `active_only`)
- Metrics chart time range (`from`, `to`, `resolution`)
---
## WebSocket Integration
### Connection Lifecycle
```
App Mount
├── Connect to ws://localhost:8000/ws/all?token=<JWT>
│ ├── On open: wsStore.status = "connected"
│ ├── On close: wsStore.status = "disconnected"
│ │ └── Auto-reconnect with exponential backoff (1s → 2s → 4s → ... → 30s)
│ ├── On error: wsStore.status = "reconnecting"
│ └── On message: dispatch to handlers
├── Subscribe to: ["status", "event"]
└── On server detail navigation:
└── Subscribe to: ["logs", "players", "metrics", "status", "event"]
for that specific server_id
```
### Message Dispatch
```typescript
// hooks/useWebSocket.ts
function handleWsMessage(msg: WsMessage) {
switch (msg.type) {
case "status":
queryClient.setQueryData(["servers", msg.server_id], (old) => ({
...old, status: msg.data.status, pid: msg.data.pid, started_at: msg.data.started_at,
}));
break;
case "log":
// Prepend to log cache (newest-first)
queryClient.setQueryData(["servers", msg.server_id, "logs"], (old) => ({
...old, logs: [msg.data, ...old.logs],
}));
break;
case "players":
queryClient.setQueryData(["servers", msg.server_id, "players"], msg.data.players);
break;
case "metrics":
queryClient.setQueryData(["servers", msg.server_id, "metrics"], (old) => ({
...old, current: msg.data,
}));
break;
case "event":
// Prepend to events cache + show toast for critical events
if (msg.data.event_type === "crashed") {
toast.error(`Server ${msg.server_id} crashed`);
}
break;
}
}
```
### Reconnection UX
- **StatusBar** shows connection state at all times: green dot = connected, amber spinner = reconnecting, red X = disconnected.
- During reconnection, all WS-driven data shows its last known value with a subtle "last updated X seconds ago" indicator.
- On reconnect, TanStack Query invalidates all server data to get fresh state.
- If JWT expires during WS connection, server closes the socket. Client detects 4xx on reconnect → redirect to login.
---
## Adapter-Aware UI Patterns
The frontend never hardcodes game-specific logic. It queries adapter metadata and renders accordingly.
### Capability Check Pattern
```typescript
// hooks/useCapability.ts
function useCapability(serverId: number) {
const { data: server } = useServerDetail(serverId);
const { data: gameType } = useGameType(server?.game_type);
return {
hasMissions: gameType?.capabilities.includes("mission_manager") ?? false,
hasMods: gameType?.capabilities.includes("mod_manager") ?? false,
hasRemoteAdmin: gameType?.capabilities.includes("remote_admin") ?? false,
hasBanManager: gameType?.capabilities.includes("ban_manager") ?? false,
};
}
// Usage in ServerDetailPage:
const { hasMissions, hasMods, hasRemoteAdmin } = useCapability(serverId);
// Only render tabs that the adapter supports
```
### Dynamic Config Form Pattern
```typescript
// lib/jsonSchemaToFields.ts
interface FieldDescriptor {
name: string;
label: string;
type: "text" | "number" | "boolean" | "select" | "textarea" | "password" | "array";
default?: unknown;
min?: number;
max?: number;
step?: number;
enum?: string[];
description?: string;
isSensitive?: boolean;
}
function jsonSchemaToFields(
schema: JsonSchema,
sensitiveFields: string[]
): FieldDescriptor[] {
// Walk schema.properties, map each to a FieldDescriptor
// Mark fields in sensitiveFields as type: "password"
}
```
### Game Type Card Pattern
```typescript
// Used in NewServerDialog and Dashboard
<GameTypeCard
gameType="arma3"
displayName="Arma 3"
capabilities={[...]}
selected={selectedGame === "arma3"}
onSelect={() => setSelectedGame("arma3")}
/>
```
Future adapters register themselves; the card list auto-populates from `GET /games`.
---
## Error Handling UX
| Scenario | UI Response |
|----------|-------------|
| API 401 | Redirect to login with "Session expired" toast |
| API 403 | "You don't have permission" inline message |
| API 404 | Empty state component with "Not found" |
| API 409 (config conflict) | ConflictDialog with diff + override/merge |
| API 422 (validation) | Field-level red highlights + error messages |
| API 429 (rate limit) | "Too many requests, try again in X seconds" toast |
| API 500 | "Server error" toast with retry button |
| WS disconnect | StatusBar indicator + stale data with timestamp |
| WS reconnect | Automatic; no user action needed |
| Server crashed | Toast notification + status dot turns red |
**No `alert()` calls.** All feedback uses toast (transient) or inline (persistent) patterns.
---
## Performance Budget
| Metric | Target |
|--------|--------|
| First Contentful Paint | < 1.5s |
| Time to Interactive | < 3s |
| Bundle size (gzipped) | < 250KB JS |
| CSS size (gzipped) | < 40KB |
| Log viewer render (1000 lines) | < 16ms per frame |
| WebSocket message processing | < 5ms per message |
**Techniques:**
- Vite code splitting per route (lazy `React.lazy` + `Suspense`)
- Virtual list for log viewer (react-window)
- TanStack Query deduplication (same query key = same request)
- Tailwind purge for minimal CSS
- Fonts: preload critical weights only (`Inter` 400/500/600, `JetBrains Mono` 400)
- No icon font — Lucide is tree-shaken SVGs
---
## Accessibility
- **Focus management:** Modals trap focus. Close on Escape. Return focus to trigger.
- **Keyboard navigation:** All interactive elements reachable via Tab. Action bar buttons have shortcuts (S=Start, X=Stop, R=Restart).
- **Color is not the only status indicator:** Status LEDs are paired with text labels. Logs use prefix tags (INFO, WARN, ERROR), not just color.
- **Contrast:** All text meets WCAG AA against its surface (4.5:1 minimum for body text).
- **Reduced motion:** Respect `prefers-reduced-motion` — disable transitions when active.
- **ARIA:** Live regions for WS status changes. `aria-label` on icon-only buttons.
---
## Toast System
Transient notifications. Stack in bottom-right.
| Type | Use | Duration |
|------|-----|----------|
| Success | Config saved, server started, mod enabled | 3s |
| Error | API errors, WS disconnect, validation failures | 7s (or dismiss) |
| Warning | Rate limited, high CPU, approaching memory limit | 5s |
| Info | Ban synced to file, mission uploaded | 3s |
Max 3 visible at once. Oldest dismissed automatically.
---
## Loading States
- **Route transitions:** Suspense boundary with a minimal loading bar at the top of the content area (not a full-page spinner).
- **Data loading:** Skeleton placeholders matching the layout shape of the loaded content. No spinners.
- **Actions:** Button shows inline spinner for duration of request. Disabled during request.
- **Initial load:** Dashboard shows skeleton cards → real data populates in place.
---
## Empty States
Every list/table has a designed empty state:
| Context | Empty State |
|---------|------------|
| No servers | "No servers yet" + [Create Server] button |
| No players | "No players connected" (dimmed, no action needed) |
| No missions | "No missions uploaded" + [Upload Mission] button |
| No mods registered | "No mods registered" + [Register Mod] button |
| No bans | "No active bans" (this is good news — no action needed) |
| No logs | "No log entries yet — server may not have started" |
---
## Build & Dev Commands
```bash
# Development
npm run dev # Vite dev server on :5173
# Production build
npm run build # tsc + vite build
npm run preview # Preview production build locally
# Quality
npm run lint # ESLint
npm run format # Prettier
npm run typecheck # tsc --noEmit
```
### globals.css (neumorphic classes)
The neumorphic shadow classes live in `src/styles/globals.css` and are referenced by Tailwind components via `@apply` or direct class names. They cannot be expressed as Tailwind utilities (multi-shadow syntax):
```css
/* src/styles/globals.css — neumorphic primitives */
.neu-raised {
background: var(--color-surface);
border: none;
border-radius: var(--radius-md);
box-shadow:
4px 4px 8px var(--neu-dark),
-4px -4px 8px var(--neu-light);
transition: box-shadow var(--duration-fast) var(--ease-out);
}
.neu-raised:active {
box-shadow:
inset 3px 3px 6px var(--neu-dark),
inset -3px -3px 6px var(--neu-light);
}
.neu-inset {
background: var(--color-base);
border: none;
border-radius: var(--radius-sm);
box-shadow:
inset 3px 3px 6px var(--neu-dark),
inset -3px -3px 6px var(--neu-light);
}
.neu-flat {
background: var(--color-elevated);
border: none;
border-radius: var(--radius-lg);
box-shadow: 0 8px 32px oklch(0% 0 0 / 0.5);
}
.neu-raised-accent {
background: var(--color-accent);
color: #0d0d0d;
border: none;
border-radius: var(--radius-md);
box-shadow:
4px 4px 8px var(--neu-dark),
-4px -4px 8px var(--neu-light),
0 0 12px oklch(72% 0.15 80 / 0.2);
transition: box-shadow var(--duration-fast) var(--ease-out);
}
.neu-raised-accent:hover {
background: var(--color-accent-hover);
}
.neu-raised-accent:active {
box-shadow:
inset 3px 3px 6px var(--neu-dark),
inset -3px -3px 6px var(--neu-light);
}
/* LED status indicators */
.led {
width: 8px;
height: 8px;
border-radius: 9999px;
display: inline-block;
}
.led-running {
composes: led;
background: var(--color-running);
box-shadow: var(--glow-amber);
}
.led-crashed {
composes: led;
background: var(--color-crashed);
box-shadow: var(--glow-red);
}
.led-stopped {
composes: led;
background: var(--color-stopped);
box-shadow: none;
}
```
### Vite Proxy (dev only)
```typescript
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': 'http://localhost:8000',
'/ws': {
target: 'ws://localhost:8000',
ws: true,
},
},
},
});
```
---
## Frontend ↔ Backend Contract Summary
| Frontend Action | API Call | WS Channel |
|----------------|----------|------------|
| View server list | `GET /servers` | — |
| View server detail | `GET /servers/{id}` | Subscribe to server |
| Start server | `POST /servers/{id}/start` | status |
| Stop server | `POST /servers/{id}/stop` | status |
| View live logs | `GET /servers/{id}/logs` (initial) | log |
| View player list | `GET /servers/{id}/players` (initial) | players |
| View metrics | `GET /servers/{id}/metrics` (initial) | metrics |
| Edit config | `GET/PUT /servers/{id}/config/{section}` | — |
| Preview config | `GET /servers/{id}/config/preview` | — |
| Upload mission | `POST /servers/{id}/missions/upload` | — |
| Manage rotation | `GET/PUT /servers/{id}/missions/rotation` | — |
| Enable mods | `PUT /servers/{id}/mods` | — |
| Kick player | `POST /servers/{id}/players/{slot}/kick` | players |
| Ban player | `POST /servers/{id}/players/{slot}/ban` | players |
| View events | `GET /servers/{id}/events` | event |
| Check capabilities | `GET /games/{type}` | — |
| Get config schema | `GET /games/{type}/config-schema` | — |