Compare commits

91 Commits

Author SHA1 Message Date
Raphaël Numbus 0d2384456f Bugfixes. 2026-05-29 22:14:02 +02:00
Raphaël Numbus 185988d411 Added sensitive input. 2026-05-29 22:11:42 +02:00
Raphaël Numbus f38ab53719 Fixed syntax error. 2026-05-29 22:06:42 +02:00
Raphaël Numbus f77e395635 Updated IPV4 regex. 2026-05-29 22:04:52 +02:00
Raphaël Numbus 5ad0584e9e Updated IPV4 regex. 2026-05-29 21:59:45 +02:00
Raphaël Numbus fc92aee0fe Misc fixes. Updated IPV4 regex. 2026-05-29 21:38:05 +02:00
Raphaël Numbus 1f508ad264 Debugging SSH setup. 2026-05-29 21:28:09 +02:00
Raphaël Numbus a3fc5e8f17 Debugging SSH setup. 2026-05-29 21:13:21 +02:00
Raphaël Numbus 74ebd6339c Debugging SSH setup. 2026-05-29 21:08:29 +02:00
Raphaël Numbus ac1a03ace7 Debugging SSH setup. 2026-05-29 21:05:26 +02:00
Raphaël Numbus e3c0370b0e Debugging SSH setup. 2026-05-29 21:03:50 +02:00
Raphaël Numbus c61a45e5d7 Debugging SSH setup. 2026-05-29 21:02:42 +02:00
Raphaël Numbus bf33639749 Debugging SSH setup. 2026-05-29 21:01:45 +02:00
Raphaël Numbus f186ac502a Improved the SSH error handling. 2026-05-29 20:55:50 +02:00
Raphaël Numbus d0e08c0f76 Improved the SSH error handling. 2026-05-29 20:17:03 +02:00
Raphaël Numbus a23dd9dc22 Improved the SSH error handling. 2026-05-29 20:11:58 +02:00
Raphaël Numbus a34232b489 Fixed syntax error. 2026-05-29 16:55:14 +02:00
Raphaël Numbus f6063fe153 Improved the SSH error handling. 2026-05-29 16:53:24 +02:00
Raphaël Numbus 39104ecf92 Improved the SSH error handling. 2026-05-29 16:10:59 +02:00
Raphaël Numbus 92e0f077a4 Changed known_hosts file location. 2026-05-29 16:04:04 +02:00
Raphaël Numbus 9376a87caa Added duration unit. 2026-05-29 16:02:09 +02:00
Raphaël Numbus 9973b8054e Removed Gum unnecessary flags. 2026-05-29 16:01:22 +02:00
Raphaël Numbus faf8e9816a Hide output. Improve the error messages. 2026-05-29 15:56:41 +02:00
Raphaël Numbus f35b917362 Export the Gum variables to make them work. Added descriptive text. 2026-05-29 15:54:45 +02:00
Raphaël Numbus 873fed9fd2 Changed known_hosts file location. 2026-05-29 15:52:03 +02:00
Raphaël Numbus 3680768414 Added quotes to var. 2026-05-29 15:50:28 +02:00
Raphaël Numbus 514d13d8ff Improved SSH known_hosts error handling. Added error codes file. 2026-05-29 15:47:16 +02:00
Raphaël Numbus 91d90be2a9 Creds test 2026-05-28 09:44:44 +02:00
Raphaël Numbus 8f8a8b4be4 Misc change 2026-05-28 09:42:48 +02:00
Raphaël Numbus 35fbe3ee12 Removed unnecessary STDOUT and STDERR redirections 2026-05-28 09:40:17 +02:00
Raphaël Numbus 26441e5130 Misc changes 2026-05-28 09:38:11 +02:00
Raphaël Numbus d4af5bbdb1 Bugfixes. 2026-05-25 22:06:50 +02:00
Raphaël Numbus 55c51c7205 Improved disks part of hardware detection. 2026-05-25 22:02:34 +02:00
Raphaël Numbus 297df7bb2e Debugging. 2026-05-25 21:37:16 +02:00
Raphaël Numbus d3bdf02a6a Debugging. 2026-05-25 21:28:35 +02:00
Raphaël Numbus 8752e4ff16 Debugging. 2026-05-25 21:26:48 +02:00
Raphaël Numbus 5b63d890ca Debugging. 2026-05-25 21:22:39 +02:00
Raphaël Numbus aeea4d23c5 Hide nixos configuration generation stdoutput. 2026-05-25 21:18:23 +02:00
Raphaël Numbus 74e5211bee Changed variable name. 2026-05-25 21:14:21 +02:00
Raphaël Numbus e77174e595 Get GPUs JSON output. 2026-05-25 21:11:53 +02:00
Raphaël Numbus bad0e9ee84 Added unknown brand. 2026-05-25 21:04:51 +02:00
Raphaël Numbus 49fd4aeb5a Escaped variables. 2026-05-25 20:57:56 +02:00
Raphaël Numbus 3c5a746413 Get GPUs JSON output. 2026-05-25 20:56:25 +02:00
Raphaël Numbus 568d5aa165 Get sudo working. 2026-05-25 20:45:29 +02:00
Raphaël Numbus f83efd001e Fix logs path. 2026-05-25 20:42:58 +02:00
Raphaël Numbus 6982bf70c9 Try to fix rational expression error. 2026-05-25 20:41:00 +02:00
Raphaël Numbus c8d496039a Changed target_user for testing purposes. 2026-05-25 20:32:36 +02:00
Raphaël Numbus 8c0e3acf20 Fixed permission problem. Improving debugging. 2026-05-25 19:18:44 +02:00
Raphaël Numbus 44a02b36af Escaped remote variables. 2026-05-25 19:07:22 +02:00
Raphaël Numbus 1eacc23288 Improved graphics detection. 2026-05-25 19:05:22 +02:00
Raphaël Numbus 354b3a8aa8 Improving debugging. 2026-05-25 17:31:43 +02:00
Raphaël Numbus adbbda4a9d Improved debugging with clean up function. 2026-05-25 17:25:41 +02:00
Raphaël Numbus 0f070e1124 Fixed No such file or dir error. 2026-05-25 17:17:06 +02:00
Raphaël Numbus cba5cea39d Fixed > typo. 2026-05-25 17:14:44 +02:00
Raphaël Numbus 19ae7369e3 Fix SSHEND error. 2026-05-25 17:13:25 +02:00
Raphaël Numbus e3142e3032 Improved debugging features. Added quotes to live.yaml. 2026-05-25 17:11:59 +02:00
Raphaël Numbus f989dd8f3a Added quotes to live.yaml. 2026-05-25 17:04:37 +02:00
Raphaël Numbus 50f624e9e4 Improved debugging features. Added quotes to live.yaml. Improving hardware detection. 2026-05-25 16:54:57 +02:00
Raphaël Numbus 5d4b8eb77f Updated index.html for better responsive design. 2026-05-25 15:56:28 +02:00
Raphaël Numbus 61c8c38853 Moved hierarchy preparation earlier in the script. 2026-05-17 20:06:56 +02:00
Raphaël Numbus 5aeec9ac89 Added missing ssh setup. 2026-05-17 20:05:10 +02:00
Raphaël Numbus 6bf9a575bb Fixed syntax error. 2026-05-17 19:55:29 +02:00
Raphaël Numbus 440f4c8fb5 Update the logic to wait for the hardware detection data. 2026-05-17 19:52:02 +02:00
Raphaël Numbus de890387d5 Allow user to continue without filling the git url/username/password when interactive mode selected. 2026-05-17 15:22:47 +02:00
Raphaël Numbus 20553b472f Make the startDiscovery button work. 2026-05-17 15:12:32 +02:00
Raphaël Numbus ff450a5770 Python bridge now works and serves files correctly. Only necessary folders are accessible from the browser. 2026-05-17 14:43:35 +02:00
Raphaël Numbus 7a6202c014 Trying to get python bridge to work. 2026-05-17 14:31:46 +02:00
Raphaël Numbus 27a5eca7f8 Trying to get python bridge to work. 2026-05-17 14:24:57 +02:00
Raphaël Numbus df5132e97a Trying to get python bridge to work. 2026-05-17 14:21:23 +02:00
Raphaël Numbus a8de439ece Trying to get python bridge to work. 2026-05-17 14:14:50 +02:00
Raphaël Numbus 2bed788e92 Trying to get python bridge to work. 2026-05-17 14:08:54 +02:00
Raphaël Numbus 3a7c5bed8d Revert changes 2026-05-17 14:08:12 +02:00
Raphaël Numbus a070e90576 Trying to get python bridge to work. 2026-05-17 14:07:41 +02:00
Raphaël Numbus bf4d973a32 Trying to get html images to work. 2026-05-17 14:00:36 +02:00
Raphaël Numbus 8c4dc60b68 Trying to get python bridge to work. 2026-05-17 13:42:16 +02:00
Raphaël Numbus c5d81677a1 Laid a base for better hardware detection. Removed unnecessary packages. Removed unnecessary TUI. 2026-05-17 13:40:08 +02:00
Raphaël Numbus 30745adf4f Added folders needed by the script. Removed TUI. 2026-05-15 22:33:41 +02:00
Raphaël Numbus 36c1039df6 Added sleep timers. 2026-05-15 22:25:27 +02:00
Raphaël Numbus 27433d6258 Added sleep timers. 2026-05-15 22:24:47 +02:00
Raphaël Numbus 4ca5ae5c8f Removed a lot of unnecessary code. Now all configuration is done through the browser. Need to work more on the deployment process. 2026-05-15 22:23:49 +02:00
Raphaël Numbus 522ee16611 Update non-interactive python bridge. Updated index.html for the new folder hierachy. 2026-05-15 21:54:41 +02:00
Raphaël Numbus b53f3be190 Added the index.html file. 2026-05-15 13:27:15 +02:00
Raphaël Numbus 4811714b0a Moved preparation file to index.html 2026-05-15 13:24:31 +02:00
Raphaël Numbus 15ba7797b0 Added a remove command to prevent directory already exists error. 2026-05-15 12:09:28 +02:00
Raphaël Numbus 3cfc72221d Added a branch selector for testing purposes. 2026-05-15 11:52:48 +02:00
Raphaël Numbus 96be47824f Moved clear command from parent to child process. 2026-05-15 11:49:57 +02:00
Raphaël Numbus 17a6affa85 Moved clear command from child to parent process. 2026-05-15 11:47:48 +02:00
Raphaël Numbus f55e12c039 Added missing -e. 2026-05-15 11:44:56 +02:00
Raphaël Numbus e7e354b0e7 Removed compatibility check from deploy script and other now useless stuff. 2026-05-15 11:43:51 +02:00
Raphaël Numbus e9628a112c Moved the compatibility check from the deploy script to the deploy script to the start script. Moved install files and folders to /run/user for better security. 2026-05-15 11:38:52 +02:00
Raphaël Numbus 7aae0c5ab5 Added a bootstrap script 2026-05-15 10:58:41 +02:00
15 changed files with 630 additions and 774 deletions
+1
View File
@@ -1,5 +1,6 @@
/config/
web/ux/
test*
example*
.DS_Store
.env
+3
View File
@@ -0,0 +1,3 @@
{
"liveServer.settings.multiRootWorkspaceName": "numbus"
}
+396 -564
View File
@@ -1,5 +1,5 @@
#!/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
#!nix-shell -i bash -p bash coreutils gnused gum xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto curl jq yq python3
@@ -14,7 +14,7 @@ echod() {
ssh_to_host() {
local COMMAND="${1}"
ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}"
ssh -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}"
}
get_valid_input() {
@@ -32,12 +32,12 @@ get_valid_input() {
fi
while true; do
local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}")
local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}" --password="${SENSITIVE}")
# Handle empty input
if [[ -z "${INPUT}" ]]; then
if [[ "${MANDATORY}" == true ]]; then
gum style --foreground "#ff0000" -- " This field is mandatory."
gum style --foreground "#ff0000" -- " This field is mandatory."
continue
else
INPUT=""
@@ -51,7 +51,7 @@ get_valid_input() {
export "${VAR_NAME}"="${INPUT}"
break
else
gum style --foreground "#ff0000" -- " Invalid format. Please try again."
gum style --foreground "#ff0000" -- " Invalid format. Please try again."
fi
else
export "${VAR_NAME}"="${INPUT}"
@@ -65,196 +65,357 @@ get_valid_input() {
# --- GLOBAL FUNCTIONS --->
cleanup() {
echo -e "\n ✅ Cleaning up..."
if [[ "${DEBUG}" -eq 1 ]]; then
echo -e "\n ✅ Exiting..."
echo -e "\n ✅ Debug mode is enabled. Clean up manually files located at ${INSTALL_DIR} or reboot."
else
echo -e "\n ✅ Cleaning up..."
rm -${DIR_RM_FLAGS} "${INSTALL_DIR}"
fi
rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/
if ps -p ${BRIDGE_PID:-} > /dev/null; then
if [[ -n "${BRIDGE_PID:-}" ]] && ps -p ${BRIDGE_PID} >> "${STDOUT}" 2>> "${STDERR}"; then
kill ${BRIDGE_PID}
fi
echo -e "\n 🌟 Thanks for using Numbus, consider supporting the project !"
}
compatibility_check() {
TEST_FAIL=0
launch_gui() {
echo -e "\n 🚀 Launching Numbus Configurator..."
echo -e " ➡️ You will now proceed to the configuration of your device through your browser"
if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then
echo -e "\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
python3 "${BRIDGE_SCRIPT}" >> "${STDOUT}" 2>> "${STDERR}" &
export BRIDGE_PID=$!
local START_URL="http://localhost:${WEBSERVER_PORT}/pages/index.html"
if [[ "$(uname -m)" == "x86_64" ]]; then
echo -e "\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
xdg-open "${START_URL}" >> "${STDOUT}" 2>> "${STDERR}" || open "${START_URL}" >> "${STDOUT}" 2>> "${STDERR}" || true
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
sleep 5
return 0
echo -e "\n ⚠️ If it doesn't automatically, open your browser at: $(gum style "${START_URL}")"
}
hierarchy_preparation() {
echod "\n 🔄 Preparing the folder hierarchy for the final configuration..."
echod "\n 🔄 Preparing the folder hierarchy for the final configuration...\n"
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
# Extra files folders
mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/
mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/var/lib/sops-nix/
mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/etc/nixos/secrets/disks
mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/etc/nixos/secrets/system
mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/etc/nixos/secrets/podman
echod "\n ✅ Folder hierarchy ready"
}
setup_ssh() {
edit_var() {
echo -e "${1}"
echo -e " Please check the credentials provided in the configuration."
echo -e "\n ➡️ Here are the current settings :
Target IP address : $(gum style --italic "\"${LIVE_TARGET_IP}\"")
Target password : $(gum style --italic "\"${LIVE_TARGET_PASSWORD}\"")"
gum confirm "Are these correct ?" || {
get_valid_input "LIVE_TARGET_IP" "➡️ Provide the IP address of your machine in a NixOS live environment :" "192.168.1.100" "${IP_REGEX}";
get_valid_input "LIVE_TARGET_PASSWORD" "➡️ Provide the password of your machine in a NixOS live environment :" "password" "" "true" "true";
return 0;
}
gum confirm "Retry connection ?" || {
echo -e "\n ❌ Host unreachable or connection refused.";
exit 226;
}
}
echod "\n ➡️ Generating new SSH key for numbus-admin..."
chmod 700 "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/"
ssh-keygen -t "ed25519" -C "numbus-admin@numbus-${DEVICE_TYPE}" -f "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q
echod "\n ➡️ Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..."
while true; do
if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}"; then
echod "\n ✅ SSH key copied successfully"
return 0
else
local EXIT_CODE=$?
if [[ ${EXIT_CODE} -eq 5 ]]; then
edit_var "\n ❌ Invalid password for ${TARGET_USER}@${LIVE_TARGET_IP}."
elif ! ping -c 2 "${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}"; then
edit_var "\n ❌ The IP address you specified cannot be reached."
elif ssh-keygen -F "${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}"; then
echo -e "\n ⚠️ The SSH fingerprint for the selected IP address $(gum style --italic "\"${LIVE_TARGET_IP}\"") is not the same as the one in $(gum style --italic "\".ssh/known_hosts\"").
This could occur for multiple reasons :
- You ran this script multiple times
- Your live machine uses an IP address that was used by another devices you SSHed in
- You are under a Man-In-The-Middle attack
- Other
The script $(gum style --bold "cannot continue") without the correct fingerprint installed.
If you are unsure, it is always better to check manually.\n"
gum confirm "Remove the old fingerprint and accept the new one ?" || {
echo -e "\n ❌ SSH fingerprints don't match.";
exit 22;
}
ssh-keygen -R "${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}"
fi
fi
done
}
hardware_detection() {
local TMPFILE="/tmp/nixos-installation-hw-detection"
local TMPFILE="/run/user/1000/numbus-installer/hw_detection.json"
ssh_to_host 'bash -s' << SSHEND
TARGET_GRAPHICS_BRAND=()
ssh_to_host "nix-shell -p jq pciutils usbutils smartmontools iproute2 --run 'bash -s'" << SSHEND >> "${STDOUT}" 2>> "${STDERR}"
set -euo pipefail
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
HW_REPORT=\$(jq -n '{}')
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=""
append_to_report() {
local key="\${1}"
local json_array="\${2}"
HW_REPORT=\$(echo "\${HW_REPORT}" | jq --argjson arr "\${json_array}" --arg k "\${key}" '.[\${k}] = \${arr}')
}
TARGET_INTERFACE=\$(ip -4 route show default | awk '{print \$5}' | head -n1)
detect_graphics() {
local gpus="[]"
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
while read -r line; do
[[ -z "\${line}" ]] && continue
local brand="unknown"
local renderer="none"
local product="unknown"
local integrated="false"
local pci_addr="none"
HDD=1
DISK_DEVPATH=()
DISK_NAME=()
DISK_TYPE=()
DISK_HEALTH=()
DISK_ID=()
# Extract PCI address
pci_addr="\$(echo "\${line}" | cut -d' ' -f1)"
for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do
# Brand
for b in Intel AMD NVIDIA; do
if echo "\${line}" | grep -iq "\${b}"; then
brand="\${b}"
break
fi
done
# 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")
# Renderer
if [[ -d "/dev/dri/by-path" ]]; then
local render_node=\$(ls /dev/dri/by-path | grep "\${pci_addr}" | grep "render" | head -n1 || true)
if [[ -n "\${render_node}" ]]; then
renderer=\$(basename "\$(readlink -f "/dev/dri/by-path/\${render_node}")")
fi
fi
# Product name
product="\${line#*:}"
product="\${product#*: }"
# Form factor
if [[ "\${brand}" == "NVIDIA" ]]; then
integrated="false"
fi
if [[ "\${brand}" == "Intel" ]]; then
if echo "\${line}" | grep -Ei "HD Graphics|Xe"; then
integrated="true"
else
integrated="false"
fi
fi
if [[ "\${brand}" == "AMD" ]]; then
if echo "\$line" | grep -i "Mobile"; then
integrated="true"
else
integrated="false"
fi
fi
local obj=\$(jq -n \
--arg b "\${brand}" \
--arg r "\${renderer}" \
--arg p "\${product}" \
--argjson i "\${integrated}" \
'{brand: \$b, renderer: \$r, product: \$p, integrated: \$i}')
gpus=\$(echo "\$gpus" | jq --argjson obj "\$obj" '. += [\$obj]')
done < <(lspci | grep -Ei "VGA|3D")
append_to_report "graphics" "\$gpus"
}
# --- 2. Detect Coral TPUs ---
detect_corals() {
local corals="[]"
# Check PCIe Coral (Google ID 1ac1:089a)
if lspci -nn | grep -iq "1ac1:089a"; then
local pcie_count
pcie_count=\$(lspci -nn | grep -ic "1ac1:089a")
for ((i=1; i<=pcie_count; i++)); do
local obj=\$(jq -n --arg i "PCIe" '{interface: \$i, type: "Edge TPU"}')
corals=\$(echo "\$corals" | jq --argjson obj "\$obj" '. += [\$obj]')
done
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")
# Check USB Coral (Google ID 18d1:9302)
if lsusb | grep -iq "18d1:9302"; then
local usb_count
usb_count=\$(lsusb | grep -ic "18d1:9302")
for ((i=1; i<=usb_count; i++)); do
local obj=\$(jq -n --arg i "USB" '{interface: \$i, type: "Edge TPU"}')
corals=\$(echo "\$corals" | jq --argjson obj "\$obj" '. += [\$obj]')
done
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
append_to_report "coral_devices" "\$corals"
}
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
# --- 3. Detect Zigbee Coordinators ---
detect_zigbee() {
local zigbees="[]"
local serial_dir="/dev/serial/by-id"
scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
source "${TMPFILE}"
if [[ -d "\$serial_dir" ]]; then
for dev in "\$serial_dir"/*; do
[[ -e "\$dev" ]] || continue # skip if empty directory pattern matched
# Match common Zigbee adapter names (Sonoff, ConBee, ITead, CC2531, etc.)
if echo "\$dev" | grep -iE 'zigbee|conbee|sonoff|cc2531|efr32|itead'; then
local obj=\$(jq -n --arg p "\$dev" '{device_path: \$p}')
zigbees=\$(echo "\$zigbees" | jq --argjson obj "\$obj" '. += [\$obj]')
fi
done
fi
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]}")
append_to_report "zigbee_devices" "\$zigbees"
}
# --- 4. Detect Network Interfaces ---
detect_network() {
local networks="[]"
local default_iface
default_iface=\$(ip -4 route show default | awk '{print \$5}' | head -n1)
for iface_path in /sys/class/net/*; do
[[ -e "\$iface_path" ]] || continue
local iface
iface=\$(basename "\$iface_path")
# Skip loopback and virtual interfaces
[[ "\$iface" == "lo" ]] && continue
[[ -L "\$iface_path" && \$(readlink "\$iface_path") == *"virtual"* ]] && continue
local type="wired"
[[ -d "\$iface_path/wireless" ]] && type="wireless"
local is_default="false"
[[ "\$iface" == "\$default_iface" ]] && is_default="true"
local obj=\$(jq -n --arg n "\$iface" --arg t "\$type" --argjson d "\$is_default" \
'{name: \$n, type: \$t, default: \$d}')
networks=\$(echo "\$networks" | jq --argjson obj "\$obj" '. += [\$obj]')
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}
append_to_report "network_interfaces" "\$networks"
}
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"
# --- 5. Detect TPM ---
detect_tpm() {
local tpms="[]"
for tpm_dir in /sys/class/tpm/tpm*; do
[[ -e "\$tpm_dir" ]] || continue
local name
name=\$(basename "\$tpm_dir")
local version="Unknown"
if [[ -f "\$tpm_dir/tpm_version_major" ]]; then
version=\$(cat "\$tpm_dir/tpm_version_major")
fi
local obj=\$(jq -n --arg n "\$name" --arg v "\$version" '{name: \$n, version: \$v}')
tpms=\$(echo "\$tpms" | jq --argjson obj "\$obj" '. += [\$obj]')
done
append_to_report "tpm" "\$tpms"
}
# --- 6. Detect Disks ---
detect_disks() {
local disks="[]"
while read -r disk_name; do
# Disk Type Mapping
local disk_type="unknown"
local disk_transport=\$(lsblk -d -n -o TRAN "/dev/\$disk_name" || echo "unknown")
local rotational=\$(lsblk -d -n -o ROTA "/dev/\$disk_name" || echo "1")
if [[ "\$disk_name" == nvme* ]]; then
disk_type="NVMe"
elif [[ "\$rotational" == "1" ]]; then
disk_type="HDD"
elif [[ "\$rotational" == "0" ]]; then
disk_type="SSD"
fi
# Size in GB
local disk_size=\$(lsblk -d -n -o SIZE "/dev/\$disk_name" || echo "unknown")
# ID via by-id
local disk_id="unknown"
if [[ -d "/dev/disk/by-id" ]]; then
local id_match=\$(find /dev/disk/by-id/ -type l -not -name "wwn-*" -not -name "nvme-eui*" -printf "%p %l\n" | grep -m1 "/\$disk_name\$" | awk '{print \$1}')
[[ -n "\$id_match" ]] && disk_id="\$id_match"
fi
# Health
local health="unknown"
if echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$disk_name 2>/dev/null | grep -i "PASSED"; then
disk_health="Passed"
elif echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$disk_name 2>/dev/null | grep -i "FAILED"; then
disk_health="Failed"
fi
local obj=\$(jq -n \
--arg n "\${disk_name}" \
--arg t "\${disk_type}" \
--arg tr "\${disk_transport}" \
--arg h "\${disk_health}" \
--arg s "\${disk_size}" \
--arg i "\${disk_id}" \
'{name: \$n, type: \$t, transport: \$tr, health: \$h, size: \$s, id: \$i}')
disks=\$(echo "\$disks" | jq --argjson obj "\$obj" '. += [\$obj]')
done < <(lsblk -d -n -o NAME -e 7,11,252)
append_to_report "disks" "\$disks"
}
# --- Execution ---
detect_graphics
detect_corals
detect_zigbee
detect_network
detect_tpm
detect_disks
# --- Output ---
echo "\$HW_REPORT" | jq '.' > "$TMPFILE"
SSHEND
scp -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${HW_DATA_FILE}" >> "${STDOUT}" 2>> "${STDERR}"
[[ ${DEBUG} -eq 1 ]] && scp -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${REMOTE_STDOUT}" "${INSTALL_DIR}/web/logs/hw_std.log" >> "${STDOUT}" 2>> "${STDERR}"
[[ ${DEBUG} -eq 1 ]] && scp -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${REMOTE_STDERR}" "${INSTALL_DIR}/web/logs/hw_err.log" >> "${STDOUT}" 2>> "${STDERR}"
if ssh_to_host "echo \"${LIVE_TARGET_PASSWORD}\" | sudo -S nixos-generate-config --no-filesystems --show-hardware-config &> /dev/null" > ${TMP_EXTRA_PATH}/etc/nixos/hardware-configuration.nix; then
echo -e "\n ✅ Hardware configuration generated"
else
echo -e "\n❌ Failed to generate hardware configuration"
echo -e "\n ❌ Failed to generate hardware configuration"
exit 1
fi
}
@@ -262,269 +423,8 @@ SSHEND
# --- MAIN WEB FUNCTIONS --->
launch_gui() {
echo -e "\n ➡️ You will now proceed to the configuration of your device through your browser"
echo -e "\n 🚀 Launching Numbus Configurator..."
python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 &
export BRIDGE_PID=$!
xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true
sleep 5
echo -e "\n ➡️ If it doesn't automatically, open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")"
}
# --- 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." \
"Numbus Game Console : An unbreakable Steam bigscreen experience.")
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" ;;
"Numbus Game Console : "* ) DEVICE_TYPE="console" ;;
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
}
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
}
# --- MAIN SCRIPT FUNCTIONS --->
server_config_generation() {
echod "\n 📝 Generating structured settings.json..."
@@ -558,13 +458,13 @@ server_config_generation() {
enabledApps: $apps,
managementConsole: $cockpit_enabled
}
}' > "${EXTRA_FILES_PATH}/etc/nixos/settings.json"
}' > "${TMP_EXTRA_PATH}/etc/nixos/settings.json"
echo -e "{\n numbus.settings = builtins.fromJSON (builtins.readFile ./settings.json);\n}" > "${CONFIGURATION_PATH}"
# Ensure the settings file is writable by the management service
# and that the directory is prepared for local git tracking
chmod 664 "${EXTRA_FILES_PATH}/etc/nixos/settings.json"
chmod 664 "${TMP_EXTRA_PATH}/etc/nixos/settings.json"
}
# The existing network_config_generation and services_config_generation functions
@@ -600,8 +500,8 @@ disk_config_generation() {
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}"
echo -n "$PASS" > "${TMP_EXTRA_PATH}/etc/secrets/disks/boot-${i}"
chmod 600 "${TMP_EXTRA_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}"
@@ -610,8 +510,8 @@ 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}"
echo -n "$PASS" > "${TMP_EXTRA_PATH}/etc/secrets/disks/content-${i}"
chmod 600 "${TMP_EXTRA_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}
@@ -619,8 +519,8 @@ 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}"
echo -n "$PASS" > "${TMP_EXTRA_PATH}/etc/secrets/disks/parity-${i}"
chmod 600 "${TMP_EXTRA_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}
@@ -628,7 +528,7 @@ EOF
done
local SSH_KEYS_FORMATTED=""
if [[ "$(declare -p AUTHORIZED_SSH_PUBLIC_KEY 2>/dev/null)" =~ "declare -a" ]]; then
if [[ "$(declare -p AUTHORIZED_SSH_PUBLIC_KEY)" =~ "declare -a" ]]; then
for key in "${AUTHORIZED_SSH_PUBLIC_KEY[@]}"; do
SSH_KEYS_FORMATTED+=" $key"$'\n'
done
@@ -638,99 +538,18 @@ EOF
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)
ssh-to-age -private-key -i ${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${TMP_EXTRA_PATH}/var/lib/sops-nix/key.txt
export SOPS_PUBLIC_KEY=$(age-keygen -y ${TMP_EXTRA_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
envsubst < templates/nix-config/sops-nix/.sops.yaml > ${TMP_EXTRA_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
--output ${TMP_EXTRA_PATH}/etc/nixos/secrets/secrets.yaml
}
cloudflare_dns_setup() {
@@ -762,7 +581,7 @@ cloudflare_dns_setup() {
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}\`.
⚠️ $(gum style '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." \
@@ -775,7 +594,7 @@ cloudflare_dns_setup() {
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
-H "Content-Type: application/json" >> "${STDOUT}" 2>> "${STDERR}"
done
create_records "${SUBDOMAIN}"
@@ -835,24 +654,24 @@ cloudflare_dns_setup() {
}
deploy() {
git -C . add -f "${EXTRA_FILES_PATH}/"
git -C . add -f "${TMP_EXTRA_PATH}/"
git -C . add -f "templates/"
git -C . add -f "deploy.conf"
# Initialize a git repo in the configuration to be deployed
# This allows the Management UI on the appliance to commit changes
# and provide a local history/rollback UI to the user.
if [ ! -d "${EXTRA_FILES_PATH}/etc/nixos/.git" ]; then
git -C "${EXTRA_FILES_PATH}/etc/nixos" init -q
git -C "${EXTRA_FILES_PATH}/etc/nixos" add .
git -C "${EXTRA_FILES_PATH}/etc/nixos" commit -m "Initial bootstrap via Numbus Deploy" -q
if [ ! -d "${TMP_EXTRA_PATH}/etc/nixos/.git" ]; then
git -C "${TMP_EXTRA_PATH}/etc/nixos" init -q
git -C "${TMP_EXTRA_PATH}/etc/nixos" add .
git -C "${TMP_EXTRA_PATH}/etc/nixos" commit -m "Initial bootstrap via Numbus Deploy" -q
fi
echo -e "\n\n🔄 Deploying to the remote server..."
nix flake update --flake ./${EXTRA_FILES_PATH}/etc/nixos
nix flake update --flake ./${TMP_EXTRA_PATH}/etc/nixos
nix run github:nix-community/nixos-anywhere -- \
--flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server \
--extra-files ${EXTRA_FILES_PATH} \
--flake ${TMP_EXTRA_PATH}/etc/nixos#numbus-server \
--extra-files ${TMP_EXTRA_PATH} \
--chown "/home/numbus-admin/" 1000:1000 \
--target-host ${TARGET_USER}@${LIVE_TARGET_IP}
@@ -875,7 +694,7 @@ postrun_action() {
FOUND="false"
i="0"
while [[ "${FOUND}" == "false" ]]; do
if ping -c1 -W1 $HOME_SERVER_IP >/dev/null 2>&1; then
if ping -c1 -W1 $HOME_SERVER_IP >> "${STDOUT}" 2>> "${STDERR}"; then
FOUND="true"
echo -e "\n✅ Ping ${HOME_SERVER_IP} successful ! Continuing..."
else
@@ -900,7 +719,7 @@ postrun_action() {
Do you want to enable automatic disk decryption on boot ?"
if gum confirm "➡️ I understand, 'yes' to proceed."; then
sshpass -p "${LIVE_TARGET_PASSWORD}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF
sshpass -p "${LIVE_TARGET_PASSWORD}" ssh -i "${TMP_EXTRA_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[@]})
@@ -934,7 +753,7 @@ EOF
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.
⚠️ $(gum style '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)."
@@ -943,29 +762,24 @@ securely on a hidden sheet of paper or add it to your password manager (locally
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 ---<
# --- MAIN SCRIPT FUNCTIONS ---<
# --- DEFAULT VARIABLES --->
INSTALL_DIR="/run/user/$(id -u)/numbus-installer"
WEBSERVER_PORT=${WEBSERVER_PORT:-8088}
LIVE_DATA_PATH="/run/user/$(id -u)/numbus/web/live_settings.json"
HARDWARE_DATA_PATH="/run/user/$(id -u)/numbus/web/hardware.json"
LIVE_DATA_FILE="web/config/live.yaml"
HW_DATA_FILE="web/config/hardware.yaml"
CONFIG_FILE="web/config/numbus.yaml"
CONFIG_FILE="../config/numbus.yaml"
BRIDGE_SCRIPT="web/logic/interactive.py"
TARGET_USER="nixos"
TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")"
EXTRA_FILES_PATH="${TMP_FILES_PATH}/config"
TMP_EXTRA_PATH="${INSTALL_DIR}/extra"
if [[ ${DEBUG-0} -eq 1 ]]; then
FILES_CP_FLAGS="vau"
@@ -973,6 +787,10 @@ if [[ ${DEBUG-0} -eq 1 ]]; then
DIR_RM_FLAGS="rvf"
MKDIR_FLAGS="pv"
MV_FLAGS="vu"
STDOUT="${INSTALL_DIR}/web/logs/std.log"
STDERR="${INSTALL_DIR}/web/logs/err.log"
REMOTE_STDOUT="/run/user/1000/numbus-installer/std.log"
REMOTE_STDERR="/run/user/1000/numbus-installer/err.log"
else
DEBUG=0
FILES_CP_FLAGS="au"
@@ -980,9 +798,12 @@ else
DIR_RM_FLAGS="rf"
MKDIR_FLAGS="p"
MV_FLAGS="u"
STDOUT="/dev/null"
STDERR="/dev/null"
fi
IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
IP_OCTET='(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'
IP_REGEX="^${IP_OCTET}\\.${IP_OCTET}\\.${IP_OCTET}\\.${IP_OCTET}$"
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,}$'
@@ -990,10 +811,11 @@ 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"
export FOREGROUND="212"
export GUM_INPUT_PADDING="1 1"
export GUM_INPUT_HEADER_FOREGROUND="212"
export GUM_INPUT_CURSOR_FOREGROUND="212"
export GUM_INPUT_TIMEOUT="3600s"
# --- DEFAULTS VARIABLES ---<
@@ -1001,9 +823,7 @@ GUM_INPUT_TIMEOUT="3600"
# --- PRE MAIN LOGIC --->
set -euo pipefail
clear
trap cleanup EXIT
compatibility_check
# --- PRE MAIN LOGIC ---<
@@ -1016,21 +836,33 @@ echo """
/_/|_/\____/_/ /_/____/\____/___/
"""
DEPLOYMENT_STRATEGY=$(gum choose --header "Choose your preferred deployment strategy :" \
"I don't have a configuration" \
"I have a valid configuration hosted on a Git platform")
launch_gui
if [[ "${DEPLOYMENT_STRATEGY}" == "I don't have a configuration" ]]; then
BRIDGE_SCRIPT="../web/logic/interactive.py"
launch_gui
else
DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \
"Through my web browser (Recommended for beginners)" \
"Through my terminal (TUI)")
if [[ "${DEPLOYMENT_MODE}" == "Through my web browser (Recommended for beginners)" ]]; then
BRIDGE_SCRIPT="../web/logic/non-interactive.py"
launch_gui
else
launch_tui
fi
fi
until [[ -e "${LIVE_DATA_FILE}" ]]; do
sleep 5
done
INTERNATIONALIZATION_LANGUAGE="$(yq -r '.internationalization.language' ${LIVE_DATA_FILE})"
INTERNATIONALIZATION_COUNTRY="$(yq -r '.internationalization.country' ${LIVE_DATA_FILE})"
INTERNATIONALIZATION_TIMEZONE="$(yq -r '.internationalization.timeZone' ${LIVE_DATA_FILE})"
DEVICE_TYPE="$(yq -r '.device.type' ${LIVE_DATA_FILE})"
DEPLOYMENT_MODE="$(yq -r '.deployment.mode' ${LIVE_DATA_FILE})"
DEPLOYMENT_GIT_URL="$(yq -r '.deployment.git_url' ${LIVE_DATA_FILE})"
DEPLOYMENT_GIT_USERNAME="$(yq -r '.deployment.git_username' ${LIVE_DATA_FILE})"
DEPLOYMENT_GIT_PASSWORD="$(yq -r '.deployment.git_password' ${LIVE_DATA_FILE})"
LIVE_TARGET_IP="$(yq -r '.live_target.ip' ${LIVE_DATA_FILE})"
LIVE_TARGET_PASSWORD="$(yq -r '.live_target.password' ${LIVE_DATA_FILE})"
hierarchy_preparation
setup_ssh
hardware_detection
until [[ -e web/signals/configuration_ready ]]; do
sleep 5
done
until [[ -e web/signals/deployment_ready ]]; do
sleep 5
done
# --- MAIN LOGIC ---<
+5
View File
@@ -0,0 +1,5 @@
0: successful.
1: error.
225: Bad SSH credentials.
226: Host unreachable or connection refused.
22: SSH fingerprint in `known_hosts` for the IP is different than the current one.
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
# Numbus Installer Bootstrap
# This script clones the repository and launches the NixOS deployment script.
REPO_URL="https://gittea.dev/numbus/numbus.git"
INSTALL_DIR="/run/user/$(id -u)/numbus-installer"
BRANCH="${BRANCH:-production}"
echo -e "\n ☁️ Initializing Numbus Installer..."
# 1. Check for Nix
TEST_FAIL=0
if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then
echo -e "\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
echo -e "\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
echo -e "\n ⚠️ Continuing anyways, this is not supported by Numbus."
fi
# 2. Clone/Update the repository
if [[ -e ${INSTALL_DIR}/config/* ]]; then
echo " ⚠️ It seems you have already run this script. Previously generated files need to be cleaned up."
OLD_CONFIG_PATH="${INSTALL_DIR}/trash/$(date +"%Y-%m-%d-%Hh%M")"
mkdir -p ${OLD_CONFIG_PATH}
mv ${INSTALL_DIR} ${OLD_CONFIG_PATH}
echo " ✅ Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed."
else
rm -rf "${INSTALL_DIR}"
fi
git clone -b "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" -q
# 3. Launch the deployment script
cd "${INSTALL_DIR}"
chmod +x script/deploy.sh
./script/deploy.sh
+4 -4
View File
@@ -1,6 +1,6 @@
# Terminology for the variables used
|Variable|Meaning|Possible values|
|-|-------|-|
|DEPLOYMENT_STRATEGY|Either deploy the machine with a config you already have or let the script guide you through the config options|**interactive** or **non-interactive**|
|DEPLOYMENT_MODE|Either configure the machine through your terminal (TUI) or through a slick web UI (GUI)|**TUI** (only available for **non-interactive** strategy) or **GUI**|
|Variable|Meaning|Possible values|Deprecated|
|-|-------|-|-|
|DEPLOYMENT_STRATEGY|Either deploy the machine with a config you already have or let the script guide you through the config options|**interactive** or **non-interactive**|NO|
|DEPLOYMENT_MODE|Either configure the machine through your terminal (TUI) or through a slick web UI (GUI)|**TUI** (only available for **non-interactive** strategy) or **GUI**|YES|
View File
-49
View File
@@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Numbus Configurator</title>
<!-- Tailwind CSS for modern styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Alpine.js for lightweight reactivity -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- JS-YAML to convert JS objects to YAML strings -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
<!-- Material Design Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
<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>
+13 -10
View File
@@ -6,12 +6,14 @@ import sys
### Variables -->
SECRET_PATH = "/run/user/{}/numbus".format(os.getuid()) if os.path.exists("/run/user/{}".format(os.getuid())) else "../secrets"
os.makedirs(SECRET_PATH, exist_ok=True)
LOGS_DIR = "../web/logs/"
PAGES_DIR = "../web/pages/"
CONFIG_DIR = "../web/config/"
SIGNALS_DIR = "../web/signal/"
LOGS_DIR = "logs/"
PAGES_DIR = "pages/"
CONFIG_DIR = "config/"
SIGNALS_DIR = "signals/"
### <-- Variables
os.chdir('web')
class BridgeHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
# Route for logs: /logs?type=out or /logs?type=err
@@ -29,6 +31,12 @@ class BridgeHandler(http.server.SimpleHTTPRequestHandler):
# Read last 50 lines for better context during errors
self.wfile.write("".join(f.readlines()[-50:]).encode())
return
# Restrict static file access to specific directories only
if not any(self.path.startswith(prefix) for prefix in ['/pages', '/media', '/config']):
self.send_error(403, "Access Denied: Resource is restricted.")
return
return http.server.SimpleHTTPRequestHandler.do_GET(self)
def do_POST(self):
@@ -36,20 +44,15 @@ class BridgeHandler(http.server.SimpleHTTPRequestHandler):
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:
with open(os.path.join(CONFIG_DIR, "live.yaml"), "wb") as f:
f.write(post_data)
self.send_response(200)
self.end_headers()
# Signal Bash that discovery data is ready
with open(os.path.join(SIGNALS_DIR, ".discovery_ready"), "w") as f: f.write("1")
elif self.path == '/deploy':
with open(os.path.join(CONFIG_DIR, "numbus.yaml"), "wb") as f:
f.write(post_data)
self.send_response(200)
self.end_headers()
with open(os.path.join(SIGNALS_DIR, ".deploy_signal"), "w") as f: f.write("1")
os.chdir(PAGES_DIR)
http.server.HTTPServer(('localhost', 8088), BridgeHandler).serve_forever()
-50
View File
@@ -1,50 +0,0 @@
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()
View File
+9 -9
View File
@@ -13,7 +13,7 @@
<!-- 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">
<link rel="icon" href="/media/favicon.ico" type="image/x-icon">
</head>
@@ -36,7 +36,7 @@
<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 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">
@@ -63,29 +63,29 @@
<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">
<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>
<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>
<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>
<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>
+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="justify-center flex flex-col items-center sm:p-6">
<img class="auto drop-shadow-2xl animate-pulse-slow mt-4 w-32 h-32 sm:mt-0 sm:w-48 sm:h-48 2xl:w-64 2xl:h-64" src="/media/logo.png" alt="Numbus Logo">
<h1 class="text-center font-extrabold tracking-tight p-4 pb-2 pt-2 text-3xl sm:p-2 sm:pt-4 sm:pb-7 sm:text-4xl md:text-5xl 2xl:text-6xl 2xl:pt-8 2xl:pb-10">Welcome to <span class="text-sky-400 animate-pulse-slow text-shadow-glow">NUMBUS</span></h1>
<p class="text-center text-sm pl-4 pr-4 p-2 sm:text-lg sm:pl-10 sm:pr-10 md:text-xl xl:text-2xl 2xl:p-4">Transform your device into a <strong>reliable and private</strong> appliance <br class="invisible md:visible"> using the power of open-source software.</p>
<p class="text-center text-sm pl-4 pr-4 pt-2 sm:text-lg sm:pl-10 sm:pr-10 md:text-xl xl:text-2xl 2xl:p-4">You will be <strong>guided</strong> through the configuration process.</p>
<div class="bg-amber-500/20 border border-amber-500/30 rounded-xl flex text-left items-center mx-auto gap-5 mt-5 mb-5 ml-4 mr-4 p-2 sm:p-4 sm:ml-20 sm:mr-20 sm:mt-10 max-w-3xl">
<span class="bg-amber-500 rounded-full shrink-0 p-1 px-2">
<i class="mdi mdi-shield-lock text-slate-900 text-md sm:text-lg md:text-2xl 2xl:text-4xl"></i>
</span>
<p class="text-amber-200/90 italic text-xs sm:text-md md:text-lg xl:text-xl"><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 pt-6 pb-4 pl-4 pr-4 sm:p-12 2xl:p-14">
<a class="text-shadow-glow px-10 py-4 bg-fuchsia-600 hover:bg-fuchsia-500 rounded-full text-sm sm: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>
+96 -88
View File
@@ -13,7 +13,7 @@
<!-- 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">
<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">
@@ -26,33 +26,85 @@
function numbusPreparation() {
return {
step: 1,
discoveryLoading: false,
formData: {
1: { language: 'French', country: 'France', timeZone: 'Europe/Paris' },
2: { deviceType: '' },
3: { deploymentMode: '' },
4: { replicationHardware: '', replicationStrategy: '', replicationSecrets: '' },
5: { liveIp: '', livePassword: '' },
3: { deploymentMode: '', gitUrl: '', gitUsername: '', gitPassword: '' },
4: { liveIp: '', livePassword: '' },
},
isStepValid() {
const currentStepData = this.formData[this.step];
if (!currentStepData) return true;
if (this.step === 3 && currentStepData.deploymentMode === 'interactive') 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
async startDiscovery() {
if (this.discoveryLoading) return;
this.discoveryLoading = true;
const data = JSON.parse(JSON.stringify(this.formData));
console.log("Discovery started with:", data);
const payload = {
internationalization: {
language: data[1].language,
country: data[1].country,
time_zone: data[1].timeZone
},
device: {
type: data[2].deviceType
},
deployment: {
mode: data[3].deploymentMode,
git_url: data[3].gitUrl || '',
git_username: data[3].gitUsername || '',
git_password: data[3].gitPassword || ''
},
live_target: {
ip: data[4].liveIp,
password: data[4].livePassword
}
};
try {
const response = await fetch('/discovery', {
method: 'POST',
headers: { 'Content-Type': 'text/yaml' },
body: jsyaml.dump(payload, { quotingType: '"', forceQuotes: true })
});
if (response.ok) {
console.log("Discovery signal sent successfully.");
this.pollHardwareResults();
} else {
console.error("Server returned an error:", response.statusText);
this.discoveryLoading = false;
}
} catch (error) {
console.error("Network error during discovery:", error);
this.discoveryLoading = false;
}
},
pollHardwareResults() {
// Poll every 2 seconds for the hardware detection results file
const interval = setInterval(async () => {
try {
const response = await fetch('/config/hardware.yaml', { cache: 'no-store' });
if (response.ok) {
clearInterval(interval);
window.location.href = 'configuration.html';
}
} catch (err) {
// Errors are expected until the file is actually created
}
}, 2000);
}
}
}
@@ -61,8 +113,8 @@
<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>
<a target="_blank" 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" target="_blank" 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>
@@ -70,8 +122,8 @@
<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>
<a target="_blank" 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 target="_blank" 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>
@@ -169,6 +221,9 @@
</div>
</div>
</div>
<!-- Step 1: Language & Region -->
<!-- Device Type -->
<div x-show="step === 2" x-cloak class="pl-3 pr-3">
@@ -177,7 +232,7 @@
<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">
<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>
@@ -200,7 +255,7 @@
</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">
<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>
@@ -223,7 +278,7 @@
</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">
<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>
@@ -246,7 +301,7 @@
</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">
<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>
@@ -270,6 +325,9 @@
</button>
</div>
</div>
<!-- Device Type -->
<!-- Deployment Mode -->
<div x-show="step === 3" x-cloak class="pl-3 pr-3">
@@ -325,81 +383,29 @@
</button>
</div>
</div>
<!-- Deployment Mode -->
<!-- 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">
<div x-show="step === 4" 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">
<input x-model="formData[4].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="••••••••">
<input x-model="formData[4].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>
<!-- Live Setup -->
<!-- Footer -->
<div class="bg-[#1e293b] border-t bottom-0 w-full rounded-3xl absolute border-slate-700 p-6 flex items-center justify-between">
@@ -407,22 +413,24 @@
<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"
x-show="step != 4"
: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-show="step === 4"
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"/>
:disabled="!isStepValid() || discoveryLoading"
class="px-10 py-3 text-white font-bold rounded-xl flex items-center gap-3 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">
<span x-text="discoveryLoading ? 'Discovering...' : 'Start Discovery'"></span>
<svg x-show="discoveryLoading" aria-hidden="true" class="w-5 h-5 text-white animate-spin" 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>
<!-- Footer -->
</div>
View File