feat(ui): implement modern NaxOS Web Dashboard with Perses analytics and ZFS migration wizard
CI & Build NaxOS Web Dashboard / build (push) Failing after 1m29s

This commit is contained in:
Lukas Holzner
2026-09-03 23:59:06 +02:00
parent 155beec162
commit eaad4719eb
27 changed files with 5580 additions and 2 deletions
+234
View File
@@ -0,0 +1,234 @@
import React, { useState } from 'react';
import {
HardDrive,
Network,
UserCheck,
CheckCircle2,
ArrowRight,
ArrowLeft,
Database,
ShieldCheck,
Server,
} from 'lucide-react';
export const WizardView: React.FC = () => {
const [step, setStep] = useState(1);
const [storageMode, setStorageMode] = useState<'create' | 'import'>('import');
const [poolName, setPoolName] = useState('tank');
const [hostname, setHostname] = useState('naxos');
const [adminUser, setAdminUser] = useState('admin');
const [adminPassword, setAdminPassword] = useState('');
const [gitopsUrl, setGitopsUrl] = useState('ssh://git@git.lholz.de:2222/naxos/naxos-config.git');
const [isDone, setIsDone] = useState(false);
const handleFinish = () => {
setIsDone(true);
};
return (
<div className="max-w-3xl mx-auto space-y-6">
<div className="text-center space-y-1">
<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
</p>
</div>
{/* Progress Steps */}
<div className="flex items-center justify-between px-6 py-4 rounded-2xl bg-slate-900 border border-slate-800">
{[
{ num: 1, title: 'Storage & Pools' },
{ num: 2, title: 'Network' },
{ num: 3, title: 'Credentials' },
{ num: 4, title: 'GitOps & Finish' },
].map((s) => (
<div key={s.num} className="flex items-center gap-2">
<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'
: step > s.num
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-slate-800 text-slate-500'
}`}
>
{step > s.num ? <CheckCircle2 className="h-4 w-4" /> : s.num}
</div>
<span className={`text-xs font-medium hidden sm:inline ${step === s.num ? 'text-white' : 'text-slate-500'}`}>
{s.title}
</span>
</div>
))}
</div>
{/* Form Container */}
<div className="p-8 rounded-2xl bg-slate-900 border border-slate-800 space-y-6">
{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" />
<span>Storage Architecture & ZFS Pool</span>
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
type="button"
onClick={() => setStorageMode('import')}
className={`p-4 rounded-xl border text-left transition ${
storageMode === 'import'
? 'bg-emerald-500/10 border-emerald-500 text-white'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
}`}
>
<div className="font-bold text-xs">Safe Import Existing Pool</div>
<div className="text-[11px] text-slate-400 mt-1">
Import TrueNAS or nixos-lukas pool without formatting. Preserves Immich photo library.
</div>
</button>
<button
type="button"
onClick={() => setStorageMode('create')}
className={`p-4 rounded-xl border text-left transition ${
storageMode === 'create'
? 'bg-emerald-500/10 border-emerald-500 text-white'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
}`}
>
<div className="font-bold text-xs">Create New Pool</div>
<div className="text-[11px] text-slate-400 mt-1">
Initialize fresh ZFS mirror or RAID-Z pool across raw disks with ashift=12.
</div>
</button>
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1">Target Pool Name</label>
<input
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"
/>
</div>
</div>
)}
{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" />
<span>Network Configuration</span>
</h3>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1">Appliance Hostname</label>
<input
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"
/>
</div>
<div className="p-3 rounded-xl bg-slate-950 border border-slate-800 text-xs text-slate-400">
DHCP will automatically request an IP address and advertise mDNS name <strong className="text-white font-mono">{hostname}.local</strong> via Avahi.
</div>
</div>
)}
{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" />
<span>Administrator Credentials</span>
</h3>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1">Admin Username</label>
<input
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"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1">Admin Password</label>
<input
type="password"
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"
/>
</div>
</div>
)}
{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" />
<span>GitOps Remote & Final Validation</span>
</h3>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1">Remote Git Repository (Gitea)</label>
<input
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"
/>
<p className="text-[11px] text-slate-500 mt-1">
All future web dashboard configuration adjustments will be committed here.
</p>
</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" />
<span>NaxOS Appliance successfully initialized and ready for production!</span>
</div>
)}
</div>
)}
{/* Buttons */}
<div className="flex justify-between pt-4 border-t border-slate-800">
<button
type="button"
disabled={step === 1}
onClick={() => setStep(step - 1)}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 disabled:opacity-30 text-slate-300 text-xs font-medium"
>
<ArrowLeft className="h-3.5 w-3.5" />
<span>Back</span>
</button>
{step < 4 ? (
<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"
>
<span>Next Step</span>
<ArrowRight className="h-3.5 w-3.5" />
</button>
) : (
<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"
>
<CheckCircle2 className="h-3.5 w-3.5" />
<span>Complete Setup</span>
</button>
)}
</div>
</div>
</div>
);
};