Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ version skew, script and test timings, workspace validation) and rolls what it
sees into one authoritative verdict: **GREEN / STALE / YELLOW / RED**. GREEN
means it is safe to release.

See the live **[health board](https://pyautolabs.github.io/PyAutoHeart/)** for
the whole picture on one page β€” every check a traffic-light row, and every red
or yellow finding carrying links to the failing run and a one-tap πŸ“‹ button
that copies a ready-made Claude prompt (`/bug …`), so going from "something is
red" to "an agent is fixing it" is copy β†’ paste, on a laptop or a phone.
See the **[PyAutoHeart Dashboard](https://pyautolabs.github.io/PyAutoHeart/)**
(mobile phone dashboard) for the whole picture on one page β€” every check a
traffic-light row, and every red or yellow finding carrying links to the
failing run and a one-tap πŸ“‹ button that copies a ready-made Claude prompt
(`/bug …`), so going from "something is red" to "an agent is fixing it" is
copy β†’ paste, on a laptop or a phone.

## How PyAutoHeart works

Expand Down
36 changes: 22 additions & 14 deletions heart/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,11 +365,17 @@ def _repo_section(
continue
state, label = _lib_row(name, body, unobserved=unobserved)
rows.append((state, name, label))
# The way OUT of a red row: the failing run itself.
# The way OUT of a red row: the failing run itself, plus a ready-made
# /bug prompt (rendered as the link's paired πŸ“‹ on the html surface).
ci = body.get("ci_status") or {}
if (state == FAIL and ci.get("url")
and str(ci.get("conclusion") or "") not in ("", "success")):
links.append({"label": f"{name} run", "url": str(ci["url"])})
wf = ci.get("workflow") or "CI"
links.append({
"label": f"{name} run", "url": str(ci["url"]),
"prompt": (f"/bug Heart board: {name} {wf} failing on main β€” "
f"failing run: {ci['url']}"),
})
if not rows:
return None
overall = _worst(s for s, _, _ in rows)
Expand Down Expand Up @@ -943,22 +949,21 @@ def _render_md(board: Board) -> str:


def _render_md_brief(board: Board) -> str:
"""The README strip: verdict + linked blockers + the board link. The full
table lives on the Pages board; the README stays a glance, not a wall."""
"""The README strip: one glance, not a wall. A single verdict line with
the board link inline; blockers/warnings appear only when there ARE any
(stale evidence gaps and timestamps live on the board, not the README β€”
the strip earns its README lines only when something needs a human)."""
word = _VERDICT_WORD.get(board.verdict, "GREEN")
emoji = _STATE_MD[_VERDICT_STATE.get(board.verdict, OK)]
age = format_age(board.age_seconds, stale=board.stale)
lines = [
f"## {emoji} PyAuto health β€” **{word}** (score {board.score})",
"",
f"_snapshot `{board.ts}` Β· {age}_",
"",
f"## {emoji} PyAuto health β€” **{word}** (score {board.score}) Β· "
f"[full board β†’]({PAGES_URL})"
]
label, items = _shown_reasons(board)
if items:
lines.append(f"**{label}:** " + "; ".join(_md_reason(i) for i in items[:4]))
lines.append("")
lines.append(f"**[Full board β†’]({PAGES_URL})** β€” live page with one-tap πŸ“‹ fix prompts")
if board.verdict in ("red", "yellow"):
label, items = _shown_reasons(board)
if items:
lines += ["", f"**{label}:** "
+ "; ".join(_md_reason(i) for i in items[:4])]
return "\n".join(lines)


Expand Down Expand Up @@ -1003,6 +1008,9 @@ def _render_html(board: Board) -> str:
for link in sec.links:
summary += (f" <a class='out' href=\"{_html.escape(str(link.get('url', '')), quote=True)}\">"
f"{_html.escape(str(link.get('label', 'link')))} β†—</a>")
if link.get("prompt"):
summary += " " + _copy_btn(str(link["prompt"]),
"copy the fix prompt for a Claude Code chat")
if sec.action and sec.action.get("payload"):
summary += " " + _copy_btn(str(sec.action["payload"]),
str(sec.action.get("label", "copy")))
Expand Down
27 changes: 27 additions & 0 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,33 @@ def test_md_brief_is_a_strip_not_a_table():
assert "[autolens_workspace](" in out
assert dashboard.PAGES_URL in out
assert "| Check |" not in out # no table β€” the Pages board carries it
assert "snapshot" not in out # no timestamp clutter β€” the board has it


def test_md_brief_is_one_line_unless_something_is_wrong():
# GREEN and STALE both collapse to the single verdict+link line: evidence
# gaps are the board's business, not the README's.
green = dashboard.render(make_snapshot(), make_verdict(), fmt="md-brief",
now=FRESH_NOW)
assert "\n" not in green and dashboard.PAGES_URL in green
stale_v = {"verdict": "stale", "score": 65,
"stale_reasons": ["install verification not run"], "ts": TS}
stale = dashboard.render(make_snapshot(), stale_v, fmt="md-brief", now=FRESH_NOW)
assert "\n" not in stale
assert "install verification" not in stale


def test_failing_repo_row_link_carries_its_own_prompt():
v = make_verdict("red", 45,
red_reasons=["autolens_workspace: Smoke Tests failure on main"])
board = dashboard.build_board(_failing_snapshot(), v, now=FRESH_NOW)
ws = {s.key: s for s in board.sections}["workspaces"]
(link,) = ws.links
assert link["prompt"].startswith("/bug Heart board: autolens_workspace Smoke Tests")
assert RUN_URL in link["prompt"]
html = dashboard.render(_failing_snapshot(), v, fmt="html", now=FRESH_NOW)
# the row-level πŸ“‹ renders beside the run link, not only in the blockers
assert html.count("/bug Heart board: autolens_workspace") >= 2


def test_unobserved_rows_carry_watch_line_and_observe_action():
Expand Down
Loading