Files
Numbus/configurator/index.html
T
Raphaël Numbus 068bf78cfb HTML updates
2026-03-29 14:27:53 +02:00

1236 lines
98 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Numbus Configurator</title>
<!-- Tailwind CSS for modern styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Alpine.js for lightweight reactivity -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- JS-YAML to convert JS objects to YAML strings -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
<!-- Material Design Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
</head>
<body class="bg-[#0f172a] text-slate-100 min-h-screen font-sans selection:bg-fuchsia-500/30">
<div x-data="configurator()" x-init="init()" class="min-h-screen flex flex-col md:flex-row relative" @keydown.window.escape="showAdvanced = false">
<!-- Sidebar Navigation -->
<nav x-show="step > 0" class="w-full md:w-64 bg-[#1e293b] border-r border-slate-700 p-6 flex-shrink-0" x-cloak>
<div class="flex items-center gap-3 mb-10">
<i class="mdi mdi-cloud-check text-sky-400 text-2xl"></i>
<span class="font-bold text-xl tracking-tight bg-gradient-to-r from-sky-400 to-fuchsia-500 bg-clip-text text-transparent uppercase">NUMBUS</span>
</div>
<nav class="space-y-8">
<template x-for="section in getNavigation()" :key="section.title">
<div class="space-y-3">
<h3 :class="isSectionActive(section) ? 'text-white' : 'text-slate-500'"
class="text-xs font-black uppercase tracking-widest border-b border-slate-800 pb-2 transition-colors"
x-text="section.title"></h3>
<ul x-show="isSectionActive(section)" x-collapse
class="space-y-0 pl-1 border-l-2 border-slate-800 ml-1 transition-all duration-500">
<template x-for="minor in section.minors" :key="minor.step">
<li class="relative flex items-center gap-3 py-2 -ml-[7px] group">
<!-- Status Dot -->
<div :class="{
'bg-fuchsia-500 shadow-[0_0_10px_rgba(192,38,211,0.8)] scale-125': step === minor.step,
'bg-green-500': step > minor.step,
'bg-slate-700 group-hover:bg-slate-500': step < minor.step
}"
class="w-3 h-3 rounded-full shrink-0 transition-all duration-300 z-10"></div>
<!-- Label -->
<span :class="step === minor.step ? 'text-white font-bold' : 'text-slate-500'"
class="text-[11px] leading-tight transition-colors" x-text="minor.label"></span>
</li>
</template>
</ul>
</div>
</template>
</nav>
</nav>
<!-- Main Content Area -->
<main class="flex-grow flex items-center justify-center p-4 md:p-12 relative overflow-hidden">
<!-- Welcome Screen -->
<div x-show="step === 0" x-transition:enter="transition ease-out duration-500" class="max-w-2xl text-center space-y-8 z-10">
<img src="logo.png" alt="Numbus Logo" class="w-48 h-48 mx-auto drop-shadow-2xl animate-pulse-slow">
<h1 class="text-6xl font-extrabold tracking-tight">Welcome to <span class="text-sky-400 text-shadow-glow">Numbus</span></h1>
<p class="text-2xl text-slate-400 leading-relaxed">Let's transform your hardware into a powerful, private appliance. We'll guide you through discovery and configuration.</p>
<div class="bg-amber-500/10 border border-amber-500/20 p-4 rounded-xl flex gap-4 text-left items-center">
<span class="bg-amber-500 rounded-full p-1 px-1.5 shrink-0">
<i class="mdi mdi-shield-lock text-slate-900 text-sm"></i>
</span>
<p class="text-sm text-amber-200/80 italic"><strong>Privacy First:</strong> No data entered here ever leaves your device. This configurator runs entirely locally in your browser and is fully private.</p>
</div>
<button @click="step = 1" class="px-10 py-4 bg-fuchsia-600 hover:bg-fuchsia-500 rounded-full text-xl font-bold transition-all transform hover:scale-105 shadow-lg shadow-fuchsia-600/20">
Get Started
</button>
</div>
<!-- Configuration Card -->
<div x-show="step > 0" x-transition:enter="transition ease-out duration-300" class="w-full max-w-5xl bg-[#1e293b] rounded-2xl shadow-2xl border border-slate-700 overflow-hidden flex flex-col min-h-[650px]" x-cloak>
<div class="p-8 md:p-10 flex-grow overflow-y-auto custom-scrollbar">
<!-- Step 1: Deployment Mode -->
<div x-show="step === 1" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Deployment Mode</h2>
<p class="text-slate-400 mt-2 text-lg">How would you like to set up your device?</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button @click="formData.deploymentMode = 'interactive'"
:class="formData.deploymentMode === 'interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
<div class="font-bold text-lg mb-1" :class="formData.deploymentMode === 'interactive' ? 'text-white' : 'text-slate-200 group-hover:text-white'">Interactive</div>
<p class="text-xs transition-colors" :class="formData.deploymentMode === 'interactive' ? 'text-white' : 'text-slate-500 group-hover:text-white'">Guide me through every setting manually.</p>
</button>
<button @click="formData.deploymentMode = 'non-interactive'"
:class="formData.deploymentMode === 'non-interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
<div class="font-bold text-lg mb-1" :class="formData.deploymentMode === 'non-interactive' ? 'text-white' : 'text-slate-200 group-hover:text-white'">Non-Interactive</div>
<p class="text-xs transition-colors" :class="formData.deploymentMode === 'non-interactive' ? 'text-white' : 'text-slate-500 group-hover:text-white'">Reuse an existing configuration from a Git repo.</p>
</button>
</div>
</div>
<!-- Step 2: Device Personality -->
<div x-show="step === 2" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Device Type</h2>
<p class="text-slate-400 mt-2 text-lg">Select the personality for your new Numbus machine.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<template x-for="device in deviceTypes" :key="device.id">
<button @click="formData.deviceType = device.id"
:class="formData.deviceType === device.id ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
<div class="flex items-center gap-4 mb-2">
<div class="w-12 h-12 bg-slate-800 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
<i :class="'mdi ' + device.icon" class="text-sky-400"></i>
</div>
<div>
<div class="font-bold text-lg capitalize" :class="formData.deviceType === device.id ? 'text-white' : 'text-slate-200'" x-text="device.name"></div>
<p class="text-xs transition-colors" :class="formData.deviceType === device.id ? 'text-white' : 'text-slate-500 group-hover:text-white'" x-text="device.desc"></p>
</div>
</div>
</button>
</template>
</div>
</div>
<!-- Step 3: Live Setup & BIOS Guide -->
<div x-show="step === 3" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Live Setup</h2>
<p class="text-slate-400 mt-2 text-lg">Follow these steps to prepare your hardware for discovery.</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10">
<div class="space-y-6">
<div class="flex gap-4">
<div class="w-8 h-8 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold shrink-0">1</div>
<p class="text-sm text-slate-300">Create a <strong>NixOS Boot ISO</strong> and flash it to a USB drive.</p>
</div>
<div class="flex gap-4">
<div class="w-8 h-8 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold shrink-0">2</div>
<div class="space-y-3">
<p class="text-sm text-slate-300">In BIOS, enable <strong>UEFI, VT-x/SVM, VT-d/IOMMU, TPM 2.0</strong> and <strong>Disable Secure Boot</strong>.</p>
</div>
</div>
<div class="flex gap-4">
<div class="w-8 h-8 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold shrink-0">3</div>
<p class="text-sm text-slate-300">Boot the device. Type <code class="bg-slate-800 px-1 rounded text-fuchsia-400">ip a</code> to get the IP, then <code class="bg-slate-800 px-1 rounded text-fuchsia-400">passwd</code> to set a temporary password.</p>
</div>
</div>
<div class="bg-slate-900/50 p-6 rounded-2xl border border-slate-700/50 space-y-6">
<div class="space-y-2">
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Live Target IP Address</label>
<input type="text" x-model="formData.network.live_target_ip" placeholder="192.168.1.100" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="space-y-2">
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Temporary Password</label>
<input type="password" x-model="formData.network.live_target_password" placeholder="••••••••" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<button @click="startDiscovery()" :disabled="!validators.ip(formData.network.live_target_ip) || !formData.network.live_target_password" class="w-full py-4 bg-sky-600 hover:bg-sky-500 disabled:opacity-30 rounded-xl font-bold transition-all shadow-lg shadow-sky-600/20">Initialize Discovery</button>
</div>
</div>
</div>
<!-- Step 4: Discovery Waiting Room -->
<div x-show="step === 4" class="flex flex-col items-center justify-center py-20 text-center space-y-8">
<div class="relative w-32 h-32">
<div class="absolute inset-0 rounded-full border-4 border-slate-800 border-t-fuchsia-500 animate-spin"></div>
<div class="absolute inset-4 rounded-full border-4 border-slate-800 border-b-sky-500 animate-spin-slow"></div>
</div>
<h2 class="text-4xl font-bold text-white">Hardware Discovery in Progress</h2>
<p class="text-slate-400 max-w-md mx-auto text-lg">We're remotely probing your machine for disks and interfaces. This usually takes 1-2 minutes.</p>
</div>
<!-- Step 5: Language & Region -->
<div x-show="step === 5" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Language</h2>
<p class="text-slate-400 mt-2 text-lg">Set your regional preferences to ensure correct time and language display.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">System Language <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">Choose the language for your server's system and primary interfaces.</p>
<select x-model="formData.language.lang" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none transition-all appearance-none cursor-pointer">
<option value="EN">English</option>
<option value="FR">French</option>
<option value="DE">German</option>
<option value="ES">Spanish</option>
<option value="IT">Italian</option>
</select>
</div>
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">Country (Locale) <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">Defines regional formats for dates, currencies, and numbers (e.g., en_US for USA).</p>
<select x-model="formData.language.locale" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none appearance-none cursor-pointer">
<option value="en_US">United States</option>
<option value="en_GB">United Kingdom</option>
<option value="fr_FR">France</option>
<option value="de_DE">Germany</option>
<option value="it_IT">Italy</option>
</select>
</div>
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">Timezone <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">Used to synchronize server logs, backups, and scheduled updates with your local time.</p>
<select x-model="formData.language.timezone" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none appearance-none cursor-pointer">
<option value="UTC">UTC</option>
<option value="Europe/Paris">Europe/Paris</option>
<option value="Europe/London">Europe/London</option>
<option value="Europe/Berlin">Europe/Berlin</option>
<option value="America/New_York">America/New_York</option>
<option value="America/Los_Angeles">America/Los_Angeles</option>
</select>
</div>
</div>
</div>
<!-- Step 10: Users -->
<div x-show="step === 10" class="space-y-8" x-data="{ groupHelp: false }">
<div class="border-b border-slate-700 pb-4 flex justify-between items-end">
<div>
<h2 class="text-4xl font-bold text-sky-400">Users</h2>
<p class="text-slate-400 mt-2 text-lg">Manage people and their access permissions.</p>
</div>
<button @click="addUser()"
:disabled="formData.users.length >= 10"
:class="formData.users.length >= 10 ? 'opacity-50 cursor-not-allowed' : ''"
class="bg-sky-600 hover:bg-sky-500 px-4 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2">
<i class="mdi mdi-account-plus"></i> Add User (<span x-text="formData.users.length"></span>/10)
</button>
</div>
<div class="space-y-4 pr-2">
<template x-for="(user, index) in formData.users" :key="index">
<div :class="user.isStatic ? 'border-fuchsia-500/50 bg-fuchsia-500/5' : 'border-slate-700 bg-slate-900/50'" class="border p-6 rounded-2xl space-y-4 relative group">
<div class="flex justify-between items-start">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-sky-400 font-bold" x-text="(user.name.charAt(0) || user.username.charAt(0) || '?').toUpperCase()"></div>
<div>
<p class="font-bold text-slate-200 text-lg group-hover:text-white transition-colors" x-text="user.name || user.username || 'New User'"></p>
<p class="text-[10px] text-slate-500 uppercase tracking-widest font-bold group-hover:text-white transition-colors" x-text="user.isStatic ? 'Administrator (Sudo)' : 'Standard User'"></p>
</div>
</div>
<button x-show="!user.isStatic" @click="removeUser(index)" class="text-slate-500 hover:text-red-500 transition-colors">
<i class="mdi mdi-trash-can-outline text-xl"></i>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">Username <span class="text-fuchsia-500" x-show="user.isStatic">*</span></label>
<div class="relative">
<i class="mdi mdi-account absolute left-3 top-1/2 -translate-y-1/2 text-slate-600"></i>
<input type="text" x-model="user.username" :disabled="user.isStatic"
:class="user.isStatic ? 'opacity-50 cursor-not-allowed' : ''"
class="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 pl-10 text-sm outline-none focus:ring-1 focus:ring-fuchsia-500 placeholder:text-slate-500/40">
</div>
</div>
<div class="space-y-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">Display Name <span class="text-fuchsia-500" x-show="user.isStatic">*</span></label>
<div class="relative">
<i class="mdi mdi-card-account-details absolute left-3 top-1/2 -translate-y-1/2 text-slate-600"></i>
<input type="text" x-model="user.name" :placeholder="user.isStatic ? 'Alexandre' : 'Full Name'"
class="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 pl-10 text-sm outline-none focus:ring-1 focus:ring-fuchsia-500 placeholder:text-slate-500/40">
</div>
</div>
<div class="space-y-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">Email Address <span class="text-fuchsia-500" x-show="user.isStatic">*</span></label>
<div class="relative">
<i class="mdi mdi-email absolute left-3 top-1/2 -translate-y-1/2 text-slate-600"></i>
<input type="email" x-model="user.email" placeholder="admin@example.com"
class="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 pl-10 text-sm outline-none focus:ring-1 focus:ring-fuchsia-500 placeholder:text-slate-500/40">
</div>
</div>
</div>
<div class="space-y-3" x-show="!user.isStatic">
<label class="text-sm font-bold text-slate-500 uppercase">Assigned Groups (ACLs)</label>
<div class="flex flex-wrap gap-2">
<template x-for="group in Object.keys(formData.groups)" :key="group" x-init="$nextTick(() => {})">
<label :class="user.groups.includes(group) ? 'bg-sky-500/20 border-sky-500/50 text-sky-400' : 'bg-slate-800 border-slate-700 text-slate-500 opacity-60 hover:opacity-100'"
class="px-4 py-1.5 border rounded-full text-xs font-bold cursor-pointer transition-all flex items-center gap-2">
<input type="checkbox" :value="group" x-model="user.groups" class="hidden">
<span x-text="group.charAt(0).toUpperCase() + group.slice(1).replace('_', ' ')"></span>
</label>
</template>
</div>
</div>
</div>
</template>
</div>
<!-- Advanced Group Customization -->
<div class="pt-6 border-t border-slate-700">
<button @click="showGroupCustomization = !showGroupCustomization" class="flex items-center gap-2 text-slate-400 hover:text-white transition-colors">
<svg :class="showGroupCustomization ? 'rotate-90' : ''" class="w-4 h-4 transition-transform" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
<span class="text-sm font-bold uppercase tracking-wider">Configure Group ACLs (RBAC)</span>
</button>
<div x-show="showGroupCustomization" x-transition class="mt-6 space-y-8 bg-slate-900/30 p-6 rounded-2xl border border-slate-700/50">
<div class="flex gap-2">
<input type="text" x-model="newGroupName" placeholder="Add custom group name (e.g. Guest)..." class="flex-grow bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm outline-none focus:ring-1 focus:ring-fuchsia-500">
<button @click="addGroup()" class="bg-fuchsia-600 hover:bg-fuchsia-500 px-4 py-2 rounded-lg text-sm font-bold">Add</button>
</div>
<template x-for="group in Object.keys(formData.groups).filter(g => g !== 'admin')" :key="group">
<div class="space-y-4">
<h4 class="text-fuchsia-400 font-bold uppercase text-base tracking-widest" x-text="group.replaceAll('_', ' ') + ' access'"></h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
<template x-for="app in webApps" :key="app">
<label :class="formData.groups[group].includes(app) ? 'text-sky-400 bg-sky-500/5 border-sky-500/20' : 'text-slate-600 border border-slate-800 opacity-50'" class="flex items-center gap-3 p-3 border rounded-lg cursor-pointer text-sm font-bold transition-all">
<input type="checkbox" :value="app" x-model="formData.groups[group]" class="w-4 h-4 rounded border-slate-700 text-sky-500 focus:ring-sky-500 bg-slate-800">
<span class="capitalize" x-text="app"></span>
</label>
</template>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Step 11: Alerts (Mail) -->
<div x-show="step === 11" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Mail</h2>
<p class="text-slate-400 mt-2 text-lg">Configure system notifications and service alerts.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">SMTP Username <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The email address used to send alerts (e.g. system@yourdomain.com).</p>
<input type="text" x-model="formData.mail.smtp_username" placeholder="your-address@your-domain.com" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">SMTP Password <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The password or app-token for the sender email account.</p>
<input type="password" x-model="formData.mail.smtp_password" placeholder="emrp raps vzoi vnoe" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">SMTP Host <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The server address of your email provider (e.g. smtp.gmail.com).</p>
<input type="text" x-model="formData.mail.smtp_host" placeholder="smtp.yourdomain.com" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">SMTP Port <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The network port for secure mail transmission. Common: 587 (TLS) or 465 (SSL).</p>
<input type="text" x-model="formData.mail.smtp_port" placeholder="587" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
</div>
</div>
<!-- Step 6: Network -->
<div x-show="step === 6" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Network</h2>
<p class="text-slate-400 mt-2 text-lg">Target host networking and server IP allocation.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">Server Static IP <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The fixed address of your server on your local network. It should be outside your router's DHCP range.</p>
<input type="text" x-model="formData.network.ip_address" placeholder="192.168.1.5" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="space-y-2" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">Router IP <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The address of the device providing internet (the gateway). Usually 192.168.1.1.</p>
<input type="text" x-model="formData.network.router_ip" placeholder="192.168.1.1" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
</div>
</div>
<!-- Step 7: Remote Access -->
<div x-show="step === 7" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Remote Access</h2>
<p class="text-slate-400 mt-2 text-lg">Choose how you will access your server from outside your home.</p>
</div>
<div class="space-y-6">
<div class="grid grid-cols-1 gap-4">
<!-- Netbird Cloud - Only for <= 5 users -->
<button x-show="formData.users.length <= 5"
@click="formData.remote_access.provider = 'netbird-cloud'"
:class="formData.remote_access.provider === 'netbird-cloud' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-4">
<img src="https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/netbird.svg" class="w-8 h-8" alt="NetBird">
<div class="font-bold text-xl" :class="formData.remote_access.provider === 'netbird-cloud' ? 'text-white' : 'text-slate-200 group-hover:text-white'">NetBird Managed</div>
</div>
<span class="text-[10px] bg-green-500/20 text-green-400 px-2 py-1 rounded uppercase font-bold">Recommended for small teams</span>
</div>
<p class="text-sm leading-relaxed transition-colors" :class="formData.remote_access.provider === 'netbird-cloud' ? 'text-white' : 'text-slate-400 group-hover:text-white'">The easiest zero-trust solution. Perfect for families and teams of up to 5 people.</p>
</button>
<!-- Netbird Cloud Token Input -->
<div x-show="formData.remote_access.provider === 'netbird-cloud'" x-transition class="mt-2 space-y-4 bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50">
<div class="flex items-center gap-2">
<label class="text-base font-semibold text-slate-300">Service User Token <span class="text-fuchsia-500">*</span></label>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline text-lg"></i>
</button>
<div x-show="open" @click.away="open = false" class="absolute left-0 md:left-full ml-0 md:ml-4 top-full md:top-0 mt-2 md:mt-0 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
<p class="font-bold text-sky-400 mb-2">How to get your token:</p>
<ol class="list-decimal list-inside space-y-2 text-slate-300">
<li>Create an account at <a href="https://app.netbird.io" target="_blank" class="text-fuchsia-400 underline font-bold">NetBird</a></li>
<li>Verify your email via the link sent to you</li>
<li>Navigate to <span class="font-mono bg-slate-900 px-1 rounded">Teams > Service Users</span></li>
<li>Click <span class="font-bold text-white">"Create Service User"</span>, name it "numbus-server" and set role to <span class="text-white">"Network Admin"</span></li>
<li>Click on the new user, then <span class="text-white">"Add Token"</span>. Name it "deployment" and set expiry to <span class="text-white">7 days</span></li>
<li>Copy the token and paste it here!</li>
</ol>
</div>
</div>
</div>
<input type="password" x-model="formData.remote_access.netbird_token" placeholder="nbp_..." class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40 text-base">
</div>
<!-- Netbird Self-Hosted -->
<button @click="formData.remote_access.provider = 'netbird-selfhosted'"
:class="formData.remote_access.provider === 'netbird-selfhosted' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
<div class="flex items-center gap-3 mb-2">
<i class="mdi mdi-server-network text-3xl" :class="formData.remote_access.provider === 'netbird-selfhosted' ? 'text-white' : 'text-sky-400 group-hover:text-white'"></i>
<div class="font-bold text-xl" :class="formData.remote_access.provider === 'netbird-selfhosted' ? 'text-white' : 'text-slate-200 group-hover:text-white'">Managed On-Premise</div>
</div>
<p class="text-sm leading-relaxed transition-colors" :class="formData.remote_access.provider === 'netbird-selfhosted' ? 'text-white' : 'text-slate-400 group-hover:text-white'">Complete digital sovereignty. You host the management platform on your own Numbus server. Required for teams larger than 5 users.</p>
</button>
<!-- External -->
<button @click="formData.remote_access.provider = 'external'"
:class="formData.remote_access.provider === 'external' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
<div class="flex items-center gap-3 mb-2">
<i class="mdi mdi-earth text-3xl" :class="formData.remote_access.provider === 'external' ? 'text-white' : 'text-sky-400 group-hover:text-white'"></i>
<div class="font-bold text-xl" :class="formData.remote_access.provider === 'external' ? 'text-white' : 'text-slate-200 group-hover:text-white'">External / Manual Access</div>
</div>
<p class="text-sm leading-relaxed transition-colors" :class="formData.remote_access.provider === 'external' ? 'text-white' : 'text-slate-400 group-hover:text-white'">Use your own VPN or direct port-forwarding. <strong>Warning:</strong> This server won't be compatible with Numbus integrated offsite backups.</p>
</button>
</div>
</div>
</div>
<!-- Step 9: Security -->
<div x-show="step === 9" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Security</h2>
<p class="text-slate-400 mt-2 text-lg">Manage infrastructure access and advanced protection.</p>
</div>
<div class="space-y-6">
<!-- SSO Toggle -->
<div class="bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50 space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<label class="text-lg font-bold text-slate-200 transition-colors">Use Single Sign-On (SSO)</label>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline text-lg"></i>
</button>
<div x-show="open" @click.away="open = false" class="absolute left-full ml-4 top-0 w-64 p-4 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
One login for everything. Centralized identity management via LLDAP and Authelia. Provides enterprise-grade security and 2FA for all your apps.
</div>
</div>
</div>
<button @click="formData.security.use_sso = !formData.security.use_sso"
:class="formData.security.use_sso ? 'bg-fuchsia-600 shadow-[0_0_10px_rgba(192,38,211,0.4)]' : 'bg-slate-700'"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none">
<span :class="formData.security.use_sso ? 'translate-x-5' : 'translate-x-0'" class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</div>
<!-- Public Sharing Toggle -->
<div class="bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50 space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<label class="text-lg font-bold text-slate-200">Enable Secure Public Sharing</label>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline text-lg"></i>
</button>
<div x-show="open" @click.away="open = false" class="absolute left-full ml-4 top-0 w-64 p-4 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
Allows you to share links (like Nextcloud folders) with friends who don't have NetBird. Only specific URLs are exposed; everything else remains locked behind the VPN.
</div>
</div>
</div>
<button @click="formData.security.public_sharing = !formData.security.public_sharing"
:class="formData.security.public_sharing ? 'bg-fuchsia-600 shadow-[0_0_10px_rgba(192,38,211,0.4)]' : 'bg-slate-700'"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none">
<span :class="formData.security.public_sharing ? 'translate-x-5' : 'translate-x-0'" class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</div>
</div>
<div class="space-y-4" x-data="{ help: false }">
<div class="flex items-center gap-2">
<label class="text-lg font-bold text-slate-200">Cloudflare DNS API Token <span class="text-fuchsia-500">*</span></label>
<button @click="help = !help" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline text-lg"></i>
</button>
</div>
<p x-show="help" class="text-xs text-sky-400 italic mb-2">A security token from Cloudflare that allows Numbus to verify you own the domain and secure it via SSL.</p>
<input type="password" x-model="formData.ssl.cloudflare_token" placeholder="••••••••••••••••" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="space-y-4" x-data="{ open: false }">
<div class="flex items-center gap-2">
<label class="text-lg font-bold text-slate-200">Authorized SSH Public Keys <span class="text-fuchsia-500">*</span></label>
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
<i class="mdi mdi-information-outline text-lg"></i>
</button>
</div>
<p x-show="open" class="text-xs text-sky-400 italic mb-2">A secure way to log in without a password. Like a physical key, but digital.</p>
<p class="text-sm text-slate-500 mb-2 italic">Paste here the public keys that are allowed to connect to your server's admin account (one per line).</p>
<textarea x-model="formData.security.ssh_keys" rows="5" placeholder="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..." class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none transition-all font-mono text-base placeholder:text-slate-500/40"></textarea>
</div>
<!-- Security Warning -->
<div class="bg-amber-500/10 border border-amber-500/20 p-6 rounded-2xl flex gap-6 text-left items-start mt-4">
<div class="bg-amber-500 rounded-full p-2 mt-1 shrink-0">
<i class="mdi mdi-alert text-slate-900 text-xl"></i>
</div>
<p class="text-base text-amber-200/90 leading-relaxed font-medium"><strong>Security Warning:</strong> Anyone who possesses one of these public keys AND their corresponding private key will have full administrator access to your server. Protect your private keys as if they were physical keys to your home.</p>
</div>
</div>
<!-- Step 8: Services selection -->
<div x-show="step === 8" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Services</h2>
<p class="text-slate-400 mt-2 text-lg">Choose the applications to deploy.</p>
</div>
<div class="space-y-10 pr-4">
<div class="space-y-2">
<div class="flex items-center gap-2">
<label class="text-lg font-bold text-slate-200">Domain Name (FQDN) <span class="text-fuchsia-500">*</span></label>
<button @click="openHelp('domain')" class="text-slate-500 hover:text-sky-400">
<i class="mdi mdi-information-outline"></i>
</button>
</div>
<input type="text" x-model="formData.ssl.domain" placeholder="example.com" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<h3 class="text-lg font-semibold text-fuchsia-400">DNS Filter</h3>
<div class="flex gap-4">
<template x-for="dns in ['pi-hole', 'adguard']" :key="dns">
<button @click="formData.services.dns = dns" :class="formData.services.dns === dns ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex-1 p-5 border rounded-xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-3">
<img :src="'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/' + (serviceMetadata[dns].icon || dns) + '.svg'" class="w-6 h-6" :alt="dns">
<div class="font-bold capitalize text-lg" :class="formData.services.dns === dns ? 'text-white' : 'text-slate-200 group-hover:text-white'" x-text="dns"></div>
</div>
<a :href="serviceMetadata[dns].link" target="_blank" @click.stop class="text-[10px] text-sky-400 underline opacity-40 group-hover:opacity-100 transition-opacity">Website</a>
</div>
<p class="text-sm leading-relaxed transition-colors" :class="formData.services.dns === dns ? 'text-white' : 'text-slate-400 group-hover:text-white'" x-text="serviceMetadata[dns].desc"></p>
</button>
</template>
</div>
<h3 class="text-lg font-semibold text-fuchsia-400 mt-6">Web Applications</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<template x-for="app in webApps" :key="app">
<label :class="formData.services.apps.includes(app) ? 'border-fuchsia-500 bg-fuchsia-600/20' : 'border-slate-700 bg-slate-900/40'" class="flex flex-col gap-3 p-5 border rounded-xl cursor-pointer transition-all hover:bg-fuchsia-600/20 hover:border-fuchsia-500 group relative">
<img :src="'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/' + (serviceMetadata[app].icon || app) + '.svg'" :class="formData.services.apps.includes(app) ? 'opacity-100' : 'opacity-20 group-hover:opacity-100'" class="w-8 h-8 transition-opacity absolute right-4 top-4" :alt="app">
<div class="flex items-center gap-3">
<input type="checkbox" :value="app" x-model="formData.services.apps" class="w-5 h-5 rounded border-slate-700 text-sky-500 focus:ring-sky-500 bg-slate-800">
<span class="text-base font-bold capitalize" x-text="app"></span>
</div>
<p class="text-sm leading-normal transition-colors" :class="formData.services.apps.includes(app) ? 'text-white' : 'text-slate-500 group-hover:text-white'" x-text="serviceMetadata[app].desc"></p>
<a :href="serviceMetadata[app].link" target="_blank" class="text-[10px] text-sky-400 underline opacity-0 group-hover:opacity-100 transition-opacity mt-auto">Learn more</a>
</label>
</template>
</div>
<h3 class="text-lg font-semibold text-fuchsia-400 mt-6">System Services</h3>
<div class="grid grid-cols-2 gap-4">
<template x-for="sys in ['clamav', 'virtualization']" :key="sys">
<label :class="formData.services.system.includes(sys) ? 'border-fuchsia-500 bg-fuchsia-600/20' : 'border-slate-700 bg-slate-900/40'" class="flex flex-col gap-3 p-5 border rounded-xl cursor-pointer transition-all hover:bg-fuchsia-600/20 hover:border-fuchsia-500 group">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" :value="sys" x-model="formData.services.system" class="w-5 h-5 rounded border-slate-700 text-sky-500 focus:ring-sky-500 bg-slate-800">
<span class="text-base font-bold capitalize" x-text="sys"></span>
</div>
<img :src="'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/' + (serviceMetadata[sys].icon || sys) + '.svg'" :class="formData.services.system.includes(sys) ? 'opacity-100' : 'opacity-40 group-hover:opacity-100'" class="w-6 h-6 transition-opacity" :alt="sys">
</div>
<p class="text-sm transition-colors" :class="formData.services.system.includes(sys) ? 'text-white' : 'text-slate-500 group-hover:text-white'" x-text="serviceMetadata[sys].desc"></p>
<a :href="serviceMetadata[sys].link" target="_blank" class="text-[10px] text-sky-400 underline opacity-0 group-hover:opacity-100 transition-opacity mt-auto">Learn more</a>
</label>
</template>
</div>
</div>
<!-- Conditional Crafty Settings -->
<div x-show="formData.services.apps.includes('crafty')" x-transition class="pt-8 border-t border-slate-800 space-y-10">
<h3 class="text-xl font-bold text-fuchsia-400 flex items-center gap-3">
<img src="https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/crafty-controller.svg" class="w-6 h-6" alt="">
Crafty Control Settings
</h3>
<div class="space-y-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-2">
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Minecraft Java Servers <span class="text-fuchsia-500">*</span></label>
<input type="number" min="0" x-model="formData.services.apps_extra_opts.crafty.crafty_java" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 outline-none focus:ring-2 focus:ring-sky-500">
</div>
<div class="space-y-2">
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Minecraft Bedrock Servers <span class="text-fuchsia-500">*</span></label>
<input type="number" min="0" x-model="formData.services.apps_extra_opts.crafty.crafty_bedrock" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 outline-none focus:ring-2 focus:ring-sky-500">
</div>
</div>
<div class="flex items-center justify-between bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50">
<label class="text-base font-bold text-slate-300">Enable Dynmap</label>
<button @click="formData.services.apps_extra_opts.crafty.crafty_dynmap = !formData.services.apps_extra_opts.crafty.crafty_dynmap"
:class="formData.services.apps_extra_opts.crafty.crafty_dynmap ? 'bg-fuchsia-600 shadow-[0_0_10px_rgba(192,38,211,0.4)]' : 'bg-slate-700'"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none">
<span :class="formData.services.apps_extra_opts.crafty.crafty_dynmap ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out">
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Step 12: Non-Interactive Git Config -->
<div x-show="step === 12" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Configuration Source</h2>
<p class="text-slate-400 mt-2 text-lg">Link your Git repository containing the numbus.yaml file.</p>
</div>
<div class="space-y-4">
<div class="space-y-2">
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Git Repository URL</label>
<input type="text" x-model="formData.gitConfig.url" placeholder="https://github.com/user/my-numbus-config" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Username (Optional)</label>
<input type="text" x-model="formData.gitConfig.user" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
</div>
<div class="space-y-2">
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Password/Token (Optional)</label>
<input type="password" x-model="formData.gitConfig.password" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
</div>
</div>
</div>
</div>
<!-- Step 13: Setup Type & Restore -->
<div x-show="step === 13" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Setup Type</h2>
<p class="text-slate-400 mt-2 text-lg">Are we creating a new server instance or restoring an existing one?</p>
</div>
<div class="space-y-6">
<select x-model="formData.restoreConfig.setup_type" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 outline-none appearance-none cursor-pointer">
<option value="new">New Server (Inherit settings, new identity)</option>
<option value="replica">Exact Replica (Full system recovery)</option>
</select>
<div class="bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50 flex items-center justify-between">
<div>
<label class="text-lg font-bold text-white">Restore Data</label>
<p class="text-xs text-slate-500">Pull data from an existing numbus-backup-server.</p>
</div>
<button @click="formData.restoreConfig.restore_data = !formData.restoreConfig.restore_data"
:class="formData.restoreConfig.restore_data ? 'bg-fuchsia-600' : 'bg-slate-700'"
class="relative inline-flex h-6 w-11 shrink-0 rounded-full transition-colors">
<span :class="formData.restoreConfig.restore_data ? 'translate-x-5' : 'translate-x-0'" class="inline-block h-5 w-5 transform rounded-full bg-white transition"></span>
</button>
</div>
<div x-show="formData.restoreConfig.restore_data" x-transition class="space-y-4 pl-6 border-l-2 border-fuchsia-500/30">
<input type="text" x-model="formData.restoreConfig.backup_host" placeholder="Backup Host (IP/Domain)" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
<input type="password" x-model="formData.restoreConfig.setup_key" placeholder="Remote Access Setup Key" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
<input type="password" x-model="formData.restoreConfig.backup_key" placeholder="Backup Encryption Key" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
</div>
</div>
</div>
<!-- Step 14: Tweaks -->
<div x-show="step === 14" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Environment Overrides</h2>
<p class="text-slate-400 mt-2 text-lg">Apply specific tweaks to this local deployment.</p>
</div>
<p class="text-sm text-slate-500 italic">Select settings you wish to override from the Git configuration.</p>
<div class="space-y-4">
<!-- Simplified tweak UI for now -->
<div class="bg-slate-900/40 p-4 rounded-xl border border-slate-800">
<p class="text-xs font-mono text-fuchsia-400">tweaks: []</p>
</div>
</div>
</div>
<!-- Step 15: Installation Progress -->
<div x-show="step === 15" class="flex flex-col items-center justify-center py-20 text-center space-y-8">
<div class="w-full max-w-md bg-slate-900 rounded-full h-4 overflow-hidden border border-slate-700">
<div class="bg-fuchsia-600 h-full animate-pulse" style="width: 45%"></div>
</div>
<h2 class="text-4xl font-bold text-white">Deploying Numbus...</h2>
<div class="bg-black/40 rounded-xl p-6 w-full font-mono text-left text-xs text-green-400 border border-slate-800">
<div class="flex justify-between items-center mb-4">
<span class="uppercase tracking-widest text-slate-500">Live Terminal Logs</span>
<button class="text-[10px] bg-slate-800 px-2 py-1 rounded">Hide Logs</button>
</div>
<div class="h-40 overflow-y-auto custom-scrollbar">
<p>> Initializing nixos-anywhere...</p>
<p>> Checking connection to target...</p>
<p>> Copying closure to remote...</p>
</div>
</div>
<!-- Step 12: Git Configuration -->
<div x-show="step === 12" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Configuration Source</h2>
<p class="text-slate-400 mt-2 text-lg">Link your Git repository containing the numbus.yaml file.</p>
</div>
<div class="space-y-4">
<div class="space-y-2">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Git Repository URL</label>
<input type="text" x-model="formData.gitConfig.url" placeholder="https://github.com/user/my-numbus-config" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Username (Optional)</label>
<input type="text" x-model="formData.gitConfig.user" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
</div>
<div class="space-y-2">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Password/Token (Optional)</label>
<input type="password" x-model="formData.gitConfig.password" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
</div>
</div>
</div>
</div>
<!-- Step 13: Setup Type & Restore -->
<div x-show="step === 13" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Setup Type</h2>
<p class="text-slate-400 mt-2 text-lg">Are we creating a new server instance or restoring an existing one?</p>
</div>
<div class="space-y-6">
<select x-model="formData.restoreConfig.setup_type" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 outline-none appearance-none cursor-pointer">
<option value="new">New Server (Inherit settings, new identity)</option>
<option value="replica">Exact Replica (Full system recovery)</option>
</select>
<div class="bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50 flex items-center justify-between">
<div>
<label class="text-lg font-bold text-white">Restore Data</label>
<p class="text-xs text-slate-500">Pull data from an existing numbus-backup-server.</p>
</div>
<button @click="formData.restoreConfig.restore_data = !formData.restoreConfig.restore_data"
:class="formData.restoreConfig.restore_data ? 'bg-fuchsia-600' : 'bg-slate-700'"
class="relative inline-flex h-6 w-11 shrink-0 rounded-full transition-colors">
<span :class="formData.restoreConfig.restore_data ? 'translate-x-5' : 'translate-x-0'" class="inline-block h-5 w-5 transform rounded-full bg-white transition"></span>
</button>
</div>
<div x-show="formData.restoreConfig.restore_data" x-transition class="space-y-4 pl-6 border-l-2 border-fuchsia-500/30">
<input type="text" x-model="formData.restoreConfig.backup_host" placeholder="Backup Host (IP/Domain)" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
<input type="password" x-model="formData.restoreConfig.setup_key" placeholder="Remote Access Setup Key" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
<input type="password" x-model="formData.restoreConfig.backup_key" placeholder="Backup Encryption Key" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none">
</div>
</div>
</div>
<!-- Step 14: Tweaks -->
<div x-show="step === 14" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Environment Overrides</h2>
<p class="text-slate-400 mt-2 text-lg">Apply specific tweaks to this local deployment.</p>
</div>
<div class="space-y-4">
<p class="text-sm text-slate-500 italic">Select settings you wish to override from the Git configuration.</p>
<div class="bg-slate-900/40 p-4 rounded-xl border border-slate-800 font-mono text-xs text-fuchsia-400">
tweaks: []
</div>
</div>
</div>
<!-- Step 15: Installation Progress -->
<div x-show="step === 15" class="flex flex-col items-center justify-center py-20 text-center space-y-8">
<div class="w-full max-w-md bg-slate-900 rounded-full h-4 overflow-hidden border border-slate-700">
<div class="bg-fuchsia-600 h-full animate-pulse" style="width: 35%"></div>
</div>
<h2 class="text-4xl font-bold text-white">Deploying Numbus...</h2>
<div class="bg-black/40 rounded-xl p-6 w-full font-mono text-left text-[10px] text-green-400 border border-slate-800">
<div class="flex justify-between items-center mb-4 border-b border-slate-800 pb-2">
<span class="uppercase tracking-widest text-slate-500">Live Terminal Logs</span>
<button class="text-[9px] bg-slate-800 px-2 py-0.5 rounded text-slate-400">Toggle Logs</button>
</div>
<div class="h-40 overflow-y-auto custom-scrollbar">
<p class="text-fuchsia-400">[INFO] Starting deployment sequence...</p>
<p class="text-sky-400">[CMD] nixos-anywhere --flake .#numbus-server</p>
<p class="text-slate-500">[STDOUT] Initializing remote connection...</p>
</div>
</div>
</div>
</div>
<!-- Final Review (Step 16) -->
<div x-show="step === 16" class="space-y-8 text-center py-10">
<div class="w-20 h-20 bg-green-500/20 text-green-500 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>
</div>
<h2 class="text-4xl font-bold">Ready to Deploy</h2>
<p class="text-slate-400 max-w-md mx-auto text-base">Your configuration has been generated successfully. Download the file and place it in your Numbus project root.</p>
<div class="p-8">
<div class="bg-slate-900 rounded-xl text-left font-mono text-sm text-sky-400 border border-slate-700 overflow-hidden shadow-inner">
<pre class="p-2" x-text="jsyaml.dump(getCleanData())"></pre>
</div>
</div>
</div>
<!-- Footer Navigation -->
<div class="bg-[#1e293b] border-t border-slate-700 p-6 flex justify-between items-center">
<button
@click="step--"
class="px-8 py-3 text-slate-400 hover:text-white font-bold transition-all"
>Back</button>
<button
x-show="step > 0 && step < maxSteps && ![3,4,15].includes(step)"
@click="if(canGoNext()) step++"
:disabled="!canGoNext()"
:class="!canGoNext() ? 'opacity-30 cursor-not-allowed' : ''"
class="px-10 py-3 bg-fuchsia-600 hover:bg-fuchsia-500 rounded-xl font-bold transition-all shadow-lg shadow-fuchsia-600/20"
>Continue</button>
<button
x-show="step === maxSteps"
@click="downloadYaml()"
class="px-10 py-3 bg-green-600 hover:bg-green-500 rounded-xl font-bold transition-all shadow-lg shadow-green-600/20 flex items-center"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
Download numbus.yaml
</button>
</div>
</div>
</main>
</div>
<style>
[x-cloak] { display: none !important; }
@keyframes pulse-slow {
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 20px rgba(14, 165, 233, 0.2)); }
50% { transform: scale(1.05); filter: drop-shadow(0 0 40px rgba(192, 38, 211, 0.4)); }
}
.animate-pulse-slow { animation: pulse-slow 6s infinite ease-in-out; }
@keyframes spin-slow { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
.animate-spin-slow { animation: spin-slow 3s linear infinite; }
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
.text-shadow-glow { text-shadow: 0 0 15px rgba(56, 189, 248, 0.4); }
</style>
<script>
function configurator() {
return {
step: 0,
maxSteps: 15,
init() {
// Watch the step and reset scroll position of the main content area
this.updateNavigation();
this.$watch('step', () => {
const container = this.$el.querySelector('.flex-grow.overflow-y-auto');
if (container) {
container.scrollTop = 0;
}
this.updateNavigation();
});
this.$watch('formData.deploymentMode', () => this.updateNavigation());
},
showAdvanced: false,
showGroupCustomization: false,
navigation: [
{ title: 'Live Device Setup', steps: [1,2,3,4], minors: [
{ label: 'Deployment Mode', step: 1 },
{ label: 'Device Type', step: 2 },
{ label: 'Live Setup', step: 3 },
{ label: 'Discovery', step: 4 }
]},
{ title: 'Server Configuration', steps: [5,6,7,8,9,10,11,12,13,14], minors: [
{ label: 'Configuration', step: 12 }
]},
{ title: 'Installation', steps: [15], minors: [
{ label: 'Progress', step: 15 }
]}
],
getNavigation() {
const isInteractive = this.formData.deploymentMode === 'interactive';
const isServer = this.formData.deviceType === 'server' || this.formData.deviceType === 'backup-server';
let configMinors = [];
if (isInteractive) {
configMinors = [
{ label: 'Language', step: 5 },
{ label: 'Network', step: 6 },
{ label: 'Remote Access', step: 7 },
{ label: 'Services', step: 8 },
{ label: 'Security', step: 9 },
{ label: 'Users & Groups', step: 10 }
];
// Only servers have alerting/mail config usually
if (isServer) {
configMinors.push({ label: 'Alerts', step: 11 });
}
} else {
configMinors = [
{ label: 'Source Repo', step: 12 },
{ label: 'Setup Type', step: 13 },
{ label: 'Environment Tweaks', step: 14 }
];
}
return [
{
title: 'Device Setup',
steps: [1, 2, 3, 4],
minors: [
{ label: 'Deployment Mode', step: 1 },
{ label: 'Device Type', step: 2 },
{ label: 'Targeting', step: 3 },
{ label: 'Probing Hardware', step: 4 }
]
},
{
title: 'Configuration',
steps: isInteractive ? [5, 6, 7, 8, 9, 10, 11] : [12, 13, 14],
minors: configMinors
},
{
title: 'Finalize',
steps: [15, 16],
minors: [{ label: 'Deployment Status', step: 15 }]
}
];
},
isSectionActive(section) {
return section.steps.includes(this.step);
},
deviceTypes: [
{ id: 'server', name: 'Numbus Server', icon: 'mdi-server', desc: 'Cloud, containers & automation' },
{ id: 'backup-server', name: 'Numbus Backup', icon: 'mdi-backup-restore', desc: 'Cold storage & offsite safety' },
{ id: 'computer', name: 'Numbus Computer', icon: 'mdi-laptop', desc: 'Daily driver workstation' },
{ id: 'tv', name: 'Numbus TV', icon: 'mdi-television-guide', desc: 'HTPC & Media center' }
],
webApps: ['crafty', 'frigate', 'gitea', 'home-assistant', 'homepage', 'immich', 'it-tools', 'jellyfin', 'n8n', 'netbootxyz', 'nextcloud', 'ntfy', 'odoo', 'passbolt', 'uptime-kuma', 'vscodium'],
serviceMetadata: {
'pi-hole': { icon: 'pi-hole', desc: 'A network-wide ad blocker via DNS sinkholing.', link: 'https://pi-hole.net' },
'adguard': { icon: 'adguard-home', desc: 'Alternative DNS filter with advanced parental controls.', link: 'https://adguard-dns.io' },
'crafty': { icon: 'crafty-controller', desc: 'Web-based Minecraft server manager and dashboard.', link: 'https://craftycontrol.com/' },
'frigate': { desc: 'NVR with real-time local object detection for IP cameras.', link: 'https://frigate.video/' },
'gitea': { desc: 'Painless self-hosted Git service (like GitHub).', link: 'https://gitea.io/' },
'home-assistant': { desc: 'Open source home automation gateway.', link: 'https://www.home-assistant.io/' },
'homepage': { desc: 'A modern, secure application dashboard.', link: 'https://gethomepage.dev/' },
'immich': { desc: 'Self-hosted photo and video backup solution.', link: 'https://immich.app/' },
'it-tools': { desc: 'Collection of handy online tools for developers.', link: 'https://it-tools.tech/' },
'jellyfin': { desc: 'The volunteer-built media solution.', link: 'https://jellyfin.org/' },
'n8n': { desc: 'Powerful workflow automation tool.', link: 'https://n8n.io/' },
'netbootxyz': { icon: 'netboot-xyz', desc: 'PXE boot various OS installers from the web.', link: 'https://netboot.xyz/' },
'nextcloud': { desc: 'The most popular self-hosted collaboration platform.', link: 'https://nextcloud.com/' },
'ntfy': { desc: 'Send push notifications via HTTP requests.', link: 'https://ntfy.sh/' },
'odoo': { desc: 'Open Source ERP and business suite.', link: 'https://www.odoo.com/' },
'passbolt': { desc: 'Password manager for teams.', link: 'https://www.passbolt.com/' },
'uptime-kuma': { desc: 'Fancy self-hosted monitoring tool.', link: 'https://uptimekuma.org/' },
'vscodium': { desc: 'Free/Libre Open Source binaries of VS Code.', link: 'https://vscodium.com/' },
'clamav': { icon: 'clamav', desc: 'Open-source antivirus engine.', link: 'https://www.clamav.net' },
'virtualization': { icon: 'qemu', desc: 'KVM/QEMU virtualization via Libvirt.', link: 'https://libvirt.org' }
},
formData: {
deploymentMode: 'interactive',
deviceType: 'server',
language: {
lang: 'FR',
locale: 'fr_FR',
timezone: 'Europe/Paris'
},
users: [
{ username: 'admin', name: '', email: '', groups: ['admin'], isStatic: true }
],
groups: {
admin: ['crafty', 'frigate', 'gitea', 'home-assistant', 'homepage', 'immich', 'it-tools', 'jellyfin', 'n8n', 'netbootxyz', 'nextcloud', 'ntfy', 'odoo', 'passbolt', 'uptime-kuma', 'vscodium'],
developer: ['gitea', 'vscodium', 'it-tools', 'odoo'],
family: ['frigate', 'home-assistant', 'homepage', 'immich', 'jellyfin', 'n8n', 'nextcloud', 'ntfy', 'passbolt'],
office: ['passbolt', 'nextcloud']
},
mail: {
smtp_username: '',
smtp_password: '',
smtp_host: '',
smtp_port: ''
},
network: {
ip_address: '',
router_ip: '',
live_target_ip: '',
live_target_password: ''
},
remote_access: {
provider: 'netbird-cloud',
netbird_token: ''
},
security: {
use_sso: true,
public_sharing: true,
ssh_keys: ''
},
ssl: {
domain: '',
cloudflare_token: ''
},
gitConfig: {
url: '',
user: '',
password: ''
},
restoreConfig: {
setup_type: 'new',
restore_data: false,
backup_host: '',
setup_key: '',
backup_key: ''
},
services: {
dns: 'pi-hole',
apps: [],
system: ['clamav'],
apps_extra_opts: {
crafty: {
crafty_java: 1,
crafty_bedrock: 0,
crafty_dynmap: false
}
}
}
},
addGroup() {
const name = this.newGroupName.trim().toLowerCase().replaceAll(' ', '_');
if (name && !this.formData.groups[name]) {
this.formData.groups[name] = [];
this.newGroupName = '';
}
},
addUser() {
if (this.formData.users.length < 20) {
this.formData.users.push({ username: '', name: '', email: '', groups: ['family'], isStatic: false });
}
},
removeUser(index) {
if (this.formData.users.length > 1 && !this.formData.users[index].isStatic) {
this.formData.users.splice(index, 1);
}
},
validators: {
ip: (val) => /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(val),
email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
domain: (val) => /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(val),
port: (val) => /^\d{2,5}$/.test(val),
netbirdToken: (val) => val.startsWith('nbp_')
},
updateMinorSteps() {
// Dynamically set steps for navigation
const configGroup = this.navigation.find(n => n.title === 'Server Configuration' || n.title === 'Configuration');
if (this.formData.deploymentMode === 'interactive') {
configGroup.title = 'Configuration';
configGroup.minors = [
{ label: 'Language', step: 5 },
{ label: 'Network', step: 6 },
{ label: 'Remote Access', step: 7 },
{ label: 'Services', step: 8 },
{ label: 'Security', step: 9 },
{ label: 'Users', step: 10 },
{ label: 'Alerts', step: 11 }
];
} else {
configGroup.title = 'Git Configuration';
configGroup.minors = [
{ label: 'Source Repo', step: 12 },
{ label: 'Setup Type', step: 13 },
{ label: 'Tweaks', step: 14 }
];
}
},
updateNavigation() {
this.updateMinorSteps();
},
async startDiscovery() {
this.updateNavigation();
this.step = 4; // Move to waiting room
try {
await fetch('/discovery', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device: this.formData.deviceType,
live_ip: this.formData.network.live_target_ip,
live_password: this.formData.network.live_target_password
})
});
this.pollHardware();
} catch (e) {
console.error("Bridge Error:", e);
}
},
pollHardware() {
const timer = setInterval(async () => {
try {
const res = await fetch('/hardware.json');
if (res.ok) {
this.hardwareData = await res.json();
clearInterval(timer);
this.step = (this.formData.deploymentMode === 'interactive') ? 5 : 12;
}
} catch (e) { /* Bridge might not have written file yet */ }
}, 2000);
},
canGoNext() {
const rules = {
0: () => true,
1: () => !!this.formData.deploymentMode,
2: () => !!this.formData.deviceType,
3: () => this.validators.ip(this.formData.network.live_target_ip) && !!this.formData.network.live_target_password,
4: () => false,
5: () => !!(this.formData.language.lang && this.formData.language.locale && this.formData.language.timezone),
6: () => this.validators.ip(this.formData.network.ip_address) && this.validators.ip(this.formData.network.router_ip),
7: () => this.formData.remote_access.provider !== 'netbird-cloud' || (this.formData.remote_access.netbird_token && this.validators.netbirdToken(this.formData.remote_access.netbird_token)),
8: () => this.validators.domain(this.formData.ssl.domain),
9: () => this.formData.security.ssh_keys.trim().length > 10 && this.formData.ssl.cloudflare_token.length > 10,
10: () => !!(this.formData.users[0].username && this.formData.users[0].name && this.validators.email(this.formData.users[0].email)),
11: () => this.validators.domain(this.formData.mail.smtp_host) && this.validators.port(this.formData.mail.smtp_port),
12: () => this.formData.gitConfig.url.length > 5,
13: () => !this.formData.restoreConfig.restore_data || (this.formData.restoreConfig.backup_host && this.formData.restoreConfig.setup_key),
14: () => true,
15: () => false
};
if (rules[this.step]) {
return rules[this.step]();
}
return true;
},
getCleanData() {
const exportData = JSON.parse(JSON.stringify(this.formData));
// Remove helper properties from users
exportData.users.forEach(u => delete u.isStatic);
// Only export crafty options if selected
if (!exportData.services.apps.includes('crafty')) {
delete exportData.services.apps_extra_opts.crafty;
}
// Remove apps_extra_opts entirely if empty
if (exportData.services.apps_extra_opts && Object.keys(exportData.services.apps_extra_opts).length === 0) {
delete exportData.services.apps_extra_opts;
}
return exportData;
},
downloadYaml() {
try {
const yaml = jsyaml.dump(this.getCleanData());
const blob = new Blob([yaml], { type: 'text/yaml' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'numbus.yaml';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (e) {
alert('Error generating YAML: ' + e.message);
}
}
}
}
</script>
</body>
</html>