Migrated from multi repos to monorepo architecture.

This commit is contained in:
Raphaël Numbus
2026-05-02 12:52:08 +02:00
parent 72668492f5
commit 73adb395c0
218 changed files with 9639 additions and 57 deletions
+49
View File
@@ -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>
+50
View File
@@ -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

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+145
View File
@@ -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>
+430
View File
@@ -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>
+22
View File
@@ -0,0 +1,22 @@
# Buttons
1. Button with :
- an offset ring on focus
- a small shadow
- a nice transition
- a disabled feature
```html
px-10 py-3 bg-fuchsia-600 rounded-xl font-bold shadow-lg shadow-fuchsia-600/30 transition duration-200 hover:bg-fuchsia-500 focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2 focus:ring-offset-slate-700 disabled:opacity-50 disabled:cursor-not-allowed
```
2. Button with :
- a grow transformation on hover
- a gradient
- a small shadow
- a nice transition
- a disabled feature
```html
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
```
+10
View File
@@ -0,0 +1,10 @@
# Color palette
|Usage|Name|Tailwind code|Matching border|
|-|-|-|-|
|Body background|Slate|slate-900|N/A|
|Cards Background|Slate|slate-800|slate-700|
|Accent|Fuchsia|fuchsia-600|fuchsia-600
|Warning|Amber|amber-500|amber-400|
|Success|Emerald|emerald-500|emerald-400|
|Failure|Rose|rose-600|rose-500|
+8
View File
@@ -0,0 +1,8 @@
# Guidelines
|Type|Importance|Classes|
|-|-|-|
|Title|1|text-4xl font-bold text-sky-400|
|Title|2|text-4xl font-bold text-sky-400|
|Title|3|text-4xl font-bold text-sky-400|
|Title|4|text-4xl font-bold text-sky-400|
+43
View File
@@ -0,0 +1,43 @@
# Taglines, subtitles and descriptions
## Global
**Tagline :** Reclaim Your Digital Independence.
## Devices
### Numbus Server
The Infrastructure
**Tagline :** Reclaim Your Cloud.
**Subtitle :** Professional-grade hosting, strictly kept under your roof.
**Detailed Description :** The Numbus Server is the heartbeat of your digital sovereignty. Built on an immutable NixOS foundation, it provides a rock-solid environment to host the services you rely on—from Nextcloud and Passbolt to Home Assistant and Frigate. Whether you are a small business looking to internalize your data or a power user securing your smart home, the Numbus Server delivers containerized efficiency and VM flexibility without the "Big Tech" tax. Its not just a server; its your private corner of the internet, accessible from anywhere but controlled only by you.
### Numbus Backup Server
The Guardian
**Tagline :** Your Digital Safety Net.
**Subtitle :** Automated, high-efficiency protection for your entire ecosystem.
**Detailed Description :** Data loss and downtime aren't just frustrating; they're disastrous. The Numbus Backup Server is your ecosystem's ultimate guardian. It works silently in the background, automatically pulling high-efficiency backups from your Numbus devices the moment they start charging. But it does more than just store files—it actively acts as a watchdog. By constantly monitoring your Numbus Server and other critical network devices, it immediately alerts you the second something goes offline. It is automated peace of mind for your entire digital infrastructure.
### Numbus Computer
The Workstation
**Tagline :** The No-Bullshit Workstation.
**Subtitle :** A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat.
**Description :** Experience computing the way it was meant to be: entirely under your control. The Numbus Computer is a powerful workstation that strips away unwanted telemetry, forced updates, and bundled adware. Under the hood, it harnesses the unparalleled stability of NixOS, but we've hidden the complexity behind a sleek, intuitive interface. Our default apps make managing drivers, software, and system options a breeze. Whether you are coding, browsing, or gaming via Proton, the Numbus Computer delivers a snappy, secure, and truly "libre" desktop experience that respects your privacy while delivering the raw power of the hardware you paid for.
### Numbus TV
The Experience
**Tagline :** Entertainment, Liberated.
**Subtitle :** A premium cinematic experience free from trackers and forced subscriptions.
**Detailed Description :** The Numbus TV reclaims your living room. Unlike "Smart" TVs that have essentially become surveillance machines and clutter your screen with unwanted ads, the Numbus TV offers a clean, classy interface powered by KDE Bigscreen. Its a full-power PC in a TVs body, allowing you to stream, browse, and play on your own terms. It doesn't force you into a specific ecosystem; it simply provides a beautiful, privacy-hardened portal to your favorite media. No spying, no "walled gardens"—just the big screen experience, perfected.
+1399
View File
@@ -0,0 +1,1399 @@
#!/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
# --- UTILITY FUNCTIONS --->
echod() {
MESSAGE=${1}
if [[ ${DEBUG} -eq 1 ]]; then
echo -e ${MESSAGE}
fi
}
ssh_to_host() {
local COMMAND="${1}"
ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}"
}"Invalid IP address format."
get_valid_input() {
local VAR_NAME="${1}"
local HEADER="${2}"
local PLACEHOLDER="${3}"
local REGEX="${4}"
local MANDATORY="${5:-true}"
local SENSITIVE="${6:-false}"
if [[ "${MANDATORY}" == "true" ]]; then
local PROMPT="(Required) > "
elif [[ "${MANDATORY}" == "false" ]]; then
local PROMPT="(Optional) > "
fi
while true; do
local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}")
# Handle empty input
if [[ -z "${INPUT}" ]]; then
if [[ "${MANDATORY}" == true ]]; then
gum style --foreground "#ff0000" -- "✖ This field is mandatory."
continue
else
INPUT=""
break
fi
fi
# Handle Regex Validation
if [[ -n "${REGEX}" ]]; then
if [[ "${INPUT}" =~ ${REGEX} ]]; then
export "${VAR_NAME}"="${INPUT}"
break
else
gum style --foreground "#ff0000" -- "✖ Invalid format. Please try again."
fi
else
export "${VAR_NAME}"="${INPUT}"
break
fi
done
}
# --- UTILITY FUNCTIONS ---<
# --- GLOBAL FUNCTIONS --->
cleanup() {
echo -e "\n ✅ Cleaning up..."
rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/
if [[ ${WEB_MODE} -eq 1 && -n "${BRIDGE_PID:-}" ]]; then
kill ${BRIDGE_PID}
fi
}
compatibility_check() {
TEST_FAIL=0
if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then
echod "\n ✅ NixOS system detected."
else
TEST_FAIL=$((TEST_FAIL + 1))
echo -e "\n ❌ You are not on a NixOS based system. This is required to continue."
fi
if [[ "$(uname -m)" == "x86_64" ]]; then
echod "\n ✅ x86_64 system detected."
else
TEST_FAIL=$((TEST_FAIL + 1))
echo -e "\n ❌ You are not on a x86_64 based system. This is required to continue."
fi
if [[ ${TEST_FAIL} -gt 0 ]]; then
COMPATIBILITY_OVERRIDE=$(gum choose --header "Some compatibility checks failed. The installation will very likely fail. Continue ?" \
"No" \
"Yes, I know what I am doing")
[[ "${COMPATIBILITY_OVERRIDE}" == "No" ]] && exit 1
[[ "${COMPATIBILITY_OVERRIDE}" != "No" ]] && echo -e "\n ⚠️ Continuing anyways, this is not supported by Numbus."
fi
return 0
}
hierarchy_preparation() {
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."
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
# Script folders
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/config
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/logs
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/tmp
[[ ${WEB_MODE} -eq 1 ]] && mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/web
# 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
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system
if [[ "${DEVICE_TYPE}" == "server" || "${DEVICE_TYPE}" == "backup" ]]; then
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman
fi
echod "\n ✅ Folder hierarchy ready"
}
hardware_detection() {
local TMPFILE="/tmp/nixos-installation-hw-detection"
ssh_to_host 'bash -s' << SSHEND
TARGET_GRAPHICS_BRAND=()
for brand in Intel AMD NVIDIA; do
if lspci -nn 2>/dev/null | grep -i "vga" | grep -iq "\${brand}"; then
TARGET_GRAPHICS="true"
TARGET_GRAPHICS_BRAND+=("\${brand}")
else
TARGET_GRAPHICS="false"
fi
done
ls /dev/dri/ > /dev/null 2>&1 | grep -iq "renderD128" && TARGET_GRAPHICS_RENDERER="true" || TARGET_GRAPHICS_RENDERER="false"
lsusb > /dev/null 2>&1 | grep -iq "google" && TARGET_USB_CORAL="true" || TARGET_USB_CORAL="false"
lspci -nn > /dev/null 2>&1 | grep -iq "089a" && TARGET_PCIE_CORAL="true" || TARGET_PCIE_CORAL="false"
ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" && TARGET_ZIGBEE_DEVICE=\$(ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" | head -n 1) || TARGET_ZIGBEE_DEVICE=""
TARGET_INTERFACE=\$(ip -4 route show default | awk '{print \$5}' | head -n1)
if ls -l /sys/class/tpm/tpm0/ > /dev/null 2>&1; then
TARGET_TPM="true"
TARGET_TPM_VERSION=\$(cat /sys/class/tpm/tpm0/tpm_version_major)
else
TARGET_TPM="false"
TARGET_TPM_VERSION="N/A"
fi
HDD=1
DISK_DEVPATH=()
DISK_NAME=()
DISK_TYPE=()
DISK_HEALTH=()
DISK_ID=()
for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do
# Disk name and simple path
DISK_DEVPATH+=("/dev/\$DISK")
DISK_NAME+=("\$DISK")
# Disk type
HDD=\$(cat /sys/block/\$DISK/queue/rotational)
TRANSPORT_PROTOCOL=\$(lsblk -x SIZE -d -n -e 7,11 -o TRAN /dev/\$DISK)
if [[ "\$DISK" == "nvme*" ]]; then DISK_TYPE+=("NVMe");
elif [[ "\$TRANSPORT_PROTOCOL" == "usb" ]]; then DISK_TYPE+=("USB");
elif [[ "\$HDD" -eq 1 ]]; then DISK_TYPE+=("HDD");
elif [[ "\$HDD" -eq 0 ]]; then DISK_TYPE+=("SSD");
else DISK_TYPE+=("Other")
fi
# Disk health
if [[ \$(echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$DISK 2>/dev/null | grep 'self-assessment' | awk '{print \$6}') == "PASSED" ]]; then
DISK_HEALTH+=("PASSED")
else
DISK_HEALTH+=("N/A")
fi
# Disk ID
DISK_ID+=("\$(ls -l /dev/disk/by-id | grep -m1 "../../\$DISK" | awk '{print "/dev/disk/by-id/" \$9}')")
DISK_SIZE+=("\$(lsblk -x SIZE -d -n -e 7,11 -o SIZE /dev/\$DISK)")
done
echo "# Hardware detection results on \$(date)" > "${TMPFILE}"
for var in \
TARGET_GRAPHICS \
TARGET_GRAPHICS_RENDERER \
TARGET_USB_CORAL \
TARGET_PCIE_CORAL \
TARGET_ZIGBEE_DEVICE \
TARGET_INTERFACE \
TARGET_TPM \
TARGET_TPM_VERSION; do
echo "export \${var}=\${!var}" >> "${TMPFILE}"
done
for var in \
TARGET_GRAPHICS_BRAND \
DISK_DEVPATH \
DISK_NAME \
DISK_TYPE \
DISK_HEALTH \
DISK_ID \
DISK_SIZE; do
declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}"
done
SSHEND
scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
source "${TMPFILE}"
local DISK_FLAT_ARRAY=()
for i in "${!DISK_NAME[@]}"; do
DISK_FLAT_ARRAY+=("${DISK_NAME[$i]}" "${DISK_DEVPATH[$i]}" "${DISK_TYPE[$i]}" "${DISK_HEALTH[$i]}" "${DISK_ID[$i]}" "${DISK_SIZE[$i]}")
done
jq -n \
--argjson graphics_enabled "${TARGET_GRAPHICS:-false}" \
--argjson graphics_renderer "${TARGET_GRAPHICS_RENDERER:-false}" \
--argjson tpu_usb "${TARGET_USB_CORAL:-false}" \
--argjson tpu_pcie "${TARGET_PCIE_CORAL:-false}" \
--argjson tpm_enabled "${TARGET_TPM:-false}" \
--arg tpm_version "${TARGET_TPM_VERSION:-N/A}" \
--arg zigbee_device "${TARGET_ZIGBEE_DEVICE:-}" \
--arg interface "${TARGET_INTERFACE:-}" \
--argjson brands "$(jq -n '$ARGS.positional' --args ${TARGET_GRAPHICS_BRAND[@]:-})" \
'
{
graphics: { enabled: $graphics_enabled, brands: $brands, renderer: $graphics_renderer },
tpu: { usb: $tpu_usb, pcie: $tpu_pcie },
tpm: { enabled: $tpm_enabled, version: $tpm_version },
zigbee: { device: $zigbee_device },
network: { interface: $interface },
disks: [
$ARGS.positional | range(0; length; 6) as $i | {
name: .[$i], path: .[$i+1], type: .[$i+2], health: .[$i+3], id: .[$i+4], size: .[$i+5]
}
]
}' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH}
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"
exit 1
fi
}
# --- GLOBAL FUNCTIONS ---<
# --- MAIN WEB FUNCTIONS --->
launch_configurator() {
echo -e "\n 🚀 Launching Numbus Configurator..."
python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 &
export BRIDGE_PID=$!
echo -e "\n ➡️ Open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")"
xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true
}
# --- MAIN WEB FUNCTIONS ---<
# --- MAIN TUI FUNCTIONS --->
preparation() {
echo -e "\n ➡️ This script will now guide you through the configuration and gather the necessary information."
echo ""
RAW_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.")
case "${RAW_DEVICE_TYPE}" in
"Numbus Server : "* ) DEVICE_TYPE="server" ;;
"Numbus Backup Server : "* ) DEVICE_TYPE="backup" ;;
"Numbus Computer : "* ) DEVICE_TYPE="computer" ;;
"Numbus TV : "* ) DEVICE_TYPE="tv" ;;
esac
RAW_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.")
case "${RAW_DEPLOYMENT_MODE}" in
"Interactive : "* ) DEPLOYMENT_MODE="interactive" ;;
"Non-interactive : "* ) DEPLOYMENT_MODE="non-interactive" ;;
esac
if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then
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" ]]; then
git_url
fi
echo -e "\n You will be prompted for your credentials again. Make sure that they are correct."
done
fi
echo ""
gum format -- \
"➡️ To continue, you need to start the target device in a NixOS live environment :
1. Download the NixOS iso from the **[official website](https://nixos.org/download/)**.
2. Flash it to a USB stick. (use a flashing tool like **[Rufus](https://rufus.ie/en/#download)**, **[BalenaEtcher](https://etcher.balena.io/#download-etcher)**, **[Impression](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression)**, ...)
3. Make sure your computer allows booting from USB drives and is in UEFI mode.
4. Boot into the NixOS live environment.
5. Launch a terminal. Set a password using \`passwd\` and find the IP address using \`ip a\`"
echo ""
gum confirm "Is the device ready ?" || { echo "❌ You need to prepare the device. The script cannot continue."; exit 1; }
# LIVE TARGET SETTINGS
user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}"
user_input "LIVE_TARGET_PASSWORD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true"
# INTERNATIONALIZATION SETTINGS
user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc"
user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc"
user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc"
}
configuration() {
if [[ "${DEVICE_TYPE}" == "server" ]]; then
# Users & Groups
user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve"
user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}"
user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide the SSH public key of an authorized device (or a comma-separated list) :" "For example : ssh-ed25519 AAAAC3Nzam0uYewNAbxL8Fci8 user@your-pc or ssh-* * *, ssh-* * *, etc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)."
echo -e "\n\n ➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains"
# TRAEFIK SETTINGS
user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}"
user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true"
echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)"
# SMTP SETTINGS
user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}"
user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true"
user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format."
user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number."
echo -e "\n\n ➡️ This server will connect to your local network and you will configure its IP address\n"
# NETWORK SETTINGS
user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)."
user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format."
user_input "HOME_SERVER_IP" " Please choose the ip address that your server will use (i.e. any address in the 192.168.1.1/24 range that is not in use.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format."
elif [[ "${DEVICE_TYPE}" == "backup" ]]; then
:
elif [[ "${DEVICE_TYPE}" == "computer" ]]; then
:
elif [[ "${DEVICE_TYPE}" == "tv" ]]; then
:
fi
}
setup_ssh() {
echod "\n ✅ Generating new SSH key for numbus-admin..."
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_PASSWORD}" 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
else
echo -e "\n ❌ Failed to copy SSH key. Please check the host IP and password."
exit 1
fi
}
services_selection() {
services_choice() {
local SERVICES_LIST=( "${1[@]}" )
local SERVICES_DESCRIPTION=( "${2[@]}" )
local FINAL_VARIABLE="${3}"
local HEADER="${4}"
local LIMIT="${5:---no-limit}"
local SELECTED_SERVICES=()
local SELECTED_SERVICES_DESCRIPTION=()
local SELECTED_SERVICES_DESCRIPTION=$(gum choose ${LIMIT} --header "${HEADER}" "${SERVICES_DESCRIPTION[@]}")
for i in ${!SERVICES_LIST[@]}; do
if printf '%s' "${SELECTED_SERVICES_DESCRIPTION}" | grep -iq "${SERVICES_LIST[${i}]}"; then
SELECTED_SERVICES+=("${SERVICES_LIST[${i}]}")
fi
done
export "${FINAL_VARIABLE}=(${SELECTED_SERVICES[@]})"
}
echo -e "\n\n ➡️ You will now select the services you want installed on your server:"
services_choice "${DNS_SERVICES_LIST[@]}" "${DNS_SERVICES_DESCRIPTION[@]}" "SELECTED_DNS_SERVICE" "Choose your preferred DNS service :" "--limit=1"
services_choice "${WEB_APPLICATIONS_LIST[@]}" "${WEB_APPLICATIONS_DESCRIPTION[@]}" "SELECTED_WEB_APPLICATIONS" "Choose the web applications you want to install :"
services_choice "${SYSTEM_SERVICES_LIST[@]}" "${SYSTEM_SERVICES_DESCRIPTION[@]}" "SELECTED_SYSTEM_SERVICES" "Choose the system services you want to install :"
gum confirm "Do you want to edit the default subdomain of your services ?" || { echo -e "\n\n✅ Continuing..."; return 0; }
for service in ${SELECTED_WEB_APPLICATIONS[@]} ${SELECTED_DNS_SERVICE[@]}; do
if gum confirm "Change the subdomain of ${service} ?"; then
SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" )
fi
done
return 0
}
users_and_groups() {
declare -A ACL_GROUPS
declare -A ACL_USERS
compute_acl_services() {
EXCLUDED_SERVICES=( "clamav" ) # Those are the services that don't have a web page or don't support SSO
local ALL_SERVICES=("${SELECTED_DNS_SERVICE[@]}" "${SELECTED_WEB_APPLICATIONS[@]}" "${SELECTED_SYSTEM_SERVICES[@]}")
for i in "${!ALL_SERVICES[@]}"; do
for excluded in "${EXCLUDED_SERVICES[@]}"; do
if [[ "${ALL_SERVICES[${i}]}" == "${excluded}" ]]; then
unset "ALL_SERVICES[${i}]"
fi
done
done
}
show_groups_table() {
if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then
gum style --italic --foreground "#6272a4" -- "No groups configured."
return
fi
# We use CSV format with quotes to handle comma-separated services correctly
local csv="Group Name,Allowed Services\n"
for g in "${!ACL_GROUPS[@]}"; do
csv+="\"$g\",\"${ACL_GROUPS[$g]}\"\n"
done
printf "%b" "$csv" | gum table
}
add_group() {
if [[ ${#ACL_GROUPS[@]} -ge 10 ]]; then
gum style --foreground "#ffb86c" -- "⚠ Maximum of 10 groups reached."
sleep 2; return
fi
local group_name
get_valid_input group_name "Group Name" "^[a-zA-Z0-9_-]+$" true ""
if [[ -n "${ACL_GROUPS[$group_name]}" ]]; then
gum style --foreground "#ff0000" -- "✖ Group already exists."
sleep 2; return
fi
gum style --foreground "#50fa7b" -- "Select services for $group_name (Space to select, Enter to confirm):"
local chosen_services
chosen_services=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
ACL_GROUPS["$group_name"]="$chosen_services"
gum style --foreground "#50fa7b" -- "✔ Group '$group_name' created."
sleep 1
}
edit_group() {
if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then return; fi
local group_keys=("${!ACL_GROUPS[@]}")
gum style -- "Select a group to edit:"
local group_name=$(gum choose "${group_keys[@]}")
if [[ -z "$group_name" ]]; then return; fi
if [[ "$group_name" == "admin" ]]; then
gum style --foreground "#ff0000" -- "✖ The admin group cannot be modified."
sleep 2; return
fi
gum style --foreground "#50fa7b" -- "Select NEW services for $group_name:"
local chosen_services=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
ACL_GROUPS["$group_name"]="$chosen_services"
gum style --foreground "#50fa7b" -- "✔ Group '$group_name' updated."
sleep 1
}
remove_group() {
if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then return; fi
local group_keys=("${!ACL_GROUPS[@]}")
gum style -- "Select a group to REMOVE:"
local group_name=$(gum choose "${group_keys[@]}")
if [[ -z "$group_name" ]]; then return; fi
if [[ "$group_name" == "admin" ]]; then
gum style --foreground "#ff0000" -- "✖ The admin group cannot be removed."
sleep 2; return
fi
gum style --foreground "#ff5555" --bold -- "Are you sure you want to delete '$group_name'?"
if gum confirm; then
unset ACL_GROUPS["$group_name"]
gum style --foreground "#50fa7b" -- "✔ Group deleted."
sleep 1
fi
}
manage_groups_menu() {
while true; do
clear
gum style --border double --margin "1" --padding "0 1" --border-foreground "#8be9fd" -- "Group Management (${#ACL_GROUPS[@]}/10)"
show_groups_table
local action=$(gum choose "Add Group" "Edit Group" "Remove Group" "Back")
case "$action" in
"Add Group") add_group ;;
"Edit Group") edit_group ;;
"Remove Group") remove_group ;;
"Back"|"") break ;;
esac
done
}
show_users_table() {
if [[ ${#ACL_USERS[@]} -eq 0 ]]; then
gum style --italic --foreground "#6272a4" -- "No users configured."
return
fi
local csv="Username,Name,Email,Health Alerts,ACL Type,ACL Value\n"
for u in "${!ACL_USERS[@]}"; do
IFS='|' read -r name email phone health type input <<< "${ACL_USERS[$u]}"
csv+="\"$u\",\"$name\",\"$email\",\"$health\",\"$type\",\"$input\"\n"
done
printf "%b" "$csv" | gum table
}
add_user() {
if [[ ${#ACL_USERS[@]} -ge 20 ]]; then
gum style --foreground "#ffb86c" -- "⚠ Maximum of 20 users reached."
sleep 2; return
fi
local name username email phone health_alert acl_type acl_value
get_valid_input username "Username" "^[a-z0-9_-]+$" true ""
if [[ -n "${ACL_USERS[$username]}" ]]; then
gum style --foreground "#ff0000" -- "✖ Username already exists."
sleep 2; return
fi
get_valid_input name "Full Name" "" true ""
get_valid_input email "Email Address" "$EMAIL_REGEX" true ""
get_valid_input phone "Phone Number (E.164, optional)" "$PHONE_REGEX" false ""
gum style -- "Inform about server health?"
if gum confirm; then health_alert="true"; else health_alert="false"; fi
gum style -- "How should ACL be managed for $username?"
acl_type=$(gum choose "Assign to Group" "Manual Service Selection")
if [[ "$acl_type" == "Assign to Group" ]]; then
acl_type="group"
local group_keys=("${!ACL_GROUPS[@]}")
acl_value=$(gum choose "${group_keys[@]}")
else
acl_type="manual"
gum style --foreground "#50fa7b" -- "Select services for $username:"
acl_value=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
fi
ACL_USERS["$username"]="$name|$email|$phone|$health_alert|$acl_type|$acl_value"
gum style --foreground "#50fa7b" -- "✔ User '$username' created."
sleep 1
}
edit_user() {
if [[ ${#ACL_USERS[@]} -eq 0 ]]; then return; fi
local user_keys=("${!ACL_USERS[@]}")
gum style -- "Select a user to edit:"
local username=$(gum choose "${user_keys[@]}")
if [[ -z "$username" ]]; then return; fi
# Extract current values
IFS='|' read -r curr_name curr_email curr_phone curr_health curr_type curr_val <<< "${ACL_USERS[$username]}"
local name email phone health_alert acl_type acl_value
gum style --foreground "#f1fa8c" -- "Editing User: $username"
get_valid_input name "Full Name" "" true "$curr_name"
get_valid_input email "Email Address" "$EMAIL_REGEX" true "$curr_email"
get_valid_input phone "Phone Number" "$PHONE_REGEX" false "$curr_phone"
if [[ "$username" == "admin" ]]; then
gum style --foreground "#ffb86c" -- "Admin health alerts and ACL settings cannot be changed."
health_alert="true"
acl_type="group"
acl_value="admin"
sleep 2
else
gum style -- "Inform about server health? (Currently: $curr_health)"
if gum confirm; then health_alert="true"; else health_alert="false"; fi
gum style -- "How should ACL be managed? (Currently: $curr_type)"
acl_type=$(gum choose "Assign to Group" "Manual Service Selection")
if [[ "$acl_type" == "Assign to Group" ]]; then
acl_type="group"
local group_keys=("${!ACL_GROUPS[@]}")
acl_value=$(gum choose "${group_keys[@]}")
else
acl_type="manual"
gum style --foreground "#50fa7b" -- "Select services for $username:"
acl_value=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
fi
fi
ACL_USERS["$username"]="$name|$email|$phone|$health_alert|$acl_type|$acl_value"
gum style --foreground "#50fa7b" -- "✔ User '$username' updated."
sleep 1
}
remove_user() {
if [[ ${#ACL_USERS[@]} -eq 0 ]]; then return; fi
local user_keys=("${!ACL_USERS[@]}")
gum style -- "Select a user to REMOVE:"
local username=$(gum choose "${user_keys[@]}")
if [[ -z "$username" ]]; then return; fi
if [[ "$username" == "admin" ]]; then
gum style --foreground "#ff0000" -- "✖ The admin user cannot be removed."
sleep 2; return
fi
gum style --foreground "#ff5555" --bold -- "Are you sure you want to delete '$username'?"
if gum confirm; then
unset ACL_USERS["$username"]
gum style --foreground "#50fa7b" -- "✔ User deleted."
sleep 1
fi
}
manage_users_menu() {
while true; do
clear
gum style --border double --margin "1" --padding "0 1" --border-foreground "#ff79c6" -- "User Management (${#ACL_USERS[@]}/20)"
show_users_table
local action=$(gum choose "Add User" "Edit User" "Remove User" "Back")
case "$action" in
"Add User") add_user ;;
"Edit User") edit_user ;;
"Remove User") remove_user ;;
"Back"|"") break ;;
esac
done
}
setup_admin_user() {
if [[ -n "${ACL_USERS["admin"]}" ]]; then return; fi
gum style --border rounded --padding "1 2" --margin "1" --border-foreground "#ff79c6" -- "Initial Setup: Administrator User"
local name email phone
get_valid_input name "Admin Full Name" "" true ""
get_valid_input email "Admin Email Address" "$EMAIL_REGEX" true ""
get_valid_input phone "Admin Phone Number (optional)" "$PHONE_REGEX" false ""
ACL_USERS["admin"]="$name|$email|$phone|true|group|admin"
gum style --foreground "#50fa7b" -- "✔ Administrator configured."
sleep 1
}
export_data() {
clear
gum style --foreground "#50fa7b" --bold -- "--- Provisioning Data Payload ---"
echo "GROUPS:"
for group in "${!ACL_GROUPS[@]}"; do
echo " $group -> Allowed: ${ACL_GROUPS[$group]}"
done
echo ""
echo "USERS (Name|Email|Phone|Alert|AclType|AclValue):"
for user in "${!ACL_USERS[@]}"; do
echo " $user -> ${ACL_USERS[$user]}"
done
}
compute_acl_services
ACL_GROUPS["admin"]=$(printf "%s," "${ACL_SERVICES[@]}" | sed 's/,$//')
setup_admin_user
while true; do
clear
gum style --border double --margin "1" --padding "1 2" --border-foreground "#bd93f9" -- "Numbus Deployment - Access Management"
gum style -- "Current state: ${#ACL_GROUPS[@]}/10 Groups | ${#ACL_USERS[@]}/20 Users"
echo ""
local choice=$(gum choose "1. Manage Groups" "2. Manage Users" "3. Finish & Apply Configuration")
case "$choice" in
"1. Manage Groups") manage_groups_menu ;;
"2. Manage Users") manage_users_menu ;;
"3. Finish & Apply Configuration")
export_data
break
;;
esac
done
}
disks_selection() {
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'WARNING:') You will choose the disks to install NixOS on.
!! PLEASE MAKE SURE YOU BACKED UP ANY IMPORTANT DATA !!
!! ALL DATA WILL BE WIPED ON THE DISKS YOU CHOOSE !!
Please press CTRL+C to abort.
"
gum confirm "Do you understand and wish to proceed?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
echo -e "\n\n 🔎 Fetching and analyzing disks from target host... (This may take a moment)"
if [[ "${#DISK_NAME[@]}" -eq 0 ]]; then
echo -e "\n❌ No disks found on the target host. Aborting."
exit 1
fi
local HEADER=$(printf " %-12s %-12s %-12s %-12s %s" "Device" "Type" "Size" "SMART" "Path")
for i in ${!DISK_NAME[@]}; do
local GUM_PRINTED_ELEMENT=$(printf "%-12s %-12s %-12s %-12s %s" \
"${DISK_NAME[${i}]}" "${DISK_TYPE[${i}]}" "${DISK_SIZE[${i}]}" \
"${DISK_HEALTH[${i}]}" "${DISK_DEVPATH[${i}]}")
local GUM_PRINTED_ELEMENTS+=("${GUM_PRINTED_ELEMENT}")
done
echo ""
gum style --foreground 212 "➡️ Please choose one (stripe) or two (mirror) disks for your NixOS boot installation :"
local SELECTED_BOOT_DISK=$(gum choose --limit 2 --header "${HEADER}" "${GUM_PRINTED_ELEMENTS[@]}")
for i in ${!DISK_NAME[@]}; do
if printf '%s' "$SELECTED_BOOT_DISK" | grep -iqw "${DISK_NAME[${i}]}"; then
BOOT_DISKS_ID_LIST+=("\"${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}\"")
BOOT_DISKS_NAME+=("${DISK_NAME[${i}]}")
unset "GUM_PRINTED_ELEMENTS[${i}]"
fi
done
echo ""
gum style --foreground 212 "➡️ Please choose data and parity disks (up to 9 total) :"
local SELECTED_DATA_DISK=$(gum choose --limit 9 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}")
for i in ${!DISK_NAME[@]}; do
if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[${i}]}"; then
DATA_DISKS_ID+=("${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}")
DATA_DISKS_TYPE+=("${DISK_TYPE[${i}]}")
fi
done
if [[ "${#DATA_DISKS_ID[@]}" -eq 1 ]]; then
export PARITY_DISK_NUMBER=0
export CONTENT_DISK_NUMBER=1
export PARITY_DISK_LIST=()
export CONTENT_DISK_LIST=("\"${DATA_DISKS_ID[0]}\"")
else
export PARITY_DISK_NUMBER=$(((${#DATA_DISKS_ID[@]} + 2) / 3))
export CONTENT_DISK_NUMBER=$((${#DATA_DISKS_ID[@]} - PARITY_DISK_NUMBER))
for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do
CONTENT_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"")
done
for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do
PARITY_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"")
done
fi
if [[ "${#DATA_DISKS_ID[@]}" -gt 0 ]]; then
for i in ${!DATA_DISKS_ID[@]}; do
if [[ "${DATA_DISKS_TYPE[${i}]}" == "HDD" ]]; then
SPINDOWN_DISKS_LIST+=("\"${DATA_DISKS_ID[${i}]}\"")
fi
done
fi
export SPINDOWN_DISKS_LIST
export BOOT_DISKS_ID_LIST
export PARITY_DISK_LIST
export CONTENT_DISK_LIST
}
server_config_generation() {
echo -e "\n # Server settings" >> ${CONFIGURATION_PATH}
echo -e " time.timeZone = \"${INTERNATIONALIZATION_TIMEZONE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.locale = \"${LOCALE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.language = \"${INTERNATIONALIZATION_LANGUAGE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.owner = \"${SERVER_OWNER_NAME}\";" >> ${CONFIGURATION_PATH}
}
network_config_generation() {
echo -e "\n # Network settings" >> ${CONFIGURATION_PATH}
echo -e " numbus.networking.ipAddress = \"${HOME_SERVER_IP}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.networking.interface = \"${TARGET_INTERFACE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.networking.routerIpAddress = \"${NETWORK_ROUTER_IP}\";" >> ${CONFIGURATION_PATH}
}
services_config_generation() {
echo -e "\n # DNS settings" >> ${CONFIGURATION_PATH}
echo -e " numbus.services.dns = \"${SELECTED_DNS_SERVICE[0]}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.services.${SELECTED_DNS_SERVICE[0]}.enable = true;" >> ${CONFIGURATION_PATH}
echo -e "\n # Services settings" >> ${CONFIGURATION_PATH}
echo -e " numbus.services.domain = \"${DOMAIN_NAME}\";" >> ${CONFIGURATION_PATH}
i=0
for service in "${SELECTED_WEB_APPLICATIONS[@]}"; do
if [[ -v SELECTED_WEB_APPLICATIONS_SUBDOMAIN && -n "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]}" ]]; then
echo -e " numbus.services.${service}.subdomain = \"${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]}\";" >> ${CONFIGURATION_PATH}
fi
echo -e " numbus.services.${service}.enable = true;" >> ${CONFIGURATION_PATH}
i=$((i + 1))
done
if [[ -v SELECTED_DNS_SERVICE_SUBDOMAIN && -n "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]}" ]]; then
echo -e " numbus.services.${SELECTED_DNS_SERVICE[0]}.subdomain = \"${SELECTED_DNS_SERVICE_SUBDOMAIN[0]}\";" >> ${CONFIGURATION_PATH}
fi
if [[ "${TARGET_GRAPHICS_RENDERER}" == "true" ]]; then
FRIGATE_DEVICES+=" \"/dev/dri/D128\""
fi
if [[ "${TARGET_USB_CORAL}" == "true" ]]; then
FRIGATE_DEVICES+=" \"/dev/bus/usb\""
elif [[ "${TARGET_PCIE_CORAL}" == "true" ]]; then
FRIGATE_DEVICES+=" \"/dev/apex_0\""
fi
if [[ -n "${TARGET_ZIGBEE_DEVICE}" ]]; then
HOME_ASSISTANT_DEVICES+=" \"${TARGET_ZIGBEE_DEVICE}\""
fi
if [[ -n "${FRIGATE_DEVICES:-}" ]]; then
echo -e " numbus.services.frigate.devices = [${FRIGATE_DEVICES} ];" >> ${CONFIGURATION_PATH}
fi
if [[ -n "${HOME_ASSISTANT_DEVICES:-}" ]]; then
echo -e " numbus.services.home-assistant.devices = [${HOME_ASSISTANT_DEVICES} ];" >> ${CONFIGURATION_PATH}
fi
}
mail_config_generation() {
echo -e "\n # Mail settings" >> ${CONFIGURATION_PATH}
echo -e " numbus.mail.enable = true;" >> ${CONFIGURATION_PATH}
echo -e " numbus.mail.userAddress = \"${SERVER_USER_EMAIL}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.mail.adminAddress = \"${SERVER_ADMIN_EMAIL}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.mail.smtpUsername = \"${SMTP_SERVER_USERNAME}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.mail.smtpPasswordPath = config.sops.secrets.smtpPassword.path;" >> ${CONFIGURATION_PATH}
if [[ "${SMTP_SERVER_HOST}" != "smtp.gmail.com" ]]; then
echo -e " numbus.mail.smtpServer = \"${SMTP_SERVER_HOST}\";" >> ${CONFIGURATION_PATH}
fi
if [[ "${SMTP_SERVER_PORT}" != "587" ]]; then
echo -e " numbus.mail.smtpPort = ${SMTP_SERVER_PORT};" >> ${CONFIGURATION_PATH}
fi
}
disk_config_generation() {
echo -e "\n # Hardware settings" >> ${CONFIGURATION_PATH}
if [[ "${TARGET_PCIE_CORAL}" == "true" ]]; then
echo " numbus.hardware.pcie-coral.enable = true;" >> ${CONFIGURATION_PATH}
fi
echo -e " numbus.hardware.bootDisksList = [ ${BOOT_DISKS_ID_LIST[@]} ];" >> ${CONFIGURATION_PATH}
echo -e " numbus.hardware.dataDisksList = [ ${CONTENT_DISK_LIST[@]} ];" >> ${CONFIGURATION_PATH}
echo -e " numbus.hardware.parityDisksList = [ ${PARITY_DISK_LIST[@]} ];" >> ${CONFIGURATION_PATH}
echo -e " numbus.hardware.spindownDisksList = [ ${SPINDOWN_DISKS_LIST[@]} ];" >> ${CONFIGURATION_PATH}
echo "}" >> ${CONFIGURATION_PATH}
}
keys_generation() {
for i in $(seq 1 "${#BOOT_DISKS_ID_LIST[@]}"); do
PASS="$(xkcdpass)"
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_PASSWORD" | sudo -S mkdir -p /etc/secrets/disks/
echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}"
echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/boot-${i}
EOF
done
for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do
PASS="$(xkcdpass)"
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_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}"
echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/content-${i}
EOF
done
for i in $(seq 1 "$PARITY_DISK_NUMBER"); do
PASS="$(xkcdpass)"
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_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}"
echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/parity-${i}
EOF
done
local SSH_KEYS_FORMATTED=""
if [[ "$(declare -p AUTHORIZED_SSH_PUBLIC_KEY 2>/dev/null)" =~ "declare -a" ]]; then
for key in "${AUTHORIZED_SSH_PUBLIC_KEY[@]}"; do
SSH_KEYS_FORMATTED+=" $key"$'\n'
done
else
SSH_KEYS_FORMATTED=" $AUTHORIZED_SSH_PUBLIC_KEY"$'\n'
fi
export SSH_KEYS_FORMATTED
echo -e "\n ✅ Generating sops-nix keys..."
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 > ${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 ${EXTRA_FILES_PATH}/etc/nixos/secrets/secrets.yaml
}
sum_up() {
DISK_RECAP_CONTENT=$(cat << EOF
### Disk Configuration Summary
Please review the selected disk layout before proceeding.
**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :**
* **Boot 1:** \`${BOOT_DISKS_ID_LIST[0]}\`
$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Boot 2:** \`${BOOT_DISKS_ID_LIST[1]}\`" )
**Data Disks ($CONTENT_DISK_NUMBER) :**
$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Data ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done )
$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" )
**Parity Disks ($PARITY_DISK_NUMBER) :**
$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Parity ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done )
$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" )
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${DISK_RECAP_CONTENT}")"
gum confirm "➡️ Proceed with this disk configuration?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
SERVICES_RECAP_CONTENT=$(cat << EOF
### Services Configuration Summary
Please review the selected services before proceeding.
**DNS Service (${#SELECTED_DNS_SERVICE[@]}) :**
$(echo "* \`${SELECTED_DNS_SERVICE[0]^}\`")
**Web Applications (${#SELECTED_WEB_APPLICATIONS[@]}) :**
$(for app in "${SELECTED_WEB_APPLICATIONS[@]}"; do echo "* \`${app^}\`"; done)
**System Services (${#SELECTED_SYSTEM_SERVICES[@]}) :**
$(for service in "${SELECTED_SYSTEM_SERVICES[@]}"; do echo "* \`${service^}\`"; done)
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${SERVICES_RECAP_CONTENT}")"
gum confirm "➡️ Proceed with this services configuration?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
DISK_RECAP_CONTENT=$(cat << EOF
### Secrets Summary
Please save the following secrets to a secure place (i.e. your local password manager, or a hidden sheet of paper).
**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :**
* **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 ${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 ${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${j} )\`" && j=$((j + 1)); done )
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${DISK_RECAP_CONTENT}")"
gum confirm "✅ I have stored these credentials in a safe place" || { echo -e "\n\n❌ Please store these credentials in a safe place as you will need them later."; exit 1; }
gum confirm "➡️ Would you like to manually edit the configuration (⚠️ advanced users only)" || { echo -e "\n\n✅ continuing with the installation..."; return 0; }
nano ${EXTRA_FILES_PATH}/etc/nixos/configuration.nix
}
cloudflare_dns_setup() {
gum confirm "➡️ This script can automatically create DNS records for your services. Proceed? (recommended)" || { echo -e "\n\n ⚠️ skipping the DNS records creation step..."; return 0; }
local ZONE_ID
local RECORD_COUNT
local IS_MATCHING
local DNS_RECORDS
create_records() {
local SUBDOMAIN="${1}"
local CREATION_STATUS
CREATION_STATUS=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${HOME_SERVER_IP}\",\"ttl\":1,\"proxied\":false}" | jq -r '.success')
if [[ "${CREATION_STATUS}" == "true" ]]; then
echo " ✅ Successfully created a DNS record for ${SUBDOMAIN}"
else
echo -e "❌ Failed to create a DNS record for ${SUBDOMAIN}. Check documentation to \n
learn how you can create them manually."
fi
}
erase_records() {
local SUBDOMAIN="${1}"
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'WARNING:') One or more existing type A DNS records found for \`${SUBDOMAIN}\`.
This script can clear those DNS records for you and create the correct ones needed for the server.
If you are unsure that these records are actually in use, please select \"no\"."
gum confirm "Select \"yes\" to clear ALL EXISTING type A DNS records for this subdomain and automatically create the correct ones." \
|| { echo -e "\n ⚠️ DNS records for ${SUBDOMAIN} will not be updated"; return 0; }
RECORD_IDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${SUBDOMAIN}&type=A" \
-H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \
-H "Content-Type: application/json" | jq -r '.result[].id')
for id in ${RECORD_IDS}; do
curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${id}" \
-H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \
-H "Content-Type: application/json" > /dev/null 2>&1
done
create_records "${SUBDOMAIN}"
}
echo -e "\n\n ☁️ Configuring Cloudflare DNS records..."
i=0
for service in "${SELECTED_WEB_APPLICATIONS[@]}"; do
if [[ -n "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]:-}" ]]; then
SELECTED_SERVICES_DNS+=( "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]}.${DOMAIN_NAME}" )
else
SELECTED_SERVICES_DNS+=( "${service}.${DOMAIN_NAME}" )
fi
i=$((i + 1))
[[ "${service}" == "nextcloud" ]] && SELECTED_SERVICES_DNS+=( "onlyoffice.${DOMAIN_NAME}" "whiteboard.${DOMAIN_NAME}" )
done
if [[ -n "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]:-}" ]]; then
SELECTED_SERVICES_DNS+=( "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]}.${DOMAIN_NAME}" )
else
SELECTED_SERVICES_DNS+=( "${SELECTED_DNS_SERVICE}.${DOMAIN_NAME}" )
fi
# Get Zone ID
ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN_NAME}" \
-H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \
-H "Content-Type: application/json" | jq -r '.result[0].id')
if [[ "${ZONE_ID}" == "null" || -z "${ZONE_ID}" ]]; then
echo -e "\n\n ⚠️ Could not fetch Zone ID for ${DOMAIN_NAME}. Please check your Cloudflare \"DNS ZONE\" API token"
echo "Check the Numbus-Server documentation to learn how to get one."
fi
# Check for existing records and create them if non-existent
for service_domain in "${SELECTED_SERVICES_DNS[@]}"; do
DNS_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${service_domain}&type=A" \
-H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \
-H "Content-Type: application/json")
RECORD_COUNT=$(echo "${DNS_RECORDS}" | jq '.result | length')
if [[ "${RECORD_COUNT}" -eq 0 ]]; then
echo -e "\n ⚠️ No DNS record found for ${service_domain}"
create_records "${service_domain}"
elif [[ "${RECORD_COUNT}" -eq 1 ]]; then
if [[ $(echo "${DNS_RECORDS}" | jq ".result[0].content == \"${HOME_SERVER_IP}\"") == "true" ]]; then
echo -e "\n ✅ DNS record already configured for ${service_domain}"
else
echo -e "\n ⚠️ No DNS record found for ${service_domain}"
erase_records "${service_domain}"
fi
elif [[ "${RECORD_COUNT}" -gt 1 ]]; then
erase_records "${service_domain}"
fi
done
}
export_configuration() {
cp -${FILES_CP_FLAGS} deploy.conf ${EXTRA_FILES_PATH}/var/lib/numbus-server/numbus-server.conf
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
echo -e "\n# SERVER SETTINGS" >> $CONFIG_EXPORT_FILE
echo "export SERVER_OWNER_NAME=\"${SERVER_OWNER_NAME:-User}\"" >> $CONFIG_EXPORT_FILE
echo -e "\n# DISK SETTINGS" >> $CONFIG_EXPORT_FILE
echo "export BOOT_DISKS_ID_LIST=\"(${BOOT_DISKS_ID_LIST[@]})\"" >> $CONFIG_EXPORT_FILE
echo "export DATA_DISKS_ID=\"(${DATA_DISKS_ID[@]})\"" >> $CONFIG_EXPORT_FILE
echo "export DATA_DISKS_TYPE=\"(${DATA_DISKS_TYPE[@]})\"" >> $CONFIG_EXPORT_FILE
echo "export SPINDOWN_DISKS_LIST=\"(${SPINDOWN_DISKS_LIST[@]})\"" >> $CONFIG_EXPORT_FILE
echo "export CONTENT_DISK_NUMBER=\"${CONTENT_DISK_NUMBER}\"" >> $CONFIG_EXPORT_FILE
echo "export PARITY_DISK_NUMBER=\"${PARITY_DISK_NUMBER}\"" >> $CONFIG_EXPORT_FILE
echo -e "\n# TPM SETTINGS" >> $CONFIG_EXPORT_FILE
echo "export TARGET_TPM=\"${TARGET_TPM}\"" >> $CONFIG_EXPORT_FILE
echo "export TARGET_TPM_VERSION=\"${TARGET_TPM_VERSION:-}\"" >> $CONFIG_EXPORT_FILE
}
deploy() {
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 ./${EXTRA_FILES_PATH}/etc/nixos
nix run github:nix-community/nixos-anywhere -- \
--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}
echo -e "\n\n✅ Installation successfull !"
sleep 1
}
postrun_action() {
TARGET_USER="numbus-admin"
LIVE_TARGET_IP="${HOME_SERVER_IP}"
LIVE_TARGET_PASSWORD="changeMe!"
echo -e "\n\n Now the remote machine will reboot. You will need to input the boot disk(s) passphrase.
This will be the only time you will have to do so, it will be automatic in the future."
gum spin --title "Rebooting the remote..." -- sleep 120
gum confirm "➡️ Select \"yes\" once the machine rebooted and you unlocked the disks." || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
FOUND="false"
i="0"
while [[ "${FOUND}" == "false" ]]; do
if ping -c1 -W1 $HOME_SERVER_IP >/dev/null 2>&1; then
FOUND="true"
echo -e "\n✅ Ping ${HOME_SERVER_IP} successful ! Continuing..."
else
i=$((i + 1))
if [[ "${i}" -gt 150 ]]; then
echo -e "\n\n❌ Could not connect to the server after 150 retries. \
This is most likely due to a networking issue. Please double check your network settings. Aborting."
exit 1
fi
fi
done
if [[ "${TARGET_TPM}" == "true" && ${TARGET_TPM_VERSION} -eq 2 ]]; then
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
A TPM version 2 has been detected on the system. You can choose to enable automatic disk decryption on boot.
Enabling automatic disk decryption on boot means that you won't have to enter your disk password everytime you start your server.
This comes in very handy if you don't plan to leave your server accessible with a keyboard or if you don't have an IP KVM.
Note : This feature is currently vulnerable to on-site attacks. This means that an attacker with physical access to your machine
could steal the password from the TPM, and therefore have access to all your date.
Do you want to enable automatic disk decryption on boot ?"
if gum confirm "➡️ I understand, 'yes' to proceed."; then
sshpass -p "${LIVE_TARGET_PASSWORD}" 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[@]})
DEBUG=${DEBUG}
DISK_PATH=""
j=1
for i in \${!BOOT_DISKS_NAME[@]}; do
if echo "\${BOOT_DISKS_NAME[\${i}]}" | grep -iq "nvme"; then
[[ "\${DEBUG}" == "true" ]] && echo "NVMe detected..."
DISK_PATH="/dev/\${BOOT_DISKS_NAME[\${i}]}p2"
else
[[ "\${DEBUG}" == "true" ]] && echo "Non-NVMe drive detected..."
DISK_PATH="/dev/\${BOOT_DISKS_NAME[\${i}]}2"
fi
[[ "\${DEBUG}" == "true" ]] && echo "Issuing enroll command for disk \${DISK_PATH}..."
echo ${LIVE_TARGET_PASSWORD} | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-\${j} \${DISK_PATH}
j=\$((j + 1))
done
echo "Getting PCRS 15 hash..."
PCR_HASH=\$(echo ${LIVE_TARGET_PASSWORD} | sudo -S systemd-analyze pcrs 15 --json=short)
echo ${LIVE_TARGET_PASSWORD} | sudo -S sed -i "s|PCR_HASH|\${PCR_HASH}|" /etc/nixos/configuration.nix
EOF
else
echo "Skipping TPM configuration."
fi
else
echo "No supported TPM detected (TPM version 2 required). Skipping TPM configuration."
fi
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'WARNING:') You will now set the password of the numbus-admin user.
You will almost never user it. Consider using a very strong password : you can write it down
securely on a hidden sheet of paper or add it to your password manager (locally with Passbolt
with any other online password manager provider)."
gum confirm "➡️ I understand, 'yes' to proceed." || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
echo $LIVE_TARGET_PASSWORD | sudo -S passwd numbus-admin
}
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 ${EXTRA_FILES_PATH}/etc/nixos#numbus-server
}
# --- MAIN FUNCTIONS ---<
# --- DEFAULT VARIABLES --->
WEBSERVER_PORT=${WEBSERVER_PORT:-8088}
LIVE_DATA_PATH="/run/numbus/web/live_settings.json"
HARDWARE_DATA_PATH="/run/numbus/web/hardware.json"
BRIDGE_SCRIPT="web/logic/bridge.py"
CONFIG_FILE="config/numbus.yaml"
TARGET_USER="nixos"
TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")"
EXTRA_FILES_PATH="${TMP_FILES_PATH}/config"
if [[ ${DEBUG-0} -eq 1 ]]; then
FILES_CP_FLAGS="vau"
FILES_RM_FLAGS="vf"
DIR_RM_FLAGS="rvf"
MKDIR_FLAGS="pv"
MV_FLAGS="vu"
else
DEBUG=0
FILES_CP_FLAGS="au"
FILES_RM_FLAGS="f"
DIR_RM_FLAGS="rf"
MKDIR_FLAGS="p"
MV_FLAGS="u"
fi
IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
PORT_REGEX='^[0-9]{1,5}$'
SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*'
PHONE_REGEX='^\+[1-9][0-9]{7,14}$'
GUM_INPUT_PADDING="1 1"
GUM_INPUT_HEADER_FOREGROUND="212"
GUM_INPUT_CURSOR_FOREGROUND="212"
GUM_INPUT_TIMEOUT="3600"
# --- DEFAULTS VARIABLES ---<
# --- PRE MAIN LOGIC --->
set -euo pipefail
clear
trap cleanup EXIT
compatibility_check
# --- PRE MAIN LOGIC ---<
# --- MAIN LOGIC --->
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 (TUI)" ]]; then
WEB_MODE=0
preparation
configuration
else
WEB_MODE=1
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."
INTERNATIONALIZATION_LANGUAGE=$(jq -r '.language' ${LIVE_DATA_PATH})
COUNTRY=$(jq -r '.country' ${LIVE_DATA_PATH})
INTERNATIONALIZATION_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})
fi
# --- MAIN LOGIC ---<
# 3. Load Credentials and run Discovery
setup_ssh
hardware_detection
if [[ ${DEBUG} -eq 1 ]]; then
echo -e "\n ✅ Discovery complete. Hardware data sent to Configurator."
fi
# 4. Wait for Final Configuration Submission
if [[ ${DEBUG} -eq 1 ]]; then
echo -e "\n ⏳ Waiting for final configuration deployment signal..."
fi
while [ ! -f configurator/.deploy_signal ]; do
sleep 1
done
# 5. Execute Deployment
echo -e "\n🚀 Starting deployment sequence..."
deploy > deploy-out.log 2> deploy-err.log