fix(server): apply lifecycle reply state optimistically (heartbeat lag)
All checks were successful
CI / backend-types (push) Successful in 9s
CI / frontend-build (push) Successful in 16s
CI / agent-tests (push) Successful in 42s
CI / integration (push) Successful in 21s

The agent reply is authoritative for the action just taken; the fleet
DB only updates on the next heartbeat (~10s), so the immediate refetch
read a stale state and reverted the UI (Start -> still Stopped). Now
apply the reply's state/uptime directly to the instance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 18:41:19 -04:00
parent c0b20f2f78
commit e897a4802f

View File

@@ -68,8 +68,10 @@ export const useInstancesStore = defineStore('instances', () => {
} }
/** /**
* Send a lifecycle command to the current instance. Returns the agent's * Send a lifecycle command to the current instance and apply the agent's
* reply (which carries the new state); refetches so the list reflects it. * 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. * Throws on failure so the view can toast.
*/ */
async function lifecycle(action: LifecycleAction): Promise<Record<string, unknown>> { async function lifecycle(action: LifecycleAction): Promise<Record<string, unknown>> {
@@ -78,13 +80,26 @@ export const useInstancesStore = defineStore('instances', () => {
acting.value = action acting.value = action
try { try {
const res = await api.post<Record<string, unknown>>(`/instances/${id}/lifecycle`, { action }) const res = await api.post<Record<string, unknown>>(`/instances/${id}/lifecycle`, { action })
await fetchInstances() applyReplyState(id, res)
return res return res
} finally { } finally {
acting.value = null acting.value = null
} }
} }
/** Update an instance's state/uptime from a lifecycle/status reply. */
function applyReplyState(id: string, res: Record<string, unknown>): 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<Record<string, unknown>> { async function rcon(command: string): Promise<Record<string, unknown>> {
const id = currentId.value const id = currentId.value
if (!id) throw new Error('No instance selected') if (!id) throw new Error('No instance selected')