@shield-agent/kya 0.16.0 → 0.18.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/assets/activity-receipt.png +0 -0
- package/assets/report-activity.png +0 -0
- package/assets/report-certify.png +0 -0
- package/assets/report-changes.png +0 -0
- package/assets/report-gateway-backends.png +0 -0
- package/assets/report-gateway-listeners.png +0 -0
- package/assets/report-gateway-playground.png +0 -0
- package/assets/report-gateway-policies.png +0 -0
- package/assets/report-gateway-routes.png +0 -0
- package/assets/report-gateway.png +0 -0
- package/assets/report-overview.png +0 -0
- 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/receipt/gate-page.d.ts +41 -2
- package/dist/receipt/gate-page.d.ts.map +1 -1
- package/dist/receipt/gate-page.js +59 -0
- package/dist/receipt/gate-page.js.map +1 -1
- package/dist/receipt/live-server.d.ts.map +1 -1
- package/dist/receipt/live-server.js +47 -2
- package/dist/receipt/live-server.js.map +1 -1
- package/dist/receipt/receipt-css.d.ts.map +1 -1
- package/dist/receipt/receipt-css.js +94 -44
- package/dist/receipt/receipt-css.js.map +1 -1
- package/dist/receipt/render-receipt.d.ts +17 -0
- package/dist/receipt/render-receipt.d.ts.map +1 -1
- package/dist/receipt/render-receipt.js +528 -270
- 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
|
@@ -10,8 +10,15 @@ import { loadCertifyCard, loadIdentity, loadOrrCard, loadSandboxes, loadShowback
|
|
|
10
10
|
import { SHOWBACK_DISCLAIMER, } from "../showback/cost-per-task.js";
|
|
11
11
|
import { computeDashboard } from "./dashboard.js";
|
|
12
12
|
import { buildChangesModel } from "./changes.js";
|
|
13
|
-
import { GATE_NOT_SETUP_QUICKSTART, loadGatePage,
|
|
13
|
+
import { GATE_NOT_SETUP_QUICKSTART, loadGatePage, toolNameFromToolId, toolServerPrefix, } from "./gate-page.js";
|
|
14
14
|
import { receiptCss } from "./receipt-css.js";
|
|
15
|
+
export const PAGE_SIZE = 50;
|
|
16
|
+
export function clampPage(page, total) {
|
|
17
|
+
if (total === 0)
|
|
18
|
+
return 1;
|
|
19
|
+
const p = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
|
|
20
|
+
return Math.min(p, total);
|
|
21
|
+
}
|
|
15
22
|
function esc(s) {
|
|
16
23
|
return stripEscapes(s)
|
|
17
24
|
.replace(/&/g, "&")
|
|
@@ -203,6 +210,7 @@ export function buildReceiptModel(sessionId, events, extras) {
|
|
|
203
210
|
rangeLabel: `session ${sessionId}`,
|
|
204
211
|
generatedAt: new Date().toISOString(),
|
|
205
212
|
events: filtered,
|
|
213
|
+
page: extras?.page ?? 1,
|
|
206
214
|
spend: extras?.spend,
|
|
207
215
|
mcpSeen: extras?.mcpSeen,
|
|
208
216
|
live: extras?.live,
|
|
@@ -223,6 +231,7 @@ export function buildWindowReceiptModel(events, days, extras) {
|
|
|
223
231
|
rangeLabel: `last ${n} day${n === 1 ? "" : "s"}`,
|
|
224
232
|
generatedAt: new Date().toISOString(),
|
|
225
233
|
events,
|
|
234
|
+
page: extras?.page ?? 1,
|
|
226
235
|
spend: extras?.spend,
|
|
227
236
|
mcpSeen: extras?.mcpSeen,
|
|
228
237
|
live: extras?.live,
|
|
@@ -244,6 +253,7 @@ export function loadReceiptModel(input) {
|
|
|
244
253
|
? readTrail(input.cwd)
|
|
245
254
|
: readTrailSince(input.cwd, new Date(Date.now() - days * 24 * 60 * 60 * 1000));
|
|
246
255
|
const extras = {
|
|
256
|
+
page: input.page ?? 1,
|
|
247
257
|
live: input.live,
|
|
248
258
|
liveToken: input.liveToken,
|
|
249
259
|
identity: loadIdentity(input.cwd),
|
|
@@ -424,6 +434,19 @@ function navItem(href, label, count, runningDot) {
|
|
|
424
434
|
${countSpan}
|
|
425
435
|
</a>`;
|
|
426
436
|
}
|
|
437
|
+
function navGroup(label, runningDot) {
|
|
438
|
+
const dot = runningDot ? '<span class="status-dot" aria-hidden="true"></span>' : "";
|
|
439
|
+
return `<div class="nav-group">
|
|
440
|
+
<span class="nav-group-label">${dot}${esc(label)}</span>
|
|
441
|
+
</div>`;
|
|
442
|
+
}
|
|
443
|
+
function navSubItem(href, label, count) {
|
|
444
|
+
const countSpan = count !== undefined ? `<span class="nav-count">${count}</span>` : "";
|
|
445
|
+
return `<a class="nav-link nav-sub" id="tab-${href.slice(1)}" href="${href}">
|
|
446
|
+
<span class="nav-label">${esc(label)}</span>
|
|
447
|
+
${countSpan}
|
|
448
|
+
</a>`;
|
|
449
|
+
}
|
|
427
450
|
function statChips(agg) {
|
|
428
451
|
// Chips are filter toggles: native buttons keep identical styling via .stat.
|
|
429
452
|
const chip = (label, n, group, value, cls = "") => n > 0
|
|
@@ -599,11 +622,9 @@ function wiredHostsPanel(hosts) {
|
|
|
599
622
|
</section>`;
|
|
600
623
|
}
|
|
601
624
|
/**
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* up with the feed's data-server stamps; ids come from gateways.json and are
|
|
606
|
-
* always escaped.
|
|
625
|
+
* System-tab gateway summary: a single status row that links to the dedicated
|
|
626
|
+
* Gateway section. Detail lives in the Gateway pages so the System tab stays
|
|
627
|
+
* focused on host/runtime state.
|
|
607
628
|
*/
|
|
608
629
|
function gatePanel(card, nowMs) {
|
|
609
630
|
if (!card)
|
|
@@ -613,60 +634,27 @@ function gatePanel(card, nowMs) {
|
|
|
613
634
|
<h2>Gateway</h2>
|
|
614
635
|
<div class="orrline">
|
|
615
636
|
<span class="pill">not set up</span>
|
|
637
|
+
<span class="mute"><a href="#gateway-home">Open Gateway</a> to configure</span>
|
|
616
638
|
</div>
|
|
617
|
-
<p class="line">Gateway not set up — <code>kya start</code> puts a governed gateway in front of any MCP server automatically: every tool call proxied, evaluated, and on this trail.</p>
|
|
618
|
-
<ol class="steps">
|
|
619
|
-
<li>Add any MCP server to a host config (or <code>kya gate init</code> to pick from recipes)</li>
|
|
620
|
-
<li><code>kya start</code> — imports it, installs the gateway, and routes the host through it (originals backed up under .kya/backups)</li>
|
|
621
|
-
<li><code>kya gate doctor</code> — binary, config, listener health, loopback-only posture</li>
|
|
622
|
-
</ol>
|
|
623
639
|
</section>`;
|
|
624
640
|
}
|
|
625
|
-
const rows = card.servers
|
|
626
|
-
.map((s) => {
|
|
627
|
-
const dot = s.worst === "none"
|
|
628
|
-
? ""
|
|
629
|
-
: `<span class="wdot ${s.worst}" title="worst verdict: ${s.worst}"></span>`;
|
|
630
|
-
return ` <li data-server="${esc(s.serverFacet)}">${dot}<code>${esc(s.id)}</code> — ${esc(s.transport)} · ${s.events} event${s.events === 1 ? "" : "s"}</li>`;
|
|
631
|
-
})
|
|
632
|
-
.join("\n");
|
|
633
641
|
const binary = card.binaryPresent
|
|
634
642
|
? `binary ${card.binaryVersion ? esc(clip(card.binaryVersion, 40)) : "installed"}`
|
|
635
643
|
: "binary missing — kya gate setup";
|
|
636
|
-
if (card.state === "configured-stopped") {
|
|
637
|
-
return `<section class="panel gate" aria-label="Gateway">
|
|
638
|
-
<h2>Gateway</h2>
|
|
639
|
-
<div class="orrline">
|
|
640
|
-
<span class="pill">stopped</span>
|
|
641
|
-
<span class="mute">${card.servers.length} server${card.servers.length === 1 ? "" : "s"} configured · ${binary}</span>
|
|
642
|
-
</div>
|
|
643
|
-
<ul class="rows">
|
|
644
|
-
${rows}
|
|
645
|
-
</ul>
|
|
646
|
-
<p class="mute small">stopped — <code>kya gate run</code> to start the listener</p>
|
|
647
|
-
</section>`;
|
|
648
|
-
}
|
|
649
644
|
const uptime = card.startedAt ? ` · up since ${esc(relativeTime(card.startedAt, nowMs))}` : "";
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
</div>
|
|
657
|
-
<p class="line">running, no servers configured — add servers to .kya/gateways.json and restart (<code>kya gate stop</code>, then <code>kya gate run</code>)</p>
|
|
658
|
-
</section>`;
|
|
659
|
-
}
|
|
645
|
+
const pill = card.state === "running"
|
|
646
|
+
? `<span class="pill ok">running</span>`
|
|
647
|
+
: `<span class="pill">stopped</span>`;
|
|
648
|
+
const detail = card.state === "running"
|
|
649
|
+
? `${card.url ? `<code>${esc(clip(card.url, 60))}</code>` : ""}${uptime} · ${card.servers.length} server${card.servers.length === 1 ? "" : "s"} · ${card.events} event${card.events === 1 ? "" : "s"}`
|
|
650
|
+
: `${card.servers.length} server${card.servers.length === 1 ? "" : "s"} configured · ${binary}`;
|
|
660
651
|
return `<section class="panel gate" aria-label="Gateway">
|
|
661
652
|
<h2>Gateway</h2>
|
|
662
653
|
<div class="orrline">
|
|
663
|
-
|
|
664
|
-
<span class="mute">${
|
|
654
|
+
${pill}
|
|
655
|
+
<span class="mute">${detail}</span>
|
|
656
|
+
<span class="mute"><a href="#gateway-home">Open Gateway</a></span>
|
|
665
657
|
</div>
|
|
666
|
-
<ul class="rows">
|
|
667
|
-
${rows}
|
|
668
|
-
</ul>
|
|
669
|
-
<p class="mute small">${card.events} gateway event${card.events === 1 ? "" : "s"} in this window · <code>kya gate stop</code> stops everything</p>
|
|
670
658
|
</section>`;
|
|
671
659
|
}
|
|
672
660
|
/**
|
|
@@ -675,40 +663,42 @@ ${rows}
|
|
|
675
663
|
* playground, quick actions, and a gateway-native events table. All user-controlled
|
|
676
664
|
* values (server ids, commands, urls, tool names) are escaped.
|
|
677
665
|
*/
|
|
678
|
-
function
|
|
679
|
-
if (!page) {
|
|
680
|
-
return `<section class="zone" id="gateway" aria-labelledby="tab-gateway">
|
|
681
|
-
<p class="mute small zone-empty">Gateway details will appear here after setup.</p>
|
|
682
|
-
</section>`;
|
|
683
|
-
}
|
|
684
|
-
if (page.state === "not-set-up") {
|
|
685
|
-
return `<section class="zone gate-zone" id="gateway" aria-labelledby="tab-gateway">
|
|
686
|
-
<h2>Gateway</h2>
|
|
687
|
-
<p class="sub">Local MCP gate — every proxied tool call is evaluated and audited.</p>
|
|
688
|
-
<div class="card">
|
|
689
|
-
<span class="pill">not set up</span>
|
|
690
|
-
<p class="line">${esc(GATE_NOT_SETUP_QUICKSTART)}</p>
|
|
691
|
-
<div class="quick-actions">
|
|
692
|
-
<button type="button" class="stat" data-action="doctor" id="gate-action-doctor">Doctor</button>
|
|
693
|
-
</div>
|
|
694
|
-
<div id="gate-doctor-result"></div>
|
|
695
|
-
</div>
|
|
696
|
-
</section>`;
|
|
697
|
-
}
|
|
698
|
-
const denyCount = page.gateEvents.filter((e) => e.verdict.toUpperCase() === "DENY" || e.neverEvent || e.reasonCode === "NEVER_EVENT").length;
|
|
666
|
+
function gateStatusHeader(page, nowMs) {
|
|
699
667
|
const listenerStatus = page.state === "running"
|
|
700
668
|
? `<span class="pill ok">running</span>`
|
|
701
669
|
: `<span class="pill">stopped</span>`;
|
|
702
670
|
const bindBadge = page.bindScope.loopbackOnly
|
|
703
671
|
? `<span class="pill ok" title="${esc(page.bindScope.detail)}">loopback-only</span>`
|
|
704
672
|
: `<span class="pill warn" title="${esc(page.bindScope.detail)}">not loopback-only</span>`;
|
|
705
|
-
const
|
|
673
|
+
const uptime = page.startedAt
|
|
674
|
+
? ` · up since <time datetime="${esc(page.startedAt)}">${esc(relativeTime(page.startedAt, nowMs))}</time>`
|
|
675
|
+
: "";
|
|
676
|
+
const urlLine = page.url ? ` <code>${esc(clip(page.url, 80))}</code>` : "";
|
|
677
|
+
return `<div class="gate-status-header">
|
|
678
|
+
<span class="gate-status-title">Gateway</span>
|
|
679
|
+
<span class="gate-status-meta">${listenerStatus}${urlLine}${uptime} · ${bindBadge}</span>
|
|
680
|
+
</div>`;
|
|
681
|
+
}
|
|
682
|
+
function gateQuickActions(page) {
|
|
683
|
+
const buttons = page.state === "not-set-up"
|
|
684
|
+
? `<button type="button" class="stat" data-action="doctor">Doctor</button>`
|
|
685
|
+
: page.state === "configured-stopped"
|
|
686
|
+
? `<button type="button" class="stat ok" data-action="start">Start</button>
|
|
687
|
+
<button type="button" class="stat" data-action="doctor">Doctor</button>`
|
|
688
|
+
: `<button type="button" class="stat bad" data-action="stop">Stop</button>
|
|
689
|
+
<button type="button" class="stat warn" data-action="restart">Restart</button>
|
|
690
|
+
<button type="button" class="stat" data-action="doctor">Doctor</button>`;
|
|
691
|
+
return `<div class="quick-actions">${buttons}</div><div id="gate-doctor-result"></div>`;
|
|
692
|
+
}
|
|
693
|
+
function gateStatCards(page) {
|
|
694
|
+
const denyCount = page.gateEvents.filter((e) => e.verdict.toUpperCase() === "DENY" || e.neverEvent || e.reasonCode === "NEVER_EVENT").length;
|
|
695
|
+
return `<div class="stat-cards">
|
|
706
696
|
<div class="stat-card">
|
|
707
|
-
<span class="lab">
|
|
708
|
-
<span class="
|
|
697
|
+
<span class="lab">Listeners</span>
|
|
698
|
+
<span class="num">${page.listeners.length}</span>
|
|
709
699
|
</div>
|
|
710
700
|
<div class="stat-card">
|
|
711
|
-
<span class="lab">
|
|
701
|
+
<span class="lab">Backends</span>
|
|
712
702
|
<span class="num">${page.servers.length}</span>
|
|
713
703
|
</div>
|
|
714
704
|
<div class="stat-card">
|
|
@@ -720,125 +710,31 @@ function gatePagePanel(page, nowMs) {
|
|
|
720
710
|
<span class="num${denyCount > 0 ? " bad" : ""}">${denyCount}</span>
|
|
721
711
|
</div>
|
|
722
712
|
</div>`;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const binaryLine = page.binaryPresent
|
|
727
|
-
? `<p class="line">Binary <code>${page.binaryVersion ? esc(clip(page.binaryVersion, 40)) : "installed"}</code> · <code>${esc(page.binaryPath)}</code></p>`
|
|
728
|
-
: `<p class="line">Binary missing — <code>kya gate setup</code></p>`;
|
|
729
|
-
const listenerUrl = page.url
|
|
730
|
-
? `<p class="line">Listener <code>${esc(clip(page.url, 80))}</code> · port ${page.port ?? page.otlpPort}</p>`
|
|
731
|
-
: `<p class="line">Listener port ${page.port ?? 3930} (stopped)</p>`;
|
|
732
|
-
const listenerCard = `<div class="card">
|
|
733
|
-
<h3>Listener</h3>
|
|
734
|
-
${listenerUrl}
|
|
735
|
-
${uptime}
|
|
736
|
-
${binaryLine}
|
|
737
|
-
<p class="line">OTLP receiver port ${page.otlpPort} · failure mode ${esc(page.failureMode)}</p>
|
|
738
|
-
</div>`;
|
|
739
|
-
const targetsTable = (() => {
|
|
740
|
-
if (page.servers.length === 0) {
|
|
741
|
-
return `<div class="card">
|
|
742
|
-
<h3>Targets</h3>
|
|
743
|
-
<p class="empty small">No servers configured — add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
|
|
744
|
-
</div>`;
|
|
745
|
-
}
|
|
746
|
-
const rows = page.servers
|
|
747
|
-
.map((s) => {
|
|
748
|
-
const transport = s.transport === "stdio"
|
|
749
|
-
? `<code title="${esc(s.cmd?.join(" ") ?? "")}">${esc(clip(s.cmd?.join(" ") ?? s.transport, 50))}</code>`
|
|
750
|
-
: `<code title="${esc(s.url ?? "")}">${esc(clip(s.url ?? s.transport, 50))}</code>`;
|
|
751
|
-
const provenance = s.importedFrom
|
|
752
|
-
.map((h) => `<span class="chip">${esc(h)}</span>`)
|
|
753
|
-
.join("");
|
|
754
|
-
const policy = `<span class="pill">${esc(s.policy.defaultTier ?? "unknown")}</span> · ${s.policy.denyPatterns.length} pattern${s.policy.denyPatterns.length === 1 ? "" : "s"}`;
|
|
755
|
-
const dot = s.worst === "none"
|
|
756
|
-
? ""
|
|
757
|
-
: `<span class="wdot ${s.worst}" title="worst verdict: ${s.worst}"></span>`;
|
|
758
|
-
const denied = s.deniedTools.length === 0
|
|
759
|
-
? '<span class="mute">—</span>'
|
|
760
|
-
: s.deniedTools.map((t) => `<span class="chip bad">${esc(t)}</span>`).join("");
|
|
761
|
-
return ` <tr data-server="${esc(s.serverFacet)}">
|
|
762
|
-
<td><span class="wdot ${s.worst === "none" ? "mute" : s.worst}"></span> <code>${esc(s.id)}</code></td>
|
|
763
|
-
<td>${transport}</td>
|
|
764
|
-
<td>${provenance}</td>
|
|
765
|
-
<td>${policy}</td>
|
|
766
|
-
<td>${dot} ${s.events}</td>
|
|
767
|
-
<td>${denied}</td>
|
|
768
|
-
</tr>`;
|
|
769
|
-
})
|
|
770
|
-
.join("\n");
|
|
713
|
+
}
|
|
714
|
+
function gateEventsTable(events, nowMs) {
|
|
715
|
+
if (events.length === 0) {
|
|
771
716
|
return `<div class="card">
|
|
772
|
-
<h3>Targets</h3>
|
|
773
|
-
<table class="tbl targets-table">
|
|
774
|
-
<thead><tr><th>Server</th><th>Transport</th><th>Provenance</th><th>Policy</th><th>Events</th><th>Denied tools</th></tr></thead>
|
|
775
|
-
<tbody>
|
|
776
|
-
${rows}
|
|
777
|
-
</tbody>
|
|
778
|
-
</table>
|
|
779
|
-
</div>`;
|
|
780
|
-
})();
|
|
781
|
-
const policiesCard = `<div class="card">
|
|
782
|
-
<h3>Policies</h3>
|
|
783
|
-
<p class="sub">Network rule</p>
|
|
784
|
-
<pre><code>${esc(LOOPBACK_RULE)}</code></pre>
|
|
785
|
-
<p class="sub">Per-server deny patterns</p>
|
|
786
|
-
${page.servers.length === 0 ? '<p class="mute small">No servers configured.</p>' : page.servers
|
|
787
|
-
.map((s) => `<details><summary><code>${esc(s.id)}</code> · ${s.policy.denyPatterns.length} pattern${s.policy.denyPatterns.length === 1 ? "" : "s"}</summary><pre><code>${esc(s.policy.denyPatterns.join("\n"))}</code></pre></details>`)
|
|
788
|
-
.join("\n")}
|
|
789
|
-
<p class="mute small">READ and WRITE tools are allowed; WRITE calls are observed/audited. Only destructive/admin patterns are denied.</p>
|
|
790
|
-
</div>`;
|
|
791
|
-
const serverOptions = page.servers
|
|
792
|
-
.map((s) => `<option value="${esc(s.id)}">${esc(s.id)}</option>`)
|
|
793
|
-
.join("");
|
|
794
|
-
const playgroundCard = `<div class="card">
|
|
795
|
-
<h3>Playground</h3>
|
|
796
|
-
<p class="mute small">Dry-run — no tool is executed.</p>
|
|
797
|
-
<form class="playground-form" id="gate-play-form">
|
|
798
|
-
<label class="sr-only" for="gate-play-server">Server</label>
|
|
799
|
-
<select id="gate-play-server" name="server" required>
|
|
800
|
-
<option value="" disabled selected>Server</option>
|
|
801
|
-
${serverOptions}
|
|
802
|
-
</select>
|
|
803
|
-
<label class="sr-only" for="gate-play-tool">Tool</label>
|
|
804
|
-
<input id="gate-play-tool" name="tool" type="text" placeholder="tool_name" required />
|
|
805
|
-
<button type="submit" class="stat">Evaluate</button>
|
|
806
|
-
</form>
|
|
807
|
-
<div id="gate-play-result"></div>
|
|
808
|
-
</div>`;
|
|
809
|
-
const quickActions = (() => {
|
|
810
|
-
if (page.state === "configured-stopped") {
|
|
811
|
-
return `<button type="button" class="stat ok" data-action="start" id="gate-action-start">Start</button>
|
|
812
|
-
<button type="button" class="stat" data-action="doctor" id="gate-action-doctor">Doctor</button>`;
|
|
813
|
-
}
|
|
814
|
-
return `<button type="button" class="stat bad" data-action="stop" id="gate-action-stop">Stop</button>
|
|
815
|
-
<button type="button" class="stat warn" data-action="restart" id="gate-action-restart">Restart</button>
|
|
816
|
-
<button type="button" class="stat" data-action="doctor" id="gate-action-doctor">Doctor</button>`;
|
|
817
|
-
})();
|
|
818
|
-
const eventsTable = (() => {
|
|
819
|
-
if (page.gateEvents.length === 0) {
|
|
820
|
-
return `<div class="card">
|
|
821
717
|
<h3>Gateway events</h3>
|
|
822
718
|
<p class="empty small">No gateway events in this window.</p>
|
|
823
719
|
</div>`;
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
720
|
+
}
|
|
721
|
+
const rows = [...events]
|
|
722
|
+
.sort((a, b) => b.ts.localeCompare(a.ts))
|
|
723
|
+
.slice(0, 25)
|
|
724
|
+
.map((e) => {
|
|
725
|
+
const server = toolServerPrefix(e.toolId) ?? "";
|
|
726
|
+
const tool = toolNameFromToolId(e.toolId) ?? e.toolId;
|
|
727
|
+
const t = tone(e.verdict);
|
|
728
|
+
const never = e.neverEvent || e.reasonCode === "NEVER_EVENT";
|
|
729
|
+
return ` <tr>
|
|
834
730
|
<td><time datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(clock(e.ts))} <span class="rel">${esc(relativeTime(e.ts, nowMs))}</span></time></td>
|
|
835
731
|
<td><a href="?f=server:${esc(mcpServerLabel(server))}#activity">${esc(server)}</a></td>
|
|
836
732
|
<td><code>${esc(tool)}</code></td>
|
|
837
733
|
<td><span class="pill ${t}${never ? " orr-red" : ""}">${esc(verdictWord(e.verdict))}${never ? " · never" : ""}</span></td>
|
|
838
734
|
</tr>`;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
735
|
+
})
|
|
736
|
+
.join("\n");
|
|
737
|
+
return `<div class="card">
|
|
842
738
|
<h3>Gateway events <span class="mute">latest 25</span></h3>
|
|
843
739
|
<table class="tbl">
|
|
844
740
|
<thead><tr><th>Time</th><th>Server</th><th>Tool</th><th>Verdict</th></tr></thead>
|
|
@@ -847,29 +743,229 @@ ${rows}
|
|
|
847
743
|
</tbody>
|
|
848
744
|
</table>
|
|
849
745
|
</div>`;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
${
|
|
855
|
-
<
|
|
856
|
-
|
|
857
|
-
|
|
746
|
+
}
|
|
747
|
+
function gateHomePage(page, nowMs) {
|
|
748
|
+
if (page.state === "not-set-up") {
|
|
749
|
+
return `<section class="zone gate-zone" id="gateway-home" aria-labelledby="tab-gateway-home">
|
|
750
|
+
${gateStatusHeader(page, nowMs)}
|
|
751
|
+
<p class="page-sub">Local MCP gate — every proxied tool call is evaluated and audited.</p>
|
|
752
|
+
<div class="card">
|
|
753
|
+
<span class="pill">not set up</span>
|
|
754
|
+
<p class="line">${esc(GATE_NOT_SETUP_QUICKSTART)}</p>
|
|
755
|
+
${gateQuickActions(page)}
|
|
858
756
|
</div>
|
|
859
|
-
|
|
757
|
+
</section>`;
|
|
758
|
+
}
|
|
759
|
+
return `<section class="zone gate-zone" id="gateway-home" aria-labelledby="tab-gateway-home">
|
|
760
|
+
${gateStatusHeader(page, nowMs)}
|
|
761
|
+
<p class="page-sub">Local MCP gate — every proxied tool call is evaluated and audited.</p>
|
|
762
|
+
${gateStatCards(page)}
|
|
860
763
|
<div class="card-grid">
|
|
861
|
-
|
|
764
|
+
<div class="card">
|
|
765
|
+
<h3>Listener</h3>
|
|
766
|
+
<p class="line">${page.url ? `<code>${esc(clip(page.url, 80))}</code>` : `Listener port ${page.port ?? 3930}`} · ${page.state}</p>
|
|
767
|
+
<p class="line">OTLP receiver port ${page.otlpPort}</p>
|
|
768
|
+
<p class="line">Failure mode <span class="pill">${esc(page.failureMode)}</span></p>
|
|
769
|
+
${page.binaryPresent ? `<p class="line">Binary <code>${page.binaryVersion ? esc(clip(page.binaryVersion, 40)) : "installed"}</code></p>` : `<p class="line">Binary missing — <code>kya gate setup</code></p>`}
|
|
770
|
+
</div>
|
|
862
771
|
<div class="card">
|
|
863
772
|
<h3>Quick actions</h3>
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
773
|
+
${gateQuickActions(page)}
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
${gateEventsTable(page.gateEvents, nowMs)}
|
|
777
|
+
</section>`;
|
|
778
|
+
}
|
|
779
|
+
function gateListenersPage(page) {
|
|
780
|
+
const rows = page.listeners
|
|
781
|
+
.map((l) => ` <tr>
|
|
782
|
+
<td><span class="pill ${l.state === "running" ? "ok" : ""}">${esc(l.state)}</span></td>
|
|
783
|
+
<td>${esc(l.name)}</td>
|
|
784
|
+
<td>${esc(l.protocol)}</td>
|
|
785
|
+
<td><code>${esc(l.address)}</code></td>
|
|
786
|
+
<td>${l.uptime ? `<time datetime="${esc(l.uptime)}">${esc(relativeTime(l.uptime, Date.now()))}</time>` : '<span class="mute">—</span>'}</td>
|
|
787
|
+
<td>${esc(l.detail ?? "")}</td>
|
|
788
|
+
</tr>`)
|
|
789
|
+
.join("\n");
|
|
790
|
+
return `<section class="zone gate-zone" id="gateway-listeners" aria-labelledby="tab-gateway-listeners">
|
|
791
|
+
<h2 class="page-title">Listeners</h2>
|
|
792
|
+
<p class="page-sub">MCP listener and OTLP receiver configured by the gateway.</p>
|
|
793
|
+
<div class="card">
|
|
794
|
+
<table class="tbl">
|
|
795
|
+
<thead><tr><th>State</th><th>Name</th><th>Protocol</th><th>Address</th><th>Since</th><th>Detail</th></tr></thead>
|
|
796
|
+
<tbody>
|
|
797
|
+
${rows}
|
|
798
|
+
</tbody>
|
|
799
|
+
</table>
|
|
800
|
+
</div>
|
|
801
|
+
</section>`;
|
|
802
|
+
}
|
|
803
|
+
function gateRoutesPage(page) {
|
|
804
|
+
if (page.routes.length === 0) {
|
|
805
|
+
return `<section class="zone gate-zone" id="gateway-routes" aria-labelledby="tab-gateway-routes">
|
|
806
|
+
<h2 class="page-title">Routes</h2>
|
|
807
|
+
<p class="page-sub">One route per configured backend server.</p>
|
|
808
|
+
<div class="card">
|
|
809
|
+
<p class="empty small">No servers configured — add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
|
|
810
|
+
</div>
|
|
811
|
+
</section>`;
|
|
812
|
+
}
|
|
813
|
+
const rows = page.routes
|
|
814
|
+
.map((r) => {
|
|
815
|
+
const dot = r.worst === "none" ? "" : `<span class="wdot ${r.worst}"></span>`;
|
|
816
|
+
const denied = r.deniedTools.length === 0
|
|
817
|
+
? '<span class="mute">—</span>'
|
|
818
|
+
: r.deniedTools.map((t) => `<span class="chip bad">${esc(t)}</span>`).join("");
|
|
819
|
+
return ` <tr data-server="${esc(r.backendLabel)}">
|
|
820
|
+
<td><code>${esc(r.pattern)}</code></td>
|
|
821
|
+
<td>${esc(r.backendLabel)}</td>
|
|
822
|
+
<td><span class="pill">${esc(r.tier ?? "unknown")}</span></td>
|
|
823
|
+
<td>${r.denyCount}</td>
|
|
824
|
+
<td>${dot} ${r.events}</td>
|
|
825
|
+
<td>${denied}</td>
|
|
826
|
+
</tr>`;
|
|
827
|
+
})
|
|
828
|
+
.join("\n");
|
|
829
|
+
return `<section class="zone gate-zone" id="gateway-routes" aria-labelledby="tab-gateway-routes">
|
|
830
|
+
<h2 class="page-title">Routes</h2>
|
|
831
|
+
<p class="page-sub">One route per configured backend server. Pattern matches proxied tool calls.</p>
|
|
832
|
+
<div class="card">
|
|
833
|
+
<table class="tbl">
|
|
834
|
+
<thead><tr><th>Pattern</th><th>Backend</th><th>Tier</th><th>Deny patterns</th><th>Events</th><th>Denied tools</th></tr></thead>
|
|
835
|
+
<tbody>
|
|
836
|
+
${rows}
|
|
837
|
+
</tbody>
|
|
838
|
+
</table>
|
|
839
|
+
</div>
|
|
840
|
+
</section>`;
|
|
841
|
+
}
|
|
842
|
+
function gateBackendsPage(page) {
|
|
843
|
+
if (page.servers.length === 0) {
|
|
844
|
+
return `<section class="zone gate-zone" id="gateway-backends" aria-labelledby="tab-gateway-backends">
|
|
845
|
+
<h2 class="page-title">Backends</h2>
|
|
846
|
+
<p class="page-sub">Configured upstream MCP servers.</p>
|
|
847
|
+
<div class="card">
|
|
848
|
+
<p class="empty small">No servers configured — add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
|
|
849
|
+
</div>
|
|
850
|
+
</section>`;
|
|
851
|
+
}
|
|
852
|
+
const rows = page.servers
|
|
853
|
+
.map((s) => {
|
|
854
|
+
const transport = s.transport === "stdio"
|
|
855
|
+
? `<code title="${esc(s.cmd?.join(" ") ?? "")}">${esc(clip(s.cmd?.join(" ") ?? s.transport, 60))}</code>`
|
|
856
|
+
: `<code title="${esc(s.url ?? "")}">${esc(clip(s.url ?? s.transport, 60))}</code>`;
|
|
857
|
+
const provenance = s.importedFrom.map((h) => `<span class="chip">${esc(h)}</span>`).join("");
|
|
858
|
+
const dot = s.worst === "none" ? "" : `<span class="wdot ${s.worst}"></span>`;
|
|
859
|
+
const denied = s.deniedTools.length === 0
|
|
860
|
+
? '<span class="mute">—</span>'
|
|
861
|
+
: s.deniedTools.map((t) => `<span class="chip bad">${esc(t)}</span>`).join("");
|
|
862
|
+
return ` <tr data-server="${esc(s.serverFacet)}">
|
|
863
|
+
<td><span class="wdot ${s.worst === "none" ? "mute" : s.worst}"></span> <code>${esc(s.id)}</code></td>
|
|
864
|
+
<td>${esc(s.transport)}</td>
|
|
865
|
+
<td>${transport}</td>
|
|
866
|
+
<td>${provenance}</td>
|
|
867
|
+
<td><span class="pill">${esc(s.policy.defaultTier ?? "unknown")}</span></td>
|
|
868
|
+
<td>${dot} ${s.events}</td>
|
|
869
|
+
<td>${denied}</td>
|
|
870
|
+
</tr>`;
|
|
871
|
+
})
|
|
872
|
+
.join("\n");
|
|
873
|
+
return `<section class="zone gate-zone" id="gateway-backends" aria-labelledby="tab-gateway-backends">
|
|
874
|
+
<h2 class="page-title">Backends</h2>
|
|
875
|
+
<p class="page-sub">Configured upstream MCP servers and their connection details.</p>
|
|
876
|
+
<div class="card">
|
|
877
|
+
<table class="tbl targets-table">
|
|
878
|
+
<thead><tr><th>Server</th><th>Transport</th><th>Target</th><th>Provenance</th><th>Policy</th><th>Events</th><th>Denied tools</th></tr></thead>
|
|
879
|
+
<tbody>
|
|
880
|
+
${rows}
|
|
881
|
+
</tbody>
|
|
882
|
+
</table>
|
|
883
|
+
</div>
|
|
884
|
+
</section>`;
|
|
885
|
+
}
|
|
886
|
+
function gatePoliciesPage(page) {
|
|
887
|
+
const { policySummary } = page;
|
|
888
|
+
const mixTotal = policySummary.verdicts.allow +
|
|
889
|
+
policySummary.verdicts.deny +
|
|
890
|
+
policySummary.verdicts.hold +
|
|
891
|
+
policySummary.verdicts.never;
|
|
892
|
+
const pct = (n) => (mixTotal > 0 ? Math.round((n / mixTotal) * 100) : 0);
|
|
893
|
+
const mixRow = (label, n, fill) => n > 0
|
|
894
|
+
? `<div class="mix-row"><span class="mix-lab">${esc(label)}</span><span class="mix-bar"><span class="mix-fill ${fill}" style="width:${pct(n)}%"></span></span><span class="mix-num">${n}</span></div>`
|
|
895
|
+
: "";
|
|
896
|
+
const verdictMix = `<div class="card">
|
|
897
|
+
<h3>Verdict mix</h3>
|
|
898
|
+
${mixRow("Allow", policySummary.verdicts.allow, "ok")}
|
|
899
|
+
${mixRow("Review", policySummary.verdicts.hold, "warn")}
|
|
900
|
+
${mixRow("Deny", policySummary.verdicts.deny, "bad")}
|
|
901
|
+
${mixRow("Never", policySummary.verdicts.never, "bad")}
|
|
902
|
+
<p class="mute small">${mixTotal} gateway event${mixTotal === 1 ? "" : "s"} in this window</p>
|
|
903
|
+
</div>`;
|
|
904
|
+
const denyCards = page.servers.length === 0
|
|
905
|
+
? '<div class="card"><h3>Per-server deny patterns</h3><p class="mute small">No servers configured.</p></div>'
|
|
906
|
+
: page.servers
|
|
907
|
+
.map((s) => `<div class="card">
|
|
908
|
+
<h3><code>${esc(s.id)}</code> · ${s.policy.denyPatterns.length} pattern${s.policy.denyPatterns.length === 1 ? "" : "s"}</h3>
|
|
909
|
+
${s.policy.denyPatterns.length === 0 ? '<p class="mute small">No deny patterns — all tools observed.</p>' : `<pre><code>${esc(s.policy.denyPatterns.join("\n"))}</code></pre>`}
|
|
910
|
+
</div>`)
|
|
911
|
+
.join("\n");
|
|
912
|
+
return `<section class="zone gate-zone" id="gateway-policies" aria-labelledby="tab-gateway-policies">
|
|
913
|
+
<h2 class="page-title">Policies</h2>
|
|
914
|
+
<p class="page-sub">Network rule, failure mode, and per-server deny patterns.</p>
|
|
915
|
+
<div class="card-grid">
|
|
916
|
+
<div class="card">
|
|
917
|
+
<h3>Network rule</h3>
|
|
918
|
+
<pre><code>${esc(policySummary.networkRule)}</code></pre>
|
|
919
|
+
<h3>Failure mode</h3>
|
|
920
|
+
<p class="line"><span class="pill">${esc(policySummary.failureMode)}</span></p>
|
|
921
|
+
<p class="mute small">Total policies evaluated: ${policySummary.totalPolicies}</p>
|
|
868
922
|
</div>
|
|
923
|
+
${verdictMix}
|
|
869
924
|
</div>
|
|
870
|
-
${
|
|
925
|
+
${denyCards}
|
|
871
926
|
</section>`;
|
|
872
927
|
}
|
|
928
|
+
function gatePlaygroundPage(page) {
|
|
929
|
+
const serverOptions = page.servers
|
|
930
|
+
.map((s) => `<option value="${esc(s.id)}">${esc(s.id)}</option>`)
|
|
931
|
+
.join("");
|
|
932
|
+
const samples = page.playgroundSamples.length === 0
|
|
933
|
+
? ""
|
|
934
|
+
: `<p class="mute small">Recent tools: ${page.playgroundSamples.map((t) => `<code>${esc(t)}</code>`).join(" ")}</p>`;
|
|
935
|
+
return `<section class="zone gate-zone" id="gateway-playground" aria-labelledby="tab-gateway-playground">
|
|
936
|
+
<h2 class="page-title">Playground</h2>
|
|
937
|
+
<p class="page-sub">Dry-run a tool call against the generated policy — no tool is executed.</p>
|
|
938
|
+
<div class="card">
|
|
939
|
+
<form class="playground-form" id="gate-play-form">
|
|
940
|
+
<label class="sr-only" for="gate-play-server">Server</label>
|
|
941
|
+
<select id="gate-play-server" name="server" required>
|
|
942
|
+
<option value="" disabled selected>Server</option>
|
|
943
|
+
${serverOptions}
|
|
944
|
+
</select>
|
|
945
|
+
<label class="sr-only" for="gate-play-tool">Tool</label>
|
|
946
|
+
<input id="gate-play-tool" name="tool" type="text" placeholder="tool_name" required />
|
|
947
|
+
<button type="submit" class="stat">Evaluate</button>
|
|
948
|
+
</form>
|
|
949
|
+
${samples}
|
|
950
|
+
<div id="gate-play-result"></div>
|
|
951
|
+
</div>
|
|
952
|
+
</section>`;
|
|
953
|
+
}
|
|
954
|
+
function gatePagePanel(page, nowMs) {
|
|
955
|
+
if (!page) {
|
|
956
|
+
return `<section class="zone" id="gateway-home" aria-labelledby="tab-gateway-home">
|
|
957
|
+
<p class="mute small zone-empty">Gateway details will appear here after setup.</p>
|
|
958
|
+
</section>`;
|
|
959
|
+
}
|
|
960
|
+
return [
|
|
961
|
+
gateHomePage(page, nowMs),
|
|
962
|
+
page.state === "not-set-up" ? "" : gateListenersPage(page),
|
|
963
|
+
page.state === "not-set-up" ? "" : gateRoutesPage(page),
|
|
964
|
+
page.state === "not-set-up" ? "" : gateBackendsPage(page),
|
|
965
|
+
page.state === "not-set-up" ? "" : gatePoliciesPage(page),
|
|
966
|
+
page.state === "not-set-up" ? "" : gatePlaygroundPage(page),
|
|
967
|
+
].join("\n");
|
|
968
|
+
}
|
|
873
969
|
function sandboxesPanel(card, nowMs) {
|
|
874
970
|
if (!card)
|
|
875
971
|
return "";
|
|
@@ -1132,29 +1228,34 @@ ${topRuns
|
|
|
1132
1228
|
<p class="mute small">${esc(SHOWBACK_DISCLAIMER)}</p>
|
|
1133
1229
|
</section>`;
|
|
1134
1230
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
* the feature is discoverable before setup; the System tab carries the full
|
|
1138
|
-
* panel (server rows, quickstart).
|
|
1139
|
-
*/
|
|
1140
|
-
function gateHeroCard(card) {
|
|
1141
|
-
if (!card)
|
|
1231
|
+
export function gatewayHeroCard(page, _nowMs) {
|
|
1232
|
+
if (!page)
|
|
1142
1233
|
return "";
|
|
1143
|
-
const
|
|
1144
|
-
? `${card.url ? `<code>${esc(clip(card.url, 48))}</code> · ` : ""}${card.servers.length} server${card.servers.length === 1 ? "" : "s"} · ${card.events} event${card.events === 1 ? "" : "s"}`
|
|
1145
|
-
: card.state === "configured-stopped"
|
|
1146
|
-
? `${card.servers.length} server${card.servers.length === 1 ? "" : "s"} configured — <code>kya gate run</code>`
|
|
1147
|
-
: `add an MCP server to a host config — <code>kya start</code> governs it`;
|
|
1148
|
-
const pill = card.state === "running"
|
|
1234
|
+
const statePill = page.state === "running"
|
|
1149
1235
|
? `<span class="pill ok">running</span>`
|
|
1150
|
-
:
|
|
1151
|
-
|
|
1236
|
+
: page.state === "not-set-up"
|
|
1237
|
+
? `<span class="pill">not set up</span>`
|
|
1238
|
+
: `<span class="pill">stopped</span>`;
|
|
1239
|
+
const binary = page.binaryPresent
|
|
1240
|
+
? `binary ${page.binaryVersion ? esc(clip(page.binaryVersion, 40)) : "installed"}`
|
|
1241
|
+
: "binary missing — kya gate setup";
|
|
1242
|
+
const detail = page.state === "running"
|
|
1243
|
+
? `${page.url ? `<code>${esc(clip(page.url, 60))}</code>` : `port ${page.port ?? 3930}`} · ${page.listeners.length} listener${page.listeners.length === 1 ? "" : "s"} · ${page.servers.length} backend${page.servers.length === 1 ? "" : "s"}`
|
|
1244
|
+
: `${page.servers.length} backend${page.servers.length === 1 ? "" : "s"} configured · ${binary}`;
|
|
1245
|
+
const denyCount = page.gateEvents.filter((e) => e.verdict.toUpperCase() === "DENY" || e.neverEvent || e.reasonCode === "NEVER_EVENT").length;
|
|
1246
|
+
return `<section class="panel" aria-label="Gateway">
|
|
1152
1247
|
<h2>Gateway</h2>
|
|
1153
1248
|
<div class="orrline">
|
|
1154
|
-
${
|
|
1249
|
+
${statePill}
|
|
1250
|
+
<span class="mute">${detail}</span>
|
|
1155
1251
|
</div>
|
|
1156
|
-
<
|
|
1157
|
-
|
|
1252
|
+
<div class="kpi-row">
|
|
1253
|
+
${kpiTile("Listeners", page.listeners.length)}
|
|
1254
|
+
${kpiTile("Backends", page.servers.length)}
|
|
1255
|
+
${kpiTile("Events", page.gateEvents.length)}
|
|
1256
|
+
${kpiTile("Denies", denyCount, denyCount > 0 ? "bad" : "")}
|
|
1257
|
+
</div>
|
|
1258
|
+
<p class="mute small"><a href="#gateway-home">Open Gateway</a></p>
|
|
1158
1259
|
</section>`;
|
|
1159
1260
|
}
|
|
1160
1261
|
/**
|
|
@@ -1172,9 +1273,99 @@ function normalizeTrailEvents(events) {
|
|
|
1172
1273
|
targetPath: e.targetPath ? clip(e.targetPath, 80) : undefined,
|
|
1173
1274
|
}));
|
|
1174
1275
|
}
|
|
1276
|
+
function eventMatchesFilter(e, group, value) {
|
|
1277
|
+
switch (group) {
|
|
1278
|
+
case "verdict":
|
|
1279
|
+
return e.verdict.toUpperCase() === value;
|
|
1280
|
+
case "never":
|
|
1281
|
+
return (e.neverEvent || e.reasonCode === "NEVER_EVENT") && value === "1";
|
|
1282
|
+
case "mode":
|
|
1283
|
+
return e.mode === value;
|
|
1284
|
+
case "plane":
|
|
1285
|
+
return (e.host?.trim() || "unknown") === value;
|
|
1286
|
+
case "product":
|
|
1287
|
+
return (e.product ?? "other") === value;
|
|
1288
|
+
case "project":
|
|
1289
|
+
if (value === "")
|
|
1290
|
+
return false;
|
|
1291
|
+
return (e.project?.trim() ?? "") === value;
|
|
1292
|
+
case "tool":
|
|
1293
|
+
return clip(e.toolId, 60) === value;
|
|
1294
|
+
case "session":
|
|
1295
|
+
return (e.sessionId || "unknown") === value;
|
|
1296
|
+
case "server": {
|
|
1297
|
+
const parsed = parseMcpToolId(e.toolId);
|
|
1298
|
+
return parsed != null && mcpServerLabel(parsed.server) === value;
|
|
1299
|
+
}
|
|
1300
|
+
default:
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function applyFeedFilters(events, filters) {
|
|
1305
|
+
const groups = Object.entries(filters)
|
|
1306
|
+
.map(([group, vals]) => [group, vals.filter((v) => v !== "")])
|
|
1307
|
+
.filter(([, vals]) => vals.length > 0);
|
|
1308
|
+
if (groups.length === 0)
|
|
1309
|
+
return [...events];
|
|
1310
|
+
return events.filter((e) => groups.every(([group, vals]) => vals.some((v) => eventMatchesFilter(e, group, v))));
|
|
1311
|
+
}
|
|
1312
|
+
export function renderFeedPage(events, filters, page, nowMs, live) {
|
|
1313
|
+
const sorted = [...events].sort((a, b) => b.ts.localeCompare(a.ts));
|
|
1314
|
+
const filtered = applyFeedFilters(sorted, filters);
|
|
1315
|
+
const total = filtered.length;
|
|
1316
|
+
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
1317
|
+
const p = clampPage(page, pages);
|
|
1318
|
+
const start = (p - 1) * PAGE_SIZE;
|
|
1319
|
+
const pageEvents = filtered.slice(start, start + PAGE_SIZE);
|
|
1320
|
+
return {
|
|
1321
|
+
feedHtml: renderFeed(pageEvents, nowMs, live),
|
|
1322
|
+
paginationHtml: paginationBar(total, p, pages),
|
|
1323
|
+
total,
|
|
1324
|
+
page: p,
|
|
1325
|
+
pages,
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function paginationBar(total, page, pages) {
|
|
1329
|
+
if (pages <= 1 && total === 0) {
|
|
1330
|
+
return `<div class="pagination-bar"><span class="mute">${total} events</span></div>`;
|
|
1331
|
+
}
|
|
1332
|
+
if (pages <= 1) {
|
|
1333
|
+
return `<div class="pagination-bar"><span class="mute">Page ${page} of ${pages} · ${total} event${total === 1 ? "" : "s"}</span></div>`;
|
|
1334
|
+
}
|
|
1335
|
+
const prev = `<button type="button" class="stat" data-page="prev" ${page <= 1 ? "disabled" : ""}>Previous</button>`;
|
|
1336
|
+
const next = `<button type="button" class="stat" data-page="next" ${page >= pages ? "disabled" : ""}>Next</button>`;
|
|
1337
|
+
const maxWindow = 7;
|
|
1338
|
+
let startPage = Math.max(1, page - Math.floor(maxWindow / 2));
|
|
1339
|
+
let endPage = Math.min(pages, startPage + maxWindow - 1);
|
|
1340
|
+
if (endPage - startPage + 1 < maxWindow) {
|
|
1341
|
+
startPage = Math.max(1, endPage - maxWindow + 1);
|
|
1342
|
+
}
|
|
1343
|
+
const numbers = [];
|
|
1344
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
1345
|
+
const active = i === page ? " active" : "";
|
|
1346
|
+
numbers.push(`<button type="button" class="stat${active}" data-page="${i}" ${i === page ? "disabled" : ""}>${i}</button>`);
|
|
1347
|
+
}
|
|
1348
|
+
return `<div class="pagination-bar">\n ${prev}\n ${numbers.join("\n ")}\n ${next}\n <span class="mute">Page ${page} of ${pages} · ${total} event${total === 1 ? "" : "s"}</span>\n</div>`;
|
|
1349
|
+
}
|
|
1175
1350
|
export function renderReceiptHtml(model) {
|
|
1176
1351
|
const nowMs = Date.now();
|
|
1177
1352
|
const events = normalizeTrailEvents(model.events);
|
|
1353
|
+
// Pagination + live filtering are only meaningful when the report is served
|
|
1354
|
+
// by the loopback live server (liveToken present). Static on-disk artifacts
|
|
1355
|
+
// render the full event list so nothing is hidden behind inactive buttons.
|
|
1356
|
+
const livePaged = !!model.liveToken;
|
|
1357
|
+
const currentPage = livePaged
|
|
1358
|
+
? clampPage(model.page ?? 1, Math.max(1, Math.ceil(events.length / PAGE_SIZE)))
|
|
1359
|
+
: 1;
|
|
1360
|
+
const initialFeed = livePaged
|
|
1361
|
+
? renderFeedPage(events, {}, currentPage, nowMs, model.live ?? false)
|
|
1362
|
+
: {
|
|
1363
|
+
feedHtml: renderFeed(events, nowMs, model.live ?? false),
|
|
1364
|
+
paginationHtml: `<div class="pagination-bar"><span class="mute">${events.length} event${events.length === 1 ? "" : "s"}</span></div>`,
|
|
1365
|
+
total: events.length,
|
|
1366
|
+
page: 1,
|
|
1367
|
+
pages: 1,
|
|
1368
|
+
};
|
|
1178
1369
|
const c = countVerdicts(events);
|
|
1179
1370
|
const agg = aggregateEvents(events);
|
|
1180
1371
|
const chips = statChips(agg);
|
|
@@ -1211,8 +1402,8 @@ export function renderReceiptHtml(model) {
|
|
|
1211
1402
|
})();
|
|
1212
1403
|
</script>`
|
|
1213
1404
|
: "";
|
|
1214
|
-
// Client-side chip filters
|
|
1215
|
-
//
|
|
1405
|
+
// Client-side chip filters and pagination: only emitted for live reports
|
|
1406
|
+
// (liveToken present). Static receipts render page 1 without interactivity.
|
|
1216
1407
|
// State maps are null-prototype: attacker-controlled values (project names)
|
|
1217
1408
|
// must never resolve via Object.prototype. The id lets tests extract just
|
|
1218
1409
|
// this script. KNOWN is space-delimited so `indexOf(' '+g+' ')` doubles as a
|
|
@@ -1223,23 +1414,43 @@ export function renderReceiptHtml(model) {
|
|
|
1223
1414
|
// chip toggle and every tab click would wipe the filter. Legacy #f= hashes
|
|
1224
1415
|
// are still parsed on load (read-only); the next save writes the ?f= form
|
|
1225
1416
|
// and drops the legacy fragment.
|
|
1226
|
-
const filterScript =
|
|
1417
|
+
const filterScript = model.liveToken
|
|
1418
|
+
? `<script id="kya-filters">
|
|
1227
1419
|
(function(){
|
|
1228
1420
|
var feed = document.getElementById('feed');
|
|
1229
1421
|
if (!feed) return;
|
|
1230
1422
|
var chg = document.getElementById('changes-list');
|
|
1423
|
+
var bar = document.getElementById('pagination-bar');
|
|
1231
1424
|
var KNOWN = ' verdict never mode plane product project tool session server ';
|
|
1232
1425
|
var state = Object.create(null);
|
|
1426
|
+
var page = 1;
|
|
1427
|
+
var LIVE = !!(document.querySelectorAll && document.querySelectorAll('main[data-live-token]').length);
|
|
1233
1428
|
var clearBtn = document.getElementById('clear-filters');
|
|
1234
1429
|
var chips = document.querySelectorAll('[data-fgroup]');
|
|
1430
|
+
function readToken(){
|
|
1431
|
+
var s = location.search;
|
|
1432
|
+
if (s.indexOf('?') !== 0) return '';
|
|
1433
|
+
var params = s.slice(1).split('&');
|
|
1434
|
+
for (var i = 0; i < params.length; i++) {
|
|
1435
|
+
if (params[i].indexOf('t=') === 0) {
|
|
1436
|
+
try { return decodeURIComponent(params[i].slice(2)); } catch(e){ return params[i].slice(2); }
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return '';
|
|
1440
|
+
}
|
|
1235
1441
|
function parse(){
|
|
1236
1442
|
state = Object.create(null);
|
|
1443
|
+
page = 1;
|
|
1237
1444
|
var raw = null;
|
|
1238
1445
|
var s = location.search;
|
|
1239
1446
|
if (s.indexOf('?') === 0) {
|
|
1240
1447
|
var params = s.slice(1).split('&');
|
|
1241
1448
|
for (var i = 0; i < params.length; i++) {
|
|
1242
|
-
if (params[i].indexOf('f=') === 0) { raw = params[i].slice(2);
|
|
1449
|
+
if (raw === null && params[i].indexOf('f=') === 0) { raw = params[i].slice(2); }
|
|
1450
|
+
else if (params[i].indexOf('page=') === 0) {
|
|
1451
|
+
var p = parseInt(params[i].slice(5), 10);
|
|
1452
|
+
if (p > 0) page = p;
|
|
1453
|
+
}
|
|
1243
1454
|
}
|
|
1244
1455
|
}
|
|
1245
1456
|
if (raw === null) {
|
|
@@ -1270,10 +1481,11 @@ export function renderReceiptHtml(model) {
|
|
|
1270
1481
|
if (s.indexOf('?') === 0) {
|
|
1271
1482
|
var params = s.slice(1).split('&');
|
|
1272
1483
|
for (var i = 0; i < params.length; i++) {
|
|
1273
|
-
if (params[i] && params[i].indexOf('f=') !== 0) kept.push(params[i]);
|
|
1484
|
+
if (params[i] && params[i].indexOf('f=') !== 0 && params[i].indexOf('page=') !== 0) kept.push(params[i]);
|
|
1274
1485
|
}
|
|
1275
1486
|
}
|
|
1276
1487
|
if (parts.length) kept.push('f=' + parts.join(','));
|
|
1488
|
+
if (page > 1) kept.push('page=' + page);
|
|
1277
1489
|
var q = kept.length ? '?' + kept.join('&') : '';
|
|
1278
1490
|
// The tab hash rides along untouched; a legacy #f= fragment is not a tab
|
|
1279
1491
|
// hash and is dropped now that the state lives in the query.
|
|
@@ -1294,17 +1506,21 @@ export function renderReceiptHtml(model) {
|
|
|
1294
1506
|
}
|
|
1295
1507
|
function apply(){
|
|
1296
1508
|
var on = active();
|
|
1297
|
-
feed
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
var
|
|
1304
|
-
if (
|
|
1305
|
-
|
|
1509
|
+
// Guard: the script is only emitted for live reports, so the feed is
|
|
1510
|
+
// always server-rendered; changes entries still filter client-side.
|
|
1511
|
+
if (!LIVE) {
|
|
1512
|
+
feed.querySelectorAll('.ev').forEach(function(row){
|
|
1513
|
+
row.classList.toggle('filtered-out', !rowVisible(row, on));
|
|
1514
|
+
});
|
|
1515
|
+
var day = null, dayHasRows = false;
|
|
1516
|
+
function flush(){ if (day) day.classList.toggle('filtered-out', on && !dayHasRows); }
|
|
1517
|
+
for (var i = 0; i < feed.children.length; i++) {
|
|
1518
|
+
var el = feed.children[i];
|
|
1519
|
+
if (el.classList.contains('day')) { flush(); day = el; dayHasRows = false; }
|
|
1520
|
+
else if (el.classList.contains('ev') && !el.classList.contains('filtered-out')) dayHasRows = true;
|
|
1521
|
+
}
|
|
1522
|
+
flush();
|
|
1306
1523
|
}
|
|
1307
|
-
flush();
|
|
1308
1524
|
// Changes tab: entries filter individually; file/session groups collapse
|
|
1309
1525
|
// when no entry inside survives.
|
|
1310
1526
|
if (chg) {
|
|
@@ -1322,6 +1538,21 @@ export function renderReceiptHtml(model) {
|
|
|
1322
1538
|
});
|
|
1323
1539
|
if (clearBtn) clearBtn.hidden = !on;
|
|
1324
1540
|
}
|
|
1541
|
+
function feedUpdate(){
|
|
1542
|
+
var T = readToken();
|
|
1543
|
+
if (!T) return;
|
|
1544
|
+
fetch('/feed?t=' + encodeURIComponent(T), {
|
|
1545
|
+
method: 'POST',
|
|
1546
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1547
|
+
body: JSON.stringify({ filters: state, page: page })
|
|
1548
|
+
})
|
|
1549
|
+
.then(function(r){ return r.json(); })
|
|
1550
|
+
.then(function(data){
|
|
1551
|
+
if (feed && data.feedHtml) feed.innerHTML = data.feedHtml;
|
|
1552
|
+
if (bar && data.paginationHtml) bar.innerHTML = data.paginationHtml;
|
|
1553
|
+
})
|
|
1554
|
+
.catch(function(err){ console.error('KYA feed update failed', err); });
|
|
1555
|
+
}
|
|
1325
1556
|
chips.forEach(function(chip){
|
|
1326
1557
|
chip.addEventListener('click', function(){
|
|
1327
1558
|
var g = chip.dataset.fgroup, v = chip.dataset.fvalue;
|
|
@@ -1331,13 +1562,32 @@ export function renderReceiptHtml(model) {
|
|
|
1331
1562
|
} else {
|
|
1332
1563
|
(state[g] || (state[g] = Object.create(null)))[v] = true;
|
|
1333
1564
|
}
|
|
1565
|
+
page = 1;
|
|
1334
1566
|
save(); apply();
|
|
1567
|
+
if (LIVE) feedUpdate();
|
|
1335
1568
|
});
|
|
1336
1569
|
});
|
|
1337
|
-
if (
|
|
1570
|
+
if (LIVE && bar) {
|
|
1571
|
+
bar.addEventListener('click', function(e){
|
|
1572
|
+
var btn = e.target.closest('button[data-page]');
|
|
1573
|
+
if (!btn) return;
|
|
1574
|
+
var dp = btn.getAttribute('data-page');
|
|
1575
|
+
if (dp === 'prev') page = Math.max(1, page - 1);
|
|
1576
|
+
else if (dp === 'next') page = page + 1;
|
|
1577
|
+
else {
|
|
1578
|
+
var np = parseInt(dp, 10);
|
|
1579
|
+
if (np > 0) page = np;
|
|
1580
|
+
}
|
|
1581
|
+
save(); apply();
|
|
1582
|
+
feedUpdate();
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
if (clearBtn) clearBtn.addEventListener('click', function(){ state = Object.create(null); page = 1; save(); apply(); if (LIVE) feedUpdate(); });
|
|
1338
1586
|
parse(); apply();
|
|
1587
|
+
if (LIVE && (active() || page !== 1)) feedUpdate();
|
|
1339
1588
|
})();
|
|
1340
|
-
</script
|
|
1589
|
+
</script>`
|
|
1590
|
+
: "";
|
|
1341
1591
|
const livePill = model.live ? `<span class="live" title="Watching trail.jsonl">Live</span>` : "";
|
|
1342
1592
|
const themeScript = `<script>
|
|
1343
1593
|
(function(){
|
|
@@ -1423,36 +1673,33 @@ export function renderReceiptHtml(model) {
|
|
|
1423
1673
|
});
|
|
1424
1674
|
}
|
|
1425
1675
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
.
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (action === 'doctor') {
|
|
1439
|
-
setResult(resultEl, '<pre>' + esc(JSON.stringify(data.report, null, 2)) + '</pre>', false);
|
|
1440
|
-
btn.disabled = false;
|
|
1441
|
-
} else {
|
|
1442
|
-
setTimeout(function(){ location.reload(); }, 800);
|
|
1443
|
-
}
|
|
1444
|
-
} else {
|
|
1445
|
-
setResult(resultEl, '<p>Error: ' + esc(data && data.error ? data.error : 'unknown') + '</p>', true);
|
|
1676
|
+
document.querySelectorAll('.gate-zone [data-action]').forEach(function(btn){
|
|
1677
|
+
btn.addEventListener('click', function(){
|
|
1678
|
+
var action = btn.getAttribute('data-action');
|
|
1679
|
+
var resultEl = document.getElementById('gate-doctor-result');
|
|
1680
|
+
btn.disabled = true;
|
|
1681
|
+
if (action === 'doctor') setResult(resultEl, 'Running doctor...', false);
|
|
1682
|
+
post('/gate/action', { action: action })
|
|
1683
|
+
.then(parseJson)
|
|
1684
|
+
.then(function(data){
|
|
1685
|
+
if (data && data.ok) {
|
|
1686
|
+
if (action === 'doctor') {
|
|
1687
|
+
setResult(resultEl, '<pre>' + esc(JSON.stringify(data.report, null, 2)) + '</pre>', false);
|
|
1446
1688
|
btn.disabled = false;
|
|
1689
|
+
} else {
|
|
1690
|
+
setTimeout(function(){ location.reload(); }, 800);
|
|
1447
1691
|
}
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
setResult(resultEl, '<p>Error: ' + esc(err.message) + '</p>', true);
|
|
1692
|
+
} else {
|
|
1693
|
+
setResult(resultEl, '<p>Error: ' + esc(data && data.error ? data.error : 'unknown') + '</p>', true);
|
|
1451
1694
|
btn.disabled = false;
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1695
|
+
}
|
|
1696
|
+
})
|
|
1697
|
+
.catch(function(err){
|
|
1698
|
+
setResult(resultEl, '<p>Error: ' + esc(err.message) + '</p>', true);
|
|
1699
|
+
btn.disabled = false;
|
|
1700
|
+
});
|
|
1454
1701
|
});
|
|
1455
|
-
}
|
|
1702
|
+
});
|
|
1456
1703
|
|
|
1457
1704
|
var stopLink = document.getElementById('stop-report');
|
|
1458
1705
|
if (stopLink) {
|
|
@@ -1515,7 +1762,13 @@ export function renderReceiptHtml(model) {
|
|
|
1515
1762
|
${navItem("#activity", "Activity", events.length)}
|
|
1516
1763
|
${navItem("#changes", "Changes", changes.fileCount)}
|
|
1517
1764
|
${navItem("#certify", "Certify", model.certify?.gap ?? 0)}
|
|
1518
|
-
${
|
|
1765
|
+
${navGroup("Gateway", model.gate?.state === "running")}
|
|
1766
|
+
${navSubItem("#gateway-home", "Home", model.gate?.servers.length ?? 0)}
|
|
1767
|
+
${navSubItem("#gateway-listeners", "Listeners", model.gate?.listeners?.length ?? 0)}
|
|
1768
|
+
${navSubItem("#gateway-routes", "Routes", model.gate?.routes?.length ?? 0)}
|
|
1769
|
+
${navSubItem("#gateway-backends", "Backends", model.gate?.servers.length ?? 0)}
|
|
1770
|
+
${navSubItem("#gateway-policies", "Policies", model.gate?.policySummary?.totalPolicies ?? 0)}
|
|
1771
|
+
${navSubItem("#gateway-playground", "Playground", model.gate?.playgroundSamples?.length ?? 0)}
|
|
1519
1772
|
${navItem("#system", "System", undefined)}
|
|
1520
1773
|
</nav>
|
|
1521
1774
|
<footer class="sidebar-footer">
|
|
@@ -1524,7 +1777,7 @@ export function renderReceiptHtml(model) {
|
|
|
1524
1777
|
<a href="#" id="stop-report">Stop report</a>
|
|
1525
1778
|
</footer>
|
|
1526
1779
|
</aside>
|
|
1527
|
-
<main>
|
|
1780
|
+
${model.liveToken ? '<main data-live-token="1">' : '<main>'}
|
|
1528
1781
|
<header class="page-header">
|
|
1529
1782
|
<div class="title-row">
|
|
1530
1783
|
<h1>${esc(model.title)}</h1>
|
|
@@ -1533,14 +1786,16 @@ export function renderReceiptHtml(model) {
|
|
|
1533
1786
|
${identityLine(model.identity)}
|
|
1534
1787
|
</header>
|
|
1535
1788
|
${blockedBanner}
|
|
1536
|
-
<div class="hero">
|
|
1537
|
-
${heroCertifyPanel(model.certify)}
|
|
1538
|
-
${verdictsHeroCard(c, events.length)}
|
|
1539
|
-
${activityHeroCard(agg, dashboard, events.length)}
|
|
1540
|
-
${gateHeroCard(model.gate)}
|
|
1541
|
-
${showbackHeroCard(model.showback)}
|
|
1542
|
-
</div>
|
|
1543
1789
|
<section class="zone" id="overview" aria-labelledby="tab-overview">
|
|
1790
|
+
<div class="hero">
|
|
1791
|
+
${heroCertifyPanel(model.certify)}
|
|
1792
|
+
<div class="hero-tiles">
|
|
1793
|
+
${verdictsHeroCard(c, events.length)}
|
|
1794
|
+
${activityHeroCard(agg, dashboard, events.length)}
|
|
1795
|
+
${gatewayHeroCard(model.gate, nowMs)}
|
|
1796
|
+
${showbackHeroCard(model.showback)}
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1544
1799
|
${overviewBody}
|
|
1545
1800
|
</section>
|
|
1546
1801
|
<section class="zone" id="activity" aria-labelledby="tab-activity">
|
|
@@ -1558,8 +1813,11 @@ export function renderReceiptHtml(model) {
|
|
|
1558
1813
|
${chips.servers}
|
|
1559
1814
|
</div>
|
|
1560
1815
|
<section class="feed" id="feed" aria-label="Activity feed">
|
|
1561
|
-
${
|
|
1816
|
+
${initialFeed.feedHtml}
|
|
1562
1817
|
</section>
|
|
1818
|
+
<div id="pagination-bar" aria-label="Activity pagination">
|
|
1819
|
+
${initialFeed.paginationHtml}
|
|
1820
|
+
</div>
|
|
1563
1821
|
</section>
|
|
1564
1822
|
<section class="zone" id="changes" aria-labelledby="tab-changes">
|
|
1565
1823
|
<section class="feed chg" id="changes-list" aria-label="Changes by session and file">
|