|
|
@@ -0,0 +1,1073 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+<head>
|
|
|
+<meta charset="utf-8">
|
|
|
+<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
+<title>Agent Analysis Report</title>
|
|
|
+<style>
|
|
|
+ :root {
|
|
|
+ --bg: #0f1b2d;
|
|
|
+ --panel: #16263d;
|
|
|
+ --panel-2: #1d3250;
|
|
|
+ --ink: #e9eef6;
|
|
|
+ --ink-dim: #9fb0c7;
|
|
|
+ --line: #294366;
|
|
|
+ --accent: #b66d46;
|
|
|
+ --accent-ink: #f4d9c8;
|
|
|
+ --critical: #e05656;
|
|
|
+ --high: #e0904a;
|
|
|
+ --medium: #d8c24a;
|
|
|
+ --low: #5aa0d0;
|
|
|
+ --ok: #4caf72;
|
|
|
+ }
|
|
|
+ * { box-sizing: border-box; }
|
|
|
+ body {
|
|
|
+ margin: 0;
|
|
|
+ background: var(--bg);
|
|
|
+ color: var(--ink);
|
|
|
+ font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
|
+ }
|
|
|
+ .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 80px; }
|
|
|
+ header h1 { font-size: 22px; margin: 0 0 4px; }
|
|
|
+ header .meta { color: var(--ink-dim); font-size: 13px; }
|
|
|
+ header .meta b { color: var(--ink); font-weight: 600; }
|
|
|
+
|
|
|
+ .banner {
|
|
|
+ display: none;
|
|
|
+ background: #3a1414;
|
|
|
+ border: 1px solid var(--critical);
|
|
|
+ color: #ffd9d9;
|
|
|
+ padding: 14px 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 16px 0;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ .banner.show { display: block; }
|
|
|
+
|
|
|
+ .overview {
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 18px;
|
|
|
+ margin: 18px 0;
|
|
|
+ }
|
|
|
+ .grade {
|
|
|
+ font-size: 34px;
|
|
|
+ font-weight: 800;
|
|
|
+ margin: 0 0 8px;
|
|
|
+ text-transform: capitalize;
|
|
|
+ }
|
|
|
+ .grade.g-excellent { color: var(--ok); }
|
|
|
+ .grade.g-good { color: var(--low); }
|
|
|
+ .grade.g-fair { color: var(--medium); }
|
|
|
+ .grade.g-poor { color: var(--critical); }
|
|
|
+ .verdict { font-size: 16px; font-weight: 600; margin: 0 0 14px; }
|
|
|
+ .summary { color: var(--ink-dim); margin: 0 0 14px; }
|
|
|
+ .counts { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
|
+ .pill {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ .pill .dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
|
+ .pill .n { font-weight: 700; }
|
|
|
+ .dot.critical { background: var(--critical); }
|
|
|
+ .dot.high { background: var(--high); }
|
|
|
+ .dot.medium { background: var(--medium); }
|
|
|
+ .dot.low { background: var(--low); }
|
|
|
+
|
|
|
+ /* Agent portrait (agent_profile block) */
|
|
|
+ .portrait {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 18px;
|
|
|
+ margin: 18px 0;
|
|
|
+ }
|
|
|
+ .portrait .icon {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ width: 56px;
|
|
|
+ height: 56px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 30px;
|
|
|
+ }
|
|
|
+ .portrait .who { flex: 1 1 auto; min-width: 0; }
|
|
|
+ .portrait .who .name { font-size: 18px; font-weight: 700; }
|
|
|
+ .portrait .who .title { color: var(--ink-dim); font-size: 13px; margin-top: 1px; }
|
|
|
+ .portrait .who .mission { margin-top: 8px; }
|
|
|
+ .portrait .who .type {
|
|
|
+ display: inline-block;
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 11px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.04em;
|
|
|
+ padding: 3px 8px;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ border: 1px solid var(--accent);
|
|
|
+ color: var(--accent-ink);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Generic agent-block / synthesis panel */
|
|
|
+ .block {
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 18px;
|
|
|
+ margin: 18px 0;
|
|
|
+ }
|
|
|
+ .block > h2 {
|
|
|
+ font-size: 13px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.06em;
|
|
|
+ color: var(--ink-dim);
|
|
|
+ margin: 0 0 12px;
|
|
|
+ }
|
|
|
+ .cap-list { list-style: none; margin: 0; padding: 0; }
|
|
|
+ .cap-list li {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-top: 1px solid var(--line);
|
|
|
+ }
|
|
|
+ .cap-list li:first-child { border-top: none; }
|
|
|
+ .cap-list .cap-name { font-weight: 600; flex: 0 0 auto; }
|
|
|
+ .cap-list .cap-kind {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ font-size: 11px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.04em;
|
|
|
+ padding: 2px 7px;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ color: var(--ink-dim);
|
|
|
+ }
|
|
|
+ .cap-list .cap-note { color: var(--ink-dim); flex: 1 1 auto; min-width: 0; }
|
|
|
+ .kv { margin: 0; display: grid; grid-template-columns: 150px 1fr; gap: 6px 14px; }
|
|
|
+ .kv dt { color: var(--ink-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
|
+ .kv dd { margin: 0; }
|
|
|
+ .block .journey { padding: 8px 0; border-top: 1px solid var(--line); }
|
|
|
+ .block .journey:first-of-type { border-top: none; padding-top: 0; }
|
|
|
+ .block .journey .j-name { font-weight: 600; }
|
|
|
+ .block .journey .j-steps { color: var(--ink-dim); margin-top: 2px; }
|
|
|
+ .block .mono, .block code {
|
|
|
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ padding: 1px 5px;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Themes */
|
|
|
+ .theme { padding: 12px 0; border-top: 1px solid var(--line); }
|
|
|
+ .theme:first-of-type { border-top: none; padding-top: 0; }
|
|
|
+ .theme .t-head { display: flex; align-items: center; gap: 10px; }
|
|
|
+ .theme .t-title { font-weight: 600; flex: 1 1 auto; min-width: 0; }
|
|
|
+ .theme .t-cause { color: var(--ink-dim); margin-top: 4px; }
|
|
|
+ .theme .t-action { margin-top: 4px; }
|
|
|
+ .theme .t-findings { margin-top: 8px; padding-left: 12px; border-left: 2px solid var(--line); }
|
|
|
+ .theme .t-finding { font-size: 13px; color: var(--ink-dim); padding: 2px 0; }
|
|
|
+
|
|
|
+ /* Strengths */
|
|
|
+ .strength-list { margin: 0; padding-left: 20px; }
|
|
|
+ .strength-list li { padding: 2px 0; }
|
|
|
+
|
|
|
+ /* Recommendations */
|
|
|
+ .rec { padding: 8px 0; border-top: 1px solid var(--line); }
|
|
|
+ .rec:first-of-type { border-top: none; padding-top: 0; }
|
|
|
+ .rec .rank { font-weight: 700; color: var(--accent-ink); margin-right: 8px; }
|
|
|
+ .rec .resolves { color: var(--ink-dim); font-size: 12px; margin-left: 8px; }
|
|
|
+
|
|
|
+ .toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin: 18px 0 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ .toolbar .sel-count { color: var(--ink-dim); font-size: 13px; }
|
|
|
+ button {
|
|
|
+ font: inherit;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ background: var(--panel-2);
|
|
|
+ color: var(--ink);
|
|
|
+ padding: 8px 14px;
|
|
|
+ }
|
|
|
+ button.primary {
|
|
|
+ background: var(--accent);
|
|
|
+ border-color: var(--accent);
|
|
|
+ color: #1a0e07;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ button:disabled { opacity: 0.5; cursor: default; }
|
|
|
+ button.link {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: var(--accent-ink);
|
|
|
+ padding: 4px 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ button.small { padding: 5px 10px; font-size: 13px; flex: 0 0 auto; }
|
|
|
+
|
|
|
+ .no-findings {
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px dashed var(--line);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 28px;
|
|
|
+ text-align: center;
|
|
|
+ color: var(--ink-dim);
|
|
|
+ }
|
|
|
+ .no-findings .big { font-size: 18px; color: var(--ok); margin-bottom: 6px; }
|
|
|
+
|
|
|
+ .group { margin: 18px 0; }
|
|
|
+ .group > h2 {
|
|
|
+ font-size: 13px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.06em;
|
|
|
+ color: var(--ink-dim);
|
|
|
+ margin: 0 0 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .finding {
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-left: 4px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 8px 0;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ .finding.sev-critical { border-left-color: var(--critical); }
|
|
|
+ .finding.sev-high { border-left-color: var(--high); }
|
|
|
+ .finding.sev-medium { border-left-color: var(--medium); }
|
|
|
+ .finding.sev-low { border-left-color: var(--low); }
|
|
|
+
|
|
|
+ .finding .row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ }
|
|
|
+ .finding .row .chk { width: 16px; height: 16px; flex: 0 0 auto; cursor: pointer; }
|
|
|
+ .finding .row .head { flex: 1 1 auto; cursor: pointer; min-width: 0; }
|
|
|
+ .finding .row .title { font-weight: 600; }
|
|
|
+ .finding .row .sub { color: var(--ink-dim); font-size: 12px; margin-top: 2px; }
|
|
|
+ .finding .tag {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ font-size: 11px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.04em;
|
|
|
+ padding: 3px 8px;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ color: var(--ink-dim);
|
|
|
+ }
|
|
|
+ .finding .caret { flex: 0 0 auto; color: var(--ink-dim); transition: transform 0.15s; cursor: pointer; }
|
|
|
+ .finding.open .caret { transform: rotate(90deg); }
|
|
|
+
|
|
|
+ .finding .body {
|
|
|
+ display: none;
|
|
|
+ padding: 0 14px 14px 42px;
|
|
|
+ border-top: 1px solid var(--line);
|
|
|
+ }
|
|
|
+ .finding.open .body { display: block; }
|
|
|
+ .finding .body dl { margin: 12px 0 0; display: grid; grid-template-columns: 130px 1fr; gap: 6px 14px; }
|
|
|
+ .finding .body dt { color: var(--ink-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
|
+ .finding .body dd { margin: 0; }
|
|
|
+ .finding .body code, .finding .body .mono {
|
|
|
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ padding: 1px 5px;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast {
|
|
|
+ position: fixed;
|
|
|
+ left: 50%;
|
|
|
+ bottom: 28px;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ background: var(--ok);
|
|
|
+ color: #06160c;
|
|
|
+ padding: 10px 18px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-weight: 600;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+ .toast.show { opacity: 1; }
|
|
|
+
|
|
|
+ .fallback-area { margin-top: 12px; display: none; }
|
|
|
+ .fallback-area.show { display: block; }
|
|
|
+ .fallback-area textarea {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 160px;
|
|
|
+ background: var(--panel-2);
|
|
|
+ color: var(--ink);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 10px;
|
|
|
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+<div class="wrap">
|
|
|
+ <header>
|
|
|
+ <h1>Agent Analysis Report</h1>
|
|
|
+ <div class="meta">
|
|
|
+ <span>Subject: <b id="m-subject">—</b></span> ·
|
|
|
+ <span>Generated: <b id="m-generated">—</b></span> ·
|
|
|
+ <span>Schema: <b id="m-schema">—</b></span>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div id="parse-banner" class="banner"></div>
|
|
|
+
|
|
|
+ <!-- Agent portrait (agent_profile). Hidden when the block is absent. -->
|
|
|
+ <section id="portrait" class="portrait" hidden></section>
|
|
|
+
|
|
|
+ <section id="overview" class="overview" hidden>
|
|
|
+ <div id="grade" class="grade" hidden></div>
|
|
|
+ <p id="verdict" class="verdict"></p>
|
|
|
+ <p id="summary-text" class="summary" hidden></p>
|
|
|
+ <div id="counts" class="counts"></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- Capability dashboard (capabilities). Hidden when absent. -->
|
|
|
+ <section id="capabilities" class="block" hidden></section>
|
|
|
+
|
|
|
+ <!-- Per-lens verdicts (detailed_analysis). Hidden when absent. -->
|
|
|
+ <section id="lens-verdicts" class="block" hidden></section>
|
|
|
+
|
|
|
+ <!-- Sanctum block (sanctum). Conditional; hidden for stateless agents. -->
|
|
|
+ <section id="sanctum" class="block" hidden></section>
|
|
|
+
|
|
|
+ <!-- Experience: journeys plus headless (experience). Hidden when absent. -->
|
|
|
+ <section id="experience" class="block" hidden></section>
|
|
|
+
|
|
|
+ <!-- Synthesis layer (themes, strengths, recommendations). Hidden when absent. -->
|
|
|
+ <section id="themes" class="block" hidden></section>
|
|
|
+ <section id="strengths" class="block" hidden></section>
|
|
|
+ <section id="recommendations" class="block" hidden></section>
|
|
|
+
|
|
|
+ <div id="toolbar" class="toolbar" hidden>
|
|
|
+ <button id="btn-copy" class="primary" disabled>Copy selected as paste-back prompt</button>
|
|
|
+ <span id="sel-count" class="sel-count">0 selected</span>
|
|
|
+ <button id="btn-select-all" class="link">Select all</button>
|
|
|
+ <button id="btn-clear" class="link">Clear</button>
|
|
|
+ <button id="btn-expand-all" class="link">Expand all</button>
|
|
|
+ <button id="btn-collapse-all" class="link">Collapse all</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="fallback" class="fallback-area">
|
|
|
+ <p class="sel-count">Clipboard was unavailable. Copy the text below manually:</p>
|
|
|
+ <textarea id="fallback-text" readonly></textarea>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="findings-root"></div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<div id="toast" class="toast">Copied</div>
|
|
|
+
|
|
|
+<!-- scripts/render_report.py replaces the contents of this island per run.
|
|
|
+ The placeholder below is intentionally unusable: the shell refuses to
|
|
|
+ render it, so a failed injection can never look like real findings. -->
|
|
|
+<script type="application/json" id="report-data">
|
|
|
+{
|
|
|
+ "schema_version": 2,
|
|
|
+ "subject": "__PLACEHOLDER__",
|
|
|
+ "generated": "",
|
|
|
+ "verdict": "",
|
|
|
+ "findings": []
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<script>
|
|
|
+(function () {
|
|
|
+ "use strict";
|
|
|
+
|
|
|
+ var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
|
+ var SEV_LABEL = { critical: "Critical", high: "High", medium: "Medium", low: "Low" };
|
|
|
+ var GRADES = ["excellent", "good", "fair", "poor"];
|
|
|
+ var PLACEHOLDER_SUBJECT = "__PLACEHOLDER__";
|
|
|
+
|
|
|
+ var els = {
|
|
|
+ banner: document.getElementById("parse-banner"),
|
|
|
+ portrait: document.getElementById("portrait"),
|
|
|
+ overview: document.getElementById("overview"),
|
|
|
+ grade: document.getElementById("grade"),
|
|
|
+ verdict: document.getElementById("verdict"),
|
|
|
+ summaryText: document.getElementById("summary-text"),
|
|
|
+ counts: document.getElementById("counts"),
|
|
|
+ capabilities: document.getElementById("capabilities"),
|
|
|
+ lensVerdicts: document.getElementById("lens-verdicts"),
|
|
|
+ sanctum: document.getElementById("sanctum"),
|
|
|
+ experience: document.getElementById("experience"),
|
|
|
+ themes: document.getElementById("themes"),
|
|
|
+ strengths: document.getElementById("strengths"),
|
|
|
+ recommendations: document.getElementById("recommendations"),
|
|
|
+ toolbar: document.getElementById("toolbar"),
|
|
|
+ root: document.getElementById("findings-root"),
|
|
|
+ subject: document.getElementById("m-subject"),
|
|
|
+ generated: document.getElementById("m-generated"),
|
|
|
+ schema: document.getElementById("m-schema"),
|
|
|
+ selCount: document.getElementById("sel-count"),
|
|
|
+ btnCopy: document.getElementById("btn-copy"),
|
|
|
+ btnSelectAll: document.getElementById("btn-select-all"),
|
|
|
+ btnClear: document.getElementById("btn-clear"),
|
|
|
+ btnExpandAll: document.getElementById("btn-expand-all"),
|
|
|
+ btnCollapseAll: document.getElementById("btn-collapse-all"),
|
|
|
+ fallback: document.getElementById("fallback"),
|
|
|
+ fallbackText: document.getElementById("fallback-text"),
|
|
|
+ toast: document.getElementById("toast")
|
|
|
+ };
|
|
|
+
|
|
|
+ var selected = Object.create(null);
|
|
|
+ var findings = [];
|
|
|
+ var findingsById = Object.create(null);
|
|
|
+ var subjectPath = "";
|
|
|
+ var standards = null;
|
|
|
+
|
|
|
+ function showBanner(message) {
|
|
|
+ els.banner.textContent = message;
|
|
|
+ els.banner.classList.add("show");
|
|
|
+ }
|
|
|
+
|
|
|
+ function esc(value) {
|
|
|
+ var s = value == null ? "" : String(value);
|
|
|
+ return s.replace(/[&<>"']/g, function (c) {
|
|
|
+ return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Normalize an arbitrary parsed object against schema_version 2, supplying
|
|
|
+ // defaults so a partial or future island still renders. Unknown fields are
|
|
|
+ // ignored, not fatal. Severity counts are always derived from the findings
|
|
|
+ // array, never read from the island, so they cannot disagree with it. The
|
|
|
+ // agent blocks (agent_profile, capabilities, detailed_analysis, sanctum,
|
|
|
+ // experience) and the synthesis blocks (grade, summary, themes, strengths,
|
|
|
+ // recommendations) are OPTIONAL: each normalizes to an empty value that
|
|
|
+ // renders nothing rather than an empty panel or an error.
|
|
|
+ function normalize(raw) {
|
|
|
+ var obj = raw && typeof raw === "object" ? raw : {};
|
|
|
+ var rawFindings = Array.isArray(obj.findings) ? obj.findings : [];
|
|
|
+
|
|
|
+ var norm = {
|
|
|
+ schema_version: typeof obj.schema_version === "number" ? obj.schema_version : 2,
|
|
|
+ subject: obj.subject != null ? String(obj.subject) : "(unspecified)",
|
|
|
+ generated: obj.generated != null ? String(obj.generated) : "(unspecified)",
|
|
|
+ verdict: obj.verdict != null ? String(obj.verdict) : "(no verdict supplied)",
|
|
|
+ grade: GRADES.indexOf(String(obj.grade || "").toLowerCase()) >= 0
|
|
|
+ ? String(obj.grade).toLowerCase() : "",
|
|
|
+ summary: typeof obj.summary === "string" ? obj.summary : "",
|
|
|
+ standards: (obj.standards && typeof obj.standards === "object") ? {
|
|
|
+ canon: obj.standards.canon != null ? String(obj.standards.canon) : "",
|
|
|
+ principles: obj.standards.principles != null ? String(obj.standards.principles) : "",
|
|
|
+ scripts: obj.standards.scripts != null ? String(obj.standards.scripts) : ""
|
|
|
+ } : null,
|
|
|
+ themes: normalizeThemes(obj.themes),
|
|
|
+ strengths: normalizeStrengths(obj.strengths),
|
|
|
+ recommendations: normalizeRecommendations(obj.recommendations),
|
|
|
+ counts: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
|
+ findings: [],
|
|
|
+ agent_profile: normalizeProfile(obj.agent_profile),
|
|
|
+ capabilities: normalizeCapabilities(obj.capabilities),
|
|
|
+ detailed_analysis: normalizeDetailed(obj.detailed_analysis),
|
|
|
+ sanctum: normalizeSanctum(obj.sanctum),
|
|
|
+ experience: normalizeExperience(obj.experience)
|
|
|
+ };
|
|
|
+
|
|
|
+ rawFindings.forEach(function (f, i) {
|
|
|
+ if (!f || typeof f !== "object") { return; }
|
|
|
+ var sev = SEVERITIES.indexOf(f.severity) >= 0 ? f.severity : "low";
|
|
|
+ norm.findings.push({
|
|
|
+ id: f.id != null ? String(f.id) : "finding-" + (i + 1),
|
|
|
+ lens: f.lens != null ? String(f.lens) : "(unknown)",
|
|
|
+ severity: sev,
|
|
|
+ title: f.title != null ? String(f.title) : "(untitled finding)",
|
|
|
+ location: f.location != null ? String(f.location) : "",
|
|
|
+ evidence: f.evidence != null ? String(f.evidence) : "",
|
|
|
+ recommendation: f.recommendation != null ? String(f.recommendation) : "",
|
|
|
+ proposed_smallest: f.proposed_smallest != null ? String(f.proposed_smallest) : "",
|
|
|
+ predicted_delta: f.predicted_delta != null ? String(f.predicted_delta) : ""
|
|
|
+ });
|
|
|
+ norm.counts[sev] += 1;
|
|
|
+ });
|
|
|
+
|
|
|
+ return norm;
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeThemes(raw) {
|
|
|
+ if (!Array.isArray(raw)) { return []; }
|
|
|
+ var list = [];
|
|
|
+ raw.forEach(function (t) {
|
|
|
+ if (!t || typeof t !== "object") { return; }
|
|
|
+ var ids = [];
|
|
|
+ if (Array.isArray(t.finding_ids)) {
|
|
|
+ t.finding_ids.forEach(function (id) { if (id != null) { ids.push(String(id)); } });
|
|
|
+ }
|
|
|
+ var title = t.title != null ? String(t.title) : "";
|
|
|
+ if (!title && !ids.length) { return; }
|
|
|
+ list.push({
|
|
|
+ title: title || "(untitled theme)",
|
|
|
+ root_cause: t.root_cause != null ? String(t.root_cause) : "",
|
|
|
+ action: t.action != null ? String(t.action) : "",
|
|
|
+ finding_ids: ids
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeStrengths(raw) {
|
|
|
+ if (!Array.isArray(raw)) { return []; }
|
|
|
+ var list = [];
|
|
|
+ raw.forEach(function (s) {
|
|
|
+ if (typeof s === "string" && s) { list.push(s); }
|
|
|
+ else if (s && typeof s === "object" && s.title) {
|
|
|
+ list.push(String(s.title) + (s.detail ? " — " + String(s.detail) : ""));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeRecommendations(raw) {
|
|
|
+ if (!Array.isArray(raw)) { return []; }
|
|
|
+ var list = [];
|
|
|
+ raw.forEach(function (r, i) {
|
|
|
+ if (!r || typeof r !== "object") { return; }
|
|
|
+ var action = r.action != null ? String(r.action) : "";
|
|
|
+ if (!action) { return; }
|
|
|
+ var resolves = "";
|
|
|
+ if (Array.isArray(r.resolves)) { resolves = r.resolves.map(String).join(", "); }
|
|
|
+ else if (typeof r.resolves === "number") { resolves = r.resolves + " findings"; }
|
|
|
+ else if (r.resolves != null) { resolves = String(r.resolves); }
|
|
|
+ list.push({
|
|
|
+ rank: typeof r.rank === "number" ? r.rank : i + 1,
|
|
|
+ action: action,
|
|
|
+ resolves: resolves
|
|
|
+ });
|
|
|
+ });
|
|
|
+ list.sort(function (a, b) { return a.rank - b.rank; });
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional agent_profile portrait: name/title/icon/agent_type/mission.
|
|
|
+ // Returns null when nothing usable is present, so the portrait stays hidden.
|
|
|
+ function normalizeProfile(raw) {
|
|
|
+ if (!raw || typeof raw !== "object") { return null; }
|
|
|
+ var p = {
|
|
|
+ name: raw.name != null ? String(raw.name) : "",
|
|
|
+ title: raw.title != null ? String(raw.title) : "",
|
|
|
+ icon: raw.icon != null ? String(raw.icon) : "",
|
|
|
+ agent_type: raw.agent_type != null ? String(raw.agent_type) : "",
|
|
|
+ mission: raw.mission != null ? String(raw.mission) : ""
|
|
|
+ };
|
|
|
+ if (!p.name && !p.title && !p.mission && !p.agent_type) { return null; }
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional capability dashboard: a list of { name, kind, note }.
|
|
|
+ // Returns null when empty or absent.
|
|
|
+ function normalizeCapabilities(raw) {
|
|
|
+ if (!Array.isArray(raw)) { return null; }
|
|
|
+ var list = [];
|
|
|
+ raw.forEach(function (c) {
|
|
|
+ if (!c || typeof c !== "object") { return; }
|
|
|
+ var name = c.name != null ? String(c.name) : "";
|
|
|
+ if (!name) { return; }
|
|
|
+ list.push({
|
|
|
+ name: name,
|
|
|
+ kind: c.kind != null ? String(c.kind) : "",
|
|
|
+ note: c.note != null ? String(c.note) : ""
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return list.length ? list : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional detailed_analysis: a map of lens name to one-line verdict.
|
|
|
+ // Returns null when empty or absent.
|
|
|
+ function normalizeDetailed(raw) {
|
|
|
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return null; }
|
|
|
+ var entries = [];
|
|
|
+ Object.keys(raw).forEach(function (key) {
|
|
|
+ var value = raw[key];
|
|
|
+ if (value == null || typeof value === "object") { return; }
|
|
|
+ entries.push({ lens: key, verdict: String(value) });
|
|
|
+ });
|
|
|
+ return entries.length ? entries : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional sanctum block, shown only for memory/autonomous agents.
|
|
|
+ // Returns null when absent or explicitly marked not present.
|
|
|
+ function normalizeSanctum(raw) {
|
|
|
+ if (!raw || typeof raw !== "object") { return null; }
|
|
|
+ if (raw.present === false) { return null; }
|
|
|
+ var files = [];
|
|
|
+ if (Array.isArray(raw.files)) {
|
|
|
+ raw.files.forEach(function (f) { if (f != null) { files.push(String(f)); } });
|
|
|
+ }
|
|
|
+ var s = {
|
|
|
+ location: raw.location != null ? String(raw.location) : "",
|
|
|
+ files: files,
|
|
|
+ note: raw.note != null ? String(raw.note) : ""
|
|
|
+ };
|
|
|
+ if (!s.location && !s.files.length && !s.note) { return null; }
|
|
|
+ return s;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional experience block: journeys plus a headless note. Returns null when
|
|
|
+ // neither is usable.
|
|
|
+ function normalizeExperience(raw) {
|
|
|
+ if (!raw || typeof raw !== "object") { return null; }
|
|
|
+ var journeys = [];
|
|
|
+ if (Array.isArray(raw.journeys)) {
|
|
|
+ raw.journeys.forEach(function (j) {
|
|
|
+ if (!j || typeof j !== "object") { return; }
|
|
|
+ var name = j.name != null ? String(j.name) : "";
|
|
|
+ var steps = j.steps != null ? String(j.steps) : "";
|
|
|
+ if (!name && !steps) { return; }
|
|
|
+ journeys.push({ name: name, steps: steps });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ var headless = raw.headless != null ? String(raw.headless) : "";
|
|
|
+ if (!journeys.length && !headless) { return null; }
|
|
|
+ return { journeys: journeys, headless: headless };
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderOverview(data) {
|
|
|
+ els.subject.textContent = data.subject;
|
|
|
+ els.generated.textContent = data.generated;
|
|
|
+ els.schema.textContent = String(data.schema_version);
|
|
|
+ els.verdict.textContent = data.verdict;
|
|
|
+
|
|
|
+ if (data.grade) {
|
|
|
+ els.grade.textContent = data.grade;
|
|
|
+ els.grade.className = "grade g-" + data.grade;
|
|
|
+ els.grade.hidden = false;
|
|
|
+ }
|
|
|
+ if (data.summary) {
|
|
|
+ els.summaryText.textContent = data.summary;
|
|
|
+ els.summaryText.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ els.counts.innerHTML = "";
|
|
|
+ SEVERITIES.forEach(function (s) {
|
|
|
+ var pill = document.createElement("span");
|
|
|
+ pill.className = "pill";
|
|
|
+ pill.innerHTML =
|
|
|
+ '<span class="dot ' + s + '"></span>' +
|
|
|
+ '<span class="lbl">' + SEV_LABEL[s] + '</span>' +
|
|
|
+ '<span class="n">' + data.counts[s] + "</span>";
|
|
|
+ els.counts.appendChild(pill);
|
|
|
+ });
|
|
|
+ els.overview.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Each agent-block renderer leaves its section hidden when its data is null,
|
|
|
+ // so a stateless agent (no sanctum) or a minimal island never shows a blank
|
|
|
+ // panel and never throws.
|
|
|
+ function renderProfile(profile) {
|
|
|
+ if (!profile) { els.portrait.hidden = true; return; }
|
|
|
+ var typeTag = profile.agent_type
|
|
|
+ ? '<span class="type">' + esc(profile.agent_type) + "</span>"
|
|
|
+ : "";
|
|
|
+ var mission = profile.mission
|
|
|
+ ? '<div class="mission">' + esc(profile.mission) + "</div>"
|
|
|
+ : "";
|
|
|
+ els.portrait.innerHTML =
|
|
|
+ '<div class="icon">' + (profile.icon ? esc(profile.icon) : "🤖") + "</div>" +
|
|
|
+ '<div class="who">' +
|
|
|
+ '<div class="name">' + esc(profile.name || "(unnamed agent)") + "</div>" +
|
|
|
+ (profile.title ? '<div class="title">' + esc(profile.title) + "</div>" : "") +
|
|
|
+ mission +
|
|
|
+ typeTag +
|
|
|
+ "</div>";
|
|
|
+ els.portrait.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderCapabilities(list) {
|
|
|
+ if (!list) { els.capabilities.hidden = true; return; }
|
|
|
+ var items = list.map(function (c) {
|
|
|
+ return "<li>" +
|
|
|
+ '<span class="cap-name">' + esc(c.name) + "</span>" +
|
|
|
+ (c.kind ? '<span class="cap-kind">' + esc(c.kind) + "</span>" : "") +
|
|
|
+ (c.note ? '<span class="cap-note">' + esc(c.note) + "</span>" : "") +
|
|
|
+ "</li>";
|
|
|
+ }).join("");
|
|
|
+ els.capabilities.innerHTML =
|
|
|
+ "<h2>Capabilities</h2><ul class=\"cap-list\">" + items + "</ul>";
|
|
|
+ els.capabilities.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderLensVerdicts(entries) {
|
|
|
+ if (!entries) { els.lensVerdicts.hidden = true; return; }
|
|
|
+ var rows = entries.map(function (e) {
|
|
|
+ return "<dt>" + esc(e.lens) + "</dt><dd>" + esc(e.verdict) + "</dd>";
|
|
|
+ }).join("");
|
|
|
+ els.lensVerdicts.innerHTML =
|
|
|
+ "<h2>Per-lens verdicts</h2><dl class=\"kv\">" + rows + "</dl>";
|
|
|
+ els.lensVerdicts.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderSanctum(s) {
|
|
|
+ if (!s) { els.sanctum.hidden = true; return; }
|
|
|
+ var rows = "";
|
|
|
+ if (s.location) {
|
|
|
+ rows += "<dt>Location</dt><dd><code>" + esc(s.location) + "</code></dd>";
|
|
|
+ }
|
|
|
+ if (s.files.length) {
|
|
|
+ rows += "<dt>Sanctum files</dt><dd>" +
|
|
|
+ s.files.map(function (f) { return '<span class="mono">' + esc(f) + "</span>"; }).join(" ") +
|
|
|
+ "</dd>";
|
|
|
+ }
|
|
|
+ if (s.note) {
|
|
|
+ rows += "<dt>Note</dt><dd>" + esc(s.note) + "</dd>";
|
|
|
+ }
|
|
|
+ els.sanctum.innerHTML = "<h2>Sanctum (runtime memory)</h2><dl class=\"kv\">" + rows + "</dl>";
|
|
|
+ els.sanctum.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderExperience(exp) {
|
|
|
+ if (!exp) { els.experience.hidden = true; return; }
|
|
|
+ var html = "<h2>Experience</h2>";
|
|
|
+ if (exp.journeys.length) {
|
|
|
+ html += exp.journeys.map(function (j) {
|
|
|
+ return '<div class="journey">' +
|
|
|
+ '<div class="j-name">' + esc(j.name || "(unnamed journey)") + "</div>" +
|
|
|
+ (j.steps ? '<div class="j-steps">' + esc(j.steps) + "</div>" : "") +
|
|
|
+ "</div>";
|
|
|
+ }).join("");
|
|
|
+ }
|
|
|
+ if (exp.headless) {
|
|
|
+ html += '<dl class="kv" style="margin-top:12px"><dt>Headless</dt><dd>' +
|
|
|
+ esc(exp.headless) + "</dd></dl>";
|
|
|
+ }
|
|
|
+ els.experience.innerHTML = html;
|
|
|
+ els.experience.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Every copied fix prompt opens by anchoring the fixing session to the same
|
|
|
+ // standards that produced the findings, so the fix is held to the bar too.
|
|
|
+ function standardsPreamble() {
|
|
|
+ if (!standards || !standards.canon) { return []; }
|
|
|
+ var bar = standards.canon + (standards.principles ? " and " + standards.principles : "");
|
|
|
+ var lines = [
|
|
|
+ "Hold " + bar + " as the bar for every line you change — a fix that adds ceremony is a new finding, not a fix."
|
|
|
+ ];
|
|
|
+ if (standards.scripts) {
|
|
|
+ lines.push("If the fix adds or changes scripts, follow " + standards.scripts + ".");
|
|
|
+ }
|
|
|
+ lines.push("");
|
|
|
+ return lines;
|
|
|
+ }
|
|
|
+
|
|
|
+ function composeThemePrompt(theme, resolved) {
|
|
|
+ var lines = standardsPreamble();
|
|
|
+ lines.push("Fix the following theme in " + subjectPath + ": " + theme.title);
|
|
|
+ lines.push("");
|
|
|
+ if (theme.root_cause) { lines.push("Root cause: " + theme.root_cause); }
|
|
|
+ if (theme.action) { lines.push("Fix: " + theme.action); }
|
|
|
+ if (resolved.length) {
|
|
|
+ lines.push("");
|
|
|
+ lines.push("Findings to address:");
|
|
|
+ resolved.forEach(function (f, i) {
|
|
|
+ lines.push((i + 1) + ". " + f.title);
|
|
|
+ if (f.location) { lines.push(" Location: " + f.location); }
|
|
|
+ if (f.evidence) { lines.push(" Evidence: " + f.evidence); }
|
|
|
+ if (f.recommendation) { lines.push(" Recommendation: " + f.recommendation); }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return lines.join("\n") + "\n";
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderThemes(themes) {
|
|
|
+ if (!themes.length) { els.themes.hidden = true; return; }
|
|
|
+ els.themes.innerHTML = "<h2>Themes</h2>";
|
|
|
+ themes.forEach(function (t) {
|
|
|
+ var resolved = t.finding_ids
|
|
|
+ .map(function (id) { return findingsById[id]; })
|
|
|
+ .filter(function (f) { return !!f; });
|
|
|
+ var items = resolved.map(function (f) {
|
|
|
+ return '<div class="t-finding"><span class="mono">' + esc(f.id) + "</span> " +
|
|
|
+ esc(f.title) +
|
|
|
+ (f.location ? ' · <span class="mono">' + esc(f.location) + "</span>" : "") +
|
|
|
+ "</div>";
|
|
|
+ }).join("");
|
|
|
+
|
|
|
+ var node = document.createElement("div");
|
|
|
+ node.className = "theme";
|
|
|
+ node.innerHTML =
|
|
|
+ '<div class="t-head"><span class="t-title">' + esc(t.title) + "</span>" +
|
|
|
+ '<button class="small t-fix">Fix This Theme</button></div>' +
|
|
|
+ (t.root_cause ? '<div class="t-cause">Root cause: ' + esc(t.root_cause) + "</div>" : "") +
|
|
|
+ (t.action ? '<div class="t-action"><b>Fix:</b> ' + esc(t.action) + "</div>" : "") +
|
|
|
+ (items ? '<div class="t-findings">' + items + "</div>" : "");
|
|
|
+ node.querySelector(".t-fix").addEventListener("click", function () {
|
|
|
+ copyText(composeThemePrompt(t, resolved));
|
|
|
+ });
|
|
|
+ els.themes.appendChild(node);
|
|
|
+ });
|
|
|
+ els.themes.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderStrengths(list) {
|
|
|
+ if (!list.length) { els.strengths.hidden = true; return; }
|
|
|
+ els.strengths.innerHTML =
|
|
|
+ "<h2>Strengths</h2><ul class=\"strength-list\">" +
|
|
|
+ list.map(function (s) { return "<li>" + esc(s) + "</li>"; }).join("") +
|
|
|
+ "</ul>";
|
|
|
+ els.strengths.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderRecommendations(recs) {
|
|
|
+ if (!recs.length) { els.recommendations.hidden = true; return; }
|
|
|
+ var html = "<h2>Recommendations</h2>";
|
|
|
+ recs.forEach(function (r) {
|
|
|
+ html += '<div class="rec"><span class="rank">#' + esc(String(r.rank)) + "</span>" +
|
|
|
+ esc(r.action) +
|
|
|
+ (r.resolves ? '<span class="resolves">resolves: ' + esc(r.resolves) + "</span>" : "") +
|
|
|
+ "</div>";
|
|
|
+ });
|
|
|
+ els.recommendations.innerHTML = html;
|
|
|
+ els.recommendations.hidden = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderNoFindings() {
|
|
|
+ els.root.innerHTML =
|
|
|
+ '<div class="no-findings">' +
|
|
|
+ '<div class="big">No findings</div>' +
|
|
|
+ "<div>The scanners returned a clean pass for this subject.</div>" +
|
|
|
+ "</div>";
|
|
|
+ }
|
|
|
+
|
|
|
+ function findingNode(f) {
|
|
|
+ var node = document.createElement("div");
|
|
|
+ node.className = "finding sev-" + f.severity;
|
|
|
+ node.setAttribute("data-id", f.id);
|
|
|
+
|
|
|
+ var sub =
|
|
|
+ esc(f.lens) +
|
|
|
+ (f.location ? ' · <span class="mono">' + esc(f.location) + "</span>" : "");
|
|
|
+
|
|
|
+ var rows =
|
|
|
+ "<dt>Lens</dt><dd>" + esc(f.lens) + "</dd>" +
|
|
|
+ (f.location ? "<dt>Location</dt><dd><code>" + esc(f.location) + "</code></dd>" : "") +
|
|
|
+ (f.evidence ? "<dt>Evidence</dt><dd>" + esc(f.evidence) + "</dd>" : "") +
|
|
|
+ (f.recommendation ? "<dt>Recommendation</dt><dd>" + esc(f.recommendation) + "</dd>" : "") +
|
|
|
+ (f.proposed_smallest ? "<dt>Proposed smallest</dt><dd>" + esc(f.proposed_smallest) + "</dd>" : "") +
|
|
|
+ (f.predicted_delta ? "<dt>Predicted delta</dt><dd>" + esc(f.predicted_delta) + "</dd>" : "");
|
|
|
+
|
|
|
+ node.innerHTML =
|
|
|
+ '<div class="row">' +
|
|
|
+ '<input type="checkbox" class="chk" aria-label="Select finding">' +
|
|
|
+ '<div class="head">' +
|
|
|
+ '<div class="title">' + esc(f.title) + "</div>" +
|
|
|
+ '<div class="sub">' + sub + "</div>" +
|
|
|
+ "</div>" +
|
|
|
+ '<span class="tag">' + SEV_LABEL[f.severity] + "</span>" +
|
|
|
+ '<span class="caret">▸</span>' +
|
|
|
+ "</div>" +
|
|
|
+ '<div class="body"><dl>' + rows + "</dl></div>";
|
|
|
+
|
|
|
+ var chk = node.querySelector(".chk");
|
|
|
+ chk.checked = !!selected[f.id];
|
|
|
+ chk.addEventListener("change", function () {
|
|
|
+ if (chk.checked) { selected[f.id] = true; } else { delete selected[f.id]; }
|
|
|
+ updateSelection();
|
|
|
+ });
|
|
|
+
|
|
|
+ var head = node.querySelector(".head");
|
|
|
+ var caret = node.querySelector(".caret");
|
|
|
+ function toggle() { node.classList.toggle("open"); }
|
|
|
+ head.addEventListener("click", toggle);
|
|
|
+ caret.addEventListener("click", toggle);
|
|
|
+
|
|
|
+ return node;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderFindings(list) {
|
|
|
+ els.root.innerHTML = "";
|
|
|
+ if (list.length === 0) {
|
|
|
+ renderNoFindings();
|
|
|
+ els.toolbar.hidden = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ els.toolbar.hidden = false;
|
|
|
+
|
|
|
+ SEVERITIES.forEach(function (sev) {
|
|
|
+ var group = list.filter(function (f) { return f.severity === sev; });
|
|
|
+ if (group.length === 0) { return; }
|
|
|
+ var wrap = document.createElement("div");
|
|
|
+ wrap.className = "group";
|
|
|
+ var h = document.createElement("h2");
|
|
|
+ h.innerHTML = '<span class="dot ' + sev + '"></span>' + SEV_LABEL[sev] + " (" + group.length + ")";
|
|
|
+ wrap.appendChild(h);
|
|
|
+ group.forEach(function (f) { wrap.appendChild(findingNode(f)); });
|
|
|
+ els.root.appendChild(wrap);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateSelection() {
|
|
|
+ var n = Object.keys(selected).length;
|
|
|
+ els.selCount.textContent = n + " selected";
|
|
|
+ els.btnCopy.disabled = n === 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ function composePrompt() {
|
|
|
+ var picked = findings.filter(function (f) { return selected[f.id]; });
|
|
|
+ if (picked.length === 0) { return ""; }
|
|
|
+ var lines = standardsPreamble();
|
|
|
+ lines.push("Fix the following issues in " + subjectPath + ":");
|
|
|
+ lines.push("");
|
|
|
+ picked.forEach(function (f, i) {
|
|
|
+ lines.push((i + 1) + ". " + f.title);
|
|
|
+ if (f.location) { lines.push(" Location: " + f.location); }
|
|
|
+ if (f.evidence) { lines.push(" Evidence: " + f.evidence); }
|
|
|
+ if (f.recommendation) { lines.push(" Recommendation: " + f.recommendation); }
|
|
|
+ if (f.proposed_smallest) { lines.push(" Proposed smallest: " + f.proposed_smallest); }
|
|
|
+ lines.push("");
|
|
|
+ });
|
|
|
+ return lines.join("\n").replace(/\n+$/, "\n");
|
|
|
+ }
|
|
|
+
|
|
|
+ function showToast(text) {
|
|
|
+ els.toast.textContent = text;
|
|
|
+ els.toast.classList.add("show");
|
|
|
+ setTimeout(function () { els.toast.classList.remove("show"); }, 1600);
|
|
|
+ }
|
|
|
+
|
|
|
+ function fallbackCopy(text) {
|
|
|
+ els.fallbackText.value = text;
|
|
|
+ els.fallback.classList.add("show");
|
|
|
+ els.fallbackText.focus();
|
|
|
+ els.fallbackText.select();
|
|
|
+ try {
|
|
|
+ var ok = document.execCommand && document.execCommand("copy");
|
|
|
+ if (ok) {
|
|
|
+ showToast("Copied");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (e) { /* fall through to manual */ }
|
|
|
+ showToast("Copy the text shown below");
|
|
|
+ }
|
|
|
+
|
|
|
+ function copyText(text) {
|
|
|
+ if (!text) { return; }
|
|
|
+ if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
|
+ navigator.clipboard.writeText(text).then(
|
|
|
+ function () { showToast("Copied"); },
|
|
|
+ function () { fallbackCopy(text); }
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ fallbackCopy(text);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function doCopy() {
|
|
|
+ copyText(composePrompt());
|
|
|
+ }
|
|
|
+
|
|
|
+ function wireToolbar() {
|
|
|
+ els.btnCopy.addEventListener("click", doCopy);
|
|
|
+ els.btnSelectAll.addEventListener("click", function () {
|
|
|
+ findings.forEach(function (f) { selected[f.id] = true; });
|
|
|
+ document.querySelectorAll(".finding .chk").forEach(function (c) { c.checked = true; });
|
|
|
+ updateSelection();
|
|
|
+ });
|
|
|
+ els.btnClear.addEventListener("click", function () {
|
|
|
+ selected = Object.create(null);
|
|
|
+ document.querySelectorAll(".finding .chk").forEach(function (c) { c.checked = false; });
|
|
|
+ els.fallback.classList.remove("show");
|
|
|
+ updateSelection();
|
|
|
+ });
|
|
|
+ els.btnExpandAll.addEventListener("click", function () {
|
|
|
+ document.querySelectorAll(".finding").forEach(function (n) { n.classList.add("open"); });
|
|
|
+ });
|
|
|
+ els.btnCollapseAll.addEventListener("click", function () {
|
|
|
+ document.querySelectorAll(".finding").forEach(function (n) { n.classList.remove("open"); });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function init() {
|
|
|
+ var island = document.getElementById("report-data");
|
|
|
+ var parsed;
|
|
|
+ try {
|
|
|
+ if (!island) { throw new Error("report-data island element not found"); }
|
|
|
+ parsed = JSON.parse(island.textContent);
|
|
|
+ } catch (err) {
|
|
|
+ showBanner(
|
|
|
+ "Could not parse the report data island.\n\n" +
|
|
|
+ "Error: " + (err && err.message ? err.message : String(err)) + "\n\n" +
|
|
|
+ "The findings could not be rendered. The JSON inside the " +
|
|
|
+ 'report-data island (the application/json script tag) is malformed.'
|
|
|
+ );
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var data = normalize(parsed);
|
|
|
+
|
|
|
+ if (data.subject === PLACEHOLDER_SUBJECT) {
|
|
|
+ els.subject.textContent = data.subject;
|
|
|
+ showBanner(
|
|
|
+ "This is the unfilled report shell.\n\n" +
|
|
|
+ "The report-data island still carries the placeholder subject, so " +
|
|
|
+ "there are no findings here. Generate a real report with " +
|
|
|
+ "scripts/render_report.py."
|
|
|
+ );
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ findings = data.findings;
|
|
|
+ subjectPath = data.subject;
|
|
|
+ standards = data.standards;
|
|
|
+ findingsById = Object.create(null);
|
|
|
+ findings.forEach(function (f) { findingsById[f.id] = f; });
|
|
|
+
|
|
|
+ renderProfile(data.agent_profile);
|
|
|
+ renderOverview(data);
|
|
|
+ renderCapabilities(data.capabilities);
|
|
|
+ renderLensVerdicts(data.detailed_analysis);
|
|
|
+ renderSanctum(data.sanctum);
|
|
|
+ renderExperience(data.experience);
|
|
|
+ renderThemes(data.themes);
|
|
|
+ renderStrengths(data.strengths);
|
|
|
+ renderRecommendations(data.recommendations);
|
|
|
+ renderFindings(findings);
|
|
|
+ wireToolbar();
|
|
|
+ updateSelection();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (document.readyState === "loading") {
|
|
|
+ document.addEventListener("DOMContentLoaded", init);
|
|
|
+ } else {
|
|
|
+ init();
|
|
|
+ }
|
|
|
+})();
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|