Skip to content

Commit 910403f

Browse files
committed
The Heart board joins the family look
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhsBsESQUAoJQ2fvLiDD5j
1 parent 17ad76f commit 910403f

4 files changed

Lines changed: 123 additions & 70 deletions

File tree

.github/workflows/heart-health.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ jobs:
6565
- name: Checkout PyAutoHeart
6666
uses: actions/checkout@v4
6767

68+
# The shared board look the dashboard renders with — imported from the
69+
# Brain, never copied here.
70+
- name: Checkout PyAutoBrain (the shared board theme)
71+
uses: actions/checkout@v4
72+
with:
73+
repository: PyAutoLabs/PyAutoBrain
74+
path: PyAutoBrain
75+
6876
- name: Set up Python
6977
uses: actions/setup-python@v5
7078
with:

.github/workflows/heart-tests.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ jobs:
5353
with:
5454
repository: PyAutoLabs/PyAutoMind
5555
path: PyAutoMind
56+
# The shared board look the dashboard renders with — imported from the
57+
# Brain, never copied here.
58+
- name: Checkout PyAutoBrain (the shared board theme)
59+
uses: actions/checkout@v4
60+
with:
61+
repository: PyAutoLabs/PyAutoBrain
62+
path: PyAutoBrain
5663
- name: Set up Python ${{ matrix.python-version }}
5764
uses: actions/setup-python@v5
5865
with:

heart/dashboard.py

Lines changed: 93 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import datetime
4444
import html as _html
4545
import json
46+
import os
47+
import pathlib
4648
import sys
4749
from dataclasses import dataclass, field
4850
from typing import Any, Iterable, Sequence
@@ -145,6 +147,37 @@ def _library_names() -> tuple:
145147
# that links "the webpage" agrees on the URL.
146148
PAGES_URL = "https://pyautolabs.github.io/PyAutoHeart/"
147149

150+
# The family look lives once, in the Brain (``board/_theme.py``): the
151+
# stylesheet, the hero that redraws this organ's logo as a mark, and the
152+
# cross-board footer. Imported rather than copied, so the look moves for the
153+
# whole family at once — heart-health.yml checks PyAutoBrain out beside this
154+
# repo, and a local run finds the sibling checkout the way the other PyAuto
155+
# tools resolve each other.
156+
HEART_HOME = pathlib.Path(__file__).resolve().parents[1]
157+
BOARD_KEY = "heart" # this board's entry in the Brain's palette table
158+
159+
160+
def theme():
161+
"""The shared theme module, or a RuntimeError naming the fix.
162+
163+
Only the html surface needs it; the md/json/badge surfaces never call
164+
here, so the Health Agent keeps working with no PyAutoBrain in reach.
165+
"""
166+
for cand in (os.environ.get("PYAUTO_BRAIN"), HEART_HOME / "PyAutoBrain",
167+
HEART_HOME.parent / "PyAutoBrain",
168+
pathlib.Path.home() / "Code" / "PyAutoLabs" / "PyAutoBrain"):
169+
if not cand:
170+
continue
171+
board_dir = pathlib.Path(cand) / "board"
172+
if (board_dir / "_theme.py").is_file():
173+
if str(board_dir) not in sys.path:
174+
sys.path.insert(0, str(board_dir))
175+
import _theme
176+
return _theme
177+
raise RuntimeError(
178+
"the shared board theme (PyAutoBrain/board/_theme.py) is not in reach "
179+
"— check PyAutoBrain out beside this repo or set PYAUTO_BRAIN")
180+
148181
# The one-tap board family — the cross-board footer nav every board carries,
149182
# each board skipping its own entry. The base comes from PAGES_URL so the
150183
# owner is named exactly once in this file.
@@ -154,9 +187,11 @@ def _library_names() -> tuple:
154187

155188

156189
def _boards_nav_html() -> str:
190+
"""The cross-board footer — one chip per sibling, each in its own organ's
191+
colour (the theme owns the chip palette; this board owns the URLs)."""
157192
base = PAGES_URL.rsplit("/", 2)[0]
158-
return " · ".join(f'<a href="{base}/{repo}/">{name}</a>'
159-
for name, repo in BOARD_FAMILY)
193+
links = {key: f"{base}/{repo}/" for key, repo in BOARD_FAMILY}
194+
return theme().boards_footer(links, BOARD_KEY)
160195

