From 4ef5db5b0d84cca446904b7061b01b630c8c56fd Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 20:51:36 -0400 Subject: [PATCH] feat(panel): drive active game from deployed fleet instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 10 +++++ .../src/components/layout/DashboardLayout.vue | 15 ++++++- frontend/src/composables/useThemeGame.ts | 39 +++++++++++++++++-- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b777b3..ddc336a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Changed (Fleet-driven active game + signed-update CI fix — 2026-06-12) + +**Frontend — active game follows the deployed fleet:** +- The panel's active game (shell skin + sidebar nav + dashboard terminology) is now **derived from the deployed instances** instead of a localStorage-only toggle. `syncActiveGameFromFleet()` reads the distinct `game` values of the license's instances (`game_instances.game`, reported by the host agent): exactly one game deployed → the shell auto-skins to it; zero or multiple → `all` (neutral house skin). Wired into `DashboardLayout` (the always-mounted admin shell) via a watch on the fleet store. +- A manual GameSwitcher pick still wins — it persists to `cc-active-game` and suppresses auto-derive (operator intent beats the heuristic). Un-overridden panels keep tracking the fleet across sessions. +- **No backend/schema change:** a license's game(s) are the distinct games of its instances — the normalized source of truth. Deliberately did NOT add a `licenses.game` column (would duplicate `game_instances.game` and drift; see Lesson 20). + +**CI — signed host-agent build:** +- Fixed the `Sign artifacts (minisign)` step (`Error while loading the secret key file`): a minisign secret key is two lines and CI secret storage mangles the embedded newline. The job now base64-decodes the secret (single-line, mangling-proof) with auto-detect fallback to a raw key. `MINISIGN_SECRET_KEY` must be stored as `base64 < secret.key | tr -d '\n'`. Verified end-to-end: `agent-v2.0.0-alpha.8` Linux + Windows binaries validate against the agent's embedded public key; tampered byte rejected. + ### Added (Host-Agent v2 Consumer + SEO Meta — 2026-06-11) **Backend (NestJS):** diff --git a/frontend/src/components/layout/DashboardLayout.vue b/frontend/src/components/layout/DashboardLayout.vue index 67bd669..69fd86f 100644 --- a/frontend/src/components/layout/DashboardLayout.vue +++ b/frontend/src/components/layout/DashboardLayout.vue @@ -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 } diff --git a/frontend/src/composables/useThemeGame.ts b/frontend/src/composables/useThemeGame.ts index 8283bfe..33721f5 100644 --- a/frontend/src/composables/useThemeGame.ts +++ b/frontend/src/composables/useThemeGame.ts @@ -82,6 +82,40 @@ export function initThemeGame(): void { initialized = true } +/** Set the active game + its matching skin and paint . 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')