1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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())
|