feat(ui): overhaul design to match Portara logo, integrate live telemetry and remove isolated analytics tab
CI & Build NaxOS Web Dashboard / build (push) Successful in 1m55s

This commit is contained in:
Lukas Holzner
2026-09-04 13:44:15 +02:00
parent f55a2b41d0
commit 7096294d10
25 changed files with 469 additions and 427 deletions
+5 -5
View File
@@ -7,7 +7,7 @@
<p align="center"> <p align="center">
<b>Modern, Responsive Web Interface for NaxOS Storage Appliance (NixOS 26.05 & OpenZFS)</b> <b>Modern, Responsive Web Interface for NaxOS Storage Appliance (NixOS 26.05 & OpenZFS)</b>
<br /> <br />
Built with React 18, TypeScript, Tailwind CSS, Lucide Icons, and Embedded CNCF Perses Analytics Built with React 18, TypeScript, Tailwind CSS, Lucide Icons, and Seamless Integrated Telemetry
</p> </p>
<p align="center"> <p align="center">
@@ -18,15 +18,15 @@
--- ---
The **NaxOS Web Dashboard** is a modern, responsive, non-technical web interface for managing the NaxOS storage appliance running on NixOS 26.05. Built with React, TypeScript, Tailwind CSS, Lucide Icons, and embedded CNCF Perses analytics. The **NaxOS Web Dashboard** is a modern, responsive, non-technical web interface for managing the NaxOS storage appliance running on NixOS 26.05. Built with React, TypeScript, Tailwind CSS, Lucide Icons, and seamless integrated storage and system telemetry.
## Dashboard Showcase ## Dashboard Showcase
| Overview & Telemetry | OpenZFS Storage Engine | | Overview & Live Telemetry | OpenZFS Storage Engine |
| :---: | :---: | | :---: | :---: |
| ![Dashboard Overview](public/screenshots/dashboard-overview.png) | ![Storage Management](public/screenshots/storage-management.png) | | ![Dashboard Overview](public/screenshots/dashboard-overview.png) | ![Storage Management](public/screenshots/storage-management.png) |
| **CNCF Perses Analytics** | **App Store & Runtimes** | | **Integrated Performance Telemetry** | **App Store & Runtimes** |
| ![Perses Analytics](public/screenshots/perses-analytics.png) | ![App Store](public/screenshots/app-store.png) | | ![Integrated Telemetry](public/screenshots/system-telemetry.png) | ![App Store](public/screenshots/app-store.png) |
| **Setup & Migration Wizard** | **File Shares Management** | | **Setup & Migration Wizard** | **File Shares Management** |
| ![Migration Wizard](public/screenshots/migration-wizard.png) | ![Shares Management](public/screenshots/shares-management.png) | | ![Migration Wizard](public/screenshots/migration-wizard.png) | ![Shares Management](public/screenshots/shares-management.png) |
+1 -1
View File
@@ -9,7 +9,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
</head> </head>
<body class="bg-slate-950 text-slate-100 font-sans antialiased selection:bg-emerald-500 selection:text-white"> <body class="bg-slate-950 text-slate-100 font-sans antialiased selection:bg-cyan-500 selection:text-white">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

