diff --git a/frontend/src/components/FleetView.tsx b/frontend/src/components/FleetView.tsx index c76d63e7..964c0343 100644 --- a/frontend/src/components/FleetView.tsx +++ b/frontend/src/components/FleetView.tsx @@ -1,23 +1,18 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useExperimental } from '@/hooks/useExperimental'; import { - Server, Cpu, MemoryStick, HardDrive, RefreshCw, ChevronDown, ChevronRight, - Layers, Wifi, WifiOff, Search, AlertTriangle, - RotateCcw, ExternalLink, Camera, Download, Loader2, + RefreshCw, Search, Camera, Network, SlidersHorizontal, Send, KeyRound, ArrowLeftRight, } from 'lucide-react'; import { FleetMasthead } from './fleet/FleetMasthead'; -import { FleetTopology } from './fleet/FleetTopology'; import { ReconnectingOverlay } from './FleetView/ReconnectingOverlay'; import { NodeUpdatesSheet } from './FleetView/NodeUpdatesSheet'; import { LocalUpdateConfirmDialog } from './FleetView/LocalUpdateConfirmDialog'; -import { UpdateStatusBadge } from './FleetView/UpdateStatusBadge'; -import type { NodeUpdateStatus, ViewMode, FleetPreferences, FleetPaletteEntry } from './FleetView/types'; -import { OverviewToolbar } from './FleetView/OverviewToolbar'; +import { isCritical, getNodeCpu, getNodeMem, getNodeDisk } from './FleetView/nodeUtils'; +import { OverviewTab } from './FleetView/OverviewTab'; +import type { FleetNode, NodeUpdateStatus, ViewMode, FleetPreferences, FleetPaletteEntry } from './FleetView/types'; import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Skeleton } from '@/components/ui/skeleton'; import { Tabs, TabsContent, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from '@/components/ui/tabs'; import { springs } from '@/lib/motion'; import { apiFetch, fetchForNode } from '@/lib/api'; @@ -29,48 +24,12 @@ import { FleetSoonPlaceholder, SoonBadge } from './fleet/FleetSoonPlaceholder'; import { RoutingTab } from './fleet/RoutingTab'; import { DeploymentsTab } from './blueprints/DeploymentsTab'; import { toast } from '@/components/ui/toast-store'; -import { LabelDot } from './LabelPill'; import { type Label as StackLabel, type LabelColor } from './label-types'; -import { formatVersion } from '@/lib/version'; function labelPaletteKey(name: string, color: LabelColor): string { return `${name.trim().toLowerCase()}|${color}`; } -// --- Types --- - -interface FleetNodeStats { - active: number; - managed: number; - unmanaged: number; - exited: number; - total: number; -} - -interface FleetNodeSystemStats { - cpu: { usage: string; cores: number }; - memory: { total: number; used: number; free: number; usagePercent: string }; - disk: { total: number; used: number; free: number; usagePercent: string } | null; -} - -interface FleetNode { - id: number; - name: string; - type: 'local' | 'remote'; - status: 'online' | 'offline' | 'unknown'; - stats: FleetNodeStats | null; - systemStats: FleetNodeSystemStats | null; - stacks: string[] | null; -} - -interface StackContainer { - Id?: string; - Names?: string[]; - Image?: string; - State?: string; - Status?: string; -} - const PREFS_KEY = 'sencho-fleet-preferences'; function loadPreferences(): FleetPreferences { @@ -85,409 +44,6 @@ function savePreferences(prefs: FleetPreferences) { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } -// --- Utilities --- - -function formatBytes(bytes: number): string { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; -} - -function getNodeCpu(node: FleetNode): number { - return node.systemStats ? parseFloat(node.systemStats.cpu.usage) : 0; -} - -function getNodeMem(node: FleetNode): number { - return node.systemStats ? parseFloat(node.systemStats.memory.usagePercent) : 0; -} - -function getNodeDisk(node: FleetNode): number { - return node.systemStats?.disk ? parseFloat(node.systemStats.disk.usagePercent) : 0; -} - -function isCritical(node: FleetNode): boolean { - return getNodeCpu(node) > 90 || getNodeDisk(node) > 90; -} - -function containerName(c: StackContainer): string { - if (c.Names && c.Names.length > 0) { - return c.Names[0].replace(/^\//, ''); - } - return c.Id?.slice(0, 12) ?? 'unknown'; -} - -// --- Sub-Components --- - -function UsageBar({ percent, color }: { percent: number; color: string }) { - return ( -
-
-
- ); -} - -function ContainerRow({ container, nodeId, onNavigate }: { - container: StackContainer; - nodeId: number; - onNavigate: (nodeId: number) => void; -}) { - const name = containerName(container); - const state = container.State?.toLowerCase() ?? 'unknown'; - const image = container.Image; - const status = container.Status ?? ''; - - const stateColor = state === 'running' ? 'bg-success' : - state === 'restarting' ? 'bg-warning' : 'bg-destructive'; - - return ( -
-
-
-
- {name} - {state} -
- {(image || status) && ( -
- {image && {image}} - {status && {image ? '· ' : ''}{status}} -
- )} -
- -
- ); -} - -function StackSection({ stackName, nodeId, onNavigate, labelMap }: { - stackName: string; - nodeId: number; - onNavigate: (nodeId: number, stackName: string) => void; - labelMap?: Record; -}) { - const [expanded, setExpanded] = useState(false); - const [containers, setContainers] = useState(null); - const [loading, setLoading] = useState(false); - - const handleExpand = async () => { - if (loading) return; - const next = !expanded; - setExpanded(next); - - if (next) { - setLoading(true); - try { - const res = await apiFetch(`/fleet/node/${nodeId}/stacks/${encodeURIComponent(stackName)}/containers`, { localOnly: true }); - if (res.ok) { - setContainers(await res.json()); - } else { - toast.error('Failed to load containers for ' + stackName); - } - } catch (error) { - console.error('Failed to load containers for', stackName, error); - toast.error('Failed to load containers for ' + stackName); - } finally { - setLoading(false); - } - } - }; - - const runningCount = containers?.filter(c => c.State?.toLowerCase() === 'running').length ?? 0; - const totalCount = containers?.length ?? 0; - - return ( -
- - {expanded && ( -
- {loading ? ( -
- - -
- ) : containers && containers.length > 0 ? ( - containers.map(c => ( - onNavigate(nid, stackName)} - /> - )) - ) : ( -

No containers

- )} -
- )} -
- ); -} - -interface NodeCardProps { - node: FleetNode; - onNavigate: (nodeId: number, stackName: string) => void; - labelMap?: Record; - updateStatus?: NodeUpdateStatus; - onUpdate?: (nodeId: number) => void; - updatingNodeId?: number | null; - onRetryUpdate?: (nodeId: number) => void; - onDismissUpdate?: (nodeId: number) => void; -} - -function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updatingNodeId, onRetryUpdate, onDismissUpdate }: NodeCardProps) { - const { isPaid } = useLicense(); - const [expanded, setExpanded] = useState(false); - const [stacks, setStacks] = useState(node.stacks); - const [loadingStacks, setLoadingStacks] = useState(false); - - const isOnline = node.status === 'online'; - const isLocal = node.type === 'local'; - const formattedVersion = formatVersion(updateStatus?.version); - const formattedLatest = formatVersion(updateStatus?.latestVersion); - const cpuPercent = getNodeCpu(node); - const memPercent = getNodeMem(node); - const diskPercent = getNodeDisk(node); - - const handleExpand = async () => { - if (!isPaid) return; - const next = !expanded; - setExpanded(next); - - if (next && !stacks) { - setLoadingStacks(true); - try { - const res = await apiFetch(`/fleet/node/${node.id}/stacks`, { localOnly: true }); - if (res.ok) { - setStacks(await res.json()); - } else { - toast.error('Failed to load stacks for ' + node.name); - } - } catch (error) { - console.error('Failed to load stacks for', node.name, error); - toast.error('Failed to load stacks for ' + node.name); - } finally { - setLoadingStacks(false); - } - } - }; - - const localRailClasses = isLocal - ? 'relative overflow-hidden ring-1 ring-brand/30 before:absolute before:inset-y-0 before:left-0 before:w-[2px] before:bg-brand before:rounded-l-xl after:pointer-events-none after:absolute after:inset-0 after:bg-gradient-to-r after:from-brand/[0.06] after:via-transparent after:to-transparent' - : ''; - - return ( -
- {/* Card Header */} -
- {isLocal && ( - - ★ Local - - )} -
-
-
- -
-
-

{node.name}

-
- - {isOnline ? ( - <> Online - ) : ( - <> Offline - )} - - - {node.type} - - {formattedVersion && ( - - {formattedVersion} - - )} - {updateStatus?.updateStatus && ( - onRetryUpdate(node.id) : undefined} - onDismiss={onDismissUpdate ? () => onDismissUpdate(node.id) : undefined} - /> - )} - {updateStatus?.updateAvailable && !updateStatus.updateStatus && ( - - Update available - - )} - {isOnline && isCritical(node) && ( - - Critical - - )} -
-
-
-
- - {/* Container Stats */} - {isOnline && node.stats && ( -
-
-
{node.stats.active}
-
Running
-
-
-
{node.stats.exited}
-
Stopped
-
-
-
{node.stacks?.length ?? '-'}
-
Stacks
-
-
- )} - - {/* Resource Usage Bars */} - {isOnline && node.systemStats && ( -
-
-
- - CPU - - {node.systemStats.cpu.usage}% -
- 80 ? 'bg-destructive/80' : cpuPercent > 60 ? 'bg-warning' : 'bg-success'} /> -
-
-
- - RAM - - {formatBytes(node.systemStats.memory.used)} / {formatBytes(node.systemStats.memory.total)} -
- 80 ? 'bg-destructive/80' : memPercent > 60 ? 'bg-warning' : 'bg-brand/60'} /> -
- {node.systemStats.disk && ( -
-
- - Disk - - {formatBytes(node.systemStats.disk.used)} / {formatBytes(node.systemStats.disk.total)} -
- 90 ? 'bg-destructive/80' : diskPercent > 75 ? 'bg-warning' : 'bg-brand'} /> -
- )} -
- )} - - {/* Update button */} - {isOnline && updateStatus?.updateAvailable && !updateStatus.updateStatus && onUpdate && ( -
- -
- )} - - {/* Offline placeholder */} - {!isOnline && ( -
- Node unreachable -
- )} -
- - {/* Paid: Expandable Stack List with Container Drill-Down */} - {isOnline && isPaid && ( -
- - {expanded && ( -
- {loadingStacks ? ( -
- - -
- ) : stacks && stacks.length > 0 ? ( -
- {stacks.map(stack => ( - - ))} -
- ) : ( -

No stacks found

- )} -
- )} -
- )} -
- ); -} - -// --- Main Component --- - interface FleetViewProps { onNavigateToNode: (nodeId: number, stackName: string) => void; } @@ -830,7 +386,10 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) { updatePrefs({ filterStatus: 'all', filterType: 'all', filterCritical: false }); setLabelFilters(new Set()); }, [updatePrefs]); - const allNodes = localNode ? [localNode, ...remoteNodes] : remoteNodes; + const allNodes = useMemo( + () => (localNode ? [localNode, ...remoteNodes] : remoteNodes), + [localNode, remoteNodes] + ); return (
@@ -930,101 +489,31 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
- {/* Loading State */} - {loading && ( -
- {Array.from({ length: 3 }).map((_, i) => ( -
- -
- - - -
- - - -
- ))} -
- )} - - {/* Empty State */} - {!loading && nodes.length === 0 && ( -
- -

No nodes configured

-

Add nodes in Settings to see your fleet here.

-
- )} - - {/* Fleet Content */} - {!loading && nodes.length > 0 && ( - <> - - - {viewMode === 'topology' && processedNodes.length > 0 ? ( - onNavigateToNode(id, '')} - /> - ) : processedNodes.length > 0 ? ( -
- {allNodes.map(node => ( - - ))} -
- ) : ( -
- -

No nodes match your filters

-

Try adjusting your search or filter criteria.

- -
- )} - - {/* Paid tier auto-refresh indicator */} - {isPaid && ( -

- Auto-refreshing every 30 seconds -

- )} - - - )} +
{isPaid && ( diff --git a/frontend/src/components/FleetView/NodeCard.tsx b/frontend/src/components/FleetView/NodeCard.tsx new file mode 100644 index 00000000..236891e5 --- /dev/null +++ b/frontend/src/components/FleetView/NodeCard.tsx @@ -0,0 +1,405 @@ +import { useState } from 'react'; +import { + Server, Cpu, MemoryStick, HardDrive, ChevronDown, ChevronRight, + Layers, Wifi, WifiOff, AlertTriangle, ExternalLink, Download, Loader2, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { formatBytes } from '@/lib/utils'; +import { apiFetch } from '@/lib/api'; +import { useLicense } from '@/context/LicenseContext'; +import { toast } from '@/components/ui/toast-store'; +import { LabelDot } from '../LabelPill'; +import { formatVersion } from '@/lib/version'; +import { UpdateStatusBadge } from './UpdateStatusBadge'; +import type { Label as StackLabel } from '../label-types'; +import type { FleetNode, NodeUpdateStatus } from './types'; +import { getNodeCpu, getNodeMem, getNodeDisk, isCritical } from './nodeUtils'; + +// --- Types --- + +interface StackContainer { + Id?: string; + Names?: string[]; + Image?: string; + State?: string; + Status?: string; +} + +export interface NodeCardProps { + node: FleetNode; + onNavigate: (nodeId: number, stackName: string) => void; + labelMap?: Record; + updateStatus?: NodeUpdateStatus; + onUpdate?: (nodeId: number) => void; + updatingNodeId?: number | null; + onRetryUpdate?: (nodeId: number) => void; + onDismissUpdate?: (nodeId: number) => void; +} + +function containerName(c: StackContainer): string { + if (c.Names && c.Names.length > 0) { + return c.Names[0].replace(/^\//, ''); + } + return c.Id?.slice(0, 12) ?? 'unknown'; +} + +// --- Sub-Components --- + +function UsageBar({ percent, color }: { percent: number; color: string }) { + return ( +
+
+
+ ); +} + +function ContainerRow({ container, onNavigate }: { + container: StackContainer; + onNavigate: () => void; +}) { + const name = containerName(container); + const state = container.State?.toLowerCase() ?? 'unknown'; + const image = container.Image; + const status = container.Status ?? ''; + + const stateColor = state === 'running' ? 'bg-success' : + state === 'restarting' ? 'bg-warning' : 'bg-destructive'; + + return ( +
+
+
+
+ {name} + {state} +
+ {(image || status) && ( +
+ {image && {image}} + {status && {image ? '· ' : ''}{status}} +
+ )} +
+ +
+ ); +} + +function StackSection({ stackName, nodeId, onNavigate, labelMap }: { + stackName: string; + nodeId: number; + onNavigate: (nodeId: number, stackName: string) => void; + labelMap?: Record; +}) { + const [expanded, setExpanded] = useState(false); + const [containers, setContainers] = useState(null); + const [loading, setLoading] = useState(false); + + const handleExpand = async () => { + if (loading) return; + const next = !expanded; + setExpanded(next); + + if (next) { + setLoading(true); + try { + const res = await apiFetch(`/fleet/node/${nodeId}/stacks/${encodeURIComponent(stackName)}/containers`, { localOnly: true }); + if (res.ok) { + setContainers(await res.json()); + } else { + toast.error('Failed to load containers for ' + stackName); + } + } catch (error) { + console.error('Failed to load containers for', stackName, error); + toast.error('Failed to load containers for ' + stackName); + setExpanded(false); + } finally { + setLoading(false); + } + } + }; + + const runningCount = containers?.filter(c => c.State?.toLowerCase() === 'running').length ?? 0; + const totalCount = containers?.length ?? 0; + + return ( +
+ + {expanded && ( +
+ {loading ? ( +
+ + +
+ ) : containers && containers.length > 0 ? ( + containers.map(c => ( + onNavigate(nodeId, stackName)} + /> + )) + ) : ( +

No containers

+ )} +
+ )} +
+ ); +} + +// --- Main Export --- + +export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updatingNodeId, onRetryUpdate, onDismissUpdate }: NodeCardProps) { + const { isPaid } = useLicense(); + const [expanded, setExpanded] = useState(false); + const [stacks, setStacks] = useState(node.stacks); + const [loadingStacks, setLoadingStacks] = useState(false); + + const isOnline = node.status === 'online'; + const isLocal = node.type === 'local'; + const formattedVersion = formatVersion(updateStatus?.version); + const formattedLatest = formatVersion(updateStatus?.latestVersion); + const cpuPercent = getNodeCpu(node); + const memPercent = getNodeMem(node); + const diskPercent = getNodeDisk(node); + + const handleExpand = async () => { + if (!isPaid) return; + const next = !expanded; + setExpanded(next); + + if (next && !stacks) { + setLoadingStacks(true); + try { + const res = await apiFetch(`/fleet/node/${node.id}/stacks`, { localOnly: true }); + if (res.ok) { + setStacks(await res.json()); + } else { + toast.error('Failed to load stacks for ' + node.name); + } + } catch (error) { + console.error('Failed to load stacks for', node.name, error); + toast.error('Failed to load stacks for ' + node.name); + setExpanded(false); + } finally { + setLoadingStacks(false); + } + } + }; + + const localRailClasses = isLocal + ? 'relative overflow-hidden ring-1 ring-brand/30 before:absolute before:inset-y-0 before:left-0 before:w-[2px] before:bg-brand before:rounded-l-xl after:pointer-events-none after:absolute after:inset-0 after:bg-gradient-to-r after:from-brand/[0.06] after:via-transparent after:to-transparent' + : ''; + + return ( +
+ {/* Card Header */} +
+ {isLocal && ( + + ★ Local + + )} +
+
+
+ +
+
+

{node.name}

+
+ + {isOnline ? ( + <> Online + ) : ( + <> Offline + )} + + + {node.type} + + {formattedVersion && ( + + {formattedVersion} + + )} + {updateStatus?.updateStatus && ( + onRetryUpdate(node.id) : undefined} + onDismiss={onDismissUpdate ? () => onDismissUpdate(node.id) : undefined} + /> + )} + {updateStatus?.updateAvailable && !updateStatus.updateStatus && ( + + Update available + + )} + {isOnline && isCritical(node) && ( + + Critical + + )} +
+
+
+
+ + {/* Container Stats */} + {isOnline && node.stats && ( +
+
+
{node.stats.active}
+
Running
+
+
+
{node.stats.exited}
+
Stopped
+
+
+
{node.stacks?.length ?? '-'}
+
Stacks
+
+
+ )} + + {/* Resource Usage Bars */} + {isOnline && node.systemStats && ( +
+
+
+ + CPU + + {node.systemStats.cpu.usage}% +
+ 80 ? 'bg-destructive/80' : cpuPercent > 60 ? 'bg-warning' : 'bg-success'} /> +
+
+
+ + RAM + + {formatBytes(node.systemStats.memory.used, 1)} / {formatBytes(node.systemStats.memory.total, 1)} +
+ 80 ? 'bg-destructive/80' : memPercent > 60 ? 'bg-warning' : 'bg-brand/60'} /> +
+ {node.systemStats.disk && ( +
+
+ + Disk + + {formatBytes(node.systemStats.disk.used, 1)} / {formatBytes(node.systemStats.disk.total, 1)} +
+ 90 ? 'bg-destructive/80' : diskPercent > 75 ? 'bg-warning' : 'bg-brand'} /> +
+ )} +
+ )} + + {/* Update button */} + {isOnline && updateStatus?.updateAvailable && !updateStatus.updateStatus && onUpdate && ( +
+ +
+ )} + + {/* Offline placeholder */} + {!isOnline && ( +
+ Node unreachable +
+ )} +
+ + {/* Paid: Expandable Stack List with Container Drill-Down */} + {isOnline && isPaid && ( +
+ + {expanded && ( +
+ {loadingStacks ? ( +
+ + +
+ ) : stacks && stacks.length > 0 ? ( +
+ {stacks.map(stack => ( + + ))} +
+ ) : ( +

No stacks found

+ )} +
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/components/FleetView/OverviewTab.tsx b/frontend/src/components/FleetView/OverviewTab.tsx new file mode 100644 index 00000000..16265c5b --- /dev/null +++ b/frontend/src/components/FleetView/OverviewTab.tsx @@ -0,0 +1,156 @@ +import { Server, Search, RotateCcw } from 'lucide-react'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Button } from '@/components/ui/button'; +import { FleetTopology } from '../fleet/FleetTopology'; +import { NodeCard } from './NodeCard'; +import { OverviewToolbar } from './OverviewToolbar'; +import type { FleetTopologyNode } from '@/lib/fleet-topology-layout'; +import type { Label as StackLabel } from '../label-types'; +import type { FleetNode, NodeUpdateStatus, ViewMode, FleetPreferences, FleetPaletteEntry } from './types'; + +interface OverviewTabProps { + loading: boolean; + nodes: FleetNode[]; + processedNodes: FleetNode[]; + allNodes: FleetNode[]; + topologyNodes: FleetTopologyNode[]; + viewMode: ViewMode; + onViewModeChange: (mode: ViewMode) => void; + searchQuery: string; + onSearchQueryChange: (q: string) => void; + prefs: FleetPreferences; + onPrefsChange: (update: Partial) => void; + fleetPalette: FleetPaletteEntry[]; + labelFilters: Set; + onLabelFiltersChange: (filters: Set) => void; + onClearFilters: () => void; + isPaid: boolean; + fleetStackLabelMap: Record>; + updateStatusMap: Map; + onNavigateToNode: (nodeId: number, stackName: string) => void; + onUpdate?: (nodeId: number) => void; + updatingNodeId: number | null; + onRetryUpdate?: (nodeId: number) => void; + onDismissUpdate?: (nodeId: number) => void; +} + +export function OverviewTab({ + loading, + nodes, + processedNodes, + allNodes, + topologyNodes, + viewMode, + onViewModeChange, + searchQuery, + onSearchQueryChange, + prefs, + onPrefsChange, + fleetPalette, + labelFilters, + onLabelFiltersChange, + onClearFilters, + isPaid, + fleetStackLabelMap, + updateStatusMap, + onNavigateToNode, + onUpdate, + updatingNodeId, + onRetryUpdate, + onDismissUpdate, +}: OverviewTabProps) { + return ( + <> + {loading && ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ +
+ + + +
+ + + +
+ ))} +
+ )} + + {!loading && nodes.length === 0 && ( +
+ +

No nodes configured

+

Add nodes in Settings to see your fleet here.

+
+ )} + + {!loading && nodes.length > 0 && ( + <> + + + {viewMode === 'topology' && processedNodes.length > 0 ? ( + onNavigateToNode(id, '')} + /> + ) : processedNodes.length > 0 ? ( +
+ {allNodes.map(node => ( + + ))} +
+ ) : ( +
+ +

No nodes match your filters

+

Try adjusting your search or filter criteria.

+ +
+ )} + + {isPaid && ( +

+ Auto-refreshing every 30 seconds +

+ )} + + )} + + ); +} diff --git a/frontend/src/components/FleetView/nodeUtils.ts b/frontend/src/components/FleetView/nodeUtils.ts new file mode 100644 index 00000000..18cd8a73 --- /dev/null +++ b/frontend/src/components/FleetView/nodeUtils.ts @@ -0,0 +1,17 @@ +import type { FleetNode } from './types'; + +export function getNodeCpu(node: FleetNode): number { + return node.systemStats ? parseFloat(node.systemStats.cpu.usage) : 0; +} + +export function getNodeMem(node: FleetNode): number { + return node.systemStats ? parseFloat(node.systemStats.memory.usagePercent) : 0; +} + +export function getNodeDisk(node: FleetNode): number { + return node.systemStats?.disk ? parseFloat(node.systemStats.disk.usagePercent) : 0; +} + +export function isCritical(node: FleetNode): boolean { + return getNodeCpu(node) > 90 || getNodeDisk(node) > 90; +} diff --git a/frontend/src/components/FleetView/types.ts b/frontend/src/components/FleetView/types.ts index 9feb03ab..042a032e 100644 --- a/frontend/src/components/FleetView/types.ts +++ b/frontend/src/components/FleetView/types.ts @@ -1,5 +1,29 @@ import type { LabelColor } from '../label-types'; +export interface FleetNodeStats { + active: number; + managed: number; + unmanaged: number; + exited: number; + total: number; +} + +export interface FleetNodeSystemStats { + cpu: { usage: string; cores: number }; + memory: { total: number; used: number; free: number; usagePercent: string }; + disk: { total: number; used: number; free: number; usagePercent: string } | null; +} + +export interface FleetNode { + id: number; + name: string; + type: 'local' | 'remote'; + status: 'online' | 'offline' | 'unknown'; + stats: FleetNodeStats | null; + systemStats: FleetNodeSystemStats | null; + stacks: string[] | null; +} + export interface NodeUpdateStatus { nodeId: number; name: string;