feat(panel): drive active game from deployed fleet instances
All checks were successful
CI / backend-types (push) Successful in 8s
CI / frontend-build (push) Successful in 16s
CI / agent-tests (push) Successful in 40s
CI / integration (push) Successful in 23s

The shell skin / sidebar nav / dashboard terminology now follow the games
actually deployed (game_instances.game, agent-reported) instead of a
localStorage-only toggle. syncActiveGameFromFleet() derives: one game ->
auto-skin to it; zero/multiple -> 'all' neutral. A manual GameSwitcher pick
persists and overrides the heuristic. Wired into DashboardLayout via a watch
on the fleet store.

No schema change: a license's games are the distinct games of its instances
(the normalized source of truth) — deliberately not duplicating into a
licenses.game column that would drift (Lesson 20).

Build-green (vue-tsc) + boots clean in-browser (0 console errors, theming
initializes). Authenticated auto-derive confirms live on next instance deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 20:51:36 -04:00
parent bb71763714
commit 4ef5db5b0d
3 changed files with 59 additions and 5 deletions

View File

@@ -6,11 +6,12 @@
* Preserves: permission gating, super-admin section, logout, mobile sidebar,
* GameSwitcher, agent-health footer, topbar.
*/
import { ref, computed } from 'vue'
import { ref, computed, watch, onMounted } 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 { useFleetStore } from '@/stores/fleet'
import { useThemeGame, syncActiveGameFromFleet } from '@/composables/useThemeGame'
import { useGameProfile } from '@/config/gameProfiles'
import type { NavSection, NavItemDef } from '@/config/gameProfiles'
import { safeDate } from '@/utils/formatters'
@@ -31,8 +32,18 @@ const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const server = useServerStore()
const fleet = useFleetStore()
const { theme, activeGame, setActiveGame, toggleTheme } = useThemeGame()
// ---- Active game follows the deployed fleet (instances are the source of
// truth for which game a license runs). A manual GameSwitcher pick overrides
// this; see syncActiveGameFromFleet. ----
const deployedGames = computed(() =>
fleet.hosts.flatMap((h) => h.instances.map((i) => i.game)),
)
watch(deployedGames, (games) => syncActiveGameFromFleet(games), { immediate: true })
onMounted(() => { if (!fleet.hosts.length) void fleet.fetchFleet() })
// ---- Mobile sidebar ----
const sidebarOpen = ref(false)
function closeSidebar() { sidebarOpen.value = false }

View File

@@ -82,6 +82,40 @@ export function initThemeGame(): void {
initialized = true
}
/** Set the active game + its matching skin and paint <html>. Shared by the
* manual switcher (persist=true → becomes an operator override) and the
* fleet-derived sync (persist=false → stays "auto", re-tracks the fleet). */
function applyActiveGame(g: ActiveGame, persist: boolean): void {
activeGame.value = g
game.value = g === 'all' ? 'rust' : g
if (persist) {
localStorage.setItem(ACTIVE_GAME_KEY, g)
localStorage.setItem(GAME_KEY, game.value)
}
apply()
}
/**
* Derive the active game from the deployed fleet — the game instances are the
* source of truth for which game(s) a license runs (game_instances.game, set by
* the host agent). Exactly one game deployed → skin the shell to it; zero or
* multiple → 'all' (neutral house skin).
*
* NO-OP when the operator has a manual pick stored (cc-active-game present): an
* explicit GameSwitcher choice always beats the heuristic. Does not persist, so
* an un-overridden panel keeps tracking the fleet across sessions and as
* instances are added/removed. Safe to call repeatedly (idempotent).
*/
export function syncActiveGameFromFleet(games: readonly string[]): void {
if (localStorage.getItem(ACTIVE_GAME_KEY)) return // operator override wins
const distinct = Array.from(
new Set(games.filter((g): g is Game => (VALID_GAMES as string[]).includes(g))),
)
const derived: ActiveGame = distinct.length === 1 ? distinct[0]! : 'all'
if (derived === activeGame.value) return
applyActiveGame(derived, false)
}
export function useThemeGame() {
function setTheme(t: Theme): void {
theme.value = t
@@ -94,10 +128,9 @@ export function useThemeGame() {
apply()
}
function setActiveGame(g: ActiveGame): void {
activeGame.value = g
localStorage.setItem(ACTIVE_GAME_KEY, g)
// Manual pick: persist so it overrides fleet auto-derive from now on.
// 'all' uses the neutral house skin (rust/oxide); a game re-skins to itself.
setGame(g === 'all' ? 'rust' : g)
applyActiveGame(g, true)
}
function toggleTheme(): void {
setTheme(theme.value === 'dark' ? 'light' : 'dark')