All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Drives the panel off the active game (GameSwitcher selection) + the GameProfile registry, so each game visibly differs (not just accent color). Sidebar nav: Rust = full (uMod plugins + plugin configs); Conan/Soulmask/Dune drop uMod + plugin-configs and relabel reset (Wipe World / World Reset / Deep Desert), Dune relabels Console->Broadcast (no RCON) and is Docker-managed. ServerView: management-model badge + game-appropriate panels (Rust deploy + Oxide; Dune Docker/BattleGroup-Sietches; Conan clans/thralls/avatars/purge; Soulmask main-client cluster) with HONEST EmptyStates where no backend data exists yet. Dashboard: per-game reset terminology + stat labels. No invented routes (all map to existing router entries); no fabricated data. Build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
/**
|
|
* gameProfiles.ts — Source of truth for per-game UI adaptation.
|
|
*
|
|
* Every game-specific label, terminology, Steam app ID, management model,
|
|
* stat field list, AND sidebar nav lives here. The dashboard, server cards,
|
|
* wipe manager, sidebar, 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.
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Nav structure — drives the per-game sidebar
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** A single sidebar nav item. route must be an existing panel route path. */
|
|
export interface NavItemDef {
|
|
label: string
|
|
route: string
|
|
icon: string
|
|
/** Permission key required to show this item (e.g. 'plugins.view'). Null = always visible. */
|
|
permission: string | null
|
|
}
|
|
|
|
/** A labelled section grouping nav items in the sidebar. */
|
|
export interface NavSection {
|
|
/** Section heading (eyebrow text). Empty string = no heading. */
|
|
label: string
|
|
items: NavItemDef[]
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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]
|
|
/**
|
|
* Per-game sidebar navigation. Ordered list of sections, each with items.
|
|
* Items MUST use only existing panel routes (see router/index.ts).
|
|
* The sidebar renders exactly these sections for the active game.
|
|
*/
|
|
nav: NavSection[]
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Registry
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared nav building blocks — reused across game nav definitions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const NAV_DASHBOARD: NavItemDef = { label: 'Dashboard', route: '/', icon: 'layout-dashboard', permission: null }
|
|
const NAV_SERVER: NavItemDef = { label: 'Server', route: '/server', icon: 'server', permission: 'server.view' }
|
|
const NAV_CONSOLE: NavItemDef = { label: 'Console', route: '/console', icon: 'terminal', permission: 'console.view' }
|
|
const NAV_PLAYERS: NavItemDef = { label: 'Players', route: '/players', icon: 'users', permission: 'players.view' }
|
|
const NAV_PLUGINS: NavItemDef = { label: 'Plugins (uMod)', route: '/plugins', icon: 'puzzle', permission: 'plugins.view' }
|
|
const NAV_FILES: NavItemDef = { label: 'File manager', route: '/files', icon: 'folder-open', permission: 'files.view' }
|
|
const NAV_PLUGIN_CONFIGS: NavItemDef = { label: 'Plugin configs', route: '/plugin-configs', icon: 'sliders', permission: null }
|
|
const NAV_SCHEDULES: NavItemDef = { label: 'Schedules', route: '/schedules', icon: 'calendar-clock', permission: 'schedules.view' }
|
|
const NAV_CHAT: NavItemDef = { label: 'Chat log', route: '/chat', icon: 'message-square', permission: 'chat.view' }
|
|
const NAV_ANALYTICS: NavItemDef = { label: 'Analytics', route: '/analytics', icon: 'bar-chart-3', permission: 'analytics.view' }
|
|
const NAV_ALERTS: NavItemDef = { label: 'Alerts', route: '/alerts', icon: 'triangle-alert', permission: 'alerts.view' }
|
|
const NAV_NOTIFICATIONS: NavItemDef = { label: 'Notifications', route: '/notifications', icon: 'bell', permission: 'notifications.view' }
|
|
const NAV_TEAM: NavItemDef = { label: 'Team', route: '/team', icon: 'users', permission: null }
|
|
const NAV_STORE: NavItemDef = { label: 'Store', route: '/store/config', icon: 'shopping-cart', permission: 'store.view' }
|
|
const NAV_MODULES: NavItemDef = { label: 'Modules', route: '/modules', icon: 'layers', permission: 'modules.view' }
|
|
const NAV_CHANGELOG: NavItemDef = { label: 'Changelog', route: '/changelog', icon: 'file-text', permission: 'changelog.view' }
|
|
const NAV_SETTINGS: NavItemDef = { label: 'Settings', route: '/settings', icon: 'settings', permission: 'settings.view' }
|
|
const NAV_MAPS: NavItemDef = { label: 'Maps', route: '/maps', icon: 'map', permission: 'maps.view' }
|
|
|
|
/** Full Rust / 'all' nav — superset used as fallback. */
|
|
const RUST_NAV: NavSection[] = [
|
|
{ label: '', items: [NAV_DASHBOARD] },
|
|
{
|
|
label: 'Server',
|
|
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_PLUGINS, NAV_FILES],
|
|
},
|
|
{ label: 'Plugin configs', items: [NAV_PLUGIN_CONFIGS] },
|
|
{
|
|
label: 'Operations',
|
|
items: [
|
|
{ label: 'Wipe', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
|
|
NAV_MAPS,
|
|
NAV_SCHEDULES,
|
|
],
|
|
},
|
|
{
|
|
label: 'Monitoring',
|
|
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS, NAV_NOTIFICATIONS],
|
|
},
|
|
{
|
|
label: 'Management',
|
|
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
|
|
},
|
|
]
|
|
|
|
export const GAME_PROFILES: Record<GameId, GameProfile> = {
|
|
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'],
|
|
nav: RUST_NAV,
|
|
},
|
|
|
|
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'],
|
|
nav: [
|
|
{ label: '', items: [NAV_DASHBOARD] },
|
|
{
|
|
label: 'Server',
|
|
// Conan: no uMod/Oxide; has RCON console, maps, players, files
|
|
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_FILES],
|
|
},
|
|
{
|
|
label: 'Operations',
|
|
items: [
|
|
{ label: 'Wipe World', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
|
|
NAV_MAPS,
|
|
NAV_SCHEDULES,
|
|
],
|
|
},
|
|
{
|
|
label: 'Monitoring',
|
|
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS, NAV_NOTIFICATIONS],
|
|
},
|
|
{
|
|
label: 'Management',
|
|
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
|
|
},
|
|
],
|
|
},
|
|
|
|
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'],
|
|
nav: [
|
|
{ label: '', items: [NAV_DASHBOARD] },
|
|
{
|
|
label: 'Server',
|
|
// Soulmask: no uMod/Oxide; has RCON+GM console, players, files
|
|
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_FILES],
|
|
},
|
|
{
|
|
label: 'Operations',
|
|
items: [
|
|
{ label: 'World Reset', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
|
|
NAV_SCHEDULES,
|
|
],
|
|
},
|
|
{
|
|
label: 'Monitoring',
|
|
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS],
|
|
},
|
|
{
|
|
label: 'Management',
|
|
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
|
|
},
|
|
],
|
|
},
|
|
|
|
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'],
|
|
nav: [
|
|
{ label: '', items: [NAV_DASHBOARD] },
|
|
{
|
|
label: 'Server',
|
|
// Dune: no RCON (uses RabbitMQ); label console "Broadcast"; no maps route; no plugins
|
|
items: [
|
|
NAV_SERVER,
|
|
{ label: 'Broadcast', route: '/console', icon: 'radio', permission: 'console.view' },
|
|
NAV_PLAYERS,
|
|
NAV_FILES,
|
|
],
|
|
},
|
|
{
|
|
label: 'Operations',
|
|
items: [
|
|
{ label: 'Deep Desert', route: '/wipes', icon: 'wind', permission: 'wipes.view' },
|
|
NAV_SCHEDULES,
|
|
],
|
|
},
|
|
{
|
|
label: 'Monitoring',
|
|
items: [NAV_ANALYTICS, NAV_ALERTS],
|
|
},
|
|
{
|
|
label: 'Management',
|
|
items: [NAV_TEAM, NAV_STORE, NAV_CHANGELOG, NAV_SETTINGS],
|
|
},
|
|
],
|
|
},
|
|
} 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<string, GameProfile>)[id] ?? GAME_PROFILES.rust
|
|
}
|