# 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 | | 1024–1439px | 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) - **Error boundary**: if `jsonSchemaToFields()` encounters an unsupported or malformed schema type (deeply nested objects, unknown formats), render a fallback "This section uses an unsupported field type. Edit raw JSON." with a raw JSON editor instead of crashing the form **ConfigForm test plan** (critical — highest-risk component): - **Unit: `jsonSchemaToFields`** — test every supported type mapping (string→text, integer→number, boolean→toggle, enum→select, array→repeatable), test sensitive field masking, test unknown type → fallback descriptor with `type: "raw_json"` - **Unit: `jsonSchemaToFields`** — test malformed schema input (missing `properties`, nested `$ref`, `oneOf`/`anyOf`) → returns fallback descriptor, never throws - **Integration: `ConfigForm`** — render with a 2-section schema, verify field rendering, toggle a boolean, verify dirty state, submit and verify request payload - **Integration: 409 conflict** — after save, mock a 409 response, verify ConflictDialog appears with diff - **Integration: error boundary** — mount `ConfigForm` with a schema that has unsupported `type: "object"` nested 3 levels deep, verify raw JSON fallback renders instead of crash **`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) **Invalidation**: when `GET /games/{type}` returns a different `schema_version` than previously cached, TanStack Query invalidates all config schema queries for that game type. This prevents stale forms after an adapter update. | **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= │ ├── 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" | "raw_json"; 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" // Unknown types (nested objects, oneOf/anyOf, $ref) → type: "raw_json" // The form renders a textarea with JSON editing for these fields // rather than crashing or hiding the field silently } ``` ### Game Type Card Pattern ```typescript // Used in NewServerDialog and Dashboard setSelectedGame("arma3")} /> ``` Future adapters register themselves; the card list auto-populates from `GET /games`. ### Schema Cache Invalidation Adapter config schemas are cached with `staleTime: 30min`. When an adapter is updated and its `schema_version` changes, the frontend must not serve a stale schema. The invalidation pattern: ```typescript // When fetching game type info, compare schema_version function useGameTypeWithInvalidation(gameType: string) { const queryClient = useQueryClient(); return useQuery({ queryKey: ["gameType", gameType], staleTime: 30 * 60 * 1000, onSuccess: (data) => { const prevVersion = localStorage.getItem(`schema_version_${gameType}`); if (prevVersion && prevVersion !== data.schema_version) { // Adapter was updated — invalidate all config schema queries queryClient.invalidateQueries({ queryKey: ["configSchema", gameType] }); localStorage.setItem(`schema_version_${gameType}`, data.schema_version); } else if (!prevVersion) { localStorage.setItem(`schema_version_${gameType}`, data.schema_version); } }, }); } ``` --- ## 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 | | Malformed adapter schema | Raw JSON fallback in ConfigForm section ("This section uses an unsupported field type. Edit raw JSON.") | **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` | — |