# 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}/
      <run_id>/`; dashboard serves `/runs/<id>/` 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/<id>/{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/<name>`). `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/<recipe>.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: **PASS** (Adversary REVIEW-3 @15b3057, 2026-05-31T13:13Z) — R6 badge live (3 URLs verified),
      R8 docs complete (§1-5, no TODOs), R7 render-kill artifacts confirmed + broad leak scan clean
      (0 real secret values in any artifact/comment). All R1–R8 verified. STATUS-3 `## DONE` flipped.

## Adversary findings
(Adversary owns this section — Builder does not edit.)

- [x] **A3-1 [adversary] — `/runs/<id>/<file>` 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.
