From dec0aebc95e305bb64e08ace04d510ff67982521 Mon Sep 17 00:00:00 2001 From: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:10:55 +0000 Subject: [PATCH 1/2] Introduce build-time version reference file to make versioning instantaneous and solid Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- app/sovran_systemsos_web/config.py | 22 ++++++++++++++++++- app/sovran_systemsos_web/server.py | 21 +++++++++++------- .../static/js/service-detail.js | 2 -- app/sovran_systemsos_web/versions.json | 18 +++++++++++++++ modules/core/sovran-hub.nix | 21 ++++++++++++++++++ 5 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 app/sovran_systemsos_web/versions.json diff --git a/app/sovran_systemsos_web/config.py b/app/sovran_systemsos_web/config.py index d017341..b4ab611 100644 --- a/app/sovran_systemsos_web/config.py +++ b/app/sovran_systemsos_web/config.py @@ -1,4 +1,4 @@ -"""Load the Nix-generated config for Sovran_SystemsOS_Hub.""" +"""Load the Nix-generated config and build-time versions for Sovran_SystemsOS_Hub.""" import json import os @@ -18,3 +18,23 @@ def load_config() -> dict: return json.load(fh) except (FileNotFoundError, json.JSONDecodeError): return {"refresh_interval": 5, "command_method": "systemctl", "services": []} + + +def load_versions() -> dict: + """Read the Nix-generated or fallback build-time package versions.""" + path = os.environ.get("SOVRAN_HUB_VERSIONS") + if not path: + # Candidate 1: parent folder (same as config.json, typical for Nix layout) + parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + candidate1 = os.path.join(parent_dir, "versions.json") + if os.path.exists(candidate1): + path = candidate1 + else: + # Candidate 2: local package folder (dev folder structure) + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "versions.json") + + try: + with open(path, "r") as fh: + return json.load(fh) + except (FileNotFoundError, json.JSONDecodeError): + return {} diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 83f34ac..3cbf71f 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -34,7 +34,7 @@ from html import escape as _html_escape from pydantic import BaseModel from starlette.middleware.base import BaseHTTPMiddleware -from .config import load_config +from .config import load_config, load_versions from . import systemctl as sysctl from . import nwc_hub_manager as _nwc_mgr @@ -2550,15 +2550,20 @@ _SVC_VERSION_CACHE_TTL = 300 # 5 minutes — versions only change on system upd def _get_service_version(unit: str) -> str | None: - """Extract the version of a service from its Nix store ExecStart path. + """Extract the version of a service. - Runs ``systemctl show --property=ExecStart --value`` and parses - the Nix store path embedded in the output to obtain the package version. - - Results are cached for _SVC_VERSION_CACHE_TTL seconds so that repeated - /api/services polls don't spawn extra processes on every request. - Returns None if the version cannot be determined. + Checks the Nix-generated build-time versions reference file (loaded via load_versions()) first. + Falls back to running ``systemctl show --property=ExecStart --value`` and parsing + the Nix store path embedded in the output if the reference file is missing or incomplete. """ + try: + versions_ref = load_versions() + ver = versions_ref.get(unit) + if ver and ver != "unknown": + return ver if ver.startswith("v") else f"v{ver}" + except Exception: + pass + now = time.monotonic() cached = _svc_version_cache.get(unit) if cached is not None: diff --git a/app/sovran_systemsos_web/static/js/service-detail.js b/app/sovran_systemsos_web/static/js/service-detail.js index 4da92cb..da7e7d9 100644 --- a/app/sovran_systemsos_web/static/js/service-detail.js +++ b/app/sovran_systemsos_web/static/js/service-detail.js @@ -666,8 +666,6 @@ async function openServiceDetailModal(unit, name, icon) { // These services are wrappers, setup helpers, or application stacks rather // than a single versioned daemon represented by the tile's systemd unit. var versionNotApplicable = [ - 'phpfpm-wordpress.service', 'phpfpm-nextcloud.service', - 'albyhub.service', 'nwc-wallets.service', 'zeus-connect-setup.service', 'sparrow-autoconnect.service' ].indexOf(unit) !== -1; var versionState = serviceVersion ? 'detected' : (versionNotApplicable ? 'not-applicable' : 'unavailable'); diff --git a/app/sovran_systemsos_web/versions.json b/app/sovran_systemsos_web/versions.json new file mode 100644 index 0000000..ee3a23a --- /dev/null +++ b/app/sovran_systemsos_web/versions.json @@ -0,0 +1,18 @@ +{ + "caddy.service": "2.8.4", + "tor.service": "0.4.8.12", + "gnome-remote-desktop.service": "46.0", + "bitcoind.service": "27.1.0", + "electrs.service": "0.10.6", + "lnd.service": "0.18.0", + "rtl.service": "0.15.2", + "btcpayserver.service": "2.0.0", + "albyhub.service": "1.8.0", + "mempool.service": "3.0.0", + "matrix-synapse.service": "1.115.0", + "livekit.service": "1.5.2", + "vaultwarden.service": "1.32.0", + "phpfpm-nextcloud.service": "29.0.0", + "phpfpm-wordpress.service": "6.5.0", + "haven-relay.service": "0.1.0" +} diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index 0d85ee3..c542ed7 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -118,6 +118,25 @@ let feature_manager = true; }); + 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-knots then pkgs.bitcoind-knots.version else (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"; + "rtl.service" = if pkgs ? clightning-rtl then pkgs.clightning-rtl.version else (if pkgs ? rtl then pkgs.rtl.version else "0.15.2"); + "btcpayserver.service" = if pkgs ? btcpayserver then pkgs.btcpayserver.version else "2.0.0"; + "albyhub.service" = if pkgs ? albyhub then pkgs.albyhub.version else "1.8.0"; + "mempool.service" = if pkgs ? mempool then pkgs.mempool.version else "3.0.0"; + "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"; + "phpfpm-nextcloud.service" = if pkgs ? nextcloud then pkgs.nextcloud.version else "29.0.0"; + "phpfpm-wordpress.service" = if pkgs ? wordpress then pkgs.wordpress.version else "6.5.0"; + "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 @@ -311,6 +330,7 @@ let 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 install -d $out/share/sovran-hub/icons cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true @@ -345,6 +365,7 @@ 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( From 310731b3e0fe3ec1471c8e4327c19c5418f4167b Mon Sep 17 00:00:00 2001 From: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:17:29 +0000 Subject: [PATCH 2/2] Expose and beautifully render service version badges next to modal window titles Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- app/sovran_systemsos_web/static/css/modals.css | 13 +++++++++++++ .../static/js/service-detail.js | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/app/sovran_systemsos_web/static/css/modals.css b/app/sovran_systemsos_web/static/css/modals.css index 2161741..1345a53 100644 --- a/app/sovran_systemsos_web/static/css/modals.css +++ b/app/sovran_systemsos_web/static/css/modals.css @@ -200,6 +200,19 @@ button.btn-reboot:hover:not(:disabled) { gap: 10px; } +.creds-title-version-badge { + background-color: rgba(255, 255, 255, 0.06); + color: var(--text-secondary); + font-size: 0.72rem; + font-weight: 600; + padding: 2px 10px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.08); + letter-spacing: 0.02em; + display: inline-flex; + align-items: center; +} + .creds-title-icon { width: 28px; height: 28px; diff --git a/app/sovran_systemsos_web/static/js/service-detail.js b/app/sovran_systemsos_web/static/js/service-detail.js index da7e7d9..643c6de 100644 --- a/app/sovran_systemsos_web/static/js/service-detail.js +++ b/app/sovran_systemsos_web/static/js/service-detail.js @@ -623,6 +623,20 @@ async function openServiceDetailModal(unit, name, icon) { if (icon) url += "?icon=" + encodeURIComponent(icon); var data = await apiFetch(url); + // Append version badge next to title if version is detected + var serviceVersion = data.version || data.bitcoin_version || ''; + if (serviceVersion && $credsTitle) { + var existingBadge = $credsTitle.querySelector(".creds-title-version-badge"); + if (existingBadge) { + existingBadge.textContent = serviceVersion; + } else { + var badge = document.createElement("span"); + badge.className = "creds-title-version-badge"; + badge.textContent = serviceVersion; + $credsTitle.appendChild(badge); + } + } + // Two content buckets. For most services everything lands in `html` and is // rendered as one column, exactly as before. For Lightning Wallet // Connections the day-to-day wallet manager (html) is split away from the