history_for() now enumerates run dirs' results.json, groups by recipe, sorts newest-first by finished timestamp (mixed numeric+named ids — timestamp is the only correct key), caps at HISTORY_CAP=30, skips malformed/empty/no-recipe dirs. Overview + badges + /runs + security guards + stdlib-only unchanged. Local verify: 13/13 unit tests; full-fixture vs 308 real results.json → bluesky-pds=8 in exact ts order, plausible capped 30 newest, edge dirs skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.0 KiB
STATUS — phase dash (per-recipe run history fix)
SSOT: /srv/cc-ci/cc-ci-plan/plan-phase-dash-recipe-history.md Gates: M1 (fix implemented + locally verified) · M2 (deployed + verified live)
Gate: M1 CLAIMED, awaiting Adversary
WHAT — history_for(recipe) in dashboard/dashboard.py now sources the FULL per-recipe run
history from the local run artifacts under /var/lib/cc-ci-runs (each run dir's results.json),
newest-first by the finished timestamp, display-capped at HISTORY_CAP (default 30). It no longer
reads the Drone …/builds?per_page=100 slice (the root cause: that window dropped a recipe's older
runs out of view, so most recipes showed 1 run). Overview (/), /badge/<recipe>.svg,
/runs/<id>/<file>, security guards, and stdlib-only constraint are unchanged.
WHERE —
- Commit: see
git logon origin/main for theclaim(M1)commit (this push). - Changed files:
dashboard/dashboard.py(new_run_status,_numeric_id,_local_history_row,_local_history; rewrittenhistory_for; newHISTORY_CAP; new_LOCALcache), andtests/unit/test_dashboard.py(newtest_history_sourced_from_local_artifacts). - Host artifacts the page reads:
/var/lib/cc-ci-runs/<id>/results.json(bind-mounted read-only into the dashboard container, unchanged from before).
HOW to verify (cold, from a fresh clone) —
- Unit suite (stdlib render + new local-sourcing test):
EXPECTED:
nix-shell -p 'python3.withPackages(ps:[ps.pytest])' --run \ 'DRONE_TOKEN_FILE=$(mktemp) python3 -m pytest tests/unit/test_dashboard.py -q'13 passed. - Verify against the REAL host artifacts. Build a fixture of every
results.jsonand runhistory_foragainst it (no Drone, no network):EXPECTED:FIX=/tmp/advfix; rm -rf $FIX; mkdir -p $FIX ssh cc-ci 'cd /var/lib/cc-ci-runs && tar -cf - */results.json 2>/dev/null' | tar -xf - -C $FIX printf x > /tmp/t.tok DRONE_TOKEN_FILE=/tmp/t.tok CCCI_RUNS_DIR=$FIX python3 -c ' import sys; sys.path.insert(0,"dashboard"); import dashboard as d r=d.history_for("bluesky-pds") print("count", len(r), [x["number"] for x in r]) print("total parseable", sum(len(v) for v in d._local_history().values())) print("plausible cap", len(d.history_for("plausible")))'bluesky-pdscount 8, order EXACTLY['753','556','435','427','423','ab-bluesky-pds-oldmain','m2rr-bluesky-pds','m2r-bluesky-pds'](newest-first byfinished; note 423 sorts BELOW 427 though id 423<427, and named ids land in their timestamp positions — the mixed numeric+named id trap).- total parseable grouped rows 308 (matches host: 432 dirs, 308 with parseable
results.json). plausiblecapped at 30 (of 33), newest kept.
EXPECTED — invariants the Adversary's break-tests should confirm hold
- The 124 run dirs with no/malformed
results.jsonare skipped (no 500, no garbage row):_results_forreturns{}on miss/malformed/non-dir,_local_historyskips any row with norecipe. - Security preserved (untouched code paths):
/recipe/<name>still gated by_RUN_ID_RE(^[A-Za-z0-9][A-Za-z0-9._-]*$→ rejects../..,foo/.., spaces,;);_results_for/serve_run_filestill realpath-guarded against escaping/var/lib/cc-ci-runs. - stdlib-only: no new imports (still
html,json,os,re,sys,time,urllib,http.server). - Overview (
/) and/badge/<recipe>.svgstill sourced from Drone latest-per-recipe (_custom_recipe_builds/latest_per_recipeunchanged) — only the history page changed source. - Run-link resolution: numeric id →
{DRONE_URL}/{CI_REPO}/<id>; named id (m2r-*,ab-*) →/runs/<id>/summary.html(local, since no Drone build number exists). - Status pill derived from the per-stage
resultsmap (results.jsonhas no top-level status): anyfail/error→ failure; allpass/skip→ success; else unknown.
Gate: M2 — NOT STARTED (deploy + live verify; begins after M1 PASS)
Blocked
(none)