import { Head } from '@inertiajs/react'; import { DotsThreeIcon } from '@phosphor-icons/react'; import { useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; import { AppNavbar } from '@/components/app-navbar'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Field, FieldError, FieldLabel } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { csrfToken } from '@/lib/csrf'; import { usePendingIds } from '@/lib/use-pending-ids'; import type { V5Cluster, V5DashboardProps, V5Server } from '@/types'; type ClusterFormErrors = { name?: string[]; description?: string[]; wireguard_interface?: string[]; wireguard_management_pool?: string[]; wireguard_listen_port?: string[]; container_network_pool?: string[]; container_network_prefix?: string[]; namespaces?: string[]; default_deny_containers?: string[]; coold_version?: string[]; corrosion_version?: string[]; corrosion_gossip_port?: string[]; corrosion_api_port?: string[]; builder_enabled?: string[]; builder_capacity?: string[]; builder_cpu_quota?: string[]; builder_memory_max?: string[]; builder_timeout_secs?: string[]; }; type StoreClusterResponse = { cluster: V5Cluster; }; type ServerFormErrors = { name?: string[]; host?: string[]; ssh_user?: string[]; ssh_port?: string[]; private_key_id?: string[]; node_address?: string[]; builder_enabled?: string[]; builder_capacity?: string[]; builder_cpu_quota?: string[]; ingress_type?: string[]; wireguard_listen_port_override?: string[]; wireguard_endpoint_override?: string[]; }; type StoreServerResponse = { cluster: V5Cluster; }; type UpdateServerResponse = { cluster: V5Cluster; }; type CheckServerResponse = { status: string; output: string; checkedAt: string; }; type DeleteServerResponse = { cluster: V5Cluster; }; type CooldLogsResponse = { output: string; fetchedAt: string; }; type CorrosionTablesResponse = { output: string; fetchedAt: string; }; type FirewallRule = { id?: string; namespace?: string; src?: string; dst?: string; proto?: string; port?: number; }; type FirewallRulesResponse = { rules: FirewallRule[]; fetchedAt: string; }; type CorrosionTableDump = { limit?: number; tables?: CorrosionTable[]; }; type CorrosionTable = { name: string; columns: string[]; rows: unknown[][]; }; type BootstrapServerResponse = { cluster?: V5Cluster; message?: string; }; type ServerSshCheck = { status: string; output: string; checkedAt: string; }; type V5ClusterUpdatedEvent = { cluster: V5Cluster | null; }; function formatCorrosionCell(value: unknown): string { if (value === null || value === undefined) { return 'null'; } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); } return JSON.stringify(value); } function parseCorrosionTables(output: string): CorrosionTableDump | null { if (!output.trim()) { return null; } try { const parsed = JSON.parse(output) as CorrosionTableDump; return Array.isArray(parsed.tables) ? parsed : null; } catch { return null; } } function statusLabel(status: string): string { return status .split(/[-_\s]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); } function statusBadgeClass(status: string): string { if (status === 'installed' || status === 'running') { return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400'; } if (['queued', 'starting', 'bootstrapping'].includes(status)) { return 'border-warning/30 bg-warning/10 text-warning'; } if (['unreachable', 'failed', 'error'].includes(status)) { return 'border-destructive/30 bg-destructive/10 text-destructive'; } return 'border-border bg-muted/40 text-muted-foreground'; } type EchoChannel = { listen: (event: string, callback: (payload: unknown) => void) => EchoChannel; subscribed?: (callback: () => void) => EchoChannel; error?: (callback: (error: unknown) => void) => EchoChannel; }; type EchoClient = { private: (channel: string) => EchoChannel; leave?: (channel: string) => void; leaveChannel?: (channel: string) => void; }; declare global { interface Window { Echo?: EchoClient; } } const clusterDefaults = { wireguardInterface: 'wg0', wireguardManagementPool: '100.64.0.0/16', wireguardListenPort: '51820', containerNetworkPool: '10.210.0.0/16', containerNetworkPrefix: '24', namespaces: 'default', defaultDenyContainers: true, cooldVersion: 'nightly', corrosionVersion: 'v1.0.0', corrosionGossipPort: '8787', corrosionApiPort: '8080', builderEnabled: true, builderCapacity: '2', builderCpuQuota: '200%', builderMemoryMax: '2G', builderTimeoutSecs: '1800', }; const ingressTypes = [ { label: 'Caddy', value: 'caddy', }, ]; function formatDate(value: string | null): string { if (value === null) { return 'Never'; } return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short', }).format(new Date(value)); } export default function Clusters({ flux, currentTeam = null, clusters = [], privateKeys = [], projects = [], selectedProjectUuid = null, selectedEnvironmentUuid = null, }: V5DashboardProps) { const [clusterList, setClusterList] = useState(clusters); const [selectedClusterId, setSelectedClusterId] = useState(clusters[0]?.id ?? ''); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [wireguardInterface, setWireguardInterface] = useState(clusterDefaults.wireguardInterface); const [wireguardManagementPool, setWireguardManagementPool] = useState(clusterDefaults.wireguardManagementPool); const [wireguardListenPort, setWireguardListenPort] = useState(clusterDefaults.wireguardListenPort); const [containerNetworkPool, setContainerNetworkPool] = useState(clusterDefaults.containerNetworkPool); const [containerNetworkPrefix, setContainerNetworkPrefix] = useState(clusterDefaults.containerNetworkPrefix); const [namespaces, setNamespaces] = useState(clusterDefaults.namespaces); const [defaultDenyContainers, setDefaultDenyContainers] = useState(clusterDefaults.defaultDenyContainers); const [cooldVersion, setCooldVersion] = useState(clusterDefaults.cooldVersion); const [corrosionVersion, setCorrosionVersion] = useState(clusterDefaults.corrosionVersion); const [corrosionGossipPort, setCorrosionGossipPort] = useState(clusterDefaults.corrosionGossipPort); const [corrosionApiPort, setCorrosionApiPort] = useState(clusterDefaults.corrosionApiPort); const [builderEnabled, setBuilderEnabled] = useState(clusterDefaults.builderEnabled); const [builderCapacity, setBuilderCapacity] = useState(clusterDefaults.builderCapacity); const [builderCpuQuota, setBuilderCpuQuota] = useState(clusterDefaults.builderCpuQuota); const [builderMemoryMax, setBuilderMemoryMax] = useState(clusterDefaults.builderMemoryMax); const [builderTimeoutSecs, setBuilderTimeoutSecs] = useState(clusterDefaults.builderTimeoutSecs); const [errors, setErrors] = useState({}); const [serverName, setServerName] = useState(''); const [serverHost, setServerHost] = useState(''); const [serverSshUser, setServerSshUser] = useState('root'); const [serverSshPort, setServerSshPort] = useState('22'); const [selectedPrivateKeyId, setSelectedPrivateKeyId] = useState(''); const [serverNodeAddress, setServerNodeAddress] = useState(''); const [serverBuilderEnabled, setServerBuilderEnabled] = useState(true); const [serverIngressEnabled, setServerIngressEnabled] = useState(false); const [serverIngressType, setServerIngressType] = useState('caddy'); const [serverBuilderCapacity, setServerBuilderCapacity] = useState('2'); const [serverBuilderCpuQuota, setServerBuilderCpuQuota] = useState(clusterDefaults.builderCpuQuota); const [wireguardListenPortOverride, setWireguardListenPortOverride] = useState(''); const [wireguardEndpointOverride, setWireguardEndpointOverride] = useState(''); const [serverErrors, setServerErrors] = useState({}); const [editingServer, setEditingServer] = useState(null); const [editServerBuilderEnabled, setEditServerBuilderEnabled] = useState(true); const [editServerIngressEnabled, setEditServerIngressEnabled] = useState(false); const [editServerIngressType, setEditServerIngressType] = useState('caddy'); const [editServerBuilderCapacity, setEditServerBuilderCapacity] = useState('2'); const [editServerBuilderCpuQuota, setEditServerBuilderCpuQuota] = useState(clusterDefaults.builderCpuQuota); const [editServerErrors, setEditServerErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isServerSubmitting, setIsServerSubmitting] = useState(false); const [isServerUpdateSubmitting, setIsServerUpdateSubmitting] = useState(false); const checkingServers = usePendingIds(); const [sshChecks, setSshChecks] = useState>({}); const [visibleBootstrapLogs, setVisibleBootstrapLogs] = useState>({}); const bootstrappingServers = usePendingIds(); const [bootstrapServerError, setBootstrapServerError] = useState(null); const deletingServers = usePendingIds(); const [isDeletingCluster, setIsDeletingCluster] = useState(false); const [deleteClusterError, setDeleteClusterError] = useState(null); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [clusterPendingDelete, setClusterPendingDelete] = useState(null); const [serverPendingDelete, setServerPendingDelete] = useState(null); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isAddServerDialogOpen, setIsAddServerDialogOpen] = useState(false); const [isEditServerDialogOpen, setIsEditServerDialogOpen] = useState(false); const [isCooldLogsDialogOpen, setIsCooldLogsDialogOpen] = useState(false); const [cooldLogsServer, setCooldLogsServer] = useState(null); const [cooldLogsOutput, setCooldLogsOutput] = useState(''); const [cooldLogsFetchedAt, setCooldLogsFetchedAt] = useState(null); const [cooldLogsError, setCooldLogsError] = useState(null); const [isLoadingCooldLogs, setIsLoadingCooldLogs] = useState(false); const [isCorrosionTablesDialogOpen, setIsCorrosionTablesDialogOpen] = useState(false); const [corrosionTablesServer, setCorrosionTablesServer] = useState(null); const [corrosionTablesOutput, setCorrosionTablesOutput] = useState(''); const [corrosionTablesFetchedAt, setCorrosionTablesFetchedAt] = useState(null); const [corrosionTablesError, setCorrosionTablesError] = useState(null); const [isLoadingCorrosionTables, setIsLoadingCorrosionTables] = useState(false); const [isFirewallRulesDialogOpen, setIsFirewallRulesDialogOpen] = useState(false); const [firewallRulesServer, setFirewallRulesServer] = useState(null); const [firewallRules, setFirewallRules] = useState([]); const [firewallRulesFetchedAt, setFirewallRulesFetchedAt] = useState(null); const [firewallRulesError, setFirewallRulesError] = useState(null); const [isLoadingFirewallRules, setIsLoadingFirewallRules] = useState(false); const [showAdvancedConfiguration, setShowAdvancedConfiguration] = useState(false); const [showAdvancedServerConfiguration, setShowAdvancedServerConfiguration] = useState(false); const selectedCluster = useMemo( () => clusterList.find((cluster) => cluster.id === selectedClusterId) ?? clusterList[0] ?? null, [clusterList, selectedClusterId], ); const notInitializedServers = selectedCluster?.servers.filter((server) => server.lastBootstrappedAt === null) ?? []; const initializedServers = selectedCluster?.servers.filter((server) => server.lastBootstrappedAt !== null) ?? []; const hasBootstrapInProgress = selectedCluster?.servers.some((server) => ['queued', 'running'].includes(server.lastBootstrapStatus ?? '')) ?? false; useEffect(() => { if (!currentTeam) { return; } let isCancelled = false; let attempts = 0; const channelName = `team.${currentTeam.id}`; const interval = window.setInterval(() => { attempts += 1; if (!window.Echo) { if (attempts === 1) { console.debug('Waiting for window.Echo before subscribing to cluster updates'); } if (attempts >= 20) { window.clearInterval(interval); } return; } window.clearInterval(interval); if (isCancelled) { return; } const channel = window.Echo.private(channelName); channel.subscribed?.(() => console.debug(`Subscribed to private-${channelName} for cluster updates`)); channel.error?.((error) => console.error(`Subscription error on private-${channelName}`, error)); channel.listen('.v5.cluster.updated', (payload) => { const event = payload as V5ClusterUpdatedEvent; if (!event.cluster) { return; } setClusterList((currentClusters) => currentClusters.map((cluster) => (cluster.id === event.cluster?.id ? event.cluster : cluster)), ); }); }, 500); return () => { isCancelled = true; window.clearInterval(interval); window.Echo?.leave?.(channelName) ?? window.Echo?.leaveChannel?.(`private-${channelName}`); }; }, [currentTeam]); useEffect(() => { if (!selectedCluster || !hasBootstrapInProgress) { return; } let isCancelled = false; async function refreshCluster(): Promise { if (!selectedCluster) { return; } const response = await fetch(`/v5/clusters/${selectedCluster.id}`, { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json', }, }); if (!response.ok || isCancelled) { return; } const payload = (await response.json()) as { cluster?: V5Cluster }; if (payload.cluster) { setClusterList((currentClusters) => currentClusters.map((cluster) => (cluster.id === payload.cluster?.id ? payload.cluster : cluster)), ); } } const interval = window.setInterval(() => void refreshCluster(), 3000); return () => { isCancelled = true; window.clearInterval(interval); }; }, [hasBootstrapInProgress, selectedCluster]); async function createCluster(event: FormEvent): Promise { event.preventDefault(); setIsSubmitting(true); setErrors({}); const response = await fetch('/v5/clusters', { method: 'POST', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, body: JSON.stringify({ name, description: description.trim() === '' ? null : description, wireguard_interface: wireguardInterface, wireguard_management_pool: wireguardManagementPool, wireguard_listen_port: Number(wireguardListenPort), container_network_pool: containerNetworkPool, container_network_prefix: Number(containerNetworkPrefix), namespaces: namespaces .split(',') .map((namespace) => namespace.trim()) .filter(Boolean), default_deny_containers: defaultDenyContainers, coold_version: cooldVersion, corrosion_version: corrosionVersion, corrosion_gossip_port: Number(corrosionGossipPort), corrosion_api_port: Number(corrosionApiPort), builder_enabled: builderEnabled, builder_capacity: Number(builderCapacity), builder_cpu_quota: builderCpuQuota, builder_memory_max: builderMemoryMax, builder_timeout_secs: Number(builderTimeoutSecs), }), }); if (response.status === 422) { const payload = (await response.json()) as { errors?: ClusterFormErrors; }; setErrors(payload.errors ?? {}); setIsSubmitting(false); return; } if (!response.ok) { setErrors({ name: ['Unable to create this cluster. Please try again.'], }); setIsSubmitting(false); return; } const payload = (await response.json()) as StoreClusterResponse; const nextClusters = [...clusterList, payload.cluster].sort((first, second) => first.name.localeCompare(second.name), ); setClusterList(nextClusters); setSelectedClusterId(payload.cluster.id); setName(''); setDescription(''); resetAdvancedConfiguration(); setIsCreateDialogOpen(false); setIsSubmitting(false); } async function createServer(event: FormEvent): Promise { event.preventDefault(); if (!selectedCluster) { return; } setIsServerSubmitting(true); setServerErrors({}); const response = await fetch(`/v5/clusters/${selectedCluster.id}/servers`, { method: 'POST', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, body: JSON.stringify({ name: serverName, host: serverHost, ssh_user: serverSshUser, ssh_port: Number(serverSshPort), private_key_id: selectedPrivateKeyId === '' ? null : Number(selectedPrivateKeyId), node_address: serverNodeAddress.trim() === '' ? null : serverNodeAddress, builder_enabled: serverBuilderEnabled, ingress_enabled: serverIngressEnabled, ingress_type: serverIngressEnabled ? serverIngressType : null, builder_capacity: Number(serverBuilderCapacity), builder_cpu_quota: serverBuilderCpuQuota, wireguard_listen_port_override: wireguardListenPortOverride.trim() === '' ? null : Number(wireguardListenPortOverride), wireguard_endpoint_override: wireguardEndpointOverride.trim() === '' ? null : wireguardEndpointOverride, }), }); if (response.status === 422) { const payload = (await response.json()) as { errors?: ServerFormErrors; }; setServerErrors(payload.errors ?? {}); setIsServerSubmitting(false); return; } if (!response.ok) { setServerErrors({ name: ['Unable to add this server. Please try again.'], }); setIsServerSubmitting(false); return; } const payload = (await response.json()) as StoreServerResponse; setClusterList((currentClusters) => currentClusters.map((cluster) => (cluster.id === payload.cluster.id ? payload.cluster : cluster)), ); resetServerForm(); setIsAddServerDialogOpen(false); setIsServerSubmitting(false); } async function updateServer(event: FormEvent): Promise { event.preventDefault(); if (!selectedCluster || !editingServer) { return; } setIsServerUpdateSubmitting(true); setEditServerErrors({}); const response = await fetch(`/v5/clusters/${selectedCluster.id}/servers/${editingServer.id}`, { method: 'PATCH', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, body: JSON.stringify({ builder_enabled: editServerBuilderEnabled, ingress_enabled: editServerIngressEnabled, ingress_type: editServerIngressEnabled ? editServerIngressType : null, builder_capacity: Number(editServerBuilderCapacity), builder_cpu_quota: editServerBuilderCpuQuota, }), }); if (response.status === 422) { const payload = (await response.json()) as { errors?: ServerFormErrors; }; setEditServerErrors(payload.errors ?? {}); setIsServerUpdateSubmitting(false); return; } if (!response.ok) { const payload = (await response.json().catch(() => null)) as { message?: string } | null; setEditServerErrors({ builder_capacity: [payload?.message ?? 'Unable to update this server. Please try again.'], }); setIsServerUpdateSubmitting(false); return; } const payload = (await response.json()) as UpdateServerResponse; setClusterList((currentClusters) => currentClusters.map((cluster) => (cluster.id === payload.cluster.id ? payload.cluster : cluster)), ); resetEditServerForm(); setIsEditServerDialogOpen(false); setIsServerUpdateSubmitting(false); } async function checkServer(server: V5Server): Promise { if (!selectedCluster) { return; } checkingServers.start(server.id); const response = await fetch(`/v5/clusters/${selectedCluster.id}/servers/${server.id}/check`, { method: 'POST', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, }); if (response.ok) { const payload = (await response.json()) as CheckServerResponse; setSshChecks((currentChecks) => ({ ...currentChecks, [server.id]: payload, })); } checkingServers.finish(server.id); } async function bootstrapServer(server: V5Server): Promise { if (!selectedCluster) { return; } bootstrappingServers.start(server.id); setBootstrapServerError(null); const response = await fetch(`/v5/clusters/${selectedCluster.id}/servers/${server.id}/bootstrap`, { method: 'POST', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, }); const payload = (await response.json()) as BootstrapServerResponse; if (payload.cluster) { setClusterList((currentClusters) => currentClusters.map((cluster) => (cluster.id === payload.cluster?.id ? payload.cluster : cluster)), ); } if (!response.ok) { setBootstrapServerError(payload.message ?? 'Unable to queue bootstrap for this server.'); } bootstrappingServers.finish(server.id); } async function loadCooldLogs(server: V5Server): Promise { if (!selectedCluster) { return; } setCooldLogsServer(server); setIsCooldLogsDialogOpen(true); setIsLoadingCooldLogs(true); setCooldLogsError(null); setCooldLogsOutput(''); setCooldLogsFetchedAt(null); const response = await fetch(`/v5/clusters/${selectedCluster.id}/servers/${server.id}/coold-logs?tail=200`, { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json', }, }); const payload = (await response.json().catch(() => null)) as CooldLogsResponse & { message?: string } | null; if (!response.ok) { setCooldLogsError(payload?.message ?? 'Unable to load coold logs.'); setIsLoadingCooldLogs(false); return; } setCooldLogsOutput(payload?.output ?? ''); setCooldLogsFetchedAt(payload?.fetchedAt ?? null); setIsLoadingCooldLogs(false); } async function loadCorrosionTables(server: V5Server): Promise { if (!selectedCluster) { return; } setCorrosionTablesServer(server); setIsCorrosionTablesDialogOpen(true); setIsLoadingCorrosionTables(true); setCorrosionTablesError(null); setCorrosionTablesOutput(''); setCorrosionTablesFetchedAt(null); const response = await fetch(`/v5/clusters/${selectedCluster.id}/servers/${server.id}/corrosion-tables?limit=200`, { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json', }, }); const payload = (await response.json().catch(() => null)) as CorrosionTablesResponse & { message?: string } | null; if (!response.ok) { setCorrosionTablesError(payload?.message ?? 'Unable to load Corrosion tables.'); setIsLoadingCorrosionTables(false); return; } setCorrosionTablesOutput(payload?.output ?? ''); setCorrosionTablesFetchedAt(payload?.fetchedAt ?? null); setIsLoadingCorrosionTables(false); } async function loadFirewallRules(server: V5Server): Promise { if (!selectedCluster) { return; } setFirewallRulesServer(server); setIsFirewallRulesDialogOpen(true); setIsLoadingFirewallRules(true); setFirewallRulesError(null); setFirewallRules([]); setFirewallRulesFetchedAt(null); const response = await fetch(`/v5/clusters/${selectedCluster.id}/servers/${server.id}/firewall-rules`, { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json', }, }); const payload = (await response.json().catch(() => null)) as FirewallRulesResponse & { message?: string } | null; if (!response.ok) { setFirewallRulesError(payload?.message ?? 'Unable to load firewall rules.'); setIsLoadingFirewallRules(false); return; } setFirewallRules(Array.isArray(payload?.rules) ? payload.rules : []); setFirewallRulesFetchedAt(payload?.fetchedAt ?? null); setIsLoadingFirewallRules(false); } function openDeleteClusterDialog(): void { if (!selectedCluster || selectedCluster.serversCount !== 0) { setDeleteClusterError('Only empty clusters can be deleted.'); return; } setDeleteClusterError(null); setClusterPendingDelete(selectedCluster); setServerPendingDelete(null); setIsDeleteDialogOpen(true); } function openDeleteServerDialog(server: V5Server): void { if (!selectedCluster) { return; } setDeleteClusterError(null); setClusterPendingDelete(selectedCluster); setServerPendingDelete(server); setIsDeleteDialogOpen(true); } async function deleteServer(cluster: V5Cluster, server: V5Server): Promise { deletingServers.start(server.id); const response = await fetch(`/v5/clusters/${cluster.id}/servers/${server.id}`, { method: 'DELETE', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, }); if (response.ok) { const payload = (await response.json()) as DeleteServerResponse; setClusterList((currentClusters) => currentClusters.map((cluster) => (cluster.id === payload.cluster.id ? payload.cluster : cluster)), ); } deletingServers.finish(server.id); setIsDeleteDialogOpen(false); setClusterPendingDelete(null); setServerPendingDelete(null); } function openEditServerDialog(server: V5Server): void { setEditingServer(server); setEditServerBuilderEnabled(server.builderEnabled); setEditServerIngressEnabled(server.ingressEnabled); setEditServerIngressType(server.ingressType ?? 'caddy'); setEditServerBuilderCapacity(String(server.builderCapacity)); setEditServerBuilderCpuQuota(server.builderCpuQuota); setEditServerErrors({}); setIsEditServerDialogOpen(true); } async function deleteCluster(cluster: V5Cluster): Promise { if (cluster.serversCount !== 0) { setDeleteClusterError('Only empty clusters can be deleted.'); return; } setIsDeletingCluster(true); setDeleteClusterError(null); const response = await fetch(`/v5/clusters/${cluster.id}`, { method: 'DELETE', credentials: 'same-origin', headers: { Accept: 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, }); if (response.status === 422) { const payload = (await response.json()) as { message?: string }; setDeleteClusterError(payload.message ?? 'Only empty clusters can be deleted.'); setIsDeletingCluster(false); return; } if (!response.ok) { setDeleteClusterError('Unable to delete this cluster. Please try again.'); setIsDeletingCluster(false); return; } const nextClusters = clusterList.filter((cluster) => cluster.id !== selectedCluster.id); setClusterList(nextClusters); setSelectedClusterId(nextClusters[0]?.id ?? ''); setIsDeletingCluster(false); setIsDeleteDialogOpen(false); setClusterPendingDelete(null); setServerPendingDelete(null); } async function confirmDelete(): Promise { if (!clusterPendingDelete) { return; } if (serverPendingDelete) { await deleteServer(clusterPendingDelete, serverPendingDelete); return; } await deleteCluster(clusterPendingDelete); } function resetAdvancedConfiguration(): void { setWireguardInterface(clusterDefaults.wireguardInterface); setWireguardManagementPool(clusterDefaults.wireguardManagementPool); setWireguardListenPort(clusterDefaults.wireguardListenPort); setContainerNetworkPool(clusterDefaults.containerNetworkPool); setContainerNetworkPrefix(clusterDefaults.containerNetworkPrefix); setNamespaces(clusterDefaults.namespaces); setDefaultDenyContainers(clusterDefaults.defaultDenyContainers); setCooldVersion(clusterDefaults.cooldVersion); setCorrosionVersion(clusterDefaults.corrosionVersion); setCorrosionGossipPort(clusterDefaults.corrosionGossipPort); setCorrosionApiPort(clusterDefaults.corrosionApiPort); setBuilderEnabled(clusterDefaults.builderEnabled); setBuilderCapacity(clusterDefaults.builderCapacity); setBuilderCpuQuota(clusterDefaults.builderCpuQuota); setBuilderMemoryMax(clusterDefaults.builderMemoryMax); setBuilderTimeoutSecs(clusterDefaults.builderTimeoutSecs); setShowAdvancedConfiguration(false); } function resetServerForm(): void { setServerName(''); setServerHost(''); setServerSshUser('root'); setServerSshPort('22'); setSelectedPrivateKeyId(''); setServerNodeAddress(''); setServerBuilderEnabled(selectedCluster?.builderEnabled ?? true); setServerIngressEnabled(false); setServerIngressType('caddy'); setServerBuilderCapacity(String(selectedCluster?.builderCapacity ?? 2)); setServerBuilderCpuQuota(selectedCluster?.builderCpuQuota ?? clusterDefaults.builderCpuQuota); setWireguardListenPortOverride(''); setWireguardEndpointOverride(''); setServerErrors({}); setShowAdvancedServerConfiguration(false); } function resetEditServerForm(): void { setEditingServer(null); setEditServerBuilderEnabled(true); setEditServerIngressEnabled(false); setEditServerIngressType('caddy'); setEditServerBuilderCapacity('2'); setEditServerBuilderCpuQuota(clusterDefaults.builderCpuQuota); setEditServerErrors({}); } function renderServerCard(server: V5Server) { const isCheckingServer = checkingServers.has(server.id); const isBootstrapInProgress = ['queued', 'running'].includes(server.lastBootstrapStatus ?? ''); const isBootstrappingServer = bootstrappingServers.has(server.id) || isBootstrapInProgress; const isDeletingServer = deletingServers.has(server.id); const isServerInitialized = server.lastBootstrappedAt !== null; const latestSshCheck = sshChecks[server.id] ?? null; const hasBootstrapLogs = server.lastBootstrapOutput !== null && server.lastBootstrapOutput.trim() !== ''; const canShowBootstrapLogs = hasBootstrapLogs || isBootstrapInProgress; const isBootstrapLogVisible = isBootstrapInProgress || (canShowBootstrapLogs && (visibleBootstrapLogs[server.id] ?? false)); return (

{server.name}

{server.host}

{server.status === 'unreachable' && server.lastStatusOutput ? (

{server.lastStatusOutput}

) : null}
{statusLabel(server.status)} {!isServerInitialized ? (
Not initialized
) : null} } > void checkServer(server)} > {isCheckingServer ? 'Checking...' : 'Check connection'} {canShowBootstrapLogs ? ( setVisibleBootstrapLogs((currentLogs) => ({ ...currentLogs, [server.id]: !isBootstrapLogVisible, })) } > {isBootstrapInProgress ? 'Install logs shown' : isBootstrapLogVisible ? 'Hide install logs' : 'Show install logs'} ) : null} void loadCooldLogs(server)}> Coold logs void loadCorrosionTables(server)}> Corrosion tables void loadFirewallRules(server)}> Firewall rules openEditServerDialog(server)}> Edit server openDeleteServerDialog(server)} > {isDeletingServer ? 'Deleting...' : 'Delete server'}
Builder capacity
{server.builderEnabled ? server.builderCapacity : 'Disabled'}
{server.builderEnabled ? (
Builder CPU quota
{server.builderCpuQuota}
) : null}
Caddy ingress
{server.ingressEnabled ? 'Enabled' : 'Disabled'}
WireGuard IP
{server.wireguardManagementIp ?? 'Not assigned'}
Server IP
{server.host}
Private key
{server.privateKeyName ?? 'No key'}
{latestSshCheck ? (
Latest SSH check: {latestSshCheck.status} {formatDate(latestSshCheck.checkedAt)}
                            {latestSshCheck.output}
                        
) : null} {isBootstrapLogVisible ? (
Install logs {server.lastBootstrapStatus ? `: ${server.lastBootstrapStatus}` : ''} {formatDate(server.lastBootstrapRanAt)}
{server.lastBootstrapOutput ? (
                                {server.lastBootstrapOutput}
                            
) : (

No install logs captured yet.

)}
) : null}
); } return ( <>

Clusters

Cluster inventory

{clusterList.length === 0 ? (
No clusters yet.
) : ( )}
{selectedCluster ? (

Cluster details

{selectedCluster.name}

{selectedCluster.description ?? 'No description provided.'}

{selectedCluster.serversCount === 0 ? ( ) : null}
{deleteClusterError ? (

{deleteClusterError}

) : null}

WireGuard mesh

Interface
{selectedCluster.wireguardInterface}
Management pool
{selectedCluster.wireguardManagementPool}
Listen port
{selectedCluster.wireguardListenPort}

Container networking

Pool
{selectedCluster.containerNetworkPool}
Prefix
/{selectedCluster.containerNetworkPrefix}
Namespaces
{selectedCluster.namespaces.join(', ')}

Cluster state

coold
{selectedCluster.cooldVersion}
Corrosion
{selectedCluster.corrosionVersion}

Firewall rules

Inspect defined coold allow rules persisted on each initialized server.

{initializedServers.length === 0 ? (

Initialize a server to inspect its firewall rules.

) : (
{initializedServers.map((server) => (

{server.name}

Host ID: {server.wireguardManagementIp ?? server.nodeAddress ?? 'Not assigned'}

))}
)}

Servers in this cluster

Connection and builder details for each server assigned to this cluster.

{bootstrapServerError ? (

{bootstrapServerError}

) : null} {selectedCluster.servers.length === 0 ? (

No servers assigned

Servers will appear here after they are added to this cluster.

) : (
{notInitializedServers.length > 0 ? (

Not initialized servers

Bootstrap these servers before using them for workloads.

{notInitializedServers.map(renderServerCard)}
) : null} {initializedServers.length > 0 ? (

Servers

Initialized servers currently assigned to this cluster.

{initializedServers.map(renderServerCard)}
) : null}
)}
) : (

No cluster selected

Create a cluster to start organizing servers.

)}
{ setIsDeleteDialogOpen(open); if (!open) { setClusterPendingDelete(null); setServerPendingDelete(null); } }} > Confirm deletion {serverPendingDelete ? `Delete server ${serverPendingDelete.name}? This removes it from this cluster inventory so you can add it again later.` : `Delete cluster ${clusterPendingDelete?.name ?? ''}? This cannot be undone.`} { setIsCreateDialogOpen(open); if (!open) { setErrors({}); } }} > Create cluster Create an empty cluster now. Servers can be assigned after they exist.
Name setName(event.target.value)} placeholder="Production Mesh" aria-invalid={errors.name ? true : undefined} /> Description