From 602464189f7df4244151a93572b82397668f42a3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Jun 2026 19:36:51 +0000
Subject: [PATCH 1/4] Initial plan
From df08a7c41320e258be133f6b9524c1dda9f65b0e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Jun 2026 19:42:23 +0000
Subject: [PATCH 2/4] Add live BIP-110 deployment status: new helpers,
endpoint, badge UI
---
app/sovran_systemsos_web/server.py | 133 ++++++++++++++++++
app/sovran_systemsos_web/static/css/tiles.css | 51 +++++++
.../static/js/service-detail.js | 46 ++++++
app/sovran_systemsos_web/static/js/tiles.js | 68 ++++++++-
4 files changed, 297 insertions(+), 1 deletion(-)
diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py
index 5db2959..0a436a5 100644
--- a/app/sovran_systemsos_web/server.py
+++ b/app/sovran_systemsos_web/server.py
@@ -2253,6 +2253,9 @@ _BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
+# Cache for ``bitcoin-cli getdeploymentinfo`` output (BIP-110 live status)
+_btc_deployment_cache: tuple[float, dict | None] = (0.0, None)
+
# ── Generic service version detection (NixOS store path) ─────────
@@ -2367,6 +2370,119 @@ def _get_bitcoin_version_info() -> dict | None:
return None
+def _get_bitcoin_deployment_info() -> dict | None:
+ """Call bitcoin-cli getdeploymentinfo and return parsed JSON, or None on error.
+
+ Results are cached for _BTC_VERSION_CACHE_TTL seconds. Never raises.
+ """
+ global _btc_deployment_cache
+ now = time.monotonic()
+ cached_at, cached_val = _btc_deployment_cache
+ if now - cached_at < _BTC_VERSION_CACHE_TTL:
+ return cached_val
+
+ try:
+ result = subprocess.run(
+ ["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getdeploymentinfo"],
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ if result.returncode != 0:
+ _btc_deployment_cache = (now, None)
+ return None
+ info = json.loads(result.stdout)
+ _btc_deployment_cache = (now, info)
+ return info
+ except Exception:
+ _btc_deployment_cache = (now, None)
+ return None
+
+
+def _get_bip110_status() -> dict:
+ """Return a dict describing the live BIP-110 deployment/signaling state.
+
+ The returned struct has four stable keys::
+
+ {
+ "supported": bool, # node build is BIP-110-capable
+ "signaling": bool, # node is actively signaling / locked-in / active
+ "state": str, # "active" | "locked_in" | "signaling" |
+ # "not_signaling" | "unsupported" | "unknown"
+ "source": str, # "getdeploymentinfo" | "subversion" | "none"
+ }
+
+ Resolution order (authoritative → fallback → honest unknown):
+
+ 1. ``getdeploymentinfo`` (authoritative) — scan the ``deployments`` dict for an
+ entry whose key (case-insensitive) contains "bip110" or "110". The exact
+ deployment key name is **not** hard-coded because it may vary across Knots
+ releases; detection is intentionally generic so that a name change degrades
+ to "unknown" rather than producing a false result.
+
+ 2. Subversion fallback — if getdeploymentinfo is unavailable or yields no
+ recognisable BIP-110 entry, inspect the ``subversion`` field from
+ ``getnetworkinfo``. A case-insensitive match for "bip110" or "uasf" in the
+ subversion string is treated as "signaling".
+
+ 3. Unknown — if the node is entirely unreachable or neither source is
+ conclusive, return state="unknown", signaling=False, source="none".
+ """
+ _unknown: dict = {"supported": False, "signaling": False, "state": "unknown", "source": "none"}
+
+ # ── 1. getdeploymentinfo (authoritative) ──────────────────────────
+ deploy_info = _get_bitcoin_deployment_info()
+ if deploy_info is not None:
+ deployments = deploy_info.get("deployments", {})
+ if isinstance(deployments, dict):
+ for key, entry in deployments.items():
+ # Generic scan: match key containing "bip110" or "110"
+ if not ("bip110" in key.lower() or "110" in key.lower()):
+ continue
+ if not isinstance(entry, dict):
+ continue
+
+ # bip9 / bip8 status field
+ bip9 = entry.get("bip9", {}) or {}
+ bip8 = entry.get("bip8", {}) or {}
+ status = (
+ bip9.get("status")
+ or bip8.get("status")
+ or entry.get("status")
+ or ""
+ ).lower()
+ active = entry.get("active", False)
+
+ if active or status == "active":
+ return {"supported": True, "signaling": True, "state": "active", "source": "getdeploymentinfo"}
+ if status == "locked_in":
+ return {"supported": True, "signaling": True, "state": "locked_in", "source": "getdeploymentinfo"}
+ if status in ("started", "defined"):
+ # Check whether the node is currently signaling this period
+ stats = bip9.get("statistics") or bip8.get("statistics") or {}
+ signaling = bool(stats.get("signaling", False)) if stats else False
+ if signaling:
+ return {"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"}
+ return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
+ if status == "failed":
+ return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
+ # Entry found but status unrecognised — node supports BIP-110 but state unclear
+ return {"supported": True, "signaling": False, "state": "unknown", "source": "getdeploymentinfo"}
+
+ # ── 2. Subversion fallback ─────────────────────────────────────────
+ net_info = _get_bitcoin_version_info()
+ if net_info is not None:
+ subversion = net_info.get("subversion", "") or ""
+ sv_lower = subversion.lower()
+ if "bip110" in sv_lower or "uasf-bip110" in sv_lower or "uasf" in sv_lower:
+ return {"supported": True, "signaling": True, "state": "signaling", "source": "subversion"}
+ # Node is reachable via RPC but no BIP-110 marker found anywhere
+ return {"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"}
+
+ # ── 3. Node unreachable / RPC not ready ───────────────────────────
+ return _unknown
+
+
def _get_bitcoind_version() -> str | None:
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
@@ -2481,6 +2597,19 @@ async def api_bitcoin_version():
}
+@app.get("/api/bitcoin/bip110")
+async def api_bitcoin_bip110():
+ """Return live BIP-110 deployment/signaling status from bitcoin-cli.
+
+ Always returns HTTP 200. When bitcoind is unreachable or the node is mid-IBD
+ the response will contain ``state = "unknown"`` so the UI can render a neutral
+ badge rather than an error toast.
+ """
+ loop = asyncio.get_event_loop()
+ status = await loop.run_in_executor(None, _get_bip110_status)
+ return status
+
+
@app.get("/api/services")
async def api_services():
cfg = load_config()
@@ -2661,6 +2790,8 @@ async def api_services():
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_data["bitcoin_version"] = btc_ver # backwards compat
service_data["version"] = btc_ver
+ if icon == "bip110":
+ service_data["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_data
results = await asyncio.gather(*[get_status(s) for s in services])
@@ -2945,6 +3076,8 @@ async def api_service_detail(unit: str, icon: str | None = None):
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_detail["bitcoin_version"] = btc_ver # backwards compat
service_detail["version"] = btc_ver
+ if icon == "bip110":
+ service_detail["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_detail
diff --git a/app/sovran_systemsos_web/static/css/tiles.css b/app/sovran_systemsos_web/static/css/tiles.css
index 32f2278..e8aba2c 100644
--- a/app/sovran_systemsos_web/static/css/tiles.css
+++ b/app/sovran_systemsos_web/static/css/tiles.css
@@ -155,6 +155,57 @@
white-space: nowrap;
}
+/* ── BIP-110 status badge (tile + detail modal) ───────────────────── */
+
+.tile-bip110-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ font-size: 0.64rem;
+ font-weight: 600;
+ border-radius: 4px;
+ padding: 2px 6px;
+ margin-top: 4px;
+ white-space: nowrap;
+ letter-spacing: 0.02em;
+}
+
+.tile-bip110-badge--active {
+ background: rgba(109, 191, 139, 0.18);
+ color: var(--green);
+ border: 1px solid rgba(109, 191, 139, 0.3);
+}
+
+.tile-bip110-badge--locked_in {
+ background: rgba(94, 173, 138, 0.15);
+ color: var(--accent-color);
+ border: 1px solid rgba(94, 173, 138, 0.3);
+}
+
+.tile-bip110-badge--signaling {
+ background: rgba(94, 173, 138, 0.12);
+ color: var(--accent-color);
+ border: 1px solid rgba(94, 173, 138, 0.2);
+}
+
+.tile-bip110-badge--not_signaling {
+ background: rgba(229, 165, 10, 0.12);
+ color: var(--yellow);
+ border: 1px solid rgba(229, 165, 10, 0.25);
+}
+
+.tile-bip110-badge--unsupported {
+ background: rgba(94, 122, 106, 0.12);
+ color: var(--grey);
+ border: 1px solid rgba(94, 122, 106, 0.2);
+}
+
+.tile-bip110-badge--unknown {
+ background: transparent;
+ color: var(--text-dim);
+ border: 1px solid var(--border-color);
+}
+
/* ── Service detail modal sections ───────────────────────────────── */
.svc-detail-section {
diff --git a/app/sovran_systemsos_web/static/js/service-detail.js b/app/sovran_systemsos_web/static/js/service-detail.js
index 29e1531..87918bf 100644
--- a/app/sovran_systemsos_web/static/js/service-detail.js
+++ b/app/sovran_systemsos_web/static/js/service-detail.js
@@ -107,6 +107,52 @@ async function openServiceDetailModal(unit, name, icon) {
'' +
'';
+ // Section B2: BIP-110 live status (bip110 tile only)
+ if (icon === 'bip110' && data.bip110) {
+ var bip110 = data.bip110;
+ var bip110State = bip110.state || 'unknown';
+ var bip110BadgeCls, bip110Label, bip110Tooltip;
+ switch (bip110State) {
+ case 'active':
+ bip110BadgeCls = 'tile-bip110-badge--active';
+ bip110Label = 'BIP\u2011110: Active \u2713';
+ bip110Tooltip = 'BIP-110 is active on this node';
+ break;
+ case 'locked_in':
+ bip110BadgeCls = 'tile-bip110-badge--locked_in';
+ bip110Label = 'BIP\u2011110: Locked In';
+ bip110Tooltip = 'BIP-110 is locked in and will activate shortly';
+ break;
+ case 'signaling':
+ bip110BadgeCls = 'tile-bip110-badge--signaling';
+ bip110Label = 'BIP\u2011110: Signaling';
+ bip110Tooltip = 'Node is signaling readiness for BIP-110';
+ break;
+ case 'not_signaling':
+ bip110BadgeCls = 'tile-bip110-badge--not_signaling';
+ bip110Label = 'BIP\u2011110: Not Signaling';
+ bip110Tooltip = 'Node supports BIP-110 but is not signaling this period';
+ break;
+ case 'unsupported':
+ bip110BadgeCls = 'tile-bip110-badge--unsupported';
+ bip110Label = 'BIP\u2011110: Not Supported';
+ bip110Tooltip = 'This node build does not include BIP-110';
+ break;
+ default:
+ bip110BadgeCls = 'tile-bip110-badge--unknown';
+ bip110Label = 'BIP\u2011110: \u2014';
+ bip110Tooltip = 'Status unavailable (node syncing or RPC not ready)';
+ }
+ var bip110Source = bip110.source ? ' (source: ' + escHtml(bip110.source) + ')' : '';
+ html += '
' +
+ '
BIP-110 Deployment Status
' +
+ '
' +
+ '' + escHtml(bip110Label) + '' +
+ bip110Source +
+ '
' +
+ '
';
+ }
+
// Section C: Domain diagnostics (domain services)
if (data.needs_domain) {
var steps = data.domain_check_steps || [];
diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js
index d1bac29..7d8b974 100644
--- a/app/sovran_systemsos_web/static/js/tiles.js
+++ b/app/sovran_systemsos_web/static/js/tiles.js
@@ -4,6 +4,46 @@
// Keyed by tileId: { progress: float, timestamp: ms }
var _btcSyncPrev = {};
+// ── BIP-110 badge helper ──────────────────────────────────────────
+
+function _renderBip110Badge(bip110) {
+ if (!bip110) return '';
+ var state = bip110.state || 'unknown';
+ var label, cls, title;
+ switch (state) {
+ case 'active':
+ label = 'BIP\u2011110: Active \u2713';
+ cls = 'tile-bip110-badge--active';
+ title = 'BIP-110 is active on this node';
+ break;
+ case 'locked_in':
+ label = 'BIP\u2011110: Locked In';
+ cls = 'tile-bip110-badge--locked_in';
+ title = 'BIP-110 is locked in and will activate shortly';
+ break;
+ case 'signaling':
+ label = 'BIP\u2011110: Signaling';
+ cls = 'tile-bip110-badge--signaling';
+ title = 'Node is signaling readiness for BIP-110';
+ break;
+ case 'not_signaling':
+ label = 'BIP\u2011110: Not Signaling';
+ cls = 'tile-bip110-badge--not_signaling';
+ title = 'Node supports BIP-110 but is not signaling this period';
+ break;
+ case 'unsupported':
+ label = 'BIP\u2011110: Not Supported';
+ cls = 'tile-bip110-badge--unsupported';
+ title = 'This node build does not include BIP-110';
+ break;
+ default:
+ label = 'BIP\u2011110: \u2014';
+ cls = 'tile-bip110-badge--unknown';
+ title = 'Status unavailable (node syncing or RPC not ready)';
+ }
+ return '' + escHtml(label) + '
';
+}
+
// ── Render: initial build ─────────────────────────────────────────
function buildTiles(services, categoryLabels) {
@@ -165,7 +205,8 @@ function buildTile(svc) {
var ver = svc.version || svc.bitcoin_version || '';
var versionLabel = ver ? '' + escHtml(ver) + '
' : '';
- tile.innerHTML = ' + '.svg)
?
' + escHtml(svc.name) + '
' + versionLabel + '' + st + '
';
+ var bip110Badge = (svc.icon === 'bip110') ? _renderBip110Badge(svc.bip110) : '';
+ tile.innerHTML = ' + '.svg)
?
' + escHtml(svc.name) + '
' + versionLabel + bip110Badge + '' + st + '
';
tile.style.cursor = "pointer";
tile.addEventListener("click", function() {
@@ -265,6 +306,31 @@ function updateTiles(services) {
}
}
}
+ // Update BIP-110 badge for bip110 tiles
+ if (svc.icon === 'bip110') {
+ var badgeHtml = _renderBip110Badge(svc.bip110);
+ var badgeEl = tile.querySelector(".tile-bip110-badge");
+ if (badgeEl) {
+ // Replace existing badge in-place
+ var tmp = document.createElement("div");
+ tmp.innerHTML = badgeHtml;
+ var newBadge = tmp.firstElementChild;
+ 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");
+ if (anchorEl) {
+ var tmpDiv = document.createElement("div");
+ tmpDiv.innerHTML = badgeHtml;
+ var newBadgeEl = tmpDiv.firstElementChild;
+ if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl);
+ }
+ }
+ }
}
}
}
From 69b84153b4036f7c5c046ed035110a7aafe9fef8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Jun 2026 19:46:40 +0000
Subject: [PATCH 3/4] Address code review: tighten bip110 key matching, fix
redundant condition, extract shared badge config, add CSS classes
---
app/sovran_systemsos_web/server.py | 10 +++--
app/sovran_systemsos_web/static/css/tiles.css | 12 ++++++
app/sovran_systemsos_web/static/js/helpers.js | 14 +++++++
.../static/js/service-detail.js | 39 ++-----------------
app/sovran_systemsos_web/static/js/tiles.js | 35 +----------------
5 files changed, 38 insertions(+), 72 deletions(-)
diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py
index 0a436a5..2eea02f 100644
--- a/app/sovran_systemsos_web/server.py
+++ b/app/sovran_systemsos_web/server.py
@@ -2436,8 +2436,10 @@ def _get_bip110_status() -> dict:
deployments = deploy_info.get("deployments", {})
if isinstance(deployments, dict):
for key, entry in deployments.items():
- # Generic scan: match key containing "bip110" or "110"
- if not ("bip110" in key.lower() or "110" in key.lower()):
+ # Generic scan: match key that contains "bip110" (case-insensitive).
+ # Deliberately not matching bare "110" to avoid false positives on
+ # unrelated deployments whose names happen to include that digit sequence.
+ if "bip110" not in key.lower():
continue
if not isinstance(entry, dict):
continue
@@ -2460,7 +2462,7 @@ def _get_bip110_status() -> dict:
if status in ("started", "defined"):
# Check whether the node is currently signaling this period
stats = bip9.get("statistics") or bip8.get("statistics") or {}
- signaling = bool(stats.get("signaling", False)) if stats else False
+ signaling = bool(stats.get("signaling", False))
if signaling:
return {"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"}
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
@@ -2474,7 +2476,7 @@ def _get_bip110_status() -> dict:
if net_info is not None:
subversion = net_info.get("subversion", "") or ""
sv_lower = subversion.lower()
- if "bip110" in sv_lower or "uasf-bip110" in sv_lower or "uasf" in sv_lower:
+ if "bip110" in sv_lower or "uasf-bip110" in sv_lower:
return {"supported": True, "signaling": True, "state": "signaling", "source": "subversion"}
# Node is reachable via RPC but no BIP-110 marker found anywhere
return {"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"}
diff --git a/app/sovran_systemsos_web/static/css/tiles.css b/app/sovran_systemsos_web/static/css/tiles.css
index e8aba2c..94e9f7f 100644
--- a/app/sovran_systemsos_web/static/css/tiles.css
+++ b/app/sovran_systemsos_web/static/css/tiles.css
@@ -206,6 +206,18 @@
border: 1px solid var(--border-color);
}
+.bip110-status-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.bip110-source-label {
+ color: var(--text-dim);
+ font-size: 0.75rem;
+}
+
/* ── Service detail modal sections ───────────────────────────────── */
.svc-detail-section {
diff --git a/app/sovran_systemsos_web/static/js/helpers.js b/app/sovran_systemsos_web/static/js/helpers.js
index 470e711..d452fca 100644
--- a/app/sovran_systemsos_web/static/js/helpers.js
+++ b/app/sovran_systemsos_web/static/js/helpers.js
@@ -60,3 +60,17 @@ async function apiFetch(path, options) {
}
return res.json();
}
+
+
+// ── BIP-110 badge state config ────────────────────────────────────
+// Shared lookup used by tiles.js and service-detail.js.
+// Keys match the "state" values returned by /api/bitcoin/bip110.
+
+var BIP110_BADGE_CONFIG = {
+ active: { cls: 'tile-bip110-badge--active', label: 'BIP\u2011110: Active \u2713', title: 'BIP-110 is active on this node' },
+ locked_in: { cls: 'tile-bip110-badge--locked_in', label: 'BIP\u2011110: Locked In', title: 'BIP-110 is locked in and will activate shortly' },
+ signaling: { cls: 'tile-bip110-badge--signaling', label: 'BIP\u2011110: Signaling', title: 'Node is signaling readiness for BIP-110' },
+ not_signaling: { cls: 'tile-bip110-badge--not_signaling',label: 'BIP\u2011110: Not Signaling', title: 'Node supports BIP-110 but is not signaling this period' },
+ unsupported: { cls: 'tile-bip110-badge--unsupported', label: 'BIP\u2011110: Not Supported', title: 'This node build does not include BIP-110' },
+ unknown: { cls: 'tile-bip110-badge--unknown', label: 'BIP\u2011110: \u2014', title: 'Status unavailable (node syncing or RPC not ready)' }
+};
diff --git a/app/sovran_systemsos_web/static/js/service-detail.js b/app/sovran_systemsos_web/static/js/service-detail.js
index 87918bf..a86a6c6 100644
--- a/app/sovran_systemsos_web/static/js/service-detail.js
+++ b/app/sovran_systemsos_web/static/js/service-detail.js
@@ -111,43 +111,12 @@ async function openServiceDetailModal(unit, name, icon) {
if (icon === 'bip110' && data.bip110) {
var bip110 = data.bip110;
var bip110State = bip110.state || 'unknown';
- var bip110BadgeCls, bip110Label, bip110Tooltip;
- switch (bip110State) {
- case 'active':
- bip110BadgeCls = 'tile-bip110-badge--active';
- bip110Label = 'BIP\u2011110: Active \u2713';
- bip110Tooltip = 'BIP-110 is active on this node';
- break;
- case 'locked_in':
- bip110BadgeCls = 'tile-bip110-badge--locked_in';
- bip110Label = 'BIP\u2011110: Locked In';
- bip110Tooltip = 'BIP-110 is locked in and will activate shortly';
- break;
- case 'signaling':
- bip110BadgeCls = 'tile-bip110-badge--signaling';
- bip110Label = 'BIP\u2011110: Signaling';
- bip110Tooltip = 'Node is signaling readiness for BIP-110';
- break;
- case 'not_signaling':
- bip110BadgeCls = 'tile-bip110-badge--not_signaling';
- bip110Label = 'BIP\u2011110: Not Signaling';
- bip110Tooltip = 'Node supports BIP-110 but is not signaling this period';
- break;
- case 'unsupported':
- bip110BadgeCls = 'tile-bip110-badge--unsupported';
- bip110Label = 'BIP\u2011110: Not Supported';
- bip110Tooltip = 'This node build does not include BIP-110';
- break;
- default:
- bip110BadgeCls = 'tile-bip110-badge--unknown';
- bip110Label = 'BIP\u2011110: \u2014';
- bip110Tooltip = 'Status unavailable (node syncing or RPC not ready)';
- }
- var bip110Source = bip110.source ? ' (source: ' + escHtml(bip110.source) + ')' : '';
+ var bip110Cfg = BIP110_BADGE_CONFIG[bip110State] || BIP110_BADGE_CONFIG.unknown;
+ var bip110Source = bip110.source ? ' (source: ' + escHtml(bip110.source) + ')' : '';
html += '' +
'
BIP-110 Deployment Status
' +
- '
' +
- '
' + escHtml(bip110Label) + '' +
+ '
' +
+ '' + escHtml(bip110Cfg.label) + '' +
bip110Source +
'
' +
'
';
diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js
index 7d8b974..045ccd2 100644
--- a/app/sovran_systemsos_web/static/js/tiles.js
+++ b/app/sovran_systemsos_web/static/js/tiles.js
@@ -9,39 +9,8 @@ var _btcSyncPrev = {};
function _renderBip110Badge(bip110) {
if (!bip110) return '';
var state = bip110.state || 'unknown';
- var label, cls, title;
- switch (state) {
- case 'active':
- label = 'BIP\u2011110: Active \u2713';
- cls = 'tile-bip110-badge--active';
- title = 'BIP-110 is active on this node';
- break;
- case 'locked_in':
- label = 'BIP\u2011110: Locked In';
- cls = 'tile-bip110-badge--locked_in';
- title = 'BIP-110 is locked in and will activate shortly';
- break;
- case 'signaling':
- label = 'BIP\u2011110: Signaling';
- cls = 'tile-bip110-badge--signaling';
- title = 'Node is signaling readiness for BIP-110';
- break;
- case 'not_signaling':
- label = 'BIP\u2011110: Not Signaling';
- cls = 'tile-bip110-badge--not_signaling';
- title = 'Node supports BIP-110 but is not signaling this period';
- break;
- case 'unsupported':
- label = 'BIP\u2011110: Not Supported';
- cls = 'tile-bip110-badge--unsupported';
- title = 'This node build does not include BIP-110';
- break;
- default:
- label = 'BIP\u2011110: \u2014';
- cls = 'tile-bip110-badge--unknown';
- title = 'Status unavailable (node syncing or RPC not ready)';
- }
- return '
' + escHtml(label) + '
';
+ var cfg = BIP110_BADGE_CONFIG[state] || BIP110_BADGE_CONFIG.unknown;
+ return '
' + escHtml(cfg.label) + '
';
}
// ── Render: initial build ─────────────────────────────────────────
From 06988d0ff0440317afb7d4c9489b35ecd9a10ef0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Jun 2026 19:49:01 +0000
Subject: [PATCH 4/4] Fix docstring accuracy, extract _firstElementFromHtml
helper, address all code review feedback
---
app/sovran_systemsos_web/server.py | 6 +++---
app/sovran_systemsos_web/static/js/tiles.js | 20 +++++++++-----------
2 files changed, 12 insertions(+), 14 deletions(-)
diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py
index 2eea02f..a8e043d 100644
--- a/app/sovran_systemsos_web/server.py
+++ b/app/sovran_systemsos_web/server.py
@@ -2415,15 +2415,15 @@ def _get_bip110_status() -> dict:
Resolution order (authoritative → fallback → honest unknown):
1. ``getdeploymentinfo`` (authoritative) — scan the ``deployments`` dict for an
- entry whose key (case-insensitive) contains "bip110" or "110". The exact
+ entry whose key (case-insensitive) contains "bip110". The exact
deployment key name is **not** hard-coded because it may vary across Knots
releases; detection is intentionally generic so that a name change degrades
to "unknown" rather than producing a false result.
2. Subversion fallback — if getdeploymentinfo is unavailable or yields no
recognisable BIP-110 entry, inspect the ``subversion`` field from
- ``getnetworkinfo``. A case-insensitive match for "bip110" or "uasf" in the
- subversion string is treated as "signaling".
+ ``getnetworkinfo``. A case-insensitive match for "bip110" or "uasf-bip110"
+ in the subversion string is treated as "signaling".
3. Unknown — if the node is entirely unreachable or neither source is
conclusive, return state="unknown", signaling=False, source="none".
diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js
index 045ccd2..e07bf04 100644
--- a/app/sovran_systemsos_web/static/js/tiles.js
+++ b/app/sovran_systemsos_web/static/js/tiles.js
@@ -13,6 +13,12 @@ function _renderBip110Badge(bip110) {
return '
' + escHtml(cfg.label) + '
';
}
+function _firstElementFromHtml(html) {
+ var tmp = document.createElement("div");
+ tmp.innerHTML = html;
+ return tmp.firstElementChild || null;
+}
+
// ── Render: initial build ─────────────────────────────────────────
function buildTiles(services, categoryLabels) {
@@ -281,21 +287,13 @@ function updateTiles(services) {
var badgeEl = tile.querySelector(".tile-bip110-badge");
if (badgeEl) {
// Replace existing badge in-place
- var tmp = document.createElement("div");
- tmp.innerHTML = badgeHtml;
- var newBadge = tmp.firstElementChild;
- if (newBadge) {
- badgeEl.replaceWith(newBadge);
- } else {
- badgeEl.remove();
- }
+ 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");
if (anchorEl) {
- var tmpDiv = document.createElement("div");
- tmpDiv.innerHTML = badgeHtml;
- var newBadgeEl = tmpDiv.firstElementChild;
+ var newBadgeEl = _firstElementFromHtml(badgeHtml);
if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl);
}
}