From 06615a3541bc614234fe1224aac83a35573c8a36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:33:08 +0000 Subject: [PATCH 1/2] Initial plan From a0c1628461e4994c9482228c0e0b4fd7db9b010f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:38:17 +0000 Subject: [PATCH 2/2] feat: display bitcoind version on Bitcoin node tile in Hub dashboard Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5b4f8da9-beec-45f2-b116-b5c0dcf4506d Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> --- app/sovran_systemsos_web/server.py | 76 +++++++++++++++++++ app/sovran_systemsos_web/static/css/tiles.css | 7 ++ app/sovran_systemsos_web/static/js/tiles.js | 35 ++++++++- 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index a0259bf..e43f557 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -1499,6 +1499,54 @@ BITCOIN_DATADIR = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node" _btc_sync_cache: tuple[float, dict | None] = (0.0, None) _BTC_SYNC_CACHE_TTL = 5 # seconds +_btc_version_cache: tuple[float, dict | None] = (0.0, None) +_BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime + + +def _parse_bitcoin_subversion(subversion: str) -> str: + """Parse a subversion string like '/Bitcoin Knots:27.1.0/' into 'v27.1.0'. + + Examples: + '/Bitcoin Knots:27.1.0/' → 'v27.1.0' + '/Satoshi:27.0.0/' → 'v27.0.0' + '/Bitcoin Knots:27.1.0(bip110)/' → 'v27.1.0' + Falls back to the raw subversion string if parsing fails. + """ + m = re.search(r":(\d+\.\d+(?:\.\d+)*)", subversion) + if m: + return "v" + m.group(1) + return subversion + + +def _get_bitcoin_version_info() -> dict | None: + """Call bitcoin-cli getnetworkinfo and return parsed JSON, or None on error. + + Results are cached for _BTC_VERSION_CACHE_TTL seconds since the version + does not change while the service is running. + """ + global _btc_version_cache + now = time.monotonic() + cached_at, cached_val = _btc_version_cache + if now - cached_at < _BTC_VERSION_CACHE_TTL: + return cached_val + + try: + result = subprocess.run( + ["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getnetworkinfo"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + _btc_version_cache = (now, None) + return None + info = json.loads(result.stdout) + _btc_version_cache = (now, info) + return info + except Exception: + _btc_version_cache = (now, None) + return None + def _get_bitcoin_sync_info() -> dict | None: """Call bitcoin-cli getblockchaininfo and return parsed JSON, or None on error. @@ -1548,6 +1596,23 @@ async def api_bitcoin_sync(): } +@app.get("/api/bitcoin/version") +async def api_bitcoin_version(): + """Return the version string of the active bitcoind implementation.""" + loop = asyncio.get_event_loop() + info = await loop.run_in_executor(None, _get_bitcoin_version_info) + if info is None: + return JSONResponse( + status_code=503, + content={"error": "bitcoin-cli unavailable or bitcoind not running"}, + ) + subversion = info.get("subversion", "") + return { + "version": _parse_bitcoin_subversion(subversion), + "subversion": subversion, + } + + @app.get("/api/services") async def api_services(): cfg = load_config() @@ -1668,6 +1733,11 @@ async def api_services(): service_data["sync_progress"] = sync_progress service_data["sync_blocks"] = sync_blocks service_data["sync_headers"] = sync_headers + if unit == "bitcoind.service": + ver_info = await loop.run_in_executor(None, _get_bitcoin_version_info) + if ver_info is not None: + subversion = ver_info.get("subversion", "") + service_data["bitcoin_version"] = _parse_bitcoin_subversion(subversion) return service_data results = await asyncio.gather(*[get_status(s) for s in services]) @@ -1940,6 +2010,12 @@ async def api_service_detail(unit: str, icon: str | None = None): service_detail["sync_progress"] = sync_progress service_detail["sync_blocks"] = sync_blocks service_detail["sync_headers"] = sync_headers + if unit == "bitcoind.service": + loop = asyncio.get_event_loop() + ver_info = await loop.run_in_executor(None, _get_bitcoin_version_info) + if ver_info is not None: + subversion = ver_info.get("subversion", "") + service_detail["bitcoin_version"] = _parse_bitcoin_subversion(subversion) return service_detail diff --git a/app/sovran_systemsos_web/static/css/tiles.css b/app/sovran_systemsos_web/static/css/tiles.css index 613fbc9..7651f02 100644 --- a/app/sovran_systemsos_web/static/css/tiles.css +++ b/app/sovran_systemsos_web/static/css/tiles.css @@ -71,6 +71,13 @@ color: var(--text-secondary); } +.tile-version { + font-size: 0.7rem; + color: var(--text-dim); + margin-top: 2px; + text-align: center; +} + .status-dot { width: 8px; height: 8px; diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js index 436cd32..cea77b0 100644 --- a/app/sovran_systemsos_web/static/js/tiles.js +++ b/app/sovran_systemsos_web/static/js/tiles.js @@ -131,10 +131,12 @@ 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 versionLabel = svc.bitcoin_version ? '