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 @@