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
@@ -7,7 +7,7 @@
|
||||
<p align="center">
|
||||
<b>Modern, Responsive Web Interface for NaxOS Storage Appliance (NixOS 26.05 & OpenZFS)</b>
|
||||
<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 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
|
||||
|
||||
| Overview & Telemetry | OpenZFS Storage Engine |
|
||||
| Overview & Live Telemetry | OpenZFS Storage Engine |
|
||||
| :---: | :---: |
|
||||
|  |  |
|
||||
| **CNCF Perses Analytics** | **App Store & Runtimes** |
|
||||
|  |  |
|
||||
| **Integrated Performance Telemetry** | **App Store & Runtimes** |
|
||||
|  |  |
|
||||
| **Setup & Migration Wizard** | **File Shares Management** |
|
||||
|  |  |
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<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">
|
||||
</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>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 271 KiB |
@@ -7,7 +7,6 @@ import { OverviewView } from './views/OverviewView.js';
|
||||
import { StorageView } from './views/StorageView.js';
|
||||
import { SharesView } from './views/SharesView.js';
|
||||
import { AppsView } from './views/AppsView.js';
|
||||
import { AnalyticsView } from './views/AnalyticsView.js';
|
||||
import { GitOpsView } from './views/GitOpsView.js';
|
||||
import { LogsView } from './views/LogsView.js';
|
||||
import { WizardView } from './views/WizardView.js';
|
||||
@@ -187,8 +186,8 @@ export const App: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen overflow-hidden bg-slate-950 text-slate-100 font-sans">
|
||||
<Sidebar currentTab={currentTab} onSelectTab={selectTab} />
|
||||
<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 === 'analytics' ? 'overview' : currentTab} onSelectTab={selectTab} />
|
||||
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
<Topbar
|
||||
@@ -199,7 +198,7 @@ export const App: React.FC = () => {
|
||||
/>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-8">
|
||||
{currentTab === 'overview' && (
|
||||
{(currentTab === 'overview' || currentTab === 'analytics') && (
|
||||
<OverviewView
|
||||
status={status}
|
||||
pools={pools}
|
||||
@@ -241,8 +240,6 @@ export const App: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentTab === 'analytics' && <AnalyticsView />}
|
||||
|
||||
{currentTab === 'gitops' && (
|
||||
<GitOpsView
|
||||
status={gitops}
|
||||
|
||||
@@ -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">
|
||||
{/* Header */}
|
||||
<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="p-1.5 rounded-lg bg-slate-800 text-emerald-400">
|
||||
<Terminal className="h-4 w-4" />
|
||||
<div className="flex items-center gap-3">
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</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">
|
||||
{logs.map((log, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<span className="text-slate-600 select-none">></span>
|
||||
<span className={log.includes('PASSED') ? 'text-emerald-400 font-semibold' : ''}>
|
||||
<span className="text-cyan-500/60 select-none">></span>
|
||||
<span className={log.includes('PASSED') ? 'text-cyan-400 font-semibold' : ''}>
|
||||
{log}
|
||||
</span>
|
||||
</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="flex items-center gap-2">
|
||||
{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" />
|
||||
Switch completed successfully
|
||||
</span>
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
HardDrive,
|
||||
FolderSync,
|
||||
Layers,
|
||||
LineChart,
|
||||
GitBranch,
|
||||
FileTerminal,
|
||||
Server,
|
||||
Settings,
|
||||
Sparkles,
|
||||
ShieldCheck,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SidebarProps {
|
||||
@@ -21,30 +21,36 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentTab, onSelectTab }) =>
|
||||
{ id: 'overview', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ id: 'storage', label: 'Storage & ZFS', icon: HardDrive, badge: 'ZFS' },
|
||||
{ id: 'shares', label: 'File Shares', icon: FolderSync, badge: 'SMB/NFS' },
|
||||
{ id: 'apps', label: 'App Store & Runtime', icon: Layers },
|
||||
{ id: 'analytics', label: 'Perses Analytics', icon: LineChart, badge: 'Native' },
|
||||
{ id: 'apps', label: 'App Store & Services', icon: Layers },
|
||||
{ id: 'gitops', label: 'GitOps & Updates', icon: GitBranch },
|
||||
{ id: 'logs', label: 'System Logs', icon: FileTerminal },
|
||||
{ id: 'wizard', label: 'Setup Wizard', icon: Settings },
|
||||
{ id: 'wizard', label: 'Setup & Migration', icon: Settings },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-slate-900/90 border-r border-slate-800 flex flex-col backdrop-blur-xl shrink-0">
|
||||
{/* Brand Header */}
|
||||
<div className="h-16 flex items-center px-6 gap-3 border-b border-slate-800">
|
||||
<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">
|
||||
<img src="/logo.svg" alt="NaxOS Logo" className="h-full w-full object-contain" />
|
||||
<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 with Portara Logo */}
|
||||
<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-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 filter drop-shadow-[0_2px_8px_rgba(12,179,229,0.35)] transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold tracking-tight text-white flex items-center gap-1.5">
|
||||
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>
|
||||
<div className="min-w-0">
|
||||
<div className="font-bold tracking-tight text-white flex items-center gap-1.5 text-base">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = currentTab === item.id;
|
||||
@@ -52,19 +58,19 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentTab, onSelectTab }) =>
|
||||
<button
|
||||
key={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
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
{item.badge && (
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded font-mono font-semibold ${
|
||||
isActive
|
||||
? 'bg-emerald-500/20 text-emerald-300'
|
||||
? 'bg-cyan-500/20 text-cyan-300'
|
||||
: 'bg-slate-800 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
@@ -77,15 +83,19 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentTab, onSelectTab }) =>
|
||||
</nav>
|
||||
|
||||
{/* 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="h-2.5 w-2.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<div className="text-xs">
|
||||
<p className="text-slate-200 font-medium">System Protected</p>
|
||||
<p className="text-slate-500 text-[11px]">GitOps Synchronized</p>
|
||||
<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 min-w-0">
|
||||
<p className="text-slate-200 font-medium truncate">System Healthy</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>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ export const Topbar: React.FC<TopbarProps> = ({
|
||||
</span>
|
||||
</h1>
|
||||
<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" />
|
||||
<span>ARC Hit: <strong className="text-emerald-400">{status?.arcHitRatioPercent || 98.6}%</strong></span>
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-cyan-400" />
|
||||
<span>ARC Hit: <strong className="text-cyan-400">{status?.arcHitRatioPercent || 98.6}%</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export const Topbar: React.FC<TopbarProps> = ({
|
||||
|
||||
<button
|
||||
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" />
|
||||
<span>Apply Changes</span>
|
||||
|
||||
@@ -20,10 +20,20 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
background: #1e293b;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(12, 179, 229, 0.15);
|
||||
}
|
||||
|
||||
::-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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -69,7 +69,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
|
||||
<button
|
||||
onClick={() => setActiveTab('catalog')}
|
||||
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})
|
||||
@@ -77,7 +77,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
|
||||
<button
|
||||
onClick={() => setActiveTab('installed')}
|
||||
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})
|
||||
@@ -95,7 +95,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
|
||||
return (
|
||||
<div
|
||||
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 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 })
|
||||
}
|
||||
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) => (
|
||||
<option key={r} value={r}>
|
||||
@@ -135,7 +135,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
|
||||
|
||||
{/* Action button */}
|
||||
{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" />
|
||||
<span>Installed & Active</span>
|
||||
</div>
|
||||
@@ -143,7 +143,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
|
||||
<button
|
||||
onClick={() => handleInstall(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'}
|
||||
</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="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">
|
||||
<Layers className="h-4 w-4 text-emerald-400" />
|
||||
<Layers className="h-4 w-4 text-cyan-400" />
|
||||
<span>Active Workloads</span>
|
||||
</h3>
|
||||
<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>
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded-full font-semibold ${
|
||||
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'
|
||||
}`}>
|
||||
{app.status.toUpperCase()}
|
||||
@@ -217,7 +217,7 @@ export const AppsView: React.FC<AppsViewProps> = ({
|
||||
) : (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Play className="h-3.5 w-3.5 fill-current" />
|
||||
|
||||
@@ -53,7 +53,7 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
|
||||
|
||||
<button
|
||||
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" />
|
||||
<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">
|
||||
<span className="text-xs text-slate-400 font-medium">Remote Synchronization</span>
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
<span className="text-xs text-slate-400 font-medium">Current Head Commit</span>
|
||||
<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>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-slate-300 truncate">
|
||||
@@ -90,14 +90,14 @@ export const GitOpsView: React.FC<GitOpsViewProps> = ({
|
||||
<select
|
||||
value={selectedChannel}
|
||||
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="beta">Beta Channel</option>
|
||||
<option value="nightly">Nightly Unstable</option>
|
||||
</select>
|
||||
</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" />
|
||||
<span>Appliance is up to date</span>
|
||||
</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="p-4 border-b border-slate-800 flex items-center justify-between">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<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 && (
|
||||
<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
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -58,7 +58,7 @@ export const LogsView: React.FC<LogsViewProps> = ({
|
||||
onClick={() => setIsStreaming(!isStreaming)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium transition ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -85,7 +85,7 @@ export const LogsView: React.FC<LogsViewProps> = ({
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -96,7 +96,7 @@ export const LogsView: React.FC<LogsViewProps> = ({
|
||||
onClick={() => onSelectUnit(u)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition ${
|
||||
selectedUnit === u
|
||||
? 'bg-emerald-600 text-white shadow-sm'
|
||||
? 'bg-cyan-600 text-white shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
HardDrive,
|
||||
Cpu,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
TrendingUp,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { AppIcon } from '../components/AppIcon.js';
|
||||
import type { SystemStatus, ZfsPool, InstalledApp } from '../types/index.js';
|
||||
@@ -26,42 +27,85 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
onNavigate,
|
||||
}) => {
|
||||
const primaryPool = pools[0];
|
||||
const [telemetryRange, setTelemetryRange] = useState<'15m' | '1h' | '6h' | '24h'>('1h');
|
||||
|
||||
// Sparkline data based on selected time range
|
||||
const throughputData = {
|
||||
'15m': [34, 45, 62, 50, 78, 110, 95, 82, 60, 88, 115, 142, 90, 75, 105],
|
||||
'1h': [42, 65, 30, 85, 95, 120, 80, 45, 60, 90, 110, 75, 40, 95, 130],
|
||||
'6h': [20, 35, 45, 80, 110, 145, 130, 95, 70, 85, 60, 50, 65, 90, 85],
|
||||
'24h': [15, 25, 40, 65, 90, 120, 140, 110, 85, 70, 60, 55, 70, 80, 95],
|
||||
}[telemetryRange];
|
||||
|
||||
const arcRatioData = {
|
||||
'15m': [98.2, 98.4, 98.5, 98.3, 98.6, 98.8, 98.7, 98.5, 98.9, 98.6, 98.7, 98.6, 98.8, 98.7, 98.6],
|
||||
'1h': [97.8, 98.1, 98.4, 98.2, 98.6, 98.8, 98.5, 98.7, 98.4, 98.9, 98.6, 98.7, 98.5, 98.8, 98.6],
|
||||
'6h': [97.2, 97.6, 98.0, 98.3, 98.5, 98.7, 98.4, 98.6, 98.8, 98.5, 98.7, 98.6, 98.4, 98.6, 98.6],
|
||||
'24h': [96.8, 97.2, 97.9, 98.1, 98.4, 98.6, 98.7, 98.5, 98.6, 98.7, 98.5, 98.6, 98.7, 98.8, 98.6],
|
||||
}[telemetryRange];
|
||||
|
||||
const cpuData = {
|
||||
'15m': [8, 12, 14, 22, 38, 18, 11, 9, 15, 18, 28, 15, 12, 14, 11],
|
||||
'1h': [10, 14, 18, 25, 42, 19, 12, 10, 16, 20, 32, 16, 14, 15, 12],
|
||||
'6h': [8, 10, 15, 30, 48, 25, 14, 12, 18, 22, 35, 18, 15, 16, 13],
|
||||
'24h': [6, 8, 12, 28, 45, 30, 15, 11, 16, 24, 38, 20, 16, 18, 12],
|
||||
}[telemetryRange];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Top Banner / Welcome */}
|
||||
<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">
|
||||
{/* Top Banner / Hero Header with Portara Styling */}
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-r from-slate-900 via-slate-900/90 to-cyan-950/40 border border-slate-800 p-6 shadow-xl">
|
||||
<div className="absolute right-0 top-0 bottom-0 w-96 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-cyan-500/10 via-transparent to-transparent pointer-events-none" />
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 relative z-10">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-12 w-12 rounded-xl bg-slate-950 border border-cyan-500/30 p-2 flex items-center justify-center shadow-lg shadow-cyan-500/10 shrink-0">
|
||||
<img src="/logo.svg" alt="NaxOS" className="h-full w-full object-contain filter drop-shadow-[0_2px_6px_rgba(12,179,229,0.4)]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<h2 className="text-xl font-bold text-white tracking-tight">Appliance Overview</h2>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/15 text-cyan-300 border border-cyan-500/30 font-mono">
|
||||
NixOS 26.05
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-blue-500/10 text-blue-300 border border-blue-500/20 font-mono">
|
||||
OpenZFS 2.2
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
<button
|
||||
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>
|
||||
<ArrowUpRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Storage Pool Card */}
|
||||
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between">
|
||||
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">Pool: {primaryPool?.name || 'tank'}</span>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-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'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -72,7 +116,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
{/* Progress Bar */}
|
||||
<div className="w-full bg-slate-800 h-2 rounded-full mt-2 overflow-hidden">
|
||||
<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}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -84,27 +128,28 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
</div>
|
||||
|
||||
{/* ZFS ARC Hit Ratio */}
|
||||
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between">
|
||||
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">ZFS ARC Cache</span>
|
||||
<ShieldCheck className="h-4 w-4 text-emerald-400" />
|
||||
<ShieldCheck className="h-4 w-4 text-cyan-400" />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="text-2xl font-bold text-white tracking-tight">
|
||||
{status?.arcHitRatioPercent || 98.6}%
|
||||
<div className="text-2xl font-bold text-white tracking-tight flex items-baseline gap-2">
|
||||
<span>{status?.arcHitRatioPercent || 98.6}%</span>
|
||||
<span className="text-xs text-cyan-400 font-normal">Efficiency</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
ARC Size: <strong>4.0 GB</strong> (Max: 4.0 GB)
|
||||
ARC Size: <strong className="text-slate-200">4.0 GB</strong> (Target: 4.0 GB)
|
||||
</p>
|
||||
</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" />
|
||||
<span>Optimal cache hit performance</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workloads Card */}
|
||||
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between">
|
||||
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">Running Workloads</span>
|
||||
<Layers className="h-4 w-4 text-sky-400" />
|
||||
@@ -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">
|
||||
<AppIcon name="jellyfin" className="h-5 w-5" />
|
||||
</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" />
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{/* 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">
|
||||
<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 className="mt-4">
|
||||
<div className="text-2xl font-bold text-white tracking-tight">
|
||||
@@ -148,7 +193,173 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
@@ -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="flex items-center justify-between mb-4">
|
||||
<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>
|
||||
<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">
|
||||
Homarr Dashboard
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
Native Workload Engine
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onNavigate('apps')}
|
||||
className="text-xs text-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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Workload 1: Immich Photo Hub */}
|
||||
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-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="immich" 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>
|
||||
<span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
|
||||
<span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-white mt-3">Immich Photo Hub</h4>
|
||||
@@ -198,7 +409,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
href="http://naxos.local:2283"
|
||||
target="_blank"
|
||||
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>
|
||||
<ArrowUpRight className="h-3 w-3" />
|
||||
@@ -207,15 +418,15 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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 className="flex items-start justify-between">
|
||||
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
|
||||
<AppIcon name="jellyfin" className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span className="text-[10px] font-mono text-emerald-400 font-medium">ONLINE</span>
|
||||
<span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
|
||||
<span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-white mt-3">Jellyfin Media Server</h4>
|
||||
@@ -231,7 +442,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
href="http://naxos.local:8096"
|
||||
target="_blank"
|
||||
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>
|
||||
<ArrowUpRight className="h-3 w-3" />
|
||||
@@ -239,39 +450,8 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workload 3: CNCF Perses Analytics */}
|
||||
<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 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">
|
||||
{/* Workload 3: Nextcloud Hub */}
|
||||
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
|
||||
@@ -279,7 +459,7 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
</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">READY</span>
|
||||
<span className="text-[10px] font-mono text-slate-400 font-medium">CATALOG</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-white mt-3">Nextcloud Hub</h4>
|
||||
@@ -289,11 +469,42 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
</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">
|
||||
Catalog
|
||||
Ready
|
||||
</span>
|
||||
<button
|
||||
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>
|
||||
<ArrowUpRight className="h-3 w-3" />
|
||||
@@ -302,70 +513,6 @@ export const OverviewView: React.FC<OverviewViewProps> = ({
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
<button
|
||||
onClick={() => setActiveTab('smb')}
|
||||
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)
|
||||
@@ -74,7 +74,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
<button
|
||||
onClick={() => setActiveTab('nfs')}
|
||||
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
|
||||
@@ -83,7 +83,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
|
||||
<button
|
||||
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" />
|
||||
<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="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">
|
||||
<FolderSync className="h-4 w-4 text-emerald-400" />
|
||||
<FolderSync className="h-4 w-4 text-cyan-400" />
|
||||
<span>Samba (SMB) Shares</span>
|
||||
</h3>
|
||||
<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 className="px-4 py-3.5">
|
||||
{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" />
|
||||
<span>Enabled {share.timeMachineMaxSize ? `(${share.timeMachineMaxSize})` : ''}</span>
|
||||
</div>
|
||||
@@ -175,7 +175,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
<div className="flex items-center gap-2 mt-1 text-slate-400">
|
||||
<span>Allowed Clients:</span>
|
||||
{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})
|
||||
</span>
|
||||
))}
|
||||
@@ -200,7 +200,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
value={newShareName}
|
||||
onChange={(e) => setNewShareName(e.target.value)}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
value={newSharePath}
|
||||
onChange={(e) => setNewSharePath(e.target.value)}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
@@ -226,7 +226,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
type="checkbox"
|
||||
checked={newShareGuest}
|
||||
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>
|
||||
|
||||
@@ -240,7 +240,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
type="checkbox"
|
||||
checked={newShareTimeMachine}
|
||||
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>
|
||||
|
||||
@@ -268,7 +268,7 @@ export const SharesView: React.FC<SharesViewProps> = ({
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -69,7 +69,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
|
||||
onClick={() => setActiveSubTab('pools')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
|
||||
activeSubTab === 'pools'
|
||||
? 'bg-emerald-600 text-white shadow-sm'
|
||||
? 'bg-cyan-600 text-white shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -79,7 +79,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
|
||||
onClick={() => setActiveSubTab('datasets')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
|
||||
activeSubTab === 'datasets'
|
||||
? 'bg-emerald-600 text-white shadow-sm'
|
||||
? 'bg-cyan-600 text-white shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -89,7 +89,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
|
||||
onClick={() => setActiveSubTab('snapshots')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
|
||||
activeSubTab === 'snapshots'
|
||||
? 'bg-emerald-600 text-white shadow-sm'
|
||||
? 'bg-cyan-600 text-white shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -112,8 +112,8 @@ export const StorageView: React.FC<StorageViewProps> = ({
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-400 shrink-0" />
|
||||
<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-cyan-400 shrink-0" />
|
||||
<span>{importSuccess}</span>
|
||||
</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 className="flex flex-col sm:flex-row sm:items-center justify-between 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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<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}
|
||||
</span>
|
||||
</h3>
|
||||
@@ -160,12 +160,33 @@ export const StorageView: React.FC<StorageViewProps> = ({
|
||||
</div>
|
||||
<div className="w-full bg-slate-800 h-2.5 rounded-full overflow-hidden">
|
||||
<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}%` }}
|
||||
/>
|
||||
</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 */}
|
||||
<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>
|
||||
@@ -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="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">
|
||||
<FolderTree className="h-4 w-4 text-emerald-400" />
|
||||
<FolderTree className="h-4 w-4 text-cyan-400" />
|
||||
<span>ZFS Datasets & Mountpoints</span>
|
||||
</h3>
|
||||
<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>
|
||||
</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">
|
||||
<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="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">
|
||||
<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>
|
||||
</h3>
|
||||
<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-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 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">
|
||||
<button
|
||||
onClick={() => onRollbackSnapshot(snap.name)}
|
||||
@@ -313,7 +334,7 @@ export const StorageView: React.FC<StorageViewProps> = ({
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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}
|
||||
</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
|
||||
onClick={() => handleImport(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'}
|
||||
</button>
|
||||
|
||||
@@ -27,10 +27,13 @@ export const WizardView: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +49,9 @@ export const WizardView: React.FC = () => {
|
||||
<div
|
||||
className={`h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold transition ${
|
||||
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
|
||||
? 'bg-emerald-500/20 text-emerald-400'
|
||||
? 'bg-cyan-500/20 text-cyan-400'
|
||||
: 'bg-slate-800 text-slate-500'
|
||||
}`}
|
||||
>
|
||||
@@ -66,7 +69,7 @@ export const WizardView: React.FC = () => {
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
@@ -76,7 +79,7 @@ export const WizardView: React.FC = () => {
|
||||
onClick={() => setStorageMode('import')}
|
||||
className={`p-4 rounded-xl border text-left transition ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -91,7 +94,7 @@ export const WizardView: React.FC = () => {
|
||||
onClick={() => setStorageMode('create')}
|
||||
className={`p-4 rounded-xl border text-left transition ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -108,7 +111,7 @@ export const WizardView: React.FC = () => {
|
||||
type="text"
|
||||
value={poolName}
|
||||
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>
|
||||
@@ -117,7 +120,7 @@ export const WizardView: React.FC = () => {
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
@@ -127,7 +130,7 @@ export const WizardView: React.FC = () => {
|
||||
type="text"
|
||||
value={hostname}
|
||||
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>
|
||||
|
||||
@@ -140,7 +143,7 @@ export const WizardView: React.FC = () => {
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
@@ -150,7 +153,7 @@ export const WizardView: React.FC = () => {
|
||||
type="text"
|
||||
value={adminUser}
|
||||
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>
|
||||
|
||||
@@ -161,7 +164,7 @@ export const WizardView: React.FC = () => {
|
||||
value={adminPassword}
|
||||
onChange={(e) => setAdminPassword(e.target.value)}
|
||||
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>
|
||||
@@ -170,7 +173,7 @@ export const WizardView: React.FC = () => {
|
||||
{step === 4 && (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
@@ -180,7 +183,7 @@ export const WizardView: React.FC = () => {
|
||||
type="text"
|
||||
value={gitopsUrl}
|
||||
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">
|
||||
All future web dashboard configuration adjustments will be committed here.
|
||||
@@ -188,8 +191,8 @@ export const WizardView: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-400 shrink-0" />
|
||||
<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-cyan-400 shrink-0" />
|
||||
<span>NaxOS Appliance successfully initialized and ready for production!</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -212,7 +215,7 @@ export const WizardView: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
@@ -221,7 +224,7 @@ export const WizardView: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
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" />
|
||||
<span>Complete Setup</span>
|
||||
|
||||
@@ -6,19 +6,26 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
naxos: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
900: '#14532d',
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0cb3e5', // Signature NaxOS Logo Cyan
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
950: '#082f49',
|
||||
},
|
||||
slate: {
|
||||
850: '#151e2e',
|
||||
950: '#0b1120',
|
||||
850: '#111827',
|
||||
925: '#0c1222',
|
||||
950: '#070d19',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
|
||||