feat(server): instance-centric controls — real per-instance state + lifecycle
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 <noreply@anthropic.com>
This commit is contained in:
106
frontend/src/stores/instances.ts
Normal file
106
frontend/src/stores/instances.ts
Normal file
@@ -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<ManagedInstance[]>([])
|
||||
const currentId = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const acting = ref<LifecycleAction | null>(null)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const api = useApi()
|
||||
|
||||
const current = computed<ManagedInstance | null>(
|
||||
() => 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<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await api.get<FleetData>('/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<Record<string, unknown>> {
|
||||
const id = currentId.value
|
||||
if (!id) throw new Error('No instance selected')
|
||||
acting.value = action
|
||||
try {
|
||||
const res = await api.post<Record<string, unknown>>(`/instances/${id}/lifecycle`, { action })
|
||||
await fetchInstances()
|
||||
return res
|
||||
} finally {
|
||||
acting.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function rcon(command: string): Promise<Record<string, unknown>> {
|
||||
const id = currentId.value
|
||||
if (!id) throw new Error('No instance selected')
|
||||
return api.post<Record<string, unknown>>(`/instances/${id}/rcon`, { command })
|
||||
}
|
||||
|
||||
return {
|
||||
instances,
|
||||
currentId,
|
||||
current,
|
||||
loading,
|
||||
acting,
|
||||
error,
|
||||
fetchInstances,
|
||||
select,
|
||||
lifecycle,
|
||||
rcon,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user