From bb7db0693a224707f50eec9dac298f5b456ec308 Mon Sep 17 00:00:00 2001 From: naturallaw77 Date: Thu, 2 Apr 2026 13:42:24 -0500 Subject: [PATCH] fixed updater --- app/sovran_systemsos_web/server.py | 95 ++++------------------- app/sovran_systemsos_web/static/app.js | 28 ++++--- app/sovran_systemsos_web/static/style.css | 12 +-- modules/core/sovran-hub.nix | 13 ++-- 4 files changed, 45 insertions(+), 103 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 90532dc..037e6af 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -24,9 +24,9 @@ FLAKE_LOCK_PATH = "/etc/nixos/flake.lock" FLAKE_INPUT_NAME = "Sovran_Systems" GITEA_API_BASE = "https://git.sovransystems.com/api/v1/repos/Sovran_Systems/Sovran_SystemsOS/commits" -UPDATE_UNIT = "sovran-hub-update.service" -UPDATE_LOG = "/var/log/sovran-hub-update.log" -UPDATE_LOCK = "/run/sovran-hub-update.lock" +UPDATE_LOG = "/var/log/sovran-hub-update.log" +UPDATE_STATUS = "/var/log/sovran-hub-update.status" +UPDATE_UNIT = "sovran-hub-update.service" REBOOT_COMMAND = ["reboot"] @@ -156,51 +156,15 @@ def _get_external_ip() -> str: return "unavailable" -# ── Update unit helpers ────────────────────────────────────────── +# ── Update helpers (file-based, no systemctl) ──────────────────── -def _update_is_active() -> bool: - """Return True if the update unit is currently running.""" - r = subprocess.run( - ["systemctl", "is-active", "--quiet", UPDATE_UNIT], - capture_output=True, - ) - return r.returncode == 0 - - -def _update_result() -> str: - """Return 'success', 'failed', or 'unknown'.""" - r = subprocess.run( - ["systemctl", "show", "-p", "Result", "--value", UPDATE_UNIT], - capture_output=True, text=True, - ) - val = r.stdout.strip() - if val == "success": - return "success" - elif val: - return "failed" - return "unknown" - - -def _update_lock_exists() -> bool: - """Check if the file-based update lock exists (survives server restart).""" - return os.path.exists(UPDATE_LOCK) - - -def _create_update_lock(): - """Create the lock file to indicate an update is in progress.""" +def _read_update_status() -> str: + """Read the status file. Returns RUNNING, SUCCESS, FAILED, or IDLE.""" try: - with open(UPDATE_LOCK, "w") as f: - f.write(str(os.getpid())) - except OSError: - pass - - -def _remove_update_lock(): - """Remove the lock file.""" - try: - os.unlink(UPDATE_LOCK) + with open(UPDATE_STATUS, "r") as f: + return f.read().strip() except FileNotFoundError: - pass + return "IDLE" def _read_log(offset: int = 0) -> tuple[str, int]: @@ -208,10 +172,9 @@ def _read_log(offset: int = 0) -> tuple[str, int]: Returns (new_text, new_offset).""" try: with open(UPDATE_LOG, "rb") as f: - f.seek(0, 2) # seek to end + f.seek(0, 2) size = f.tell() if offset > size: - # Log was truncated (new run), start over offset = 0 f.seek(offset) chunk = f.read() @@ -351,8 +314,8 @@ async def api_updates_run(): """Kick off the detached update systemd unit.""" loop = asyncio.get_event_loop() - running = await loop.run_in_executor(None, _update_is_active) - if running: + status = await loop.run_in_executor(None, _read_update_status) + if status == "RUNNING": return {"ok": True, "status": "already_running"} # Reset failed state if any @@ -362,9 +325,6 @@ async def api_updates_run(): stderr=asyncio.subprocess.DEVNULL, ) - # Create a file-based lock that survives server restarts - _create_update_lock() - proc = await asyncio.create_subprocess_exec( "systemctl", "start", "--no-block", UPDATE_UNIT, stdout=asyncio.subprocess.DEVNULL, @@ -377,38 +337,17 @@ async def api_updates_run(): @app.get("/api/updates/status") async def api_updates_status(offset: int = 0): - """Poll endpoint: returns running state, result, and new log content.""" + """Poll endpoint: reads status file + log file. No systemctl needed.""" loop = asyncio.get_event_loop() - active = await loop.run_in_executor(None, _update_is_active) - result = await loop.run_in_executor(None, _update_result) - lock_exists = _update_lock_exists() + status = await loop.run_in_executor(None, _read_update_status) new_log, new_offset = await loop.run_in_executor(None, _read_log, offset) - # If the unit is active, it's definitely still running - if active: - return { - "running": True, - "result": "pending", - "log": new_log, - "offset": new_offset, - } + running = (status == "RUNNING") + result = "pending" if running else status.lower() - # If the lock file exists but the unit is not active, the update - # finished (or the server just restarted after nixos-rebuild switch). - # The lock file persists across server restarts because it's on disk. - if lock_exists: - _remove_update_lock() - return { - "running": False, - "result": result, - "log": new_log, - "offset": new_offset, - } - - # No lock, not active — nothing happening return { - "running": False, + "running": running, "result": result, "log": new_log, "offset": new_offset, diff --git a/app/sovran_systemsos_web/static/app.js b/app/sovran_systemsos_web/static/app.js index f39293b..cc59588 100644 --- a/app/sovran_systemsos_web/static/app.js +++ b/app/sovran_systemsos_web/static/app.js @@ -72,7 +72,7 @@ async function apiFetch(path, options = {}) { return res.json(); } -// ── Render: initial build ────────────────────────��──────────────── +// ── Render: initial build ───────────────────────────────────────── function buildTiles(services, categoryLabels) { _servicesCache = services; @@ -341,24 +341,28 @@ async function pollUpdateStatus() { if ($modalStatus) $modalStatus.textContent = "Updating…"; } - // Append any new log content + // Append new log content if (data.log) { appendLog(data.log); } _updateLogOffset = data.offset; - // Check if finished - if (!data.running) { - _updateFinished = true; - stopUpdatePoll(); - if (data.result === "success") { - onUpdateDone(true); - } else { - onUpdateDone(false); - } + // RUNNING → keep polling + if (data.running) { + return; + } + + // Finished — check result + _updateFinished = true; + stopUpdatePoll(); + + if (data.result === "success") { + onUpdateDone(true); + } else { + onUpdateDone(false); } } catch (err) { - // Server is likely restarting during nixos-rebuild switch — keep polling + // Server is restarting during nixos-rebuild switch — keep polling if (!_serverWasDown) { _serverWasDown = true; appendLog("\n[Server restarting — waiting for it to come back…]\n"); diff --git a/app/sovran_systemsos_web/static/style.css b/app/sovran_systemsos_web/static/style.css index 6263d99..bd0ef60 100644 --- a/app/sovran_systemsos_web/static/style.css +++ b/app/sovran_systemsos_web/static/style.css @@ -473,14 +473,14 @@ button:disabled { border-top: 1px solid var(--border-color); } -/* Reboot button: green */ -.btn-reboot { - background-color: var(--green); - color: #fff; +/* Reboot button */ +#btn-reboot { + background-color: #2ec27e !important; + color: #fff !important; } -.btn-reboot:hover:not(:disabled) { - background-color: #27ae6e; +#btn-reboot:hover:not(:disabled) { + background-color: #27ae6e !important; } .btn-save { diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index 121032f..3586db2 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -28,7 +28,7 @@ let { name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; } { name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; category = "communication"; } ] - # ── Self-Hosted Apps ────────────────��────────────────────── + # ── Self-Hosted Apps ─────────────────────────────────────── ++ [ { name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; category = "apps"; } { name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; enabled = cfg.services.nextcloud; category = "apps"; } @@ -58,18 +58,15 @@ let export PATH="${lib.makeBinPath [ pkgs.nix pkgs.nixos-rebuild pkgs.git pkgs.flatpak pkgs.coreutils ]}:$PATH" LOG="/var/log/sovran-hub-update.log" - LOCK="/run/sovran-hub-update.lock" + STATUS="/var/log/sovran-hub-update.status" - # Create lock file (survives server restarts, cleared on reboot since /run is tmpfs) - echo $$ > "$LOCK" + # Mark as RUNNING + echo "RUNNING" > "$STATUS" # Truncate the log and redirect ALL output (stdout + stderr) into it : > "$LOG" exec > >(tee -a "$LOG") 2>&1 - # Ensure lock is removed on exit (success or failure) - trap 'rm -f "$LOCK"' EXIT - echo "══════════════════════════════════════════════════" echo " Sovran_SystemsOS Update — $(date)" echo "══════════════════════════════════════════════════" @@ -105,10 +102,12 @@ let echo "══════════════════════════════════════════════════" echo " ✓ Update completed successfully" echo "══════════════════════════════════════════════════" + echo "SUCCESS" > "$STATUS" else echo "══════════════════════════════════════════════════" echo " ✗ Update failed — see errors above" echo "══════════════════════════════════════════════════" + echo "FAILED" > "$STATUS" fi exit "$RC"