"use client"; import { FormEvent, useEffect, useState } from "react"; import { Activity, SlidersHorizontal } from "lucide-react"; import { AlgaLogo } from "../AlgaLogo"; import { LogoutButton } from "../auth/LogoutButton"; import styles from "../status.module.css"; // The manual edition selector + airgap license-key entry are hidden for now: the // install code is the only supported entitlement path (operators re-issue a fresh // code rather than choosing an edition or pasting a signed license by hand). Kept // in source (guarded, not deleted) so the airgap/manual flow is trivial to bring // back — flip this to true. const SHOW_MANUAL_EDITION_AND_LICENSE = false; type SetupConfig = { mode?: string; defaults?: { channel?: string; appHostname?: string; dnsMode?: string; dnsServers?: string; releaseRef?: string; }; network?: { addresses?: string[]; resolvers?: string[]; }; }; type FieldErrors = Partial< Record< | "installCode" | "tenantName" | "adminFirstName" | "adminLastName" | "adminEmail" | "adminPassword" | "adminPasswordConfirm" | "appHostname" | "dnsServers" | "releaseRef" | "licenseKey", string > >; function isValidIpv4(value: string) { const parts = value.split("."); return ( parts.length === 4 && parts.every( (part) => /^\d+$/.test(part) && Number(part) >= 0 && Number(part) <= 255, ) ); } function passwordValidationError(value: string) { if (value.length < 8) return "Use at least 8 characters."; if (!/[a-z]/.test(value)) return "Include a lowercase letter."; if (!/[A-Z]/.test(value)) return "Include an uppercase letter."; if (!/\d/.test(value)) return "Include a number."; if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) return "Include a special character."; return null; } function isWellFormedJwt(value: string) { return /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/.test(value); } function validateSetupForm(payload: { installCode?: string; tenantName: string; adminFirstName: string; adminLastName: string; adminEmail: string; adminPassword: string; adminPasswordConfirm: string; appHostname: string; dnsMode: string; dnsServers: string; releaseRef: string; licenseKey?: string; }) { const errors: FieldErrors = {}; // The install code is the only supported entitlement path (manual edition / // airgap license entry is hidden), so it is required. if (!payload.installCode?.trim()) errors.installCode = "Enter the install code from your registration email."; if (!payload.tenantName.trim()) errors.tenantName = "Enter the company name for the initial tenant."; if (!payload.adminFirstName.trim()) errors.adminFirstName = "Enter the admin first name."; if (!payload.adminLastName.trim()) errors.adminLastName = "Enter the admin last name."; if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(payload.adminEmail.trim())) errors.adminEmail = "Enter a valid admin email address."; const passwordError = passwordValidationError(payload.adminPassword); if (passwordError) errors.adminPassword = passwordError; if (payload.adminPassword !== payload.adminPasswordConfirm) errors.adminPasswordConfirm = "Passwords do not match."; if (!payload.appHostname.trim()) { errors.appHostname = "Enter the full URL users will open after setup."; } else { try { const parsed = new URL(payload.appHostname.trim()); if (!["http:", "https:"].includes(parsed.protocol)) { errors.appHostname = "Use an http or https URL."; } } catch { errors.appHostname = "Use a full URL, for example http://192.168.1.50:3000."; } } if (payload.dnsMode === "custom") { const servers = payload.dnsServers .split(",") .map((value) => value.trim()) .filter(Boolean); if (servers.length === 0) { errors.dnsServers = "Enter at least one DNS server for custom DNS."; } else { const invalid = servers.filter((server) => !isValidIpv4(server)); if (invalid.length) errors.dnsServers = `Check these IPv4 addresses: ${invalid.join(", ")}.`; } } if (payload.releaseRef && !/^[A-Za-z0-9._:@/-]+$/.test(payload.releaseRef)) { errors.releaseRef = "Use a release version (e.g. 1.0.3) or a digest (sha256:...)."; } if (payload.licenseKey && !isWellFormedJwt(payload.licenseKey.trim())) { errors.licenseKey = "License key format is invalid. Paste the full key as provided."; } return errors; } function SkeletonCard() { return (
); } export default function SetupPage() { const [config, setConfig] = useState(null); const [error, setError] = useState(null); const [fieldErrors, setFieldErrors] = useState({}); const [busy, setBusy] = useState(false); const [channel, setChannel] = useState("stable"); const [tenantName, setTenantName] = useState(""); const [adminFirstName, setAdminFirstName] = useState(""); const [adminLastName, setAdminLastName] = useState(""); const [adminEmail, setAdminEmail] = useState(""); const [adminPassword, setAdminPassword] = useState(""); const [adminPasswordConfirm, setAdminPasswordConfirm] = useState(""); const [appHostname, setAppHostname] = useState(""); const [dnsMode, setDnsMode] = useState("system"); const [dnsServers, setDnsServers] = useState(""); const [releaseRef, setReleaseRef] = useState(""); const [editionChoice, setEditionChoice] = useState<"ee" | "ce">("ee"); const [licenseKey, setLicenseKey] = useState(""); const [installCode, setInstallCode] = useState(""); useEffect(() => { let cancelled = false; async function loadConfig() { try { const response = await fetch("/api/setup/config", { cache: "no-store", }); if (response.status === 401) { window.location.reload(); return; } if (!response.ok) throw new Error("Unable to load setup defaults."); const data = (await response.json()) as SetupConfig; if (cancelled) return; setConfig(data); setChannel(data.defaults?.channel || "stable"); setAppHostname(data.defaults?.appHostname || ""); setDnsMode(data.defaults?.dnsMode || "system"); setDnsServers(data.defaults?.dnsServers || ""); setReleaseRef(data.defaults?.releaseRef || ""); setError(null); } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : String(err)); } } loadConfig(); return () => { cancelled = true; }; }, []); async function submit(event: FormEvent) { event.preventDefault(); const formData = new FormData(event.currentTarget); const payload = { channel: String(formData.get("channel") || channel), tenantName: String(formData.get("tenantName") || ""), adminFirstName: String(formData.get("adminFirstName") || ""), adminLastName: String(formData.get("adminLastName") || ""), adminEmail: String(formData.get("adminEmail") || ""), adminPassword: String(formData.get("adminPassword") || ""), adminPasswordConfirm: String(formData.get("adminPasswordConfirm") || ""), appHostname: String(formData.get("appHostname") || ""), dnsMode: String(formData.get("dnsMode") || dnsMode), dnsServers: String(formData.get("dnsServers") || ""), releaseRef: String(formData.get("releaseRef") || releaseRef), editionChoice, licenseKey: licenseKey.trim() || undefined, installCode: installCode.trim() || undefined, }; const validation = validateSetupForm(payload); setFieldErrors(validation); if (Object.keys(validation).length > 0) return; setBusy(true); setError(null); try { const response = await fetch("/api/setup", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload), }); if (response.status === 401) { window.location.reload(); return; } const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || "Unable to save setup."); window.location.href = "/"; } catch (err) { setError(err instanceof Error ? err.message : String(err)); setBusy(false); } } function clearFieldError(field: keyof FieldErrors) { setFieldErrors((current) => { const next = { ...current }; delete next[field]; return next; }); } const resolvers = config?.network?.resolvers || []; const addresses = config?.network?.addresses || []; const disabled = busy || !config; return (
Guided setup

Configure your appliance

Start with the install code from your registration email. The appliance details appear after that, with support-only release settings tucked under Advanced.

{busy ? "Starting setup" : "Setup"}
{!config && !error ? : null}

Network detected from Ubuntu

{!config && !error ? ( <>
) : (
Node addresses
{addresses.length ? addresses.join(", ") : "No non-loopback address detected"}
System resolvers
{resolvers.length ? resolvers.join(", ") : "No resolver detected"}
)}

Setup details

Step 1

Enter your install code

Find this code in the registration email from Nine Minds. It binds this appliance to your tenant and applies the correct edition automatically.

{ setInstallCode(event.target.value.toUpperCase()); clearFieldError("installCode"); }} placeholder="K7QPM2RX" disabled={disabled} required autoComplete="off" aria-invalid={Boolean(fieldErrors.installCode)} aria-describedby="setup-install-code-help setup-install-code-error" style={{ fontFamily: "monospace", letterSpacing: "0.1em" }} /> Lost or already-used code? Re-issue a fresh one from the portal, then paste the new code here. {fieldErrors.installCode ? ( {fieldErrors.installCode} ) : null}
{error ? (
{error}
) : null} {installCode.trim() ? ( <>
Step 2

Configure the first tenant and network

Create the first admin account, confirm the URL users will open, and keep default networking unless support tells you otherwise.

{/* Hidden for now (SHOW_MANUAL_EDITION_AND_LICENSE) — manual edition selection + airgap license-key entry. The install code is the only supported path; kept guarded (not deleted) for easy revival. */} {SHOW_MANUAL_EDITION_AND_LICENSE && ( <>
{editionChoice === "ee" && (