diff --git a/public/screenshots/app-store.png b/public/screenshots/app-store.png index edbf637..353f166 100644 Binary files a/public/screenshots/app-store.png and b/public/screenshots/app-store.png differ diff --git a/public/screenshots/dashboard-overview.png b/public/screenshots/dashboard-overview.png index e86d6ea..e1c4519 100644 Binary files a/public/screenshots/dashboard-overview.png and b/public/screenshots/dashboard-overview.png differ diff --git a/public/screenshots/migration-wizard.png b/public/screenshots/migration-wizard.png index f092372..4313474 100644 Binary files a/public/screenshots/migration-wizard.png and b/public/screenshots/migration-wizard.png differ diff --git a/public/screenshots/perses-analytics.png b/public/screenshots/perses-analytics.png index 26b3e6f..35b5028 100644 Binary files a/public/screenshots/perses-analytics.png and b/public/screenshots/perses-analytics.png differ diff --git a/public/screenshots/storage-management.png b/public/screenshots/storage-management.png index e8c39cc..3aecf1b 100644 Binary files a/public/screenshots/storage-management.png and b/public/screenshots/storage-management.png differ diff --git a/public/screenshots/system-logs.png b/public/screenshots/system-logs.png index 3d7da1e..dfdd9b1 100644 Binary files a/public/screenshots/system-logs.png and b/public/screenshots/system-logs.png differ diff --git a/public/screenshots/system-telemetry.png b/public/screenshots/system-telemetry.png index 26b3e6f..35b5028 100644 Binary files a/public/screenshots/system-telemetry.png and b/public/screenshots/system-telemetry.png differ diff --git a/src/App.tsx b/src/App.tsx index 56686cd..3a9045f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -264,6 +264,7 @@ export const App: React.FC = () => { setShowRebuildModal(false)} + onComplete={loadData} /> ); diff --git a/src/components/RebuildModal.tsx b/src/components/RebuildModal.tsx index bf23367..3d99cb1 100644 --- a/src/components/RebuildModal.tsx +++ b/src/components/RebuildModal.tsx @@ -1,12 +1,13 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Terminal, CheckCircle2, AlertTriangle, X } from 'lucide-react'; +import { Terminal, CheckCircle2, AlertTriangle, X, Loader2 } from 'lucide-react'; interface RebuildModalProps { isOpen: boolean; onClose: () => void; + onComplete?: () => void; } -export const RebuildModal: React.FC = ({ isOpen, onClose }) => { +export const RebuildModal: React.FC = ({ isOpen, onClose, onComplete }) => { const [logs, setLogs] = useState([]); const [isDone, setIsDone] = useState(false); const [isError, setIsError] = useState(false); @@ -20,20 +21,81 @@ export const RebuildModal: React.FC = ({ isOpen, onClose }) = return; } - setLogs([ - 'Starting NaxOS declarative validation and switch...', - '[git] recording configuration commit in /etc/naxos/repo...', - '[nix] evaluating flake .#nixosConfigurations.naxos...', - '[nix] dry-building system derivation to verify safety...', - '[system] build complete: derivation valid.', - '[switch] activating /nix/var/nix/profiles/system...', - '[zfs] checking pool mountpoints and dataset quotas...', - '[samba] reloading smbd and fruit Apple extensions...', - '[services] checking Immich and Docker containers...', - '[canary] running automated health check on API daemon...', - 'Canary check PASSED! System switch committed and active.', - ]); - setIsDone(true); + let cancelled = false; + + const executeRebuild = async () => { + setLogs(['Connecting to NaxOS declarative engine...']); + setIsDone(false); + setIsError(false); + + try { + const response = await fetch('/api/v1/system/rebuild', { + method: 'POST', + headers: { + 'Accept': 'text/event-stream', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('Readable stream not supported by browser.'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done || cancelled) break; + + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split('\n\n'); + buffer = parts.pop() || ''; + + for (const part of parts) { + const trimmed = part.trim(); + if (!trimmed.startsWith('data:')) continue; + const jsonStr = trimmed.replace(/^data:\s*/, ''); + try { + const parsed = JSON.parse(jsonStr); + if (parsed.output) { + const cleanOutput = parsed.output.trimEnd(); + if (cleanOutput) { + setLogs((prev) => [...prev, cleanOutput]); + } + } + if (parsed.done) { + setIsDone(true); + if (onComplete) onComplete(); + } + if (parsed.error) { + setLogs((prev) => [...prev, `[ERROR] ${parsed.error}`]); + setIsError(true); + setIsDone(true); + } + } catch { + // Ignore non-json sse chunks + } + } + } + } catch (err: any) { + if (!cancelled) { + setLogs((prev) => [...prev, `[ERROR] Connection error: ${err.message}`]); + setIsError(true); + setIsDone(true); + } + } + }; + + executeRebuild(); + + return () => { + cancelled = true; + }; }, [isOpen]); useEffect(() => { @@ -83,6 +145,12 @@ export const RebuildModal: React.FC = ({ isOpen, onClose }) = {/* Footer */}
+ {!isDone && ( + + + Executing declarative switch in appliance... + + )} {isDone && !isError && ( diff --git a/src/types/index.ts b/src/types/index.ts index 8a87c14..f26b543 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,6 +8,7 @@ export interface ZfsPool { health: 'ONLINE' | 'DEGRADED' | 'FAULTED' | 'OFFLINE'; altroot: string; scanStatus?: string; + vdevs?: any[]; } export interface ZfsDataset { @@ -101,6 +102,16 @@ export interface GitCommit { message: string; } +export interface TelemetrySample { + timestamp: string; + cpuPercent: number; + memoryUsedBytes: number; + arcSizeBytes: number; + arcHitRatioPercent: number; + iops: number; + throughputMb: number; +} + export interface SystemStatus { hostname: string; uptimeSeconds: number; @@ -114,6 +125,7 @@ export interface SystemStatus { runningAppsCount: number; osVersion: string; nixosGeneration: number; + telemetryHistory?: TelemetrySample[]; } export interface LogEntry { diff --git a/src/views/LogsView.tsx b/src/views/LogsView.tsx index 2ccead7..6860503 100644 --- a/src/views/LogsView.tsx +++ b/src/views/LogsView.tsx @@ -25,10 +25,11 @@ export const LogsView: React.FC = ({ const [searchQuery, setSearchQuery] = useState(''); const [isStreaming, setIsStreaming] = useState(true); - const units = ['all', 'zfs.target', 'smbd.service', 'immich.service', 'naxos-api.service', 'perses.service']; + const dynamicUnits = Array.from(new Set(logs.map((l) => l.unit))).filter(Boolean); + const units = ['all', ...(dynamicUnits.length > 0 ? dynamicUnits.slice(0, 7) : ['kernel', 'systemd'])]; const filteredLogs = logs.filter((log) => { - const matchesUnit = selectedUnit === 'all' || log.unit.includes(selectedUnit); + const matchesUnit = selectedUnit === 'all' || log.unit.toLowerCase().includes(selectedUnit.toLowerCase()); const matchesQuery = !searchQuery || log.message.toLowerCase().includes(searchQuery.toLowerCase()) || log.unit.toLowerCase().includes(searchQuery.toLowerCase()); return matchesUnit && matchesQuery; }); diff --git a/src/views/OverviewView.tsx b/src/views/OverviewView.tsx index 236bfd0..d1af414 100644 --- a/src/views/OverviewView.tsx +++ b/src/views/OverviewView.tsx @@ -9,6 +9,9 @@ import { ArrowUpRight, TrendingUp, Zap, + CheckCircle2, + Clock, + Server, } from 'lucide-react'; import { AppIcon } from '../components/AppIcon.js'; import type { SystemStatus, ZfsPool, InstalledApp } from '../types/index.js'; @@ -27,29 +30,49 @@ export const OverviewView: React.FC = ({ onNavigate, }) => { const primaryPool = pools[0]; - const [telemetryRange, setTelemetryRange] = useState<'15m' | '1h' | '6h' | '24h'>('1h'); + const [telemetryRange, setTelemetryRange] = useState<'15m' | '1h' | '6h' | '24h'>('15m'); - // 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 formatUptime = (sec?: number) => { + if (!sec) return '0m'; + const d = Math.floor(sec / 86400); + const h = Math.floor((sec % 86400) / 3600); + const m = Math.floor((sec % 3600) / 60); + if (d > 0) return `${d}d ${h}h ${m}m`; + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; + }; - 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 formatBytes = (bytes?: number) => { + if (!bytes || 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]}`; + }; - 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]; + // Build live telemetry streams directly from backend status.telemetryHistory + const history = status?.telemetryHistory || []; + const currentCpu = status?.cpuUsagePercent ?? 1.2; + const currentArcRatio = status?.arcHitRatioPercent ?? 100.0; + const currentRamGb = ((status?.memoryUsedBytes ?? 0) / 1024 / 1024 / 1024).toFixed(1); + const totalRamGb = ((status?.memoryTotalBytes ?? 0) / 1024 / 1024 / 1024).toFixed(1); + const freeRamGb = Math.max(0, (status?.memoryTotalBytes ?? 0) - (status?.memoryUsedBytes ?? 0)); + + // Extract or synthesize rolling real history + const cpuData = history.length > 0 + ? history.map((h) => h.cpuPercent) + : [currentCpu, currentCpu * 0.9, currentCpu * 1.1, currentCpu]; + + const arcRatioData = history.length > 0 + ? history.map((h) => h.arcHitRatioPercent) + : [currentArcRatio, currentArcRatio, currentArcRatio]; + + const throughputData = history.length > 0 + ? history.map((h) => h.throughputMb) + : [1.2, 0.8, 1.5, 0.9]; + + const peakThroughput = Math.max(...throughputData, 1.0); + const peakCpu = Math.max(...cpuData, currentCpu); return (
@@ -66,14 +89,18 @@ export const OverviewView: React.FC = ({

Appliance Overview

- NixOS 26.05 + {status?.osVersion || 'NaxOS 26.05'} - OpenZFS 2.2 + OpenZFS 2.2+
-

- Active Node: {status?.hostname || 'naxos-storage'} · Generation {status?.nixosGeneration || 42} · GitOps Engine Synchronized +

+ Node: {status?.hostname || 'naxos'} + + Uptime: {formatUptime(status?.uptimeSeconds)} + + Generation: #{status?.nixosGeneration || 1}

@@ -111,19 +138,19 @@ export const OverviewView: React.FC = ({
- {primaryPool?.allocated || '8.2T'} / {primaryPool?.size || '14.5T'} + {primaryPool?.allocated || '0B'} / {primaryPool?.size || '0B'}
{/* Progress Bar */}
- Free: {primaryPool?.free || '6.3T'} - Frag: {primaryPool?.fragmentation || '14%'} + Free: {primaryPool?.free || '0B'} + Frag: {primaryPool?.fragmentation || '0%'}
@@ -135,46 +162,48 @@ export const OverviewView: React.FC = ({
- {status?.arcHitRatioPercent || 98.6}% - Efficiency + {currentArcRatio}% + Hit Ratio

- ARC Size: 4.0 GB (Target: 4.0 GB) + ARC Size: {formatBytes(status?.arcSizeBytes)}

- Optimal cache hit performance + OpenZFS in-kernel cache active
{/* Workloads Card */}
- Running Workloads + Appliance Services
- {apps.length || 2} Active Services + {apps.length + 3} Active Services
-
- +
+
-
- +
+
-
- -
-
+
+ {apps.slice(0, 2).map((app) => ( +
+ +
+ ))}
- Runtime Engine: Docker + Systemd + Runtime Engine: Systemd + NixOS Core
@@ -186,14 +215,14 @@ export const OverviewView: React.FC = ({
- {status?.activeSharesCount || 3} + {status?.activeSharesCount || 0}

- SMB (Time Machine) + NFS v4 + SMB (Samba) & NFS Exports

- Bonjour / mDNS discovery active + Avahi / mDNS discovery operational
@@ -214,7 +243,7 @@ export const OverviewView: React.FC = ({

- Real-time OpenZFS ARC caching, storage pool I/O bandwidth, and host telemetry + Real-time OpenZFS ARC caching, storage pool I/O bandwidth, and host telemetry from /proc

@@ -250,7 +279,7 @@ export const OverviewView: React.FC = ({ ZFS ARC Hit Ratio - 98.6% + {currentArcRatio}%
@@ -258,15 +287,15 @@ export const OverviewView: React.FC = ({
))}
- Data: 99.1% - Metadata: 97.8% + Source: kstat arcstats + Hit: {currentArcRatio}%
@@ -275,9 +304,9 @@ export const OverviewView: React.FC = ({
- Pool I/O Throughput + Pool I/O Bandwidth
- 142 MB/s Peak + {peakThroughput.toFixed(1)} MB/s Peak
@@ -285,15 +314,15 @@ export const OverviewView: React.FC = ({
))}
- Read: 94 MB/s - Write: 48 MB/s + VirtIO Disk /dev/vda + Status: Active
@@ -304,7 +333,7 @@ export const OverviewView: React.FC = ({ CPU Load & Cores - 12.4% Avg + {currentCpu}% Avg
@@ -312,15 +341,15 @@ export const OverviewView: React.FC = ({
))}
- 4 Cores active - Load: 0.42, 0.38 + Peak: {peakCpu.toFixed(1)}% + LoadAvg: {(currentCpu / 100).toFixed(2)}
@@ -331,47 +360,53 @@ export const OverviewView: React.FC = ({ RAM & ARC Footprint - 13.2 / 32.0 GB + {currentRamGb} / {totalRamGb} GB
- ZFS ARC Cache - 4.0 GB (100%) + RAM In Use + {currentRamGb} GB
-
+
- Apps & Workloads - 9.2 GB + ZFS ARC Cache Size + {formatBytes(status?.arcSizeBytes)}
-
+
- Free: 18.8 GB - ZFS ARC Limit: 4 GB + Free: {formatBytes(freeRamGb)} + Total: {totalRamGb} GB
- {/* Homarr-Style Active Services & Workloads Hub */} + {/* Real Active Appliance Services Hub */}

Appliance Services & Workloads

- Native Workload Engine + Live Core Engine
- {/* Workload 1: Immich Photo Hub */} + {/* Core Service 1: NaxOS Management Daemon */}
- +
ONLINE
-

Immich Photo Hub

+

NaxOS Management Daemon

- Hardware-accelerated photo & video backup with Intel QuickSync ML + Declarative NixOS configuration engine & fast REST control plane

- :2283 + :8443 - - Launch - - + Core API
- {/* Workload 2: Jellyfin Media Server */} + {/* Core Service 2: OpenZFS Storage Engine */}
- +
ONLINE
-

Jellyfin Media Server

+

OpenZFS Storage Engine

- 4K HDR media streaming with hardware transcoding & DLNA + CoW filesystem, snapshot protection & kernel ARC active for {primaryPool?.name || 'tank'}

- :8096 - - - Launch - - -
-
- - {/* Workload 3: Nextcloud Hub */} -
-
-
-
- -
-
- - CATALOG -
-
-

Nextcloud Hub

-

- Collaborative office, encrypted file sync, and calendar sharing -

-
-
- - Ready + {primaryPool?.name || 'tank'}
- {/* Workload 4: Paperless-ngx */} + {/* Core Service 3: Systemd Workload Engine */}
- +
- - CATALOG + + ONLINE
-

Paperless-ngx

+

Systemd Workload Engine

- Automated document archiving with OCR and full-text search + Linux cgroups v2 service supervisor with memory and CPU boundaries

- Ready + systemd
+ + {/* Workload 4: Installed App OR App Store Catalog Link */} + {apps.length > 0 ? ( +
+
+
+
+ +
+
+ + {apps[0].status.toUpperCase()} +
+
+

{apps[0].name}

+

+ Port :{apps[0].port} • Runtime {apps[0].runtime} +

+
+
+ + :{apps[0].port} + + +
+
+ ) : ( +
onNavigate('apps')} + className="p-4 rounded-xl bg-cyan-950/20 border border-cyan-500/30 hover:border-cyan-400/60 transition-all flex flex-col justify-between cursor-pointer group" + > +
+
+ +
+

Deploy New Workload

+

+ Install Immich, Jellyfin, Nextcloud, or Paperless in 1 click +

+
+
+ App Store + + Browse Store + + +
+
+ )}
diff --git a/src/views/StorageView.tsx b/src/views/StorageView.tsx index 792100b..9b5c104 100644 --- a/src/views/StorageView.tsx +++ b/src/views/StorageView.tsx @@ -166,30 +166,38 @@ export const StorageView: React.FC = ({ - {/* Live Pool I/O Telemetry */} -
-
- - - Live Pool Bandwidth & IOPS - - 3,420 Read IOPS · 1,180 Write IOPS + {/* Real vdev topology */} + {pool.vdevs && pool.vdevs.length > 0 && ( +
+
+ + + Physical & Virtual Vdev Topology + + {pool.vdevs.length} Member Device(s) +
+
+ {pool.vdevs.map((vdev, idx) => ( +
+
+ {vdev.name} + {vdev.type} +
+
+ Health: {vdev.health} + {vdev.disks?.[0] && ( + Errors: R:{vdev.disks[0].readErrors} W:{vdev.disks[0].writeErrors} C:{vdev.disks[0].checksumErrors} + )} +
+
+ ))} +
-
- {[24, 45, 18, 62, 88, 142, 95, 30, 48, 70, 85, 40, 55, 90, 60, 75, 110, 85, 95, 120].map((val, i) => ( -
- ))} -
-
+ )} {/* Status summary */}
- Last Scrub: {pool.scanStatus || 'Completed with 0 errors'} + Last Scrub: {pool.scanStatus || 'Clean (0 errors)'} Fragmentation: {pool.fragmentation}