diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/gen-bug-report.py | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/scripts/gen-bug-report.py b/scripts/gen-bug-report.py new file mode 100644 index 0000000..f995bfc --- /dev/null +++ b/scripts/gen-bug-report.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Generate a deterministic BUG-REPORT.html from data/reports/*.json. + +Every field is HTML-escaped; the page is self-contained (no external assets). +Output is stable for a given set of reports — no wall-clock timestamps. +""" +import glob +import html +import json +import os +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +REPORTS_DIR = os.path.join(ROOT, "data", "reports") +OUT_FILE = os.path.join(ROOT, "downloads", "bugs", "BUG-REPORT.html") +MAX_REPORTS = 200 + + +def esc(value): + return html.escape("" if value is None else str(value), quote=True) + + +def load_reports(): + rows = [] + for path in sorted(glob.glob(os.path.join(REPORTS_DIR, "RPT-*.json"))): + try: + with open(path, encoding="utf-8") as fh: + rec = json.load(fh) + if isinstance(rec, dict) and rec.get("id"): + rows.append(rec) + except (OSError, ValueError): + continue + rows.sort(key=lambda r: (str(r.get("created_at", "")), str(r.get("id", ""))), reverse=True) + return rows[:MAX_REPORTS] + + +def pretty(value, limit=6000): + try: + text = json.dumps(value, indent=2, ensure_ascii=False) + except (TypeError, ValueError): + text = str(value) + if len(text) > limit: + text = text[:limit] + "\n… (truncated)" + return esc(text) + + +def render(rows): + total = len(rows) + latest = esc(rows[0].get("created_at", "—")) if rows else "—" + cards = [] + + for rec in rows: + diag = rec.get("diagnostics") if isinstance(rec.get("diagnostics"), dict) else {} + errors = diag.get("errors") if isinstance(diag.get("errors"), list) else [] + cards.append( + f""" <article class="card"> + <header> + <h3>{esc(rec.get('title', '(untitled)'))}</h3> + <span class="pill">{esc(rec.get('id', ''))}</span> + </header> + <p class="meta">{esc(rec.get('created_at', ''))} · version {esc(diag.get('version', 'unknown'))} · {len(errors)} error(s)</p> + {f'<p class="body">{esc(rec.get("body"))}</p>' if rec.get('body') else ''} + <details> + <summary>Diagnostics</summary> + <pre>{pretty(diag)}</pre> + </details> + </article>""" + ) + + if not cards: + cards.append(' <p class="empty">No reports yet. Run <code>bug <title></code> in newOS4.</p>') + + return f"""<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>newOS4 — Bug Reports</title> +<style> + :root {{ --bg:#0b0f19; --panel:#141a29; --line:#243049; --text:#e6ebf5; --muted:#93a1bb; --accent:#6366f1; }} + * {{ box-sizing:border-box; }} + body {{ margin:0; padding:32px 20px; background:radial-gradient(circle at 20% -10%,#1b2540 0,var(--bg) 55%); color:var(--text); + font:15px/1.55 Inter,system-ui,-apple-system,"Segoe UI",sans-serif; }} + main {{ max-width:920px; margin:0 auto; }} + h1 {{ font-size:1.6rem; margin:0 0 6px; }} + .sub {{ color:var(--muted); margin:0 0 26px; }} + .stats {{ display:flex; gap:12px; flex-wrap:wrap; margin-bottom:24px; }} + .stat {{ background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:12px 18px; min-width:130px; }} + .stat b {{ display:block; font-size:1.4rem; color:var(--accent); }} + .stat span {{ color:var(--muted); font-size:.82rem; }} + .card {{ background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:16px 18px; margin-bottom:14px; }} + .card header {{ display:flex; gap:10px; align-items:center; justify-content:space-between; }} + .card h3 {{ margin:0; font-size:1.02rem; }} + .pill {{ font-size:.72rem; color:var(--accent); border:1px solid var(--accent); border-radius:999px; padding:2px 9px; white-space:nowrap; }} + .meta {{ color:var(--muted); font-size:.82rem; margin:6px 0 0; }} + .body {{ margin:10px 0 0; }} + details {{ margin-top:12px; }} + summary {{ cursor:pointer; color:var(--muted); }} + pre {{ background:#0d1322; border:1px solid var(--line); border-radius:10px; padding:12px; overflow:auto; max-height:420px; + font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; }} + .empty {{ color:var(--muted); }} + code {{ background:#0d1322; border:1px solid var(--line); border-radius:6px; padding:1px 5px; }} +</style> +</head> +<body> +<main> + <h1>🐞 newOS4 Bug Reports</h1> + <p class="sub">Generated from <code>data/reports/*.json</code> — no manual edits.</p> + <section class="stats"> + <div class="stat"><b>{total}</b><span>reports</span></div> + <div class="stat"><b>{esc(latest)}</b><span>latest report</span></div> + </section> +{chr(10).join(cards)} +</main> +</body> +</html> +""" + + +def main(): + rows = load_reports() + page = render(rows) + os.makedirs(os.path.dirname(OUT_FILE), exist_ok=True) + with open(OUT_FILE, "w", encoding="utf-8") as fh: + fh.write(page) + os.chmod(OUT_FILE, 0o664) + print(f"wrote {OUT_FILE} ({len(rows)} report(s), {len(page)} bytes)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |