import { useEffect, useState } from 'react'; import { copy } from './copy'; import { StorageMapWidget, TemperatureHeatmap, type HeatmapPoint, type StorageMapNode } from './StorageVisuals'; import { SourceStatusDetails, type SourceStatus } from './SourceStatusDetails'; type Severity = 'normal' | 'attention' | 'critical' | 'unknown'; type Disk = { id: string; name: string; role: string; state: string; utilizationPercent: number; capacitySeverity?: Severity; thermalSeverity?: Severity; temperature?: { celsius?: number; status: string; observedAt?: string } }; type DiskSnapshot = { source: SourceStatus; disks: Disk[] }; type Pool = { id: string; name: string; filesystem: string; state: string; utilizationPercent: number; capacitySeverity?: Severity }; type PoolSnapshot = { source: SourceStatus; pools: Pool[] }; type ArrayMember = { id: string; name: string; role: string; state: string }; type ArraySnapshot = { source: SourceStatus; state: string; members: ArrayMember[] }; export type StorageData = { array: ArraySnapshot; disks: DiskSnapshot; pools: PoolSnapshot }; function availability(value: string): string { return value === 'online' || value === 'healthy' || value === 'operational' ? 'healthy' : value === 'unknown' ? 'unknown' : 'degraded'; } function visualSeverity(...values: Array): string { if (values.includes('critical')) return 'critical'; if (values.some((value) => value === 'attention' || value === 'degraded' || value === 'faulted')) return 'degraded'; if (values.includes('unknown')) return 'unknown'; return 'healthy'; } function signalLabel(value: string): string { return value === 'normal' || value === 'healthy' || value === 'online' ? 'normaal' : value === 'critical' ? 'kritiek' : value === 'attention' || value === 'degraded' ? 'aandacht' : 'onbekend'; } export function buildStorageNodes(data: StorageData): StorageMapNode[] { const members = new Map((data.array.members ?? []).map((member) => [member.id.toLowerCase(), member])); const diskNodes = (data.disks.disks ?? []).slice(0, 64).map((disk) => { const member = members.get(disk.id.toLowerCase()); if (member) members.delete(disk.id.toLowerCase()); const capacity = disk.capacitySeverity ?? 'unknown'; const thermal = disk.thermalSeverity ?? disk.temperature?.status ?? 'unknown'; return { id: 'disk-' + disk.id, label: disk.name, kind: member?.role ?? disk.role, state: visualSeverity(availability(disk.state), capacity, thermal), detail: `Beschikbaarheid ${signalLabel(disk.state)} · capaciteit ${signalLabel(capacity)} · temperatuur ${signalLabel(thermal)}`, href: '/disks/' + encodeURIComponent(disk.id) }; }); const unmatchedMembers = [...members.values()].slice(0, 64).map((member) => ({ id: 'array-' + member.id, label: member.name, kind: member.role, state: availability(member.state), detail: `Beschikbaarheid ${signalLabel(member.state)} · disktelemetrie onbekend`, href: '/array' })); const poolNodes = (data.pools.pools ?? []).slice(0, 64).map((pool) => { const capacity = pool.capacitySeverity ?? 'unknown'; return { id: 'pool-' + pool.id, label: pool.name, kind: `pool · ${pool.filesystem}`, state: visualSeverity(availability(pool.state), capacity), detail: `Device-health ${signalLabel(pool.state)} · capaciteit ${signalLabel(capacity)} (${pool.utilizationPercent.toLocaleString('nl-BE', { maximumFractionDigits: 1 })}% gebruikt)`, href: '/pools/' + encodeURIComponent(pool.id) }; }); return [...unmatchedMembers, ...poolNodes, ...diskNodes]; } export function StoragePage() { const [state, setState] = useState<'loading' | 'ready' | 'error'>('loading'); const [data, setData] = useState(null); useEffect(() => { const controller = new AbortController(); Promise.all([fetch('/api/v1/array', { signal: controller.signal }).then((response) => response.json() as Promise), fetch('/api/v1/disks?limit=100', { signal: controller.signal }).then((response) => response.json() as Promise), fetch('/api/v1/pools?limit=100', { signal: controller.signal }).then((response) => response.json() as Promise)]).then(([array, disks, pools]) => { setData({ array, disks, pools }); setState('ready'); }).catch((error: unknown) => { if (error instanceof DOMException && error.name === 'AbortError') return; setState('error'); }); return () => controller.abort(); }, []); if (state === 'loading') return
; if (state === 'error' || !data) return

{copy.storage.errorTitle}

{copy.storage.errorDetail}

; const nodes = buildStorageNodes(data); const heatmap: HeatmapPoint[] = (data.disks.disks ?? []).slice(0, 64).map((disk) => ({ id: disk.id, label: disk.name, observedAt: disk.temperature?.observedAt || '', value: disk.temperature?.celsius ?? null, status: visualSeverity(disk.thermalSeverity ?? disk.temperature?.status ?? 'unknown'), href: '/disks/' + encodeURIComponent(disk.id) })); return <>

{copy.storage.eyebrow}

{copy.storage.title}

{copy.storage.intro}

{copy.storage.source}

{copy.storage.sourceTitle}

{copy.storage.accessible}

{copy.storage.arraySource}

{copy.storage.diskSource}

{copy.storage.poolSource}

{nodes.length} {copy.storage.mapNodes} · {heatmap.length} {copy.storage.heatmapPoints}

; }