From 910403f4fa72355aa03e69729c2cf84735d010d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:23:10 +0000 Subject: [PATCH 1/2] The Heart board joins the family look MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The health board carried its own copy of the GitHub-grey stylesheet: a plain h1, dark-only, GitHub-blue links, and verdict pills in colours picked here rather than from the logo above the README. It renders through the Brain's `board/_theme.py` instead: the hero (the heart with its ECG and check badge, the wordmark, the hairline-and-dot rule, the tagline), the accent as link and heading colour, and the verdict as the family's banner. The theme is imported, never copied — heart-health.yml and heart-tests.yml check PyAutoBrain out beside this repo, and a local run resolves the sibling checkout the way the other PyAuto tools find each other. Only the html surface calls the theme. The md, json and badge surfaces are untouched, so the Health Agent and mobile keep reading the same contract with no PyAutoBrain in reach — a look is not evidence, and nothing here changes what the Heart observes or how it scores. The per-row state dot, the evidence lists and the stale banner stay here, written against the theme's variables so this board follows the family accent rather than setting a second palette. Verdict tone maps once (red/yellow/stale/green → the theme's bad/warn/warn/ok); `_VERDICT_STATE` stays the internal truth. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FhsBsESQUAoJQ2fvLiDD5j --- .github/workflows/heart-health.yml | 8 ++ .github/workflows/heart-tests.yml | 7 ++ heart/dashboard.py | 161 +++++++++++++++++------------ tests/test_dashboard.py | 17 ++- 4 files changed, 123 insertions(+), 70 deletions(-) diff --git a/.github/workflows/heart-health.yml b/.github/workflows/heart-health.yml index 77ea25d..3c20dd8 100644 --- a/.github/workflows/heart-health.yml +++ b/.github/workflows/heart-health.yml @@ -65,6 +65,14 @@ jobs: - name: Checkout PyAutoHeart uses: actions/checkout@v4 + # The shared board look the dashboard renders with — imported from the + # Brain, never copied here. + - name: Checkout PyAutoBrain (the shared board theme) + uses: actions/checkout@v4 + with: + repository: PyAutoLabs/PyAutoBrain + path: PyAutoBrain + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/heart-tests.yml b/.github/workflows/heart-tests.yml index 6d58252..c0e9537 100644 --- a/.github/workflows/heart-tests.yml +++ b/.github/workflows/heart-tests.yml @@ -53,6 +53,13 @@ jobs: with: repository: PyAutoLabs/PyAutoMind path: PyAutoMind + # The shared board look the dashboard renders with — imported from the + # Brain, never copied here. + - name: Checkout PyAutoBrain (the shared board theme) + uses: actions/checkout@v4 + with: + repository: PyAutoLabs/PyAutoBrain + path: PyAutoBrain - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: diff --git a/heart/dashboard.py b/heart/dashboard.py index 260d77f..8cd5135 100644 --- a/heart/dashboard.py +++ b/heart/dashboard.py @@ -43,6 +43,8 @@ import datetime import html as _html import json +import os +import pathlib import sys from dataclasses import dataclass, field from typing import Any, Iterable, Sequence @@ -145,6 +147,37 @@ def _library_names() -> tuple: # that links "the webpage" agrees on the URL. PAGES_URL = "https://pyautolabs.github.io/PyAutoHeart/" +# The family look lives once, in the Brain (``board/_theme.py``): the +# stylesheet, the hero that redraws this organ's logo as a mark, and the +# cross-board footer. Imported rather than copied, so the look moves for the +# whole family at once — heart-health.yml checks PyAutoBrain out beside this +# repo, and a local run finds the sibling checkout the way the other PyAuto +# tools resolve each other. +HEART_HOME = pathlib.Path(__file__).resolve().parents[1] +BOARD_KEY = "heart" # this board's entry in the Brain's palette table + + +def theme(): + """The shared theme module, or a RuntimeError naming the fix. + + Only the html surface needs it; the md/json/badge surfaces never call + here, so the Health Agent keeps working with no PyAutoBrain in reach. + """ + for cand in (os.environ.get("PYAUTO_BRAIN"), HEART_HOME / "PyAutoBrain", + HEART_HOME.parent / "PyAutoBrain", + pathlib.Path.home() / "Code" / "PyAutoLabs" / "PyAutoBrain"): + if not cand: + continue + board_dir = pathlib.Path(cand) / "board" + if (board_dir / "_theme.py").is_file(): + if str(board_dir) not in sys.path: + sys.path.insert(0, str(board_dir)) + import _theme + return _theme + raise RuntimeError( + "the shared board theme (PyAutoBrain/board/_theme.py) is not in reach " + "— check PyAutoBrain out beside this repo or set PYAUTO_BRAIN") + # The one-tap board family — the cross-board footer nav every board carries, # each board skipping its own entry. The base comes from PAGES_URL so the # owner is named exactly once in this file. @@ -154,9 +187,11 @@ def _library_names() -> tuple: def _boards_nav_html() -> str: + """The cross-board footer — one chip per sibling, each in its own organ's + colour (the theme owns the chip palette; this board owns the URLs).""" base = PAGES_URL.rsplit("/", 2)[0] - return " · ".join(f'{name}' - for name, repo in BOARD_FAMILY) + links = {key: f"{base}/{repo}/" for key, repo in BOARD_FAMILY} + return theme().boards_footer(links, BOARD_KEY) # v2: sections gained links/action/observed_ago; the board gained structured # `blockers` ({text, severity, repo, repo_url, run_url, prompt}). Additive. @@ -1195,8 +1230,7 @@ def _copy_btn(payload: str, label: str = "copy") -> str: and the payload — a Claude prompt or a command — is ready to paste.""" return (f"") + f"data-cmd=\"{_html.escape(payload, quote=True)}\">📋") def _html_reason(item: dict) -> str: @@ -1214,6 +1248,42 @@ def _html_reason(item: dict) -> str: return f"
  • {text}
  • " +# The Heart's verdict in the theme's tone vocabulary. The board's own +# `_VERDICT_STATE` stays the internal truth; this is only how it is painted. +_VERDICT_TONE = {"red": "bad", "yellow": "warn", "stale": "warn", + "green": "ok"} + +_LEDE = ("Is it safe to release? Every check the Heart observes, with the " + "evidence behind each verdict. \U0001f4cb copies a ready-to-paste prompt " + "or command for a Claude Code chat.") + +# The page-specific shapes the shared sheet has no opinion on: the per-row +# state dot, the evidence list, the stale banner. Written against the theme's +# variables, so this board follows the family accent rather than setting a +# second palette. +_EXTRA_CSS = """ +table.board td.dot{width:1.15rem;padding-right:.35rem} +table.board td.dot::before{content:"";display:inline-block;width:10px; + height:10px;border-radius:50%;margin-top:.35rem;background:var(--muted)} +table.board tr.ok td.dot::before{background:var(--ok)} +table.board tr.warn td.dot::before{background:var(--warn)} +table.board tr.fail td.dot::before{background:var(--bad)} +table.board tr.info td.dot::before{background:var(--accent)} +table.board td.name{font-weight:600;white-space:nowrap} +table.board tr.unobs td.name,table.board tr.unobs td.sum{color:var(--muted)} +ul.det{margin:.35rem 0 0;padding-left:1.1rem;color:var(--muted); + font-size:.85rem} +.ago{color:var(--muted)} +a.out{font-size:.85rem;white-space:nowrap} +.stale{background:var(--btn);border:1px solid var(--warn);color:var(--warn); + padding:.55rem .75rem;border-radius:8px} +.reasons{margin:1.5rem 0} +.reasons li{margin:.3rem 0} +.hint{color:var(--muted);font-size:.85em;margin:.5rem 0 0} +footer{margin-top:2rem;color:var(--muted);font-size:.82em} +""" + + def _render_html(board: Board) -> str: word = _VERDICT_WORD.get(board.verdict, "GREEN") vstate = _VERDICT_STATE.get(board.verdict, OK) @@ -1253,73 +1323,28 @@ def _render_html(board: Board) -> str: "

    ⚠️ This board is stale — the last tick is older than the " "freshness threshold; the numbers may not be current.

    " if board.stale else "" ) + t_ = theme() + hero = t_.hero(BOARD_KEY, "Dashboard", _LEDE) return f""" -PyAuto health — {word} - - -
    -

    PyAutoHeart Dashboard

    -

    {word} · score {board.score}

    -

    snapshot {_html.escape(board.ts)} · {age} · markdown version

    - {stale_html} - {reasons_html} - {''.join(rows)}
    -
    Rendered by heart/dashboard.py — one renderer, many surfaces. - Observer only: PyAutoHeart never writes outside its own repo/state. - 📋 buttons copy a Claude prompt or command to your clipboard.
    -

    Boards: {_boards_nav_html()}

    -
    +PyAutoHeart Dashboard — {word} + + + +{hero} +

    {word} · score + {board.score}snapshot {_html.escape(board.ts)} · + {age} · markdown version

    +{stale_html} +{reasons_html} +{''.join(rows)}
    +{_boards_nav_html()} +
    Rendered by heart/dashboard.py — one renderer, many +surfaces. Observer only: PyAutoHeart never writes outside its own repo/state. +\U0001f4cb buttons copy a Claude prompt or command to your clipboard.
    + + """ diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index e143aca..fcff740 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -466,7 +466,7 @@ def test_html_carries_copy_buttons_and_run_links(): v = make_verdict("red", 45, red_reasons=["autolens_workspace: Smoke Tests failure on main"]) out = dashboard.render(_failing_snapshot(), v, fmt="html", now=FRESH_NOW) - assert "data-copy=" in out and "cp(this)" in out + assert "data-cmd=" in out # the shared copy handler's payload hook assert "/bug Heart board: autolens_workspace" in out assert RUN_URL in out # the failing repo group row links the run too @@ -800,7 +800,7 @@ def test_performance_no_run_rows_are_capped_at_ten(): def test_html_carries_the_event_prompt_in_a_data_copy_attribute(): out = dashboard.render(_perf_snapshot(), make_verdict(), fmt="html", now=FRESH_NOW) - assert f'data-copy="{_html_escape(EVENT_PROMPT)}"' in out + assert f'data-cmd="{_html_escape(EVENT_PROMPT)}"' in out assert EVENT_URL in out @@ -827,3 +827,16 @@ def test_malformed_performance_slices_never_break_the_board(): for fmt in ("term", "md", "html", "json"): assert isinstance(dashboard.render(snap, make_verdict(), fmt=fmt, now=FRESH_NOW), str) + + +def test_html_wears_the_shared_family_theme(): + # The look is the Brain's `board/_theme.py`, not a stylesheet copied in + # here: the page must carry this board's hero (mark, wordmark, tagline) + # and its accent, or it has silently fallen out of the family. + t = dashboard.theme() + out = dashboard.render(_failing_snapshot(), make_verdict("red", 45), + fmt="html", now=FRESH_NOW) + assert t.MARKS[dashboard.BOARD_KEY] in out + assert t.ORGANS[dashboard.BOARD_KEY]["tagline"] in out + assert t.ORGANS[dashboard.BOARD_KEY]["ink_dark"] in out + assert "#58a6ff" not in out # the old hard-coded GitHub blue From 43c0162dfc534dc291d1bc2d8a14ec16b57a9b1e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:34:58 +0000 Subject: [PATCH 2/2] Resolve the Brain checkout without naming the org MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tenant firewall caught it (PyAutoHeart#169 CI): the theme resolver's last candidate was a literal `~/Code/PyAutoLabs/PyAutoBrain`, and the org's own directory name is an instance fact — organ code may not carry one, whatever the local convention happens to be. The fallback reads `$PYAUTO_ROOT` instead (defaulting to `~/Code`), the same declared surface the dev-flow doors use. `$PYAUTO_BRAIN` and the two checkout paths are unchanged, so CI and a sibling workspace both resolve exactly as before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FhsBsESQUAoJQ2fvLiDD5j --- heart/dashboard.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/heart/dashboard.py b/heart/dashboard.py index 8cd5135..87ef3ac 100644 --- a/heart/dashboard.py +++ b/heart/dashboard.py @@ -157,6 +157,16 @@ def _library_names() -> tuple: BOARD_KEY = "heart" # this board's entry in the Brain's palette table +def _workspace_root() -> pathlib.Path: + """Where the sibling PyAuto checkouts live: `$PYAUTO_ROOT`, else `~/Code`. + + The org's own directory name is an instance fact, so it is never written + here — a workspace that does not follow the default sets `$PYAUTO_ROOT` + (the same variable the dev-flow doors read). + """ + return pathlib.Path(os.environ.get("PYAUTO_ROOT") or pathlib.Path.home() / "Code") + + def theme(): """The shared theme module, or a RuntimeError naming the fix. @@ -165,7 +175,7 @@ def theme(): """ for cand in (os.environ.get("PYAUTO_BRAIN"), HEART_HOME / "PyAutoBrain", HEART_HOME.parent / "PyAutoBrain", - pathlib.Path.home() / "Code" / "PyAutoLabs" / "PyAutoBrain"): + _workspace_root() / "PyAutoBrain"): if not cand: continue board_dir = pathlib.Path(cand) / "board"