diff --git a/frontend/src/components/layout/DashboardLayout.vue b/frontend/src/components/layout/DashboardLayout.vue index e2868a1..2ec0f3a 100644 --- a/frontend/src/components/layout/DashboardLayout.vue +++ b/frontend/src/components/layout/DashboardLayout.vue @@ -1,15 +1,18 @@ @@ -197,20 +151,20 @@ import { computed } from 'vue' /> - + - + {{ section.label }} diff --git a/frontend/src/config/gameProfiles.ts b/frontend/src/config/gameProfiles.ts index 7512692..107e95c 100644 --- a/frontend/src/config/gameProfiles.ts +++ b/frontend/src/config/gameProfiles.ts @@ -2,9 +2,9 @@ * 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. + * 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 @@ -15,6 +15,26 @@ * 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 // --------------------------------------------------------------------------- @@ -87,12 +107,67 @@ export interface GameProfile { * 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 = { rust: { label: 'Rust', @@ -109,6 +184,7 @@ export const GAME_PROFILES: Record = { group: 'Team', }, statFields: ['Players', 'uMod', 'Wipe'], + nav: RUST_NAV, }, conan: { @@ -130,6 +206,30 @@ export const GAME_PROFILES: Record = { }, 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: { @@ -151,6 +251,29 @@ export const GAME_PROFILES: Record = { }, 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: { @@ -170,6 +293,34 @@ export const GAME_PROFILES: Record = { }, 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 diff --git a/frontend/src/views/admin/DashboardView.vue b/frontend/src/views/admin/DashboardView.vue index 075b57e..67bbda5 100644 --- a/frontend/src/views/admin/DashboardView.vue +++ b/frontend/src/views/admin/DashboardView.vue @@ -25,6 +25,7 @@ import { useWipeStore } from '@/stores/wipe' import { useApi } from '@/composables/useApi' import { useWebSocket, type WebSocketMessage } from '@/composables/useWebSocket' import { useGameProfile } from '@/config/gameProfiles' +import { useThemeGame } from '@/composables/useThemeGame' import Panel from '@/components/ds/data/Panel.vue' import StatCard from '@/components/ds/data/StatCard.vue' import ConsoleLineDS from '@/components/ds/data/ConsoleLine.vue' @@ -44,10 +45,14 @@ const server = useServerStore() const wipeStore = useWipeStore() const router = useRouter() const api = useApi() +const { activeGame } = useThemeGame() -// Today every license is Rust. When the backend adds a `game` field to the -// license or server_config, pass it here: useGameProfile(server.config?.game ?? 'rust') -const profile = computed(() => useGameProfile('rust')) +// Profile follows the GameSwitcher selection. 'all' falls back to rust (neutral house skin). +// When the backend adds a `game` field on licenses, swap activeGame for server.config?.game. +const profile = computed(() => { + const game = activeGame.value === 'all' ? 'rust' : activeGame.value + return useGameProfile(game) +}) // --------------------------------------------------------------------------- // Derived server state — all real, no fallbacks to fabricated values @@ -298,7 +303,7 @@ function navServer() { router.push('/server') } - - + + {{ nextWipeType }} @@ -433,8 +438,8 @@ function navServer() { router.push('/server') } diff --git a/frontend/src/views/admin/ServerView.vue b/frontend/src/views/admin/ServerView.vue index a754c31..8e83c7a 100644 --- a/frontend/src/views/admin/ServerView.vue +++ b/frontend/src/views/admin/ServerView.vue @@ -3,6 +3,8 @@ import { ref, computed, onMounted } from 'vue' import { useServerStore } from '@/stores/server' import { useAuthStore } from '@/stores/auth' import { useToastStore } from '@/stores/toast' +import { useThemeGame } from '@/composables/useThemeGame' +import { useGameProfile } from '@/config/gameProfiles' import type { DeploymentConfig, DeploymentStatus } from '@/types' import { useWebSocket } from '@/composables/useWebSocket' import Panel from '@/components/ds/data/Panel.vue' @@ -11,6 +13,7 @@ import Badge from '@/components/ds/core/Badge.vue' import StatusDot from '@/components/ds/core/StatusDot.vue' import Icon from '@/components/ds/core/Icon.vue' import Alert from '@/components/ds/feedback/Alert.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' import Input from '@/components/ds/forms/Input.vue' import Switch from '@/components/ds/forms/Switch.vue' import Tabs from '@/components/ds/navigation/Tabs.vue' @@ -18,6 +21,39 @@ import Tabs from '@/components/ds/navigation/Tabs.vue' const server = useServerStore() const auth = useAuthStore() const toast = useToastStore() +const { activeGame } = useThemeGame() + +// Profile follows the GameSwitcher. 'all' defaults to rust (neutral house skin). +const profile = computed(() => { + const game = activeGame.value === 'all' ? 'rust' : activeGame.value + return useGameProfile(game) +}) + +// Game-specific derived flags +const isRust = computed(() => profile.value.mods === 'umod') +const hasPluginSystem = computed(() => profile.value.mods === 'umod') +const isDockerManaged = computed(() => profile.value.managementModel === 'docker-compose') + +// Management model human label for the identity badge +const managementModelLabel = computed(() => { + const m = profile.value.managementModel + const c = profile.value.console + if (m === 'docker-compose') { + return profile.value.clustering === 'battlegroup' ? 'Docker · BattleGroup' : 'Docker · Compose' + } + if (c === 'rcon+ingame') return 'Process · RCON + In-game' + if (c === 'rcon+gm') return 'Process · RCON + GM' + return 'Process · RCON' +}) + +// Clustering section label per game +const clusterLabel = computed(() => { + const cl = profile.value.clustering + if (cl === 'battlegroup') return 'BattleGroups & Sietches' + if (cl === 'main-client') return 'Cluster' + if (cl === 'character-transfer') return 'Clans & Character Transfer' + return '' +}) const editMode = ref(false) const saving = ref(false) @@ -278,17 +314,18 @@ onMounted(async () => { - + - Server management + {{ profile.label }} · Server management Server + {{ managementModelLabel }} @@ -444,8 +481,8 @@ onMounted(async () => { - - + + @@ -560,8 +597,28 @@ onMounted(async () => { - - + + + + + + + + Docker · Compose + + + + + + @@ -611,6 +668,79 @@ onMounted(async () => { + + + + + + + + + + + + + + + Clans + Player factions. Clan management via in-game admin panel or RCON. + + + + + + Thralls & Avatars + Server-controlled NPCs and deity summons. Purge cycle managed via server settings. + + + + + + Purge + NPC raid events targeting player bases. Enable / tune via server config. + + + + + + + + + + + + + + + @@ -708,8 +838,13 @@ onMounted(async () => { - Auto-update on force wipe - Update when Facepunch pushes + + + {{ isRust ? 'Auto-update on force wipe' : 'Auto-update on patch' }} + + + {{ isRust ? 'Update when Facepunch pushes' : 'Update when the developer pushes a patch' }} + { @update:model-value="toggleAutomation('auto_update_on_force_wipe')" /> - + + Force wipe eligible Server participates in force wipes @@ -848,4 +984,19 @@ onMounted(async () => { .sv__toggle-row:first-child { padding-top: 0; } .sv__toggle-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); } .sv__toggle-sub { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; } + +/* Management model badge in page head */ +.sv__model-badge { align-self: center; } + +/* Game concept cards (Conan Exiles special features) */ +.sv__concept-grid { display: flex; flex-direction: column; gap: 14px; } +.sv__concept { + display: flex; align-items: flex-start; gap: 12px; + padding: 12px 14px; + background: var(--surface-raised); border-radius: var(--radius-md); + box-shadow: var(--ring-default); + color: var(--accent); +} +.sv__concept-label { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); margin-bottom: 2px; } +.sv__concept-desc { font-size: var(--text-xs); color: var(--text-tertiary); line-height: 1.5; }