From 772614a8cd2632b66b9564ba765a448a4e161e10 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Mon, 13 Jul 2026 21:28:05 -0500 Subject: [PATCH] fix(dashboard): Worker Inspect edit path activates for masked-token rigs (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard container only ever reads the MASKED config (#440), where a rig's dashboard.workers[].token is the sentinel {"__secret__": true}. load_worker_endpoints required a string token and dropped the whole entry, so DASHBOARD_WORKERS came up empty and every worker reported editable=false — even with dashboard.workers[] fully populated. Accept the masked sentinel as "token present" (keep the entry); the host-side runner still resolves the real token from the raw config when it dials the rig (pithead:4737). A bad string or any non-sentinel dict still drops the entry, fail-closed. Adds a loader test for the masked-config shape (the gap that let this ship). Bumps VERSION 1.5.0 -> 1.5.1. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 11 +++++++ VERSION | 2 +- .../mining_dashboard/config/config.py | 13 ++++++-- build/dashboard/pyproject.toml | 2 +- build/dashboard/tests/config/test_config.py | 31 +++++++++++++++++++ build/dashboard/uv.lock | 2 +- 6 files changed, 56 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1234c92..749ddc7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ per the process in [`docs/releasing.md`](docs/releasing.md). ## [Unreleased] +## [1.5.1] - 2026-07-14 + +### Fixed + +- **Worker Inspect edit path now activates for configured rigs (#508).** The dashboard reads a masked + copy of `config.json` (the container never holds real tokens, #440), where a rig's `token` is the + sentinel `{"__secret__": true}`. The worker-endpoint loader required a string token and dropped the + entry whole, so every worker showed as non-editable even with `dashboard.workers[]` set correctly. + The loader now accepts the masked sentinel as "token present"; the host-side runner still supplies + the real token when it dials the rig. + ## [1.5.0] - 2026-07-13 ### Added diff --git a/VERSION b/VERSION index bc80560f..26ca5946 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.0 +1.5.1 diff --git a/build/dashboard/mining_dashboard/config/config.py b/build/dashboard/mining_dashboard/config/config.py index 3bc754dc..5fc6d821 100644 --- a/build/dashboard/mining_dashboard/config/config.py +++ b/build/dashboard/mining_dashboard/config/config.py @@ -251,9 +251,18 @@ def load_worker_endpoints(path=None): continue entry["control_port"] = cport if "token" in item: - if not isinstance(item["token"], str) or not _WORKER_NAME_RE.match(item["token"]): + tok = item["token"] + # The container only ever reads the MASKED config (#440), where a real token is replaced + # by the sentinel {"__secret__": true}. Keep the entry then (token present, value hidden) + # so the worker stays editable — the HOST-side runner supplies the real token when it + # dials the rig (#508). A genuinely bad token (bad string, or any other dict) still drops + # the whole entry, fail-closed. + if isinstance(tok, dict) and tok.get("__secret__") is True: + entry["token"] = tok + elif isinstance(tok, str) and _WORKER_NAME_RE.match(tok): + entry["token"] = tok + else: continue - entry["token"] = item["token"] if "watts" in item: watts = _valid_watts(item["watts"]) if watts is None: diff --git a/build/dashboard/pyproject.toml b/build/dashboard/pyproject.toml index bffaabe7..74a7aec2 100644 --- a/build/dashboard/pyproject.toml +++ b/build/dashboard/pyproject.toml @@ -7,7 +7,7 @@ name = "mining-dashboard" # Keep in lockstep with the top-level VERSION file — the single source of truth for the stack version # (#44). A shell test (tests/stack/run.sh) fails if these drift; the dashboard *displays* the version # from VERSION (baked in as PITHEAD_VERSION, #58), so this is packaging metadata only. -version = "1.5.0" +version = "1.5.1" description = "Monitoring dashboard and XvB switching engine for Pithead" readme = "README.md" requires-python = ">=3.11" diff --git a/build/dashboard/tests/config/test_config.py b/build/dashboard/tests/config/test_config.py index 380ae697..af759b14 100644 --- a/build/dashboard/tests/config/test_config.py +++ b/build/dashboard/tests/config/test_config.py @@ -152,6 +152,37 @@ def test_invalid_entries_are_dropped_whole(self, tmp_path): ) assert got == [{"name": "ok", "port": 8081}] + def test_masked_sentinel_token_is_kept_but_other_dicts_drop(self, tmp_path): + # The container ONLY ever reads the masked config (#440), where a real token is the sentinel + # {"__secret__": true}. The entry must SURVIVE (token present, value hidden) so the worker is + # editable — the host-side runner uses the real token when it dials the rig (#508). Any other + # dict is not the sentinel and drops the whole entry, fail-closed. + got = self._load( + tmp_path, + { + "dashboard": { + "workers": [ + { + "name": "rig1", + "host": "10.0.0.5", + "control_port": 8082, + "token": {"__secret__": True}, + }, + {"name": "rig2", "host": "10.0.0.6", "token": {"__secret__": False}}, + {"name": "rig3", "host": "10.0.0.7", "token": {"foo": "bar"}}, + ] + } + }, + ) + assert got == [ + { + "name": "rig1", + "host": "10.0.0.5", + "control_port": 8082, + "token": {"__secret__": True}, + }, + ] + def test_missing_file_and_missing_key_read_empty(self, tmp_path): from mining_dashboard.config.config import load_worker_endpoints diff --git a/build/dashboard/uv.lock b/build/dashboard/uv.lock index d410830e..be352f19 100644 --- a/build/dashboard/uv.lock +++ b/build/dashboard/uv.lock @@ -772,7 +772,7 @@ wheels = [ [[package]] name = "mining-dashboard" -version = "1.5.0" +version = "1.5.1" source = { editable = "." } dependencies = [ { name = "aiofiles" },