diff --git a/.github/workflows/release_board.yml b/.github/workflows/release_board.yml index 392fd3b..cb5c287 100644 --- a/.github/workflows/release_board.yml +++ b/.github/workflows/release_board.yml @@ -41,6 +41,13 @@ jobs: steps: - uses: actions/checkout@v4 + # The family look (hero, palette, stylesheet) is defined once in the + # Brain and imported by the renderer — never copied in here. + - uses: actions/checkout@v4 + with: + repository: PyAutoLabs/PyAutoBrain + path: PyAutoBrain + - uses: actions/setup-python@v5 with: python-version: "3.12" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2919508..dfa99eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -91,6 +91,13 @@ jobs: with: repository: PyAutoLabs/PyAutoMind path: PyAutoMind + # The shared board look the dashboard renders with — imported from the + # Brain, never copied here, so the board tests exercise the real thing. + - 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/autohands/board.py b/autohands/board.py index 6c97512..849b394 100644 --- a/autohands/board.py +++ b/autohands/board.py @@ -35,6 +35,7 @@ import datetime import html as _html import json +import os import re import subprocess import sys @@ -43,6 +44,46 @@ from pathlib import Path HANDS_HOME = Path(__file__).resolve().parents[1] + +# 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 — release_board.yml checks PyAutoBrain out beside this +# repo, and a local run finds the sibling checkout the way the other PyAuto +# tools resolve each other. +BOARD_KEY = "hands" # this board's entry in the Brain's palette table + + +def _workspace_root() -> 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 Path(os.environ.get("PYAUTO_ROOT") or Path.home() / "Code") + + +def theme(): + """The shared theme module, or a RuntimeError naming the fix. + + Only the html path needs it; ``--md``/``--badge``/``--json`` never call + here, so the digest keeps working with no PyAutoBrain in reach. + """ + for cand in (os.environ.get("PYAUTO_BRAIN"), HANDS_HOME / "PyAutoBrain", + HANDS_HOME.parent / "PyAutoBrain", + _workspace_root() / "PyAutoBrain"): + if not cand: + continue + board = Path(cand) / "board" + if (board / "_theme.py").is_file(): + if str(board) not in sys.path: + sys.path.insert(0, str(board)) + 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") CONFIG_PATH = Path(__file__).resolve().parent / "config" / "workspaces.yaml" SCHEMA_VERSION = 1 @@ -275,11 +316,14 @@ def _heart_board_url(snapshot: dict) -> str: def _boards_nav(snapshot: dict) -> 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).""" owner = str(snapshot.get("owner") or "").lower() if not owner: return "" - return " · ".join(f'{name}' - for name, repo in BOARD_FAMILY) + links = {key: f"https://{owner}.github.io/{repo}/" + for key, repo in BOARD_FAMILY} + return theme().boards_footer(links, BOARD_KEY) def _latest(snapshot: dict) -> dict | None: @@ -358,16 +402,45 @@ def _render_md_brief(snapshot: dict) -> str: def _copy_btn(payload: str, label: str = "copy") -> str: + """A one-tap payload chip. The behaviour is the family's shared script + (``_theme.JS``): a delegated click handler reading ``data-cmd``.""" return (f"") + f"data-cmd=\"{_html.escape(payload, quote=True)}\">\U0001f4cb") _PYPI_CLS = {"live": "ok", "yanked": "fail", "missing": "warn", "unavailable": "unobs"} +# The lede, and the page-specific shapes the shared sheet has no opinion on: +# the status dot each row carries and the version chips. Written against the +# theme's variables, so this board follows the family accent rather than +# setting a second palette. +_LEDE = ("What the Hands shipped — a record of execution, newest first. Tap " + "\U0001f4cb to put a command on your clipboard for a Claude Code chat.") + +_EXTRA_CSS = """ +.chip{display:inline-block;background:var(--btn);border:1px solid var(--line); + border-radius:999px;padding:.15rem .6rem;margin:.15rem .2rem 0 0; + font-size:.85rem;white-space:nowrap} +table.recent td.dot{width:10px;padding-right:0} +table.recent td.dot::before{content:"";display:inline-block;width:10px; + height:10px;border-radius:50%;margin-top:.35rem;background:var(--muted)} +tr.ok td.dot::before{background:var(--ok)} +tr.warn td.dot::before{background:var(--warn)} +tr.fail td.dot::before{background:var(--bad)} +table.recent td.name{font-weight:600;white-space:nowrap} +.ndot{display:inline-block;width:12px;height:12px;border-radius:50%; + margin-right:.25rem;background:var(--muted)} +.ndot.ok{background:var(--ok)} +.ndot.fail{background:var(--bad)} +.errors{margin-top:1.2rem} +footer{margin-top:2rem;color:var(--muted);font-size:.82em} +""" + + def _render_html(snapshot: dict) -> str: + t_ = theme() latest = _latest(snapshot) head = (f"{_html.escape(latest['version'])}" if latest else "unavailable") head_age = _age(latest.get("date")) if latest else "" @@ -422,74 +495,31 @@ def _render_html(snapshot: dict) -> str: errors = (f"
{head}
- -{chips}
- -{head}{head_age}
+Whether it is safe to release lives with the +PyAutoHeart Dashboard — the Hands execute, never gate. +markdown version
+{chips}
+