"use client" import { useEffect, useState, useRef } from "react" import { useRouter } from "next/navigation" import { api, Pool } from "@/lib/api" import { Header } from "@/components/Header" import { PoolCard } from "@/components/PoolCard" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { RefreshCw, AlertCircle, Cpu, HardDrive, Zap, Clock, Network, Database } from "lucide-react" export default function Dashboard() { const router = useRouter() const [pools, setPools] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [lastUpdate, setLastUpdate] = useState(null) const [zfsAvailable, setZfsAvailable] = useState(null) const [systemInfo, setSystemInfo] = useState(null) const [memoryInfo, setMemoryInfo] = useState(null) const [cpuInfo, setCpuInfo] = useState(null) const [uptimeInfo, setUptimeInfo] = useState(null) const [networkInfo, setNetworkInfo] = useState(null) const [networkTraffic, setNetworkTraffic] = useState(null) const [diskIO, setDiskIO] = useState(null) // History buffers for sparklines (rolling window of 30 points, ~2.5 minutes at 5s intervals) const cpuHistoryRef = useRef([]) const memoryHistoryRef = useRef([]) const networkTrafficHistoryRef = useRef>(new Map()) const [cpuHistory, setCpuHistory] = useState([]) const [memoryHistory, setMemoryHistory] = useState([]) useEffect(() => { // Check authentication const token = localStorage.getItem("access_token") if (!token) { router.push("/login") return } // Load data if authenticated const init = async () => { await checkZfsStatus() await fetchPools() await loadSystemStats() const interval = setInterval(fetchPools, 30000) // Refresh every 30 seconds return () => clearInterval(interval) } init() }, [router]) const checkZfsStatus = async () => { try { const response = await fetch("/api/status") const data = await response.json() setZfsAvailable(data.zfs_available ?? false) return data.zfs_available ?? false } catch (err) { console.error("Failed to check ZFS status:", err) setZfsAvailable(false) return false } } const formatBytes = (bytes: number) => { if (bytes === 0) return "0 B" const k = 1024 const sizes = ["B", "KB", "MB", "GB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i] } const formatUptime = (seconds: number) => { const days = Math.floor(seconds / 86400) const hours = Math.floor((seconds % 86400) / 3600) const minutes = Math.floor((seconds % 3600) / 60) const parts = [] if (days > 0) parts.push(`${days} day${days > 1 ? 's' : ''}`) if (hours > 0) parts.push(`${hours} hr${hours > 1 ? 's' : ''}`) if (minutes > 0 || parts.length === 0) parts.push(`${minutes} min`) return parts.join(', ') } const formatBootTime = (timestamp: number) => { try { return new Date(timestamp * 1000).toLocaleString() } catch { return 'N/A' } } // Sparkline helper: convert array of 0-100 values to SVG polyline points const sparklinePoints = (data: number[], width = 120, height = 32): string => { if (data.length < 2) return "" const step = width / (data.length - 1) return data.map((v, i) => `${i * step},${height - Math.max(0, Math.min(100, v)) / 100 * height}`).join(" ") } const loadSystemStats = async () => { try { const [sysInfo, memInfo, cpuData, uptime, network, traffic, diskio] = await Promise.all([ api.getSystemInfo().catch(() => null), api.getMemory().catch(() => null), api.getCpuInfo().catch(() => null), api.getUptime().catch(() => null), api.getNetwork().catch(() => null), api.getNetworkTraffic().catch(() => null), api.getDiskIO().catch(() => null), ]) setSystemInfo(sysInfo) setMemoryInfo(memInfo) setCpuInfo(cpuData) setUptimeInfo(uptime) setNetworkInfo(network) setNetworkTraffic(traffic) setDiskIO(diskio) // Add to history if (cpuData?.percent !== undefined) { const newCpuHistory = [...cpuHistoryRef.current, cpuData.percent] if (newCpuHistory.length > 30) newCpuHistory.shift() cpuHistoryRef.current = newCpuHistory setCpuHistory(newCpuHistory) } if (memInfo?.total && memInfo?.used !== undefined) { const memPercent = (memInfo.used / memInfo.total) * 100 const newMemHistory = [...memoryHistoryRef.current, memPercent] if (newMemHistory.length > 30) newMemHistory.shift() memoryHistoryRef.current = newMemHistory setMemoryHistory(newMemHistory) } } catch (err) { console.error("Failed to load system stats:", err) } } // Periodic update for history every 5 seconds useEffect(() => { const interval = setInterval(async () => { try { const [cpuData, memInfo, traffic, diskio] = await Promise.all([ api.getCpuInfo().catch(() => null), api.getMemory().catch(() => null), api.getNetworkTraffic().catch(() => null), api.getDiskIO().catch(() => null), ]) if (cpuData?.percent !== undefined) { const newCpuHistory = [...cpuHistoryRef.current, cpuData.percent] if (newCpuHistory.length > 30) newCpuHistory.shift() cpuHistoryRef.current = newCpuHistory setCpuHistory(newCpuHistory) } if (memInfo?.total && memInfo?.used !== undefined) { const memPercent = (memInfo.used / memInfo.total) * 100 const newMemHistory = [...memoryHistoryRef.current, memPercent] if (newMemHistory.length > 30) newMemHistory.shift() memoryHistoryRef.current = newMemHistory setMemoryHistory(newMemHistory) } if (traffic?.interfaces) { setNetworkTraffic(traffic) } if (diskio?.disks) { setDiskIO(diskio) } if (traffic?.interfaces) { const newHistory = new Map(networkTrafficHistoryRef.current) for (const iface of traffic.interfaces) { if (iface.name === 'lo') continue // Skip loopback const key = `${iface.name}_rx` const current = newHistory.get(key) || [] const updated = [...current, iface.rx_bytes] if (updated.length > 30) updated.shift() newHistory.set(key, updated) } networkTrafficHistoryRef.current = newHistory } } catch (err) { // Silently fail } }, 5000) // Every 5 seconds return () => clearInterval(interval) }, []) const fetchPools = async () => { // If ZFS is not available, don't try to fetch pools if (zfsAvailable === false) { setLoading(false) return } try { setLoading(true) setError(null) const data = await api.getPools() setPools(data) setLastUpdate(new Date()) } catch (err) { const message = err instanceof Error ? err.message : "Failed to fetch pools" setError(message) console.error(err) } finally { setLoading(false) } } return (
{/* Page Header */}

Dashboard

{lastUpdate ? `Last updated: ${lastUpdate.toLocaleTimeString()}` : "Loading..."}

{/* Quick Stats - System Metrics (Phase 3a) */} {(systemInfo || memoryInfo || cpuInfo || uptimeInfo) && (
{/* Hostname & Uptime */} {systemInfo && uptimeInfo && ( System

{systemInfo.hostname}

Uptime: {uptimeInfo.uptime_string}

{systemInfo.kernel}

)} {/* CPU Usage with Sparkline */} {cpuInfo && ( CPU

{cpuInfo.percent !== undefined ? cpuInfo.percent.toFixed(1) : "N/A"}%

{cpuHistory.length > 1 && ( )}

Load: {cpuInfo.load_average?.[0]?.toFixed(2)}

)} {/* Memory Usage with Sparkline */} {memoryInfo && ( Memory

{((memoryInfo.used / memoryInfo.total) * 100).toFixed(1)}%

{memoryHistory.length > 1 && ( )}

{formatBytes(memoryInfo.used)} / {formatBytes(memoryInfo.total)}

)} {/* Uptime */} {uptimeInfo && ( System Uptime

Uptime

{formatUptime(uptimeInfo.uptime_seconds || 0)}

Booted

{formatBootTime(uptimeInfo.boot_time || 0)}

)} {/* Disk Usage */} Storage
{zfsAvailable ? (

ZFS

View pools below

) : (

N/A

ZFS not available

)}
)} {/* System Details Card */} {systemInfo && ( Systeminformationen
{systemInfo.model && (

Modell

{systemInfo.model}

)} {systemInfo.machine_id && (

Maschinen-ID

{systemInfo.machine_id}

)} {systemInfo.processor && (

Prozessor

{systemInfo.processor}

)} {systemInfo.kernel && (

Kernel

{systemInfo.kernel}

)} {systemInfo.system && (

Betriebssystem

{systemInfo.system}

)} {systemInfo.domain && (

Domain

{systemInfo.domain}

)}
)} {/* Error Message */} {error && zfsAvailable !== false && (

Error

{error}

)} {/* Loading State */} {loading && pools.length === 0 && zfsAvailable !== false && (

Loading pools...

)} {/* Network Interfaces */} {networkInfo?.interfaces && networkInfo.interfaces.length > 0 && (

Network Interfaces

{networkInfo.interfaces.map((iface: any) => ( ))}
Interface Status IP Address
{iface.name} {iface.state} {iface.addresses && iface.addresses.length > 0 ? (
{iface.addresses.map((addr: any, idx: number) => (
{addr.local}
))}
) : ( "—" )}
)} {/* Network Traffic */} {networkTraffic?.interfaces && networkTraffic.interfaces.length > 0 && (

Network Traffic

{networkTraffic.interfaces .filter((iface: any) => iface.name !== 'lo') // Skip loopback .map((iface: any) => ( {iface.name}

RX

{formatBytes(iface.rx_bytes)}

{(iface.rx_packets ?? 0).toLocaleString()} packets

TX

{formatBytes(iface.tx_bytes)}

{(iface.tx_packets ?? 0).toLocaleString()} packets

{(iface.rx_drops > 0 || iface.tx_drops > 0) && (

⚠ {iface.rx_drops + iface.tx_drops} dropped packets

)}
))}
)} {/* Disk I/O */} {diskIO?.disks && diskIO.disks.length > 0 && (

Disk I/O

{diskIO.disks.map((disk: any) => ( {disk.name}

Reads

{disk.reads_completed.toLocaleString()} ops

{formatBytes(disk.reads_bytes)}

Writes

{disk.writes_completed.toLocaleString()} ops

{formatBytes(disk.writes_bytes)}

Total: {formatBytes(disk.reads_bytes + disk.writes_bytes)}

))}
)} {/* Pools Grid */} {!loading && pools.length > 0 && zfsAvailable !== false && (

Storage Pools ({pools.length})

{pools.map((pool) => ( router.push("/zfs")} /> ))}
)} {/* Empty State */} {!loading && pools.length === 0 && !error && zfsAvailable !== false && ( No Pools Found

No ZFS pools are available on this system. Create a new pool to get started.

)}
) }