feat(panel): drive active game from deployed fleet instances
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:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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)
|
### Added (Host-Agent v2 Consumer + SEO Meta — 2026-06-11)
|
||||||
|
|
||||||
**Backend (NestJS):**
|
**Backend (NestJS):**
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
* Preserves: permission gating, super-admin section, logout, mobile sidebar,
|
* Preserves: permission gating, super-admin section, logout, mobile sidebar,
|
||||||
* GameSwitcher, agent-health footer, topbar.
|
* 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 { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useServerStore } from '@/stores/server'
|
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 { useGameProfile } from '@/config/gameProfiles'
|
||||||
import type { NavSection, NavItemDef } from '@/config/gameProfiles'
|
import type { NavSection, NavItemDef } from '@/config/gameProfiles'
|
||||||
import { safeDate } from '@/utils/formatters'
|
import { safeDate } from '@/utils/formatters'
|
||||||
@@ -31,8 +32,18 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const server = useServerStore()
|
const server = useServerStore()
|
||||||
|
const fleet = useFleetStore()
|
||||||
const { theme, activeGame, setActiveGame, toggleTheme } = useThemeGame()
|
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 ----
|
// ---- Mobile sidebar ----
|
||||||
const sidebarOpen = ref(false)
|
const sidebarOpen = ref(false)
|
||||||
function closeSidebar() { sidebarOpen.value = false }
|
function closeSidebar() { sidebarOpen.value = false }
|
||||||
|
|||||||
@@ -82,6 +82,40 @@ export function initThemeGame(): void {
|
|||||||
initialized = true
|
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() {
|
export function useThemeGame() {
|
||||||
function setTheme(t: Theme): void {
|
function setTheme(t: Theme): void {
|
||||||
theme.value = t
|
theme.value = t
|
||||||
@@ -94,10 +128,9 @@ export function useThemeGame() {
|
|||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
function setActiveGame(g: ActiveGame): void {
|
function setActiveGame(g: ActiveGame): void {
|
||||||
activeGame.value = g
|
// Manual pick: persist so it overrides fleet auto-derive from now on.
|
||||||
localStorage.setItem(ACTIVE_GAME_KEY, g)
|
|
||||||
// 'all' uses the neutral house skin (rust/oxide); a game re-skins to itself.
|
// '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 {
|
function toggleTheme(): void {
|
||||||
setTheme(theme.value === 'dark' ? 'light' : 'dark')
|
setTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||||
|
|||||||
Reference in New Issue
Block a user