Bunch of changes. Settled on keeping both TUI and WebUI options.
@@ -1,4 +1,4 @@
|
||||
final-nix-config/
|
||||
config/
|
||||
test*
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -1,5 +1,5 @@
|
||||
agents/
|
||||
final-nix-config/
|
||||
web/ux/
|
||||
config/
|
||||
test*
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -1,39 +0,0 @@
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
elif self.path == '/deploy':
|
||||
with open("../numbus.yaml", "wb") as f:
|
||||
f.write(post_data)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
with open(".deploy_signal", "w") as f: f.write("1")
|
||||
|
||||
os.chdir("configurator")
|
||||
http.server.HTTPServer(('localhost', 8088), BridgeHandler).serve_forever()
|
||||
@@ -1,1372 +0,0 @@
|
||||
<!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.5 -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-400'"
|
||||
class="text-[11px] leading-tight transition-colors" x-text="minor.label"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-grow flex items-center justify-center p-4 md:p-12 relative overflow-hidden">
|
||||
|
||||
<!-- Welcome Screen -->
|
||||
<div x-show="step === 0" x-transition:enter="transition ease-out duration-500" class="max-w-2xl text-center space-y-8 z-10">
|
||||
<img src="logo.png" alt="Numbus Logo" class="w-48 h-48 mx-auto drop-shadow-2xl animate-pulse-slow">
|
||||
<h1 class="text-6xl font-extrabold tracking-tight">Welcome to <span class="text-sky-400 text-shadow-glow">Numbus</span></h1>
|
||||
<p class="text-2xl text-slate-400 leading-relaxed">Let's transform your hardware into a powerful, private appliance. We'll guide you through discovery and configuration.</p>
|
||||
|
||||
<div class="bg-amber-500/10 border border-amber-500/20 p-4 rounded-xl flex gap-4 text-left items-center">
|
||||
<span class="bg-amber-500 rounded-full p-1 px-1.5 shrink-0">
|
||||
<i class="mdi mdi-shield-lock text-slate-900 text-sm"></i>
|
||||
</span>
|
||||
<p class="text-sm text-amber-200/80 italic"><strong>Privacy First:</strong> No data entered here ever leaves your device. This configurator runs entirely locally in your browser and is fully private.</p>
|
||||
</div>
|
||||
|
||||
<button @click="step = 1" class="px-10 py-4 bg-fuchsia-600 hover:bg-fuchsia-500 rounded-full text-xl font-bold transition-all transform hover:scale-105 shadow-lg shadow-fuchsia-600/20">
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Card -->
|
||||
<div x-show="step > 0" x-transition:enter="transition ease-out duration-300" class="w-full max-w-5xl bg-[#1e293b] rounded-2xl shadow-2xl border border-slate-700 overflow-hidden flex flex-col min-h-[650px]" x-cloak>
|
||||
<div class="p-8 md:p-10 flex-grow overflow-y-auto custom-scrollbar">
|
||||
|
||||
<!-- Step 1: Deployment Mode -->
|
||||
<div x-show="step === 1" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Deployment Mode</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">How would you like to set up your device?</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button @click="formData.deploymentMode = 'interactive'"
|
||||
:class="formData.deploymentMode === 'interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
|
||||
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<div class="font-bold text-lg mb-1" :class="formData.deploymentMode === 'interactive' ? 'text-white' : 'text-slate-200 group-hover:text-white'">Interactive</div>
|
||||
<p class="text-xs transition-colors" :class="formData.deploymentMode === 'interactive' ? 'text-white' : 'text-slate-500 group-hover:text-white'">Guide me through every setting manually.</p>
|
||||
</button>
|
||||
<button @click="formData.deploymentMode = 'non-interactive'"
|
||||
:class="formData.deploymentMode === 'non-interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
|
||||
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<div class="font-bold text-lg mb-1" :class="formData.deploymentMode === 'non-interactive' ? 'text-white' : 'text-slate-200 group-hover:text-white'">Non-Interactive</div>
|
||||
<p class="text-xs transition-colors" :class="formData.deploymentMode === 'non-interactive' ? 'text-white' : 'text-slate-500 group-hover:text-white'">Reuse an existing configuration from a Git repo.</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Device Personality -->
|
||||
<div x-show="step === 2" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Device Type</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Select the personality for your new Numbus machine.</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<template x-for="device in deviceTypes" :key="device.id">
|
||||
<button @click="formData.deviceType = device.id"
|
||||
:class="formData.deviceType === device.id ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
|
||||
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="w-12 h-12 bg-slate-800 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||
<i :class="'mdi ' + device.icon" class="text-sky-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-lg capitalize" :class="formData.deviceType === device.id ? 'text-white' : 'text-slate-200'" x-text="device.name"></div>
|
||||
<p class="text-xs transition-colors" :class="formData.deviceType === device.id ? 'text-white' : 'text-slate-500 group-hover:text-white'" x-text="device.desc"></p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Live Setup & BIOS Guide -->
|
||||
<div x-show="step === 3" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Live Setup</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Follow these steps to prepare your hardware for discovery.</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10">
|
||||
<div class="space-y-6">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold shrink-0">1</div>
|
||||
<p class="text-sm text-slate-300">Create a <strong>NixOS Boot ISO</strong> and flash it to a USB drive.</p>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold shrink-0">2</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-slate-300">In BIOS, enable <strong>UEFI, VT-x/SVM, VT-d/IOMMU, TPM 2.0</strong> and <strong>Disable Secure Boot</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold shrink-0">3</div>
|
||||
<p class="text-sm text-slate-300">Boot the device. Type <code class="bg-slate-800 px-1 rounded text-fuchsia-400">ip a</code> to get the IP, then <code class="bg-slate-800 px-1 rounded text-fuchsia-400">passwd</code> to set a temporary password.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-900/50 p-6 rounded-2xl border border-slate-700/50 space-y-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Live Target IP Address</label>
|
||||
<input type="text" x-model="formData.network.live_target_ip" placeholder="192.168.1.100" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Temporary Password</label>
|
||||
<input type="password" x-model="formData.network.live_target_password" placeholder="••••••••" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
|
||||
</div>
|
||||
<button @click="startDiscovery()" :disabled="!validators.ip(formData.network.live_target_ip) || !formData.network.live_target_password" class="w-full py-4 bg-sky-600 hover:bg-sky-500 disabled:opacity-30 rounded-xl font-bold transition-all shadow-lg shadow-sky-600/20">Initialize Discovery</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Discovery Waiting Room -->
|
||||
<div x-show="step === 4" class="flex flex-col items-center justify-center py-20 text-center space-y-8">
|
||||
<div class="relative w-32 h-32">
|
||||
<div class="absolute inset-0 rounded-full border-4 border-slate-800 border-t-fuchsia-500 animate-spin"></div>
|
||||
<div class="absolute inset-4 rounded-full border-4 border-slate-800 border-b-sky-500 animate-spin-slow"></div>
|
||||
</div>
|
||||
<h2 class="text-4xl font-bold text-white">Hardware Discovery in Progress</h2>
|
||||
<p class="text-slate-400 max-w-md mx-auto text-lg">We're remotely probing your machine for disks and interfaces. This usually takes 1-2 minutes.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Language & Region -->
|
||||
<div x-show="step === 5" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Language</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Set your regional preferences to ensure correct time and language display.</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2" x-data="{ open: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-base font-semibold text-slate-300">System Language <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="open" class="text-xs text-sky-400 italic mb-2">Choose the language for your server's system and primary interfaces.</p>
|
||||
<select x-model="formData.language.lang" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none transition-all appearance-none cursor-pointer">
|
||||
<option value="EN">English</option>
|
||||
<option value="FR">French</option>
|
||||
<option value="DE">German</option>
|
||||
<option value="ES">Spanish</option>
|
||||
<option value="IT">Italian</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2" x-data="{ open: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-base font-semibold text-slate-300">Country (Locale) <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="open" class="text-xs text-sky-400 italic mb-2">Defines regional formats for dates, currencies, and numbers (e.g., en_US for USA).</p>
|
||||
<select x-model="formData.language.locale" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none appearance-none cursor-pointer">
|
||||
<option value="en_US">United States</option>
|
||||
<option value="en_GB">United Kingdom</option>
|
||||
<option value="fr_FR">France</option>
|
||||
<option value="de_DE">Germany</option>
|
||||
<option value="it_IT">Italy</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2" x-data="{ open: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-base font-semibold text-slate-300">Timezone <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="open" class="text-xs text-sky-400 italic mb-2">Used to synchronize server logs, backups, and scheduled updates with your local time.</p>
|
||||
<select x-model="formData.language.timezone" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none appearance-none cursor-pointer">
|
||||
<option value="UTC">UTC</option>
|
||||
<option value="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 10: Users -->
|
||||
<div x-show="step === 10" class="space-y-8" x-data="{ groupHelp: false }">
|
||||
<div class="border-b border-slate-700 pb-4 flex justify-between items-end">
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-sky-400">Users</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Manage people and their access permissions.</p>
|
||||
</div>
|
||||
<button @click="addUser()"
|
||||
:disabled="formData.users.length >= 10"
|
||||
:class="formData.users.length >= 10 ? 'opacity-50 cursor-not-allowed' : ''"
|
||||
class="bg-sky-600 hover:bg-sky-500 px-4 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2">
|
||||
<i class="mdi mdi-account-plus"></i> Add User (<span x-text="formData.users.length"></span>/10)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 pr-2">
|
||||
<template x-for="(user, index) in formData.users" :key="index">
|
||||
<div :class="user.isStatic ? 'border-fuchsia-500/50 bg-fuchsia-500/5' : 'border-slate-700 bg-slate-900/50'" class="border p-6 rounded-2xl space-y-4 relative group">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-sky-400 font-bold" x-text="(user.name.charAt(0) || user.username.charAt(0) || '?').toUpperCase()"></div>
|
||||
<div>
|
||||
<p class="font-bold text-slate-200 text-lg group-hover:text-white transition-colors" x-text="user.name || user.username || 'New User'"></p>
|
||||
<p class="text-xs 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-xs 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-xs 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-xs 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 12: Disks -->
|
||||
<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">Disks</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Select where NixOS will be installed and where your data will live.</p>
|
||||
</div>
|
||||
|
||||
<!-- Critical Warning -->
|
||||
<div class="bg-red-500/10 border border-red-500/50 p-6 rounded-2xl flex gap-6 items-start">
|
||||
<div class="bg-red-500 rounded-full p-2 mt-1 shrink-0">
|
||||
<i class="mdi mdi-alert-octagon text-white text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-red-400 font-bold text-lg">Data Wipe Warning</h4>
|
||||
<p class="text-sm text-red-200/80 leading-relaxed">Selecting a disk for Numbus will <strong>completely erase all existing data</strong> on that drive. Ensure you have backed up any important information before proceeding.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-6 items-start">
|
||||
<!-- Headers Row: Aligned horizontally -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-xl font-bold text-white flex items-center gap-2">
|
||||
<i class="mdi mdi-nix text-sky-400"></i> Boot Disks
|
||||
</h3>
|
||||
<p class="text-sm text-slate-400">Choose 1 (Stripe) or 2 (Mirror) disks for the operating system.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-xl font-bold text-white flex items-center gap-2">
|
||||
<i class="mdi mdi-server text-sky-400"></i> Data Disks
|
||||
</h3>
|
||||
<p class="text-sm text-slate-400">Select disks for storage. SnapRAID parity is used if >1 disk is chosen.</p>
|
||||
</div>
|
||||
|
||||
<!-- Lists Row: Aligned horizontally -->
|
||||
<div class="space-y-3">
|
||||
<template x-for="disk in sortedDisks()" :key="disk.id">
|
||||
<button @click="toggleDisk(disk.id, 'boot')"
|
||||
x-show="!formData.disks.data.includes(disk.id)"
|
||||
:class="formData.disks.boot.includes(disk.id) ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700 opacity-60 hover:opacity-100'"
|
||||
class="w-full p-4 border rounded-xl text-left transition-all group">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<i :class="'mdi ' + getDiskIcon(disk.type) + ' text-2xl'" :class="formData.disks.boot.includes(disk.id) ? 'text-white' : 'text-slate-500'"></i>
|
||||
<div>
|
||||
<div class="font-bold text-sm" x-text="disk.name"></div>
|
||||
<div class="text-xs uppercase text-slate-500 font-bold" x-text="disk.size + ' • ' + disk.type"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-xs font-bold" :class="disk.health === 'PASSED' ? 'text-green-500' : 'text-amber-500'" x-text="'Health: ' + disk.health"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="formData.disks.boot.includes(disk.id) && disk.health !== 'PASSED'" class="mt-3 p-2 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
||||
<p class="text-xs text-amber-200/80"><i class="mdi mdi-alert-circle mr-1"></i>Disk did not passed SMART test. You should consider using another disk.</p>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div x-show="formData.disks.boot.length === 1" class="bg-amber-500/5 border border-amber-500/20 p-4 rounded-xl">
|
||||
<p class="text-xs text-amber-200/70"><i class="mdi mdi-shield-alert mr-1"></i> <strong>Striped Config:</strong> Using only one boot disk provides no hardware redundancy. If this drive fails, the system will not boot.</p>
|
||||
</div>
|
||||
<div x-show="formData.disks.boot.length === 2" class="bg-green-500/5 border border-green-500/20 p-4 rounded-xl">
|
||||
<p class="text-xs text-green-400/70"><i class="mdi mdi-shield-check mr-1"></i> <strong>Mirror RAID Active:</strong> Your data is protected by redundancy.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<template x-for="disk in sortedDisks()" :key="disk.id">
|
||||
<button @click="toggleDisk(disk.id, 'data')"
|
||||
x-show="!formData.disks.boot.includes(disk.id)"
|
||||
:class="formData.disks.data.includes(disk.id) ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700 opacity-60 hover:opacity-100'"
|
||||
class="w-full p-4 border rounded-xl text-left transition-all group">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<i :class="'mdi ' + getDiskIcon(disk.type) + ' text-2xl'" :class="formData.disks.data.includes(disk.id) ? 'text-white' : 'text-slate-500'"></i>
|
||||
<div>
|
||||
<div class="font-bold text-sm" x-text="disk.name"></div>
|
||||
<div class="text-xs uppercase text-slate-500 font-bold" x-text="disk.size + ' • ' + disk.type"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-xs font-bold" :class="disk.health === 'PASSED' ? 'text-green-500' : 'text-amber-500'" x-text="'Health: ' + disk.health"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="formData.disks.data.includes(disk.id) && disk.health !== 'PASSED'" class="mt-3 p-2 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
||||
<p class="text-xs text-amber-200/80"><i class="mdi mdi-alert-circle mr-1"></i>Disk did not passed SMART test. You should consider using another disk.</p>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div x-show="formData.disks.data.length === 1" class="bg-amber-500/5 border border-amber-500/20 p-4 rounded-xl">
|
||||
<p class="text-xs text-amber-200/70"><i class="mdi mdi-shield-alert mr-1"></i> <strong>Striped Config:</strong> One data disk provides no parity. Data loss will occur if this drive fails.</p>
|
||||
</div>
|
||||
<div x-show="formData.disks.data.length > 1" class="bg-green-500/5 border border-green-500/20 p-4 rounded-xl">
|
||||
<p class="text-xs text-green-400/70"><i class="mdi mdi-shield-check mr-1"></i> <strong>SnapRAID Active:</strong> Your data is protected by redundancy.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 6: Network -->
|
||||
<div x-show="step === 6" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Network</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Target host networking and server IP allocation.</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="space-y-2" x-data="{ open: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-base font-semibold text-slate-300">Server Static IP <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The fixed address of your server on your local network. It should be outside your router's DHCP range.</p>
|
||||
<input type="text" x-model="formData.network.ip_address" placeholder="192.168.1.5" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
|
||||
</div>
|
||||
<div class="space-y-2" x-data="{ open: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-base font-semibold text-slate-300">Router IP <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="open" class="text-xs text-sky-400 italic mb-2">The address of the device providing internet (the gateway). Usually 192.168.1.1.</p>
|
||||
<input type="text" x-model="formData.network.router_ip" placeholder="192.168.1.1" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 7: Remote Access -->
|
||||
<div x-show="step === 7" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Remote Access</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Choose how you will access your server from outside your home.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<!-- Netbird Cloud - Only for <= 5 users -->
|
||||
<button x-show="formData.users.length <= 5"
|
||||
@click="formData.remote_access.provider = 'netbird-cloud'"
|
||||
:class="formData.remote_access.provider === 'netbird-cloud' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
|
||||
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<img src="https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/netbird.svg" class="w-8 h-8" alt="NetBird">
|
||||
<div class="font-bold text-xl" :class="formData.remote_access.provider === 'netbird-cloud' ? 'text-white' : 'text-slate-200 group-hover:text-white'">NetBird Cloud</div>
|
||||
</div>
|
||||
<span class="text-xs bg-green-500/20 text-green-400 px-2 py-1 rounded uppercase font-bold">Recommended for small teams</span>
|
||||
</div>
|
||||
<p class="text-sm leading-relaxed transition-colors" :class="formData.remote_access.provider === 'netbird-cloud' ? 'text-white' : 'text-slate-400 group-hover:text-white'">The easiest zero-trust solution. Perfect for families and teams of up to 5 people.</p>
|
||||
</button>
|
||||
|
||||
<!-- Netbird Cloud Token Input -->
|
||||
<div x-show="formData.remote_access.provider === 'netbird-cloud'" x-transition class="mt-2 space-y-4 bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-base font-semibold text-slate-300">Service User Token <span class="text-fuchsia-500">*</span></label>
|
||||
<div x-data="{ open: false }" class="relative">
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline text-lg"></i>
|
||||
</button>
|
||||
<div x-show="open" @click.away="open = false" class="absolute left-0 md:left-full ml-0 md:ml-4 top-full md:top-0 mt-2 md:mt-0 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="font-bold text-sky-400 mb-2">How to get your token:</p>
|
||||
<ol class="list-decimal list-inside space-y-2 text-slate-300">
|
||||
<li>Create an account at <a href="https://app.netbird.io" target="_blank" class="text-fuchsia-400 underline font-bold">NetBird</a></li>
|
||||
<li>Verify your email via the link sent to you</li>
|
||||
<li>Navigate to <span class="font-mono bg-slate-900 px-1 rounded">Teams > Service Users</span></li>
|
||||
<li>Click <span class="font-bold text-white">"Create Service User"</span>, name it "numbus-server" and set role to <span class="text-white">"Network Admin"</span></li>
|
||||
<li>Click on the new user, then <span class="text-white">"Add Token"</span>. Name it "deployment" and set expiry to <span class="text-white">7 days</span></li>
|
||||
<li>Copy the token and paste it here!</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="password" x-model="formData.remote_access.netbird_token" placeholder="nbp_..." class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40 text-base">
|
||||
</div>
|
||||
|
||||
<!-- Netbird Self-Hosted -->
|
||||
<button @click="formData.remote_access.provider = 'netbird-selfhosted'"
|
||||
:class="formData.remote_access.provider === 'netbird-selfhosted' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
|
||||
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<img src="https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/netbird.svg" class="w-8 h-8" alt="NetBird">
|
||||
<div class="font-bold text-xl" :class="formData.remote_access.provider === 'netbird-selfhosted' ? 'text-white' : 'text-slate-200 group-hover:text-white'">NetBird Self-Hosted</div>
|
||||
</div>
|
||||
<p class="text-sm leading-relaxed transition-colors" :class="formData.remote_access.provider === 'netbird-selfhosted' ? 'text-white' : 'text-slate-400 group-hover:text-white'">Complete digital sovereignty. You host the management platform on your own Numbus server. Required for teams larger than 5 users.</p>
|
||||
</button>
|
||||
|
||||
<!-- External -->
|
||||
<button @click="formData.remote_access.provider = 'external'"
|
||||
:class="formData.remote_access.provider === 'external' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'"
|
||||
class="p-6 border rounded-2xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<i class="mdi mdi-earth text-3xl" :class="formData.remote_access.provider === 'external' ? 'text-white' : 'text-sky-400 group-hover:text-white'"></i>
|
||||
<div class="font-bold text-xl" :class="formData.remote_access.provider === 'external' ? 'text-white' : 'text-slate-200 group-hover:text-white'">External / Manual Access</div>
|
||||
</div>
|
||||
<p class="text-sm leading-relaxed transition-colors" :class="formData.remote_access.provider === 'external' ? 'text-white' : 'text-slate-400 group-hover:text-white'">Use your own VPN or direct port-forwarding. <strong>Warning:</strong> This server won't be compatible with Numbus integrated offsite backups.</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 9: Security -->
|
||||
<div x-show="step === 9" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Security</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Manage infrastructure access and advanced protection.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- SSO Toggle -->
|
||||
<div class="bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-lg font-bold text-slate-200 transition-colors">Use Single Sign-On (SSO)</label>
|
||||
<div x-data="{ open: false }" class="relative">
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline text-lg"></i>
|
||||
</button>
|
||||
<div x-show="open" @click.away="open = false" class="absolute left-full ml-4 top-0 w-64 p-4 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
One login for everything. Centralized identity management via LLDAP and Authelia. Provides enterprise-grade security and 2FA for all your apps.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="formData.security.use_sso = !formData.security.use_sso"
|
||||
:class="formData.security.use_sso ? 'bg-fuchsia-600 shadow-[0_0_10px_rgba(192,38,211,0.4)]' : 'bg-slate-700'"
|
||||
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none">
|
||||
<span :class="formData.security.use_sso ? 'translate-x-5' : 'translate-x-0'" class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Sharing Toggle -->
|
||||
<div class="bg-slate-900/40 p-6 rounded-2xl border border-slate-700/50 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-lg font-bold text-slate-200">Enable Secure Public Sharing</label>
|
||||
<div x-data="{ open: false }" class="relative">
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline text-lg"></i>
|
||||
</button>
|
||||
<div x-show="open" @click.away="open = false" class="absolute left-full ml-4 top-0 w-64 p-4 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
Allows you to share links (like Nextcloud folders) with friends who don't have NetBird. Only specific URLs are exposed; everything else remains locked behind the VPN.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="formData.security.public_sharing = !formData.security.public_sharing"
|
||||
:class="formData.security.public_sharing ? 'bg-fuchsia-600 shadow-[0_0_10px_rgba(192,38,211,0.4)]' : 'bg-slate-700'"
|
||||
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none">
|
||||
<span :class="formData.security.public_sharing ? 'translate-x-5' : 'translate-x-0'" class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4" x-data="{ help: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-bold text-slate-200">Cloudflare DNS API Token <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="help = !help" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="help" class="text-xs text-sky-400 italic mb-2">A security token from Cloudflare that allows Numbus to verify you own the domain and secure it via SSL.</p>
|
||||
<input type="password" x-model="formData.ssl.cloudflare_token" placeholder="••••••••••••••••" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
|
||||
</div>
|
||||
|
||||
<div class="space-y-4" x-data="{ open: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-bold text-slate-200">Authorized SSH Public Keys <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="open = !open" class="text-slate-500 hover:text-sky-400 transition-colors">
|
||||
<i class="mdi mdi-information-outline text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="open" class="text-xs text-sky-400 italic mb-2">A secure way to log in without a password. Like a physical key, but digital.</p>
|
||||
<p class="text-sm text-slate-500 mb-2 italic">Paste here the public keys that are allowed to connect to your server's admin account (one per line).</p>
|
||||
<textarea x-model="formData.security.ssh_keys" rows="5" placeholder="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..." class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none transition-all font-mono text-base placeholder:text-slate-500/40"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Security Warning -->
|
||||
<div class="bg-amber-500/10 border border-amber-500/20 p-6 rounded-2xl flex gap-6 text-left items-start mt-4">
|
||||
<div class="bg-amber-500 rounded-full p-2 mt-1 shrink-0">
|
||||
<i class="mdi mdi-alert text-slate-900 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-base text-amber-200/90 leading-relaxed font-medium"><strong>Security Warning:</strong> Anyone who possesses one of these public keys AND their corresponding private key will have full administrator access to your server. Protect your private keys as if they were physical keys to your home.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 8: Services selection -->
|
||||
<div x-show="step === 8" class="space-y-8">
|
||||
<div class="border-b border-slate-700 pb-4">
|
||||
<h2 class="text-4xl font-bold text-sky-400">Services</h2>
|
||||
<p class="text-slate-400 mt-2 text-lg">Choose the applications to deploy.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-10 pr-4">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-bold text-slate-200">Domain Name (FQDN) <span class="text-fuchsia-500">*</span></label>
|
||||
<button @click="openHelp('domain')" class="text-slate-500 hover:text-sky-400">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" x-model="formData.ssl.domain" placeholder="example.com" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40">
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-fuchsia-400">DNS Filter</h3>
|
||||
<div class="flex gap-4">
|
||||
<template x-for="dns in ['pi-hole', 'adguard']" :key="dns">
|
||||
<button @click="formData.services.dns = dns" :class="formData.services.dns === dns ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex-1 p-5 border rounded-xl transition-all text-left group hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<img :src="'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/' + (serviceMetadata[dns].icon || dns) + '.svg'" class="w-6 h-6" :alt="dns">
|
||||
<div class="font-bold capitalize text-lg" :class="formData.services.dns === dns ? 'text-white' : 'text-slate-200 group-hover:text-white'" x-text="dns"></div>
|
||||
</div>
|
||||
<a :href="serviceMetadata[dns].link" target="_blank" @click.stop class="text-xs text-sky-400 underline opacity-40 group-hover:opacity-100 transition-opacity">Website</a>
|
||||
</div>
|
||||
<p class="text-sm leading-relaxed transition-colors" :class="formData.services.dns === dns ? 'text-white' : 'text-slate-400 group-hover:text-white'" x-text="serviceMetadata[dns].desc"></p>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-fuchsia-400 mt-6">Web Applications</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<template x-for="app in webApps" :key="app">
|
||||
<label :class="formData.services.apps.includes(app) ? 'border-fuchsia-500 bg-fuchsia-600/20' : 'border-slate-700 bg-slate-900/40'" class="flex flex-col gap-3 p-5 border rounded-xl cursor-pointer transition-all hover:bg-fuchsia-600/20 hover:border-fuchsia-500 group relative">
|
||||
<img :src="'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/' + (serviceMetadata[app].icon || app) + '.svg'" :class="formData.services.apps.includes(app) ? 'opacity-100' : 'opacity-20 group-hover:opacity-100'" class="w-8 h-8 transition-opacity absolute right-4 top-4" :alt="app">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" :value="app" x-model="formData.services.apps" class="w-5 h-5 rounded border-slate-700 text-sky-500 focus:ring-sky-500 bg-slate-800">
|
||||
<span class="text-base font-bold capitalize" x-text="app"></span>
|
||||
</div>
|
||||
<p class="text-sm leading-normal transition-colors" :class="formData.services.apps.includes(app) ? 'text-white' : 'text-slate-500 group-hover:text-white'" x-text="serviceMetadata[app].desc"></p>
|
||||
<a :href="serviceMetadata[app].link" target="_blank" class="text-xs text-sky-400 underline opacity-0 group-hover:opacity-100 transition-opacity mt-auto">Learn more</a>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-fuchsia-400 mt-6">System Services</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<template x-for="sys in ['clamav', 'virtualization']" :key="sys">
|
||||
<label :class="formData.services.system.includes(sys) ? 'border-fuchsia-500 bg-fuchsia-600/20' : 'border-slate-700 bg-slate-900/40'" class="flex flex-col gap-3 p-5 border rounded-xl cursor-pointer transition-all hover:bg-fuchsia-600/20 hover:border-fuchsia-500 group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" :value="sys" x-model="formData.services.system" class="w-5 h-5 rounded border-slate-700 text-sky-500 focus:ring-sky-500 bg-slate-800">
|
||||
<span class="text-base font-bold capitalize" x-text="sys"></span>
|
||||
</div>
|
||||
<img :src="'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/' + (serviceMetadata[sys].icon || sys) + '.svg'" :class="formData.services.system.includes(sys) ? 'opacity-100' : 'opacity-40 group-hover:opacity-100'" class="w-6 h-6 transition-opacity" :alt="sys">
|
||||
</div>
|
||||
<p class="text-sm transition-colors" :class="formData.services.system.includes(sys) ? 'text-white' : 'text-slate-500 group-hover:text-white'" x-text="serviceMetadata[sys].desc"></p>
|
||||
<a :href="serviceMetadata[sys].link" target="_blank" class="text-xs 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 13: Non-Interactive Git Config -->
|
||||
<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">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 14: Setup Type & Restore -->
|
||||
<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">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 15: Tweaks -->
|
||||
<div x-show="step === 15" 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 16: Installation Progress -->
|
||||
<div x-show="step === 16" 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 text-xs">Live Terminal Logs</span>
|
||||
<button class="text-xs bg-slate-800 px-2 py-1 rounded text-slate-400">Toggle Logs</button>
|
||||
</div>
|
||||
<div class="h-40 overflow-y-auto custom-scrollbar">
|
||||
<p class="text-fuchsia-400 text-xs">[INFO] Starting deployment sequence...</p>
|
||||
<p class="text-sky-400 text-xs">[CMD] nixos-anywhere --flake .#numbus-server</p>
|
||||
<p class="text-slate-500 text-xs">[STDOUT] Initializing remote connection...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Final Review (Step 17) -->
|
||||
<div x-show="step === 17" 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,16].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: 16,
|
||||
hardwareData: { disks: [] },
|
||||
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');
|
||||
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,15], minors: [
|
||||
{ label: 'Configuration', step: 13 }
|
||||
]},
|
||||
{ title: 'Installation', steps: [16], minors: [
|
||||
{ label: 'Progress', step: 15 }
|
||||
]}
|
||||
],
|
||||
getNavigation() {
|
||||
const isInteractive = this.formData.deploymentMode === 'interactive';
|
||||
const isServer = this.formData.deviceType === 'server' || this.formData.deviceType === 'backup-server';
|
||||
|
||||
let configMinors = [];
|
||||
if (isInteractive) {
|
||||
configMinors = [
|
||||
{ label: 'Language', step: 5 },
|
||||
{ label: 'Network', step: 6 },
|
||||
{ label: 'Remote Access', step: 7 },
|
||||
{ label: 'Services', step: 8 },
|
||||
{ label: 'Security', step: 9 },
|
||||
{ label: 'Users & Groups', step: 10 }
|
||||
];
|
||||
// Only servers have alerting/mail config usually
|
||||
if (isServer) {
|
||||
configMinors.push({ label: 'Alerts', step: 11 });
|
||||
}
|
||||
if (isServer) {
|
||||
configMinors.push({ label: 'Disks', step: 12 });
|
||||
}
|
||||
} else {
|
||||
configMinors = [
|
||||
{ label: 'Source Repo', step: 13 },
|
||||
{ label: 'Setup Type', step: 14 },
|
||||
{ label: 'Environment Tweaks', step: 15 }
|
||||
];
|
||||
}
|
||||
|
||||
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, 15],
|
||||
minors: configMinors
|
||||
},
|
||||
{
|
||||
title: 'Finalize',
|
||||
steps: [16, 17],
|
||||
minors: [{ label: 'Deployment Status', step: 16 }]
|
||||
}
|
||||
];
|
||||
},
|
||||
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: ''
|
||||
},
|
||||
disks: {
|
||||
boot: [],
|
||||
data: []
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
sortedDisks() {
|
||||
const order = { 'NVMe': 0, 'SSD': 1, 'HDD': 2, 'USB': 3, 'Other': 4 };
|
||||
return [...this.hardwareData.disks].sort((a, b) => {
|
||||
return (order[a.type] ?? 5) - (order[b.type] ?? 5);
|
||||
});
|
||||
},
|
||||
getDiskIcon(type) {
|
||||
switch(type) {
|
||||
case 'NVMe': return 'mdi-flash';
|
||||
case 'SSD': return 'mdi-memory';
|
||||
case 'HDD': return 'mdi-harddisk';
|
||||
case 'USB': return 'mdi-usb';
|
||||
default: return 'mdi-harddisk';
|
||||
}
|
||||
},
|
||||
toggleDisk(id, category) {
|
||||
const list = this.formData.disks[category];
|
||||
const index = list.indexOf(id);
|
||||
if (index > -1) {
|
||||
list.splice(index, 1);
|
||||
} else {
|
||||
if (category === 'boot' && list.length >= 2) return;
|
||||
if (category === 'data' && list.length >= 9) return;
|
||||
list.push(id);
|
||||
}
|
||||
},
|
||||
addGroup() {
|
||||
const name = this.newGroupName.trim().toLowerCase().replaceAll(' ', '_');
|
||||
if (name && !this.formData.groups[name]) {
|
||||
this.formData.groups[name] = [];
|
||||
this.newGroupName = '';
|
||||
}
|
||||
},
|
||||
addUser() {
|
||||
if (this.formData.users.length < 20) {
|
||||
this.formData.users.push({ username: '', name: '', email: '', groups: ['family'], isStatic: false });
|
||||
}
|
||||
},
|
||||
removeUser(index) {
|
||||
if (this.formData.users.length > 1 && !this.formData.users[index].isStatic) {
|
||||
this.formData.users.splice(index, 1);
|
||||
}
|
||||
},
|
||||
validators: {
|
||||
ip: (val) => /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(val),
|
||||
email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
|
||||
domain: (val) => /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(val),
|
||||
port: (val) => /^\d{2,5}$/.test(val),
|
||||
netbirdToken: (val) => val.startsWith('nbp_')
|
||||
},
|
||||
updateMinorSteps() {
|
||||
// Dynamically set steps for navigation
|
||||
const configGroup = this.navigation.find(n => n.title === 'Server Configuration' || n.title === 'Configuration');
|
||||
if (this.formData.deploymentMode === 'interactive') {
|
||||
configGroup.title = 'Configuration';
|
||||
configGroup.minors = [
|
||||
{ label: 'Language', step: 5 },
|
||||
{ label: 'Network', step: 6 },
|
||||
{ label: 'Remote Access', step: 7 },
|
||||
{ label: 'Services', step: 8 },
|
||||
{ label: 'Security', step: 9 },
|
||||
{ label: 'Users', step: 10 },
|
||||
{ label: 'Alerts', step: 11 }
|
||||
];
|
||||
} else {
|
||||
configGroup.title = 'Git Configuration';
|
||||
configGroup.minors = [
|
||||
{ label: 'Source Repo', step: 12 },
|
||||
{ label: 'Setup Type', step: 13 },
|
||||
{ label: 'Tweaks', step: 14 }
|
||||
];
|
||||
}
|
||||
},
|
||||
updateNavigation() {
|
||||
this.updateMinorSteps();
|
||||
},
|
||||
async startDiscovery() {
|
||||
this.updateNavigation();
|
||||
this.step = 4; // Move to waiting room
|
||||
try {
|
||||
await fetch('/discovery', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
device: this.formData.deviceType,
|
||||
live_ip: this.formData.network.live_target_ip,
|
||||
live_password: this.formData.network.live_target_password
|
||||
})
|
||||
});
|
||||
this.pollHardware();
|
||||
} catch (e) {
|
||||
console.error("Bridge Error:", e);
|
||||
}
|
||||
},
|
||||
pollHardware() {
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetch('/hardware.json');
|
||||
if (res.ok) {
|
||||
this.hardwareData = await res.json();
|
||||
clearInterval(timer);
|
||||
this.step = (this.formData.deploymentMode === 'interactive') ? 5 : 13;
|
||||
}
|
||||
} catch (e) { /* Bridge might not have written file yet */ }
|
||||
}, 2000);
|
||||
},
|
||||
canGoNext() {
|
||||
const rules = {
|
||||
0: () => true,
|
||||
1: () => !!this.formData.deploymentMode,
|
||||
2: () => !!this.formData.deviceType,
|
||||
3: () => this.validators.ip(this.formData.network.live_target_ip) && !!this.formData.network.live_target_password,
|
||||
4: () => false,
|
||||
5: () => !!(this.formData.language.lang && this.formData.language.locale && this.formData.language.timezone),
|
||||
6: () => this.validators.ip(this.formData.network.ip_address) && this.validators.ip(this.formData.network.router_ip),
|
||||
7: () => this.formData.remote_access.provider !== 'netbird-cloud' || (this.formData.remote_access.netbird_token && this.validators.netbirdToken(this.formData.remote_access.netbird_token)),
|
||||
8: () => this.validators.domain(this.formData.ssl.domain),
|
||||
9: () => this.formData.security.ssh_keys.trim().length > 10 && this.formData.ssl.cloudflare_token.length > 10,
|
||||
10: () => !!(this.formData.users[0].username && this.formData.users[0].name && this.validators.email(this.formData.users[0].email)),
|
||||
11: () => this.validators.domain(this.formData.mail.smtp_host) && this.validators.port(this.formData.mail.smtp_port),
|
||||
12: () => this.formData.disks.boot.length >= 1,
|
||||
13: () => this.formData.gitConfig.url.length > 5,
|
||||
14: () => !this.formData.restoreConfig.restore_data || (this.formData.restoreConfig.backup_host && this.formData.restoreConfig.setup_key),
|
||||
15: () => true,
|
||||
16: () => 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>
|
||||
@@ -1,14 +1,15 @@
|
||||
#!/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 yq python3
|
||||
|
||||
cleanup() {
|
||||
rm -${DIR_RM_FLAGS} /run/numbus/logs
|
||||
rm -${DIR_RM_FLAGS} /run/numbus/web
|
||||
rm -${DIR_RM_FLAGS} /run/numbus/config
|
||||
|
||||
kill ${BRIDGE_PID}
|
||||
}
|
||||
|
||||
launch_configurator() {
|
||||
local PORT=8088
|
||||
local CONFIG_FILE="numbus.yaml"
|
||||
local BRIDGE_SCRIPT="configurator/bridge.py"
|
||||
|
||||
# Cleanup old signals
|
||||
rm -${FILES_RM_FLAGS} configurator/.discovery_ready configurator/.deploy_signal configurator/live_settings.json configurator/hardware.json
|
||||
|
||||
echo -e "\n 🚀 Launching Numbus Configurator..."
|
||||
python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 &
|
||||
export BRIDGE_PID=$!
|
||||
@@ -17,37 +18,70 @@ launch_configurator() {
|
||||
xdg-open "http://localhost:${PORT}" 2>/dev/null || open "http://localhost:${PORT}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
preparation() {
|
||||
SELECTED_DEVICE_TYPE=$(gum choose --header "Choose the device you want to deploy :" \
|
||||
"Numbus Server : Professional-grade hosting, strictly kept under your roof." \
|
||||
"Numbus Backup Server : Automated, high-efficiency protection for your entire ecosystem." \
|
||||
"Numbus Computer : A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat." \
|
||||
"Numbus TV : A premium cinematic experience free from trackers and forced subscriptions.")
|
||||
|
||||
SELECTED_DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \
|
||||
"Interactive : You don't already have a configuration." \
|
||||
"Non-interactive : You have a valid configuration hosted on a Git platform.")
|
||||
|
||||
git_url() {
|
||||
IMPORTED_CONFIG_URL=$(gum input --placeholder "https://yourgitplatform.tld/your-user/repo-containing-the-configuration" --header "Please provide the URL to the git repository containing your configuration :")
|
||||
}
|
||||
|
||||
git_url
|
||||
|
||||
until git clone "${IMPORTED_CONFIG_URL}" imported_configuration; do
|
||||
echo -e "\n ⚠️ This did not work correctly."
|
||||
|
||||
echo -e "\n Is this URL correct [y/n] ? ${IMPORTED_CONFIG_URL}"
|
||||
read URL
|
||||
|
||||
if [[ "${URL^^}" == "N" ]];
|
||||
git_url
|
||||
fi
|
||||
|
||||
echo -e "\n You will be prompted for your credentials again. Make sure that they are correct."
|
||||
done
|
||||
}
|
||||
|
||||
hierarchy_preparation() {
|
||||
if [[ -e configuration/etc/nixos/configuration.nix || -e configuration/home/numbus-admin/.ssh/id_ed25519 ]]; then
|
||||
mkdir -${MKDIR_FLAGS} .trash/
|
||||
echod "\n 🔄 Preparing the folder hierarchy for the final configuration..."
|
||||
|
||||
if [[ -e config/* ]]; then
|
||||
echo " ⚠️ It seems you have already run this script. Previously generated files need to be cleaned up."
|
||||
mv -${MV_FLAGS} configuration/{.,}* .trash/"$(date)_cleanup"/
|
||||
echo " ✅ Your files have been moved to the .trash directory. You can retrieve them there if needed."
|
||||
OLD_CONFIG_PATH="trash/$(date +"%Y-%m-%d-%Hh%M")/"
|
||||
mkdir -${MKDIR_FLAGS} ${OLD_CONFIG_PATH}
|
||||
mv -${MV_FLAGS} config/ ${OLD_CONFIG_PATH}
|
||||
echo " ✅ Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed."
|
||||
fi
|
||||
|
||||
mkdir -${MKDIR_FLAGS} configuration/home/numbus-admin/.ssh/
|
||||
mkdir -${MKDIR_FLAGS} configuration/etc/nixos/secrets/podman
|
||||
mkdir -${MKDIR_FLAGS} configuration/etc/nixos/secrets/system
|
||||
mkdir -${MKDIR_FLAGS} configuration/etc/nixos/secrets/disks
|
||||
mkdir -${MKDIR_FLAGS} configuration/var/lib/sops-nix/
|
||||
mkdir -${MKDIR_FLAGS} configuration/var/lib/numbus-server/
|
||||
|
||||
# Secrets
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/var/lib/sops-nix/
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/disks
|
||||
if [[ "${SELECTED_DEVICE_TYPE}" == "" ]]; then
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system
|
||||
fi
|
||||
mkdir -${MKDIR_FLAGS} to-keep-preciously/
|
||||
}
|
||||
|
||||
setup_ssh() {
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ✅ Generating new SSH key for numbus-admin..."
|
||||
fi
|
||||
echod "\n ✅ Generating new SSH key for numbus-admin..."
|
||||
|
||||
chmod 700 configuration/home/numbus-admin/.ssh/
|
||||
ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "configuration/home/numbus-admin/.ssh/id_ed25519" -N "" -q
|
||||
chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/
|
||||
ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ➡️ Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..."
|
||||
fi
|
||||
|
||||
if sshpass -p "${LIVE_TARGET_PASSWD}" ssh-copy-id -o StrictHostKeyChecking=no -i "configuration/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then
|
||||
if sshpass -p "${LIVE_TARGET_PASSWD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ✅ SSH key copied successfully"
|
||||
fi
|
||||
@@ -59,7 +93,7 @@ setup_ssh() {
|
||||
|
||||
ssh_to_host() {
|
||||
local COMMAND="${1}"
|
||||
ssh -i "configuration/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}"
|
||||
ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}"
|
||||
}
|
||||
|
||||
hardware_detection() {
|
||||
@@ -152,7 +186,7 @@ done
|
||||
SSHEND
|
||||
### Get hardware information <--
|
||||
|
||||
scp -i "configuration/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
|
||||
scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
|
||||
source "${TMPFILE}"
|
||||
|
||||
### Transform the bash variables into JSON -->
|
||||
@@ -185,11 +219,11 @@ SSHEND
|
||||
name: .[$i], path: .[$i+1], type: .[$i+2], health: .[$i+3], id: .[$i+4], size: .[$i+5]
|
||||
}
|
||||
]
|
||||
}' --args "${DISK_FLAT_ARRAY[@]:-}" > configurator/hardware.json
|
||||
}' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH}
|
||||
### Transform the bash variables into JSON <--
|
||||
|
||||
### --> Generate hardware-configuration.nix
|
||||
if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > configuration/etc/nixos/hardware-configuration.nix; then
|
||||
if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > ${EXTRA_FILES_PATH}/etc/nixos/hardware-configuration.nix; then
|
||||
echo -e "\n✅ Hardware configuration generated"
|
||||
else
|
||||
echo -e "\n❌ Failed to generate hardware configuration"
|
||||
@@ -402,8 +436,8 @@ disk_config_generation() {
|
||||
keys_generation() {
|
||||
for i in $(seq 1 "${#BOOT_DISKS_ID_LIST[@]}"); do
|
||||
PASS="$(xkcdpass)"
|
||||
echo -n "$PASS" > "configuration/etc/secrets/disks/boot-${i}"
|
||||
chmod 600 "configuration/etc/secrets/disks/boot-${i}"
|
||||
echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWD" | sudo -S mkdir -p /etc/secrets/disks/
|
||||
echo "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}"
|
||||
@@ -412,8 +446,8 @@ EOF
|
||||
done
|
||||
for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do
|
||||
PASS="$(xkcdpass)"
|
||||
echo -n "$PASS" > "configuration/etc/secrets/disks/content-${i}"
|
||||
chmod 600 "configuration/etc/secrets/disks/content-${i}"
|
||||
echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}"
|
||||
echo "$LIVE_TARGET_PASSWD" | sudo -S chmod 600 /etc/secrets/disks/content-${i}
|
||||
@@ -421,8 +455,8 @@ EOF
|
||||
done
|
||||
for i in $(seq 1 "$PARITY_DISK_NUMBER"); do
|
||||
PASS="$(xkcdpass)"
|
||||
echo -n "$PASS" > "configuration/etc/secrets/disks/parity-${i}"
|
||||
chmod 600 "configuration/etc/secrets/disks/parity-${i}"
|
||||
echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}"
|
||||
echo "$LIVE_TARGET_PASSWD" | sudo -S chmod 600 /etc/secrets/disks/parity-${i}
|
||||
@@ -440,18 +474,18 @@ EOF
|
||||
export SSH_KEYS_FORMATTED
|
||||
|
||||
echo -e "\n ✅ Generating sops-nix keys..."
|
||||
ssh-to-age -private-key -i configuration/home/numbus-admin/.ssh/id_ed25519 > configuration/var/lib/sops-nix/key.txt
|
||||
export SOPS_PUBLIC_KEY=$(age-keygen -y configuration/var/lib/sops-nix/key.txt)
|
||||
ssh-to-age -private-key -i ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt
|
||||
export SOPS_PUBLIC_KEY=$(age-keygen -y ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt)
|
||||
|
||||
echo -e "\n ✅ Generating sops-nix configuration files..."
|
||||
envsubst < templates/nix-config/sops-nix/.sops.yaml > configuration/etc/nixos/.sops.yaml
|
||||
envsubst < templates/nix-config/sops-nix/.sops.yaml > ${EXTRA_FILES_PATH}/etc/nixos/.sops.yaml
|
||||
|
||||
echo -e "\n ✅ Encrypting secrets in the correct file..."
|
||||
envsubst < "templates/nix-config/sops-nix/secrets.yaml" \
|
||||
| sops encrypt --filename-override secrets.yaml \
|
||||
--input-type yaml --output-type yaml \
|
||||
--age $SOPS_PUBLIC_KEY \
|
||||
--output configuration/etc/nixos/secrets/secrets.yaml
|
||||
--output ${EXTRA_FILES_PATH}/etc/nixos/secrets/secrets.yaml
|
||||
}
|
||||
|
||||
sum_up() {
|
||||
@@ -511,18 +545,18 @@ Please save the following secrets to a secure place (i.e. your local password ma
|
||||
|
||||
**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :**
|
||||
|
||||
* **Disk 1 Secret Key :** \`$( cat configuration/etc/secrets/disks/boot-1 )\`
|
||||
$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Disk 2 secret key :** \`$( cat configuration/etc/secrets/disks/boot-2 )\`" )
|
||||
* **Disk 1 Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-1 )\`
|
||||
$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Disk 2 secret key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-2 )\`" )
|
||||
|
||||
**Data Disks ($CONTENT_DISK_NUMBER):**
|
||||
|
||||
$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" )
|
||||
$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat configuration/etc/secrets/disks/content-${j} )\`" && j=$((j + 1)); done )
|
||||
$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/content-${j} )\`" && j=$((j + 1)); done )
|
||||
|
||||
**Parity Disks ($PARITY_DISK_NUMBER):**
|
||||
|
||||
$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" )
|
||||
$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat configuration/etc/secrets/disks/parity-${j} )\`" && j=$((j + 1)); done )
|
||||
$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${j} )\`" && j=$((j + 1)); done )
|
||||
|
||||
EOF
|
||||
)
|
||||
@@ -532,7 +566,7 @@ EOF
|
||||
|
||||
gum confirm "➡️ Would you like to manually edit the configuration (⚠️ advanced users only)" || { echo -e "\n\n✅ continuing with the installation..."; return 0; }
|
||||
|
||||
nano configuration/etc/nixos/configuration.nix
|
||||
nano ${EXTRA_FILES_PATH}/etc/nixos/configuration.nix
|
||||
}
|
||||
|
||||
cloudflare_dns_setup() {
|
||||
@@ -637,9 +671,9 @@ cloudflare_dns_setup() {
|
||||
}
|
||||
|
||||
export_configuration() {
|
||||
cp -${FILES_CP_FLAGS} deploy.conf configuration/var/lib/numbus-server/numbus-server.conf
|
||||
cp -${FILES_CP_FLAGS} deploy.conf ${EXTRA_FILES_PATH}/var/lib/numbus-server/numbus-server.conf
|
||||
|
||||
local CONFIG_EXPORT_DIR="configuration/var/lib/numbus-server/"
|
||||
local CONFIG_EXPORT_DIR="${EXTRA_FILES_PATH}/var/lib/numbus-server/"
|
||||
local CONFIG_EXPORT_FILE="${CONFIG_EXPORT_DIR}/numbus-server.conf"
|
||||
|
||||
echo "export TARGET_INTERFACE=\"${TARGET_INTERFACE}\"" >> $CONFIG_EXPORT_FILE
|
||||
@@ -658,15 +692,15 @@ export_configuration() {
|
||||
}
|
||||
|
||||
deploy() {
|
||||
git -C . add -f "configuration/"
|
||||
git -C . add -f "${EXTRA_FILES_PATH}/"
|
||||
git -C . add -f "templates/"
|
||||
git -C . add -f "deploy.conf"
|
||||
|
||||
echo -e "\n\n🔄 Deploying to the remote server..."
|
||||
nix flake update --flake ./configuration/etc/nixos
|
||||
nix flake update --flake ./${EXTRA_FILES_PATH}/etc/nixos
|
||||
nix run github:nix-community/nixos-anywhere -- \
|
||||
--flake ./configuration/etc/nixos#numbus-server \
|
||||
--extra-files configuration \
|
||||
--flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server \
|
||||
--extra-files ${EXTRA_FILES_PATH} \
|
||||
--chown "/home/numbus-admin/" 1000:1000 \
|
||||
--target-host ${TARGET_USER}@${LIVE_TARGET_IP}
|
||||
|
||||
@@ -714,7 +748,7 @@ postrun_action() {
|
||||
Do you want to enable automatic disk decryption on boot ?"
|
||||
|
||||
if gum confirm "➡️ I understand, 'yes' to proceed."; then
|
||||
sshpass -p "${LIVE_TARGET_PASSWD}" ssh -i "configuration/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF
|
||||
sshpass -p "${LIVE_TARGET_PASSWD}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF
|
||||
echo "Enrolling boot disk key to TPM..."
|
||||
|
||||
BOOT_DISKS_NAME=(${BOOT_DISKS_NAME[@]})
|
||||
@@ -762,45 +796,29 @@ nix_update() {
|
||||
echo -e "\n\n🔄 Updating NixOS on the remote server..."
|
||||
|
||||
nixos-rebuild --target-host numbus-admin@${LIVE_TARGET_IP} \
|
||||
--use-remote-sudo switch --flake configuration/etc/nixos#numbus-server
|
||||
--use-remote-sudo switch --flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
kill ${BRIDGE_PID}
|
||||
echod() {
|
||||
MESSAGE=${1}
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e ${MESSAGE}
|
||||
fi
|
||||
}
|
||||
|
||||
set -euo pipefail
|
||||
# --- DEFAULTS --->
|
||||
WEBSERVER_PORT=${WEBSERVER_PORT:-8088}
|
||||
|
||||
clear
|
||||
LIVE_DATA_PATH="/run/numbus/web/live_settings.json"
|
||||
HARDWARE_DATA_PATH="/run/numbus/web/hardware.json"
|
||||
|
||||
echo "
|
||||
██████ █████ █████
|
||||
▒▒██████ ▒▒███ ▒▒███
|
||||
▒███▒███ ▒███ █████ ████ █████████████ ▒███████ █████ ████ █████
|
||||
▒███▒▒███▒███ ▒▒███ ▒▒███ ▒▒███▒▒███▒▒███ ▒███▒▒███▒▒███ ▒▒███ ███▒▒
|
||||
▒███ ▒▒██████ ▒███ ▒▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒███ ▒▒█████
|
||||
▒███ ▒▒█████ ▒███ ▒▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒███ ▒▒▒▒███
|
||||
█████ ▒▒█████ ▒▒████████ █████▒███ █████ ████████ ▒▒████████ ██████
|
||||
▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒
|
||||
BRIDGE_SCRIPT="web/logic/bridge.py"
|
||||
CONFIG_FILE="config/numbus.yaml"
|
||||
|
||||
█████████
|
||||
███▒▒▒▒▒███
|
||||
▒███ ▒▒▒ ██████ ████████ █████ █████ ██████ ████████
|
||||
▒▒█████████ ███▒▒███▒▒███▒▒███▒▒███ ▒▒███ ███▒▒███▒▒███▒▒███
|
||||
▒▒▒▒▒▒▒▒███▒███████ ▒███ ▒▒▒ ▒███ ▒███ ▒███████ ▒███ ▒▒▒
|
||||
███ ▒███▒███▒▒▒ ▒███ ▒▒███ ███ ▒███▒▒▒ ▒███
|
||||
▒▒█████████ ▒▒██████ █████ ▒▒█████ ▒▒██████ █████
|
||||
▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒
|
||||
"
|
||||
TARGET_USER="nixos"
|
||||
|
||||
sleep 1
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- MAIN ORCHESTRATION LOOP ---
|
||||
|
||||
# 0. TESTING ONLY
|
||||
DEBUG=1
|
||||
EXTRA_FILES_PATH="/run/numbus/config"
|
||||
|
||||
if [[ ${DEBUG-0} -eq 1 ]]; then
|
||||
FILES_CP_FLAGS="vau"
|
||||
@@ -816,31 +834,56 @@ else
|
||||
MKDIR_FLAGS="p"
|
||||
MV_FLAGS="u"
|
||||
fi
|
||||
# --- DEFAULTS ---<
|
||||
|
||||
# 1. Initialize the UI and Bridge
|
||||
launch_configurator
|
||||
# --- MAIN PART ---
|
||||
set -euo pipefail
|
||||
|
||||
# 2. Prepare the directories
|
||||
hierarchy_preparation
|
||||
clear
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
echo """
|
||||
_ ____ ____ ______ __ ______
|
||||
/ |/ / / / / |/ / _ )/ / / / __/
|
||||
/ / /_/ / /|_/ / _ / /_/ /\ \
|
||||
/_/|_/\____/_/ /_/____/\____/___/
|
||||
|
||||
"""
|
||||
|
||||
DEPLOY_MODE=$(gum choose --header "Choose your preferred configuration interface :" "Through my browser (Recommended for beginners)" "Through my terminal (TUI)")
|
||||
|
||||
if [[ "$DEPLOY_MODE" == "Through my terminal" ]]; then
|
||||
preparation
|
||||
hierarchy_preparation
|
||||
setup_ssh
|
||||
|
||||
else
|
||||
launch_configurator
|
||||
hierarchy_preparation
|
||||
echod "\n ⏳ Waiting for device credentials from web UI..."
|
||||
while [ ! -f configurator/.discovery_ready ]; do
|
||||
sleep 5
|
||||
done
|
||||
echod "\n ✅ Credentials received."
|
||||
LANGUAGE=$(jq -r '.language' ${LIVE_DATA_PATH})
|
||||
COUNTRY=$(jq -r '.country' ${LIVE_DATA_PATH})
|
||||
TIMEZONE=$(jq -r '.timeZone' ${LIVE_DATA_PATH})
|
||||
DEVICE_TYPE=$(jq -r '.device' ${LIVE_DATA_PATH})
|
||||
DEPLOYMENT_MODE=$(jq -r '.deploymentMode' ${LIVE_DATA_PATH})
|
||||
if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then
|
||||
REPLICATION_HARDWARE=$(jq -r '.replicationHardware' ${LIVE_DATA_PATH})
|
||||
REPLICATION_STRATEGY=$(jq -r '.replicationStrategy' ${LIVE_DATA_PATH})
|
||||
REPLICATION_SECRETS=$(jq -r '.replicationSecrets' ${LIVE_DATA_PATH})
|
||||
fi
|
||||
LIVE_IP=$(jq -r '.liveIp' ${LIVE_DATA_PATH})
|
||||
LIVE_PASSWORD=$(jq -r '.livePassword' ${LIVE_DATA_PATH})
|
||||
|
||||
# 3. Wait for Live Credentials from the Website
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ⏳ Waiting for device credentials from web UI..."
|
||||
fi
|
||||
|
||||
while [ ! -f configurator/.discovery_ready ]; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ✅ Credentials received."
|
||||
fi
|
||||
|
||||
# 3. Load Credentials and run Discovery
|
||||
DEVICE_TYPE=$(jq -r '.device' configurator/live_settings.json)
|
||||
LIVE_TARGET_IP=$(jq -r '.live_ip' configurator/live_settings.json)
|
||||
LIVE_TARGET_PASSWD=$(jq -r '.live_password' configurator/live_settings.json)
|
||||
TARGET_USER="nixos" # Default for NixOS ISO
|
||||
|
||||
|
||||
setup_ssh
|
||||
hardware_detection
|
||||
@@ -860,4 +903,4 @@ done
|
||||
|
||||
# 5. Execute Deployment
|
||||
echo -e "\n🚀 Starting deployment sequence..."
|
||||
deploy | tee configurator/deploy.log
|
||||
deploy > deploy-out.log 2> deploy-err.log
|
||||
@@ -0,0 +1,49 @@
|
||||
<!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">
|
||||
<link rel="icon" href="./media/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<style>
|
||||
@keyframes pulse-slow {
|
||||
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 20px rgba(14, 165, 233, 0.2)); }
|
||||
50% { transform: scale(1.03); filter: drop-shadow(0 0 40px rgba(192, 38, 211, 0.4)); }
|
||||
}
|
||||
.animate-pulse-slow { animation: pulse-slow 6s infinite ease-in-out; }
|
||||
.text-shadow-glow { text-shadow: 0 0 15px rgba(56, 189, 248, 0.4); }
|
||||
</style>
|
||||
|
||||
<body class="bg-[#0f172a] text-slate-100 min-h-screen font-sans selection:bg-fuchsia-500/30">
|
||||
|
||||
<!-- Welcome screen -->
|
||||
<div class="p-12 justify-center flex flex-col items-center">
|
||||
<img class="w-64 h-64 auto drop-shadow-2xl animate-pulse-slow" src="./media/logo.png" alt="Numbus Logo">
|
||||
<h1 class="p-8 text-center text-6xl font-extrabold tracking-tight">Welcome to <span class="text-sky-400 animate-pulse-slow text-shadow-glow">NUMBUS</span></h1>
|
||||
<p class="text-center text-2xl p-2">Transform your device into a <strong>secure, reliable and private</strong> appliance <br> using the power of open-soure software.</p>
|
||||
<p class="text-center text-2xl p-2 pb-10">You will be <strong>guided</strong> through the configuration process.</p>
|
||||
|
||||
<div class="bg-amber-500/20 border border-amber-500/30 p-4 rounded-xl flex gap-5 text-left items-center max-w-3xl mx-auto">
|
||||
<span class="bg-amber-500 rounded-full p-1 px-2 shrink-0">
|
||||
<i class="mdi mdi-shield-lock text-slate-900 text-2xl"></i>
|
||||
</span>
|
||||
<p class="text-xl text-amber-200/90 italic"><strong>Privacy First:</strong> No data entered here ever leaves your device. <br> This configurator runs entirely locally in your browser and is fully private.
|
||||
</div>
|
||||
|
||||
<span class="animate-pulse-slow drop-shadow-2xl p-12">
|
||||
<a class="text-shadow-glow 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" href="./pages/preparation.html">Get Started</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Use a memory-backed path for temporary secrets if available, else local
|
||||
SECRET_PATH = "/run/user/{}/numbus".format(os.getuid()) if os.path.exists("/run/user/{}".format(os.getuid())) else "."
|
||||
os.makedirs(SECRET_PATH, exist_ok=True)
|
||||
|
||||
class BridgeHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
# Route for logs: /logs?type=out or /logs?type=err
|
||||
if self.path.startswith('/logs'):
|
||||
log_type = "out" if "type=err" not in self.path else "err"
|
||||
log_file = f'deploy-{log_type}.log'
|
||||
|
||||
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(log_file):
|
||||
with open(log_file, 'r') as f:
|
||||
# Read last 50 lines for better context during errors
|
||||
self.wfile.write("".join(f.readlines()[-50:]).encode())
|
||||
return
|
||||
return http.server.SimpleHTTPRequestHandler.do_GET(self)
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length)
|
||||
|
||||
if self.path == '/discovery':
|
||||
# Store secrets in memory-backed filesystem
|
||||
with open(os.path.join(SECRET_PATH, "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")
|
||||
|
||||
elif self.path == '/deploy':
|
||||
with open("../numbus.yaml", "wb") as f:
|
||||
f.write(post_data)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
with open(".deploy_signal", "w") as f: f.write("1")
|
||||
|
||||
os.chdir("configurator")
|
||||
http.server.HTTPServer(('localhost', 8088), BridgeHandler).serve_forever()
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-backup-server-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 382.85714,316.05715 a 77.142857,77.142857 0 0 0 -77.14285,77.14286 H 280 l 34.28572,34.28571 34.28571,-34.28571 h -25.71429 a 60,60 0 0 1 60,-60 60,60 0 0 1 60,60 60,60 0 0 1 -60,59.99998 c -12.85714,0 -24.94285,-4.28571 -34.8,-11.14285 L 335.71428,454.4 c 13.20001,9.94285 29.48572,15.94285 47.14286,15.94285 A 77.142857,77.142857 0 0 0 460,393.20001 77.142857,77.142857 0 0 0 382.85714,316.05715 M 400,393.20001 a 17.142857,17.142857 0 0 0 -17.14286,-17.14286 17.142857,17.142857 0 0 0 -17.14285,17.14286 17.142857,17.142857 0 0 0 17.14285,17.14285 A 17.142857,17.142857 0 0 0 400,393.20001 Z"
|
||||
id="path1-5"
|
||||
style="fill:#ffffff;stroke-width:8.57143" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-computer-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 310,348.2 h 120 v 75 H 310 m 120,15 a 15,15 0 0 0 15,-15 v -75 c 0,-8.325 -6.75,-15 -15,-15 H 310 c -8.325,0 -15,6.675 -15,15 v 75 a 15,15 0 0 0 15,15 h -30 v 15 h 180 v -15 z"
|
||||
id="path1-2"
|
||||
style="stroke-width:7.5;fill:#ffffff" /></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-server-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 377.25,436.7 h 7.25 a 7.25,7.25 0 0 1 7.25,7.25 h 50.75 v 14.5 h -50.75 a 7.25,7.25 0 0 1 -7.25,7.25 h -29 a 7.25,7.25 0 0 1 -7.25,-7.25 H 297.5 v -14.5 h 50.75 a 7.25,7.25 0 0 1 7.25,-7.25 h 7.25 V 422.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,378.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,422.2 h -50.75 v 14.5 M 312,320.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,364.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,320.7 m 36.25,29 h 7.25 v -14.5 h -7.25 v 14.5 m 0,58 h 7.25 v -14.5 h -7.25 v 14.5 m -29,-72.5 v 14.5 h 14.5 v -14.5 h -14.5 m 0,58 v 14.5 h 14.5 v -14.5 z"
|
||||
id="path1-6"
|
||||
style="fill:#ffffff;stroke-width:7.25" /></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-dark-template.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.2326894"
|
||||
inkscape:cx="261.21747"
|
||||
inkscape:cy="258.78377"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-tv-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:33.8362;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 318.5733,405.71938 v -38.9116 H 299.28665 280 V 354.28838 341.769 h 54.65141 54.65141 l 9.69089,29.09912 c 5.32999,16.00451 9.79503,29.09911 9.92233,29.09911 0.12729,0 4.59234,-13.0946 9.92232,-29.09911 L 428.52925,341.769 H 444.269 c 8.65686,0 15.73581,0.0761 15.731,0.16918 -0.005,0.0931 -8.63713,23.16087 -19.18293,51.26182 l -19.17419,51.09264 H 408.9696 396.29631 l -14.33924,-38.57326 -14.33925,-38.57324 -12.00286,-0.18479 -12.00285,-0.1848 v 38.92722 38.92723 h -12.5194 -12.51941 z"
|
||||
id="path2" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-backup-server-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 382.85714,316.05715 a 77.142857,77.142857 0 0 0 -77.14285,77.14286 H 280 l 34.28572,34.28571 34.28571,-34.28571 h -25.71429 a 60,60 0 0 1 60,-60 60,60 0 0 1 60,60 60,60 0 0 1 -60,59.99998 c -12.85714,0 -24.94285,-4.28571 -34.8,-11.14285 L 335.71428,454.4 c 13.20001,9.94285 29.48572,15.94285 47.14286,15.94285 A 77.142857,77.142857 0 0 0 460,393.20001 77.142857,77.142857 0 0 0 382.85714,316.05715 M 400,393.20001 a 17.142857,17.142857 0 0 0 -17.14286,-17.14286 17.142857,17.142857 0 0 0 -17.14285,17.14286 17.142857,17.142857 0 0 0 17.14285,17.14285 A 17.142857,17.142857 0 0 0 400,393.20001 Z"
|
||||
id="path1-5"
|
||||
style="fill:#000000;stroke-width:8.57143" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-computer-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 310,348.2 h 120 v 75 H 310 m 120,15 a 15,15 0 0 0 15,-15 v -75 c 0,-8.325 -6.75,-15 -15,-15 H 310 c -8.325,0 -15,6.675 -15,15 v 75 a 15,15 0 0 0 15,15 h -30 v 15 h 180 v -15 z"
|
||||
id="path1-2"
|
||||
style="stroke-width:7.5;fill:#000000" /></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-server-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 377.25,436.7 h 7.25 a 7.25,7.25 0 0 1 7.25,7.25 h 50.75 v 14.5 h -50.75 a 7.25,7.25 0 0 1 -7.25,7.25 h -29 a 7.25,7.25 0 0 1 -7.25,-7.25 H 297.5 v -14.5 h 50.75 a 7.25,7.25 0 0 1 7.25,-7.25 h 7.25 V 422.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,378.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,422.2 h -50.75 v 14.5 M 312,320.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,364.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,320.7 m 36.25,29 h 7.25 v -14.5 h -7.25 v 14.5 m 0,58 h 7.25 v -14.5 h -7.25 v 14.5 m -29,-72.5 v 14.5 h 14.5 v -14.5 h -14.5 m 0,58 v 14.5 h 14.5 v -14.5 z"
|
||||
id="path1-6"
|
||||
style="fill:#000000;stroke-width:7.25" /></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-dark-template.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.2326894"
|
||||
inkscape:cx="261.21747"
|
||||
inkscape:cy="258.78377"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-tv-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:33.8362;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 318.5733,405.71938 v -38.9116 H 299.28665 280 V 354.28838 341.769 h 54.65141 54.65141 l 9.69089,29.09912 c 5.32999,16.00451 9.79503,29.09911 9.92233,29.09911 0.12729,0 4.59234,-13.0946 9.92232,-29.09911 L 428.52925,341.769 H 444.269 c 8.65686,0 15.73581,0.0761 15.731,0.16918 -0.005,0.0931 -8.63713,23.16087 -19.18293,51.26182 l -19.17419,51.09264 H 408.9696 396.29631 l -14.33924,-38.57326 -14.33925,-38.57324 -12.00286,-0.18479 -12.00285,-0.1848 v 38.92722 38.92723 h -12.5194 -12.51941 z"
|
||||
id="path2" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,145 @@
|
||||
<!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">
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="../media/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
|
||||
|
||||
<body x-data="setupNavigation()" class="p-4 bg-[#0f172a] text-slate-100 min-h-screen font-sans selection:bg-fuchsia-500/30">
|
||||
|
||||
<script>
|
||||
function setupNavigation() {
|
||||
return {
|
||||
step: 1,
|
||||
goToPrevStep() {
|
||||
this.step--;
|
||||
},
|
||||
goToNextStep() {
|
||||
this.step++;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="bg-[#1e293b] border border-slate-700 rounded-2xl relative">
|
||||
<div class="mx-auto relative flex h-16 items-center justify-between">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center">
|
||||
<a href="https://numbus.eu"><img class="w-auto h-10 pr-5 pl-8" src="../media/logo.png" aria-label="The numbus logo"></a>
|
||||
<a class="font-bold text-2xl tracking-tight bg-gradient-to-r from-sky-400 to-fuchsia-500 bg-clip-text text-transparent uppercase" href="https://numbus.eu">NUMBUS</a>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<h1 class="sm:text-2xl sm:pr-20 lg:pr-0 text-xl text-white font-bold flex items-center">Step 1 - Preparation</h1>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center my-auto">
|
||||
<button class="h-auto mdi mdi-menu text-slate-100 text-2xl sm:hidden pr-8" aria-label="Menu with links"></button>
|
||||
<button class="h-auto mdi mdi-brightness-2 text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="Change theme"></button>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-source-repository text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="See the source code on Gitea"></a>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-text-box-search text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-8" aria-label="See the full documentation"></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="pt-10 w-full"></div>
|
||||
|
||||
<!-- Main content -->
|
||||
|
||||
<div class="bg-[#1e293b] border border-slate-700 rounded-3xl h-[calc(100vh-10rem)] max-w-[60vw] mx-auto relative">
|
||||
<!-- Device Type -->
|
||||
<div x-show="step === 1">
|
||||
<h1 class="p-5 pt-6 text-4xl font-bold text-sky-400">Device Type</h1>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[98%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select the <strong>device type</strong> for your new Numbus machine that matches <strong>your needs</strong>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 pb-10 grid grid-cols-2 gap-4">
|
||||
<button @click="" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-server-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus Server</h1>
|
||||
<p class="text-sm transition-colors">Your own Cloud at Home.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-backup-server-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus Backup Server</h1>
|
||||
<p class="text-sm transition-colors">Backup all Numbus devices and monitor your servers.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-computer-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus Computer</h1>
|
||||
<p class="text-sm transition-colors">Backup all Numbus devices and monitor your servers.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-tv-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus TV</h1>
|
||||
<p class="text-sm transition-colors">Your TV, your way. No spying on you.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Mode -->
|
||||
<div x-show="step === 2">
|
||||
<h1 class="p-5 pt-6 text-4xl font-bold text-sky-400">Deployment Mode</h1>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[98%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select your <strong>preferred</strong> deployment mode. Non-interactive <strong>requires</strong> a ready-to-go configuration hosted on a <strong>git platform</strong>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 grid grid-cols-2 gap-4">
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<i class="mdi mdi-gesture-tap text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Interactive</h1>
|
||||
<p class="text-sm transition-colors">We will guide you through the setup process.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<i class="mdi mdi-cog-clockwise text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Non-interactive</h1>
|
||||
<p class="text-sm transition-colors">You already have a ready-to-go configuration.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Setup -->
|
||||
<div x-show="step === 3">
|
||||
<h1 class="p-5 pt-6 text-4xl font-bold text-sky-400">Live Setup</h1>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[98%] mx-auto"></div>
|
||||
<p class="p-5 pb-10 text-xl text-slate-300">Provide the <strong>necessary information</strong> to connect to the device. It needs to be in a <strong>NixOS live environment</strong>.</p>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="p-6 space-y-2">
|
||||
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Live Target IP Address</label>
|
||||
<input 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" type="text" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="p-6 space-y-2">
|
||||
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Temporary Password</label>
|
||||
<input 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" type="password" placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="bg-[#1e293b] border-t bottom-0 w-full rounded-3xl absolute border-slate-700 p-6 flex items-center justify-between">
|
||||
<div x-show="step === 1" class="px-8 py-3"></div>
|
||||
<button @click="goToPrevStep()" x-show="step > 1" class="px-8 py-3 text-slate-400 hover:text-white font-bold transition-all">Back</button>
|
||||
<button @click="goToNextStep()" 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,430 @@
|
||||
<!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">
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="../media/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<body x-data="numbusPreparation()" class="p-4 bg-[#0f172a] text-slate-100 min-h-screen font-sans selection:bg-fuchsia-500/30">
|
||||
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function numbusPreparation() {
|
||||
return {
|
||||
step: 1,
|
||||
formData: {
|
||||
1: { language: 'French', country: 'France', timeZone: 'Europe/Paris' },
|
||||
2: { deviceType: '' },
|
||||
3: { deploymentMode: '' },
|
||||
4: { replicationHardware: '', replicationStrategy: '', replicationSecrets: '' },
|
||||
5: { liveIp: '', livePassword: '' },
|
||||
},
|
||||
isStepValid() {
|
||||
const currentStepData = this.formData[this.step];
|
||||
if (!currentStepData) return true;
|
||||
return Object.values(currentStepData).every(value => !!value);
|
||||
},
|
||||
goToPrevStep() {
|
||||
if (this.step === 5 && this.formData[3].deploymentMode === 'interactive') {
|
||||
this.step--;
|
||||
}
|
||||
this.step--;
|
||||
},
|
||||
goToNextStep() {
|
||||
if (this.step === 3 && this.formData[3].deploymentMode === 'interactive') {
|
||||
this.step++;
|
||||
}
|
||||
this.step++;
|
||||
},
|
||||
startDiscovery() {
|
||||
console.log("Discovery started with:", JSON.parse(JSON.stringify(this.formData)));
|
||||
// Add your bridge communication logic here
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="bg-[#1e293b] border border-slate-700 rounded-2xl relative">
|
||||
<div class="mx-auto relative flex h-16 items-center justify-between">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center">
|
||||
<a href="https://numbus.eu"><img class="w-auto h-10 pr-5 pl-8" src="../media/logo.png" aria-label="The numbus logo"></a>
|
||||
<a class="font-bold text-2xl tracking-tight bg-gradient-to-r from-sky-400 to-fuchsia-500 bg-clip-text text-transparent uppercase" href="https://numbus.eu">NUMBUS</a>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<h1 class="sm:text-2xl sm:pr-20 lg:pr-0 text-xl text-slate-200 font-bold flex items-center">Step 1 - Preparation</h1>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center my-auto">
|
||||
<button class="h-auto mdi mdi-menu text-slate-100 text-2xl sm:hidden pr-8" aria-label="Menu with links"></button>
|
||||
<button class="h-auto mdi mdi-brightness-2 text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="Change theme"></button>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-source-repository text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="See the source code on Gitea"></a>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-text-box-search text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-8" aria-label="See the full documentation"></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="pt-10 w-full"></div>
|
||||
|
||||
<!-- Main content -->
|
||||
|
||||
<div class="bg-[#1e293b] border border-slate-700 rounded-3xl h-[calc(100vh-10rem)] max-w-[60vw] mx-auto relative">
|
||||
|
||||
<!-- Step 1: Language & Region -->
|
||||
<div x-show="step === 1" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Language</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-200">Set your regional preferences to ensure <b>correct</b> time and language display.</p>
|
||||
<div class="pl-5 pr-5 pt-10 pb-10 grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2 relative" x-data="{ infoBubbleOpen: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-semibold text-slate-300">System Language</label>
|
||||
<button class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl shadow-slate-900 z-50">
|
||||
<p class="text-slate-200 text-lg italic">Select the primary language for the operating system and management interfaces.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<select x-model="formData[1].language" 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="FR">French</option>
|
||||
<option value="EN">English</option>
|
||||
<option value="DE">German</option>
|
||||
<option value="IT">Italian</option>
|
||||
<option value="ES">Spanish</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2 relative" x-data="{ infoBubbleOpen: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-semibold text-slate-300">Country</label>
|
||||
<button class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl shadow-slate-900 z-50">
|
||||
<p class="text-slate-200 text-lg italic">Select the country where you are located to help define the device's locale.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<select x-model="formData[1].country" 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="fr_FR">France</option>
|
||||
<option value="de_DE">Germany</option>
|
||||
<option value="it_IT">Italy</option>
|
||||
<option value="en_GB">United Kingdom</option>
|
||||
<option value="en_US">United States</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2 relative" x-data="{ infoBubbleOpen: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-semibold text-slate-300">Time zone</label>
|
||||
<button class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl shadow-slate-900 z-50">
|
||||
<p class="text-slate-200 text-lg italic">Select the time zone in which the device is located. <br> Don't know your time zone ? Find it at <a target="_blank" href="https://en.wikipedia.org/wiki/List_of_UTC_offsets" class="text-sky-400 underline font-semibold">Wikipedia</a>.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<select x-model="formData[1].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="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||
<option value="UTC">UTC</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Type -->
|
||||
<div x-show="step === 2" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Device Type</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select the <b>device type</b> for your new Numbus machine that matches <b>your needs</b>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 pb-10 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[2].deviceType = 'server'" :class="formData[2].deviceType === 'server' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-server-light.svg" alt="Numbus Server icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center gap-2 flex">
|
||||
<span class="font-bold text-2xl">Numbus Server</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">A versatile home cloud solution for hosting containers, media, and automated services.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">Your own Cloud at Home.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[2].deviceType = 'backup-server'" :class="formData[2].deviceType === 'backup-server' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-backup-server-light.svg" alt="Numbus Backup Server icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Numbus Backup Server</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">An all-in-one backup solution for all your Numbus devices, with monitoring tools.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">Backup all Numbus devices and monitor your servers.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[2].deviceType = 'computer'" :class="formData[2].deviceType === 'computer' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-computer-light.svg" alt="Numbus Computer icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Numbus Computer</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">A polished workstation powered by leading open-source software.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">A workstation powered by leading open-source software.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[2].deviceType = 'tv'" :class="formData[2].deviceType === 'tv' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-tv-light.svg" alt="Numbus TV icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Numbus TV</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">A computer -and all its accompanying advantages- but with a slick, familiar TV interface.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">Your TV, your way. No spying on you.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Mode -->
|
||||
<div x-show="step === 3" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Deployment Mode</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select your <b>preferred</b> deployment mode. Non-interactive <b>requires</b> a ready-to-go configuration hosted on a <b>git platform</b>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[3].deploymentMode = 'interactive'" :class="formData[3].deploymentMode === 'interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-gesture-tap text-5xl flex-shrink-0"></i>
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Interactive</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">If this is your first time setting up the Numbus device you chose, follow this option.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">We will guide you through the setup process.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[3].deploymentMode = 'non-interactive'" :class="formData[3].deploymentMode === 'non-interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-cog-clockwise text-5xl flex-shrink-0"></i>
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Non-interactive</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">This option is used for mass devices deployments. It requires a first run in interactive mode.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">You already have a ready-to-go configuration.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replication Mode -->
|
||||
<div class="max-h-[calc(100vh-17rem)] overflow-y-auto" x-show="step === 4" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Replication Mode</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select your <b>preferred</b> replication mode. It compares the current deployment to the old one.</p>
|
||||
<h3 class="text-xl font-semibold text-slate-300 text-center pb-3">Hardware</h3>
|
||||
<div class="pl-5 pr-5 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[4].replicationHardware = 'exact_same'" :class="formData[4].replicationHardware === 'exact_same' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-check-network text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Same hardware</span>
|
||||
<p class="text-sm transition-colors">The hardware listed in the configuration is exactly the same as the hardware on the machine you want to deploy.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[4].replicationHardware = 'different'" :class="formData[4].replicationHardware === 'different' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-close-network text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Different hardware</span>
|
||||
<p class="text-sm transition-colors">The hardware listed in the configuration is different compared to the hardware on the machine you want to deploy.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-center text-slate-300 pb-3 pt-5">Strategy</h3>
|
||||
<div class="pl-5 pr-5 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[4].replicationStrategy = 'exact_same'" :class="formData[4].replicationStrategy === 'exact_same' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-file-check text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Same configuration</span>
|
||||
<p class="text-sm transition-colors">Re-deploy the exact same configuration.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[4].replicationStrategy = 'with_tweaks'" :class="formData[4].replicationStrategy=== 'with_tweaks' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-file-cog text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">With tweaks</span>
|
||||
<p class="text-sm transition-colors">Use the configuration as a base and tweak the desired settings.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-center text-slate-300 pt-5 pb-3">Secrets</h3>
|
||||
<div class="pl-5 pr-5 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[4].replicationSecrets = 'exact_same'" :class="formData[4].replicationSecrets === 'exact_same' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-key-star text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Same secrets</span>
|
||||
<p class="text-sm transition-colors">Re-use the secrets in the repository.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[4].replicationSecrets = 'regenerate'" :class="formData[4].replicationSecrets === 'regenerate' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-key-remove text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">New secrets</span>
|
||||
<p class="text-sm transition-colors">Provide and auto-generate new secrets.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Setup -->
|
||||
<div x-show="step === 5" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Live Setup</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 pb-10 text-xl text-slate-300">Provide the <b>necessary information</b> to connect to the device. It needs to be in a <b>NixOS live environment</b>.</p>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="p-6 space-y-2">
|
||||
<span class="text-base font-bold text-center text-slate-300 uppercase">Live Target IP Address</span>
|
||||
<input x-model="formData[5].liveIp" 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" type="text" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="p-6 space-y-2">
|
||||
<span class="text-base font-bold text-center text-slate-300 uppercase">Temporary Password</span>
|
||||
<input x-model="formData[5].livePassword" 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" type="password" placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="bg-[#1e293b] border-t bottom-0 w-full rounded-3xl absolute border-slate-700 p-6 flex items-center justify-between">
|
||||
<div x-show="step === 1" class="px-8 py-3"></div>
|
||||
<button @click="goToPrevStep()" x-show="step > 1" x-cloak class="px-8 py-3 text-slate-400 font-bold rounded-xl hover:text-white transition-colors duration-200">Back</button>
|
||||
|
||||
<button @click="goToNextStep()"
|
||||
x-show="step != 5"
|
||||
:disabled="!isStepValid()"
|
||||
class="px-10 py-3 text-white font-bold rounded-xl scale-100 transition duration-200 ease-in bg-gradient-to-r from-fuchsia-500 via-fuchsia-600 to-fuchsia-700 hover:scale-105 hover:bg-gradient-to-br shadow-lg shadow-fuchsia-500/30 dark:shadow-lg dark:shadow-fuchsia-800/80 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">Continue</button>
|
||||
|
||||
<button
|
||||
@click="startDiscovery()"
|
||||
x-show="step === 5"
|
||||
x-cloak
|
||||
:disabled="!isStepValid()"
|
||||
class="px-10 py-3 text-white font-bold rounded-xl scale-100 transition duration-200 ease-in bg-gradient-to-r from-fuchsia-500 via-fuchsia-600 to-fuchsia-700 hover:scale-105 hover:bg-gradient-to-br shadow-lg shadow-fuchsia-500/30 dark:shadow-lg dark:shadow-fuchsia-800/80 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">Start Discovery
|
||||
<svg aria-hidden="true" class="w-4 h-4 text-neutral-tertiary animate-spin fill-brand me-2" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||