feat: add initial setup wizard, workload insights, and policy audit mode
CI / backend (push) Failing after 3s
CI / frontend (push) Failing after 29s

Add setup wizard with status tracking via SystemSetting model, implement /setup/status and /setup/complete endpoints to create initial admin user and optional cluster configuration, add workload insights endpoint with traffic analysis and policy matching including audit mode detection, implement enforcement_mode property on Policy model with audit/enforced states, add Modal component for dialogs, create SetupWizard page with multi
This commit is contained in:
2026-07-09 12:47:08 +02:00
parent a911d36f34
commit 3cd2c0a2f1
18 changed files with 600 additions and 79 deletions
+89
View File
@@ -0,0 +1,89 @@
import { FormEvent, useState } from "react";
import { CheckCircle2, Server, ShieldCheck, Sparkles } from "lucide-react";
import { publicApi } from "../api/client";
import { buttonClass, Field, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls";
export function SetupWizard() {
const [step, setStep] = useState(0);
const [error, setError] = useState("");
const [form, setForm] = useState({
admin_email: "admin@nexafabric.local",
admin_name: "NexaFabric Administrator",
admin_password: "ChangeMe_UseEnvInstead",
cluster_name: "Production Proxmox",
cluster_api_url: "https://pve.example.local:8006",
cluster_api_token: "",
cluster_provider: "proxmox",
cluster_mode: "read_only",
verify_tls: true,
});
async function complete(event: FormEvent) {
event.preventDefault();
setError("");
try {
await publicApi("/setup/complete", { method: "POST", body: JSON.stringify(form) });
window.location.href = "/login";
} catch (err) {
setError(err instanceof Error ? err.message : "Setup failed");
}
}
return (
<div className="min-h-screen bg-canvas px-4 py-8 text-slate-900 dark:text-slate-100">
<div className="mx-auto max-w-4xl">
<div className="mb-8 flex items-center gap-3">
<div className="rounded-md bg-accent p-3 text-white"><Sparkles size={24} /></div>
<div>
<h1 className="text-3xl font-semibold">Welcome to NexaFabric</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">Create your first administrator and connect your first provider.</p>
</div>
</div>
<div className="mb-6 grid gap-3 md:grid-cols-3">
{["Admin", "Provider", "Finish"].map((label, index) => (
<div key={label} className={`rounded-md border p-3 text-sm ${index === step ? "border-accent bg-panel" : "border-border bg-panel/60"}`}>
<div className="font-medium">{label}</div>
<div className="text-xs text-slate-500">{index < step ? "Done" : index === step ? "Current" : "Pending"}</div>
</div>
))}
</div>
<form onSubmit={complete} className="rounded-md border border-border bg-panel p-5">
{step === 0 ? (
<div className="grid gap-4">
<div className="flex items-center gap-2 font-medium"><ShieldCheck size={18} /> Super Admin</div>
<Field label="Email"><input className={inputClass} value={form.admin_email} onChange={(event) => setForm({ ...form, admin_email: event.target.value })} /></Field>
<Field label="Name"><input className={inputClass} value={form.admin_name} onChange={(event) => setForm({ ...form, admin_name: event.target.value })} /></Field>
<Field label="Password"><input className={inputClass} type="password" value={form.admin_password} onChange={(event) => setForm({ ...form, admin_password: event.target.value })} /></Field>
</div>
) : null}
{step === 1 ? (
<div className="grid gap-4">
<div className="flex items-center gap-2 font-medium"><Server size={18} /> First Provider</div>
<Field label="Cluster Name"><input className={inputClass} value={form.cluster_name} onChange={(event) => setForm({ ...form, cluster_name: event.target.value })} /></Field>
<Field label="API URL"><input className={inputClass} value={form.cluster_api_url} onChange={(event) => setForm({ ...form, cluster_api_url: event.target.value })} /></Field>
<Field label="API Token"><input className={inputClass} value={form.cluster_api_token} onChange={(event) => setForm({ ...form, cluster_api_token: event.target.value })} placeholder="PVEAPIToken=..." /></Field>
<div className="grid gap-3 md:grid-cols-2">
<Field label="Provider"><select className={selectClass} value={form.cluster_provider} onChange={(event) => setForm({ ...form, cluster_provider: event.target.value })}><option>proxmox</option><option>demo</option></select></Field>
<Field label="Mode"><select className={selectClass} value={form.cluster_mode} onChange={(event) => setForm({ ...form, cluster_mode: event.target.value })}><option value="read_only">read_only</option><option value="write_enabled">write_enabled</option></select></Field>
</div>
</div>
) : null}
{step === 2 ? (
<div className="grid gap-4">
<div className="flex items-center gap-2 font-medium"><CheckCircle2 size={18} /> Ready</div>
<div className="rounded-md border border-border bg-canvas p-4 text-sm">
NexaFabric will create the administrator, store the provider in read-only mode by default, and open the login screen.
</div>
</div>
) : null}
{error ? <div className="mt-4 rounded-md border border-danger p-3 text-sm text-danger">{error}</div> : null}
<div className="mt-6 flex justify-between">
<button className={secondaryButtonClass} type="button" onClick={() => setStep(Math.max(0, step - 1))} disabled={step === 0}>Back</button>
{step < 2 ? <button className={buttonClass} type="button" onClick={() => setStep(step + 1)}>Next</button> : <button className={buttonClass}>Complete Setup</button>}
</div>
</form>
</div>
</div>
);
}