diff --git a/frontend/src/components/ds/data/PlayersChart.vue b/frontend/src/components/ds/data/PlayersChart.vue index 834cb9e..8ee8c3c 100644 --- a/frontend/src/components/ds/data/PlayersChart.vue +++ b/frontend/src/components/ds/data/PlayersChart.vue @@ -1,10 +1,15 @@ - + + + + + + + + Awaiting telemetry + Player data will appear once the server connects and reports stats + + + diff --git a/frontend/src/config/gameProfiles.ts b/frontend/src/config/gameProfiles.ts new file mode 100644 index 0000000..7512692 --- /dev/null +++ b/frontend/src/config/gameProfiles.ts @@ -0,0 +1,191 @@ +/** + * gameProfiles.ts — Source of truth for per-game UI adaptation. + * + * Every game-specific label, terminology, Steam app ID, management model, + * and stat field list lives here. The dashboard, server cards, wipe manager, + * and any future multi-game surface should key off this registry — never + * hard-code game-specific strings in components. + * + * Backend status: the backend has NO game field on licenses yet. Today every + * license is implicitly Rust. This registry is ready: when the backend adds a + * `game` column to `licenses` (or `server_config`), the frontend only needs to + * read that field and call `useGameProfile(id)` — no component changes required. + * + * To add a new game: add a GameId union member and a corresponding entry in + * GAME_PROFILES. Nothing else changes. + */ + +// --------------------------------------------------------------------------- +// Union types — exhaustive, never widen to string +// --------------------------------------------------------------------------- + +/** Every supported game identifier. */ +export type GameId = 'rust' | 'conan' | 'soulmask' | 'dune' + +/** How the server process is managed. */ +export type ManagementModel = 'process+rcon' | 'docker-compose' + +/** Mod ecosystem the game uses. */ +export type ModSystem = 'umod' | 'workshop' | 'none' + +/** Primary console / remote-admin interface. */ +export type ConsoleType = 'rcon' | 'rcon+ingame' | 'rcon+gm' | 'rabbitmq' + +/** + * How a "reset" is performed — each value maps to a distinct wipe code path. + * Pipe-delimited strings intentionally encode composite operations. + */ +export type ResetModel = + | 'map-bp-wipe' + | 'wipe-world-structures+decay' + | 'worlddb-delete+decay' + | 'deep-desert-coriolis-seed' + +/** Cross-server or character-sharing mechanism. */ +export type ClusteringModel = 'none' | 'character-transfer' | 'main-client' | 'battlegroup' + +// --------------------------------------------------------------------------- +// GameProfile shape +// --------------------------------------------------------------------------- + +export interface GameTerminology { + /** What the operator calls a reset / wipe. */ + reset: string + /** What the operator calls plugins / mods (null if no mod system). */ + mods: string | null + /** What the operator calls a player group / faction. */ + group: string +} + +export interface GamePorts { + game: number + query: number + rcon: number + cluster?: number +} + +export interface GameProfile { + /** Human-readable game name. */ + label: string + /** CSS design-token key — maps to data-game attr and --accent token. */ + accent: string + managementModel: ManagementModel + steamAppId: number | { windows: number; linux: number } + /** Default ports (game-specific defaults; operator can override). */ + ports?: GamePorts + mods: ModSystem + console: ConsoleType + resetModel: ResetModel + clustering: ClusteringModel + /** Available map names, if the game ships with named maps. */ + maps?: string[] + terminology: GameTerminology + /** Notable game-specific mechanics that affect server administration. */ + special?: string[] + /** + * Stat field labels shown on server cards and the dashboard. + * First entry is always Players; subsequent entries are game-specific. + */ + statFields: [string, string, string] +} + +// --------------------------------------------------------------------------- +// Registry +// --------------------------------------------------------------------------- + +export const GAME_PROFILES: Record = { + rust: { + label: 'Rust', + accent: 'rust', + managementModel: 'process+rcon', + steamAppId: 258550, + mods: 'umod', + console: 'rcon', + resetModel: 'map-bp-wipe', + clustering: 'none', + terminology: { + reset: 'Wipe', + mods: 'Plugins', + group: 'Team', + }, + statFields: ['Players', 'uMod', 'Wipe'], + }, + + conan: { + label: 'Conan Exiles', + accent: 'conan', + managementModel: 'process+rcon', + steamAppId: 443030, + ports: { game: 7777, query: 27015, rcon: 25575 }, + mods: 'workshop', + console: 'rcon+ingame', + // Player progress persists across world wipes — only structures are cleared. + resetModel: 'wipe-world-structures+decay', + clustering: 'character-transfer', + maps: ['Exiled Lands', 'Isle of Siptah'], + terminology: { + reset: 'Wipe World', + mods: 'Mods', + group: 'Clan', + }, + special: ['Clans', 'Thralls', 'Avatars', 'Purge', 'PvP windows'], + statFields: ['Players', 'Clans', 'Purge'], + }, + + soulmask: { + label: 'Soulmask', + accent: 'soulmask', + managementModel: 'process+rcon', + // Different Steam app IDs per OS (uncommon — store this explicitly). + steamAppId: { windows: 3017310, linux: 3017300 }, + ports: { game: 8777, query: 27015, rcon: 19000, cluster: 20000 }, + mods: 'workshop', + console: 'rcon+gm', + resetModel: 'worlddb-delete+decay', + clustering: 'main-client', + maps: ['Cloud Mist Forest', 'Shifting Sands'], + terminology: { + reset: 'World Reset', + mods: 'Workshop Mods', + group: 'Tribe', + }, + special: ['Cluster', 'Tribes'], + statFields: ['Players', 'Tribe', 'Mask'], + }, + + dune: { + label: 'Dune: Awakening', + accent: 'dune', + managementModel: 'docker-compose', + steamAppId: 4754530, + mods: 'none', + // Dune uses RabbitMQ for its admin messaging — not a standard RCON port. + console: 'rabbitmq', + resetModel: 'deep-desert-coriolis-seed', + clustering: 'battlegroup', + terminology: { + reset: 'Deep Desert reset', + mods: null, + group: 'Guild', + }, + special: ['Sietches', 'Deep Desert', 'Bases', 'Landsraad'], + statFields: ['Players', 'Sietches', 'Control'], + }, +} as const + +// --------------------------------------------------------------------------- +// Helper +// --------------------------------------------------------------------------- + +/** + * Returns the GameProfile for the given id, falling back to Rust if the id is + * unknown (forward-compatibility: unknown games show Rust defaults until their + * profile is added). + * + * @example + * const profile = useGameProfile('rust') + * console.log(profile.terminology.reset) // 'Wipe' + */ +export function useGameProfile(id: string): GameProfile { + return (GAME_PROFILES as Record)[id] ?? GAME_PROFILES.rust +} diff --git a/frontend/src/views/admin/DashboardView.vue b/frontend/src/views/admin/DashboardView.vue index a59023a..075b57e 100644 --- a/frontend/src/views/admin/DashboardView.vue +++ b/frontend/src/views/admin/DashboardView.vue @@ -1,136 +1,92 @@ - - - + + + - {{ scopeLabel }} - {{ fleetTitle }} - - - - Export - Deploy server - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - No servers match the current filter. - - - - - - - - - - - Live - - - {{ f.msg }} - - - - - - - - - - {{ w.name }} - {{ w.when }} - - {{ w.label }} - - - + Dashboard + Server cockpit + + + + Set up server + + + - - + + + - + + + + - {{ soloName }} + {{ soloName ?? 'Server' }} {{ soloStatusLabel }} - {{ soloRegion }} · {{ soloIp }} - · up {{ soloUptime }} + {{ soloIp }} + No IP registered + · up {{ soloUptime }} - Restart Stop Console - + - - - - + + + + - + + - - - + + + + Loading telemetry… + - + - Live + {{ isConnected ? 'Live' : 'Disconnected' }} + - {{ f.msg }} + + {{ line.msg }} + + + Waiting for output — try sending a command below + Console offline — server is not connected + + > { :mono="true" size="sm" placeholder="say, kick, ban, oxide.reload …" + :disabled="!isConnected" style="flex: 1" @keydown.enter="sendConsoleCommand" /> - Send + Send + - + + - - - + + + + + Resource metrics arrive via the companion agent heartbeat. + + Agent setup + - - - - Add - - - - - {{ p.name }} - {{ p.ver }} - - - - - - - + - + - Thu · 18:00 UTC - representative — configure in wipe manager + {{ nextWipeType }} + {{ nextWipeLabel }} + {{ nextWipe.schedule_name }} Edit + + + + Open wipe manager + + + + + + + + + + Dashboard + Server cockpit + + + + Loading server data… + + + @@ -431,23 +482,6 @@ onMounted(() => { .dash__grid { display: grid; grid-template-columns: minmax(0, 1fr) 366px; gap: 16px; align-items: start; } .dash__col { display: flex; flex-direction: column; gap: 16px; min-width: 0; } -/* ---------- Servers list ---------- */ -.server__list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 14px; } -.server__empty { grid-column: 1 / -1; font-size: var(--text-sm); color: var(--text-muted); text-align: center; padding: 24px; } - -/* ---------- Live feed ---------- */ -.feed { padding: 8px 2px; max-height: 340px; overflow-y: auto; } -.feed--solo { max-height: 230px; } - -/* ---------- Upcoming wipes ---------- */ -.wipes { display: flex; flex-direction: column; gap: 4px; } -.wipe { display: flex; align-items: center; gap: 11px; padding: 9px 6px; border-radius: var(--radius-md); } -.wipe:hover { background: var(--surface-hover); } -.wipe__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; box-shadow: 0 0 10px -1px var(--accent-glow); } -.wipe__body { flex: 1; min-width: 0; } -.wipe__name { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.wipe__when { font-family: var(--font-mono); font-size: 11px; color: var(--text-tertiary); margin-top: 1px; } - /* ---------- Solo identity header ---------- */ .solo-id { display: flex; align-items: center; gap: 13px; } .solo-id__chip { @@ -463,6 +497,21 @@ onMounted(() => { } .solo-id__meta { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 3px; } +/* ---------- Chart loading ---------- */ +.chart-loading { + display: flex; align-items: center; justify-content: center; + height: 196px; font-size: var(--text-sm); color: var(--text-muted); +} + +/* ---------- Console feed ---------- */ +.feed { padding: 8px 2px; max-height: 340px; overflow-y: auto; } +.feed--solo { max-height: 230px; } +.feed__empty { + display: flex; align-items: center; justify-content: center; + height: 100px; font-size: var(--text-sm); color: var(--text-muted); + font-style: italic; +} + /* ---------- Console bar ---------- */ .console-bar { display: flex; align-items: center; gap: 9px; padding: 11px 12px; @@ -472,24 +521,28 @@ onMounted(() => { /* ---------- Resources ---------- */ .solo-meters { display: flex; flex-direction: column; gap: 13px; } - -/* ---------- Plugin list ---------- */ -.plugs { display: flex; flex-direction: column; } -.plug { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); } -.plug:last-child { border-bottom: 0; } -.plug__id { display: flex; align-items: baseline; gap: 9px; min-width: 0; } -.plug__name { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); } -.plug__ver { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); } +.meters-note { + margin-top: 14px; font-size: var(--text-xs); color: var(--text-muted); + border-top: 1px solid var(--border-subtle); padding-top: 12px; + display: flex; align-items: center; gap: 10px; flex-wrap: wrap; +} +.meters-cta { margin-left: auto; } /* ---------- Next wipe ---------- */ -.solo-wipe { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.solo-wipe__when { font-family: var(--font-mono); font-size: var(--text-base); font-weight: 600; color: var(--text-primary); } -.solo-wipe__sub { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; } +.solo-wipe { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } +.solo-wipe__type { font-size: var(--text-xs); color: var(--text-tertiary); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tracking-wider); margin-bottom: 3px; } +.solo-wipe__when { font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); } +.solo-wipe__name { font-size: var(--text-xs); color: var(--text-muted); margin-top: 2px; } + +/* ---------- Loading ---------- */ +.dash-loading { + display: flex; align-items: center; justify-content: center; + padding: 60px; font-size: var(--text-sm); color: var(--text-muted); +} /* ---------- Responsive ---------- */ @media (max-width: 1180px) { .dash__grid { grid-template-columns: 1fr; } - .server__list { grid-template-columns: 1fr; } } @media (max-width: 768px) { .dash__kpis { grid-template-columns: repeat(2, 1fr); } diff --git a/frontend/src/views/admin/_dashboardMock.ts b/frontend/src/views/admin/_dashboardMock.ts deleted file mode 100644 index 8c16b9b..0000000 --- a/frontend/src/views/admin/_dashboardMock.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Dashboard mock data — representative placeholder pending multi-instance backend. - * Current backend is single-server-per-license; the fleet view is a forward-looking - * surface that will bind to a multi-instance API. All data here is static and clearly - * labeled so it is never confused for real tenant data. - * - * Per-game fields are isolated by game key — a Dune row NEVER receives a Rust field - * like `umod`, and vice-versa. See GAME_FIELDS for the row-field contract. - */ - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type ServerStatus = 'online' | 'offline' | 'starting' | 'wiping' | 'updating' -export type GameKey = 'rust' | 'dune' | 'conan' | 'soulmask' - -export interface MockServer { - game: GameKey - gameIcon: string - name: string - region: string - map: string - version: string - status: ServerStatus - players: { cur: number; max: number } - cpu?: number - ram?: number - ramSub?: string - ip: string - // Rust-only - umod?: string - wipe?: string - // Dune-only - sietches?: string - control?: string - // Conan-only - clans?: string - purge?: string - // Soulmask-only - tribe?: string - mask?: string -} - -export interface MockFeedLine { - time: string - level: 'cmd' | 'chat' | 'info' | 'warn' | 'error' | 'connect' | 'kill' - who?: string - msg: string -} - -export interface MockWipe { - game: GameKey - name: string - when: string - tone: 'wiping' | 'starting' | 'warn' | 'online' - label: string -} - -export interface StatItem { - label: string - value: string | number -} - -// --------------------------------------------------------------------------- -// Fleet server roster -// --------------------------------------------------------------------------- - -export const MOCK_SERVERS: MockServer[] = [ - { - game: 'rust', gameIcon: 'box', name: 'Main · 2x Vanilla', region: 'US-East', - map: 'Procedural 4500', version: 'v2024.12', status: 'online', - players: { cur: 142, max: 200 }, cpu: 41, ram: 68, ramSub: '5.4 / 8 GB', - ip: '89.142.0.7:28015', umod: '14', wipe: '2d', - }, - { - game: 'rust', gameIcon: 'box', name: '5x Modded · Build', region: 'US-East', - map: 'Barren 3000', version: 'v2024.12', status: 'online', - players: { cur: 38, max: 100 }, ip: '89.142.0.7:28017', umod: '27', wipe: '2d', - }, - { - game: 'rust', gameIcon: 'box', name: 'Hardcore · Solo/Duo', region: 'US-West', - map: 'Procedural 3500', version: 'v2024.12', status: 'wiping', - players: { cur: 0, max: 80 }, cpu: 8, ram: 30, ramSub: '2.4 / 8 GB', - ip: '74.91.3.2:28015', umod: '9', wipe: 'now', - }, - { - game: 'dune', gameIcon: 'sun', name: 'Arrakis · Hardcore', region: 'EU-Frankfurt', - map: 'Hagga Basin', version: 'v0.9.4', status: 'online', - players: { cur: 54, max: 60 }, cpu: 63, ram: 74, ramSub: '11.8 / 16 GB', - ip: '51.83.12.4:7777', sietches: '3', control: '62%', - }, - { - game: 'dune', gameIcon: 'sun', name: 'Deep Desert · PvP', region: 'EU-Frankfurt', - map: 'Deep Desert', version: 'v0.9.4', status: 'starting', - players: { cur: 0, max: 40 }, ip: '51.83.12.4:7779', sietches: '0', control: '—', - }, - { - game: 'dune', gameIcon: 'sun', name: 'Sietch · Roleplay', region: 'SG-Singapore', - map: 'Hagga Basin', version: 'v0.9.4', status: 'offline', - players: { cur: 0, max: 50 }, ip: '139.99.4.8:7777', sietches: '5', control: '—', - }, - { - game: 'conan', gameIcon: 'swords', name: 'Exiled Lands · PvP-C', region: 'US-East', - map: 'Exiled Lands', version: 'v3.0.5', status: 'online', - players: { cur: 32, max: 40 }, cpu: 48, ram: 60, ramSub: '9.6 / 16 GB', - ip: '89.142.0.7:7777', clans: '7', purge: 'Tier 4', - }, - { - game: 'soulmask', gameIcon: 'drama', name: 'Sienna Plateau · PvE', region: 'EU-Frankfurt', - map: 'Sienna Plateau', version: 'v1.4', status: 'online', - players: { cur: 18, max: 30 }, cpu: 35, ram: 52, ramSub: '8.3 / 16 GB', - ip: '51.83.12.4:8777', tribe: '4', mask: 'Jaguar', - }, -] - -// --------------------------------------------------------------------------- -// Per-game stat field sets — never share slots across games -// --------------------------------------------------------------------------- - -function pl(s: MockServer): string { - return `${s.players.cur} / ${s.players.max}` -} - -export const GAME_FIELDS: Record StatItem[]> = { - rust: (s) => [{ label: 'Players', value: pl(s) }, { label: 'uMod', value: s.umod ?? '—' }, { label: 'Wipe', value: s.wipe ?? '—' }], - dune: (s) => [{ label: 'Players', value: pl(s) }, { label: 'Sietches', value: s.sietches ?? '—' }, { label: 'Control', value: s.control ?? '—' }], - conan: (s) => [{ label: 'Players', value: pl(s) }, { label: 'Clans', value: s.clans ?? '—' }, { label: 'Purge', value: s.purge ?? '—' }], - soulmask: (s) => [{ label: 'Players', value: pl(s) }, { label: 'Tribe', value: s.tribe ?? '—' }, { label: 'Mask', value: s.mask ?? '—' }], -} - -export function buildStats(s: MockServer): StatItem[] { - const fn = GAME_FIELDS[s.game] ?? GAME_FIELDS.rust - return fn(s) -} - -// --------------------------------------------------------------------------- -// Live activity feed -// --------------------------------------------------------------------------- - -export const MOCK_FEED: MockFeedLine[] = [ - { time: '18:42:07', level: 'connect', who: 'ShadowFox', msg: 'connected — 89.142.0.7' }, - { time: '18:41:55', level: 'cmd', who: 'admin', msg: 'oxide.grant group default kits.use' }, - { time: '18:41:30', level: 'kill', who: 'ironMaiden', msg: 'was killed by Scorpion (AK-47, 84m)' }, - { time: '18:40:12', level: 'warn', msg: '5x Modded agent reconnected — telemetry resuming' }, - { time: '18:39:48', level: 'chat', who: 'BlightWalker:', msg: 'anyone selling sulfur?' }, - { time: '18:38:02', level: 'info', msg: 'RaidableBases spawned Tier-3 at G14' }, - { time: '18:36:51', level: 'connect', who: 'Vex', msg: 'connected — 51.83.12.4' }, -] - -// --------------------------------------------------------------------------- -// Upcoming wipes -// --------------------------------------------------------------------------- - -export const MOCK_WIPES: MockWipe[] = [ - { game: 'rust', name: 'Main · 2x Vanilla', when: 'Thu · 18:00 UTC', tone: 'wiping', label: 'Map + BP' }, - { game: 'rust', name: '5x Modded · Build', when: 'Thu · 18:00 UTC', tone: 'wiping', label: 'Map only' }, - { game: 'dune', name: 'Deep Desert · PvP', when: 'Sun · 12:00 UTC', tone: 'starting', label: 'Deep Desert' }, -]