feat(panel): per-game UI adaptation — sidebar, Server view, and dashboard transform by selected game
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>
This commit is contained in:
Vantz Stockwell
2026-06-11 08:37:03 -04:00
parent 8b84bba165
commit 23decd9b08
4 changed files with 349 additions and 88 deletions

View File

@@ -1,15 +1,18 @@
<script setup lang="ts">
/**
* DashboardLayout — game-aware app shell (Phase C redesign).
* Replaces the old Tailwind-only sidebar with the DS component set.
* Preserves: navSections, permission gating, super-admin section, logout, RouterView.
* Adds: GameSwitcher, Logo, DS NavItem, agent-health footer, topbar w/ search + theme toggle.
* Nav is driven by GAME_PROFILES[activeGame].nav — switching the GameSwitcher
* visibly changes nav items, labels, and sections per game.
* Preserves: permission gating, super-admin section, logout, mobile sidebar,
* GameSwitcher, agent-health footer, topbar.
*/
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useServerStore } from '@/stores/server'
import { useThemeGame } from '@/composables/useThemeGame'
import { useGameProfile } from '@/config/gameProfiles'
import type { NavSection, NavItemDef } from '@/config/gameProfiles'
import Logo from '@/components/ds/brand/Logo.vue'
import Badge from '@/components/ds/core/Badge.vue'
import StatusDot from '@/components/ds/core/StatusDot.vue'
@@ -53,61 +56,15 @@ function onActiveGame(val: string) {
setActiveGame(val as ActiveGame)
}
// ---- Navigation ----
type NavItemDef = { name: string; path: string; icon: string; permission: string | null }
type NavSection = { label: string; items: NavItemDef[] }
const navSections: NavSection[] = [
{
label: '',
items: [
{ name: 'Dashboard', path: '/', icon: 'layout-dashboard', permission: null },
],
},
{
label: 'Server',
items: [
{ name: 'Server', path: '/server', icon: 'server', permission: 'server.view' },
{ name: 'Console', path: '/console', icon: 'terminal', permission: 'console.view' },
{ name: 'Players', path: '/players', icon: 'users', permission: 'players.view' },
{ name: 'Plugins', path: '/plugins', icon: 'puzzle', permission: 'plugins.view' },
{ name: 'File manager', path: '/files', icon: 'folder-open', permission: 'files.view' },
],
},
{
label: 'Plugin configs',
items: [
{ name: 'Plugin configs', path: '/plugin-configs', icon: 'puzzle', permission: null },
],
},
{
label: 'Operations',
items: [
{ name: 'Wipe manager', path: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
{ name: 'Maps', path: '/maps', icon: 'map', permission: 'maps.view' },
{ name: 'Schedules', path: '/schedules', icon: 'calendar-clock', permission: 'schedules.view' },
],
},
{
label: 'Monitoring',
items: [
{ name: 'Chat log', path: '/chat', icon: 'message-square', permission: 'chat.view' },
{ name: 'Analytics', path: '/analytics', icon: 'bar-chart-3', permission: 'analytics.view' },
{ name: 'Alerts', path: '/alerts', icon: 'triangle-alert', permission: 'alerts.view' },
{ name: 'Notifications', path: '/notifications', icon: 'bell', permission: 'notifications.view' },
],
},
{
label: 'Management',
items: [
{ name: 'Team', path: '/team', icon: 'users', permission: null },
{ name: 'Store', path: '/store/config', icon: 'shopping-cart', permission: 'store.view' },
{ name: 'Modules', path: '/modules', icon: 'layers', permission: 'modules.view' },
{ name: 'Changelog', path: '/changelog', icon: 'file-text', permission: 'changelog.view' },
{ name: 'Settings', path: '/settings', icon: 'settings', permission: 'settings.view' },
],
},
]
// ---- Navigation — driven by the game profile registry ----
/**
* For 'all', fall back to rust (superset nav). For a specific game, look up
* its profile. noUncheckedIndexedAccess-safe: always ?? GAME_PROFILES.rust.
*/
const activeNavSections = computed<NavSection[]>(() => {
const game = activeGame.value === 'all' ? 'rust' : activeGame.value
return (useGameProfile(game)).nav
})
const adminNavItems = [
{ name: 'Admin home', path: '/admin', icon: 'shield' },
@@ -158,9 +115,6 @@ const agentName = computed(() => {
const serverName = computed(() => auth.license?.server_name ?? 'Your servers')
const userName = computed(() => auth.user?.username ?? '')
const themeIcon = computed(() => theme.value === 'dark' ? 'sun' : 'moon')
// ---- Import computed from vue (missed above) ----
import { computed } from 'vue'
</script>
<template>
@@ -197,20 +151,20 @@ import { computed } from 'vue'
/>
</div>
<!-- Navigation -->
<!-- Navigation sections driven by GAME_PROFILES[activeGame].nav -->
<nav class="side__nav">
<template v-for="section in navSections" :key="section.label">
<template v-for="section in activeNavSections" :key="section.label">
<template v-if="hasVisibleItems(section)">
<div class="side__sec">
<div v-if="section.label" class="t-eyebrow side__lbl">{{ section.label }}</div>
<NavItem
v-for="item in section.items"
v-show="canShowNavItem(item)"
:key="item.path"
:key="item.route"
:icon="item.icon"
:label="item.name"
:active="isActive(item.path)"
@click="navigate(item.path)"
:label="item.label"
:active="isActive(item.route)"
@click="navigate(item.route)"
/>
</div>
</template>

View File

@@ -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<GameId, GameProfile> = {
rust: {
label: 'Rust',
@@ -109,6 +184,7 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
group: 'Team',
},
statFields: ['Players', 'uMod', 'Wipe'],
nav: RUST_NAV,
},
conan: {
@@ -130,6 +206,30 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
},
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<GameId, GameProfile> = {
},
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<GameId, GameProfile> = {
},
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

View File

@@ -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') }
<div class="dash__kpis">
<StatCard
icon="users"
:label="profile.statFields[0] + ' online'"
:label="(profile.statFields[0] ?? 'Players') + ' online'"
:value="soloPlayers !== null ? String(soloPlayers) : '—'"
:unit="soloMaxPlayers !== null ? '/' + soloMaxPlayers : ''"
note="live via agent"
@@ -420,8 +425,8 @@ function navServer() { router.push('/server') }
</div>
</Panel>
<!-- Next wipe real schedule from wipeStore -->
<Panel title="Next wipe">
<!-- Next wipe/reset title follows game terminology -->
<Panel :title="'Next ' + profile.terminology.reset.toLowerCase()">
<div v-if="nextWipe" class="solo-wipe">
<div>
<div class="solo-wipe__type">{{ nextWipeType }}</div>
@@ -433,8 +438,8 @@ function navServer() { router.push('/server') }
<EmptyState
v-else
icon="calendar"
title="No wipe scheduled"
description="Configure automatic wipes in the wipe manager."
:title="'No ' + profile.terminology.reset.toLowerCase() + ' scheduled'"
:description="'Configure automatic ' + profile.terminology.reset.toLowerCase() + 's in the wipe manager.'"
>
<template #action>
<Button size="sm" variant="outline" icon="calendar-clock" @click="navWipes">

View File

@@ -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 () => {
<template>
<div class="sv">
<!-- Page head -->
<!-- Page head game-aware identity -->
<div class="sv__head">
<div class="sv__head-id">
<div class="sv__head-chip">
<Icon name="server" :size="20" :stroke-width="2" />
</div>
<div>
<div class="t-eyebrow">Server management</div>
<div class="t-eyebrow">{{ profile.label }} · Server management</div>
<h1 class="sv__title">Server</h1>
</div>
</div>
<Badge tone="neutral" :mono="true" class="sv__model-badge">{{ managementModelLabel }}</Badge>
</div>
<!-- Connection -->
@@ -444,8 +481,8 @@ onMounted(async () => {
</div>
</Panel>
<!-- Deploy Rust Server -->
<Panel title="Deploy Rust server" subtitle="One-click: SteamCMD, download, configure, start">
<!-- Deploy Server Rust only (SteamCMD path). Other games use docker-compose or external tooling. -->
<Panel v-if="isRust" title="Deploy Rust server" subtitle="One-click: SteamCMD, download, configure, start">
<template #title-append>
<Icon name="rocket" :size="15" />
</template>
@@ -560,8 +597,28 @@ onMounted(async () => {
</div>
</Panel>
<!-- Install Oxide / uMod -->
<Panel title="Install Oxide / uMod" subtitle="Required for all plugins including CorrosionCompanion">
<!-- Non-Rust: Docker-managed server note -->
<Panel
v-if="isDockerManaged"
:title="profile.label + ' server deployment'"
subtitle="Managed via Docker Compose"
>
<template #title-append>
<Icon name="box" :size="15" />
</template>
<EmptyState
icon="box"
title="Docker-managed deployment"
:description="profile.label + ' servers are managed via Docker Compose. Connect the companion agent on your Docker host to enable lifecycle management.'"
>
<template #action>
<Badge tone="info">Docker · Compose</Badge>
</template>
</EmptyState>
</Panel>
<!-- Install Oxide / uMod — Rust only -->
<Panel v-if="hasPluginSystem" title="Install Oxide / uMod" subtitle="Required for all plugins including CorrosionCompanion">
<template #title-append>
<Icon name="puzzle" :size="15" />
</template>
@@ -611,6 +668,79 @@ onMounted(async () => {
</div>
</Panel>
<!-- Workshop Mods info — Conan / Soulmask (Steam Workshop, no install step needed) -->
<Panel
v-else-if="profile.mods === 'workshop'"
:title="(profile.terminology.mods ?? 'Workshop Mods')"
:subtitle="profile.label + ' uses Steam Workshop — no manual install step required'"
>
<template #title-append>
<Icon name="layers" :size="15" />
</template>
<EmptyState
icon="layers"
:title="profile.label + ' mod management'"
:description="profile.label + ' loads mods directly from Steam Workshop. Manage your mod list in server config — no Corrosion install step needed.'"
/>
</Panel>
<!-- Conan Exiles special concepts (Clans / Thralls / Purge) -->
<Panel
v-if="profile.accent === 'conan'"
title="Conan Exiles concepts"
subtitle="Key admin mechanics for Conan Exiles servers"
>
<div class="sv__concept-grid">
<div class="sv__concept">
<Icon name="users" :size="16" />
<div>
<div class="sv__concept-label">Clans</div>
<div class="sv__concept-desc">Player factions. Clan management via in-game admin panel or RCON.</div>
</div>
</div>
<div class="sv__concept">
<Icon name="zap" :size="16" />
<div>
<div class="sv__concept-label">Thralls &amp; Avatars</div>
<div class="sv__concept-desc">Server-controlled NPCs and deity summons. Purge cycle managed via server settings.</div>
</div>
</div>
<div class="sv__concept">
<Icon name="shield" :size="16" />
<div>
<div class="sv__concept-label">Purge</div>
<div class="sv__concept-desc">NPC raid events targeting player bases. Enable / tune via server config.</div>
</div>
</div>
</div>
</Panel>
<!-- Soulmask clustering section -->
<Panel
v-if="profile.clustering === 'main-client'"
:title="clusterLabel"
subtitle="Main-client cluster topology for Soulmask"
>
<EmptyState
icon="network"
title="Cluster management coming soon"
:description="'Connect a ' + profile.label + ' host to manage the main-client cluster from this panel. Cluster configuration requires the companion agent.'"
/>
</Panel>
<!-- Dune BattleGroup / Sietches section -->
<Panel
v-if="profile.clustering === 'battlegroup'"
title="BattleGroups &amp; Sietches"
subtitle="Dune: Awakening server cluster topology"
>
<EmptyState
icon="map"
title="Sietch management requires a connected Dune host"
description="Connect the companion agent on your Dune: Awakening Docker host to manage BattleGroups and Sietches from this panel."
/>
</Panel>
<!-- Configuration -->
<Panel title="Configuration">
<template #actions>
@@ -708,8 +838,13 @@ onMounted(async () => {
</div>
<div class="sv__toggle-row">
<div class="sv__toggle-body">
<div class="sv__toggle-label">Auto-update on force wipe</div>
<div class="sv__toggle-sub">Update when Facepunch pushes</div>
<div class="sv__toggle-label">
<!-- Rust: "force wipe" is a Facepunch concept. Others: plain "auto-update" -->
{{ isRust ? 'Auto-update on force wipe' : 'Auto-update on patch' }}
</div>
<div class="sv__toggle-sub">
{{ isRust ? 'Update when Facepunch pushes' : 'Update when the developer pushes a patch' }}
</div>
</div>
<Switch
:model-value="server.config?.auto_update_on_force_wipe ?? false"
@@ -717,7 +852,8 @@ onMounted(async () => {
@update:model-value="toggleAutomation('auto_update_on_force_wipe')"
/>
</div>
<div class="sv__toggle-row">
<!-- Rust-only: force wipe eligibility is a Facepunch concept -->
<div v-if="isRust" class="sv__toggle-row">
<div class="sv__toggle-body">
<div class="sv__toggle-label">Force wipe eligible</div>
<div class="sv__toggle-sub">Server participates in force wipes</div>
@@ -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; }
</style>