From e2b3c1ceaecccbb5e37b91af45d4f52e6ab67e4d Mon Sep 17 00:00:00 2001 From: momentum dev Date: Tue, 14 Jul 2026 00:36:00 -0400 Subject: [PATCH] operator-plane: headless smoke + effective-config panel + ledger + unpin ruff Step 6: tests/smoke_dashboard_headless.py renders the real page in a headless browser and fails on any JS console/pageerror (standalone script, not pytest-collected; CI non-blocking first). admin_api gains GET /api/config/effective (per-key masked value + source: overrides.env vs compose, missing .env.example keys flagged as drift) + an admin.html table (values XSS-escaped). dashboard.html gains the Experiment Ledger panel reading the plan-006 verdicts from the snapshot (empty-state until the tables exist). Removes the last CI ruff per-file pin (api/main.py:F821) now that os/hmac are imported; scoped ruff is green unpinned. Marks plan 007 DONE in plans/README.md. Co-Authored-By: Claude Fable 5 --- .github/workflows/docker.yml | 7 +- admin_api.py | 52 ++++++++++++ api/static/admin.html | 9 +++ api/static/dashboard.html | 29 +++++++ plans/README.md | 2 +- tests/smoke_dashboard_headless.py | 99 +++++++++++++++++++++++ tests/unit/test_admin_effective_config.py | 52 ++++++++++++ 7 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 tests/smoke_dashboard_headless.py create mode 100644 tests/unit/test_admin_effective_config.py diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e96ee7e..ff4c8b5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,15 +35,12 @@ jobs: with: { python-version: "3.13" } # match Dockerfile's python:3.13-slim - run: pip install -r requirements.txt ruff # ruff F gate, scoped to the money path (tests/ and root-level scratch - # scripts excluded). Pre-existing errors are pinned per file+rule so NEW - # F errors anywhere in scope still fail; remove each pin when its owning - # plan lands: - # api/main.py F821 (missing os/hmac imports) -> plan 007 + # scripts excluded). All per-file pins have been cleared as their owning + # plans landed (api/main.py F821 os/hmac -> plan 007), so this is unpinned. - run: >- ruff check --select F run_live_paper.py trading_execution.py telegram_bot.py api/ alpaca_paper/ runtime/ storage/ strategy/ research/ - --per-file-ignores "api/main.py:F821" - run: python -m pytest -q env: DATABASE_URL: postgresql://momentum:momentum@localhost:5432/momentum diff --git a/admin_api.py b/admin_api.py index ae35379..87a07c7 100644 --- a/admin_api.py +++ b/admin_api.py @@ -86,6 +86,56 @@ def _editable(key: str) -> bool: and not any(m in key for m in SECRET_MARKERS)) +def _mask_value(key: str, value: str) -> str: + """Mask secret VALUES (*** + last 4). Uses the broader SECRET_MARKERS set so + DATABASE_URL / *_WEBHOOK_URL are masked too — narrowing to KEY|SECRET|TOKEN| + PASSWORD would leak the DB password in the config table.""" + if any(m in key.upper() for m in SECRET_MARKERS): + tail = value[-4:] if value and len(value) >= 4 else "" + return "***" + tail + return value + + +def _env_example_keys() -> set[str]: + """Keys documented in .env.example — the 'required' config surface used for + drift detection (a documented key absent from the effective env is drift).""" + path = os.path.join(os.path.dirname(__file__), ".env.example") + keys: set[str] = set() + try: + with open(path) as fh: + for line in fh: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + keys.add(line.split("=", 1)[0].strip()) + except Exception: # noqa: BLE001 + pass + return keys + + +def effective_config() -> dict: + """Per-key effective value + source for the admin config table. + + Merge order baked < compose < overrides.env (< learned). Sources we can prove + at runtime: ``overrides.env`` (from data/overrides.env) vs ``compose`` (the + process env — baked and compose are indistinguishable inside the container, so + both surface as ``compose``); a documented .env.example key that is absent + surfaces as ``missing`` (drift). Secret values are masked.""" + overrides = env_overrides.read_overrides() + example = _env_example_keys() + keys = set(example) | set(overrides) | {k for k in os.environ if _editable(k)} + out: dict[str, dict] = {} + for key in sorted(keys): + value = os.environ.get(key) + if value is None: + out[key] = {"value_masked": None, + "source": "missing" if key in example else "unset", + "drift": key in example} + continue + source = "overrides.env" if key in overrides else "compose" + out[key] = {"value_masked": _mask_value(key, str(value)), "source": source} + return out + + # ---------- docker over the unix socket (stdlib only) ---------- class _SockConn(http.client.HTTPConnection): def __init__(self): @@ -230,6 +280,8 @@ class Handler(BaseHTTPRequestHandler): "overrides": _safe_overrides()}) if path == "/api/profiles": return self._send(200, profiles()) + if path == "/api/config/effective": + return self._send(200, {"config": effective_config()}) return self._send(404, {"error": "unknown route"}) def do_POST(self): # noqa: N802 diff --git a/api/static/admin.html b/api/static/admin.html index 49a794f..14528ec 100644 --- a/api/static/admin.html +++ b/api/static/admin.html @@ -65,6 +65,9 @@ color:var(--bg);padding:10px 20px;border-radius:10px;font-size:14px;opacity:0;tr
+

Effective config — resolved value + source per key; overrides.env beats compose. Secrets masked. A red row = a documented .env.example key is missing (drift).

+
keyeffective valuesource
+

Trading mode

Paper ⇄ Live switch
Live requires ALPACA_LIVE_API_KEY / ALPACA_LIVE_SECRET_KEY in the deployment env. Switching restarts nothing by itself — restart the loop after.
@@ -78,6 +81,7 @@ color:var(--bg);padding:10px 20px;border-radius:10px;font-size:14px;opacity:0;tr