| """HTML / CSS / JS blob for the 4-panel live dashboard embedded in the Gradio Space. |
| |
| Kept out of ``app.py`` so the Python file stays focused on the Gradio wiring. |
| The panels are driven from four hidden ``gr.JSON`` components (one per panel) |
| whose values mutate as Python generators yield. JavaScript polls the JSON |
| state in the DOM via a ``MutationObserver`` and re-renders each panel from the |
| delta. |
| |
| Every selector here is namespaced under ``#gf-dashboard`` so the paste of |
| ``dashboard/style.css`` cannot clobber Gradio's own styling. |
| """ |
| from __future__ import annotations |
|
|
|
|
| DASHBOARD_HTML = r""" |
| <style> |
| #gf-dashboard { |
| --gf-bg: #07090c; |
| --gf-panel: #0d1117; |
| --gf-panel-2: #060a10; |
| --gf-border: #1c232e; |
| --gf-text: #d8dee9; |
| --gf-muted: #6b7280; |
| --gf-accent: #f5b400; |
| --gf-safe: #28a76588; |
| --gf-warn: #e6a23c88; |
| --gf-danger: #ef4444cc; |
| --gf-safe-solid: #28a765; |
| --gf-warn-solid: #e6a23c; |
| --gf-danger-solid: #ef4444; |
| background: var(--gf-bg); |
| color: var(--gf-text); |
| padding: 14px; |
| border-radius: 12px; |
| margin-top: 18px; |
| font-family: -apple-system, "SF Pro Text", BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; |
| } |
| #gf-dashboard * { box-sizing: border-box; } |
| #gf-dashboard .gf-head { |
| display: flex; flex-wrap: wrap; gap: 16px; |
| align-items: baseline; justify-content: space-between; |
| padding-bottom: 10px; margin-bottom: 12px; |
| border-bottom: 1px solid var(--gf-border); |
| } |
| #gf-dashboard .gf-brand { display: flex; align-items: baseline; gap: 12px; } |
| #gf-dashboard .gf-brand h2 { margin: 0; font-size: 16px; letter-spacing: -0.01em; color: var(--gf-text); } |
| #gf-dashboard .gf-brand .gf-pill { |
| font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; |
| color: var(--gf-accent); border: 1px solid var(--gf-accent); |
| padding: 2px 7px; border-radius: 999px; |
| } |
| #gf-dashboard .gf-tagline { color: var(--gf-muted); font-size: 11.5px; max-width: 64ch; margin: 0; } |
| #gf-dashboard .gf-status { display: flex; gap: 12px; font-size: 11px; color: var(--gf-muted); } |
| #gf-dashboard .gf-status .gf-dot { |
| display: inline-block; width: 8px; height: 8px; border-radius: 50%; |
| background: var(--gf-muted); margin-right: 6px; vertical-align: middle; |
| } |
| #gf-dashboard .gf-status .gf-dot.ok { background: var(--gf-safe-solid); box-shadow: 0 0 6px var(--gf-safe-solid); } |
| #gf-dashboard .gf-status .gf-dot.err { background: var(--gf-danger-solid); } |
| |
| #gf-dashboard .gf-grid { |
| display: grid; |
| grid-template-columns: 1.3fr 1fr; |
| gap: 14px; |
| } |
| @media (max-width: 1100px) { |
| #gf-dashboard .gf-grid { grid-template-columns: 1fr; } |
| } |
| #gf-dashboard .panel { |
| background: var(--gf-panel); |
| border: 1px solid var(--gf-border); |
| border-radius: 10px; |
| padding: 12px 14px; |
| display: flex; flex-direction: column; |
| min-height: 280px; |
| overflow: hidden; |
| } |
| #gf-dashboard .panel h3 { |
| margin: 0 0 10px; font-size: 11px; |
| text-transform: uppercase; letter-spacing: 0.16em; |
| color: var(--gf-muted); font-weight: 600; |
| display: flex; align-items: center; justify-content: space-between; |
| } |
| #gf-dashboard .panel h3 .sub { |
| color: var(--gf-muted); font-weight: 400; letter-spacing: 0; |
| text-transform: none; font-size: 10.5px; |
| } |
| |
| /* Panel 1: live token stream */ |
| #gf-dashboard #panel-stream #stream-output { |
| background: var(--gf-panel-2); border: 1px solid var(--gf-border); |
| border-radius: 6px; padding: 10px 12px; |
| font-family: ui-monospace, "SF Mono", Menlo, monospace; |
| font-size: 12.5px; line-height: 1.65; |
| white-space: pre-wrap; |
| flex: 1; min-height: 120px; max-height: 220px; overflow-y: auto; |
| } |
| #gf-dashboard #panel-stream #stream-output.empty { |
| display: flex; align-items: center; justify-content: center; |
| color: var(--gf-muted); |
| font-family: -apple-system, system-ui, sans-serif; font-size: 12px; |
| } |
| #gf-dashboard #panel-stream .tok { |
| padding: 1px 1px 2px; border-radius: 2px; |
| transition: background-color 0.08s ease-in; |
| } |
| #gf-dashboard .meter-wrap { margin-top: 8px; } |
| #gf-dashboard .meter { |
| height: 8px; border-radius: 999px; |
| background: linear-gradient(to right, #28a765 0%, #28a765 40%, #e6a23c 40%, #e6a23c 70%, #ef4444 70%, #ef4444 100%); |
| position: relative; box-shadow: inset 0 1px 2px rgba(0,0,0,0.4); |
| } |
| #gf-dashboard .meter .needle { |
| position: absolute; top: -3px; width: 3px; height: 14px; |
| background: #fff; border-radius: 2px; |
| transform: translateX(-1.5px); |
| transition: left 0.1s linear; |
| box-shadow: 0 0 6px rgba(255,255,255,0.5); |
| } |
| #gf-dashboard .stats { |
| margin-top: 8px; |
| display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; |
| } |
| #gf-dashboard .stats .stat { |
| background: var(--gf-panel-2); border: 1px solid var(--gf-border); |
| border-radius: 5px; padding: 6px 8px; |
| } |
| #gf-dashboard .stats .stat .v { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; } |
| #gf-dashboard .stats .stat .v.cwe { font-size: 12px; font-family: ui-monospace, monospace; color: var(--gf-accent); } |
| #gf-dashboard .stats .stat .l { font-size: 9.5px; text-transform: uppercase; color: var(--gf-muted); letter-spacing: 0.08em; margin-top: 1px; } |
| #gf-dashboard .stats .stat.maxpool .v { color: var(--gf-accent); } |
| |
| /* Panel 2: scan progress */ |
| #gf-dashboard .scan-counters { |
| display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; |
| margin-bottom: 10px; |
| } |
| #gf-dashboard .scan-counters .c { |
| background: var(--gf-panel-2); border: 1px solid var(--gf-border); |
| border-radius: 5px; padding: 6px 8px; |
| } |
| #gf-dashboard .scan-counters .c .v { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; } |
| #gf-dashboard .scan-counters .c .l { font-size: 9.5px; text-transform: uppercase; color: var(--gf-muted); letter-spacing: 0.08em; margin-top: 1px; } |
| #gf-dashboard #scan-feed { |
| flex: 1; overflow-y: auto; |
| background: var(--gf-panel-2); border: 1px solid var(--gf-border); |
| border-radius: 6px; padding: 8px; |
| font-family: ui-monospace, "SF Mono", Menlo, monospace; |
| font-size: 11.5px; |
| min-height: 0; max-height: 280px; |
| } |
| #gf-dashboard .file-row { |
| margin-bottom: 8px; padding: 6px 8px; border-radius: 4px; |
| background: rgba(255,255,255,0.015); |
| } |
| #gf-dashboard .file-row .name { |
| display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; |
| } |
| #gf-dashboard .file-row .name .p { color: var(--gf-text); word-break: break-all; } |
| #gf-dashboard .file-row .name .n { color: var(--gf-muted); font-size: 10.5px; white-space: nowrap; } |
| #gf-dashboard .file-row .strip { |
| display: flex; flex-wrap: wrap; gap: 2px; min-height: 8px; |
| } |
| #gf-dashboard .file-row .strip .dot { |
| width: 7px; height: 7px; border-radius: 2px; |
| background: var(--gf-safe); |
| } |
| #gf-dashboard .file-row .strip .dot.warn { background: var(--gf-warn); } |
| #gf-dashboard .file-row .strip .dot.danger { background: var(--gf-danger); } |
| #gf-dashboard .file-row .strip .dot[title] { cursor: help; } |
| #gf-dashboard .scan-empty { |
| color: var(--gf-muted); text-align: center; padding: 20px 0; |
| font-family: -apple-system, system-ui, sans-serif; font-size: 12px; |
| } |
| |
| /* Panel 3: leads table */ |
| #gf-dashboard #leads-table { |
| flex: 1; overflow-y: auto; |
| background: var(--gf-panel-2); border: 1px solid var(--gf-border); |
| border-radius: 6px; |
| min-height: 0; max-height: 280px; |
| } |
| #gf-dashboard table.leads { |
| width: 100%; border-collapse: collapse; font-size: 11.5px; |
| } |
| #gf-dashboard table.leads thead th { |
| position: sticky; top: 0; |
| background: var(--gf-panel-2); |
| color: var(--gf-muted); |
| font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; |
| font-size: 10px; text-align: left; |
| padding: 8px 8px; border-bottom: 1px solid var(--gf-border); |
| } |
| #gf-dashboard table.leads tbody tr { |
| border-bottom: 1px solid var(--gf-border); |
| } |
| #gf-dashboard table.leads tbody tr:hover { background: rgba(255,255,255,0.02); } |
| #gf-dashboard table.leads tbody td { padding: 7px 8px; vertical-align: top; } |
| #gf-dashboard table.leads tbody td.rank { color: var(--gf-muted); font-variant-numeric: tabular-nums; } |
| #gf-dashboard table.leads tbody td.conf .gf-confpill { |
| display: inline-block; padding: 2px 6px; border-radius: 4px; |
| font-size: 10.5px; font-weight: 700; |
| } |
| #gf-dashboard table.leads tbody td.conf .gf-confpill.danger { background: var(--gf-danger); color: #fff; } |
| #gf-dashboard table.leads tbody td.conf .gf-confpill.warn { background: var(--gf-warn); color: #1a1a1a; } |
| #gf-dashboard table.leads tbody td.conf .gf-confpill.safe { background: var(--gf-safe); color: #fff; } |
| #gf-dashboard table.leads tbody td.cwe { color: var(--gf-accent); font-family: ui-monospace, monospace; font-size: 11px; } |
| #gf-dashboard table.leads tbody td.file { |
| color: var(--gf-text); font-family: ui-monospace, monospace; font-size: 11px; |
| word-break: break-all; |
| } |
| #gf-dashboard table.leads tbody td.preview { |
| color: var(--gf-muted); font-family: ui-monospace, monospace; font-size: 10.5px; |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; |
| } |
| |
| /* Panel 4: pwnkit hand-off */ |
| #gf-dashboard .pwnkit { flex: 1; display: flex; flex-direction: column; gap: 10px; min-height: 0; } |
| #gf-dashboard .pwnkit-pills { |
| display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; |
| } |
| #gf-dashboard .pwnkit-pill { |
| background: var(--gf-panel-2); border: 1px solid var(--gf-border); |
| border-radius: 6px; padding: 8px 10px; text-align: center; |
| } |
| #gf-dashboard .pwnkit-pill .v { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; } |
| #gf-dashboard .pwnkit-pill.queued .v { color: var(--gf-accent); } |
| #gf-dashboard .pwnkit-pill.exploit .v { color: var(--gf-warn-solid); } |
| #gf-dashboard .pwnkit-pill.report .v { color: var(--gf-safe-solid); } |
| #gf-dashboard .pwnkit-pill .l { |
| font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; |
| color: var(--gf-muted); margin-top: 3px; |
| } |
| #gf-dashboard .pwnkit-log { |
| flex: 1; background: var(--gf-panel-2); border: 1px solid var(--gf-border); |
| border-radius: 6px; padding: 8px 10px; |
| font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.55; |
| overflow-y: auto; color: var(--gf-muted); |
| min-height: 0; max-height: 240px; |
| } |
| #gf-dashboard .pwnkit-log .line { padding: 2px 0; } |
| #gf-dashboard .pwnkit-log .line.ok { color: var(--gf-safe-solid); } |
| #gf-dashboard .pwnkit-log .line.warn { color: var(--gf-warn-solid); } |
| #gf-dashboard .pwnkit-log .line.danger { color: var(--gf-danger-solid); } |
| #gf-dashboard .pwnkit-log .line .ts { color: var(--gf-muted); margin-right: 8px; } |
| #gf-dashboard .pwnkit-note { font-size: 10.5px; color: var(--gf-muted); font-style: italic; } |
| </style> |
| |
| <div id="gf-dashboard"> |
| <div class="gf-head"> |
| <div class="gf-brand"> |
| <h2>Live pipeline</h2> |
| <span class="gf-pill">4 panels · real-time</span> |
| </div> |
| <p class="gf-tagline"> |
| Streaming tokens with probe risk · repo scan progress · ranked leads · |
| hand-off to pwnkit. Every panel is backed by real data from this Space. |
| </p> |
| <div class="gf-status"> |
| <span><span class="gf-dot" id="dot-stream"></span><span id="lbl-stream">probe stream idle</span></span> |
| <span><span class="gf-dot" id="dot-events"></span><span id="lbl-events">scan idle</span></span> |
| <span><span class="gf-dot" id="dot-pwnkit"></span><span id="lbl-pwnkit">pwnkit idle</span></span> |
| </div> |
| </div> |
| |
| <div class="gf-grid"> |
| <!-- Panel 1 --> |
| <section class="panel" id="panel-stream"> |
| <h3>Live token stream <span class="sub">Gemma 4 · linear probe layer 17</span></h3> |
| <div id="stream-output" class="empty"> |
| Type a prompt above and click <b>Generate with probe</b>. |
| Tokens stream here, colored by per-token probe risk |
| (<span style="color:#5fb96b">safe</span> · |
| <span style="color:#e6a23c">elevated</span> · |
| <span style="color:#ef4444">high</span>). |
| </div> |
| <div class="meter-wrap"> |
| <div class="meter"><div class="needle" id="meter-needle" style="left: 0%"></div></div> |
| </div> |
| <div class="stats"> |
| <div class="stat"><div class="v" id="stat-tokens">0</div><div class="l">tokens</div></div> |
| <div class="stat"><div class="v" id="stat-peak">0%</div><div class="l">peak</div></div> |
| <div class="stat"><div class="v" id="stat-avg">0%</div><div class="l">mean</div></div> |
| <div class="stat maxpool"><div class="v" id="stat-maxpool">0%</div><div class="l">max-pool</div></div> |
| <div class="stat"><div class="v cwe" id="stat-top-cwe">—</div><div class="l">top cwe</div></div> |
| </div> |
| </section> |
| |
| <!-- Panel 2 --> |
| <section class="panel" id="panel-scan"> |
| <h3>Repo scan <span class="sub">gemmaforge.events/v1</span></h3> |
| <div class="scan-counters"> |
| <div class="c"><div class="v" id="scan-files">0</div><div class="l">files</div></div> |
| <div class="c"><div class="v" id="scan-chunks">0</div><div class="l">chunks</div></div> |
| <div class="c"><div class="v" id="scan-leads">0</div><div class="l">leads</div></div> |
| <div class="c"><div class="v" id="scan-elapsed">0s</div><div class="l">elapsed</div></div> |
| </div> |
| <div id="scan-feed"> |
| <div class="scan-empty">Run a scan above — per-file progress streams here.</div> |
| </div> |
| </section> |
| |
| <!-- Panel 3 --> |
| <section class="panel" id="panel-leads"> |
| <h3>Top leads <span class="sub">rolling, sorted by confidence</span></h3> |
| <div id="leads-table"> |
| <table class="leads"> |
| <thead> |
| <tr><th>#</th><th>conf</th><th>cwe</th><th>file:lines</th><th>preview</th></tr> |
| </thead> |
| <tbody id="leads-body"> |
| <tr><td colspan="5" style="color:#6b7280;text-align:center;padding:18px 0;font-family:system-ui;">No leads yet.</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </section> |
| |
| <!-- Panel 4 --> |
| <section class="panel" id="panel-pwnkit"> |
| <h3>Pwnkit hand-off <span class="sub">downstream agent, live stderr</span></h3> |
| <div class="pwnkit"> |
| <div class="pwnkit-pills"> |
| <div class="pwnkit-pill queued"><div class="v" id="pk-queued">0</div><div class="l">leads queued</div></div> |
| <div class="pwnkit-pill exploit"><div class="v" id="pk-exploit">0</div><div class="l">tool calls</div></div> |
| <div class="pwnkit-pill report"><div class="v" id="pk-report">0</div><div class="l">findings</div></div> |
| </div> |
| <div class="pwnkit-log" id="pk-log"> |
| <div class="line">Run the Tier 2 pwnkit accordion below to populate this panel.</div> |
| </div> |
| <div class="pwnkit-note"> |
| Each line above is a real pwnkit stderr line parsed live. <code>save_finding</code> rows are green. |
| </div> |
| </div> |
| </section> |
| </div> |
| </div> |
| |
| <script> |
| (function () { |
| if (window.__gfDashboardInit) return; |
| window.__gfDashboardInit = true; |
| |
| const $ = (id) => document.getElementById(id); |
| const escapeHtml = (s) => String(s) |
| .replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") |
| .replace(/"/g, """).replace(/'/g, "'"); |
| |
| function classifyConf(c) { |
| if (c >= 0.7) return "danger"; |
| if (c >= 0.4) return "warn"; |
| return "safe"; |
| } |
| function riskColor(r) { |
| if (r >= 0.7) return "rgba(239,68,68,0.55)"; |
| if (r >= 0.4) return "rgba(230,162,60,0.55)"; |
| return "rgba(40,167,101,0.32)"; |
| } |
| |
| // ----- Panel 1: token stream ----- // |
| let lastStreamSeq = -1; |
| function renderStream(state) { |
| if (!state) return; |
| const seq = state.seq | 0; |
| if (state.reset) { |
| $("stream-output").innerHTML = ""; |
| $("stream-output").classList.remove("empty"); |
| $("stat-tokens").textContent = "0"; |
| $("stat-peak").textContent = "0%"; |
| $("stat-avg").textContent = "0%"; |
| $("stat-maxpool").textContent = "0%"; |
| $("stat-top-cwe").innerHTML = "—"; |
| $("meter-needle").style.left = "0%"; |
| lastStreamSeq = -1; |
| } |
| const tokens = state.tokens || []; |
| // append only new tokens |
| for (let i = lastStreamSeq + 1; i < tokens.length; i++) { |
| const t = tokens[i]; |
| const span = document.createElement("span"); |
| span.className = "tok"; |
| span.style.backgroundColor = riskColor(t.risk_maxpool || t.risk || 0); |
| span.title = `risk ${(((t.risk_maxpool || t.risk || 0))*100).toFixed(1)}% · cwe ${t.top_cwe || '—'}`; |
| span.textContent = t.token; |
| $("stream-output").appendChild(span); |
| } |
| if (tokens.length > 0) { |
| lastStreamSeq = tokens.length - 1; |
| $("stream-output").scrollTop = $("stream-output").scrollHeight; |
| } |
| const stats = state.stats || {}; |
| $("stat-tokens").textContent = String(stats.n || tokens.length || 0); |
| $("stat-peak").textContent = ((stats.peak || 0) * 100).toFixed(0) + "%"; |
| $("stat-avg").textContent = ((stats.mean || 0) * 100).toFixed(0) + "%"; |
| $("stat-maxpool").textContent = ((stats.maxpool || 0) * 100).toFixed(0) + "%"; |
| $("meter-needle").style.left = ((stats.mean || 0) * 100).toFixed(1) + "%"; |
| if (stats.top_cwe) $("stat-top-cwe").textContent = stats.top_cwe; |
| $("dot-stream").className = "gf-dot " + (state.active ? "ok" : ""); |
| $("lbl-stream").textContent = state.active ? "probe stream live" : (tokens.length ? "probe stream done" : "probe stream idle"); |
| } |
| |
| // ----- Panel 2 + 3: scan + leads ----- // |
| function renderScan(state) { |
| if (!state) return; |
| const counters = state.counters || {}; |
| $("scan-files").textContent = String(counters.files || 0); |
| $("scan-chunks").textContent = String(counters.chunks || 0); |
| $("scan-leads").textContent = String(counters.leads || 0); |
| $("scan-elapsed").textContent = (counters.elapsed || 0).toFixed(1) + "s"; |
| |
| const feed = $("scan-feed"); |
| const files = state.files || {}; |
| const order = state.file_order || Object.keys(files); |
| if (!order.length) { |
| feed.innerHTML = '<div class="scan-empty">Run a scan above — per-file progress streams here.</div>'; |
| } else { |
| // Rebuild only if file count changed; otherwise update strips in place. |
| const existing = feed.querySelectorAll(".file-row"); |
| if (existing.length !== order.length) { |
| feed.innerHTML = ""; |
| order.forEach((f) => { |
| const fr = document.createElement("div"); |
| fr.className = "file-row"; |
| fr.dataset.file = f; |
| fr.innerHTML = `<div class="name"><span class="p"></span><span class="n"></span></div><div class="strip"></div>`; |
| fr.querySelector(".p").textContent = f; |
| feed.appendChild(fr); |
| }); |
| } |
| order.forEach((f) => { |
| const info = files[f]; |
| if (!info) return; |
| const fr = feed.querySelector(`.file-row[data-file="${CSS.escape(f)}"]`); |
| if (!fr) return; |
| const expected = info.expected || info.scores.length || 0; |
| fr.querySelector(".n").textContent = `${info.scores.length}/${expected} chunks`; |
| const strip = fr.querySelector(".strip"); |
| // Append only the new dots. |
| const haveDots = strip.childElementCount; |
| for (let i = haveDots; i < info.scores.length; i++) { |
| const s = info.scores[i]; |
| const d = document.createElement("span"); |
| const cls = classifyConf(s.confidence || 0); |
| d.className = `dot ${cls}`; |
| d.title = `lines ${s.start_line}-${s.end_line} · conf ${(((s.confidence||0))*100).toFixed(0)}% · ${s.top_cwe || ''}`; |
| strip.appendChild(d); |
| } |
| }); |
| feed.scrollTop = feed.scrollHeight; |
| } |
| |
| const leads = state.leads || []; |
| const body = $("leads-body"); |
| if (!leads.length) { |
| body.innerHTML = '<tr><td colspan="5" style="color:#6b7280;text-align:center;padding:18px 0;font-family:system-ui;">No leads yet.</td></tr>'; |
| } else { |
| const sorted = [...leads].sort((a, b) => (b.confidence || 0) - (a.confidence || 0)); |
| body.innerHTML = sorted.map((l, i) => { |
| const cls = classifyConf(l.confidence || 0); |
| const preview = (l.preview || "").trim(); |
| return `<tr> |
| <td class="rank">${i + 1}</td> |
| <td class="conf"><span class="gf-confpill ${cls}">${((l.confidence||0)*100).toFixed(0)}%</span></td> |
| <td class="cwe">${escapeHtml(l.top_cwe || '—')}</td> |
| <td class="file">${escapeHtml(l.location || '')}</td> |
| <td class="preview">${escapeHtml(preview)}</td> |
| </tr>`; |
| }).join(""); |
| } |
| $("dot-events").className = "gf-dot " + (state.active ? "ok" : (counters.files ? "" : "")); |
| $("lbl-events").textContent = state.active ? `scan live · ${counters.chunks||0} chunks` : (counters.files ? `scan done · ${counters.leads||0} leads` : "scan idle"); |
| } |
| |
| // ----- Panel 4: pwnkit ----- // |
| let lastPkLogSeq = -1; |
| function renderPwnkit(state) { |
| if (!state) return; |
| if (state.reset) { |
| $("pk-log").innerHTML = ""; |
| lastPkLogSeq = -1; |
| } |
| const counts = state.counts || {}; |
| $("pk-queued").textContent = String(counts.queued || 0); |
| $("pk-exploit").textContent = String(counts.tools || 0); |
| $("pk-report").textContent = String(counts.findings || 0); |
| |
| const log = state.log || []; |
| for (let i = lastPkLogSeq + 1; i < log.length; i++) { |
| const e = log[i]; |
| const div = document.createElement("div"); |
| div.className = "line " + (e.level || ""); |
| const ts = new Date((e.ts || Date.now() / 1000) * 1000).toLocaleTimeString([], { hour12: false }); |
| div.innerHTML = `<span class="ts">${ts}</span>${escapeHtml(e.message || "")}`; |
| $("pk-log").appendChild(div); |
| } |
| if (log.length) { |
| lastPkLogSeq = log.length - 1; |
| $("pk-log").scrollTop = $("pk-log").scrollHeight; |
| // bounded |
| while ($("pk-log").childNodes.length > 200) $("pk-log").removeChild($("pk-log").firstChild); |
| } |
| $("dot-pwnkit").className = "gf-dot " + (state.active ? "ok" : ""); |
| $("lbl-pwnkit").textContent = state.active ? `pwnkit live · ${counts.tools||0} calls` : (log.length ? `pwnkit done · ${counts.findings||0} findings` : "pwnkit idle"); |
| } |
| |
| // ----- bridge from hidden gr.JSON components ----- // |
| // Each gr.JSON renders as <div class="gradio-json"> containing a <pre> |
| // with the serialised value. We watch the four mount points by elem_id. |
| function readJSON(elemId) { |
| const root = document.getElementById(elemId); |
| if (!root) return null; |
| // gr.JSON renders into a nested element; grab the deepest .json-holder or pre. |
| const pre = root.querySelector("pre, .json-holder code, code"); |
| if (!pre) return null; |
| const txt = (pre.innerText || pre.textContent || "").trim(); |
| if (!txt) return null; |
| try { return JSON.parse(txt); } |
| catch (_) { |
| // gr.JSON may render expandable nodes — fall back to data attribute set by Python tick. |
| try { return JSON.parse(root.dataset.value || "null"); } catch (__) { return null; } |
| } |
| } |
| |
| function tick() { |
| try { renderStream(readJSON("gf-state-stream")); } catch (e) { console.warn(e); } |
| try { renderScan(readJSON("gf-state-scan")); } catch (e) { console.warn(e); } |
| try { renderPwnkit(readJSON("gf-state-pwnkit")); } catch (e) { console.warn(e); } |
| } |
| // Poll every 250ms — Gradio's gr.JSON doesn't fire DOM mutation observers |
| // reliably across all versions, and a quarter-second tick is cheap. |
| setInterval(tick, 250); |
| // Also tick once on load (might race with Gradio mount, hence the interval too). |
| setTimeout(tick, 500); |
| })(); |
| </script> |
| """ |
|
|
|
|
| def initial_stream_state() -> dict: |
| return { |
| "active": False, "reset": True, "seq": 0, |
| "tokens": [], |
| "stats": {"n": 0, "peak": 0.0, "mean": 0.0, "maxpool": 0.0, "top_cwe": None}, |
| } |
|
|
|
|
| def initial_scan_state() -> dict: |
| return { |
| "active": False, |
| "counters": {"files": 0, "chunks": 0, "leads": 0, "elapsed": 0.0}, |
| "file_order": [], |
| "files": {}, |
| "leads": [], |
| } |
|
|
|
|
| def initial_pwnkit_state() -> dict: |
| return { |
| "active": False, "reset": True, |
| "counts": {"queued": 0, "tools": 0, "findings": 0}, |
| "log": [], |
| } |
|
|