The full-system updater runs as a detached systemd service and can finish successfully even when the browser loses its status connection. In that case the update log and status file correctly report REBOOT_REQUIRED, but the Hub modal can remain on "Updating..." with its controls disabled. There were four independent ways for the frontend to get stuck: * update status fetches had no deadline, so a request that stayed pending never rejected and never advanced the existing failure counter; * setInterval started async polls without waiting for the previous poll, allowing slow requests to overlap and responses to arrive out of order; * each log chunk used textContent +=, replacing the complete and growing Nix build log every two seconds, which could stall browser rendering and was especially visible over RDP; and * page reload, tab resume, and RDP reconnect did not reattach the modal to the update status persisted by the backend. This produced a dangerous UX mismatch: the machine had a fully staged NixOS generation and was ready to reboot, while the Hub continued telling the user that the update was still running. Bound status requests with AbortController, prevent overlapping polls, and replace the endless spinner after sustained failures with an explicit "Update status unavailable" state and Retry Status action. Reconcile state immediately on focus, visibility, online, page startup, and before starting a new update. Use no-store requests and render verbose logs incrementally with a bounded visible tail while retaining the complete report in memory. Apply the same timeout and single-flight protection to rebuild polling. Record the exact generation produced by `nixos-rebuild boot`. The Hub now keeps REBOOT_REQUIRED visible until that generation matches /run/current-system, then clears the marker after reboot. For an update started by an older updater that did not write the marker, recover the staged generation from the final nixos-rebuild log line. The dashboard sidebar also distinguishes update-in-progress and restart-required states. Regression coverage verifies generation marker/log recovery, pre- versus post-reboot detection, request timeout wiring, single-flight polling, connection-loss UX, RDP/tab resume reconciliation, bounded log rendering, page-reload recovery, and JavaScript syntax. Validation: * python3 -m unittest discover -s tests -p 'test_*.py' -v (170 passed) * node --check app/sovran_systemsos_web/static/js/*.js * python3 -m py_compile for changed Python modules * git diff --check A Nix evaluation was not available in the development sandbox; the NixOS module should still be evaluated and built in CI or on a test machine before release.
543 lines
26 KiB
Nix
543 lines
26 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.sovran_systemsOS;
|
|
|
|
# Read the OS version from the repo-root VERSION file at eval time so the
|
|
# Hub always ships a real, baked-in version string instead of relying on
|
|
# a runtime file lookup (which returns "dev" when /etc/nixos/VERSION is
|
|
# missing, e.g. in a dev checkout — this is the "vdev" bug in the Hub UI).
|
|
versionFile = ../../VERSION;
|
|
sovranVersion =
|
|
if builtins.pathExists versionFile
|
|
then builtins.replaceStrings [ "v" "\n" "\r" ] [ "" "" "" ] (builtins.readFile versionFile)
|
|
else "1.0.0";
|
|
|
|
monitoredServices =
|
|
# ── Infrastructure — System Passwords (always present) ─────
|
|
[
|
|
{ name = "System Passwords"; unit = "root-password-setup.service"; type = "system"; icon = "passwords"; enabled = true; category = "infrastructure"; credentials = [
|
|
{ label = "Free Account — Username"; value = "free"; }
|
|
{ label = "Free Account / Hub Login — Password"; file = "/var/lib/secrets/free-password"; }
|
|
{ label = "Administrator (root) Password"; file = "/var/lib/secrets/root-password"; }
|
|
{ label = "SSH Passphrase — use via: ssh root@localhost"; file = "/var/lib/secrets/ssh-passphrase"; }
|
|
]; }
|
|
]
|
|
# ── Infrastructure — Caddy + Tor (NOT desktop-only) ────────
|
|
++ lib.optionals (!cfg.roles.desktop) [
|
|
{ name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; enabled = true; category = "infrastructure"; credentials = []; }
|
|
{ name = "Tor"; unit = "tor.service"; type = "system"; icon = "tor"; enabled = true; category = "infrastructure"; credentials = []; }
|
|
]
|
|
# ── Infrastructure — Remote Desktop (all roles) ─────────────
|
|
++ [
|
|
{ name = "Remote Desktop"; unit = "gnome-remote-desktop.service"; type = "system"; icon = "rdp"; enabled = cfg.features.rdp; category = "infrastructure"; credentials = [
|
|
{ label = "Username"; file = "/var/lib/gnome-remote-desktop/rdp-username"; }
|
|
{ label = "Password"; file = "/var/lib/gnome-remote-desktop/rdp-password"; }
|
|
{ label = "Address"; file = "/var/lib/secrets/internal-ip"; suffix = ":3389"; }
|
|
{ label = "How to Connect"; value = "1. Install an RDP client (e.g. Remmina, Microsoft Remote Desktop)\n2. Create a new RDP connection\n3. Enter the Address above as the host\n4. Enter the Username and Password above"; }
|
|
]; }
|
|
]
|
|
# ── Bitcoin Base ────────────────────────────────────────────
|
|
++ lib.optionals cfg.services.bitcoin [
|
|
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.services.bitcoin; category = "bitcoin-base"; credentials = [
|
|
{ label = "Tor Bitcoin P2P Address — Reachable only through Tor"; file = "/var/lib/tor/onion/bitcoind/hostname"; suffix = ":8333"; }
|
|
]; }
|
|
]
|
|
# ── Bitcoin Apps (services on top of the node) ─────────────
|
|
++ lib.optionals cfg.services.bitcoin [
|
|
{ name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/electrs/hostname"; prefix = "http://"; }
|
|
{ label = "Port"; value = "50001"; }
|
|
]; }
|
|
{ name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = []; }
|
|
{ name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
|
|
{ label = "Local Network — Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3051"; }
|
|
{ label = "Password"; file = "/etc/nix-bitcoin-secrets/rtl-password"; }
|
|
{ label = "How to Access"; value = "• Tor Address: Open in Tor Browser from any device, anywhere in the world\n• Local Network: Open in any browser, but only when connected to your home network"; }
|
|
]; }
|
|
{ name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.web.btcpayserver; category = "bitcoin-apps"; credentials = [
|
|
{ label = "URL"; file = "/var/lib/domains/btcpayserver"; prefix = "https://"; }
|
|
{ label = "Note"; value = "Create your admin account on first visit"; }
|
|
]; }
|
|
{ name = "Zeus Connect"; unit = "zeus-connect-setup.service"; type = "system"; icon = "zeus"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
|
{ label = "QR Code"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; qronly = true; }
|
|
]; }
|
|
{ name = "Sparrow Auto-Link"; unit = "sparrow-autoconnect.service"; type = "system"; icon = "sparrow"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
|
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
|
|
{ label = "Status"; value = "Auto-configured on first boot"; }
|
|
]; }
|
|
{ name = "Lightning Wallet Connections"; unit = "albyhub.service"; type = "system"; icon = "nwc"; enabled = cfg.features."nwc-wallets"; category = "bitcoin-apps"; credentials = [
|
|
{ label = "Lightning Address Domain"; file = "/var/lib/domains/lightning"; }
|
|
]; }
|
|
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
|
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
|
|
{ label = "Local Network — Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
|
|
{ label = "How to Access"; value = "• Tor Address: Open in Tor Browser from any device, anywhere in the world\n• Local Network: Open in any browser, but only when connected to your home network"; }
|
|
]; }
|
|
]
|
|
# ── Communication (server+desktop only) ────────────────────
|
|
++ lib.optionals cfg.roles.server_plus_desktop [
|
|
{ name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; credentials = [
|
|
{ label = "Homeserver URL"; file = "/var/lib/secrets/matrix-homeserver-url"; }
|
|
{ label = "Admin Username"; file = "/var/lib/secrets/matrix-admin-username"; }
|
|
{ label = "Admin Password"; file = "/var/lib/secrets/matrix-admin-password"; }
|
|
{ label = "Test Username"; file = "/var/lib/secrets/matrix-test-username"; }
|
|
{ label = "Test Password"; file = "/var/lib/secrets/matrix-test-password"; }
|
|
]; }
|
|
{ name = "LiveKit (Element Call)"; unit = "livekit.service"; type = "system"; icon = "element-calling"; enabled = cfg.features.element-calling; category = "communication"; credentials = []; }
|
|
]
|
|
# ── Self-Hosted Apps (server+desktop only) ─────────────────
|
|
++ lib.optionals cfg.roles.server_plus_desktop [
|
|
{ name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; category = "apps"; credentials = [
|
|
{ label = "URL"; file = "/var/lib/domains/vaultwarden"; prefix = "https://"; }
|
|
{ label = "Admin Panel"; file = "/var/lib/domains/vaultwarden"; prefix = "https://"; suffix = "/admin"; }
|
|
{ label = "Admin Token"; file = "/var/lib/secrets/vaultwarden/vaultwarden.env"; extract = "ADMIN_TOKEN"; }
|
|
]; }
|
|
{ name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; enabled = cfg.services.nextcloud; category = "apps"; credentials = [
|
|
{ label = "Credentials"; file = "/var/lib/secrets/nextcloud-admin"; multiline = true; }
|
|
]; }
|
|
{ name = "WordPress"; unit = "phpfpm-wordpress.service"; type = "system"; icon = "wordpress"; enabled = cfg.services.wordpress; category = "apps"; credentials = [
|
|
{ label = "Credentials"; file = "/var/lib/secrets/wordpress-admin"; multiline = true; }
|
|
]; }
|
|
]
|
|
# ── Nostr / Relay (server+desktop only) ────────────────────
|
|
++ lib.optionals cfg.roles.server_plus_desktop [
|
|
{ name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; category = "nostr"; credentials = []; }
|
|
]
|
|
# ── Support (always present) ────────────────────────────────
|
|
++ [
|
|
{ name = "Tech Support"; unit = "sovran-tech-support"; type = "support"; icon = "support"; enabled = true; category = "support"; credentials = []; }
|
|
];
|
|
|
|
activeRole =
|
|
if cfg.roles.desktop then "desktop"
|
|
else if cfg.roles.node then "node"
|
|
else "server_plus_desktop";
|
|
|
|
generatedConfig = pkgs.writeText "sovran-hub-config.json"
|
|
(builtins.toJSON {
|
|
refresh_interval = 5;
|
|
command_method = "systemctl";
|
|
role = activeRole;
|
|
services = monitoredServices;
|
|
feature_manager = true;
|
|
feature_states = {
|
|
bitcoin-tor-gossip = cfg.features.bitcoin-tor-gossip;
|
|
};
|
|
sovran_version = sovranVersion;
|
|
});
|
|
|
|
generatedVersions = pkgs.writeText "sovran-hub-versions.json" (builtins.toJSON {
|
|
"caddy.service" = if pkgs ? caddy then pkgs.caddy.version else "2.8.4";
|
|
"tor.service" = if pkgs ? tor then pkgs.tor.version else "0.4.8.12";
|
|
"gnome-remote-desktop.service" = if pkgs ? gnome-remote-desktop then pkgs.gnome-remote-desktop.version else "46.0";
|
|
"bitcoind.service" = if pkgs ? bitcoind then pkgs.bitcoind.version else "27.1.0";
|
|
"electrs.service" = if pkgs ? electrs then pkgs.electrs.version else "0.10.6";
|
|
"lnd.service" = if pkgs ? lnd then pkgs.lnd.version else "0.18.0";
|
|
# Keep the fallbacks aligned with the vendored packages used by the
|
|
# service modules (RTL 0.15.8 and Mempool 3.2.1). The nixpkgs attrs are
|
|
# optional because these packages are built locally in this repository.
|
|
"rtl.service" = if pkgs ? clightning-rtl then pkgs.clightning-rtl.version else (if pkgs ? rtl then pkgs.rtl.version else "0.15.8");
|
|
# BTCPay Server is intentionally sourced from pkgs.stable by the service
|
|
# module. Read the configured package here rather than pkgs.btcpayserver
|
|
# (unstable), otherwise the Hub can advertise a version that is not running.
|
|
"btcpayserver.service" = lib.getVersion config.services.btcpayserver.package;
|
|
"albyhub.service" = if pkgs ? albyhub then pkgs.albyhub.version else "1.8.0";
|
|
"mempool.service" = if pkgs ? mempool then pkgs.mempool.version else "3.2.1";
|
|
"matrix-synapse.service" = if pkgs ? matrix-synapse then pkgs.matrix-synapse.version else "1.115.0";
|
|
"livekit.service" = if pkgs ? livekit then pkgs.livekit.version else "1.5.2";
|
|
"vaultwarden.service" = if pkgs ? vaultwarden then pkgs.vaultwarden.version else "1.32.0";
|
|
# Nextcloud and WordPress are downloaded into /var/lib/www, so their
|
|
# deployed versions are read from their own PHP version files by the Hub.
|
|
"haven-relay.service" = if pkgs ? haven-relay then pkgs.haven-relay.version else (if pkgs ? haven then pkgs.haven.version else "0.1.0");
|
|
});
|
|
|
|
# ── Update wrapper script ──────────────────────────────────────
|
|
update-script = pkgs.writeShellScript "sovran-hub-update.sh" ''
|
|
set -uo pipefail
|
|
export PATH="${lib.makeBinPath [ pkgs.nix pkgs.nixos-rebuild pkgs.git pkgs.flatpak pkgs.coreutils ]}:$PATH"
|
|
|
|
LOG="/var/log/sovran-hub-update.log"
|
|
STATUS="/var/log/sovran-hub-update.status"
|
|
GENERATION="/var/log/sovran-hub-update.generation"
|
|
|
|
echo "RUNNING" > "$STATUS"
|
|
rm -f "$GENERATION"
|
|
: > "$LOG"
|
|
exec > >(tee -a "$LOG") 2>&1
|
|
|
|
echo "══════════════════════════════════════════════════"
|
|
echo " Sovran_SystemsOS Update — $(date)"
|
|
echo "══════════════════════════════════════════════════"
|
|
echo ""
|
|
|
|
RC=0
|
|
|
|
echo "── Step 1/3: nix flake update ────────────────────"
|
|
if ! nix flake update --flake /etc/nixos --print-build-logs \
|
|
--option connect-timeout 10 \
|
|
--option stalled-download-timeout 90 \
|
|
--option download-attempts 7 \
|
|
--option fallback true 2>&1; then
|
|
echo "[ERROR] nix flake update failed"
|
|
RC=1
|
|
fi
|
|
echo ""
|
|
|
|
if [ "$RC" -eq 0 ]; then
|
|
echo "── Step 2/3: nixos-rebuild boot (stage next reboot) ──"
|
|
# Stream output straight into $LOG (see rebuild-script) so the Hub UI
|
|
# shows live progress instead of an empty log during long builds.
|
|
nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
|
--option connect-timeout 10 \
|
|
--option stalled-download-timeout 90 \
|
|
--option download-attempts 7 \
|
|
--option fallback true
|
|
BOOT_RC=$?
|
|
if [ "$BOOT_RC" -ne 0 ]; then
|
|
echo "[ERROR] nixos-rebuild boot failed"
|
|
RC=1
|
|
elif ! readlink -f /nix/var/nix/profiles/system > "$GENERATION"; then
|
|
echo "[ERROR] update was built but its staged generation could not be recorded"
|
|
rm -f "$GENERATION"
|
|
RC=1
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$RC" -eq 0 ]; then
|
|
echo "── Step 3/3: flatpak update ────────────────────────"
|
|
if ! flatpak update -y 2>&1; then
|
|
echo "[WARNING] flatpak update failed (non-fatal)"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$RC" -eq 0 ]; then
|
|
echo "══════════════════════════════════════════════════"
|
|
echo " ✓ Update staged successfully"
|
|
echo " Reboot required to activate the new system"
|
|
echo "══════════════════════════════════════════════════"
|
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
|
else
|
|
echo "══════════════════════════════════════════════════"
|
|
echo " ✗ Update failed — see errors above"
|
|
echo "══════════════════════════════════════════════════"
|
|
echo "FAILED" > "$STATUS"
|
|
fi
|
|
|
|
exit "$RC"
|
|
'';
|
|
|
|
# ── Rebuild wrapper script ─────────────────────────────────────
|
|
rebuild-script = pkgs.writeShellScript "sovran-hub-rebuild.sh" ''
|
|
set -uo pipefail
|
|
export PATH="${lib.makeBinPath [ pkgs.nix pkgs.nixos-rebuild pkgs.coreutils ]}:$PATH"
|
|
|
|
LOG="/var/log/sovran-hub-rebuild.log"
|
|
STATUS="/var/log/sovran-hub-rebuild.status"
|
|
|
|
echo "RUNNING" > "$STATUS"
|
|
: > "$LOG"
|
|
exec > >(tee -a "$LOG") 2>&1
|
|
|
|
echo "══════════════════════════════════════════════════"
|
|
echo " Sovran_SystemsOS Rebuild — $(date)"
|
|
echo "══════════════════════════════════════════════════"
|
|
echo ""
|
|
echo "── Rebuilding system configuration ──────────────"
|
|
# Stream output straight into $LOG (tee'd by the exec redirect above) so
|
|
# the Hub UI shows live progress. Capturing the output in a variable
|
|
# kept the log empty for the entire build+activation, which made long
|
|
# rebuilds can otherwise look like a hang.
|
|
nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
|
--option connect-timeout 10 \
|
|
--option stalled-download-timeout 90 \
|
|
--option download-attempts 7 \
|
|
--option fallback true
|
|
SWITCH_RC=$?
|
|
if [ "$SWITCH_RC" -eq 0 ]; then
|
|
echo ""
|
|
echo "══════════════════════════════════════════════════"
|
|
echo " ✓ Rebuild completed successfully"
|
|
echo "══════════════════════════════════════════════════"
|
|
echo "SUCCESS" > "$STATUS"
|
|
elif grep -q "switchInhibitors\|Pre-switch checks failed" "$LOG"; then
|
|
echo ""
|
|
echo " ✓ Build succeeded — a reboot is required to apply this rebuild"
|
|
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
|
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
|
--option connect-timeout 10 \
|
|
--option stalled-download-timeout 90 \
|
|
--option download-attempts 7 \
|
|
--option fallback true 2>&1; then
|
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
|
else
|
|
echo "[ERROR] nixos-rebuild boot also failed"
|
|
echo "FAILED" > "$STATUS"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo ""
|
|
echo "══════════════════════════════════════════════════"
|
|
echo " ✗ Rebuild failed — see errors above"
|
|
echo "══════════════════════════════════════════════════"
|
|
echo "FAILED" > "$STATUS"
|
|
exit 1
|
|
fi
|
|
'';
|
|
|
|
# ── Brave Origin launcher wrapper: a *persistent* per-user profile dir.
|
|
# It must NOT be wiped on exit: the Hub's logout marker cookie
|
|
# (hub_manual_logout) and the session cookie live in this profile.
|
|
# Launching from a fresh/ephemeral profile every time throws away the
|
|
# marker, so /auto-login would mint a new session and silently log the
|
|
# user straight back in after they signed out and reopened the window.
|
|
# A stable directory also keeps the Wayland app_id deterministic so
|
|
# GNOME Shell can match the window to the .desktop entry (dock icon).
|
|
hub-brave-wrapper = pkgs.writeShellScript "sovran-hub-brave.sh" ''
|
|
export PATH="${lib.makeBinPath [ pkgs.brave-origin pkgs.coreutils ]}:$PATH"
|
|
# Per-user, persistent browser state. $XDG_STATE_HOME keeps it out of the
|
|
# way of backups and survives reboots and window close/reopen.
|
|
if [ -n "$XDG_STATE_HOME" ]; then
|
|
HUB_DATA="$XDG_STATE_HOME/sovran-hub-browser"
|
|
else
|
|
HUB_DATA="$HOME/.local/state/sovran-hub-browser"
|
|
fi
|
|
mkdir -p "$HUB_DATA"
|
|
export BAMF_DESKTOP_FILE_HINT="/run/current-system/sw/share/applications/sovran-hub.desktop"
|
|
export GIO_LAUNCHED_DESKTOP_FILE="/run/current-system/sw/share/applications/sovran-hub.desktop"
|
|
# With a persistent profile Brave Origin's one-time "Proceed with Origin
|
|
# for free on Linux" onboarding dialog only appears once; keep skipping it
|
|
# anyway so it can never block auto-login (Linux-only switch).
|
|
brave-origin --app=http://localhost:8937/auto-login \
|
|
--skip-origin-startup-dialog \
|
|
--class=sovran-hub \
|
|
--user-data-dir="$HUB_DATA" \
|
|
--password-store=basic \
|
|
--disable-gpu \
|
|
--disable-features=WebRtcPipeWireCapturer \
|
|
--ozone-platform=wayland
|
|
'';
|
|
|
|
# ── Hub auto-launch wrapper script ────────────────────────────────
|
|
hub-autolaunch-script = pkgs.writeShellScript "sovran-hub-autolaunch.sh" ''
|
|
export PATH="${lib.makeBinPath [ pkgs.curl pkgs.coreutils ]}:$PATH"
|
|
|
|
DISABLE_FLAG="/var/lib/sovran/hub-autolaunch-disabled"
|
|
BOOT_FLAG="/run/sovran-hub-autolaunch-done"
|
|
|
|
# User disabled auto-launch via Hub toggle
|
|
[ -f "$DISABLE_FLAG" ] && exit 0
|
|
|
|
# Already launched this boot
|
|
[ -f "$BOOT_FLAG" ] && exit 0
|
|
|
|
touch "$BOOT_FLAG"
|
|
|
|
# Wait for Hub server to become ready (max ~15 seconds)
|
|
for i in $(seq 1 15); do
|
|
curl -s -o /dev/null http://localhost:8937 && break
|
|
sleep 1
|
|
done
|
|
|
|
${hub-brave-wrapper}
|
|
'';
|
|
|
|
sovran-hub-web = pkgs.python3Packages.buildPythonApplication {
|
|
pname = "sovran-systemsos-hub-web";
|
|
# Keep the package metadata in lockstep with the version shown in the Hub.
|
|
version = sovranVersion;
|
|
format = "other";
|
|
|
|
src = ../../app;
|
|
|
|
nativeBuildInputs = [ pkgs.librsvg ];
|
|
|
|
propagatedBuildInputs = with pkgs.python3Packages; [
|
|
fastapi
|
|
uvicorn
|
|
jinja2
|
|
python-multipart
|
|
];
|
|
|
|
dontBuild = true;
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
|
|
install -d $out/lib/sovran-hub-web
|
|
cp -r sovran_systemsos_web $out/lib/sovran-hub-web/
|
|
|
|
cp ${generatedConfig} $out/lib/sovran-hub-web/config.json
|
|
cp ${generatedVersions} $out/lib/sovran-hub-web/versions.json
|
|
printf '%s' "${sovranVersion}" > $out/lib/sovran-hub-web/VERSION
|
|
|
|
install -d $out/share/sovran-hub/icons
|
|
cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true
|
|
|
|
install -d $out/share/icons/hicolor/scalable/apps
|
|
cp sovran_systemsos_web/static/sovran-hub-icon.svg $out/share/icons/hicolor/scalable/apps/sovran-hub.svg
|
|
|
|
for size in 48 128 256 512; do
|
|
install -d $out/share/icons/hicolor/''${size}x''${size}/apps
|
|
rsvg-convert -w ''${size} -h ''${size} sovran_systemsos_web/static/sovran-hub-icon.svg -o $out/share/icons/hicolor/''${size}x''${size}/apps/sovran-hub.png
|
|
done
|
|
|
|
install -d $out/share/applications
|
|
cat > $out/share/applications/sovran-hub.desktop <<DESKTOP
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Sovran Hub
|
|
Comment=Open Sovran_SystemsOS Hub dashboard
|
|
Exec=${hub-brave-wrapper}
|
|
Icon=sovran-hub
|
|
Terminal=false
|
|
Categories=System;
|
|
StartupNotify=true
|
|
StartupWMClass=brave-localhost__auto-login-Default
|
|
X-GNOME-SingleWindow=true
|
|
DESKTOP
|
|
|
|
install -d $out/bin
|
|
cat > $out/bin/sovran-hub-web <<LAUNCHER
|
|
#!${pkgs.python3}/bin/python3
|
|
import os, sys
|
|
base = os.path.join("$out", "lib", "sovran-hub-web")
|
|
sys.path.insert(0, base)
|
|
os.environ["SOVRAN_HUB_CONFIG"] = os.path.join(base, "config.json")
|
|
os.environ["SOVRAN_HUB_VERSIONS"] = os.path.join(base, "versions.json")
|
|
os.environ["SOVRAN_HUB_ICONS"] = os.path.join("$out", "share", "sovran-hub", "icons")
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"sovran_systemsos_web.server:app",
|
|
host="0.0.0.0",
|
|
port=8937,
|
|
log_level="info",
|
|
)
|
|
LAUNCHER
|
|
chmod +x $out/bin/sovran-hub-web
|
|
|
|
cat > $out/bin/nwc-wallet <<LAUNCHER
|
|
#!${pkgs.python3}/bin/python3
|
|
import os, sys
|
|
base = os.path.join("$out", "lib", "sovran-hub-web")
|
|
sys.path.insert(0, base)
|
|
from sovran_systemsos_web.nwc_wallet_cli import main
|
|
sys.exit(main())
|
|
LAUNCHER
|
|
chmod +x $out/bin/nwc-wallet
|
|
|
|
cat > $out/bin/nwc-lnurl <<LAUNCHER
|
|
#!${pkgs.python3}/bin/python3
|
|
import os, sys
|
|
base = os.path.join("$out", "lib", "sovran-hub-web")
|
|
sys.path.insert(0, base)
|
|
from sovran_systemsos_web.nwc_lnurl_service import main
|
|
main()
|
|
LAUNCHER
|
|
chmod +x $out/bin/nwc-lnurl
|
|
|
|
runHook postInstall
|
|
'';
|
|
|
|
meta = {
|
|
description = "Sovran_SystemsOS Hub — web-based systemd service manager";
|
|
mainProgram = "sovran-hub-web";
|
|
};
|
|
};
|
|
|
|
in
|
|
{
|
|
options.services.sovranHub.webPackage = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = sovran-hub-web;
|
|
description = "The sovran-hub-web Python application package. Other modules use this to reference Hub-installed scripts without duplicating the Python path setup.";
|
|
};
|
|
|
|
config = {
|
|
systemd.services.sovran-hub-web = {
|
|
description = "Sovran_SystemsOS Hub Web Interface";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" ];
|
|
conflicts = [ "sovran-hub-reboot.service" ];
|
|
|
|
serviceConfig = {
|
|
ExecStart = "${sovran-hub-web}/bin/sovran-hub-web";
|
|
Restart = "on-failure";
|
|
RestartPreventExitStatus = "SIGTERM";
|
|
RestartSec = "5s";
|
|
User = "root";
|
|
StandardOutput = "journal";
|
|
StandardError = "journal";
|
|
};
|
|
|
|
path = [
|
|
pkgs.bash
|
|
pkgs.gawk
|
|
pkgs.qrencode
|
|
pkgs.curl
|
|
pkgs.iproute2
|
|
pkgs.nftables
|
|
pkgs.iptables
|
|
pkgs.hostname
|
|
pkgs.coreutils
|
|
pkgs.findutils
|
|
pkgs.gnugrep
|
|
pkgs.rsync
|
|
pkgs.acl
|
|
pkgs.util-linux
|
|
]
|
|
++ lib.optional cfg.services.bitcoin config.services.bitcoind.package
|
|
++ lib.optionals cfg.services.bitcoin [ pkgs.lnd ]
|
|
++ lib.optionals (cfg.services.nextcloud || cfg.services.synapse) [ config.services.postgresql.package ]
|
|
++ lib.optionals config.services.mysql.enable [ config.services.mysql.package ];
|
|
};
|
|
|
|
systemd.services.sovran-hub-update = {
|
|
description = "Sovran_SystemsOS System Update";
|
|
restartIfChanged = false; # Don't let nixos-rebuild kill an in-flight update
|
|
stopIfChanged = false; # Don't stop it during activation either
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "${update-script}";
|
|
};
|
|
};
|
|
|
|
systemd.services.sovran-hub-rebuild = {
|
|
description = "Sovran_SystemsOS System Rebuild";
|
|
restartIfChanged = false; # Don't let nixos-rebuild kill an in-flight rebuild
|
|
stopIfChanged = false; # Don't stop it during activation either
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "${rebuild-script}";
|
|
};
|
|
};
|
|
|
|
systemd.services.sovran-hub-reboot = {
|
|
description = "Sovran_SystemsOS System Reboot";
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "/run/current-system/sw/bin/systemctl --force reboot";
|
|
};
|
|
};
|
|
|
|
environment.systemPackages = [ sovran-hub-web ];
|
|
|
|
networking.firewall.allowedTCPPorts = [ 8937 60847 ];
|
|
|
|
# ── Auto-launch Hub in browser on login ───────────────────────
|
|
environment.etc."xdg/autostart/sovran-hub-autolaunch.desktop".text = ''
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Sovran Hub Auto-Launch
|
|
Exec=${hub-autolaunch-script}
|
|
Terminal=false
|
|
X-GNOME-Autostart-enabled=true
|
|
NoDisplay=true
|
|
'';
|
|
|
|
};
|
|
}
|