+3 -6
View File
@@ -7,7 +7,6 @@ import { OverviewView } from './views/OverviewView.js';
import { StorageView } from './views/StorageView.js'; import { StorageView } from './views/StorageView.js';
import { SharesView } from './views/SharesView.js'; import { SharesView } from './views/SharesView.js';
import { AppsView } from './views/AppsView.js'; import { AppsView } from './views/AppsView.js';
import { AnalyticsView } from './views/AnalyticsView.js';
import { GitOpsView } from './views/GitOpsView.js'; import { GitOpsView } from './views/GitOpsView.js';
import { LogsView } from './views/LogsView.js'; import { LogsView } from './views/LogsView.js';
import { WizardView } from './views/WizardView.js'; import { WizardView } from './views/WizardView.js';
@@ -187,8 +186,8 @@ export const App: React.FC = () => {
}; };
return ( return (
<div className="flex h-screen w-screen overflow-hidden bg-slate-950 text-slate-100 font-sans"> <div className="flex h-screen w-screen overflow-hidden bg-slate-950 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-cyan-950/20 via-slate-950 to-slate-950 text-slate-100 font-sans">
<Sidebar currentTab={currentTab} onSelectTab={selectTab} /> <Sidebar currentTab={currentTab === 'analytics' ? 'overview' : currentTab} onSelectTab={selectTab} />
<div className="flex-1 flex flex-col min-w-0 overflow-hidden"> <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<Topbar <Topbar
@@ -199,7 +198,7 @@ export const App: React.FC = () => {
/> />
<main className="flex-1 overflow-y-auto p-8"> <main className="flex-1 overflow-y-auto p-8">
{currentTab === 'overview' && ( {(currentTab === 'overview' || currentTab === 'analytics') && (
<OverviewView <OverviewView
status={status} status={status}
pools={pools} pools={pools}
@@ -241,8 +240,6 @@ export const App: React.FC = () => {
/> />
)} )}
{currentTab === 'analytics' && <AnalyticsView />}
{currentTab === 'gitops' && ( {currentTab === 'gitops' && (
<GitOpsView <GitOpsView
status={gitops} status={gitops}
+10 -7
View File
@@ -47,12 +47,15 @@ export const RebuildModal: React.FC<RebuildModalProps> = ({ isOpen, onClose }) =
<div className="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden flex flex-col"> <div className="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden flex flex-col">
{/* Header */} {/* Header */}
<div className="px-5 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60"> <div className="px-5 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-3">
<div className="p-1.5 rounded-lg bg-slate-800 text-emerald-400"> <div className="h-9 w-9 rounded-xl bg-slate-950 border border-cyan-500/30 p-1.5 flex items-center justify-center shadow-md shadow-cyan-500/10">
<Terminal className="h-4 w-4" /> <img src="/logo.svg" alt="NaxOS" className="h-full w-full object-contain filter drop-shadow-[0_1px_4px_rgba(12,179,229,0.4)]" />
</div> </div>
<div> <div>
<h3 className="text-sm font-semibold text-white">Declarative NixOS Switch</h3> <h3 className="text-sm font-semibold text-white flex items-center gap-1.5">
<span>Declarative NixOS 26.05 Switch</span>
<span className="text-[10px] px-1.5 py-0.2 rounded bg-cyan-500/15 text-cyan-400 border border-cyan-500/25 font-mono">GitOps</span>
</h3>
<p className="text-xs text-slate-400">Safe atomic rebuild with auto-rollback</p> <p className="text-xs text-slate-400">Safe atomic rebuild with auto-rollback</p>
</div> </div>
</div> </div>
@@ -68,8 +71,8 @@ export const RebuildModal: React.FC<RebuildModalProps> = ({ isOpen, onClose }) =
<div className="p-4 bg-slate-950 font-mono text-xs text-slate-300 h-80 overflow-y-auto space-y-1.5 border-b border-slate-800"> <div className="p-4 bg-slate-950 font-mono text-xs text-slate-300 h-80 overflow-y-auto space-y-1.5 border-b border-slate-800">
{logs.map((log, idx) => ( {logs.map((log, idx) => (
<div key={idx} className="flex items-start gap-2"> <div key={idx} className="flex items-start gap-2">
<span className="text-slate-600 select-none">&gt;</span> <span className="text-cyan-500/60 select-none">&gt;</span>
<span className={log.includes('PASSED') ? 'text-emerald-400 font-semibold' : ''}> <span className={log.includes('PASSED') ? 'text-cyan-400 font-semibold' : ''}>
{log} {log}
</span> </span>
</div> </div>
@@ -81,7 +84,7 @@ export const RebuildModal: React.FC<RebuildModalProps> = ({ isOpen, onClose }) =
<div className="px-5 py-3.5 bg-slate-900 flex items-center justify-between"> <div className="px-5 py-3.5 bg-slate-900 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isDone && !isError && ( {isDone && !isError && (
<span className="flex items-center gap-1.5 text-xs text-emerald-400 font-medium"> <span className="flex items-center gap-1.5 text-xs text-cyan-400 font-medium">
<CheckCircle2 className="h-4 w-4" /> <CheckCircle2 className="h-4 w-4" />
Switch completed successfully Switch completed successfully
</span> </span>
+34 -24
View File
@@ -4,11 +4,11 @@ import {
HardDrive, HardDrive,
FolderSync, FolderSync,
Layers, Layers,
LineChart,
GitBranch, GitBranch,
FileTerminal, FileTerminal,
Server,
Settings, Settings,
Sparkles,
ShieldCheck,
} from 'lucide-react'; } from 'lucide-react';
interface SidebarProps { interface SidebarProps {
@@ -21,30 +21,36 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentTab, onSelectTab }) =>
{ id: 'overview', label: 'Dashboard', icon: LayoutDashboard }, { id: 'overview', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'storage', label: 'Storage & ZFS', icon: HardDrive, badge: 'ZFS' }, { id: 'storage', label: 'Storage & ZFS', icon: HardDrive, badge: 'ZFS' },
{ id: 'shares', label: 'File Shares', icon: FolderSync, badge: 'SMB/NFS' }, { id: 'shares', label: 'File Shares', icon: FolderSync, badge: 'SMB/NFS' },
{ id: 'apps', label: 'App Store & Runtime', icon: Layers }, { id: 'apps', label: 'App Store & Services', icon: Layers },
{ id: 'analytics', label: 'Perses Analytics', icon: LineChart, badge: 'Native' },
{ id: 'gitops', label: 'GitOps & Updates', icon: GitBranch }, { id: 'gitops', label: 'GitOps & Updates', icon: GitBranch },
{ id: 'logs', label: 'System Logs', icon: FileTerminal }, { id: 'logs', label: 'System Logs', icon: FileTerminal },
{ id: 'wizard', label: 'Setup Wizard', icon: Settings }, { id: 'wizard', label: 'Setup & Migration', icon: Settings },
]; ];
return ( return (
<aside className="w-64 bg-slate-900/90 border-r border-slate-800 flex flex-col backdrop-blur-xl shrink-0"> <aside className="w-64 bg-slate-900/90 border-r border-slate-800 flex flex-col backdrop-blur-xl shrink-0 z-20">
{/* Brand Header */} {/* Brand Header with Portara Logo */}
<div className="h-16 flex items-center px-6 gap-3 border-b border-slate-800"> <div className="h-20 flex items-center px-5 gap-3.5 border-b border-slate-800 bg-gradient-to-b from-slate-900 to-slate-900/80">
<div className="h-10 w-10 rounded-xl bg-slate-950 border border-slate-800/80 p-1 flex items-center justify-center shadow-lg shadow-emerald-500/10 shrink-0"> <div className="h-11 w-11 rounded-xl bg-slate-950/80 border border-cyan-500/30 p-1.5 flex items-center justify-center shadow-lg shadow-cyan-500/10 shrink-0 group">
<img src="/logo.svg" alt="NaxOS Logo" className="h-full w-full object-contain" /> <img
src="/logo.svg"
alt="NaxOS Logo"
className="h-full w-full object-contain filter drop-shadow-[0_2px_8px_rgba(12,179,229,0.35)] transition-transform duration-300 group-hover:scale-105"
/>
</div> </div>
<div> <div className="min-w-0">
<div className="font-bold tracking-tight text-white flex items-center gap-1.5"> <div className="font-bold tracking-tight text-white flex items-center gap-1.5 text-base">
NaxOS <span className="text-xs px-1.5 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">v1.0</span> <span>NaxOS</span>
<span className="text-[10px] px-1.5 py-0.5 rounded-md bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 font-mono font-semibold">
26.05
</span>
</div> </div>
<p className="text-[11px] text-slate-400">Declarative ZFS Appliance</p> <p className="text-[11px] text-slate-400 font-medium truncate">Declarative ZFS Appliance</p>
</div> </div>
</div> </div>
{/* Nav List */} {/* Nav List */}
<nav className="flex-1 px-3 py-4 space-y-1"> <nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
{navItems.map((item) => { {navItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const isActive = currentTab === item.id; const isActive = currentTab === item.id;
@@ -52,19 +58,19 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentTab, onSelectTab }) =>
<button <button
key={item.id} key={item.id}
onClick={() => onSelectTab(item.id)} onClick={() => onSelectTab(item.id)}
className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-sm font-medium transition-all ${ className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 ${
isActive isActive
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-sm' ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/25 shadow-sm shadow-cyan-500/10 font-semibold'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60'
}`} }`}
> >
<Icon className={`h-4 w-4 ${isActive ? 'text-emerald-400' : 'text-slate-400'}`} /> <Icon className={`h-4 w-4 ${isActive ? 'text-cyan-400' : 'text-slate-400'}`} />
<span className="flex-1 text-left">{item.label}</span> <span className="flex-1 text-left">{item.label}</span>
{item.badge && ( {item.badge && (
<span <span
className={`text-[10px] px-1.5 py-0.5 rounded font-mono font-semibold ${ className={`text-[10px] px-1.5 py-0.5 rounded font-mono font-semibold ${
isActive isActive
? 'bg-emerald-500/20 text-emerald-300' ? 'bg-cyan-500/20 text-cyan-300'
: 'bg-slate-800 text-slate-400' : 'bg-slate-800 text-slate-400'
}`} }`}
> >
@@ -77,15 +83,19 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentTab, onSelectTab }) =>
</nav> </nav>
{/* Appliance Status Footer */} {/* Appliance Status Footer */}
<div className="p-4 border-t border-slate-800 bg-slate-950/40"> <div className="p-4 border-t border-slate-800 bg-slate-950/60">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500 animate-pulse" /> <div className="h-2.5 w-2.5 rounded-full bg-cyan-400 shadow-sm shadow-cyan-400/50 animate-pulse" />
<div className="text-xs"> <div className="text-xs min-w-0">
<p className="text-slate-200 font-medium">System Protected</p> <p className="text-slate-200 font-medium truncate">System Healthy</p>
<p className="text-slate-500 text-[11px]">GitOps Synchronized</p> <p className="text-slate-400 text-[11px] truncate flex items-center gap-1">
<ShieldCheck className="h-3 w-3 text-cyan-400" />
<span>GitOps Protected</span>
</p>
</div> </div>
</div> </div>
</div> </div>
</aside> </aside>
); );
}; };
+3 -3
View File
@@ -25,8 +25,8 @@ export const Topbar: React.FC<TopbarProps> = ({
</span> </span>
</h1> </h1>
<div className="hidden md:flex items-center gap-2 text-xs text-slate-400 border-l border-slate-800 pl-4"> <div className="hidden md:flex items-center gap-2 text-xs text-slate-400 border-l border-slate-800 pl-4">
<ShieldCheck className="h-3.5 w-3.5 text-emerald-400" /> <ShieldCheck className="h-3.5 w-3.5 text-cyan-400" />
<span>ARC Hit: <strong className="text-emerald-400">{status?.arcHitRatioPercent || 98.6}%</strong></span> <span>ARC Hit: <strong className="text-cyan-400">{status?.arcHitRatioPercent || 98.6}%</strong></span>
</div> </div>
</div> </div>
@@ -40,7 +40,7 @@ export const Topbar: React.FC<TopbarProps> = ({
<button <button
onClick={onTriggerRebuild} onClick={onTriggerRebuild}
className="flex items-center gap-2 px-3.5 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-medium transition shadow-sm shadow-emerald-600/30" className="flex items-center gap-2 px-3.5 py-1.5 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white text-xs font-semibold transition shadow-sm shadow-cyan-600/30"
> >
<Play className="h-3.5 w-3.5 fill-current" /> <Play className="h-3.5 w-3.5 fill-current" />
<span>Apply Changes</span> <span>Apply Changes</span>
+12 -2
View File
@@ -20,10 +20,20 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #334155; background: #1e293b;
border-radius: 9999px; border-radius: 9999px;
border: 1px solid rgba(12, 179, 229, 0.15);
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #475569; background: #0284c7;
} }
.glow-cyan-sm {
box-shadow: 0 0 15px -3px rgba(12, 179, 229, 0.25);
}
.glow-cyan {
box-shadow: 0 0 25px -5px rgba(12, 179, 229, 0.35);
}
-156
View File
@@ -1,156 +0,0 @@
import React, { useState } from 'react';
import {
LineChart,
Activity,
HardDrive,
Cpu,
Clock,
RefreshCw,
Zap,
Layers,
ArrowUpRight,
} from 'lucide-react';
export const AnalyticsView: React.FC = () => {
const [timeRange, setTimeRange] = useState('1h');
const [refreshInterval, setRefreshInterval] = useState('10s');
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800 pb-4">
<div>
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold text-white tracking-tight">Perses Native Analytics</h2>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold font-mono">
CNCF Perses
</span>
</div>
<p className="text-xs text-slate-400 mt-0.5">
Embedded telemetry and OpenZFS performance graphs replacing standalone Grafana
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 p-1 bg-slate-900 border border-slate-800 rounded-xl text-xs">
{['15m', '1h', '6h', '24h', '7d'].map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-2.5 py-1 rounded-lg font-medium transition ${
timeRange === range ? 'bg-emerald-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'
}`}
>
{range}
</button>
))}
</div>
<button className="p-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800 transition">
<RefreshCw className="h-4 w-4" />
</button>
</div>
</div>
{/* Grid of Perses Dashboard Panels */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Panel 1: ZFS ARC Hit Ratio */}
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs font-semibold text-white">
<Zap className="h-4 w-4 text-emerald-400" />
<span>ZFS ARC Hit Ratio (%)</span>
</div>
<span className="text-xs font-mono font-bold text-emerald-400">98.6%</span>
</div>
<div className="h-40 flex items-end gap-1.5 pt-4 px-2 bg-slate-950/60 rounded-xl border border-slate-800/80">
{[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].map((val, i) => (
<div key={i} className="flex-1 bg-gradient-to-t from-emerald-600/40 to-emerald-400 rounded-t transition-all" style={{ height: `${(val - 90) * 10}%` }} title={`${val}%`} />
))}
</div>
<div className="flex justify-between text-[10px] text-slate-500 font-mono">
<span>Query: zfs_arc_hits / (zfs_arc_hits + zfs_arc_misses) * 100</span>
<span>Live</span>
</div>
</div>
{/* Panel 2: Storage Pool Throughput (MB/s) */}
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs font-semibold text-white">
<HardDrive className="h-4 w-4 text-sky-400" />
<span>Storage Pool Read / Write Bandwidth</span>
</div>
<span className="text-xs font-mono text-slate-400">Peak: 142 MB/s</span>
</div>
<div className="h-40 flex items-end gap-1.5 pt-4 px-2 bg-slate-950/60 rounded-xl border border-slate-800/80">
{[24, 45, 18, 62, 88, 142, 95, 30, 48, 70, 85, 40, 55, 90, 60].map((val, i) => (
<div key={i} className="flex-1 bg-gradient-to-t from-sky-600/40 to-sky-400 rounded-t transition-all" style={{ height: `${(val / 150) * 100}%` }} title={`${val} MB/s`} />
))}
</div>
<div className="flex justify-between text-[10px] text-slate-500 font-mono">
<span>Query: rate(node_disk_read_bytes_total[1m]) + write</span>
<span>Target: tank</span>
</div>
</div>
{/* Panel 3: CPU Core Utilization */}
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs font-semibold text-white">
<Cpu className="h-4 w-4 text-purple-400" />
<span>CPU Core Utilization</span>
</div>
<span className="text-xs font-mono text-purple-300">12.4% Avg</span>
</div>
<div className="h-40 flex items-end gap-1.5 pt-4 px-2 bg-slate-950/60 rounded-xl border border-slate-800/80">
{[8, 12, 14, 25, 42, 19, 11, 9, 15, 18, 30, 16, 12, 14, 10].map((val, i) => (
<div key={i} className="flex-1 bg-gradient-to-t from-purple-600/40 to-purple-400 rounded-t transition-all" style={{ height: `${val * 2}%` }} title={`${val}%`} />
))}
</div>
<div className="flex justify-between text-[10px] text-slate-500 font-mono">
<span>Query: rate(node_cpu_seconds_total)</span>
<span>4 Cores</span>
</div>
</div>
{/* Panel 4: Memory & ARC Distribution */}
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs font-semibold text-white">
<Layers className="h-4 w-4 text-amber-400" />
<span>Memory Footprint Breakdown</span>
</div>
<span className="text-xs font-mono text-slate-300">13.2 / 32.0 GB</span>
</div>
<div className="h-40 flex items-center justify-center bg-slate-950/60 rounded-xl border border-slate-800/80 p-4">
<div className="w-full space-y-3">
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">ZFS ARC Cache</span>
<span className="font-mono text-emerald-400 font-semibold">4.0 GB (100% Target)</span>
</div>
<div className="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
<div className="bg-emerald-400 h-full rounded-full" style={{ width: '100%' }} />
</div>
</div>
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">Applications & Immich ML</span>
<span className="font-mono text-sky-400 font-semibold">9.2 GB</span>
</div>
<div className="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
<div className="bg-sky-400 h-full rounded-full" style={{ width: '32%' }} />
</div>
</div>
</div>
</div>
<div className="flex justify-between text-[10px] text-slate-500 font-mono">
<span>Query: node_memory_MemTotal_bytes</span>
<span>Free: 18.8 GB</span>
</div>
</div>
</div>
</div>
);
};
+9 -9
View File
@@ -69,7 +69,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
<button <button
onClick={() => setActiveTab('catalog')} onClick={() => setActiveTab('catalog')}
className={`px-3.5 py-1.5 rounded-lg text-xs font-medium transition ${ className={`px-3.5 py-1.5 rounded-lg text-xs font-medium transition ${
activeTab === 'catalog' ? 'bg-emerald-600 text-white shadow-sm' : 'text-slate-400 hover:text-white' activeTab === 'catalog' ? 'bg-cyan-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'
}`} }`}
> >
Catalog Store ({catalog.length}) Catalog Store ({catalog.length})
@@ -77,7 +77,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
<button <button
onClick={() => setActiveTab('installed')} onClick={() => setActiveTab('installed')}
className={`px-3.5 py-1.5 rounded-lg text-xs font-medium transition ${ className={`px-3.5 py-1.5 rounded-lg text-xs font-medium transition ${
activeTab === 'installed' ? 'bg-emerald-600 text-white shadow-sm' : 'text-slate-400 hover:text-white' activeTab === 'installed' ? 'bg-cyan-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'
}`} }`}
> >
Installed Workloads ({installedApps.length}) Installed Workloads ({installedApps.length})
@@ -95,7 +95,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
return ( return (
<div <div
key={item.id} key={item.id}
className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 flex flex-col justify-between hover:border-slate-700 transition" className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 flex flex-col justify-between hover:border-cyan-500/30 transition"
> >
<div> <div>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@@ -123,7 +123,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
setSelectedRuntime({ ...selectedRuntime, [item.id]: e.target.value as AppRuntime }) setSelectedRuntime({ ...selectedRuntime, [item.id]: e.target.value as AppRuntime })
} }
disabled={isInstalled} disabled={isInstalled}
className="bg-slate-950 border border-slate-800 rounded-lg px-2 py-1 text-slate-200 text-xs font-medium focus:outline-none focus:border-emerald-500" className="bg-slate-950 border border-slate-800 rounded-lg px-2 py-1 text-slate-200 text-xs font-medium focus:outline-none focus:border-cyan-500"
> >
{item.supportedRuntimes.map((r) => ( {item.supportedRuntimes.map((r) => (
<option key={r} value={r}> <option key={r} value={r}>
@@ -135,7 +135,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
{/* Action button */} {/* Action button */}
{isInstalled ? ( {isInstalled ? (
<div className="w-full py-2 rounded-xl bg-slate-800/60 text-emerald-400 text-xs font-semibold flex items-center justify-center gap-1.5 border border-slate-700/50"> <div className="w-full py-2 rounded-xl bg-slate-800/60 text-cyan-400 text-xs font-semibold flex items-center justify-center gap-1.5 border border-slate-700/50">
<CheckCircle2 className="h-4 w-4" /> <CheckCircle2 className="h-4 w-4" />
<span>Installed & Active</span> <span>Installed & Active</span>
</div> </div>
@@ -143,7 +143,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
<button <button
onClick={() => handleInstall(item.id)} onClick={() => handleInstall(item.id)}
disabled={installingId === item.id} disabled={installingId === item.id}
className="w-full py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white text-xs font-semibold transition shadow-sm" className="w-full py-2 rounded-xl bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-white text-xs font-semibold transition shadow-sm"
> >
{installingId === item.id ? 'Deploying...' : '1-Click Install'} {installingId === item.id ? 'Deploying...' : '1-Click Install'}
</button> </button>
@@ -160,7 +160,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
<div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden"> <div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center justify-between"> <div className="p-4 border-b border-slate-800 flex items-center justify-between">
<h3 className="text-sm font-semibold text-white flex items-center gap-2"> <h3 className="text-sm font-semibold text-white flex items-center gap-2">
<Layers className="h-4 w-4 text-emerald-400" /> <Layers className="h-4 w-4 text-cyan-400" />
<span>Active Workloads</span> <span>Active Workloads</span>
</h3> </h3>
<span className="text-xs text-slate-400">{installedApps.length} services</span> <span className="text-xs text-slate-400">{installedApps.length} services</span>
@@ -178,7 +178,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
<h4 className="text-sm font-bold text-white">{app.name}</h4> <h4 className="text-sm font-bold text-white">{app.name}</h4>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-semibold ${ <span className={`text-[10px] px-2 py-0.5 rounded-full font-semibold ${
app.status === 'running' app.status === 'running'
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' ? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/20'
: 'bg-slate-800 text-slate-400' : 'bg-slate-800 text-slate-400'
}`}> }`}>
{app.status.toUpperCase()} {app.status.toUpperCase()}
@@ -217,7 +217,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
) : ( ) : (
<button <button
onClick={() => onSetStatus(app.appId, 'running')} onClick={() => onSetStatus(app.appId, 'running')}
className="p-2 rounded-lg bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-400 border border-emerald-500/30 transition" className="p-2 rounded-lg bg-cyan-600/20 hover:bg-cyan-600/30 text-cyan-400 border border-cyan-500/30 transition"
title="Start Service" title="Start Service"
> >
<Play className="h-3.5 w-3.5 fill-current" /> <Play className="h-3.5 w-3.5 fill-current" />
+8 -8
View File
@@ -53,7 +53,7 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
<button <button
onClick={onTriggerRebuild} onClick={onTriggerRebuild}
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-xs font-semibold text-white shadow-sm transition" 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 shadow-sm transition"
> >
<RefreshCw className="h-3.5 w-3.5" /> <RefreshCw className="h-3.5 w-3.5" />
<span>Synchronize & Rebuild</span> <span>Synchronize & Rebuild</span>
@@ -65,7 +65,7 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800"> <div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800">
<span className="text-xs text-slate-400 font-medium">Remote Synchronization</span> <span className="text-xs text-slate-400 font-medium">Remote Synchronization</span>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<div className="h-2 w-2 rounded-full bg-emerald-400" /> <div className="h-2 w-2 rounded-full bg-cyan-400" />
<span className="text-sm font-bold text-white font-mono">{status?.branch || 'main'}</span> <span className="text-sm font-bold text-white font-mono">{status?.branch || 'main'}</span>
</div> </div>
<div className="mt-3 text-xs text-slate-400 truncate font-mono"> <div className="mt-3 text-xs text-slate-400 truncate font-mono">
@@ -76,7 +76,7 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800"> <div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800">
<span className="text-xs text-slate-400 font-medium">Current Head Commit</span> <span className="text-xs text-slate-400 font-medium">Current Head Commit</span>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<GitCommitIcon className="h-4 w-4 text-emerald-400" /> <GitCommitIcon className="h-4 w-4 text-cyan-400" />
<span className="text-sm font-bold text-white font-mono">{status?.lastCommitSha?.slice(0, 7) || '5da58ae'}</span> <span className="text-sm font-bold text-white font-mono">{status?.lastCommitSha?.slice(0, 7) || '5da58ae'}</span>
</div> </div>
<div className="mt-3 text-xs text-slate-300 truncate"> <div className="mt-3 text-xs text-slate-300 truncate">
@@ -90,14 +90,14 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
<select <select
value={selectedChannel} value={selectedChannel}
onChange={(e) => setSelectedChannel(e.target.value as any)} onChange={(e) => setSelectedChannel(e.target.value as any)}
className="bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1 text-slate-200 text-xs font-semibold focus:outline-none focus:border-emerald-500" className="bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1 text-slate-200 text-xs font-semibold focus:outline-none focus:border-cyan-500"
> >
<option value="stable">Stable (26.05)</option> <option value="stable">Stable (26.05)</option>
<option value="beta">Beta Channel</option> <option value="beta">Beta Channel</option>
<option value="nightly">Nightly Unstable</option> <option value="nightly">Nightly Unstable</option>
</select> </select>
</div> </div>
<div className="mt-3 text-[11px] text-emerald-400 flex items-center gap-1"> <div className="mt-3 text-[11px] text-cyan-400 flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5" /> <CheckCircle2 className="h-3.5 w-3.5" />
<span>Appliance is up to date</span> <span>Appliance is up to date</span>
</div> </div>
@@ -108,7 +108,7 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
<div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden"> <div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center justify-between"> <div className="p-4 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-emerald-400" /> <GitBranch className="h-4 w-4 text-cyan-400" />
<h3 className="text-sm font-semibold text-white">Declarative Configuration Audit Trail</h3> <h3 className="text-sm font-semibold text-white">Declarative Configuration Audit Trail</h3>
</div> </div>
<span className="text-xs text-slate-400">Total {commits.length} generations</span> <span className="text-xs text-slate-400">Total {commits.length} generations</span>
@@ -119,9 +119,9 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
<div key={c.hash} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-3 hover:bg-slate-800/30 transition"> <div key={c.hash} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-3 hover:bg-slate-800/30 transition">
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-mono text-xs text-emerald-400 font-semibold">{c.hash.slice(0, 7)}</span> <span className="font-mono text-xs text-cyan-400 font-semibold">{c.hash.slice(0, 7)}</span>
{i === 0 && ( {i === 0 && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold font-mono"> <span className="text-[10px] px-2 py-0.5 rounded-full bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 font-semibold font-mono">
ACTIVE GENERATION ACTIVE GENERATION
</span> </span>
)} )}
+3 -3
View File
@@ -58,7 +58,7 @@ export const LogsView: React.FC<LogsViewProps> = ({
onClick={() => setIsStreaming(!isStreaming)} onClick={() => setIsStreaming(!isStreaming)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium transition ${ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium transition ${
isStreaming isStreaming
? 'bg-emerald-600/20 text-emerald-400 border border-emerald-500/30' ? 'bg-cyan-600/20 text-cyan-400 border border-cyan-500/30'
: 'bg-slate-800 text-slate-400' : 'bg-slate-800 text-slate-400'
}`} }`}
> >
@@ -85,7 +85,7 @@ export const LogsView: React.FC<LogsViewProps> = ({
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search log messages, services, or timestamps..." placeholder="Search log messages, services, or timestamps..."
className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-800 rounded-xl text-xs text-slate-200 placeholder:text-slate-500 focus:outline-none focus:border-emerald-500 font-mono" className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-800 rounded-xl text-xs text-slate-200 placeholder:text-slate-500 focus:outline-none focus:border-cyan-500 font-mono"
/> />
</div> </div>
@@ -96,7 +96,7 @@ export const LogsView: React.FC<LogsViewProps> = ({
onClick={() => onSelectUnit(u)} onClick={() => onSelectUnit(u)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition ${ className={`px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition ${
selectedUnit === u selectedUnit === u
? 'bg-emerald-600 text-white shadow-sm' ? 'bg-cyan-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white' : 'text-slate-400 hover:text-white'
}`} }`}
> >
+281 -134
View File
@@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { import {
HardDrive, HardDrive,
Cpu, Cpu,
@@ -8,6 +8,7 @@ import {
Activity, Activity,
ArrowUpRight, ArrowUpRight,
TrendingUp, TrendingUp,
Zap,
} from 'lucide-react'; } from 'lucide-react';
import { AppIcon } from '../components/AppIcon.js'; import { AppIcon } from '../components/AppIcon.js';
import type { SystemStatus, ZfsPool, InstalledApp } from '../types/index.js'; import type { SystemStatus, ZfsPool, InstalledApp } from '../types/index.js';
@@ -26,42 +27,85 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
onNavigate, onNavigate,
}) => { }) => {
const primaryPool = pools[0]; 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Top Banner / Welcome */} {/* Top Banner / Hero Header with Portara Styling */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 p-6 rounded-2xl bg-gradient-to-r from-slate-900 via-slate-900/80 to-emerald-950/30 border border-slate-800"> <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>
<div className="flex items-center gap-2.5">
<h2 className="text-xl font-bold text-white tracking-tight">Appliance Overview</h2> <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"> <p className="text-xs text-slate-400 mt-1">
Running {status?.osVersion || 'NaxOS 26.05'} on OpenZFS storage engine 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> </p>
</div> </div>
</div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
onClick={() => onNavigate('storage')} onClick={() => onNavigate('storage')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-medium text-slate-200 transition border border-slate-700" 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"
> >
<span>Manage Pools</span> <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" /> <ArrowUpRight className="h-3.5 w-3.5 text-slate-400" />
</button> </button>
<button <button
onClick={() => onNavigate('apps')} onClick={() => onNavigate('apps')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-xs font-medium text-white transition shadow-sm shadow-emerald-600/30" 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> <span>App Store</span>
<ArrowUpRight className="h-3.5 w-3.5" /> <ArrowUpRight className="h-3.5 w-3.5" />
</button> </button>
</div> </div>
</div> </div>
</div>
{/* KPI Cards Grid */} {/* KPI Cards Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Storage Pool Card */} {/* Storage Pool Card */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between"> <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"> <div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Pool: {primaryPool?.name || 'tank'}</span> <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-emerald-500/10 text-emerald-400 border border-emerald-500/20"> <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'} {primaryPool?.health || 'ONLINE'}
</span> </span>
</div> </div>
@@ -72,7 +116,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
{/* Progress Bar */} {/* Progress Bar */}
<div className="w-full bg-slate-800 h-2 rounded-full mt-2 overflow-hidden"> <div className="w-full bg-slate-800 h-2 rounded-full mt-2 overflow-hidden">
<div <div
className="bg-gradient-to-r from-emerald-500 to-teal-400 h-full rounded-full transition-all duration-500" className="bg-gradient-to-r from-cyan-500 to-blue-500 h-full rounded-full transition-all duration-500"
style={{ width: `${primaryPool?.capacityPercent || 56}%` }} style={{ width: `${primaryPool?.capacityPercent || 56}%` }}
/> />
</div> </div>
@@ -84,27 +128,28 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</div> </div>
{/* ZFS ARC Hit Ratio */} {/* ZFS ARC Hit Ratio */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between"> <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"> <div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">ZFS ARC Cache</span> <span className="text-xs text-slate-400 font-medium">ZFS ARC Cache</span>
<ShieldCheck className="h-4 w-4 text-emerald-400" /> <ShieldCheck className="h-4 w-4 text-cyan-400" />
</div> </div>
<div className="mt-4"> <div className="mt-4">
<div className="text-2xl font-bold text-white tracking-tight"> <div className="text-2xl font-bold text-white tracking-tight flex items-baseline gap-2">
{status?.arcHitRatioPercent || 98.6}% <span>{status?.arcHitRatioPercent || 98.6}%</span>
<span className="text-xs text-cyan-400 font-normal">Efficiency</span>
</div> </div>
<p className="text-xs text-slate-400 mt-1"> <p className="text-xs text-slate-400 mt-1">
ARC Size: <strong>4.0 GB</strong> (Max: 4.0 GB) ARC Size: <strong className="text-slate-200">4.0 GB</strong> (Target: 4.0 GB)
</p> </p>
</div> </div>
<div className="mt-3 text-[11px] text-emerald-400 flex items-center gap-1"> <div className="mt-3 text-[11px] text-cyan-400 flex items-center gap-1">
<TrendingUp className="h-3 w-3" /> <TrendingUp className="h-3 w-3" />
<span>Optimal cache hit performance</span> <span>Optimal cache hit performance</span>
</div> </div>
</div> </div>
{/* Workloads Card */} {/* Workloads Card */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between"> <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"> <div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Running Workloads</span> <span className="text-xs text-slate-400 font-medium">Running Workloads</span>
<Layers className="h-4 w-4 text-sky-400" /> <Layers className="h-4 w-4 text-sky-400" />
@@ -120,7 +165,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Jellyfin Media Server"> <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" /> <AppIcon name="jellyfin" className="h-5 w-5" />
</div> </div>
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="CNCF Perses Telemetry"> <div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Telemetry Collector">
<AppIcon name="grafana" className="h-5 w-5" /> <AppIcon name="grafana" className="h-5 w-5" />
</div> </div>
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Docker Engine"> <div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Docker Engine">
@@ -134,10 +179,10 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</div> </div>
{/* Network Shares */} {/* Network Shares */}
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between"> <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"> <div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Active File Shares</span> <span className="text-xs text-slate-400 font-medium">Active File Shares</span>
<FolderSync className="h-4 w-4 text-indigo-400" /> <FolderSync className="h-4 w-4 text-blue-400" />
</div> </div>
<div className="mt-4"> <div className="mt-4">
<div className="text-2xl font-bold text-white tracking-tight"> <div className="text-2xl font-bold text-white tracking-tight">
@@ -148,7 +193,173 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</p> </p>
</div> </div>
<div className="mt-3 text-[11px] text-slate-400"> <div className="mt-3 text-[11px] text-slate-400">
Bonjour / mDNS discovery enabled 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> </div>
</div> </div>
@@ -157,32 +368,32 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
<div className="p-6 rounded-2xl bg-slate-900/70 border border-slate-800"> <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 justify-between mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-emerald-400" /> <Layers className="h-4 w-4 text-cyan-400" />
<h3 className="text-sm font-semibold text-white">Appliance Services & Workloads</h3> <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-emerald-500/10 text-emerald-400 border border-emerald-500/20"> <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">
Homarr Dashboard Native Workload Engine
</span> </span>
</div> </div>
<button <button
onClick={() => onNavigate('apps')} onClick={() => onNavigate('apps')}
className="text-xs text-emerald-400 hover:text-emerald-300 font-medium flex items-center gap-1" className="text-xs text-cyan-400 hover:text-cyan-300 font-medium flex items-center gap-1"
> >
<span>App Store & Workload Engine</span> <span>App Store & Catalog</span>
<ArrowUpRight className="h-3.5 w-3.5" /> <ArrowUpRight className="h-3.5 w-3.5" />
</button> </button>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Workload 1: Immich Photo Hub */} {/* Workload 1: Immich Photo Hub */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-slate-700 transition flex flex-col justify-between group"> <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>
<div className="flex items-start justify-between"> <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"> <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" /> <AppIcon name="immich" className="h-8 w-8" />
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" /> <span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
<span className="text-[10px] font-mono text-emerald-400 font-medium">ONLINE</span> <span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
</div> </div>
</div> </div>
<h4 className="text-sm font-bold text-white mt-3">Immich Photo Hub</h4> <h4 className="text-sm font-bold text-white mt-3">Immich Photo Hub</h4>
@@ -198,7 +409,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
href="http://naxos.local:2283" href="http://naxos.local:2283"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex items-center gap-1 text-xs text-emerald-400 hover:text-emerald-300 font-medium" className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
> >
<span>Launch</span> <span>Launch</span>
<ArrowUpRight className="h-3 w-3" /> <ArrowUpRight className="h-3 w-3" />
@@ -207,15 +418,15 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</div> </div>
{/* Workload 2: Jellyfin Media Server */} {/* Workload 2: Jellyfin Media Server */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-slate-700 transition flex flex-col justify-between group"> <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>
<div className="flex items-start justify-between"> <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"> <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" /> <AppIcon name="jellyfin" className="h-8 w-8" />
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" /> <span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
<span className="text-[10px] font-mono text-emerald-400 font-medium">ONLINE</span> <span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
</div> </div>
</div> </div>
<h4 className="text-sm font-bold text-white mt-3">Jellyfin Media Server</h4> <h4 className="text-sm font-bold text-white mt-3">Jellyfin Media Server</h4>
@@ -231,7 +442,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
href="http://naxos.local:8096" href="http://naxos.local:8096"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex items-center gap-1 text-xs text-emerald-400 hover:text-emerald-300 font-medium" className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
> >
<span>Launch</span> <span>Launch</span>
<ArrowUpRight className="h-3 w-3" /> <ArrowUpRight className="h-3 w-3" />
@@ -239,39 +450,8 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</div> </div>
</div> </div>
{/* Workload 3: CNCF Perses Analytics */} {/* Workload 3: Nextcloud Hub */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-slate-700 transition flex flex-col justify-between group"> <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="grafana" className="h-8 w-8" />
</div>
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-[10px] font-mono text-emerald-400 font-medium">ONLINE</span>
</div>
</div>
<h4 className="text-sm font-bold text-white mt-3">CNCF Perses Telemetry</h4>
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
Declarative visualization for ZFS I/O, ARC caching, and host metrics
</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">
:8080
</span>
<button
onClick={() => onNavigate('analytics')}
className="flex items-center gap-1 text-xs text-emerald-400 hover:text-emerald-300 font-medium"
>
<span>Open</span>
<ArrowUpRight className="h-3 w-3" />
</button>
</div>
</div>
{/* Workload 4: Nextcloud Hub */}
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-slate-700 transition flex flex-col justify-between group">
<div> <div>
<div className="flex items-start justify-between"> <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"> <div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
@@ -279,7 +459,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-slate-500" /> <span className="h-2 w-2 rounded-full bg-slate-500" />
<span className="text-[10px] font-mono text-slate-400 font-medium">READY</span> <span className="text-[10px] font-mono text-slate-400 font-medium">CATALOG</span>
</div> </div>
</div> </div>
<h4 className="text-sm font-bold text-white mt-3">Nextcloud Hub</h4> <h4 className="text-sm font-bold text-white mt-3">Nextcloud Hub</h4>
@@ -289,11 +469,42 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</div> </div>
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between"> <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"> <span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
Catalog Ready
</span> </span>
<button <button
onClick={() => onNavigate('apps')} onClick={() => onNavigate('apps')}
className="flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300 font-medium" 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> <span>Deploy</span>
<ArrowUpRight className="h-3 w-3" /> <ArrowUpRight className="h-3 w-3" />
@@ -302,70 +513,6 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
</div> </div>
</div> </div>
</div> </div>
{/* Embedded Native Perses Analytics Panel */}
<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">
<Activity className="h-4 w-4 text-emerald-400" />
<h3 className="text-sm font-semibold text-white">Native Perses Telemetry</h3>
</div>
<button
onClick={() => onNavigate('analytics')}
className="text-xs text-emerald-400 hover:text-emerald-300 font-medium flex items-center gap-1"
>
<span>View Full Dashboards</span>
<ArrowUpRight className="h-3.5 w-3.5" />
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Simulated Perses Panel 1: Throughput */}
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80">
<div className="text-xs font-medium text-slate-300 mb-2">Pool Read/Write Throughput</div>
<div className="h-32 flex items-end gap-1 px-2 pt-4">
{[42, 65, 30, 85, 95, 120, 80, 45, 60, 90, 110, 75, 40, 95, 130, 85, 70, 90, 105, 140, 95, 60, 80, 115].map((val, i) => (
<div key={i} className="flex-1 bg-emerald-500/30 hover:bg-emerald-400 transition-colors rounded-t" style={{ height: `${(val / 150) * 100}%` }} title={`${val} MB/s`} />
))}
</div>
<div className="flex justify-between text-[10px] text-slate-500 mt-2 font-mono">
<span>-30m</span>
<span>115 MB/s Peak</span>
<span>Now</span>
</div>
</div>
{/* Simulated Perses Panel 2: Memory & ARC */}
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80">
<div className="text-xs font-medium text-slate-300 mb-2">System Memory & ZFS ARC Distribution</div>
<div className="h-32 flex items-center justify-center">
<div className="w-full max-w-xs space-y-2">
<div className="flex justify-between text-xs text-slate-300">
<span>ZFS ARC Cache</span>
<span className="font-mono text-emerald-400">4.0 GB</span>
</div>
<div className="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
<div className="bg-emerald-400 h-full rounded-full" style={{ width: '25%' }} />
</div>
<div className="flex justify-between text-xs text-slate-300">
<span>Services & Containers</span>
<span className="font-mono text-sky-400">9.2 GB</span>
</div>
<div className="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
<div className="bg-sky-400 h-full rounded-full" style={{ width: '45%' }} />
</div>
<div className="flex justify-between text-xs text-slate-300">
<span>Free Available</span>
<span className="font-mono text-slate-400">18.8 GB</span>
</div>
<div className="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
<div className="bg-slate-600 h-full rounded-full" style={{ width: '30%' }} />
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
); );
}; };
+11 -11
View File
@@ -66,7 +66,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
<button <button
onClick={() => setActiveTab('smb')} onClick={() => setActiveTab('smb')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
activeTab === 'smb' ? 'bg-emerald-600 text-white shadow-sm' : 'text-slate-400 hover:text-white' activeTab === 'smb' ? 'bg-cyan-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'
}`} }`}
> >
Samba (SMB) Samba (SMB)
@@ -74,7 +74,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
<button <button
onClick={() => setActiveTab('nfs')} onClick={() => setActiveTab('nfs')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
activeTab === 'nfs' ? 'bg-emerald-600 text-white shadow-sm' : 'text-slate-400 hover:text-white' activeTab === 'nfs' ? 'bg-cyan-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'
}`} }`}
> >
NFS Exports NFS Exports
@@ -83,7 +83,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
<button <button
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-xs font-semibold text-white shadow-sm transition" 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 shadow-sm transition"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
<span>Create Share</span> <span>Create Share</span>
@@ -96,7 +96,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
<div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden"> <div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center justify-between"> <div className="p-4 border-b border-slate-800 flex items-center justify-between">
<h3 className="text-sm font-semibold text-white flex items-center gap-2"> <h3 className="text-sm font-semibold text-white flex items-center gap-2">
<FolderSync className="h-4 w-4 text-emerald-400" /> <FolderSync className="h-4 w-4 text-cyan-400" />
<span>Samba (SMB) Shares</span> <span>Samba (SMB) Shares</span>
</h3> </h3>
<span className="text-xs text-slate-400">{Object.keys(smbShares).length} shares active</span> <span className="text-xs text-slate-400">{Object.keys(smbShares).length} shares active</span>
@@ -132,7 +132,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
</td> </td>
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
{share.timeMachine ? ( {share.timeMachine ? (
<div className="flex items-center gap-1.5 text-emerald-400 font-medium"> <div className="flex items-center gap-1.5 text-cyan-400 font-medium">
<Apple className="h-3.5 w-3.5" /> <Apple className="h-3.5 w-3.5" />
<span>Enabled {share.timeMachineMaxSize ? `(${share.timeMachineMaxSize})` : ''}</span> <span>Enabled {share.timeMachineMaxSize ? `(${share.timeMachineMaxSize})` : ''}</span>
</div> </div>
@@ -175,7 +175,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
<div className="flex items-center gap-2 mt-1 text-slate-400"> <div className="flex items-center gap-2 mt-1 text-slate-400">
<span>Allowed Clients:</span> <span>Allowed Clients:</span>
{exp.clients.map((c, i) => ( {exp.clients.map((c, i) => (
<span key={i} className="px-2 py-0.5 rounded bg-slate-800 text-emerald-300 font-mono text-[11px]"> <span key={i} className="px-2 py-0.5 rounded bg-slate-800 text-cyan-300 font-mono text-[11px]">
{c.subnet} ({c.options}) {c.subnet} ({c.options})
</span> </span>
))} ))}
@@ -200,7 +200,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
value={newShareName} value={newShareName}
onChange={(e) => setNewShareName(e.target.value)} onChange={(e) => setNewShareName(e.target.value)}
placeholder="e.g. photos, documents" placeholder="e.g. photos, documents"
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white focus:outline-none focus:border-emerald-500 font-mono" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white focus:outline-none focus:border-cyan-500 font-mono"
required required
/> />
</div> </div>
@@ -212,7 +212,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
value={newSharePath} value={newSharePath}
onChange={(e) => setNewSharePath(e.target.value)} onChange={(e) => setNewSharePath(e.target.value)}
placeholder="/tank/..." placeholder="/tank/..."
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white focus:outline-none focus:border-emerald-500 font-mono" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white focus:outline-none focus:border-cyan-500 font-mono"
required required
/> />
</div> </div>
@@ -226,7 +226,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
type="checkbox" type="checkbox"
checked={newShareGuest} checked={newShareGuest}
onChange={(e) => setNewShareGuest(e.target.checked)} onChange={(e) => setNewShareGuest(e.target.checked)}
className="h-4 w-4 rounded accent-emerald-500" className="h-4 w-4 rounded accent-cyan-500"
/> />
</div> </div>
@@ -240,7 +240,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
type="checkbox" type="checkbox"
checked={newShareTimeMachine} checked={newShareTimeMachine}
onChange={(e) => setNewShareTimeMachine(e.target.checked)} onChange={(e) => setNewShareTimeMachine(e.target.checked)}
className="h-4 w-4 rounded accent-emerald-500" className="h-4 w-4 rounded accent-cyan-500"
/> />
</div> </div>
@@ -268,7 +268,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold shadow-sm" className="px-4 py-2 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-white font-semibold shadow-sm"
> >
Save & Publish Save & Publish
</button> </button>
+35 -14
View File
@@ -69,7 +69,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
onClick={() => setActiveSubTab('pools')} onClick={() => setActiveSubTab('pools')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
activeSubTab === 'pools' activeSubTab === 'pools'
? 'bg-emerald-600 text-white shadow-sm' ? 'bg-cyan-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white' : 'text-slate-400 hover:text-white'
}`} }`}
> >
@@ -79,7 +79,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
onClick={() => setActiveSubTab('datasets')} onClick={() => setActiveSubTab('datasets')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
activeSubTab === 'datasets' activeSubTab === 'datasets'
? 'bg-emerald-600 text-white shadow-sm' ? 'bg-cyan-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white' : 'text-slate-400 hover:text-white'
}`} }`}
> >
@@ -89,7 +89,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
onClick={() => setActiveSubTab('snapshots')} onClick={() => setActiveSubTab('snapshots')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
activeSubTab === 'snapshots' activeSubTab === 'snapshots'
? 'bg-emerald-600 text-white shadow-sm' ? 'bg-cyan-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white' : 'text-slate-400 hover:text-white'
}`} }`}
> >
@@ -112,8 +112,8 @@ export const StorageView: React.FC<StorageViewProps> = ({
</div> </div>
{importSuccess && ( {importSuccess && (
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/30 flex items-center gap-3 text-emerald-300 text-xs"> <div className="p-4 rounded-xl bg-cyan-500/10 border border-cyan-500/30 flex items-center gap-3 text-cyan-300 text-xs">
<CheckCircle2 className="h-5 w-5 text-emerald-400 shrink-0" /> <CheckCircle2 className="h-5 w-5 text-cyan-400 shrink-0" />
<span>{importSuccess}</span> <span>{importSuccess}</span>
</div> </div>
)} )}
@@ -125,13 +125,13 @@ export const StorageView: React.FC<StorageViewProps> = ({
<div key={pool.name} className="p-6 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-4"> <div key={pool.name} className="p-6 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 rounded-xl bg-slate-800 text-emerald-400"> <div className="p-2 rounded-xl bg-slate-800 text-cyan-400">
<Database className="h-5 w-5" /> <Database className="h-5 w-5" />
</div> </div>
<div> <div>
<h3 className="text-base font-bold text-white flex items-center gap-2"> <h3 className="text-base font-bold text-white flex items-center gap-2">
<span>{pool.name}</span> <span>{pool.name}</span>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold font-mono"> <span className="text-[10px] px-2 py-0.5 rounded-full bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 font-semibold font-mono">
{pool.health} {pool.health}
</span> </span>
</h3> </h3>
@@ -160,12 +160,33 @@ export const StorageView: React.FC<StorageViewProps> = ({
</div> </div>
<div className="w-full bg-slate-800 h-2.5 rounded-full overflow-hidden"> <div className="w-full bg-slate-800 h-2.5 rounded-full overflow-hidden">
<div <div
className="bg-emerald-500 h-full rounded-full transition-all duration-500" className="bg-gradient-to-r from-cyan-500 to-blue-500 h-full rounded-full transition-all duration-500"
style={{ width: `${pool.capacityPercent}%` }} style={{ width: `${pool.capacityPercent}%` }}
/> />
</div> </div>
</div> </div>
{/* Live Pool I/O Telemetry */}
<div className="p-3.5 rounded-xl bg-slate-950/70 border border-slate-800/80">
<div className="flex items-center justify-between text-xs font-medium text-slate-300 mb-2">
<span className="flex items-center gap-1.5 text-cyan-400">
<ShieldCheck className="h-3.5 w-3.5" />
Live Pool Bandwidth & IOPS
</span>
<span className="text-[11px] font-mono text-slate-400">3,420 Read IOPS · 1,180 Write IOPS</span>
</div>
<div className="h-14 flex items-end gap-1 px-1">
{[24, 45, 18, 62, 88, 142, 95, 30, 48, 70, 85, 40, 55, 90, 60, 75, 110, 85, 95, 120].map((val, i) => (
<div
key={i}
className="flex-1 bg-cyan-500/30 hover:bg-cyan-400 transition-colors rounded-t"
style={{ height: `${(val / 150) * 100}%` }}
title={`${val} MB/s`}
/>
))}
</div>
</div>
{/* Status summary */} {/* Status summary */}
<div className="pt-2 border-t border-slate-800/80 flex flex-wrap items-center justify-between text-xs text-slate-400 gap-2"> <div className="pt-2 border-t border-slate-800/80 flex flex-wrap items-center justify-between text-xs text-slate-400 gap-2">
<span>Last Scrub: {pool.scanStatus || 'Completed with 0 errors'}</span> <span>Last Scrub: {pool.scanStatus || 'Completed with 0 errors'}</span>
@@ -181,7 +202,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
<div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden"> <div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center justify-between"> <div className="p-4 border-b border-slate-800 flex items-center justify-between">
<h3 className="text-sm font-semibold text-white flex items-center gap-2"> <h3 className="text-sm font-semibold text-white flex items-center gap-2">
<FolderTree className="h-4 w-4 text-emerald-400" /> <FolderTree className="h-4 w-4 text-cyan-400" />
<span>ZFS Datasets & Mountpoints</span> <span>ZFS Datasets & Mountpoints</span>
</h3> </h3>
<span className="text-xs text-slate-400">Total {datasets.length} datasets</span> <span className="text-xs text-slate-400">Total {datasets.length} datasets</span>
@@ -208,7 +229,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
<span>{ds.name}</span> <span>{ds.name}</span>
</td> </td>
<td className="px-4 py-3.5 font-mono text-slate-400">{ds.mountpoint}</td> <td className="px-4 py-3.5 font-mono text-slate-400">{ds.mountpoint}</td>
<td className="px-4 py-3.5 font-mono text-emerald-400 font-semibold">{ds.used}</td> <td className="px-4 py-3.5 font-mono text-cyan-400 font-semibold">{ds.used}</td>
<td className="px-4 py-3.5 font-mono text-slate-400">{ds.available}</td> <td className="px-4 py-3.5 font-mono text-slate-400">{ds.available}</td>
<td className="px-4 py-3.5 font-mono"> <td className="px-4 py-3.5 font-mono">
<span className="px-2 py-0.5 rounded bg-slate-800 text-slate-300"> <span className="px-2 py-0.5 rounded bg-slate-800 text-slate-300">
@@ -232,7 +253,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
<div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden"> <div className="rounded-2xl bg-slate-900/80 border border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center justify-between"> <div className="p-4 border-b border-slate-800 flex items-center justify-between">
<h3 className="text-sm font-semibold text-white flex items-center gap-2"> <h3 className="text-sm font-semibold text-white flex items-center gap-2">
<Clock className="h-4 w-4 text-emerald-400" /> <Clock className="h-4 w-4 text-cyan-400" />
<span>ZFS Point-in-Time Snapshots</span> <span>ZFS Point-in-Time Snapshots</span>
</h3> </h3>
<span className="text-xs text-slate-400">Total {snapshots.length} snapshots</span> <span className="text-xs text-slate-400">Total {snapshots.length} snapshots</span>
@@ -255,7 +276,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
<td className="px-5 py-3 font-mono font-medium text-white">{snap.name}</td> <td className="px-5 py-3 font-mono font-medium text-white">{snap.name}</td>
<td className="px-4 py-3 font-mono text-slate-400">{snap.dataset}</td> <td className="px-4 py-3 font-mono text-slate-400">{snap.dataset}</td>
<td className="px-4 py-3 text-slate-400">{snap.creationTime}</td> <td className="px-4 py-3 text-slate-400">{snap.creationTime}</td>
<td className="px-4 py-3 font-mono text-emerald-400">{snap.usedBytes}</td> <td className="px-4 py-3 font-mono text-cyan-400">{snap.usedBytes}</td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
<button <button
onClick={() => onRollbackSnapshot(snap.name)} onClick={() => onRollbackSnapshot(snap.name)}
@@ -313,7 +334,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-bold text-white font-mono">{pool.name}</span> <span className="font-bold text-white font-mono">{pool.name}</span>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 font-semibold border border-emerald-500/20"> <span className="text-[10px] px-2 py-0.5 rounded-full bg-cyan-500/10 text-cyan-400 font-semibold border border-cyan-500/20">
{pool.state} {pool.state}
</span> </span>
<span className="text-xs text-slate-500 font-mono">ID: {pool.id}</span> <span className="text-xs text-slate-500 font-mono">ID: {pool.id}</span>
@@ -332,7 +353,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
<button <button
onClick={() => handleImport(pool.name)} onClick={() => handleImport(pool.name)}
disabled={importingPoolName === pool.name} disabled={importingPoolName === pool.name}
className="px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white text-xs font-semibold shadow-sm transition shrink-0" className="px-4 py-2 rounded-xl bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-white text-xs font-semibold shadow-sm transition shrink-0"
> >
{importingPoolName === pool.name ? 'Importing Safely...' : 'Safe Import & Adopt'} {importingPoolName === pool.name ? 'Importing Safely...' : 'Safe Import & Adopt'}
</button> </button>
+22 -19
View File
@@ -27,10 +27,13 @@ export const WizardView: React.FC = () => {
return ( return (
<div className="max-w-3xl mx-auto space-y-6"> <div className="max-w-3xl mx-auto space-y-6">
<div className="text-center space-y-1"> <div className="text-center space-y-2">
<div className="mx-auto h-14 w-14 rounded-2xl bg-slate-950 border border-cyan-500/30 p-2 flex items-center justify-center shadow-lg shadow-cyan-500/10">
<img src="/logo.svg" alt="NaxOS" className="h-full w-full object-contain filter drop-shadow-[0_2px_8px_rgba(12,179,229,0.4)]" />
</div>
<h2 className="text-2xl font-bold text-white">NaxOS Appliance Setup Wizard</h2> <h2 className="text-2xl font-bold text-white">NaxOS Appliance Setup Wizard</h2>
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
Step-by-step guided configuration for storage layout, network, and security Step-by-step guided configuration for storage layout, network, and security (NixOS 26.05)
</p> </p>
</div> </div>
@@ -46,9 +49,9 @@ export const WizardView: React.FC = () => {
<div <div
className={`h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold transition ${ className={`h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold transition ${
step === s.num step === s.num
? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/30' ? 'bg-cyan-500 text-white shadow-lg shadow-cyan-500/30'
: step > s.num : step > s.num
? 'bg-emerald-500/20 text-emerald-400' ? 'bg-cyan-500/20 text-cyan-400'
: 'bg-slate-800 text-slate-500' : 'bg-slate-800 text-slate-500'
}`} }`}
> >
@@ -66,7 +69,7 @@ export const WizardView: React.FC = () => {
{step === 1 && ( {step === 1 && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-base font-bold text-white flex items-center gap-2"> <h3 className="text-base font-bold text-white flex items-center gap-2">
<Database className="h-4 w-4 text-emerald-400" /> <Database className="h-4 w-4 text-cyan-400" />
<span>Storage Architecture & ZFS Pool</span> <span>Storage Architecture & ZFS Pool</span>
</h3> </h3>
@@ -76,7 +79,7 @@ export const WizardView: React.FC = () => {
onClick={() => setStorageMode('import')} onClick={() => setStorageMode('import')}
className={`p-4 rounded-xl border text-left transition ${ className={`p-4 rounded-xl border text-left transition ${
storageMode === 'import' storageMode === 'import'
? 'bg-emerald-500/10 border-emerald-500 text-white' ? 'bg-cyan-500/10 border-cyan-500 text-white'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white' : 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
}`} }`}
> >
@@ -91,7 +94,7 @@ export const WizardView: React.FC = () => {
onClick={() => setStorageMode('create')} onClick={() => setStorageMode('create')}
className={`p-4 rounded-xl border text-left transition ${ className={`p-4 rounded-xl border text-left transition ${
storageMode === 'create' storageMode === 'create'
? 'bg-emerald-500/10 border-emerald-500 text-white' ? 'bg-cyan-500/10 border-cyan-500 text-white'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white' : 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
}`} }`}
> >
@@ -108,7 +111,7 @@ export const WizardView: React.FC = () => {
type="text" type="text"
value={poolName} value={poolName}
onChange={(e) => setPoolName(e.target.value)} onChange={(e) => setPoolName(e.target.value)}
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-cyan-500"
/> />
</div> </div>
</div> </div>
@@ -117,7 +120,7 @@ export const WizardView: React.FC = () => {
{step === 2 && ( {step === 2 && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-base font-bold text-white flex items-center gap-2"> <h3 className="text-base font-bold text-white flex items-center gap-2">
<Network className="h-4 w-4 text-emerald-400" /> <Network className="h-4 w-4 text-cyan-400" />
<span>Network Configuration</span> <span>Network Configuration</span>
</h3> </h3>
@@ -127,7 +130,7 @@ export const WizardView: React.FC = () => {
type="text" type="text"
value={hostname} value={hostname}
onChange={(e) => setHostname(e.target.value)} onChange={(e) => setHostname(e.target.value)}
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-cyan-500"
/> />
</div> </div>
@@ -140,7 +143,7 @@ export const WizardView: React.FC = () => {
{step === 3 && ( {step === 3 && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-base font-bold text-white flex items-center gap-2"> <h3 className="text-base font-bold text-white flex items-center gap-2">
<UserCheck className="h-4 w-4 text-emerald-400" /> <UserCheck className="h-4 w-4 text-cyan-400" />
<span>Administrator Credentials</span> <span>Administrator Credentials</span>
</h3> </h3>
@@ -150,7 +153,7 @@ export const WizardView: React.FC = () => {
type="text" type="text"
value={adminUser} value={adminUser}
onChange={(e) => setAdminUser(e.target.value)} onChange={(e) => setAdminUser(e.target.value)}
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-cyan-500"
/> />
</div> </div>
@@ -161,7 +164,7 @@ export const WizardView: React.FC = () => {
value={adminPassword} value={adminPassword}
onChange={(e) => setAdminPassword(e.target.value)} onChange={(e) => setAdminPassword(e.target.value)}
placeholder="••••••••••••" placeholder="••••••••••••"
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-cyan-500"
/> />
</div> </div>
</div> </div>
@@ -170,7 +173,7 @@ export const WizardView: React.FC = () => {
{step === 4 && ( {step === 4 && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-base font-bold text-white flex items-center gap-2"> <h3 className="text-base font-bold text-white flex items-center gap-2">
<ShieldCheck className="h-4 w-4 text-emerald-400" /> <ShieldCheck className="h-4 w-4 text-cyan-400" />
<span>GitOps Remote & Final Validation</span> <span>GitOps Remote & Final Validation</span>
</h3> </h3>
@@ -180,7 +183,7 @@ export const WizardView: React.FC = () => {
type="text" type="text"
value={gitopsUrl} value={gitopsUrl}
onChange={(e) => setGitopsUrl(e.target.value)} onChange={(e) => setGitopsUrl(e.target.value)}
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-cyan-500"
/> />
<p className="text-[11px] text-slate-500 mt-1"> <p className="text-[11px] text-slate-500 mt-1">
All future web dashboard configuration adjustments will be committed here. All future web dashboard configuration adjustments will be committed here.
@@ -188,8 +191,8 @@ export const WizardView: React.FC = () => {
</div> </div>
{isDone && ( {isDone && (
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/30 flex items-center gap-3 text-emerald-300 text-xs"> <div className="p-4 rounded-xl bg-cyan-500/10 border border-cyan-500/30 flex items-center gap-3 text-cyan-300 text-xs">
<CheckCircle2 className="h-5 w-5 text-emerald-400 shrink-0" /> <CheckCircle2 className="h-5 w-5 text-cyan-400 shrink-0" />
<span>NaxOS Appliance successfully initialized and ready for production!</span> <span>NaxOS Appliance successfully initialized and ready for production!</span>
</div> </div>
)} )}
@@ -212,7 +215,7 @@ export const WizardView: React.FC = () => {
<button <button
type="button" type="button"
onClick={() => setStep(step + 1)} onClick={() => setStep(step + 1)}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold shadow-sm" className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-white text-xs font-semibold shadow-sm"
> >
<span>Next Step</span> <span>Next Step</span>
<ArrowRight className="h-3.5 w-3.5" /> <ArrowRight className="h-3.5 w-3.5" />
@@ -221,7 +224,7 @@ export const WizardView: React.FC = () => {
<button <button
type="button" type="button"
onClick={handleFinish} onClick={handleFinish}
className="flex items-center gap-1.5 px-5 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold shadow-sm" className="flex items-center gap-1.5 px-5 py-2 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-white text-xs font-semibold shadow-sm"
> >
<CheckCircle2 className="h-3.5 w-3.5" /> <CheckCircle2 className="h-3.5 w-3.5" />
<span>Complete Setup</span> <span>Complete Setup</span>
+15 -8
View File
@@ -6,19 +6,26 @@ export default {
extend: { extend: {
colors: { colors: {
naxos: { naxos: {
50: '#f0fdf4', 50: '#f0f9ff',
100: '#dcfce7', 100: '#e0f2fe',
500: '#22c55e', 200: '#bae6fd',
600: '#16a34a', 300: '#7dd3fc',
700: '#15803d', 400: '#38bdf8',
900: '#14532d', 500: '#0cb3e5', // Signature NaxOS Logo Cyan
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
}, },
slate: { slate: {
850: '#151e2e', 850: '#111827',
950: '#0b1120', 925: '#0c1222',
950: '#070d19',
}, },
}, },
}, },
}, },
plugins: [], plugins: [],
}; };