Moving to a web based configurator. Huge changes. Surely a lot of bugfixing to do.

This commit is contained in:
Raphaël Numbus
2026-03-28 21:49:24 +01:00
parent e67dc12f42
commit 29d7eac981
19 changed files with 1469 additions and 563 deletions
+4 -1
View File
@@ -98,10 +98,13 @@ The script will guide you through the setup process, including choosing a deploy
#### Desktop-centric features :
- **[GNOME](https://www.gnome.org):** A modern, elegant desktop environment.
- **[KDE Plasma](https://kde.org):** A full-featured and highly customizable desktop environment.
- **[XFCE](https://xfce.org)**: A super lightweight desktop to breathe new life into old computers.
- **[Wide offering of free and open-source apps](https://flathub.org/en/apps)**: If you need to get something done, there is an app for it.
- **[Windows games compatibility](https://www.protondb.com)**: Most games run on Linux thanks to Proton.
#### TV-centric features :
- **[KDE Plasma Bigscreen](https://plasma-bigscreen.org):** An open-source TV interface for Linux.
- **[Web applications](https://flathub.org/en/apps/net.codelogistics.webapps)**: Install websites as apps.
## 🔧 Deployment Modes
+101
View File
@@ -0,0 +1,101 @@
### MANDATORY SETTINGS ###
## 📦 Live target settings
# See docs/numbus-server/configuration/live_target.md
LIVE_TARGET_IP="192.168.1.10"
LIVE_TARGET_PASSWD="example"
## ⚙️ Server settings
# See docs/numbus-server/configuration/server.md
SERVER_LANGUAGE="FR"
SERVER_LOCALE="fr_FR"
SERVER_TIMEZONE="Europe/Paris"
SERVER_OWNER_NAME="yourName"
SERVER_ADMIN_EMAIL="admin@your-domain.com"
SERVER_AUTHORIZED_SSH_PUBKEYS=( "ssh-ed25519 AAAAoefzefpoipoeCEZJCPEACPAcjapjcpajepcjAPJECJPEJAPJAZ yours@yourdomain.com" )
## 📬 Mail settings
# See docs/numbus-server/configuration/mail.md
SMTP_SERVER_USERNAME="your-address@your-domain.com"
SMTP_SERVER_PASSWORD="emrp raps vzoi vnoe"
SMTP_SERVER_HOST="smtp.yourdomain.com"
SMTP_SERVER_PORT="587"
## 🛜 Network settings
# See docs/numbus-server/configuration/network.md
NETWORK_SUBNET="192.168.1.0/24"
NETWORK_ROUTER_IP="192.168.1.1"
HOME_SERVER_IP="192.168.1.5"
## 🛠️ Services settings
# See docs/numbus-server/configuration/services/index.md
DOMAIN_NAME="yourdomain.com"
SELECTED_DNS_SERVICE="pi-hole" # or SELECTED_DNS_SERVICE="adguard"
SELECTED_WEB_APPLICATIONS=(
"crafty"
"frigate"
"gitea"
"home-assistant"
"homepage"
"immich"
"it-tools"
"jellyfin"
"n8n"
"netbootxyz"
"nextcloud"
"ntfy"
"odoo"
"passbolt"
"uptime-kuma"
"vscodium"
)
SELECTED_SYSTEM_SERVICES=(
"clamav"
"virtualization"
)
## 🚦 Traefik settings
# See docs/numbus-server/configuration/services/automatic_ssl_certs.md
CLOUDFLARE_DNS_API_TOKEN="yourToken"
### OPTIONAL SETTINGS ###
## ⛏️ Crafty settings
# See docs/numbus-server/configuration/services/crafty.md
DYNMAP_ENABLED="false"
WANTED_NUMBER_OF_JAVA_MINECRAFT_SERVERS="1"
WANTED_NUMBER_OF_BEDROCK_MINECRAFT_SERVERS="0"
## 📜 Script settings
# See docs/numbus-server/configuration/script.md
VERBOSE="true"
## 🗺 Custom subdomains
# See docs/numbus-server/configuration/services/custom_subdomain.md
PI_HOLE_SUBDOMAIN="pi-hole"
ADGUARD_SUBDOMAIN="adguard"
CRAFFY_SUBDOMAIN="crafty"
FRIGATE_SUBDOMAIN="frigate"
GITEA_SUBDOMAIN="gitea"
HOME_ASSISTANT_SUBDOMAIN="home-assistant"
HOMEPAGE_SUBDOMAIN="homepage"
IMMICH_SUBDOMAIN="immich"
IT_TOOLS_SUBDOMAIN="it-tools"
JELLYFIN_SUBDOMAIN="jellyfin"
N8N_SUBDOMAIN="n8n"
NETBOOTXYZ_SUBDOMAIN="netbootxyz"
NEXTCLOUD_SUBDOMAIN="nextcloud"
NTFY_SUBDOMAIN="ntfy"
ODOO_SUBDOMAIN="odoo"
PASSBOLT_SUBDOMAIN="passbolt"
UPTIME_KUMA_SUBDOMAIN="uptime-kuma"
VSCODIUM_SUBDOMAIN="vscodium"
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+1225
View File
@@ -0,0 +1,1225 @@
<!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-slate-800">
<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-slate-800">
<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:border-fuchsia-500/50 hover:bg-slate-800">
<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-slate-800 hover:border-slate-600">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-4">
<i class="mdi mdi-cloud-outline text-3xl" :class="formData.remote_access.provider === 'netbird-cloud' ? 'text-white' : 'text-sky-400 group-hover:text-white'"></i>
<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:border-fuchsia-500/50 hover:bg-slate-800">
<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:border-fuchsia-500/50 hover:bg-slate-800">
<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="{ 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-slate-800 hover:border-slate-600">
<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-sky-500 bg-sky-500/10' : 'border-slate-700 bg-slate-900/40'" class="flex flex-col gap-3 p-5 border rounded-xl cursor-pointer transition-all hover:border-sky-400 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-sky-500 bg-sky-500/10' : 'border-slate-700 bg-slate-900/40'" class="flex flex-col gap-3 p-5 border rounded-xl cursor-pointer transition-all hover:border-sky-400 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', step: 10 }
];
// Only servers have alerting/mail config usually
if (isServer) {
configMinors.push({ label: 'Alerting', 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 = 3;
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 > 5,
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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+96 -421
View File
@@ -1,310 +1,63 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash nano coreutils gnused gum fastfetch xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto curl jq
#!nix-shell -i bash -p bash nano coreutils gnused gum fastfetch xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto curl jq yq python3
### --> Default settings
export GUM_SPIN_SPINNER="minidot"
export GUM_SPIN_SPINNER_BOLD=true
export GUM_SPIN_SHOW_ERROR=true
export GUM_SPIN_TITLE_BOLD=true
launch_configurator() {
local PORT=8088
local CONFIG_FILE="numbus.yaml"
local BRIDGE_SCRIPT="configurator/bridge.py"
NECESSARY_BACKUP_SERVER_VARIABLES_LIST=(
#LIVE TARGET SETTINGS
LIVE_TARGET_IP
LIVE_TARGET_PASSWD
#SERVER SETTINGS
SERVER_LANGUAGE
SERVER_LOCALE
SERVER_TIMEZONE
SERVER_OWNER_NAME
SERVER_USER_EMAIL
SERVER_ADMIN_EMAIL
SERVER_AUTHORIZED_SSH_PUBKEYS
# TRAEFIK SETTINGS
TRAEFIK_CLOUDFLARE_TOKEN
# SMTP SETTINGS
SMTP_SERVER_USERNAME
SMTP_SERVER_PASSWORD
SMTP_SERVER_HOST
SMTP_SERVER_PORT
#NETWORK SETTINGS
NETWORK_SUBNET
NETWORK_ROUTER_IP
NETWORK_HOME_SERVER_IP
)
# Create a more robust Python Bridge
cat << EOF > "${BRIDGE_SCRIPT}"
import http.server
import json
import os
OPTIONAL_BACKUP_SERVER_VARIABLES_LIST=(
# SERVICES SETTINGS
SERVICES_DOMAIN_NAME
SERVICES_SELECTED_SYSTEM_PACKAGES
SERVICES_SELECTED_SYSTEM_SERVICES
SERVICES_SELECTED_WEB_APPLICATIONS
SERVIVCES_SELECTED_WEB_APPLICATIONS_SUBDOMAIN
)
class BridgeHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/logs':
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
if os.path.exists('deploy.log'):
with open('deploy.log', 'r') as f:
lines = f.readlines()
self.wfile.write("".join(lines[-20:]).encode())
return
return http.server.SimpleHTTPRequestHandler.do_GET(self)
NECESSARY_COMPUTER_VARIABLES_LIST=(
# LIVE TARGET SETTINGS
LIVE_TARGET_IP
LIVE_TARGET_PASSWD
# COMPUTER SETTINGS
COMPUTER_LANGUAGE
COMPUTER_LOCALE
COMPUTER_TIMEZONE
COMPUTER_OWNER_NAME
COMPUTER_USER_EMAIL
COMPUTER_ADMIN_EMAIL
COMPUTER_AUTHORIZED_SSH_PUBKEYS
# USER SETTINGS
USER_ADMINISTRATORS
USER_NORMAL_USERS
)
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
if self.path == '/discovery':
with open("live_settings.json", "wb") as f:
f.write(post_data)
self.send_response(200)
self.end_headers()
# Signal Bash that discovery data is ready
with open(".discovery_ready", "w") as f: f.write("1")
OPTIONAL_COMPUTER_VARIABLES_LIST=(
# NETWORK SETTINGS
NETWORK_SUBNET
NETWORK_ROUTER_IP
NETWORK_HOME_COMPUTER_IP
# SERVICES SETTINGS
SERVICES_SELECTED_SYSTEM_PACKAGES
SERVICES_SELECTED_DESKTOP_ENVIRONMENT
SERVICE_SELECTED_GNOME_EXTENSIONS
SERVICES_SELECTED_FLATPAK_APPLICATIONS
SERVICES_SELECTED_WEB_APPLICATIONS
)
elif self.path == '/deploy':
with open("${CONFIG_FILE}", "wb") as f:
f.write(post_data)
self.send_response(200)
self.end_headers()
with open(".deploy_signal", "w") as f: f.write("1")
NECESSARY_SERVER_VARIABLES_LIST=(
#LIVE TARGET SETTINGS
LIVE_TARGET_IP
LIVE_TARGET_PASSWD
#SERVER SETTINGS
SERVER_LANGUAGE
SERVER_LOCALE
SERVER_TIMEZONE
SERVER_OWNER_NAME
SERVER_USER_EMAIL
SERVER_ADMIN_EMAIL
SERVER_AUTHORIZED_SSH_PUBKEYS
# TRAEFIK SETTINGS
TRAEFIK_CLOUDFLARE_TOKEN
# SMTP SETTINGS
SMTP_SERVER_USERNAME
SMTP_SERVER_PASSWORD
SMTP_SERVER_HOST
SMTP_SERVER_PORT
#NETWORK SETTINGS
NETWORK_SUBNET
NETWORK_ROUTER_IP
NETWORK_HOME_SERVER_IP
# SERVICES SETTINGS
SERVICES_DOMAIN_NAME
SERVICES_SELECTED_DNS
SERVICES_SELECTED_SYSTEM
SERVICES_SELECTED_WEB_APPLICATIONS
)
os.chdir("configurator")
http.server.HTTPServer(('localhost', ${PORT}), BridgeHandler).serve_forever()
EOF
OPTIONAL_SERVER_VARIABLES_LIST=(
# SERVICES SETTINGS
SELECTED_DNS_SERVICE_SUBDOMAIN
SELECTED_WEB_APPLICATIONS_SUBDOMAIN
)
# Cleanup old signals
rm -f configurator/.discovery_ready configurator/.deploy_signal configurator/live_settings.json configurator/hardware.json
NECESSARY_TV_VARIABLES_LIST=(
#LIVE TARGET SETTINGS
LIVE_TARGET_IP
LIVE_TARGET_PASSWD
#TV SETTINGS
TV_LANGUAGE
TV_LOCALE
TV_TIMEZONE
TV_OWNER_NAME
TV_USER_EMAIL
TV_ADMIN_EMAIL
TV_AUTHORIZED_SSH_PUBKEYS
#NETWORK SETTINGS
NETWORK_SUBNET
NETWORK_ROUTER_IP
NETWORK_HOME_TV_IP
)
echo -e "🚀 Launching Numbus Configurator..."
python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 &
BRIDGE_PID=$!
OPTIONAL_TV_VARIABLES_LIST=(
# SERVICES SETTINGS
SERVICES_SELECTED_SYSTEM_PACKAGES
SERVICES_SELECTED_FLATPAK_APPLICATIONS
SERVICES_SELECTED_WEB_APPLICATIONS
)
# Available DNS services
DNS_SERVICES_LIST=(
"pi-hole"
"adguard"
)
# Available services
WEB_APPLICATIONS_LIST=(
"crafty"
"frigate"
"gitea"
"home-assistant"
"homepage"
"immich"
"it-tools"
"jellyfin"
"n8n"
"netbootxyz"
"nextcloud"
"ntfy"
"odoo"
"passbolt"
"uptime-kuma"
"vscodium"
)
# Available system services
SYSTEM_SERVICES_LIST=(
"clamav"
"virtualization"
)
# Services descriptions
DNS_SERVICES_DESCRIPTION=(
"Pi-hole : Simple, fully open network-wide Ad Blocker"
"AdGuard : Feature-rich network-wide Ad Blocker"
)
WEB_APPLICATIONS_DESCRIPTION=(
"Crafty : A web-based control panel for Minecraft servers"
"Frigate [Home Assistant required] : NVR with real-time local object detection for IP cameras"
"Gitea : Painless self-hosted Git service"
"Home-Assistant : Open source home automation that puts local control and privacy first"
"Homepage : A modern, secure, highly customizable application dashboard"
"Immich : High performance self-hosted photo and video management solution"
"IT-tools : Handy collection of online tools for developers"
"Jellyfin : The Free Software Media System"
"N8n : Workflow automation for technical people"
"netboot.xyz : Network boot various operating system installers and utilities"
"Nextcloud : The most popular self-hosted collaboration platform"
"Ntfy : Send push notifications to your phone or desktop via PUT/POST"
"Odoo : Open Source ERP and CRM"
"Passbolt : Open source password manager for teams"
"Uptime-Kuma : A fancy self-hosted monitoring tool"
"VSCodium : Free/Libre Open Source Software Binaries of VS Code"
)
SYSTEM_SERVICES_DESCRIPTION=(
"ClamAV : An open-source anti-virus"
"Virtualization : Run Virtual Machines (KVM/QEMU) with Libvirt"
)
### Default settings <--
user_input() {
local VAR_NAME="${1}"
local HEADER="${2}"
local PLACEHOLDER="${3}"
local REGEX="${4}"
local ERROR_MSG="${5}"
local SENSITIVE="${6:-false}"
while true; do
[[ "${SENSITIVE}" == "false" ]] && INPUT_VALUE=$(gum input --placeholder "${PLACEHOLDER}" --header "${HEADER}")
[[ "${SENSITIVE}" == "true" ]] && INPUT_VALUE=$(gum input --password --placeholder "${PLACEHOLDER}" --header "${HEADER}")
if [[ -z "${INPUT_VALUE}" ]]; then
echo "❌ Error: Input cannot be empty. Please provide the necessary information."
continue
fi
if [[ -n "${REGEX}" ]]; then
if [[ ! "${INPUT_VALUE}" =~ ${REGEX} ]]; then
echo "❌ Error: ${ERROR_MSG}"
continue
fi
fi
export "${VAR_NAME}"="${INPUT_VALUE}"
break
done
}
strictly_necessary_information() {
export IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" "Invalid IP address format."
user_input "LIVE_TARGET_PASSWD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true"
}
necessary_information() {
# Regex Definitions
local SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
local DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
local EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
local PORT_REGEX='^[0-9]{1,5}$'
local SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*'
echo -e "\n\n➡️ This script needs information about the target you want to install NixOS on\n"
# LIVE TARGET SETTINGS
user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" "Invalid IP address format."
user_input "LIVE_TARGET_PASSWD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true"
echo -e "\n\n➡️ Now provide some information about the server you are deploying\n"
# SERVER SETTINGS
user_input "TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin" "" ""
user_input "LANGUAGE" " Please provide the wanted language :" "For example : FR (for french), EN (for english), DE, IT, etc" "" ""
user_input "LOCALE" " Please provide your locale :" "For example : fr_FR for France, de_DE for Germany, en_US for USA or en_GB for Great-Britain, etc" "" ""
user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve" "" ""
user_input "SERVER_USER_EMAIL" " Please provide a valid user email address (to stay informed about your server's health) :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
user_input "SERVER_ADMIN_EMAIL" " Please provide a valid admin email address (will be used for ACME, and system failures notifications) :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide a list of SSH public keys of authorized devices :" "For example : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhcYDmjMo5YApLkk/3P3HZCnOSzm0uYewNAbxL8Fci8 user@your-pc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." "true"
echo -e "\n\n➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains\n"
# TRAEFIK SETTINGS
user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}" "Invalid domain name format."
user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true"
echo -e "\n\n➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)\n"
# SMTP SETTINGS
user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true"
user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format."
user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number."
echo -e "\n\n➡️ This server will connect to your local network and you will configure its IP address\n"
# NETWORK SETTINGS
user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)."
user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format."
user_input "HOME_SERVER_IP" " Please choose the ip address that your server will use (i.e. any address in the 192.168.1.1/24 range that is not in use.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format."
}
import_variables() {
VARIABLES_LIST="${1}"
NECESSARY="${2:-false}"
echo -e "\n\n➡️ Please choose your configuration file :"
local CONFIG_PATH="$(gum file)"
source "${CONFIG_PATH}"
local MISSING=false
for VAR in "${VARIABLES_LIST[@]}"; do
if [[ -v "${VAR}" && -n "${!VAR}" ]]; then
gum style "✅ "${VAR}" imported successfully from the config file"
else
gum style "❌ "${VAR}" is missing or empty"
MISSING=true
fi
done
if [[ "${MISSING}" == "true" ]]; then
if [[ "${NECESSARY}" = "true" ]]; then
echo -e "\n❌ Please check your configuration file to include all necessary variables"
exit 1
fi
fi
if [[ "${DEBUG:-false}" == "true" ]]; then
echo -e "\n✅ Debugging enabled."
export DIR_COPY_FLAGS="ravu"
export FILES_COPY_FLAGS="avu"
else
export DIR_COPY_FLAGS="rau"
export FILES_COPY_FLAGS="au"
fi
echo -e "➡️ Open your browser at: $(gum style --foreground 212 "http://localhost:${PORT}")"
xdg-open "http://localhost:${PORT}" 2>/dev/null || open "http://localhost:${PORT}" 2>/dev/null || true
}
hierarchy_preparation() {
@@ -359,15 +112,15 @@ ssh_to_host() {
hardware_detection() {
### --> Get hardware information
local TMPFILE="/tmp/nixos-installation-hardware-detection-temp-file"
local TMPFILE="/tmp/hw_detection.json"
ssh_to_host 'bash -s' << SSHEND
TARGET_GRAPHICS="false"
TARGET_GRAPHICS_BRAND=()
for brand in Intel AMD NVIDIA; do
if lspci -nn 2>/dev/null | grep -i "vga" | grep -iq "\${brand}"; then
TARGET_GRAPHICS="true"
TARGET_GRAPHICS_BRAND+=("\${brand}")
else
TARGET_GRAPHICS="false"
fi
done
@@ -420,34 +173,47 @@ for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do
done
echo "# Hardware detection results on \$(date)" > "${TMPFILE}"
for var in \
TARGET_GRAPHICS \
TARGET_GRAPHICS_BRAND \
TARGET_GRAPHICS_RENDERER \
TARGET_USB_CORAL \
TARGET_PCIE_CORAL \
TARGET_ZIGBEE_DEVICE \
TARGET_INTERFACE \
TARGET_TPM \
TARGET_TPM_VERSION; do
echo "export \${var}=\${!var}" >> "${TMPFILE}"
done
for var in \
DISK_DEVPATH \
DISK_NAME \
DISK_TYPE \
DISK_HEALTH \
DISK_ID \
DISK_SIZE; do
declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}"
done
# Build organized JSON output for yq
cat << EOF > "\${TMPFILE}"
{
"graphics": {
"enabled": \${TARGET_GRAPHICS},
"brands": [ \$(printf '"%s",' "\${TARGET_GRAPHICS_BRAND[@]}" | sed 's/,\$//') ],
"renderer": \${TARGET_GRAPHICS_RENDERER}
},
"tpu": {
"usb": \${TARGET_USB_CORAL},
"pcie": \${TARGET_PCIE_CORAL}
},
"tpm": {
"enabled": \${TARGET_TPM},
"version": "\${TARGET_TPM_VERSION}"
},
"zigbee": {
"device": "\${TARGET_ZIGBEE_DEVICE}"
},
"network": {
"interface": "\${TARGET_INTERFACE}"
},
"disks": [
\$(
count=\${#DISK_NAME[@]}
for i in "\${!DISK_NAME[@]}"; do
echo " {\"name\": \"\${DISK_NAME[\$i]}\", \"path\": \"\${DISK_DEVPATH[\$i]}\", \"type\": \"\${DISK_TYPE[\$i]}\", \"health\": \"\${DISK_HEALTH[\$i]}\", \"id\": \"\${DISK_ID[\$i]}\", \"size\": \"\${DISK_SIZE[\$i]}\"}\$( [[ \$i -lt \$((count-1)) ]] && echo ',' )"
done
)
]
}
EOF
SSHEND
### Get hardware information <--
scp -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
source "${TMPFILE}" && rm -rf "${TMPFILE}"
scp -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "hardware.json" &> /dev/null
# Create YAML for NixOS and JSON for the Configurator Website
yq -P '.' hardware.json > hardware.yaml
yq -o=json '.' hardware.yaml > configurator/hardware.json
rm hardware.json
### --> Generate hardware-configuration.nix
if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > final-nix-config/etc/nixos/hardware-configuration.nix; then
@@ -1026,32 +792,19 @@ nix_update() {
--use-remote-sudo switch --flake final-nix-config/etc/nixos#numbus-server
}
congrats() {
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'CONGRATULATIONS !!:') You now have a working home server. \
Data stored on there will be fully yours and protected. Keep in my mind this comes with the \
responsability of managing it and keeping it secure. Now, you have to log in the webpages of \
the services you installed. Create an admin account for all of them and configure them (or keep \
it simple and use defaults) and take care to note down all the passwords. Change all default passwords \
and create user accounts for your family or friends that will use the server.
Cheers !!"
}
set -euo pipefail
clear
fastfetch --logo nixos --logo-padding-left 4 --structure ' '
gum style --align center --width 80 --foreground 212 "
██████ █████ █████
▒▒██████ ▒▒███ ▒▒███
▒███▒███ ▒███ █████ ████ █████████████ ▒███████ █████ ████ █████
▒███▒███ ▒███ █████ ████ █████████████ ▒███████ █████ ████ █████
▒███▒▒███▒███ ▒▒███ ▒▒███ ▒▒███▒▒███▒▒███ ▒███▒▒███▒▒███ ▒▒███ ███▒▒
▒███ ▒▒██████ ▒███ ▒▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒███ ▒▒█████
▒███ ▒▒█████ ▒███ ▒▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒███ ▒▒▒▒███
█████ ▒▒█████ ▒▒████████ █████▒███ █████ ████████ ▒▒████████ ██████
▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒
█████ ▒▒█████ ▒▒████████ █████▒███ █████ ████████ ▒▒████████ ██████
▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒
█████████
███▒▒▒▒▒███
@@ -1062,85 +815,7 @@ gum style --align center --width 80 --foreground 212 "
▒▒█████████ ▒▒██████ █████ ▒▒█████ ▒▒██████ █████
▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒
"
sleep 1
SELECTED_DEVICE=$(gum choose --header "📦 Select the device type to deploy:" \
"numbus-server" \
"numbus-backup-server" \
"numbus-computer" \
"numbus-tv" \
)
SELECTED_MODE=$(gum choose --header "🛠️ Select the deployment strategy for ${SELECTED_DEVICE}:" \
"Semi-interactive (recommended - use a config file)" \
"Interactive (manual input)" \
"Update and Maintain (existing installation)" \
)
if [[ "${SELECTED_MODE}" == "Update and Maintain"* ]]; then
TARGET_USER="numbus-admin"
echo -e "\n➡️ Proceeding with maintenance/update for ${SELECTED_DEVICE}..."
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 \
"➡️ Ensure the remote device is powered on and accessible via SSH."
gum confirm "Ready to proceed?" || { echo "❌ Aborted."; exit 1; }
strictly_necessary_information
setup_ssh
# Maintain legacy update sequence
more_information_config
folder_tree_generation
nix_generation
nix_update
congrats
else
TARGET_USER="nixos"
echo -e "\n➡️ Proceeding with new deployment for ${SELECTED_DEVICE}..."
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 \
"➡️ On the target host: Boot into the NixOS ISO, launch a console, and set a temporary user password."
gum confirm "Ready to proceed?" || { echo "❌ Aborted."; exit 1; }
if [[ "${SELECTED_MODE}" == "Semi-interactive"* ]]; then
import_variables "${VARS_LIST[@]}" "true"
else
strictly_necessary_information
necessary_information
fi
# Standard Deployment Pipeline
hierarchy_preparation
setup_ssh
hardware_detection
# Server-specific logic
if [[ "${SELECTED_DEVICE}" == "numbus-server" ]]; then
services_selection
fi
disks_selection
server_config_generation
network_config_generation
if [[ "${SELECTED_DEVICE}" == "numbus-server" ]]; then
services_config_generation
fi
# Mail setup for server-grade devices
if [[ "${SELECTED_DEVICE}" == *"server"* ]]; then
mail_config_generation
fi
disk_config_generation
keys_generation
sum_up
if [[ "${SELECTED_DEVICE}" == "numbus-server" ]]; then
cloudflare_dns_setup
fi
export_configuration
deploy
postrun_action
fi
launch_configurator
@@ -4,11 +4,11 @@
| Variable | Description | Values | Default |
| -------- | ----------- | ------ | --------- |
| DOMAIN_NAME | The domain name that will be used to access the different services. | your-domain.com | |
| SELECTED_DNS_SERVICE | The DNS service to install (AdBlocking). | ( "pi-hole" ), ( "adguard" ) | ( "pi-hole" ) |
| SELECTED_WEB_APPLICATIONS | The list of web applications to install. | ( "nextcloud" ), ( "homepage" jellyfin" "it-tools" "netbootxyz" ), ... [see the full list below](./index.md#web-applications-list) | |
| SELECTED_SYSTEM_SERVICES | The list of system services to install. | ( "clamav" ), ( "virtualization" "clamav" ), ... [see the full list below](./index.md#system-services-list) | |
| SELECTED_DNS_SERVICE_[subdomain](../custom_subdomain.md) | Custom [subdomain](../custom_subdomain.md) for the DNS service. | "my-dns" | Will be the name of the service. I.e. pi-hole.your-domain.com or adguard.your-domain.com |
| SELECTED_WEB_APPLICATIONS_[subdomain](../custom_subdomain.md) | Custom [subdomain](../custom_subdomain.md)s for the web applications (must match the order of SELECTED_WEB_APPLICATIONS). | "my-cloud", "my-photos" | Will be the name of the service. I.e. nextcloud.your-domain.com, immich.your-domain.com, ... |
## Web applications list
@@ -37,7 +37,7 @@ This is the list of **all the available apps** that can be enabled on the numbus
## System services list
| Name | Description | Additional settings ? |
| Name | Description | Additional settings |
| -------- | ----------- | ------ |
| clamav | Open-source anti-virus software. | No |
| virtualization | Run Virtual Machines (KVM/QEMU). | No |
| clamav | Open-source anti-virus software. | |
| virtualization | Run Virtual Machines (KVM/QEMU). | |
-119
View File
@@ -1,119 +0,0 @@
## 📦 Live target settings
# See docs/numbus-server/configuration/live_target.md
export LIVE_TARGET_IP="192.168.1.10"
export LIVE_TARGET_PASSWD="example"
## ⚙️ Server settings
# See docs/numbus-server/configuration/server.md
export SERVER_LANGUAGE="FR"
export SERVER_LOCALE="fr_FR"
export SERVER_TIMEZONE="Europe/Paris"
export SERVER_OWNER_NAME="yourName"
export SERVER_USER_EMAIL="user@your-domain.com"
export SERVER_ADMIN_EMAIL="admin@your-domain.com"
export SERVER_AUTHORIZED_SSH_PUBKEYS=( "ssh-ed25519 AAAAoefzefpoipoeCEZJCPEACPAcjapjcpajepcjAPJECJPEJAPJAZ yours@yourdomain.com" )
## 📬 Mail settings
# See docs/numbus-server/configuration/mail.md
export SMTP_SERVER_USERNAME="your-address@your-domain.com"
export SMTP_SERVER_PASSWORD="emrp raps vzoi vnoe"
export SMTP_SERVER_HOST="smtp.yourdomain.com"
export SMTP_SERVER_PORT="587"
## 🚦 Traefik settings
# See docs/numbus-server/configuration/services/traefik.md
export CLOUDFLARE_DNS_API_TOKEN="yourToken"
## 🛜 Network settings
# See docs/numbus-server/configuration/network.md
export NETWORK_SUBNET="192.168.1.0/24"
export NETWORK_ROUTER_IP="192.168.1.1"
export HOME_SERVER_IP="192.168.1.5"
## 🛠️ Services settings
# See docs/numbus-server/configuration/services/index.md
export DOMAIN_NAME="yourdomain.com"
## DNS service
export SELECTED_DNS_SERVICE=(
"pi-hole"
"adguard"
)
## Web applications
export SELECTED_WEB_APPLICATIONS=(
"crafty"
"frigate"
"gitea"
"home-assistant"
"homepage"
"immich"
"it-tools"
"jellyfin"
"n8n"
"netbootxyz"
"nextcloud"
"ntfy"
"odoo"
"passbolt"
"uptime-kuma"
"vscodium"
)
## System services
export SELECTED_SYSTEM_SERVICES=(
"clamav"
"virtualization"
)
## DNS service subdomain
# See docs/numbus-server/configuration/services/index.md
export SELECTED_DNS_SERVICE_SUBDOMAIN=(
"my-pi-hole-subdomain" # or "my-adguard-subdomain"
)
## Web applications subdomain
# ⚠️ The order must strictly match the SELECTED_WEB_APPLICATIONS array above.
export SELECTED_WEB_APPLICATIONS_SUBDOMAIN=(
"my-crafty-subdomain"
"my-frigate-subdomain"
"my-gitea-subdomain"
"my-home-assistant-subdomain" # Example : your Home-assistant URL will be ; https://my-home-assistant-subdomain.yourdomain.com/
"my-homepage-subdomain"
"my-immich-subdomain"
"my-it-tools-subdomain"
"my-jellyfin-subdomain" # Example : your Jellyfin URL will be ; https://my-jellyfin-subdomain.yourdomain.com/
"my-n8n-subdomain"
"my-netbootxyz-subdomain"
"my-nextcloud-subdomain"
"my-ntfy-subdomain"
"my-odoo-subdomain"
"my-passbolt-subdomain"
"my-uptime-kuma-subdomain"
"my-vscodium-subdomain"
)
## ⛏️ Crafty settings
# See docs/numbus-server/configuration/services/crafty.md
export DYNMAP_ENABLED="false"
export WANTED_NUMBER_OF_JAVA_MINECRAFT_SERVERS="1"
export WANTED_NUMBER_OF_BEDROCK_MINECRAFT_SERVERS="0"
## 📜 Script settings
# See docs/numbus-server/configuration/script.md
export VERBOSE="true"
+8 -11
View File
@@ -1,20 +1,17 @@
{ modulesPath, config, pkgs, inputs, ... }:
# Do NOT edit this file manually.
# Please use the dedicated script : https://gittea.dev/numbus/numbus.
# This could compromise system stability and is not supported by numbus.
{ config, modulesPath, ... }:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
(modulesPath + "/profiles/qemu-guest.nix")
inputs.sops-nix.nixosModules.sops
./custom-configuration.nix
./numbus-generated.nix
];
# System
system.stateVersion = "25.11";
# Secrets management
sops.defaultSopsFile = ./secrets/secrets.yaml;
sops.age.sshKeyPaths = [ "/home/numbus-admin/.ssh/id_ed25519" ];
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
# Secrets
sops.secrets."authorizedSshPublicKeys" = { owner = "numbus-admin"; path = "/home/numbus-admin/.ssh/authorized_keys"; mode = "0600"; };
sops.secrets."smtpPassword" = { owner = "numbus-admin"; mode = "0600"; };
sops.secrets."cloudflareDnsApiToken" = { owner = "numbus-admin"; mode = "0600"; };
}
@@ -0,0 +1,5 @@
# This file is reserved for ADVANCED USERS ONLY.
# Editing could compromise system stability and is not supported by numbus.
# Do NOT set options already managed by numbus. i.e. config.numbus.* and other options (networking, storage, etc.)
# Please use the dedicated script for those options : https://gittea.dev/numbus/numbus.
+8 -4
View File
@@ -1,10 +1,14 @@
# Do NOT edit this file manually.
# Please use the dedicated script : https://gittea.dev/numbus/numbus.
# This could compromise system stability and is not supported by numbus.
{
inputs = {
# Core Nixpkgs
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
# Numbus server configuration
numbus.url = "git+https://gittea.dev/numbus/numbus-server";
numbus.inputs.nixpkgs.follows = "nixpkgs";
numbus-server.url = "git+https://gittea.dev/numbus/numbus-server";
numbus-server.inputs.nixpkgs.follows = "nixpkgs";
# Disk-partitioning helper
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
@@ -16,7 +20,7 @@
autoaspm.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, numbus, disko, sops-nix, autoaspm, ... }@inputs: let
outputs = { self, nixpkgs, numbus-server, disko, sops-nix, autoaspm, ... }@inputs: let
# System definition
system = "x86_64-linux";
pkgs = import nixpkgs {
@@ -31,7 +35,7 @@
specialArgs = { inherit inputs; };
modules = [
# Numbus server configuration
numbus.nixosModules.numbus
numbus-server.nixosModules.numbus-server
# Disk-partitioning helper
disko.nixosModules.disko
# Secrets handling
@@ -0,0 +1,10 @@
# Do NOT edit this file manually.
# Please use the dedicated script : https://gittea.dev/numbus/numbus.
# This could compromise system stability and is not supported by numbus.
{ config, pkgs, inputs, ... }:
{
imports = [
inputs.sops-nix.nixosModules.sops
];
@@ -1,4 +1,10 @@
# SSH
authorizedSshPublicKeys: |
$SSH_KEYS_FORMATTED
# SMTP
smtpPassword: "$SMTP_SERVER_PASSWORD"
cloudflareDnsApiToken: "$CLOUDFLARE_DNS_API_TOKEN"
# CLOUDFLARE
cloudflareDnsApiToken: "$CLOUDFLARE_DNS_API_TOKEN"
-1
View File
@@ -1 +0,0 @@
# Populate this file with a valid WireGuard tunnel configuration if you chose to deploy a numbus-backup-server.