@shield-agent/kya 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +1 -1
- package/README.md +6 -4
- package/amp/settings.example.json +1 -1
- package/claude/claude_desktop_config.example.json +1 -1
- package/cline/cline_mcp_settings.example.json +1 -1
- package/copilot/mcp-config.example.json +1 -1
- package/dist/certify/render.d.ts +6 -2
- package/dist/certify/render.d.ts.map +1 -1
- package/dist/certify/render.js +7 -6
- package/dist/certify/render.js.map +1 -1
- package/dist/receipt/enrich.d.ts +12 -4
- package/dist/receipt/enrich.d.ts.map +1 -1
- package/dist/receipt/enrich.js +2 -1
- package/dist/receipt/enrich.js.map +1 -1
- package/dist/receipt/receipt-css.d.ts +17 -0
- package/dist/receipt/receipt-css.d.ts.map +1 -0
- package/dist/receipt/receipt-css.js +542 -0
- package/dist/receipt/receipt-css.js.map +1 -0
- package/dist/receipt/render-receipt.d.ts.map +1 -1
- package/dist/receipt/render-receipt.js +286 -427
- package/dist/receipt/render-receipt.js.map +1 -1
- package/droid/README.md +1 -1
- package/droid/mcp.example.json +1 -1
- package/gemini/settings.example.json +1 -1
- package/grok/README.md +1 -1
- package/kilo/kilo.example.json +1 -1
- package/kimi/mcp.example.json +1 -1
- package/kiro/mcp.example.json +1 -1
- package/manifest.json +1 -1
- package/mastracode/mcp.example.json +1 -1
- package/mcp.json +1 -1
- package/openai/codex.config.example.toml +1 -1
- package/opencode/opencode.example.json +1 -1
- package/package.json +1 -1
- package/qwen/settings.example.json +1 -1
- package/server.json +2 -2
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { assertNoSecrets, clip, stripEscapes } from "../dash/render.js";
|
|
5
5
|
import { clipMultiline, DIFF_MAX_TOTAL_CHARS } from "../diff-preview.js";
|
|
6
|
+
import { DOMAIN_LABELS, domainOrder } from "../certify/render.js";
|
|
6
7
|
import { productLabel, readTrail, readTrailSince } from "../trail.js";
|
|
7
8
|
import { loadCertifyCard, loadIdentity, loadOrrCard, loadSandboxes, loadShowbackCard, loadWiredHosts, } from "./enrich.js";
|
|
8
9
|
import { SHOWBACK_DISCLAIMER, } from "../showback/cost-per-task.js";
|
|
9
10
|
import { computeDashboard } from "./dashboard.js";
|
|
11
|
+
import { receiptCss } from "./receipt-css.js";
|
|
10
12
|
function esc(s) {
|
|
11
13
|
return stripEscapes(s)
|
|
12
14
|
.replace(/&/g, "&")
|
|
@@ -264,8 +266,8 @@ function projectMeta(project) {
|
|
|
264
266
|
function renderFeed(events, nowMs, live) {
|
|
265
267
|
if (events.length === 0) {
|
|
266
268
|
const hint = live
|
|
267
|
-
? "No events yet
|
|
268
|
-
: "No events yet
|
|
269
|
+
? "No events yet — evaluate a tool call with <code>kya wrap</code> and this page will refresh."
|
|
270
|
+
: "No events yet — evaluate a tool call with <code>kya wrap</code>, then regenerate this receipt.";
|
|
269
271
|
return `<p class="empty">${hint}</p>`;
|
|
270
272
|
}
|
|
271
273
|
const sorted = [...events].sort((a, b) => b.ts.localeCompare(a.ts));
|
|
@@ -541,39 +543,191 @@ function orrPanel(orr, nowMs) {
|
|
|
541
543
|
<p class="mute small">scorecards: ${orr.scorecards.pass} pass · ${orr.scorecards.fail} fail · ${orr.scorecards.partial} partial · ${orr.scorecards.notEvaluated} n/e${when}</p>
|
|
542
544
|
</section>`;
|
|
543
545
|
}
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
546
|
+
function usdText(usd) {
|
|
547
|
+
return usd === null ? "USD n/a" : `~$${usd.toFixed(2)}`;
|
|
548
|
+
}
|
|
549
|
+
/** KPI tile: big tabular number over a small caps label. The tone class is
|
|
550
|
+
* only applied above zero — a zero count never reads as a signal. */
|
|
551
|
+
function kpiTile(label, value, cls = "") {
|
|
552
|
+
const v = typeof value === "number" ? value : esc(value);
|
|
553
|
+
const toned = typeof value === "number" && value > 0 && cls ? ` ${cls}` : "";
|
|
554
|
+
return `<div class="kpi"><span class="kpi-num${toned}">${v}</span><span class="kpi-lab">${label}</span></div>`;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Hero Certify panel: purpose-built rich card for the live Agent Trust
|
|
558
|
+
* Baseline. The section carries the result state (`hero-certify pass|gap|
|
|
559
|
+
* none`) for the accent border; an absent card is a fail-closed neutral
|
|
560
|
+
* state that must never read as a pass.
|
|
561
|
+
*/
|
|
562
|
+
function heroCertifyPanel(certify) {
|
|
563
|
+
if (!certify) {
|
|
564
|
+
return `<section class="panel hero-certify none" aria-label="Certify">
|
|
565
|
+
<h2>Certify — Agent Trust Baseline</h2>
|
|
566
|
+
<div class="orrline">
|
|
567
|
+
<span class="pill">not evaluated</span>
|
|
568
|
+
</div>
|
|
569
|
+
<p class="mute small">No live evaluation available — wrap tool calls and run kya certify to build the baseline.</p>
|
|
570
|
+
</section>`;
|
|
571
|
+
}
|
|
572
|
+
const state = certify.result === "pass" ? "pass" : "gap";
|
|
573
|
+
// Status legend: one muted line under the count tiles so the four numbers
|
|
574
|
+
// read as definitions, not bare counters.
|
|
575
|
+
const legend = `<p class="legend">gap = requirement failing — your work plan · insufficient = not enough local evidence to evaluate (never counts as pass) · attested = your signed statement, unverified</p>`;
|
|
576
|
+
// Attested summary: the hero's gap rows are gap-status only, so attestations
|
|
577
|
+
// surface as one line naming the first attested requirement (+N more) with
|
|
578
|
+
// its attestation text.
|
|
579
|
+
const firstAttested = certify.requirements.find((r) => r.status === "attested");
|
|
580
|
+
const attestedLine = certify.attested === 0
|
|
581
|
+
? ""
|
|
582
|
+
: firstAttested
|
|
583
|
+
? `<p class="att-line">attested: <code>${esc(firstAttested.id)}</code>${certify.attested > 1 ? ` (+${certify.attested - 1} more)` : ""} — "${esc(clip(firstAttested.attestation?.text ?? firstAttested.evidence, 100))}"</p>`
|
|
584
|
+
: `<p class="att-line">attested: ${certify.attested} — your signed statement, unverified</p>`;
|
|
585
|
+
// topGaps arrives severity-ordered from the loader; cap defensively at 5.
|
|
586
|
+
// Each row is id — title + severity chip, with the redacted evidence
|
|
587
|
+
// one-liner beneath (clipped so the hero stays compact).
|
|
555
588
|
const gaps = certify.topGaps.length === 0
|
|
556
589
|
? ""
|
|
557
590
|
: `<ul class="rows">
|
|
558
591
|
${certify.topGaps
|
|
559
|
-
.
|
|
592
|
+
.slice(0, 5)
|
|
593
|
+
.map((g) => ` <li><code>${esc(g.id)}</code> — ${esc(g.title)} <span class="sev ${esc(g.severity)}">${esc(g.severity)}</span>${g.evidence ? `<span class="gap-ev">${esc(clip(g.evidence, 140))}</span>` : ""}</li>`)
|
|
560
594
|
.join("\n")}
|
|
561
595
|
</ul>`;
|
|
562
|
-
// Pill tones reuse the ORR
|
|
563
|
-
return `<section class="panel" aria-label="Certify">
|
|
596
|
+
// Pill tones reuse the ORR palette: pass is green, gap is amber.
|
|
597
|
+
return `<section class="panel hero-certify ${state}" aria-label="Certify">
|
|
564
598
|
<h2>Certify — Agent Trust Baseline</h2>
|
|
565
599
|
<div class="orrline">
|
|
566
|
-
<span class="pill orr-${
|
|
600
|
+
<span class="pill orr-${state === "pass" ? "green" : "amber"}">${esc(certify.result)}</span>
|
|
567
601
|
</div>
|
|
568
|
-
<div class="
|
|
602
|
+
<div class="kpi-row">
|
|
603
|
+
${kpiTile("Pass", certify.pass, "ok")}
|
|
604
|
+
${kpiTile("Gap", certify.gap, "warn")}
|
|
605
|
+
${kpiTile("Insufficient", certify.insufficientEvidence, "mute")}
|
|
606
|
+
${kpiTile("Attested", certify.attested, "mute")}
|
|
607
|
+
</div>
|
|
608
|
+
${legend}
|
|
609
|
+
${attestedLine}
|
|
569
610
|
${gaps}
|
|
570
|
-
<p class="mute small">live evaluation — window ${certify.windowDays}d, ${certify.trailEvents} trail events · kya certify for the
|
|
611
|
+
<p class="mute small">live evaluation — window ${certify.windowDays}d, ${certify.trailEvents} trail events · the Certify tab has the full live requirement table · kya certify for the gap report + signed bundle</p>
|
|
571
612
|
</section>`;
|
|
572
613
|
}
|
|
573
|
-
|
|
574
|
-
|
|
614
|
+
/** Detail-table pill tones, reusing the existing pill/tone system: pass is
|
|
615
|
+
* green, gap is amber, insufficient evidence keeps the neutral mute default,
|
|
616
|
+
* attested gets the neutral-foreground .att tone. */
|
|
617
|
+
const DETAIL_PILL_TONE = {
|
|
618
|
+
pass: "ok",
|
|
619
|
+
gap: "orr-amber",
|
|
620
|
+
insufficient_evidence: "",
|
|
621
|
+
attested: "att",
|
|
622
|
+
};
|
|
623
|
+
const CERTIFY_DETAIL_TITLE_CLIP = 120;
|
|
624
|
+
const CERTIFY_DETAIL_CLIP = 160;
|
|
625
|
+
/**
|
|
626
|
+
* Certify tab body: the full live requirement table — every evaluated
|
|
627
|
+
* requirement grouped by domain, domains in catalog order (the card's
|
|
628
|
+
* requirements arrive in catalog order, so first-seen order IS the catalog
|
|
629
|
+
* order). Each domain gets a heading row with mini-counts, then one row per
|
|
630
|
+
* requirement: status pill, id, title, and a muted evidence line beneath —
|
|
631
|
+
* or the attestation text for attested rows. Empty input yields "" so the
|
|
632
|
+
* caller falls back to the zone-empty hint (fail-closed: no table without
|
|
633
|
+
* data).
|
|
634
|
+
*/
|
|
635
|
+
function certifyDetailPanel(requirements) {
|
|
636
|
+
if (requirements.length === 0)
|
|
637
|
+
return "";
|
|
638
|
+
const sections = domainOrder(requirements)
|
|
639
|
+
.map((domain) => {
|
|
640
|
+
const rows = requirements.filter((r) => r.domain === domain);
|
|
641
|
+
const counts = { pass: 0, gap: 0, insufficient: 0, attested: 0 };
|
|
642
|
+
for (const r of rows) {
|
|
643
|
+
if (r.status === "pass")
|
|
644
|
+
counts.pass++;
|
|
645
|
+
else if (r.status === "gap")
|
|
646
|
+
counts.gap++;
|
|
647
|
+
else if (r.status === "attested")
|
|
648
|
+
counts.attested++;
|
|
649
|
+
else
|
|
650
|
+
counts.insufficient++;
|
|
651
|
+
}
|
|
652
|
+
const items = rows
|
|
653
|
+
.map((r) => {
|
|
654
|
+
const tone = DETAIL_PILL_TONE[r.status];
|
|
655
|
+
const pill = `<span class="pill${tone ? ` ${tone}` : ""}">${esc(r.status.replace(/_/g, " "))}</span>`;
|
|
656
|
+
const detail = r.attestation
|
|
657
|
+
? `<span class="att">attested ${esc(r.attestation.at)} — "${esc(clip(r.attestation.text, CERTIFY_DETAIL_CLIP))}"</span>`
|
|
658
|
+
: r.evidence
|
|
659
|
+
? `<span class="evi">${esc(clip(r.evidence, CERTIFY_DETAIL_CLIP))}</span>`
|
|
660
|
+
: "";
|
|
661
|
+
return ` <li>${pill} <code>${esc(r.id)}</code> — ${esc(clip(r.title, CERTIFY_DETAIL_TITLE_CLIP))}${detail}</li>`;
|
|
662
|
+
})
|
|
663
|
+
.join("\n");
|
|
664
|
+
const label = DOMAIN_LABELS[domain] ?? domain;
|
|
665
|
+
return ` <section class="dom" aria-label="${esc(label)}">
|
|
666
|
+
<div class="dom-head"><h3>${esc(label)}</h3><span class="cnt">${counts.pass} pass · ${counts.gap} gap · ${counts.insufficient} insufficient · ${counts.attested} attested</span></div>
|
|
667
|
+
<ul class="rows">
|
|
668
|
+
${items}
|
|
669
|
+
</ul>
|
|
670
|
+
</section>`;
|
|
671
|
+
})
|
|
672
|
+
.join("\n");
|
|
673
|
+
return `<section class="panel certify-detail" aria-label="Certify detail">
|
|
674
|
+
<h2>Every requirement — live evaluation</h2>
|
|
675
|
+
${sections}
|
|
676
|
+
</section>`;
|
|
677
|
+
}
|
|
678
|
+
/** Hero KPI cards: compact, non-interactive rollups (filters live in the
|
|
679
|
+
* Activity zone's chip bar). Numbers keep tabular-nums via .mix-num/.kpi-num. */
|
|
680
|
+
function verdictsHeroCard(c, total) {
|
|
681
|
+
// Base 100% = total events, matching dashboard.ts's verdictMix contract:
|
|
682
|
+
// verdict is a free-form string from untrusted JSONL, so an event may land
|
|
683
|
+
// in no bucket (bogus verdict) while still counting in `never` — only the
|
|
684
|
+
// raw event count keeps every bar inside the base the Overview card uses.
|
|
685
|
+
const pct = (n) => (total > 0 ? Math.round((n / total) * 100) : 0);
|
|
686
|
+
const mixRow = (label, n, fill) => `<div class="mix-row"><span class="mix-lab">${label}</span><span class="mix-bar"><span class="mix-fill ${fill}" style="width:${pct(n)}%"></span></span><span class="mix-num">${n}</span></div>`;
|
|
687
|
+
return `<section class="panel" aria-label="Verdicts">
|
|
688
|
+
<h2>Verdicts</h2>
|
|
689
|
+
<div class="mix">
|
|
690
|
+
${mixRow("Allow", c.allow, "ok")}
|
|
691
|
+
${mixRow("Deny", c.deny, "bad")}
|
|
692
|
+
${mixRow("Review", c.require, "warn")}
|
|
693
|
+
${mixRow("Never", c.never, "bad")}
|
|
694
|
+
</div>
|
|
695
|
+
</section>`;
|
|
696
|
+
}
|
|
697
|
+
const SPARK_BARS = 7;
|
|
698
|
+
const SPARK_H = 24;
|
|
699
|
+
/**
|
|
700
|
+
* Inline 7-bar sparkline (no JS): the last 7 activity buckets, left-padded
|
|
701
|
+
* with zeros, heights normalized so the max bucket is full height. Empty
|
|
702
|
+
* input renders a flat baseline — heights are guarded, never NaN/Infinity.
|
|
703
|
+
*/
|
|
704
|
+
function activitySparkline(activity) {
|
|
705
|
+
const counts = activity.buckets.slice(-SPARK_BARS).map((b) => b.count);
|
|
706
|
+
while (counts.length < SPARK_BARS)
|
|
707
|
+
counts.unshift(0);
|
|
708
|
+
const max = Math.max(0, ...counts);
|
|
709
|
+
const rects = counts
|
|
710
|
+
.map((n, i) => {
|
|
711
|
+
const h = max > 0 ? Math.round((n / max) * SPARK_H) : 0;
|
|
712
|
+
return `<rect x="${i * 10}" y="${SPARK_H - h}" width="8" height="${h}" rx="1"></rect>`;
|
|
713
|
+
})
|
|
714
|
+
.join("");
|
|
715
|
+
const unit = activity.granularity === "hour" ? "hours" : "days";
|
|
716
|
+
return `<svg class="spark" viewBox="0 0 68 ${SPARK_H}" role="img" aria-label="Activity, last ${SPARK_BARS} ${unit}">${rects}<line class="base" x1="0" y1="${SPARK_H - 0.5}" x2="68" y2="${SPARK_H - 0.5}"></line></svg>`;
|
|
575
717
|
}
|
|
576
|
-
function
|
|
718
|
+
function activityHeroCard(agg, db, total) {
|
|
719
|
+
return `<section class="panel" aria-label="Activity">
|
|
720
|
+
<h2>Activity</h2>
|
|
721
|
+
<div class="kpi-row">
|
|
722
|
+
${kpiTile("Events", total)}
|
|
723
|
+
</div>
|
|
724
|
+
${activitySparkline(db.activity)}
|
|
725
|
+
<p class="mute small">observe ${agg.modes.observe} · hold ${agg.modes.hold} · offline ${agg.modes.offline} · IDE ${agg.planes.ide} · runtime ${agg.planes.runtime}</p>
|
|
726
|
+
</section>`;
|
|
727
|
+
}
|
|
728
|
+
/** Hero-styled showback: same fields as the old panel (tokens in/out, est.
|
|
729
|
+
* USD, top runs, disclaimer), omitted entirely when no report exists. */
|
|
730
|
+
function showbackHeroCard(report) {
|
|
577
731
|
if (!report)
|
|
578
732
|
return "";
|
|
579
733
|
const topRuns = [...report.perRun]
|
|
@@ -588,7 +742,11 @@ ${topRuns
|
|
|
588
742
|
</ul>`;
|
|
589
743
|
return `<section class="panel" aria-label="Showback">
|
|
590
744
|
<h2>Showback (observe only)</h2>
|
|
591
|
-
<
|
|
745
|
+
<div class="kpi-row">
|
|
746
|
+
${kpiTile("Tokens in", report.totalTokensIn)}
|
|
747
|
+
${kpiTile("Tokens out", report.totalTokensOut)}
|
|
748
|
+
${kpiTile("Est. USD", usdText(report.estimatedUsd))}
|
|
749
|
+
</div>
|
|
592
750
|
${runs}
|
|
593
751
|
<p class="mute small">${esc(SHOWBACK_DISCLAIMER)}</p>
|
|
594
752
|
</section>`;
|
|
@@ -644,6 +802,12 @@ export function renderReceiptHtml(model) {
|
|
|
644
802
|
// must never resolve via Object.prototype. The id lets tests extract just
|
|
645
803
|
// this script. KNOWN is space-delimited so `indexOf(' '+g+' ')` doubles as a
|
|
646
804
|
// prototype-safe whitelist ('constructor' etc. never match).
|
|
805
|
+
// Filter state persists in the ?f= QUERY param, not the hash: the hash is
|
|
806
|
+
// owned by the pure-CSS :target tabs (#overview/#certify/#activity/#system), so a
|
|
807
|
+
// hash-based filter would hide the Activity zone on every chip toggle and
|
|
808
|
+
// every tab click would wipe the filter. Legacy #f= hashes are still parsed
|
|
809
|
+
// on load (read-only); the next save writes the ?f= form and drops the
|
|
810
|
+
// legacy fragment.
|
|
647
811
|
const filterScript = `<script id="kya-filters">
|
|
648
812
|
(function(){
|
|
649
813
|
var feed = document.getElementById('feed');
|
|
@@ -654,9 +818,20 @@ export function renderReceiptHtml(model) {
|
|
|
654
818
|
var chips = document.querySelectorAll('[data-fgroup]');
|
|
655
819
|
function parse(){
|
|
656
820
|
state = Object.create(null);
|
|
657
|
-
var
|
|
658
|
-
|
|
659
|
-
|
|
821
|
+
var raw = null;
|
|
822
|
+
var s = location.search;
|
|
823
|
+
if (s.indexOf('?') === 0) {
|
|
824
|
+
var params = s.slice(1).split('&');
|
|
825
|
+
for (var i = 0; i < params.length; i++) {
|
|
826
|
+
if (params[i].indexOf('f=') === 0) { raw = params[i].slice(2); break; }
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (raw === null) {
|
|
830
|
+
var legacy = location.hash;
|
|
831
|
+
if (legacy.indexOf('#f=') === 0) raw = legacy.slice(3);
|
|
832
|
+
}
|
|
833
|
+
if (raw === null) return;
|
|
834
|
+
raw.split(',').forEach(function(seg){
|
|
660
835
|
var i = seg.indexOf(':');
|
|
661
836
|
if (i < 1) return;
|
|
662
837
|
var g = seg.slice(0, i);
|
|
@@ -672,8 +847,22 @@ export function renderReceiptHtml(model) {
|
|
|
672
847
|
Object.keys(state).forEach(function(g){
|
|
673
848
|
Object.keys(state[g]).forEach(function(v){ parts.push(g + ':' + encodeURIComponent(v)); });
|
|
674
849
|
});
|
|
675
|
-
|
|
676
|
-
|
|
850
|
+
// Rebuild the query rather than overwrite it: the live page carries its
|
|
851
|
+
// loopback token as ?t= and must not lose it on a chip toggle.
|
|
852
|
+
var kept = [];
|
|
853
|
+
var s = location.search;
|
|
854
|
+
if (s.indexOf('?') === 0) {
|
|
855
|
+
var params = s.slice(1).split('&');
|
|
856
|
+
for (var i = 0; i < params.length; i++) {
|
|
857
|
+
if (params[i] && params[i].indexOf('f=') !== 0) kept.push(params[i]);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (parts.length) kept.push('f=' + parts.join(','));
|
|
861
|
+
var q = kept.length ? '?' + kept.join('&') : '';
|
|
862
|
+
// The tab hash rides along untouched; a legacy #f= fragment is not a tab
|
|
863
|
+
// hash and is dropped now that the state lives in the query.
|
|
864
|
+
var h = location.hash.indexOf('#f=') === 0 ? '' : location.hash;
|
|
865
|
+
history.replaceState(null, '', location.pathname + q + h);
|
|
677
866
|
}
|
|
678
867
|
function active(){
|
|
679
868
|
return Object.keys(state).some(function(g){ return Object.keys(state[g]).length > 0; });
|
|
@@ -721,406 +910,76 @@ export function renderReceiptHtml(model) {
|
|
|
721
910
|
})();
|
|
722
911
|
</script>`;
|
|
723
912
|
const livePill = model.live ? `<span class="live" title="Watching trail.jsonl">Live</span>` : "";
|
|
913
|
+
// Zones never render as bare whitespace: with no panels they get a hint.
|
|
914
|
+
const zoneBody = (parts, empty) => parts.some((p) => p.length > 0)
|
|
915
|
+
? parts.filter(Boolean).join("\n ")
|
|
916
|
+
: `<p class="mute small zone-empty">${empty}</p>`;
|
|
917
|
+
const overviewBody = zoneBody([dashboardPanel(dashboard), sessionsPanel(agg, nowMs), reasonsPanel(agg)], "No activity in this window yet — analytics appear once events land.");
|
|
918
|
+
const certifyBody = zoneBody([model.certify ? certifyDetailPanel(model.certify.requirements) : ""], "No live evaluation yet — wrap tool calls and run kya certify to build the baseline.");
|
|
919
|
+
const systemBody = zoneBody([
|
|
920
|
+
wiredHostsPanel(model.wiredHosts),
|
|
921
|
+
sandboxesPanel(model.sandboxes, nowMs),
|
|
922
|
+
orrPanel(model.orr, nowMs),
|
|
923
|
+
toolsHtml,
|
|
924
|
+
], "No system state yet — wire a host, configure a sandbox, or run an ORR.");
|
|
724
925
|
const html = `<!DOCTYPE html>
|
|
725
926
|
<html lang="en">
|
|
726
927
|
<head>
|
|
727
928
|
<meta charset="utf-8"/>
|
|
728
929
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
729
930
|
<title>${esc(model.title)} - ${esc(model.rangeLabel)}</title>
|
|
730
|
-
<style>
|
|
731
|
-
:root {
|
|
732
|
-
color-scheme: dark;
|
|
733
|
-
--bg: #0a0c10;
|
|
734
|
-
--fg: #eef2f7;
|
|
735
|
-
--mute: #8b95a8;
|
|
736
|
-
--line: #1c2330;
|
|
737
|
-
--card: #10141c;
|
|
738
|
-
--ok: #3ecf8e;
|
|
739
|
-
--bad: #ff5d5d;
|
|
740
|
-
--warn: #f0b429;
|
|
741
|
-
--rail: #2a3344;
|
|
742
|
-
--day: #6b7280;
|
|
743
|
-
}
|
|
744
|
-
@media (prefers-color-scheme: light) {
|
|
745
|
-
:root {
|
|
746
|
-
color-scheme: light;
|
|
747
|
-
--bg: #f3f5f8;
|
|
748
|
-
--fg: #0f172a;
|
|
749
|
-
--mute: #64748b;
|
|
750
|
-
--line: #e2e8f0;
|
|
751
|
-
--card: #ffffff;
|
|
752
|
-
--ok: #059669;
|
|
753
|
-
--bad: #dc2626;
|
|
754
|
-
--warn: #d97706;
|
|
755
|
-
--rail: #cbd5e1;
|
|
756
|
-
--day: #64748b;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
* { box-sizing: border-box; }
|
|
760
|
-
body {
|
|
761
|
-
margin: 0;
|
|
762
|
-
font-family: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
763
|
-
background: var(--bg);
|
|
764
|
-
color: var(--fg);
|
|
765
|
-
line-height: 1.4;
|
|
766
|
-
}
|
|
767
|
-
main { max-width: 42rem; margin: 0 auto; padding: 1.35rem 1rem 3rem; }
|
|
768
|
-
header.bar {
|
|
769
|
-
position: sticky; top: 0; z-index: 3;
|
|
770
|
-
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
771
|
-
backdrop-filter: blur(10px);
|
|
772
|
-
padding: 0.85rem 0 0.75rem;
|
|
773
|
-
border-bottom: 1px solid var(--line);
|
|
774
|
-
margin-bottom: 1rem;
|
|
775
|
-
}
|
|
776
|
-
.title-row {
|
|
777
|
-
display: flex; align-items: baseline; justify-content: space-between;
|
|
778
|
-
gap: 0.75rem; flex-wrap: wrap;
|
|
779
|
-
}
|
|
780
|
-
h1 {
|
|
781
|
-
margin: 0; font-size: 1.2rem; font-weight: 600;
|
|
782
|
-
letter-spacing: -0.025em;
|
|
783
|
-
}
|
|
784
|
-
.range { color: var(--mute); font-size: 0.85rem; display: inline-flex; align-items: center; gap: 0.5rem; }
|
|
785
|
-
.live {
|
|
786
|
-
display: inline-flex; align-items: center; gap: 0.35rem;
|
|
787
|
-
font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
|
788
|
-
color: var(--ok); border: 1px solid color-mix(in srgb, var(--ok) 40%, transparent);
|
|
789
|
-
border-radius: 999px; padding: 0.12rem 0.5rem;
|
|
790
|
-
}
|
|
791
|
-
.live::before {
|
|
792
|
-
content: ""; width: 0.4rem; height: 0.4rem; border-radius: 50%;
|
|
793
|
-
background: var(--ok);
|
|
794
|
-
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent);
|
|
795
|
-
animation: pulse 1.6s ease-in-out infinite;
|
|
796
|
-
}
|
|
797
|
-
.live.off { color: var(--mute); border-color: var(--line); }
|
|
798
|
-
.live.off::before { background: var(--mute); box-shadow: none; animation: none; }
|
|
799
|
-
@keyframes pulse {
|
|
800
|
-
0%, 100% { opacity: 1; }
|
|
801
|
-
50% { opacity: 0.45; }
|
|
802
|
-
}
|
|
803
|
-
@media (prefers-reduced-motion: reduce) {
|
|
804
|
-
.live::before { animation: none; }
|
|
805
|
-
}
|
|
806
|
-
.stats {
|
|
807
|
-
display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem;
|
|
808
|
-
}
|
|
809
|
-
.stat {
|
|
810
|
-
font-size: 0.75rem; font-variant-numeric: tabular-nums;
|
|
811
|
-
color: var(--mute); background: var(--card);
|
|
812
|
-
border: 1px solid var(--line); border-radius: 999px;
|
|
813
|
-
padding: 0.2rem 0.6rem;
|
|
814
|
-
}
|
|
815
|
-
.stat b { color: var(--fg); font-weight: 650; margin-left: 0.25rem; }
|
|
816
|
-
.stat.bad b { color: var(--bad); }
|
|
817
|
-
.stat.warn b { color: var(--warn); }
|
|
818
|
-
.stat.ok b { color: var(--ok); }
|
|
819
|
-
button.stat {
|
|
820
|
-
font-family: inherit; cursor: pointer;
|
|
821
|
-
appearance: none; -webkit-appearance: none;
|
|
822
|
-
}
|
|
823
|
-
button.stat:hover { border-color: var(--mute); }
|
|
824
|
-
.stat[aria-pressed="true"] {
|
|
825
|
-
color: var(--fg);
|
|
826
|
-
border-color: color-mix(in srgb, var(--ok) 55%, var(--line));
|
|
827
|
-
background: color-mix(in srgb, var(--ok) 12%, var(--card));
|
|
828
|
-
}
|
|
829
|
-
.stat[hidden] { display: none; }
|
|
830
|
-
.clear-filters { color: var(--mute); font-size: 0.72rem; }
|
|
831
|
-
/* Analytics dashboard: 2×2 card grid, collapses to one column on narrow. */
|
|
832
|
-
.db-grid {
|
|
833
|
-
display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem 1.1rem;
|
|
834
|
-
}
|
|
835
|
-
@media (max-width: 700px) {
|
|
836
|
-
.db-grid { grid-template-columns: 1fr; }
|
|
837
|
-
}
|
|
838
|
-
.db-card { min-width: 0; }
|
|
839
|
-
.db-card h3 {
|
|
840
|
-
margin: 0 0 0.4rem; font-size: 0.68rem; font-weight: 700;
|
|
841
|
-
letter-spacing: 0.06em; text-transform: uppercase; color: var(--mute);
|
|
842
|
-
}
|
|
843
|
-
.db-card h3 .mute { text-transform: none; letter-spacing: 0.02em; font-weight: 600; }
|
|
844
|
-
.db-card .sub {
|
|
845
|
-
margin: 0.35rem 0 0.15rem; font-size: 0.66rem; font-weight: 700;
|
|
846
|
-
letter-spacing: 0.05em; text-transform: uppercase; color: var(--day);
|
|
847
|
-
}
|
|
848
|
-
/* Filter-linked bar row: block variant of .stat's hover/active contract. */
|
|
849
|
-
.db-item {
|
|
850
|
-
display: grid; grid-template-columns: 6.5rem 1fr 2.4rem;
|
|
851
|
-
gap: 0.5rem; align-items: center; width: 100%;
|
|
852
|
-
font-family: inherit; font-size: 0.76rem; font-variant-numeric: tabular-nums;
|
|
853
|
-
color: var(--mute); text-align: left;
|
|
854
|
-
background: transparent; border: 1px solid transparent; border-radius: 8px;
|
|
855
|
-
padding: 0.14rem 0.3rem; cursor: pointer;
|
|
856
|
-
appearance: none; -webkit-appearance: none;
|
|
857
|
-
}
|
|
858
|
-
.db-item:hover { border-color: var(--line); background: color-mix(in srgb, var(--bg) 55%, var(--card)); }
|
|
859
|
-
.db-item[aria-pressed="true"] {
|
|
860
|
-
color: var(--fg);
|
|
861
|
-
border-color: color-mix(in srgb, var(--ok) 55%, var(--line));
|
|
862
|
-
background: color-mix(in srgb, var(--ok) 12%, var(--card));
|
|
863
|
-
}
|
|
864
|
-
.db-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
865
|
-
.db-bar {
|
|
866
|
-
height: 0.55rem; border-radius: 4px; overflow: hidden;
|
|
867
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
868
|
-
}
|
|
869
|
-
.db-fill { display: block; height: 100%; border-radius: 4px; }
|
|
870
|
-
.db-fill.ok { background: var(--ok); }
|
|
871
|
-
.db-fill.warn { background: var(--warn); }
|
|
872
|
-
.db-fill.bad { background: var(--bad); }
|
|
873
|
-
.db-num { text-align: right; color: var(--fg); }
|
|
874
|
-
/* Vertical mini-bar timeline (not clickable — no time filter exists). */
|
|
875
|
-
.db-tl { display: flex; align-items: stretch; gap: 2px; height: 4.6rem; }
|
|
876
|
-
.db-tl .col {
|
|
877
|
-
flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
|
|
878
|
-
align-items: center; justify-content: flex-end; gap: 0.15rem;
|
|
879
|
-
}
|
|
880
|
-
.db-vbar {
|
|
881
|
-
width: 100%; min-height: 2px; border-radius: 2px 2px 0 0;
|
|
882
|
-
background: color-mix(in srgb, var(--ok) 75%, var(--mute));
|
|
883
|
-
}
|
|
884
|
-
.db-tl .lab {
|
|
885
|
-
font-size: 0.52rem; color: var(--mute); white-space: nowrap;
|
|
886
|
-
overflow: hidden; max-width: 100%; font-variant-numeric: tabular-nums;
|
|
887
|
-
}
|
|
888
|
-
/* utility: must beat .ev's display:grid (same specificity, later rule would win) */
|
|
889
|
-
.filtered-out { display: none !important; }
|
|
890
|
-
.identity {
|
|
891
|
-
margin-top: 0.45rem; color: var(--mute); font-size: 0.78rem;
|
|
892
|
-
word-break: break-word;
|
|
893
|
-
}
|
|
894
|
-
.modeb {
|
|
895
|
-
display: inline-block; font-size: 0.6rem; font-weight: 700;
|
|
896
|
-
color: var(--mute); border: 1px solid var(--line); border-radius: 4px;
|
|
897
|
-
padding: 0 0.28rem; line-height: 1.1rem; vertical-align: baseline;
|
|
898
|
-
}
|
|
899
|
-
.panel {
|
|
900
|
-
background: var(--card); border: 1px solid var(--line);
|
|
901
|
-
border-radius: 12px; padding: 0.7rem 0.9rem 0.8rem; margin-bottom: 1rem;
|
|
902
|
-
}
|
|
903
|
-
.panel h2 {
|
|
904
|
-
margin: 0 0 0.5rem; font-size: 0.72rem; font-weight: 700;
|
|
905
|
-
letter-spacing: 0.07em; text-transform: uppercase; color: var(--day);
|
|
906
|
-
}
|
|
907
|
-
.panel .sub { margin: 0 0 0.45rem; color: var(--mute); font-size: 0.8rem; }
|
|
908
|
-
.panel .line { margin: 0.25rem 0; font-size: 0.84rem; word-break: break-word; }
|
|
909
|
-
.panel .mute { color: var(--mute); }
|
|
910
|
-
.panel .small { font-size: 0.74rem; }
|
|
911
|
-
.rows {
|
|
912
|
-
list-style: none; margin: 0; padding: 0;
|
|
913
|
-
display: flex; flex-direction: column; gap: 0.3rem;
|
|
914
|
-
font-size: 0.82rem;
|
|
915
|
-
}
|
|
916
|
-
.rows li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.45rem; min-width: 0; }
|
|
917
|
-
.rows li.mute { color: var(--mute); }
|
|
918
|
-
.rows .sid, .rows code {
|
|
919
|
-
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
920
|
-
font-size: 0.78rem; word-break: break-all;
|
|
921
|
-
}
|
|
922
|
-
.rows .cnt { color: var(--mute); font-size: 0.76rem; }
|
|
923
|
-
.rows .rel { margin-left: auto; color: var(--mute); font-size: 0.74rem; white-space: nowrap; }
|
|
924
|
-
.rows .hint {
|
|
925
|
-
font-size: 0.72rem; border-bottom: 1px dotted var(--mute); cursor: help;
|
|
926
|
-
}
|
|
927
|
-
.wdot {
|
|
928
|
-
width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: none;
|
|
929
|
-
align-self: center; background: var(--mute);
|
|
930
|
-
}
|
|
931
|
-
.wdot.allow { background: var(--ok); }
|
|
932
|
-
.wdot.hold { background: var(--warn); }
|
|
933
|
-
.wdot.deny, .wdot.never { background: var(--bad); }
|
|
934
|
-
.chiprow { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
935
|
-
.chip {
|
|
936
|
-
font-size: 0.76rem; color: var(--mute);
|
|
937
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
938
|
-
border: 1px solid var(--line); border-radius: 999px; padding: 0.15rem 0.55rem;
|
|
939
|
-
}
|
|
940
|
-
.chip code {
|
|
941
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
942
|
-
color: var(--fg); font-size: 0.74rem;
|
|
943
|
-
}
|
|
944
|
-
.tbl { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
945
|
-
.tbl th {
|
|
946
|
-
text-align: left; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.06em;
|
|
947
|
-
text-transform: uppercase; color: var(--mute); padding: 0.15rem 0.5rem 0.3rem 0;
|
|
948
|
-
}
|
|
949
|
-
.tbl td { padding: 0.2rem 0.5rem 0.2rem 0; border-top: 1px solid var(--line); }
|
|
950
|
-
.tbl code {
|
|
951
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace; font-size: 0.76rem;
|
|
952
|
-
}
|
|
953
|
-
.pill {
|
|
954
|
-
display: inline-block; font-size: 0.7rem; font-weight: 700;
|
|
955
|
-
letter-spacing: 0.04em; text-transform: uppercase;
|
|
956
|
-
border-radius: 999px; padding: 0.1rem 0.55rem;
|
|
957
|
-
border: 1px solid var(--line); color: var(--mute);
|
|
958
|
-
}
|
|
959
|
-
.pill.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
|
960
|
-
.pill.orr-green { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
|
961
|
-
.pill.orr-amber { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
|
|
962
|
-
.pill.orr-red { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
|
|
963
|
-
.orrline { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.55rem; }
|
|
964
|
-
.orrline .disp { font-size: 0.84rem; font-weight: 600; }
|
|
965
|
-
.alert {
|
|
966
|
-
display: flex; flex-wrap: wrap; gap: 0.45rem 0.75rem; align-items: baseline;
|
|
967
|
-
background: color-mix(in srgb, var(--bad) 12%, var(--card));
|
|
968
|
-
border: 1px solid color-mix(in srgb, var(--bad) 35%, var(--line));
|
|
969
|
-
border-radius: 10px; padding: 0.65rem 0.85rem; margin-bottom: 1rem;
|
|
970
|
-
font-size: 0.85rem;
|
|
971
|
-
}
|
|
972
|
-
.alert strong { color: var(--bad); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
973
|
-
.alert code {
|
|
974
|
-
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
975
|
-
font-size: 0.78rem;
|
|
976
|
-
}
|
|
977
|
-
.feed {
|
|
978
|
-
background: var(--card);
|
|
979
|
-
border: 1px solid var(--line);
|
|
980
|
-
border-radius: 12px;
|
|
981
|
-
overflow: hidden;
|
|
982
|
-
}
|
|
983
|
-
.day {
|
|
984
|
-
padding: 0.55rem 0.9rem 0.35rem;
|
|
985
|
-
font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em;
|
|
986
|
-
text-transform: uppercase; color: var(--day);
|
|
987
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
988
|
-
border-bottom: 1px solid var(--line);
|
|
989
|
-
}
|
|
990
|
-
.ev {
|
|
991
|
-
display: grid; grid-template-columns: 1.1rem 1fr; gap: 0;
|
|
992
|
-
border-bottom: 1px solid var(--line);
|
|
993
|
-
}
|
|
994
|
-
.ev:last-child { border-bottom: none; }
|
|
995
|
-
.rail {
|
|
996
|
-
position: relative;
|
|
997
|
-
display: flex; justify-content: center;
|
|
998
|
-
padding-top: 1.05rem;
|
|
999
|
-
}
|
|
1000
|
-
.rail::before {
|
|
1001
|
-
content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
|
|
1002
|
-
width: 1px; background: var(--rail); transform: translateX(-50%);
|
|
1003
|
-
}
|
|
1004
|
-
.tick {
|
|
1005
|
-
width: 0.55rem; height: 0.55rem; border-radius: 50%;
|
|
1006
|
-
background: var(--mute); position: relative; z-index: 1;
|
|
1007
|
-
box-shadow: 0 0 0 3px var(--card);
|
|
1008
|
-
}
|
|
1009
|
-
.ev.ok .tick { background: var(--ok); }
|
|
1010
|
-
.ev.bad .tick { background: var(--bad); }
|
|
1011
|
-
.ev.warn .tick { background: var(--warn); }
|
|
1012
|
-
.ev.never { background: color-mix(in srgb, var(--bad) 7%, transparent); }
|
|
1013
|
-
.main { padding: 0.7rem 0.85rem 0.7rem 0.35rem; min-width: 0; }
|
|
1014
|
-
.top {
|
|
1015
|
-
display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.55rem;
|
|
1016
|
-
}
|
|
1017
|
-
@media (max-width: 480px) {
|
|
1018
|
-
.when { flex-basis: 100%; margin-left: 0; margin-top: 0.1rem; }
|
|
1019
|
-
}
|
|
1020
|
-
.verdict {
|
|
1021
|
-
font-size: 0.68rem; font-weight: 750; letter-spacing: 0.05em;
|
|
1022
|
-
min-width: 2.6rem;
|
|
1023
|
-
}
|
|
1024
|
-
.ev.ok .verdict { color: var(--ok); }
|
|
1025
|
-
.ev.bad .verdict { color: var(--bad); }
|
|
1026
|
-
.ev.warn .verdict { color: var(--warn); }
|
|
1027
|
-
.tool {
|
|
1028
|
-
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1029
|
-
font-size: 0.84rem; word-break: break-word; flex: 1 1 auto;
|
|
1030
|
-
}
|
|
1031
|
-
.when {
|
|
1032
|
-
margin-left: auto; color: var(--mute); font-size: 0.72rem;
|
|
1033
|
-
font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
1034
|
-
}
|
|
1035
|
-
.when .rel { opacity: 0.85; margin-left: 0.35rem; }
|
|
1036
|
-
.summary {
|
|
1037
|
-
margin-top: 0.2rem;
|
|
1038
|
-
font-size: 0.84rem;
|
|
1039
|
-
color: var(--fg);
|
|
1040
|
-
word-break: break-word;
|
|
1041
|
-
}
|
|
1042
|
-
details.diff {
|
|
1043
|
-
margin-top: 0.35rem;
|
|
1044
|
-
border: 1px solid var(--line);
|
|
1045
|
-
border-radius: 8px;
|
|
1046
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
1047
|
-
overflow: hidden;
|
|
1048
|
-
}
|
|
1049
|
-
details.diff > summary {
|
|
1050
|
-
cursor: pointer;
|
|
1051
|
-
list-style: none;
|
|
1052
|
-
padding: 0.35rem 0.55rem;
|
|
1053
|
-
font-size: 0.72rem;
|
|
1054
|
-
font-weight: 650;
|
|
1055
|
-
letter-spacing: 0.04em;
|
|
1056
|
-
text-transform: uppercase;
|
|
1057
|
-
color: var(--mute);
|
|
1058
|
-
}
|
|
1059
|
-
details.diff > summary::-webkit-details-marker { display: none; }
|
|
1060
|
-
details.diff pre {
|
|
1061
|
-
margin: 0;
|
|
1062
|
-
padding: 0.45rem 0.55rem 0.6rem;
|
|
1063
|
-
border-top: 1px solid var(--line);
|
|
1064
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
1065
|
-
font-size: 0.72rem;
|
|
1066
|
-
line-height: 1.35;
|
|
1067
|
-
white-space: pre-wrap;
|
|
1068
|
-
word-break: break-word;
|
|
1069
|
-
color: var(--fg);
|
|
1070
|
-
}
|
|
1071
|
-
.meta {
|
|
1072
|
-
margin-top: 0.18rem; color: var(--mute); font-size: 0.76rem;
|
|
1073
|
-
display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: baseline;
|
|
1074
|
-
}
|
|
1075
|
-
.dot { opacity: 0.55; }
|
|
1076
|
-
.empty { margin: 0; padding: 1.25rem 1rem; color: var(--mute); font-size: 0.9rem; }
|
|
1077
|
-
.tools {
|
|
1078
|
-
margin-top: 1.1rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center;
|
|
1079
|
-
}
|
|
1080
|
-
.tools .label {
|
|
1081
|
-
font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em;
|
|
1082
|
-
text-transform: uppercase; color: var(--mute); margin-right: 0.25rem;
|
|
1083
|
-
}
|
|
1084
|
-
.tools code {
|
|
1085
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
1086
|
-
font-size: 0.72rem; color: var(--mute);
|
|
1087
|
-
background: var(--card); border: 1px solid var(--line);
|
|
1088
|
-
border-radius: 6px; padding: 0.18rem 0.4rem;
|
|
1089
|
-
}
|
|
1090
|
-
</style>
|
|
931
|
+
<style>${receiptCss()}</style>
|
|
1091
932
|
</head>
|
|
1092
933
|
<body>
|
|
1093
934
|
<main>
|
|
1094
|
-
<
|
|
1095
|
-
<
|
|
1096
|
-
<
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
<
|
|
1104
|
-
<
|
|
1105
|
-
|
|
1106
|
-
<
|
|
1107
|
-
</
|
|
1108
|
-
|
|
1109
|
-
${chips.projects}
|
|
1110
|
-
</header>
|
|
935
|
+
<div class="topstick">
|
|
936
|
+
<header class="bar">
|
|
937
|
+
<div class="title-row">
|
|
938
|
+
<h1>${esc(model.title)}</h1>
|
|
939
|
+
<div class="range">${esc(model.rangeLabel)}${livePill}</div>
|
|
940
|
+
</div>
|
|
941
|
+
${identityLine(model.identity)}
|
|
942
|
+
</header>
|
|
943
|
+
<nav class="tabs" aria-label="Report sections">
|
|
944
|
+
<a class="tab" id="tab-overview" href="#overview">Overview</a>
|
|
945
|
+
<a class="tab" id="tab-certify" href="#certify">Certify</a>
|
|
946
|
+
<a class="tab" id="tab-activity" href="#activity">Activity</a>
|
|
947
|
+
<a class="tab" id="tab-system" href="#system">System</a>
|
|
948
|
+
</nav>
|
|
949
|
+
</div>
|
|
1111
950
|
${blockedBanner}
|
|
1112
|
-
|
|
1113
|
-
|
|
951
|
+
<div class="hero">
|
|
952
|
+
${heroCertifyPanel(model.certify)}
|
|
953
|
+
${verdictsHeroCard(c, events.length)}
|
|
954
|
+
${activityHeroCard(agg, dashboard, events.length)}
|
|
955
|
+
${showbackHeroCard(model.showback)}
|
|
956
|
+
</div>
|
|
957
|
+
<section class="zone" id="overview" aria-labelledby="tab-overview">
|
|
958
|
+
${overviewBody}
|
|
959
|
+
</section>
|
|
960
|
+
<section class="zone" id="certify" aria-labelledby="tab-certify">
|
|
961
|
+
${certifyBody}
|
|
962
|
+
</section>
|
|
963
|
+
<section class="zone" id="activity" aria-labelledby="tab-activity">
|
|
964
|
+
<div class="feed-head">
|
|
965
|
+
<div class="stats" aria-label="Counts">
|
|
966
|
+
<button type="button" class="stat ok" data-fgroup="verdict" data-fvalue="ALLOW" aria-pressed="false" title="Filter: Allow">Allow<b>${c.allow}</b></button>
|
|
967
|
+
<button type="button" class="stat bad" data-fgroup="verdict" data-fvalue="DENY" aria-pressed="false" title="Filter: Deny">Deny<b>${c.deny}</b></button>
|
|
968
|
+
<button type="button" class="stat warn" data-fgroup="verdict" data-fvalue="REQUIRE_APPROVE" aria-pressed="false" title="Filter: Review">Review<b>${c.require}</b></button>
|
|
969
|
+
<button type="button" class="stat" data-fgroup="never" data-fvalue="1" aria-pressed="false" title="Filter: Never">Never<b>${c.never}</b></button>
|
|
970
|
+
${chips.modes}${chips.planes}
|
|
971
|
+
<button type="button" class="stat clear-filters" id="clear-filters" hidden>Clear filters ×</button>
|
|
972
|
+
</div>
|
|
973
|
+
${chips.products}
|
|
974
|
+
${chips.projects}
|
|
975
|
+
</div>
|
|
976
|
+
<section class="feed" id="feed" aria-label="Activity feed">
|
|
1114
977
|
${renderFeed(events, nowMs, model.live)}
|
|
978
|
+
</section>
|
|
979
|
+
</section>
|
|
980
|
+
<section class="zone" id="system" aria-labelledby="tab-system">
|
|
981
|
+
${systemBody}
|
|
1115
982
|
</section>
|
|
1116
|
-
${sessionsPanel(agg, nowMs)}
|
|
1117
|
-
${reasonsPanel(agg)}
|
|
1118
|
-
${wiredHostsPanel(model.wiredHosts)}
|
|
1119
|
-
${sandboxesPanel(model.sandboxes, nowMs)}
|
|
1120
|
-
${orrPanel(model.orr, nowMs)}
|
|
1121
|
-
${certifyPanel(model.certify)}
|
|
1122
|
-
${showbackPanel(model.showback)}
|
|
1123
|
-
${toolsHtml}
|
|
1124
983
|
</main>
|
|
1125
984
|
${filterScript}
|
|
1126
985
|
${liveScript}
|
|
@@ -1170,6 +1029,17 @@ export function renderReceiptMarkdown(model) {
|
|
|
1170
1029
|
if (agg.projects.length >= 2) {
|
|
1171
1030
|
lines.push("## Projects", ...agg.projects.map((p) => `- ${mdText(p.label)} × ${p.count}`), "");
|
|
1172
1031
|
}
|
|
1032
|
+
if (model.certify) {
|
|
1033
|
+
const c = model.certify;
|
|
1034
|
+
lines.push("## Certify", `result: ${c.result} · counts: ${c.pass} pass · ${c.gap} gap · ${c.insufficientEvidence} insufficient · ${c.attested} attested`);
|
|
1035
|
+
if (c.topGaps.length > 0) {
|
|
1036
|
+
lines.push("Top gaps:");
|
|
1037
|
+
for (const g of c.topGaps) {
|
|
1038
|
+
lines.push(`- \`${mdInline(g.id)}\` — ${mdText(g.title)} (${mdText(g.severity)})`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
lines.push(`live evaluation — window ${c.windowDays}d, ${c.trailEvents} trail events · the Certify tab has the full live requirement table · kya certify for the gap report + signed bundle`, "");
|
|
1042
|
+
}
|
|
1173
1043
|
const db = computeDashboard(model.events);
|
|
1174
1044
|
if (db.verdictMix.total > 0) {
|
|
1175
1045
|
const pct = (n) => Math.round((n / db.verdictMix.total) * 100);
|
|
@@ -1259,17 +1129,6 @@ export function renderReceiptMarkdown(model) {
|
|
|
1259
1129
|
lines.push(`Most urgent fix: ${mdText(orr.mostUrgentFix)}`);
|
|
1260
1130
|
lines.push(`scorecards: ${orr.scorecards.pass} pass · ${orr.scorecards.fail} fail · ${orr.scorecards.partial} partial · ${orr.scorecards.notEvaluated} n/e${orr.generatedAt ? ` · ${relativeTime(orr.generatedAt, nowMs)}` : ""}`, "");
|
|
1261
1131
|
}
|
|
1262
|
-
if (model.certify) {
|
|
1263
|
-
const c = model.certify;
|
|
1264
|
-
lines.push("## Certify", `result: ${c.result} · counts: ${c.pass} pass · ${c.gap} gap · ${c.insufficientEvidence} insufficient · ${c.attested} attested`);
|
|
1265
|
-
if (c.topGaps.length > 0) {
|
|
1266
|
-
lines.push("Top gaps:");
|
|
1267
|
-
for (const g of c.topGaps) {
|
|
1268
|
-
lines.push(`- \`${mdInline(g.id)}\` — ${mdText(g.severity)}`);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
lines.push(`live evaluation — window ${c.windowDays}d, ${c.trailEvents} trail events · kya certify for the full gap report + signed bundle`, "");
|
|
1272
|
-
}
|
|
1273
1132
|
if (model.showback) {
|
|
1274
1133
|
const sb = model.showback;
|
|
1275
1134
|
lines.push("## Showback (observe only)", `${sb.totalTokensIn} tokens in · ${sb.totalTokensOut} tokens out · ${usdText(sb.estimatedUsd)}`);
|