feat(fleet): remove host — DELETE /api/fleet/hosts/:id + Fleet card action
Self-service host removal. DELETE /api/fleet/hosts/:id (server.manage, tenant-guarded): refuses while the host is 'connected' (409 — a live agent re-registers on its next heartbeat, stop it first), deletes the host's game_instances explicitly (FK is SET NULL, would otherwise orphan them; instance_stats cascade), and clears the legacy server_connections row if it was the license's last host. Fleet view: offline host cards get a Remove button with inline confirm + toast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,11 +77,22 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a host and its instances. Throws on failure (e.g. 409 when the host
|
||||
* is still online) so the caller can surface the message; refetches on
|
||||
* success.
|
||||
*/
|
||||
async function removeHost(hostId: string): Promise<void> {
|
||||
await api.del(`/fleet/hosts/${hostId}`)
|
||||
await fetchFleet()
|
||||
}
|
||||
|
||||
return {
|
||||
hosts,
|
||||
summary,
|
||||
loading,
|
||||
error,
|
||||
fetchFleet,
|
||||
removeHost,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
*
|
||||
* No fabricated data. All nulls render as '—' via safeFixed/safeDate.
|
||||
*/
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { onMounted, computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useFleetStore } from '@/stores/fleet'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import type { FleetHost } from '@/stores/fleet'
|
||||
import { safeFixed, safeDate } from '@/utils/formatters'
|
||||
import Panel from '@/components/ds/data/Panel.vue'
|
||||
@@ -30,6 +31,7 @@ import Icon from '@/components/ds/core/Icon.vue'
|
||||
// ---------------------------------------------------------------------------
|
||||
const fleet = useFleetStore()
|
||||
const router = useRouter()
|
||||
const toast = useToastStore()
|
||||
|
||||
onMounted(() => {
|
||||
fleet.fetchFleet()
|
||||
@@ -40,6 +42,25 @@ onMounted(() => {
|
||||
// ---------------------------------------------------------------------------
|
||||
const hasHosts = computed(() => fleet.hosts.length > 0)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Remove host (offline only — a live agent re-registers)
|
||||
// ---------------------------------------------------------------------------
|
||||
const confirmHostId = ref<string | null>(null)
|
||||
const removingHostId = ref<string | null>(null)
|
||||
|
||||
async function removeHost(host: FleetHost) {
|
||||
removingHostId.value = host.id
|
||||
try {
|
||||
await fleet.removeHost(host.id)
|
||||
toast.success(`Removed ${host.hostname}`)
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'Failed to remove host')
|
||||
} finally {
|
||||
removingHostId.value = null
|
||||
confirmHostId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/** Map host status → Badge tone */
|
||||
function hostTone(status: string): 'online' | 'offline' | 'warn' {
|
||||
if (status === 'connected') return 'online'
|
||||
@@ -184,6 +205,24 @@ function relativeHeartbeat(iso: string | null): string {
|
||||
<span class="fleet-host__meta-item" v-if="host.os || host.arch">
|
||||
<Icon name="cpu" :size="12" />{{ [host.os, host.arch].filter(Boolean).join(' / ') }}
|
||||
</span>
|
||||
<!-- Remove host — offline only; a live agent re-registers -->
|
||||
<template v-if="confirmHostId === host.id">
|
||||
<span class="fleet-host__confirm">Remove host & its instances?</span>
|
||||
<Button
|
||||
variant="danger-soft"
|
||||
size="sm"
|
||||
:loading="removingHostId === host.id"
|
||||
@click="removeHost(host)"
|
||||
>Remove</Button>
|
||||
<Button variant="ghost" size="sm" :disabled="removingHostId === host.id" @click="confirmHostId = null">Cancel</Button>
|
||||
</template>
|
||||
<Button
|
||||
v-else-if="host.status !== 'connected'"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="trash-2"
|
||||
@click="confirmHostId = host.id"
|
||||
>Remove</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user