161196
# v2: sections gained links/action/observed_ago; the board gained structured
162197
# `blockers` ({text, severity, repo, repo_url, run_url, prompt}). Additive.
@@ -1195,8 +1230,7 @@ def _copy_btn(payload: str, label: str = "copy") -> str:
11951230
and the payload — a Claude prompt or a command — is ready to paste."""
11961231
return (f"<button class='copy' type='button' "
11971232
f"title='{_html.escape(label, quote=True)}' "
1198-
f"data-copy=\"{_html.escape(payload, quote=True)}\" "
1199-
f"onclick='cp(this)'>📋</button>")
1233+
f"data-cmd=\"{_html.escape(payload, quote=True)}\">📋</button>")
12001234

12011235

12021236
def _html_reason(item: dict) -> str:
@@ -1214,6 +1248,42 @@ def _html_reason(item: dict) -> str:
12141248
return f"<li>{text}</li>"
12151249

12161250

1251+
# The Heart's verdict in the theme's tone vocabulary. The board's own
1252+
# `_VERDICT_STATE` stays the internal truth; this is only how it is painted.
1253+
_VERDICT_TONE = {"red": "bad", "yellow": "warn", "stale": "warn",
1254+
"green": "ok"}
1255+
1256+
_LEDE = ("Is it safe to release? Every check the Heart observes, with the "
1257+
"evidence behind each verdict. \U0001f4cb copies a ready-to-paste prompt "
1258+
"or command for a Claude Code chat.")
1259+
1260+
# The page-specific shapes the shared sheet has no opinion on: the per-row
1261+
# state dot, the evidence list, the stale banner. Written against the theme's
1262+
# variables, so this board follows the family accent rather than setting a
1263+
# second palette.
1264+
_EXTRA_CSS = """
1265+
table.board td.dot{width:1.15rem;padding-right:.35rem}
1266+
table.board td.dot::before{content:"";display:inline-block;width:10px;
1267+
height:10px;border-radius:50%;margin-top:.35rem;background:var(--muted)}
1268+
table.board tr.ok td.dot::before{background:var(--ok)}
1269+
table.board tr.warn td.dot::before{background:var(--warn)}
1270+
table.board tr.fail td.dot::before{background:var(--bad)}
1271+
table.board tr.info td.dot::before{background:var(--accent)}
1272+
table.board td.name{font-weight:600;white-space:nowrap}
1273+
table.board tr.unobs td.name,table.board tr.unobs td.sum{color:var(--muted)}
1274+
ul.det{margin:.35rem 0 0;padding-left:1.1rem;color:var(--muted);
1275+
font-size:.85rem}
1276+
.ago{color:var(--muted)}
1277+
a.out{font-size:.85rem;white-space:nowrap}
1278+
.stale{background:var(--btn);border:1px solid var(--warn);color:var(--warn);
1279+
padding:.55rem .75rem;border-radius:8px}
1280+
.reasons{margin:1.5rem 0}
1281+
.reasons li{margin:.3rem 0}
1282+
.hint{color:var(--muted);font-size:.85em;margin:.5rem 0 0}
1283+
footer{margin-top:2rem;color:var(--muted);font-size:.82em}
1284+
"""
1285+
1286+
12171287
def _render_html(board: Board) -> str:
12181288
word = _VERDICT_WORD.get(board.verdict, "GREEN")
12191289
vstate = _VERDICT_STATE.get(board.verdict, OK)
@@ -1253,73 +1323,28 @@ def _render_html(board: Board) -> str:
12531323
"<p class='stale'>⚠️ This board is stale — the last tick is older than the "
12541324
"freshness threshold; the numbers may not be current.</p>" if board.stale else ""
12551325
)
1326+
t_ = theme()
1327+
hero = t_.hero(BOARD_KEY, "Dashboard", _LEDE)
12561328
return f"""<!doctype html>
12571329
<html lang="en"><head><meta charset="utf-8">
12581330
<meta name="viewport" content="width=device-width, initial-scale=1">
1259-
<title>PyAuto health — {word}</title>
1260-
<style>
1261-
:root {{ color-scheme: light dark; }}
1262-
* {{ box-sizing: border-box; }}
1263-
body {{ font: 15px/1.5 -apple-system, Segoe UI, Roboto, sans-serif;
1264-
margin: 0; padding: 2rem 1rem; background: #0d1117; color: #c9d1d9; }}
1265-
.wrap {{ max-width: 760px; margin: 0 auto; }}
1266-
h1 {{ font-size: 1.3rem; margin: 0 0 .25rem; }}
1267-
.verdict {{ display: inline-block; padding: .3rem .9rem; border-radius: 999px;
1268-
font-weight: 700; letter-spacing: .04em; }}
1269-
.verdict.ok {{ background: #1a7f37; color: #fff; }}
1270-
.verdict.warn {{ background: #9e6a03; color: #fff; }}
1271-
.verdict.fail {{ background: #b62324; color: #fff; }}
1272-
.meta {{ color: #8b949e; margin: .5rem 0 1.25rem; font-size: .9rem; }}
1273-
.stale {{ background: #341a00; color: #e3b341; padding: .5rem .75rem;
1274-
border-radius: 6px; }}
1275-
table {{ width: 100%; border-collapse: collapse; }}
1276-
td {{ padding: .55rem .5rem; border-top: 1px solid #21262d; vertical-align: top; }}
1277-
td.dot {{ width: 10px; }}
1278-
td.dot::before {{ content: ""; display: inline-block; width: 10px; height: 10px;
1279-
border-radius: 50%; margin-top: .35rem; }}
1280-
tr.ok td.dot::before {{ background: #3fb950; }}
1281-
tr.warn td.dot::before {{ background: #d29922; }}
1282-
tr.fail td.dot::before {{ background: #f85149; }}
1283-
tr.unobs td.dot::before {{ background: #6e7681; }}
1284-
tr.info td.dot::before {{ background: #58a6ff; }}
1285-
td.name {{ font-weight: 600; white-space: nowrap; }}
1286-
tr.unobs td.name, tr.unobs td.sum {{ color: #8b949e; }}
1287-
ul.det {{ margin: .35rem 0 0; padding-left: 1.1rem; color: #8b949e;
1288-
font-size: .85rem; }}
1289-
.reasons {{ margin: 1.5rem 0; }}
1290-
.reasons h2 {{ font-size: 1rem; }}
1291-
.reasons li {{ margin: .25rem 0; }}
1292-
a {{ color: #58a6ff; text-decoration: none; }}
1293-
a:hover {{ text-decoration: underline; }}
1294-
a.out {{ font-size: .85rem; white-space: nowrap; }}
1295-
.ago {{ color: #8b949e; font-size: .85rem; }}
1296-
.hint {{ color: #8b949e; font-size: .8rem; margin: .5rem 0 0; }}
1297-
button.copy {{ background: #21262d; border: 1px solid #30363d; border-radius: 6px;
1298-
color: #c9d1d9; cursor: pointer; padding: .05rem .45rem;
1299-
margin-left: .35rem; font-size: .85rem; line-height: 1.4; }}
1300-
button.copy:hover {{ background: #30363d; }}
1301-
footer {{ margin-top: 2rem; color: #8b949e; font-size: .8rem; }}
1302-
</style>
1303-
<script>
1304-
function cp(b){{var t=b.getAttribute('data-copy');
1305-
if(navigator.clipboard&&navigator.clipboard.writeText){{
1306-
navigator.clipboard.writeText(t).then(function(){{ok(b)}},function(){{fb(t)}});
1307-
}}else{{fb(t)}}}}
1308-
function ok(b){{b.textContent='✓';setTimeout(function(){{b.textContent='📋'}},1200)}}
1309-
function fb(t){{window.prompt('Copy this:',t)}}
1310-
</script></head>
1311-
<body><div class="wrap">
1312-
<h1>PyAutoHeart Dashboard</h1>
1313-
<p><span class="verdict {vstate}">{word} · score {board.score}</span></p>
1314-
<p class="meta">snapshot {_html.escape(board.ts)} · {age} · <a href="dashboard.md">markdown version</a></p>
1315-
{stale_html}
1316-
{reasons_html}
1317-
<table>{''.join(rows)}</table>
1318-
<footer>Rendered by <code>heart/dashboard.py</code> — one renderer, many surfaces.
1319-
Observer only: PyAutoHeart never writes outside its own repo/state.
1320-
📋 buttons copy a Claude prompt or command to your clipboard.</footer>
1321-
<p class="meta">Boards: {_boards_nav_html()}</p>
1322-
</div></body></html>
1331+
<title>PyAutoHeart Dashboard — {word}</title>
1332+
<style>{t_.css(BOARD_KEY)}{_EXTRA_CSS}</style>
1333+
</head>
1334+
<body>
1335+
{hero}
1336+
<p class="verdict {_VERDICT_TONE.get(board.verdict, '')}"><b>{word} · score
1337+
{board.score}</b><span class="muted">snapshot {_html.escape(board.ts)} ·
1338+
{age} · <a href="dashboard.md">markdown version</a></span></p>
1339+
{stale_html}
1340+
{reasons_html}
1341+
<table class="recent board">{''.join(rows)}</table>
1342+
{_boards_nav_html()}
1343+
<footer>Rendered by <code>heart/dashboard.py</code> — one renderer, many
1344+
surfaces. Observer only: PyAutoHeart never writes outside its own repo/state.
1345+
\U0001f4cb buttons copy a Claude prompt or command to your clipboard.</footer>
1346+
<script>{t_.JS}</script>
1347+
</body></html>
13231348
"""
13241349

13251350

tests/test_dashboard.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ def test_html_carries_copy_buttons_and_run_links():
466466
v = make_verdict("red", 45,
467467
red_reasons=["autolens_workspace: Smoke Tests failure on main"])
468468
out = dashboard.render(_failing_snapshot(), v, fmt="html", now=FRESH_NOW)
469-
assert "data-copy=" in out and "cp(this)" in out
469+
assert "data-cmd=" in out # the shared copy handler's payload hook
470470
assert "/bug Heart board: autolens_workspace" in out
471471
assert RUN_URL in out
472472
# the failing repo group row links the run too
@@ -800,7 +800,7 @@ def test_performance_no_run_rows_are_capped_at_ten():
800800

801801
def test_html_carries_the_event_prompt_in_a_data_copy_attribute():
802802
out = dashboard.render(_perf_snapshot(), make_verdict(), fmt="html", now=FRESH_NOW)
803-
assert f'data-copy="{_html_escape(EVENT_PROMPT)}"' in out
803+
assert f'data-cmd="{_html_escape(EVENT_PROMPT)}"' in out
804804
assert EVENT_URL in out
805805

806806

@@ -827,3 +827,16 @@ def test_malformed_performance_slices_never_break_the_board():
827827
for fmt in ("term", "md", "html", "json"):
828828
assert isinstance(dashboard.render(snap, make_verdict(), fmt=fmt,
829829
now=FRESH_NOW), str)
830+
831+
832+
def test_html_wears_the_shared_family_theme():
833+
# The look is the Brain's `board/_theme.py`, not a stylesheet copied in
834+
# here: the page must carry this board's hero (mark, wordmark, tagline)
835+
# and its accent, or it has silently fallen out of the family.
836+
t = dashboard.theme()
837+
out = dashboard.render(_failing_snapshot(), make_verdict("red", 45),
838+
fmt="html", now=FRESH_NOW)
839+
assert t.MARKS[dashboard.BOARD_KEY] in out
840+
assert t.ORGANS[dashboard.BOARD_KEY]["tagline"] in out
841+
assert t.ORGANS[dashboard.BOARD_KEY]["ink_dark"] in out
842+
assert "#58a6ff" not in out # the old hard-coded GitHub blue

0 commit comments

Comments
 (0)