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..87ef3ac 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,47 @@ 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 _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. + + 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", + _workspace_root() / "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 +197,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 +1240,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 +1258,42 @@ def _html_reason(item: dict) -> str: return f"
⚠️ 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""" -{word} · score {board.score}
- - {stale_html} - {reasons_html} -{word} · score + {board.score}snapshot {_html.escape(board.ts)} · + {age} · markdown version
+{stale_html} +{reasons_html} +