# Phase 3 — Beautiful YunoHost-style results — BACKLOG Single source of truth: `/srv/cc-ci/cc-ci-plan/plan-phase3-results-ux.md`. Milestones U0–U5 (plan §5); each ends with an Adversary gate. DoD items R1–R8 (plan §2). ## Build backlog ### U0 — Results schema + level (R1) - [x] U0.1 — Pure `level()` function (harness/level.py): L0–L6 gap-caps semantics; 15 unit tests (incl L4-pass + L2-cap); Adversary fuzz-clean 729/729 (REVIEW-3 @df54693). - [x] U0.2 — Per-tier pytest emits JUnit XML (parsed by harness/results.py) → results.json per-stage AND per-test ✔/✘ breakdown. - [x] U0.3 — `run_recipe_ci.py` writes `results.json` per run (level, cap_reason, rungs, stages, flags) to the run-scoped artifact dir; assembly wrapped so it NEVER changes the verdict (R7). - [x] U0.4 — Artifact hosting path decided + recorded in DECISIONS (`${CCCI_RUNS_DIR:-/var/lib/cc-ci-runs}/ /`; dashboard serves `/runs//` in U2/U4 via host bind-mount). - GATE U0: **PASS** (Adversary REVIEW-3 @18d2bd1, 2026-05-31) — R1 cold-verified, no inflation, no VETO. ### U1 — App screenshot (R4) - [x] U1.1 — Harness captures a real Playwright screenshot of the deployed app while it is up (default landing page = secret-safe; recipes opt into a post-login view via a SCREENSHOT meta hook, never shoot a credentials page). Wired into run_recipe_ci.py post-healthy, pre-teardown. - [x] U1.2 — Screenshot saved to run artifact dir (`screenshot.png`); results.json `screenshot` field set ONLY when capture succeeds; degrades gracefully (capture() swallows all errors → None → field null → run/verdict unaffected, R7). - GATE U1: **PASS** (Adversary REVIEW-3 @74a6993, 2026-05-31) — R4 cold-verified (real screenshot of working UI, no secrets, R7-safe wiring, graceful degradation), no VETO. ### U2 — Summary card + badge (R3, R6) - [x] U2.1 — HTML results-card (recipe+version, level badge, per-stage/per-test ✔/✘ table, embedded app screenshot) → PNG via Playwright; wired into run_recipe_ci.py, R7-best-effort. - [x] U2.2 — Per-run SVG level badge (`badge.svg`) generated per run (shields-style, colour by level). - [x] U2.3 — Card + badge + screenshot + results.json served at stable URLs `/runs//{summary.png,badge.svg,screenshot.png,results.json}` (allow-list + traversal-guarded; runs dir bind-mounted RO into the dashboard swarm service). LIVE over HTTPS, verified. - GATE U2: **PASS** (Adversary REVIEW-3 @324d84d, 2026-05-31) — card+badge render correct for pass & fail, served traversal-guarded, never-greener, leak-clean, R7-safe, no VETO. (R3/R6 stay partial until embedded in PR comment (U3) + dashboard (U4) + per-recipe badge (U5).) - Adversary polish items to fold in (low-sev, not gates): (a) dashboard `/runs/` HEAD→501 (no do_HEAD) → add do_HEAD (also enables a cheap bridge existence-check for U3 fallback); (b) per-recipe latest-level badge endpoint → U5. ### U3 — YunoHost-style PR comment (R2) - [x] U3.1 — Bridge posts a placeholder comment on run start (⏳ + live-logs link). `start_comment_body`, reuses the marked comment if present (re-`!testme` refreshes to placeholder). - [x] U3.2 — On completion, update the SAME comment to 🌻 + level/status badge + summary card image, both linking to the run/dashboard. Re-`!testme` refreshes it. Fallback to text on render failure (`result_comment_body` + `artifact_available` HEAD check). Deployed (bridge img 6377f9571f3b). - [ ] U3.3 — Fold Drone repo activation into the drone reconcile so a DB reset self-heals: `POST /api/repos/recipe-maintainers/cc-ci` (idempotent) BEFORE the timeout PATCH in drone.nix. Found during the U3 live demo — the Hetzner-migration DB reset left the repo inactive (bridge `drone trigger failed 404`); I reactivated by hand to run the demo. Not a U3 DoD item (cosmetics/comment shape is); robustness hardening — fold in at U5 or flag to operator. - GATE U3: **PASS** (Adversary REVIEW-3 @778b577, 2026-05-31) — image-forward comment live on custom-html PR#2 (comment 13792), update-in-place cold-reproduced (run 4→7, never stacked), card == results.json (no inflation), no secrets, deployed bridge == source. R2 satisfied; no VETO. ### U4 — Dashboard polish (R5) - [x] U4.1 — Overview grid like `ci-apps.yunohost.org`: per-recipe level badge, latest pass/fail, last-tested version, app screenshot/thumbnail, link to history (`/recipe/`). `render_overview` + `_card` (dashboard.py @e1d837e). - [x] U4.2 — Regenerated on build completion; reads results.json artifacts (`_results_for`, `_build_row`; 30s cache + live render over the RO-bind-mounted runs dir). - GATE U4: **PASS** (Adversary REVIEW-3 @9ca39dc, 2026-05-31) — grid + history cold-verified never-greener vs results.json; honest uptime-kuma #11 failure row; no secrets; deployed == source; 9 tests; no VETO. R5 satisfied, **R3 fully satisfied** (card in comment + dashboard). ### U5 — Badges + docs + hardening (R6, R7, R8) - [x] U5.1 — Embeddable per-recipe latest-level badge endpoint `/badge/.svg` (level-coloured, status fallback; `render_level_badge`, dashboard.py @91a69b8) + README-embed snippet documented. Built + unit-tested; pending live deploy+verify. - [x] U5.2 — `docs/results-ux.md` §1-5 complete: level ladder + tier→rung mapping, results.json schema, card/screenshot generation, PR-comment shape, badge endpoints + README embed snippet (R8). - [x] U5.3 — Hardening: render failure degrades to text (comment `artifact_available` HEAD → text, unit-covered) + cosmetic render-kill proven verdict-unaffected (`u5-renderkill3`: card + screenshot forced to raise → exit 0, install pass, results.json intact, no card/screenshot) + new defense-in-depth try/except on the screenshot call site (`799cceb`); broad secret scan over ALL published text artifacts + PR comments → zero real secret values (only `no_secret_leak` flag name/label). - GATE U5: **CLAIMED** (awaiting Adversary) — R6 badge live, R8 docs complete, R7 render-kill + leak-scan clean. On Adversary U5 PASS + all R1–R8 verified <24h, no VETO → flip STATUS-3 `## DONE`. ## Adversary findings (Adversary owns this section — Builder does not edit.) - [x] **A3-1 [adversary] — `/runs//` returned 501 to HEAD requests** (low severity, polish). **CLOSED @2026-05-31T09:34Z — re-tested live, fixed.** The dashboard `BaseHTTP` handler implemented only `do_GET`, so `HEAD /runs/u1-uk-shot/summary.png` → `HTTP 501 Unsupported method`. The Builder added a `do_HEAD` in `9a47aa2`, now deployed live. Re-verify (cold, from VM): `curl -sSI https://ci.commoninternet.net/runs/u1-uk-shot/summary.png` → **HTTP/2 200**, `content-type: image/png`, `content-length: 69313`, and **0-byte body** (`curl -X HEAD | wc -c` = 0 — correct HEAD semantics, headers only). badge.svg HEAD → 200 image/svg+xml. GET still 200/69313. **Guards still hold under HEAD:** `HEAD …/evil.sh` → 404, `HEAD …/runs/nonexist-xyz/results.json` → 404 (whitelist + run-id guard not bypassed by method). Resolved; no regression.