From c3ddcd0b248a23dff5e04733ed043334ff71284a Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Thu, 21 May 2026 23:39:30 -0700 Subject: [PATCH] refactor(ui): renderVerdictBadge returns DocumentFragment, callers use appendChild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite the verdict-badge HTML builder from string-concat into DOM construction (createElement + textContent + setAttribute + append). The helper now returns a DocumentFragment of two top-level siblings (.verdict-badge and .verdict-detail), which appendChild expands into the parent — preserving the sibling-traversal invariants relied on by Pane.updateVerdictBadge, toggleVerdictDetail, and the d-key keyboard shortcut. Inline onclick="toggleVerdictDetail(this)" replaced with an addEventListener click handler; the non-arrow callback keeps the `this`→button binding the old inline form had. Both call sites (replayHistory + the live approval flow) swap from el.insertAdjacentHTML("beforeend", X) to el.appendChild(X). This is the last unsafe-write site in the DOM-cleanup arc started in #532; commit 2 broadens the test_app_js.py lint regex to forbid the insertAdjacent-HTML sink across all 8 tracked JS bundles. --- turnstone/ui/static/app.js | 132 +++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 58 deletions(-) diff --git a/turnstone/ui/static/app.js b/turnstone/ui/static/app.js index 05fd08fa..7ce613f9 100644 --- a/turnstone/ui/static/app.js +++ b/turnstone/ui/static/app.js @@ -1437,10 +1437,7 @@ class Pane { // judgePending=false because any verdict on replay is // final — no spinner. if (tc.verdict) { - div.insertAdjacentHTML( - "beforeend", - renderVerdictBadge(tc.verdict, false), - ); + div.appendChild(renderVerdictBadge(tc.verdict, false)); } block.appendChild(div); // Output-guard finding — defer insertion until the tool @@ -1652,10 +1649,7 @@ class Pane { // key in case a stale SSE payload arrives mid-deploy. const heuristic = item.heuristic_verdict || item.verdict; if (heuristic) { - block.insertAdjacentHTML( - "beforeend", - renderVerdictBadge(heuristic, judgePending), - ); + block.appendChild(renderVerdictBadge(heuristic, judgePending)); const rec = heuristic.recommendation || "review"; if ( !glowRec || @@ -4838,61 +4832,83 @@ function buildToolDiv(item) { } function renderVerdictBadge(verdict, judgePending) { - if (!verdict) return ""; + if (!verdict) return document.createDocumentFragment(); const risk = verdict.risk_level || "medium"; const rec = verdict.recommendation || "review"; const conf = Math.round((verdict.confidence || 0) * 100); - const summary = verdict.intent_summary || ""; - let spinnerHtml = ""; + + const badge = document.createElement("div"); + badge.className = "verdict-badge verdict-" + risk + " ts-verdict-badge"; + badge.setAttribute("data-risk", risk); + if (verdict.call_id) badge.setAttribute("data-call-id", verdict.call_id); + + const riskSpan = document.createElement("span"); + riskSpan.className = "verdict-risk"; + riskSpan.textContent = risk.toUpperCase(); + + const recSpan = document.createElement("span"); + recSpan.className = "verdict-rec"; + recSpan.textContent = rec; + + const confSpan = document.createElement("span"); + confSpan.className = "verdict-conf"; + confSpan.textContent = conf + "%"; + + badge.append(riskSpan, recSpan, confSpan); + if (judgePending) { - spinnerHtml = - '' + - ' judge analyzing\u2026'; + const spinner = document.createElement("span"); + spinner.className = "verdict-judge-spinner"; + const dot = document.createElement("span"); + dot.className = "judge-spinner-dot"; + spinner.append(dot, " judge analyzing\u2026"); + badge.appendChild(spinner); } - const callId = escapeHtml(verdict.call_id || ""); - return ( - '
' + - '' + - escapeHtml(risk.toUpperCase()) + - "" + - '' + - escapeHtml(rec) + - "" + - '' + - conf + - "%" + - spinnerHtml + - '' + - "
" + - '" - ); + + const expand = document.createElement("button"); + expand.className = "verdict-expand"; + expand.textContent = "details"; + expand.addEventListener("click", function () { + toggleVerdictDetail(this); + }); + badge.appendChild(expand); + + const detail = document.createElement("div"); + detail.className = "verdict-detail"; + detail.style.display = "none"; + + const summaryEl = document.createElement("div"); + summaryEl.className = "verdict-summary"; + summaryEl.textContent = verdict.intent_summary || ""; + + const reasoningEl = document.createElement("div"); + reasoningEl.className = "verdict-reasoning"; + reasoningEl.textContent = verdict.reasoning || ""; + + detail.append(summaryEl, reasoningEl); + + const evidence = verdict.evidence || []; + if (evidence.length) { + const evEl = document.createElement("div"); + evEl.className = "verdict-evidence"; + for (const ev of evidence) { + const row = document.createElement("div"); + row.textContent = "\u2022 " + ev; + evEl.appendChild(row); + } + detail.appendChild(evEl); + } + + const tierEl = document.createElement("div"); + tierEl.className = "verdict-tier"; + let tierText = (verdict.tier || "heuristic") + " tier"; + if (verdict.judge_model) tierText += " | " + verdict.judge_model; + tierEl.textContent = tierText; + detail.appendChild(tierEl); + + const frag = document.createDocumentFragment(); + frag.append(badge, detail); + return frag; } function toggleVerdictDetail(btn) {