|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +WORKFLOWAI-PRO-WP-033 — HTML Dashboard Renderer |
| 4 | +Generates: public/workflowai-pro.html |
| 5 | +""" |
| 6 | + |
| 7 | +import json |
| 8 | +import html as htmllib |
| 9 | +from pathlib import Path |
| 10 | + |
| 11 | +HERE = Path(__file__).parent |
| 12 | +SRC = HERE / "data" / "workflowai-pro.json" |
| 13 | +OUT = HERE / "public" / "workflowai-pro.html" |
| 14 | + |
| 15 | + |
| 16 | +def esc(v): |
| 17 | + if v is None: |
| 18 | + return "" |
| 19 | + if isinstance(v, bool): |
| 20 | + return "true" if v else "false" |
| 21 | + return htmllib.escape(str(v)) |
| 22 | + |
| 23 | + |
| 24 | +def kv_table(d): |
| 25 | + rows = "".join( |
| 26 | + f"<tr><td class='k'>{esc(k)}</td><td class='v'>{render_value(v)}</td></tr>" |
| 27 | + for k, v in d.items() |
| 28 | + ) |
| 29 | + return f"<table class='kv'>{rows}</table>" |
| 30 | + |
| 31 | + |
| 32 | +def render_value(v): |
| 33 | + if isinstance(v, dict): |
| 34 | + return kv_table(v) |
| 35 | + if isinstance(v, list): |
| 36 | + if not v: |
| 37 | + return "<em>—</em>" |
| 38 | + if all(isinstance(x, (str, int, float, bool)) for x in v): |
| 39 | + return "<ul>" + "".join(f"<li>{esc(x)}</li>" for x in v) + "</ul>" |
| 40 | + if all(isinstance(x, dict) for x in v): |
| 41 | + keys = [] |
| 42 | + for d in v: |
| 43 | + for k in d.keys(): |
| 44 | + if k not in keys: |
| 45 | + keys.append(k) |
| 46 | + head = "".join(f"<th>{esc(k)}</th>" for k in keys) |
| 47 | + body = "" |
| 48 | + for d in v: |
| 49 | + body += "<tr>" + "".join( |
| 50 | + f"<td>{render_value(d.get(k, ''))}</td>" for k in keys |
| 51 | + ) + "</tr>" |
| 52 | + return f"<table class='grid'><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>" |
| 53 | + return "<ul>" + "".join(f"<li>{render_value(x)}</li>" for x in v) + "</ul>" |
| 54 | + return esc(v) |
| 55 | + |
| 56 | + |
| 57 | +def render_section(sec): |
| 58 | + sid = sec.get("id", "") |
| 59 | + title = sec.get("title", "") |
| 60 | + html = [f"<div class='section' id='{esc(sid)}'>"] |
| 61 | + html.append(f"<h3>{esc(sid)} · {esc(title)}</h3>") |
| 62 | + for key, val in sec.items(): |
| 63 | + if key in ("id", "title"): |
| 64 | + continue |
| 65 | + html.append(f"<div class='sub'><h4>{esc(key)}</h4>{render_value(val)}</div>") |
| 66 | + html.append("</div>") |
| 67 | + return "\n".join(html) |
| 68 | + |
| 69 | + |
| 70 | +def render_module(mod): |
| 71 | + mid = mod.get("id", "") |
| 72 | + title = mod.get("title", "") |
| 73 | + summary = mod.get("summary", "") |
| 74 | + sections = mod.get("sections", []) or [] |
| 75 | + html = [f"<section class='module' id='{esc(mid)}'>"] |
| 76 | + html.append(f"<h2>{esc(mid)} · {esc(title)}</h2>") |
| 77 | + if summary: |
| 78 | + html.append(f"<p class='summary'>{esc(summary)}</p>") |
| 79 | + for sec in sections: |
| 80 | + html.append(render_section(sec)) |
| 81 | + html.append("</section>") |
| 82 | + return "\n".join(html) |
| 83 | + |
| 84 | + |
| 85 | +def main(): |
| 86 | + data = json.loads(SRC.read_text(encoding="utf-8")) |
| 87 | + meta = data["meta"] |
| 88 | + exec_sum = data["executiveSummary"] |
| 89 | + |
| 90 | + modules = [ |
| 91 | + data["m1_architecture"], data["m2_strategy"], data["m3_agi"], |
| 92 | + data["m4_reports"], data["m5_prompt"], data["m6_agents"], |
| 93 | + data["m7_orchestrator"], data["m8_taxonomy"], data["m9_incident"], |
| 94 | + data["m10_backend"], data["m11_experience"], data["m12_implementation"], |
| 95 | + ] |
| 96 | + |
| 97 | + toc_items = "".join( |
| 98 | + f"<li><a href='#{esc(m['id'])}'>{esc(m['id'])} · {esc(m['title'])}</a></li>" |
| 99 | + for m in modules |
| 100 | + ) |
| 101 | + toc_items += ( |
| 102 | + "<li><a href='#opa-policies'>OPA Policies</a></li>" |
| 103 | + "<li><a href='#indices'>Indices & KPIs</a></li>" |
| 104 | + "<li><a href='#case-studies'>Case Studies</a></li>" |
| 105 | + "<li><a href='#schemas'>Schemas</a></li>" |
| 106 | + "<li><a href='#code-examples'>Code Examples</a></li>" |
| 107 | + "<li><a href='#api'>API Endpoints</a></li>" |
| 108 | + ) |
| 109 | + |
| 110 | + modules_html = "\n".join(render_module(m) for m in modules) |
| 111 | + |
| 112 | + opa_rows = "".join( |
| 113 | + f"<tr><td>{esc(p['id'])}</td><td>{esc(p['name'])}</td><td>{esc(p['enforce'])}</td></tr>" |
| 114 | + for p in data["opaPolicies"] |
| 115 | + ) |
| 116 | + |
| 117 | + idx_rows = "".join( |
| 118 | + f"<tr><td>{esc(i['id'])}</td><td>{esc(i['name'])}</td><td>{esc(i['range'])}</td><td>{esc(i['target'])}</td></tr>" |
| 119 | + for i in data["indices"] |
| 120 | + ) |
| 121 | + |
| 122 | + cs_html = "" |
| 123 | + for cs in data["caseStudies"]: |
| 124 | + cs_html += ( |
| 125 | + f"<div class='case'><h3>{esc(cs['id'])} · {esc(cs['title'])}</h3>" |
| 126 | + f"<p><strong>Sector:</strong> {esc(cs['sector'])}</p>" |
| 127 | + f"<p>{esc(cs['summary'])}</p>" |
| 128 | + f"<div class='sub'><h4>Outcomes</h4>{kv_table(cs['outcomes'])}</div>" |
| 129 | + "</div>" |
| 130 | + ) |
| 131 | + |
| 132 | + schemas_html = "" |
| 133 | + for name, sch in data["schemas"].items(): |
| 134 | + schemas_html += ( |
| 135 | + f"<details><summary>{esc(name)}</summary>" |
| 136 | + f"<pre><code>{esc(json.dumps(sch, indent=2))}</code></pre></details>" |
| 137 | + ) |
| 138 | + |
| 139 | + code_html = "" |
| 140 | + for name, code in data["codeExamples"].items(): |
| 141 | + code_html += ( |
| 142 | + f"<details><summary>{esc(name)}</summary>" |
| 143 | + f"<pre><code>{esc(code)}</code></pre></details>" |
| 144 | + ) |
| 145 | + |
| 146 | + api = data["apiEndpoints"] |
| 147 | + api_items = "".join( |
| 148 | + f"<li><code>{esc(api['prefix'])}{esc(r)}</code></li>" for r in api["routes"] |
| 149 | + ) |
| 150 | + |
| 151 | + page = f"""<!doctype html> |
| 152 | +<html lang="en"> |
| 153 | +<head> |
| 154 | +<meta charset="utf-8" /> |
| 155 | +<meta name="viewport" content="width=device-width,initial-scale=1" /> |
| 156 | +<title>{esc(meta['docRef'])} — {esc(meta['title'])}</title> |
| 157 | +<meta name="description" content="{esc(meta['subtitle'])}" /> |
| 158 | +<style> |
| 159 | + :root {{ |
| 160 | + --bg: #0b1020; --panel:#111a34; --fg:#e8edf7; --muted:#8aa0c2; |
| 161 | + --accent:#7cc6ff; --accent2:#b693ff; --line:#1d2a52; --ok:#58f0a7; --warn:#ffcb6b; |
| 162 | + }} |
| 163 | + * {{ box-sizing:border-box; }} |
| 164 | + body {{ margin:0; background:var(--bg); color:var(--fg); |
| 165 | + font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Inter,sans-serif; }} |
| 166 | + header.hero {{ padding:32px 36px; background:linear-gradient(135deg,#121a3a,#0b1020); |
| 167 | + border-bottom:1px solid var(--line); }} |
| 168 | + header.hero h1 {{ margin:0 0 6px; font-size:22px; letter-spacing:.2px; }} |
| 169 | + header.hero .subtitle {{ color:var(--muted); max-width:1100px; }} |
| 170 | + header.hero .badges {{ margin-top:14px; display:flex; gap:8px; flex-wrap:wrap; }} |
| 171 | + header.hero .badge {{ background:#19255a; color:var(--accent); padding:4px 10px; |
| 172 | + border-radius:999px; font-size:12px; border:1px solid var(--line); }} |
| 173 | + nav.toc {{ position:sticky; top:0; background:#0b1020ee; backdrop-filter:blur(8px); |
| 174 | + border-bottom:1px solid var(--line); padding:10px 20px; z-index:5; }} |
| 175 | + nav.toc ul {{ list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:6px; }} |
| 176 | + nav.toc li a {{ color:var(--accent); text-decoration:none; padding:4px 10px; |
| 177 | + border:1px solid var(--line); border-radius:6px; font-size:12px; }} |
| 178 | + nav.toc li a:hover {{ background:#141e44; }} |
| 179 | + main {{ padding:24px 36px 80px; max-width:1280px; }} |
| 180 | + section.module {{ background:var(--panel); border:1px solid var(--line); |
| 181 | + border-radius:12px; padding:22px 24px; margin:20px 0; }} |
| 182 | + section.module h2 {{ margin:0 0 8px; color:var(--accent2); font-size:18px; }} |
| 183 | + section.module .summary {{ color:var(--muted); margin:0 0 16px; }} |
| 184 | + .section {{ border-top:1px dashed var(--line); padding-top:14px; margin-top:14px; }} |
| 185 | + .section h3 {{ margin:0 0 8px; font-size:15px; color:var(--accent); }} |
| 186 | + .section .sub {{ margin:10px 0; }} |
| 187 | + .section .sub h4 {{ margin:0 0 6px; font-size:13px; color:var(--muted); |
| 188 | + text-transform:uppercase; letter-spacing:.5px; }} |
| 189 | + table.kv, table.grid {{ width:100%; border-collapse:collapse; font-size:13px; |
| 190 | + background:#0d1532; border:1px solid var(--line); border-radius:6px; overflow:hidden; }} |
| 191 | + table.kv td, table.grid td, table.grid th {{ padding:6px 10px; border-bottom:1px solid var(--line); |
| 192 | + vertical-align:top; text-align:left; }} |
| 193 | + table.kv td.k {{ color:var(--muted); width:30%; white-space:nowrap; }} |
| 194 | + table.grid th {{ background:#162248; color:var(--accent); font-weight:600; }} |
| 195 | + ul {{ margin:6px 0 6px 20px; padding:0; }} |
| 196 | + code {{ background:#0d1532; padding:1px 5px; border-radius:4px; color:var(--accent); }} |
| 197 | + pre {{ background:#0d1532; padding:12px; border-radius:6px; overflow:auto; |
| 198 | + border:1px solid var(--line); font-size:12px; }} |
| 199 | + details {{ background:#0d1532; border:1px solid var(--line); border-radius:6px; |
| 200 | + padding:8px 12px; margin:8px 0; }} |
| 201 | + details summary {{ cursor:pointer; color:var(--accent); }} |
| 202 | + .case {{ border-top:1px dashed var(--line); padding-top:14px; margin-top:14px; }} |
| 203 | + footer {{ color:var(--muted); border-top:1px solid var(--line); |
| 204 | + padding:16px 36px; font-size:12px; }} |
| 205 | +</style> |
| 206 | +</head> |
| 207 | +<body> |
| 208 | +<header class="hero"> |
| 209 | + <h1>{esc(meta['docRef'])} — {esc(meta['title'])}</h1> |
| 210 | + <p class="subtitle">{esc(meta['subtitle'])}</p> |
| 211 | + <div class="badges"> |
| 212 | + <span class="badge">Version {esc(meta['version'])}</span> |
| 213 | + <span class="badge">Horizon {esc(meta['horizon'])}</span> |
| 214 | + <span class="badge">{esc(meta['productName'])}</span> |
| 215 | + <span class="badge">{esc(meta['productTier'])}</span> |
| 216 | + </div> |
| 217 | +</header> |
| 218 | +<nav class="toc"><ul id="api-list">{toc_items}</ul></nav> |
| 219 | +<main> |
| 220 | + <section class="module" id="exec"> |
| 221 | + <h2>Executive Summary</h2> |
| 222 | + {kv_table(exec_sum)} |
| 223 | + </section> |
| 224 | +
|
| 225 | + <section class="module" id="meta"> |
| 226 | + <h2>Document Metadata</h2> |
| 227 | + {kv_table(meta)} |
| 228 | + </section> |
| 229 | +
|
| 230 | + {modules_html} |
| 231 | +
|
| 232 | + <section class="module" id="opa-policies"> |
| 233 | + <h2>OPA / Rego Policies</h2> |
| 234 | + <table class="grid"><thead><tr><th>ID</th><th>Name</th><th>Enforcement</th></tr></thead> |
| 235 | + <tbody>{opa_rows}</tbody></table> |
| 236 | + </section> |
| 237 | +
|
| 238 | + <section class="module" id="indices"> |
| 239 | + <h2>Governance Indices & KPIs</h2> |
| 240 | + <table class="grid"><thead><tr><th>ID</th><th>Name</th><th>Range</th><th>Target</th></tr></thead> |
| 241 | + <tbody>{idx_rows}</tbody></table> |
| 242 | + </section> |
| 243 | +
|
| 244 | + <section class="module" id="case-studies"> |
| 245 | + <h2>Case Studies</h2> |
| 246 | + {cs_html} |
| 247 | + </section> |
| 248 | +
|
| 249 | + <section class="module" id="schemas"> |
| 250 | + <h2>JSON Schemas</h2> |
| 251 | + {schemas_html} |
| 252 | + </section> |
| 253 | +
|
| 254 | + <section class="module" id="code-examples"> |
| 255 | + <h2>Code Examples</h2> |
| 256 | + {code_html} |
| 257 | + </section> |
| 258 | +
|
| 259 | + <section class="module" id="api"> |
| 260 | + <h2>API Endpoints (planned)</h2> |
| 261 | + <p class="summary">Prefix: <code>{esc(api['prefix'])}</code></p> |
| 262 | + <ul>{api_items}</ul> |
| 263 | + </section> |
| 264 | +</main> |
| 265 | +<footer> |
| 266 | + © {esc(meta['docRef'])} v{esc(meta['version'])} · {esc(meta['date'])} · |
| 267 | + {esc(meta['classification'])} |
| 268 | +</footer> |
| 269 | +</body> |
| 270 | +</html> |
| 271 | +""" |
| 272 | + OUT.write_text(page, encoding="utf-8") |
| 273 | + size_kb = OUT.stat().st_size // 1024 |
| 274 | + print(f"Wrote {OUT} ({size_kb} KB)") |
| 275 | + print(f"Modules rendered: {len(modules)} | Case studies: {len(data['caseStudies'])} | " |
| 276 | + f"OPA policies: {len(data['opaPolicies'])} | Indices: {len(data['indices'])}") |
| 277 | + |
| 278 | + |
| 279 | +if __name__ == "__main__": |
| 280 | + main() |
0 commit comments