From c0b20f2f78bad1cb80ae21a59c6dbec60f12141e Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 18:37:53 -0400 Subject: [PATCH] =?UTF-8?q?feat(server):=20instance-centric=20controls=20?= =?UTF-8?q?=E2=80=94=20real=20per-instance=20state=20+=20lifecycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Server page now manages the selected GAME INSTANCE, not the legacy host connection. New instances store flattens the fleet and drives the command bridge. New 'Game instance' panel: real state badge (running/stopped/crashed/configured), uptime, host, and an instance selector when >1. Start/Stop/Restart/Refresh wired to POST /api/instances/:id/lifecycle — gated on the actual instance state (not host connectivity), with telemetry-only instances flagged. Works across all four games (state + lifecycle are game-agnostic). Co-Authored-By: Claude Fable 5 --- frontend/src/stores/instances.ts | 106 ++++++++++++++ frontend/src/views/admin/ServerView.vue | 187 ++++++++++++++++++++---- 2 files changed, 262 insertions(+), 31 deletions(-) create mode 100644 frontend/src/stores/instances.ts diff --git a/frontend/src/stores/instances.ts b/frontend/src/stores/instances.ts new file mode 100644 index 0000000..f81680a --- /dev/null +++ b/frontend/src/stores/instances.ts @@ -0,0 +1,106 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useApi } from '@/composables/useApi' +import type { FleetData, FleetInstance } from '@/stores/fleet' + +/** A game instance enriched with its host context, flattened from the fleet. */ +export interface ManagedInstance extends FleetInstance { + host_id: string + host_hostname: string + host_status: string +} + +type LifecycleAction = 'start' | 'stop' | 'restart' | 'status' | 'steam_update' + +/** + * Instance management — the Server page operates on a selected game instance + * (not the legacy single-server connection). Reads the fleet to enumerate + * instances and drives the per-instance command bridge + * (POST /api/instances/:id/lifecycle | /rcon). + */ +export const useInstancesStore = defineStore('instances', () => { + const instances = ref([]) + const currentId = ref(null) + const loading = ref(false) + const acting = ref(null) + const error = ref(null) + + const api = useApi() + + const current = computed( + () => instances.value.find((i) => i.id === currentId.value) ?? null, + ) + + /** Fetch the fleet and flatten its instances. Optionally prefer a game. */ + async function fetchInstances(preferGame?: string): Promise { + loading.value = true + error.value = null + try { + const data = await api.get('/fleet') + const flat: ManagedInstance[] = [] + for (const host of data.hosts) { + for (const inst of host.instances) { + flat.push({ + ...inst, + host_id: host.id, + host_hostname: host.hostname, + host_status: host.status, + }) + } + } + instances.value = flat + // Keep the current selection if it still exists; else prefer the active + // game, else the first instance. + if (!flat.some((i) => i.id === currentId.value)) { + const preferred = preferGame ? flat.find((i) => i.game === preferGame) : undefined + currentId.value = (preferred ?? flat[0])?.id ?? null + } + } catch (e) { + console.error('Failed to fetch instances:', e) + error.value = e instanceof Error ? e.message : 'Failed to load instances' + } finally { + loading.value = false + } + } + + function select(id: string): void { + currentId.value = id + } + + /** + * Send a lifecycle command to the current instance. Returns the agent's + * reply (which carries the new state); refetches so the list reflects it. + * Throws on failure so the view can toast. + */ + async function lifecycle(action: LifecycleAction): Promise> { + const id = currentId.value + if (!id) throw new Error('No instance selected') + acting.value = action + try { + const res = await api.post>(`/instances/${id}/lifecycle`, { action }) + await fetchInstances() + return res + } finally { + acting.value = null + } + } + + async function rcon(command: string): Promise> { + const id = currentId.value + if (!id) throw new Error('No instance selected') + return api.post>(`/instances/${id}/rcon`, { command }) + } + + return { + instances, + currentId, + current, + loading, + acting, + error, + fetchInstances, + select, + lifecycle, + rcon, + } +}) diff --git a/frontend/src/views/admin/ServerView.vue b/frontend/src/views/admin/ServerView.vue index c6f3ad4..b4e12c4 100644 --- a/frontend/src/views/admin/ServerView.vue +++ b/frontend/src/views/admin/ServerView.vue @@ -1,6 +1,7 @@