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 and apply the agent's * reply state OPTIMISTICALLY. The reply is authoritative for the action just * taken; the fleet DB only catches up on the next heartbeat (~10s), so an * immediate refetch would read a stale state and clobber the result. * 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 }) applyReplyState(id, res) return res } finally { acting.value = null } } /** Update an instance's state/uptime from a lifecycle/status reply. */ function applyReplyState(id: string, res: Record): void { if ((res as { status?: string }).status !== 'success') return const stateObj = (res as { state?: { state?: string } }).state const newState = stateObj?.state const inst = instances.value.find((i) => i.id === id) if (inst && typeof newState === 'string') { inst.state = newState const up = (res as { uptime_seconds?: number }).uptime_seconds inst.uptime_seconds = typeof up === 'number' ? up : newState === 'running' ? inst.uptime_seconds : 0 } } 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 }) } /** Read a config/text file from the current instance (jailed to its root). */ async function readFile(path: string): Promise { const id = currentId.value if (!id) throw new Error('No instance selected') const res = await api.get<{ content?: string }>( `/instances/${id}/file?path=${encodeURIComponent(path)}`, ) return res?.content ?? '' } /** Write a config/text file to the current instance. */ async function writeFile(path: string, content: string): Promise { const id = currentId.value if (!id) throw new Error('No instance selected') await api.put(`/instances/${id}/file`, { path, content }) } return { instances, currentId, current, loading, acting, error, fetchInstances, select, lifecycle, rcon, readFile, writeFile, } })