feat(3 U5.1+U5.2): per-recipe latest-level badge endpoint /badge/<recipe>.svg (R6, level-coloured, status fallback) + complete docs/results-ux.md §3-5 (card/screenshot/PR-comment/badge-embedding, R8); +2 badge unit tests
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
autonomic-bot
2026-05-31 10:04:14 +00:00
parent 9ca39dc179
commit 91a69b8971
3 changed files with 100 additions and 22 deletions

View File

@ -326,16 +326,31 @@ def render_history(recipe, rows):
return _page(f"{recipe} — cc-ci history", inner)
def render_badge(recipe, status):
color = _COLORS.get(status, "#8b949e")
label, msg = "cc-ci", status
lw, mw = 44, max(40, 7 * len(msg) + 10)
def _badge_svg(label, msg, color):
"""Two-box shields-style SVG (grey label | coloured message). Stdlib-only, deterministic sizing."""
lw = max(44, 7 * len(label) + 12)
mw = max(40, 7 * len(msg) + 12)
w = lw + mw
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="20" role="img">
<rect width="{lw}" height="20" fill="#555"/><rect x="{lw}" width="{mw}" height="20" fill="{color}"/>
<g fill="#fff" font-family="Verdana,sans-serif" font-size="11">
<text x="6" y="14">{html.escape(label)}</text>
<text x="{lw + 6}" y="14">{html.escape(msg)}</text></g></svg>"""
return (
f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="20" role="img" '
f'aria-label="{html.escape(label)}: {html.escape(msg)}">'
f'<rect width="{lw}" height="20" fill="#555"/>'
f'<rect x="{lw}" width="{mw}" height="20" fill="{color}"/>'
f'<g fill="#fff" font-family="Verdana,Geneva,sans-serif" font-size="11">'
f'<text x="6" y="14">{html.escape(label)}</text>'
f'<text x="{lw + 6}" y="14">{html.escape(msg)}</text></g></svg>'
)
def render_badge(recipe, status):
"""Status fallback badge (used when a recipe has no results.json level yet)."""
return _badge_svg("cc-ci", status, _COLORS.get(status, "#8b949e"))
def render_level_badge(recipe, level):
"""Per-recipe latest-LEVEL badge (R6): 'cc-ci: <recipe> | level N', coloured by level —
embeddable in a recipe README (`/badge/<recipe>.svg`) and shown on the dashboard."""
return _badge_svg(f"cc-ci: {recipe}", f"level {int(level)}", level_color(level))
def serve_run_file(run_id, fname):
@ -363,8 +378,11 @@ class Handler(BaseHTTPRequestHandler):
if path.startswith("/badge/") and path.endswith(".svg"):
recipe = path[len("/badge/") : -len(".svg")]
row = next((r for r in recipes_cached() if r["recipe"] == recipe), None)
status = row["status"] if row else "unknown"
return 200, render_badge(recipe, status), "image/svg+xml"
# R6: per-recipe LATEST-LEVEL badge (from results.json). Fall back to a status badge when
# the recipe has no level yet (never ran / failed before emitting results.json).
if row and row.get("level") is not None:
return 200, render_level_badge(recipe, row["level"]), "image/svg+xml"
return 200, render_badge(recipe, row["status"] if row else "unknown"), "image/svg+xml"
if path.startswith("/runs/"):
# /runs/<run_id>/<file> — stable URL for a run's results.json / summary.png / screenshot /
# badge (R3/R6). Whitelisted + traversal-guarded by serve_run_file.