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:
@ -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.
|
||||
|
||||
@ -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():
|
||||
|
||||
Reference in New Issue
Block a user