- 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
66 KiB
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.
: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:
/* ── 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
: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
: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
: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
: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 inmono. 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 fromget_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]areneu-raised(default surface style).[Kill ⚠]isneu-raisedwith red text (not a red button — danger signals through color, not surface).- Buttons press to
neu-inseton 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/maxin 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 ifformat: "multiline")type: "integer"→ number input (with min/max from schema)type: "number"→ number input (step=0.1)type: "boolean"→ toggle switchenum: [...]→ dropdown selecttype: "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_versionfrom 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 Sectionreverts to the last saved state.
Preview:
Preview Configopens a modal with rendered config fromGET /servers/{id}/config/preview.- Each entry in the
label→contentdict is shown as a labeled code block. Monospaced. Download ▾gives individual file downloads fromGET /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
BanManagersyncs 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_versionon 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:
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:
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
// 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
// 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
// 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
// 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 (
Inter400/500/600,JetBrains Mono400) - 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-labelon 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
# 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):
/* 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)
// 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 |
— |