import { useState, useEffect, useCallback, useMemo } from 'react'; import { apiFetch } from '@/lib/api'; import { formatCount } from '@/lib/utils'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Bell, Zap, Shield, HardDrive, WifiOff, CheckCircle2, RefreshCw, } from 'lucide-react'; import { useLicense } from '@/context/LicenseContext'; import { useFleetSyncStatus } from '@/hooks/useFleetSyncStatus'; import { STICKY_CONTROL_IDENTITY_MISMATCH, type FleetSyncStatus } from '@/lib/fleetSyncApi'; import type { ConfigurationStatusPayload } from '@/components/dashboard'; interface FleetNodeConfiguration { id: number; name: string; type: 'local' | 'remote'; status: 'online' | 'offline'; configuration: ConfigurationStatusPayload | null; } function SummaryRow({ icon: Icon, label, value }: { icon: typeof Bell; label: string; value: string; }) { return (
{label} {value}
); } type PolicySyncState = | { kind: 'in_sync' } | { kind: 'degraded'; lastError: string | null } | { kind: 'paused' }; function derivePolicySyncState(rows: FleetSyncStatus[]): PolicySyncState | null { if (rows.length === 0) return null; for (const row of rows) { if (row.sticky_error_code === STICKY_CONTROL_IDENTITY_MISMATCH) { return { kind: 'paused' }; } } let degradedError: string | null = null; let hasSuccess = false; for (const row of rows) { if (row.last_success_at !== null) hasSuccess = true; if ( row.last_failure_at !== null && (row.last_success_at === null || row.last_failure_at > row.last_success_at) ) { degradedError = row.last_error; } } if (degradedError !== null) return { kind: 'degraded', lastError: degradedError }; if (hasSuccess) return { kind: 'in_sync' }; return null; } function PolicySyncRow({ state }: { state: PolicySyncState }) { if (state.kind === 'in_sync') { return ; } const tooltip = state.kind === 'paused' ? 'Anchored to another central. Open Settings → Nodes to reset the anchor or remove the node.' : (state.lastError ?? 'Last push to this node failed.'); return (
Policy sync {state.kind === 'paused' ? 'paused' : 'degraded'} {tooltip}
); } function NodeCard({ node, isPaid, policySyncState }: { node: FleetNodeConfiguration; isPaid: boolean; policySyncState: PolicySyncState | null; }) { const isRemote = node.type === 'remote'; if (!node.configuration) { return (
{node.name} {isRemote ? 'Remote' : 'Local'} Offline

Node is unreachable. Configuration unavailable.

); } const { notifications, automation, security, backup, thresholds } = node.configuration; const agentCount = [ notifications.agents.discord.enabled, notifications.agents.slack.enabled, notifications.agents.webhook.enabled, ].filter(Boolean).length; return (
{node.name} {isRemote ? 'Remote' : 'Local'}
Online
{isPaid && ( )} {!automation.webhooks.locked && ( )} {!isRemote && ( )} {!security.scanPolicies.locked && ( )} {!isRemote && !backup.locked && ( )} {policySyncState && }
); } export function FleetConfiguration() { const { isPaid } = useLicense(); const { statuses: syncStatuses } = useFleetSyncStatus(); const [nodes, setNodes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const syncStateByNode = useMemo(() => { const byNode = new Map(); for (const row of syncStatuses) { const list = byNode.get(row.node_id); if (list) list.push(row); else byNode.set(row.node_id, [row]); } const out = new Map(); for (const [nodeId, rows] of byNode) { const state = derivePolicySyncState(rows); if (state) out.set(nodeId, state); } return out; }, [syncStatuses]); const fetchData = useCallback(async () => { try { const res = await apiFetch('/fleet/configuration', { localOnly: true }); if (!res.ok) { setError('Failed to fetch fleet configuration.'); return; } const data = await res.json() as FleetNodeConfiguration[]; setNodes(data); setError(null); } catch { setError('Unable to reach the server.'); } finally { setLoading(false); } }, []); useEffect(() => { void fetchData(); }, [fetchData]); if (loading) { return (
{Array.from({ length: 2 }).map((_, i) => (
{Array.from({ length: 4 }).map((_, j) => (
))} ))}
); } if (error) { return (

{error}

); } if (nodes.length === 0) { return (

No nodes configured.

); } return (
{nodes.map(node => ( ))}
); }