feat(recipe-report): TESTS rename + live binary STATUS column

Rename the table's Status column -> TESTS (the CI/test verdict, unchanged
content). Add a new STATUS column showing the PR's LIVE state, fetched
client-side: 'open' vs a ✓ for any not-open state (merged or closed). The cell
is a JS hook (data-repo/data-pr) derived from existing recipe+pr fields; an
inline, dependency-free, CSP-safe script GETs the same-origin /pr/<recipe>/<n>
proxy (cc-ci nix/modules/reports.nix) on load and every 30s, and degrades to a
muted '?' if the proxy/repo is unreachable. Blank cell when a row has no PR.
Doc + SKILL updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
autonomic-bot
2026-06-09 13:15:02 +00:00
parent eb1439324e
commit f687174b53
2 changed files with 63 additions and 5 deletions

View File

@ -56,6 +56,11 @@ Helper: `python3 /srv/cc-ci/cc-ci-plan/recipe-report.py {survey|render|publish}`
`status` GREEN/FAILED/STALE/SKIPPED/UPTODATE, **`cve`** (integer count of CVEs this PR fixes; `0`
or omit for none), `ci` as a level/result number+info string e.g. `build 154 ✓` or `RED 200 ·
install` with `ci_url`, `pr`/`pr_url`, `notes`. **CI = link + number/info only; no images.**
- The table renders **two** status columns: **TESTS** (the `status` field — the CI/test verdict)
and **STATUS** (the PR's LIVE state, fetched client-side). You only supply `status`; the live
STATUS column is derived automatically from `recipe` + `pr` and needs no spec field — it shows
`open` vs a ✓ (any not-open state), refreshing ~30s via the same-origin `/pr/<recipe>/<n>` proxy
(cc-ci `nix/modules/reports.nix`); it degrades to a muted "?" if the proxy/repo is unreachable.
- `addendum[]` — list of bullet strings (may be empty),
- `security[]` — critical-CVE bulletin entries (may be empty),
- `changes[]``{recipe, body, links[]}`, one per recipe that has a PR.

View File

@ -14,10 +14,18 @@ Subcommands (the /recipe-report agent runs them around its own review/classifica
Page order: short lead → the full wire table (priority-sorted, CVEs column) → Addendum → Security
Bulletin → per-recipe "What changed".
The full-wire table has two distinct status columns:
TESTS — the CI/test verdict for the PR (GREEN/FAILED/STALE/SKIPPED/UPTODATE; from the `status` field).
STATUS — the PR's LIVE state, fetched client-side and refreshed every ~30s: `open` vs a ✓ for any
not-open state (merged OR closed). Derived from `recipe` + `pr`; the inline script GETs the
same-origin `/pr/<recipe>/<n>` proxy served by the ccci-reports nginx stack (cc-ci repo,
nix/modules/reports.nix). Needs the recipe mirrors public; degrades to a muted "?" if the
proxy/repo is unreachable. No spec-shape change — STATUS uses existing row fields.
SPEC SHAPE (the agent writes this JSON):
{"date":"YYYY-MM-DD","subtitle":"Week of <human date>",
"lead":"<ONE short paragraph>",
"table":[{"recipe":"x","change":"a → b","status":"GREEN|FAILED|STALE|SKIPPED|UPTODATE",
"table":[{"recipe":"x","change":"a → b","status":"GREEN|FAILED|STALE|SKIPPED|UPTODATE", # -> TESTS col
"cve":16, # number of CVEs this PR fixes; 0/omit for none
"ci":"build 154 ✓","ci_url":"","pr":"#4","pr_url":"","notes":""}],
# ROWS SORTED by recommended priority to address — recipes WITH CVEs first, then failures,
@ -123,6 +131,9 @@ footer{margin-top:2.6rem;border-top:4px double var(--rule);padding-top:.8rem;fon
.idx{list-style:none;padding:0}.idx li{padding:.55rem 0;border-bottom:1px solid #d8d2c2;font-size:1.1rem}
.idx .d{color:var(--mut);font-size:.85rem;float:right}
td .cve{color:var(--red);font-weight:800}.muted{color:#999}
.pr-status{white-space:nowrap}
.pr-open{display:inline-block;font:700 .66rem/1 Georgia,serif;text-transform:uppercase;letter-spacing:.07em;color:#9b6b00;background:#fff3d6;border:1px solid #e3c98a;border-radius:.2rem;padding:.13rem .42rem}
.pr-done{color:#1a7a3c;font-weight:800;font-size:1.05rem}
.addendum-h{font-size:1rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin:1.7rem 0 .3rem;color:var(--ink)}
ul.addendum{margin:.2rem 0 1.2rem 1.2rem;padding:0}ul.addendum li{margin:.35rem 0;line-height:1.55}
.change{margin:.8rem 0;padding-bottom:.7rem;border-bottom:1px solid #d8d2c2}
@ -130,6 +141,35 @@ ul.addendum{margin:.2rem 0 1.2rem 1.2rem;padding:0}ul.addendum li{margin:.35rem
"""
# Inline (CSP-safe, dependency-free) script that lights up the live PR-STATUS column. For each
# `.pr-status[data-pr]` cell it GETs the same-origin /pr/<recipe>/<n> proxy (see reports.nix in the
# cc-ci repo) and renders BINARY state: `state==='open'` -> an "open" badge; any other state
# (closed/merged) -> a green ✓ (the title shows merged vs closed). It refreshes every 30s so an open
# tab tracks state in realtime, and is fully resilient — a network/parse error leaves a muted "?" and
# never overwrites a value already resolved, so the page renders fully even if the proxy is down.
STATUS_JS = """
(function(){
function refresh(){
document.querySelectorAll('.pr-status[data-pr]').forEach(function(cell){
var repo=cell.getAttribute('data-repo'), pr=cell.getAttribute('data-pr');
if(!repo||!pr) return;
fetch('/pr/'+encodeURIComponent(repo)+'/'+encodeURIComponent(pr),{headers:{'Accept':'application/json'}})
.then(function(r){ if(!r.ok) throw 0; return r.json(); })
.then(function(d){
var st=d&&d.state;
if(st==='open'){ cell.innerHTML='<span class="pr-open">open</span>'; cell.dataset.ok='1'; }
else if(st){ cell.innerHTML='<span class="pr-done" title="'+(d.merged?'merged':'closed')+'">\\u2713</span>'; cell.dataset.ok='1'; }
else throw 0;
})
.catch(function(){ if(!cell.dataset.ok){ cell.innerHTML='<span class="muted" title="live status unavailable">?</span>'; } });
});
}
if(document.readyState!=='loading'){ refresh(); } else { document.addEventListener('DOMContentLoaded', refresh); }
setInterval(refresh, 30000);
})();
"""
def _esc(s):
return html.escape(str(s or ""))
@ -164,8 +204,10 @@ def _stories(items, repo_url=None):
def _table(rows, repo_url=None):
if not rows:
return ""
head = ("<tr><th>Recipe</th><th>Change</th><th>Status</th><th>CVEs</th>"
"<th>CI</th><th>PR</th><th>Notes</th></tr>")
# TESTS = the CI/test verdict (GREEN/FAILED/STALE/…); STATUS = the PR's LIVE state, fetched
# client-side (open vs ✓ for any not-open state) by the inline script in _page().
head = ("<tr><th>Recipe</th><th>Change</th><th>TESTS</th><th>CVEs</th>"
"<th>CI</th><th>PR</th><th>STATUS</th><th>Notes</th></tr>")
trs = []
for r in rows:
scls = "s-" + str(r.get("status", "")).upper().replace("-", "").replace(" ", "")
@ -181,9 +223,18 @@ def _table(rows, repo_url=None):
pr = _esc(r.get("pr"))
if r.get("pr_url"):
pr = f'<a href="{_esc(r["pr_url"])}">{pr}</a>'
# Live PR-STATUS cell: a JS hook carrying the repo + PR number derived from the existing
# `recipe` + `pr` fields. The inline script fills it from /pr/<recipe>/<n>. Blank when no PR.
recipe = r.get("recipe") or ""
m = re.search(r"\d+", str(r.get("pr") or ""))
if m and recipe:
status_cell = (f'<td class="pr-status" data-repo="{_esc(recipe)}" '
f'data-pr="{_esc(m.group(0))}"><span class="muted">…</span></td>')
else:
status_cell = "<td></td>"
trs.append(f"<tr><td>{name}</td><td>{_esc(r.get('change'))}</td>"
f'<td class="{scls}">{_esc(r.get("status"))}</td><td>{cve_cell}</td>'
f"<td>{ci}</td><td>{pr}</td><td>{_esc(r.get('notes'))}</td></tr>")
f"<td>{ci}</td><td>{pr}</td>{status_cell}<td>{_esc(r.get('notes'))}</td></tr>")
return f"<table>{head}{''.join(trs)}</table>"
@ -204,10 +255,12 @@ def _changes(items, repo_url=None):
def _page(title, body):
# The live-status script ships on every page (reports + archive index); harmless where there are
# no `.pr-status` cells. Self-contained: inline CSS + inline JS, no external requests.
return (f'<!doctype html><html lang="en"><head><meta charset="utf-8">'
f'<meta name="viewport" content="width=device-width,initial-scale=1">'
f"<title>{_esc(title)}</title><style>{CSS}</style></head>"
f'<body><div class="paper">{body}</div></body></html>')
f'<body><div class="paper">{body}</div><script>{STATUS_JS}</script></body></html>')
def _mast():