Files
naxos-ui/src/views/OverviewView.tsx
T
2026-09-04 13:44:15 +02:00

519 lines
26 KiB
TypeScript

import React, { useState } from 'react';
import {
HardDrive,
Cpu,
Layers,
FolderSync,
ShieldCheck,
Activity,
ArrowUpRight,
TrendingUp,
Zap,
} from 'lucide-react';
import { AppIcon } from '../components/AppIcon.js';
import type { SystemStatus, ZfsPool, InstalledApp } from '../types/index.js';
interface OverviewViewProps {
status: SystemStatus | null;
pools: ZfsPool[];
apps: InstalledApp[];
onNavigate: (tab: string) => void;
}
export const OverviewView: React.FC<OverviewViewProps> = ({
status,
pools,
apps,
onNavigate,
}) => {
const primaryPool = pools[0];
const [telemetryRange, setTelemetryRange] = useState<'15m' | '1h' | '6h' | '24h'>('1h');
// Sparkline data based on selected time range
const throughputData = {
'15m': [34, 45, 62, 50, 78, 110, 95, 82, 60, 88, 115, 142, 90, 75, 105],
'1h': [42, 65, 30, 85, 95, 120, 80, 45, 60, 90, 110, 75, 40, 95, 130],
'6h': [20, 35, 45, 80, 110, 145, 130, 95, 70, 85, 60, 50, 65, 90, 85],
'24h': [15, 25, 40, 65, 90, 120, 140, 110, 85, 70, 60, 55, 70, 80, 95],
}[telemetryRange];
const arcRatioData = {
'15m': [98.2, 98.4, 98.5, 98.3, 98.6, 98.8, 98.7, 98.5, 98.9, 98.6, 98.7, 98.6, 98.8, 98.7, 98.6],
'1h': [97.8, 98.1, 98.4, 98.2, 98.6, 98.8, 98.5, 98.7, 98.4, 98.9, 98.6, 98.7, 98.5, 98.8, 98.6],
'6h': [97.2, 97.6, 98.0, 98.3, 98.5, 98.7, 98.4, 98.6, 98.8, 98.5, 98.7, 98.6, 98.4, 98.6, 98.6],
'24h': [96.8, 97.2, 97.9, 98.1, 98.4, 98.6, 98.7, 98.5, 98.6, 98.7, 98.5, 98.6, 98.7, 98.8, 98.6],
}[telemetryRange];
const cpuData = {
'15m': [8, 12, 14, 22, 38, 18, 11, 9, 15, 18, 28, 15, 12, 14, 11],
'1h': [10, 14, 18, 25, 42, 19, 12, 10, 16, 20, 32, 16, 14, 15, 12],
'6h': [8, 10, 15, 30, 48, 25, 14, 12, 18, 22, 35, 18, 15, 16, 13],
'24h': [6, 8, 12, 28, 45, 30, 15, 11, 16, 24, 38, 20, 16, 18, 12],
}[telemetryRange];
return (
<div className="space-y-6">
{/* Top Banner / Hero Header with Portara Styling */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-r from-slate-900 via-slate-900/90 to-cyan-950/40 border border-slate-800 p-6 shadow-xl">
<div className="absolute right-0 top-0 bottom-0 w-96 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-cyan-500/10 via-transparent to-transparent pointer-events-none" />
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 relative z-10">
<div className="flex items-start gap-4">
<div className="h-12 w-12 rounded-xl bg-slate-950 border border-cyan-500/30 p-2 flex items-center justify-center shadow-lg shadow-cyan-500/10 shrink-0">
<img src="/logo.svg" alt="NaxOS" className="h-full w-full object-contain filter drop-shadow-[0_2px_6px_rgba(12,179,229,0.4)]" />
</div>
<div>
<div className="flex items-center gap-2.5">
<h2 className="text-xl font-bold text-white tracking-tight">Appliance Overview</h2>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/15 text-cyan-300 border border-cyan-500/30 font-mono">
NixOS 26.05
</span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-blue-500/10 text-blue-300 border border-blue-500/20 font-mono">
OpenZFS 2.2
</span>
</div>
<p className="text-xs text-slate-400 mt-1">
Active Node: <strong className="text-slate-200">{status?.hostname || 'naxos-storage'}</strong> · Generation <strong className="text-slate-200">{status?.nixosGeneration || 42}</strong> · GitOps Engine Synchronized
</p>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => onNavigate('storage')}
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-slate-800/80 hover:bg-slate-700 text-xs font-semibold text-slate-200 transition border border-slate-700 shadow-sm"
>
<HardDrive className="h-3.5 w-3.5 text-cyan-400" />
<span>Manage Storage</span>
<ArrowUpRight className="h-3.5 w-3.5 text-slate-400" />
</button>
<button
onClick={() => onNavigate('apps')}
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-xs font-semibold text-white transition shadow-sm shadow-cyan-600/30"
>
<Layers className="h-3.5 w-3.5" />
<span>App Store</span>
<ArrowUpRight className="h-3.5 w-3.5" />
</button>
</div>
</div>
</div>
{/* KPI Cards Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Storage Pool Card */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Pool: {primaryPool?.name || 'tank'}</span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
{primaryPool?.health || 'ONLINE'}
</span>
</div>
<div className="mt-4">
<div className="text-2xl font-bold text-white tracking-tight">
{primaryPool?.allocated || '8.2T'} <span className="text-xs text-slate-400 font-normal">/ {primaryPool?.size || '14.5T'}</span>
</div>
{/* Progress Bar */}
<div className="w-full bg-slate-800 h-2 rounded-full mt-2 overflow-hidden">
<div
className="bg-gradient-to-r from-cyan-500 to-blue-500 h-full rounded-full transition-all duration-500"
style={{ width: `${primaryPool?.capacityPercent || 56}%` }}
/>
</div>
</div>
<div className="mt-3 flex items-center justify-between text-[11px] text-slate-400">
<span>Free: {primaryPool?.free || '6.3T'}</span>
<span>Frag: {primaryPool?.fragmentation || '14%'}</span>
</div>
</div>
{/* ZFS ARC Hit Ratio */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">ZFS ARC Cache</span>
<ShieldCheck className="h-4 w-4 text-cyan-400" />
</div>
<div className="mt-4">
<div className="text-2xl font-bold text-white tracking-tight flex items-baseline gap-2">
<span>{status?.arcHitRatioPercent || 98.6}%</span>
<span className="text-xs text-cyan-400 font-normal">Efficiency</span>
</div>
<p className="text-xs text-slate-400 mt-1">
ARC Size: <strong className="text-slate-200">4.0 GB</strong> (Target: 4.0 GB)
</p>
</div>
<div className="mt-3 text-[11px] text-cyan-400 flex items-center gap-1">
<TrendingUp className="h-3 w-3" />
<span>Optimal cache hit performance</span>
</div>
</div>
{/* Workloads Card */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Running Workloads</span>
<Layers className="h-4 w-4 text-sky-400" />
</div>
<div className="mt-4">
<div className="text-2xl font-bold text-white tracking-tight">
{apps.length || 2} <span className="text-xs text-slate-400 font-normal">Active Services</span>
</div>
<div className="flex items-center gap-1.5 mt-2.5">
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Immich Photo Hub">
<AppIcon name="immich" className="h-5 w-5" />
</div>
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Jellyfin Media Server">
<AppIcon name="jellyfin" className="h-5 w-5" />
</div>
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Telemetry Collector">
<AppIcon name="grafana" className="h-5 w-5" />
</div>
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Docker Engine">
<AppIcon name="docker" className="h-5 w-5" />
</div>
</div>
</div>
<div className="mt-3 text-[11px] text-slate-400">
Runtime Engine: <span className="text-slate-200">Docker + Systemd</span>
</div>
</div>
{/* Network Shares */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Active File Shares</span>
<FolderSync className="h-4 w-4 text-blue-400" />
</div>
<div className="mt-4">
<div className="text-2xl font-bold text-white tracking-tight">
{status?.activeSharesCount || 3}
</div>
<p className="text-xs text-slate-400 mt-1">
SMB (Time Machine) + NFS v4
</p>
</div>
<div className="mt-3 text-[11px] text-slate-400">
Bonjour / mDNS discovery active
</div>
</div>
</div>
{/* SEAMLESS LIVE TELEMETRY & SYSTEM PERFORMANCE */}
<div className="p-6 rounded-2xl bg-slate-900/70 border border-slate-800 space-y-5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800/80 pb-4">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-xl bg-cyan-500/10 border border-cyan-500/20 text-cyan-400">
<Activity className="h-4 w-4" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="text-sm font-bold text-white">Live System & Storage Telemetry</h3>
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-500" />
</span>
</div>
<p className="text-[11px] text-slate-400 mt-0.5">
Real-time OpenZFS ARC caching, storage pool I/O bandwidth, and host telemetry
</p>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center p-1 bg-slate-950 border border-slate-800 rounded-xl text-xs">
{(['15m', '1h', '6h', '24h'] as const).map((range) => (
<button
key={range}
onClick={() => setTelemetryRange(range)}
className={`px-2.5 py-1 rounded-lg font-medium transition ${
telemetryRange === range
? 'bg-cyan-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white'
}`}
>
{range}
</button>
))}
</div>
<div className="text-[11px] font-mono text-cyan-400/80 px-2 py-1 bg-slate-950 rounded-lg border border-slate-800">
Live Feed
</div>
</div>
</div>
{/* 4-Panel Telemetry Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Telemetry 1: ZFS ARC Cache Hit Ratio */}
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
<Zap className="h-3.5 w-3.5 text-cyan-400" />
<span>ZFS ARC Hit Ratio</span>
</div>
<span className="text-xs font-mono font-bold text-cyan-400">98.6%</span>
</div>
<div className="h-28 flex items-end gap-1 pt-3 px-1">
{arcRatioData.map((val, i) => (
<div
key={i}
className="flex-1 bg-gradient-to-t from-cyan-600/30 to-cyan-400 rounded-t transition-all hover:bg-cyan-300"
style={{ height: `${(val - 90) * 10}%` }}
title={`${val}%`}
/>
))}
</div>
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
<span>Data: 99.1%</span>
<span>Metadata: 97.8%</span>
</div>
</div>
{/* Telemetry 2: Storage Pool Read/Write Bandwidth */}
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
<HardDrive className="h-3.5 w-3.5 text-blue-400" />
<span>Pool I/O Throughput</span>
</div>
<span className="text-xs font-mono font-bold text-blue-300">142 MB/s Peak</span>
</div>
<div className="h-28 flex items-end gap-1 pt-3 px-1">
{throughputData.map((val, i) => (
<div
key={i}
className="flex-1 bg-gradient-to-t from-blue-600/30 to-blue-400 rounded-t transition-all hover:bg-blue-300"
style={{ height: `${(val / 150) * 100}%` }}
title={`${val} MB/s`}
/>
))}
</div>
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
<span>Read: 94 MB/s</span>
<span>Write: 48 MB/s</span>
</div>
</div>
{/* Telemetry 3: CPU Core Utilization */}
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
<Cpu className="h-3.5 w-3.5 text-teal-400" />
<span>CPU Load & Cores</span>
</div>
<span className="text-xs font-mono font-bold text-teal-300">12.4% Avg</span>
</div>
<div className="h-28 flex items-end gap-1 pt-3 px-1">
{cpuData.map((val, i) => (
<div
key={i}
className="flex-1 bg-gradient-to-t from-teal-600/30 to-teal-400 rounded-t transition-all hover:bg-teal-300"
style={{ height: `${val * 2.2}%` }}
title={`${val}%`}
/>
))}
</div>
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
<span>4 Cores active</span>
<span>Load: 0.42, 0.38</span>
</div>
</div>
{/* Telemetry 4: Memory & ARC Footprint */}
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
<Layers className="h-3.5 w-3.5 text-indigo-400" />
<span>RAM & ARC Footprint</span>
</div>
<span className="text-xs font-mono font-bold text-slate-300">13.2 / 32.0 GB</span>
</div>
<div className="h-28 flex flex-col justify-center gap-2.5 px-1">
<div>
<div className="flex justify-between text-[11px] mb-1">
<span className="text-slate-400">ZFS ARC Cache</span>
<span className="font-mono text-cyan-400 font-semibold">4.0 GB (100%)</span>
</div>
<div className="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
<div className="bg-cyan-400 h-full rounded-full" style={{ width: '100%' }} />
</div>
</div>
<div>
<div className="flex justify-between text-[11px] mb-1">
<span className="text-slate-400">Apps & Workloads</span>
<span className="font-mono text-blue-400 font-semibold">9.2 GB</span>
</div>
<div className="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
<div className="bg-blue-400 h-full rounded-full" style={{ width: '35%' }} />
</div>
</div>
</div>
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
<span>Free: 18.8 GB</span>
<span>ZFS ARC Limit: 4 GB</span>
</div>
</div>
</div>
</div>
{/* Homarr-Style Active Services & Workloads Hub */}
<div className="p-6 rounded-2xl bg-slate-900/70 border border-slate-800">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-cyan-400" />
<h3 className="text-sm font-semibold text-white">Appliance Services & Workloads</h3>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
Native Workload Engine
</span>
</div>
<button
onClick={() => onNavigate('apps')}
className="text-xs text-cyan-400 hover:text-cyan-300 font-medium flex items-center gap-1"
>
<span>App Store & Catalog</span>
<ArrowUpRight className="h-3.5 w-3.5" />
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Workload 1: Immich Photo Hub */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
<div>
<div className="flex items-start justify-between">
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
<AppIcon name="immich" className="h-8 w-8" />
</div>
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
<span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
</div>
</div>
<h4 className="text-sm font-bold text-white mt-3">Immich Photo Hub</h4>
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
Hardware-accelerated photo & video backup with Intel QuickSync ML
</p>
</div>
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
:2283
</span>
<a
href="http://naxos.local:2283"
target="_blank"
rel="noreferrer"
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
>
<span>Launch</span>
<ArrowUpRight className="h-3 w-3" />
</a>
</div>
</div>
{/* Workload 2: Jellyfin Media Server */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
<div>
<div className="flex items-start justify-between">
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
<AppIcon name="jellyfin" className="h-8 w-8" />
</div>
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
<span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
</div>
</div>
<h4 className="text-sm font-bold text-white mt-3">Jellyfin Media Server</h4>
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
4K HDR media streaming with hardware transcoding & DLNA
</p>
</div>
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
:8096
</span>
<a
href="http://naxos.local:8096"
target="_blank"
rel="noreferrer"
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
>
<span>Launch</span>
<ArrowUpRight className="h-3 w-3" />
</a>
</div>
</div>
{/* Workload 3: Nextcloud Hub */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
<div>
<div className="flex items-start justify-between">
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
<AppIcon name="nextcloud" className="h-8 w-8" />
</div>
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-slate-500" />
<span className="text-[10px] font-mono text-slate-400 font-medium">CATALOG</span>
</div>
</div>
<h4 className="text-sm font-bold text-white mt-3">Nextcloud Hub</h4>
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
Collaborative office, encrypted file sync, and calendar sharing
</p>
</div>
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
Ready
</span>
<button
onClick={() => onNavigate('apps')}
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
>
<span>Deploy</span>
<ArrowUpRight className="h-3 w-3" />
</button>
</div>
</div>
{/* Workload 4: Paperless-ngx */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
<div>
<div className="flex items-start justify-between">
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
<AppIcon name="paperless" className="h-8 w-8" />
</div>
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-slate-500" />
<span className="text-[10px] font-mono text-slate-400 font-medium">CATALOG</span>
</div>
</div>
<h4 className="text-sm font-bold text-white mt-3">Paperless-ngx</h4>
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
Automated document archiving with OCR and full-text search
</p>
</div>
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
Ready
</span>
<button
onClick={() => onNavigate('apps')}
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
>
<span>Deploy</span>
<ArrowUpRight className="h-3 w-3" />
</button>
</div>
</div>
</div>
</div>
</div>
);
};