diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 793d508..83f34ac 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -2576,6 +2576,15 @@ def _get_service_version(unit: str) -> str | None: ) if result.returncode == 0 and result.stdout.strip(): m = _NIX_STORE_VERSION_RE.search(result.stdout) + # Some wrapped systemd commands contain an escaped or nested + # store path that the strict package-path expression misses. Fall + # back to scanning each store path segment for the version token. + if not m: + for store_path in re.findall(r"/nix/store/[a-z0-9]{32}-[^\\s/\\\"]+", result.stdout): + candidates = re.findall(r"(?:^|-)(\\d+\\.\\d+(?:\\.\\d+)*(?:[+-][a-zA-Z0-9]+(?:\\.[a-zA-Z0-9]+)*)?)$", store_path) + if candidates: + m = type("_VersionMatch", (), {"group": lambda self, n: candidates[-1]})() + break if m: ver = m.group(1) # Strip a single trailing period (defensive; shouldn't appear in store paths) diff --git a/app/sovran_systemsos_web/static/css/tiles.css b/app/sovran_systemsos_web/static/css/tiles.css index 94e9f7f..d918c64 100644 --- a/app/sovran_systemsos_web/static/css/tiles.css +++ b/app/sovran_systemsos_web/static/css/tiles.css @@ -256,6 +256,80 @@ color: var(--text-primary); } +/* Version lives in the detail modal so compact tiles stay visually stable. */ +.svc-detail-version-card { + display: flex; + align-items: center; + gap: 12px; + margin: -6px 0 20px; + padding: 12px 14px; + border: 1px solid rgba(109, 191, 139, 0.28); + border-radius: 10px; + background: linear-gradient(135deg, rgba(109, 191, 139, 0.12), rgba(109, 191, 139, 0.04)); +} + +.svc-detail-version-icon { + display: grid; + place-items: center; + width: 30px; + height: 30px; + border-radius: 8px; + background: rgba(109, 191, 139, 0.18); + color: var(--accent-color); + font-size: 1.35rem; + font-weight: 700; +} + +.svc-detail-version-label { + color: var(--text-dim); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.svc-detail-version-copy { + min-width: 0; +} + +.svc-detail-version-state { + display: inline-block; + margin-left: 6px; + padding: 2px 6px; + border-radius: 999px; + color: var(--accent-color); + background: rgba(109, 191, 139, 0.14); + font-size: 0.62rem; + letter-spacing: 0.04em; +} + +.svc-detail-version-card--unavailable, +.svc-detail-version-card--not-applicable { + border-color: rgba(229, 165, 10, 0.3); + background: rgba(229, 165, 10, 0.07); +} + +.svc-detail-version-card--unavailable .svc-detail-version-icon, +.svc-detail-version-card--not-applicable .svc-detail-version-icon { + color: var(--yellow); + background: rgba(229, 165, 10, 0.14); +} + +.svc-detail-version-card--unavailable .svc-detail-version-state, +.svc-detail-version-card--not-applicable .svc-detail-version-state { + color: var(--yellow); + background: rgba(229, 165, 10, 0.14); +} + +.svc-detail-version-value { + margin-top: 2px; + color: var(--text-primary); + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 0.95rem; + font-weight: 700; + word-break: break-word; +} + /* ── Service detail: Domain ──────────────────────────────────────── */ .svc-detail-domain-value { diff --git a/app/sovran_systemsos_web/static/js/service-detail.js b/app/sovran_systemsos_web/static/js/service-detail.js index 060574a..2d077e8 100644 --- a/app/sovran_systemsos_web/static/js/service-detail.js +++ b/app/sovran_systemsos_web/static/js/service-detail.js @@ -659,6 +659,29 @@ async function openServiceDetailModal(unit, name, icon) { '' + ''); + // Keep package versions in the detail modal rather than the compact tile. + // The API uses `version` for regular services and `bitcoin_version` for + // older Bitcoin payloads, so accept both while the backend is upgraded. + var serviceVersion = data.version || data.bitcoin_version || ''; + // 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'); + var versionText = serviceVersion || (versionNotApplicable + ? 'This service does not expose a single package version' + : (effectiveEnabled ? 'Version could not be detected' : 'Start the service to detect its version')); + var versionBadge = serviceVersion ? 'Detected' : (versionNotApplicable ? 'Not applicable' : 'Not detected'); + addSetup('
' + + '
' + + '
Installed version ' + + '' + versionBadge + '
' + + '
' + escHtml(versionText) + '
' + + '
'); + // Section B2: BIP-110 live status (bip110 tile only) if (icon === 'bip110' && data.bip110) { var bip110 = data.bip110; diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js index e07bf04..c1a6d97 100644 --- a/app/sovran_systemsos_web/static/js/tiles.js +++ b/app/sovran_systemsos_web/static/js/tiles.js @@ -156,13 +156,10 @@ function buildTile(svc) { var pct = Math.round((svc.sync_progress || 0) * 100); var id = tileId(svc); var eta = _calcBtcEta(id, svc.sync_progress || 0); - var ver = svc.version || svc.bitcoin_version || ''; - var versionLabel = ver ? '
' + escHtml(ver) + '
' : ''; tile.innerHTML = '' + escHtml(svc.name) + '' + '' + '
' + escHtml(svc.name) + '
' + - versionLabel + '
' + '
\u23F3 Syncing Timechain
' + '
' + @@ -178,10 +175,8 @@ function buildTile(svc) { return tile; } - var ver = svc.version || svc.bitcoin_version || ''; - var versionLabel = ver ? '
' + escHtml(ver) + '
' : ''; var bip110Badge = (svc.icon === 'bip110') ? _renderBip110Badge(svc.bip110) : ''; - tile.innerHTML = '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + versionLabel + bip110Badge + '
' + st + '
'; + tile.innerHTML = '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + bip110Badge + '
' + st + '
'; tile.style.cursor = "pointer"; tile.addEventListener("click", function() { @@ -235,22 +230,6 @@ function updateTiles(services) { if (fill) fill.style.width = pct + "%"; if (pctEl) pctEl.textContent = pct + "%"; if (etaEl) etaEl.textContent = etaText; - // Update or insert version label - var syncVer = svc.version || svc.bitcoin_version || ''; - if (syncVer) { - var syncVerEl = tile.querySelector(".tile-version"); - if (syncVerEl) { - syncVerEl.textContent = syncVer; - } else { - var syncNameEl = tile.querySelector(".tile-name"); - if (syncNameEl) { - var newSyncVerEl = document.createElement("div"); - newSyncVerEl.className = "tile-version"; - newSyncVerEl.textContent = syncVer; - syncNameEl.insertAdjacentElement("afterend", newSyncVerEl); - } - } - } } else { // IBD finished or not syncing — if tile had sync layout rebuild it normally if (tile.querySelector(".tile-sync-container")) { @@ -265,22 +244,6 @@ function updateTiles(services) { var text = tile.querySelector(".status-text"); if (dot) dot.className = "status-dot " + sc; if (text) text.textContent = st; - // Update or insert version label for all service tiles - var tileVer = svc.version || svc.bitcoin_version || ''; - if (tileVer) { - var verEl = tile.querySelector(".tile-version"); - if (verEl) { - verEl.textContent = tileVer; - } else { - var nameEl = tile.querySelector(".tile-name"); - if (nameEl) { - var newVerEl = document.createElement("div"); - newVerEl.className = "tile-version"; - newVerEl.textContent = tileVer; - nameEl.insertAdjacentElement("afterend", newVerEl); - } - } - } // Update BIP-110 badge for bip110 tiles if (svc.icon === 'bip110') { var badgeHtml = _renderBip110Badge(svc.bip110); @@ -290,8 +253,8 @@ function updateTiles(services) { var newBadge = _firstElementFromHtml(badgeHtml); if (newBadge) { badgeEl.replaceWith(newBadge); } else { badgeEl.remove(); } } else if (badgeHtml) { - // Insert badge after version label (or after tile-name if no version) - var anchorEl = tile.querySelector(".tile-version") || tile.querySelector(".tile-name"); + // Insert badge after the service name + var anchorEl = tile.querySelector(".tile-name"); if (anchorEl) { var newBadgeEl = _firstElementFromHtml(badgeHtml); if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl); diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index e361ae5..0d85ee3 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -79,7 +79,7 @@ let { label = "Test Username"; file = "/var/lib/secrets/matrix-test-username"; } { label = "Test Password"; file = "/var/lib/secrets/matrix-test-password"; } ]; } - { name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "element-calling"; enabled = cfg.features.element-calling; category = "communication"; credentials = []; } + { 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 [