<!DOCTYPE html>
<html lang="en" data-theme="">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="{{CONTENT_SECURITY_POLICY}}" />
<title>{{PAGE_TITLE}}</title>
<style>
:root[data-theme="dark"] {
--color-background-primary: #1a1a1a;
--color-background-secondary: #242424;
--color-background-tertiary: #2e2e2e;
--color-background-info: #18324d;
--color-background-danger: #3d1f1f;
--color-background-warning: #3d2f1f;
--color-background-success: #1f3d2f;
--color-text-primary: #e8e8e8;
--color-text-secondary: #a0a0a0;
--color-text-tertiary: #707070;
--color-text-info: #6babff;
--color-text-danger: #ff7a7a;
--color-text-warning: #f0b05f;
--color-text-success: #5fd495;
--color-border-tertiary: rgba(255,255,255,0.1);
--color-border-secondary: rgba(255,255,255,0.2);
--color-border-info: #378add;
--color-border-success: #3d7d5c;
--page-bg: #121212;
}
:root[data-theme="light"] {
--color-background-primary: #ffffff;
--color-background-secondary: #f5f5f5;
--color-background-tertiary: #ececec;
--color-background-info: #e6f1fb;
--color-background-danger: #fcebeb;
--color-background-warning: #faeeda;
--color-background-success: #eaf3de;
--color-text-primary: #1a1a1a;
--color-text-secondary: #555555;
--color-text-tertiary: #888888;
--color-text-info: #0c447c;
--color-text-danger: #791f1f;
--color-text-warning: #633806;
--color-text-success: #27500a;
--color-border-tertiary: rgba(0,0,0,0.1);
--color-border-secondary: rgba(0,0,0,0.2);
--color-border-info: #378add;
--color-border-success: #7ab85a;
--page-bg: #fafafa;
}
:root {
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', Consolas, Monaco, monospace;
--border-radius-md: 8px;
--border-radius-lg: 12px;
}
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; background: var(--page-bg); color: var(--color-text-primary); font-family: var(--font-sans); font-size: 13px; line-height: 1.5; }
.ps-container { max-width: 1100px; margin: 0 auto; }
.ps-header-ctrl { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 10px; font-size: 11px; color: var(--color-text-tertiary); }
.ps-theme-toggle { background: transparent; border: 0.5px solid var(--color-border-tertiary); color: var(--color-text-secondary); padding: 4px 10px; border-radius: var(--border-radius-md); cursor: pointer; font-size: 11px; font-family: inherit; }
.ps-theme-toggle:hover { border-color: var(--color-border-secondary); }
.ps-topbar { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); background: var(--color-background-secondary); margin-bottom: 12px; flex-wrap: wrap; }
.ps-brand { font-weight: 500; font-size: 14px; }
.ps-meta { color: var(--color-text-secondary); font-size: 12px; font-family: var(--font-mono); }
.ps-gate-pass { margin-left: auto; background: var(--color-background-success); color: var(--color-text-success); padding: 3px 10px; border-radius: var(--border-radius-md); font-size: 11px; font-weight: 500; }
.ps-gate-fail { margin-left: auto; background: var(--color-background-danger); color: var(--color-text-danger); padding: 3px 10px; border-radius: var(--border-radius-md); font-size: 11px; font-weight: 500; }
.ps-tabs { display: flex; gap: 2px; border-bottom: 0.5px solid var(--color-border-tertiary); margin-bottom: 14px; }
.ps-tab { padding: 8px 14px; font-size: 13px; cursor: pointer; border: none; background: transparent; color: var(--color-text-secondary); border-bottom: 2px solid transparent; margin-bottom: -0.5px; font-family: inherit; }
.ps-tab:hover { color: var(--color-text-primary); }
.ps-tab.active { color: var(--color-text-primary); border-bottom-color: var(--color-text-primary); font-weight: 500; }
.ps-tab .ps-badge { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 10px; font-size: 10px; background: var(--color-background-tertiary); color: var(--color-text-secondary); }
.ps-metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 14px; }
.ps-metrics-3 { grid-template-columns: repeat(3, 1fr); }
.ps-metric { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 10px 12px; }
.ps-metric-label { font-size: 11px; color: var(--color-text-secondary); margin-bottom: 4px; }
.ps-metric-value { font-size: 20px; font-weight: 500; font-family: var(--font-mono); word-break: break-word; }
.ps-metric-value-sm { font-size: 14px; line-height: 1.4; }
.ps-metric-sub { font-size: 11px; color: var(--color-text-tertiary); font-family: var(--font-mono); }
.ps-filters { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; align-items: center; }
.ps-chip-group { display: contents; }
.ps-chip { padding: 4px 10px; font-size: 11px; border-radius: 12px; border: 0.5px solid var(--color-border-tertiary); background: transparent; cursor: pointer; color: var(--color-text-secondary); font-family: inherit; }
.ps-chip:hover { background: var(--color-background-secondary); }
.ps-chip.active { background: var(--color-text-primary); color: var(--color-background-primary); border-color: var(--color-text-primary); }
.ps-list { display: flex; flex-direction: column; gap: 6px; }
.ps-row { display: grid; grid-template-columns: 60px 1fr auto; align-items: center; gap: 10px; padding: 10px 12px; border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); cursor: pointer; background: var(--color-background-primary); }
.ps-row:hover { background: var(--color-background-secondary); border-color: var(--color-border-secondary); }
.ps-row.selected { border: 2px solid var(--color-border-info); padding: 9px 11px; }
.ps-sev-crit { background: var(--color-background-danger); color: var(--color-text-danger); font-size: 10px; font-weight: 500; padding: 3px 8px; border-radius: var(--border-radius-md); text-align: center; }
.ps-sev-warn { background: var(--color-background-warning); color: var(--color-text-warning); font-size: 10px; font-weight: 500; padding: 3px 8px; border-radius: var(--border-radius-md); text-align: center; }
.ps-sev-info { background: var(--color-background-info); color: var(--color-text-info); font-size: 10px; font-weight: 500; padding: 3px 8px; border-radius: var(--border-radius-md); text-align: center; }
.ps-badge-estimated { display: inline-block; background: var(--color-background-warning); color: var(--color-text-warning); padding: 3px 8px; border-radius: var(--border-radius-md); font-size: 10px; font-weight: 500; }
.ps-badge-measured { display: inline-block; background: var(--color-background-success); color: var(--color-text-success); padding: 3px 8px; border-radius: var(--border-radius-md); font-size: 10px; font-weight: 500; }
.ps-scoring-bandeau { display: flex; align-items: center; gap: 8px; margin-top: 14px; margin-bottom: 8px; font-size: 12px; flex-wrap: wrap; }
.ps-scoring-label { color: var(--color-text-secondary); font-weight: 500; }
.ps-scoring-chip { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; white-space: nowrap; }
.ps-scoring-chip-neutral { background: var(--color-background-secondary); color: var(--color-text-secondary); }
.ps-scoring-chip-warning { background: var(--color-background-warning); color: var(--color-text-warning); }
.ps-scoring-chip-accent { background: var(--color-background-secondary); color: var(--color-text-info); }
.ps-fin-main { min-width: 0; }
.ps-fin-type { font-weight: 500; font-size: 13px; margin-bottom: 2px; }
.ps-fin-service { color: var(--color-text-tertiary); font-weight: 400; font-size: 11px; }
.ps-fin-detail { font-size: 11px; color: var(--color-text-secondary); font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ps-fin-right { font-size: 11px; color: var(--color-text-secondary); font-family: var(--font-mono); text-align: right; white-space: nowrap; }
.ps-fin-right-sub { color: var(--color-text-tertiary); }
.ps-tree-header { font-size: 11px; color: var(--color-text-secondary); margin-bottom: 12px; padding: 8px 12px; background: var(--color-background-tertiary); border-radius: var(--border-radius-md); font-family: var(--font-mono); word-break: break-all; }
.ps-tree { font-family: var(--font-mono); font-size: 12px; padding: 10px 12px; background: var(--color-background-secondary); border-radius: var(--border-radius-md); line-height: 1.9; }
.ps-span { display: grid; grid-template-columns: 1fr auto; gap: 10px; padding: 2px 4px; border-radius: 4px; }
.ps-span.dim { color: var(--color-text-tertiary); }
.ps-span.hilite { background: var(--color-background-danger); }
.ps-span-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ps-span-dur { color: var(--color-text-tertiary); white-space: nowrap; }
.ps-span-find { color: var(--color-text-danger); font-weight: 500; }
.ps-drill { background: var(--color-background-info); border: 0.5px solid var(--color-border-info); border-radius: var(--border-radius-md); padding: 10px 12px; margin-top: 10px; margin-bottom: 12px; font-size: 12px; color: var(--color-text-info); }
.ps-drill-label { font-size: 10px; color: var(--color-text-info); margin-bottom: 4px; opacity: 0.8; }
.ps-drill-link { color: var(--color-text-info); }
.ps-banner { background: var(--color-background-info); border: 0.5px solid var(--color-border-info); border-radius: var(--border-radius-md); padding: 8px 12px; margin-bottom: 10px; font-size: 11px; color: var(--color-text-info); }
.ps-empty { padding: 30px; text-align: center; color: var(--color-text-secondary); font-size: 13px; background: var(--color-background-secondary); border-radius: var(--border-radius-md); }
.ps-table { width: 100%; font-size: 11px; font-family: var(--font-mono); border-collapse: collapse; }
.ps-table th { text-align: left; font-weight: 500; padding: 6px 8px; border-bottom: 0.5px solid var(--color-border-tertiary); color: var(--color-text-secondary); font-size: 10px; font-family: var(--font-sans); }
.ps-table td { padding: 6px 8px; border-bottom: 0.5px solid var(--color-border-tertiary); vertical-align: top; }
#pgstat-body tr.hilite { background: var(--color-background-danger); }
#pgstat-body tr.hilite td:first-child { color: var(--color-text-danger); font-weight: 500; }
.ps-footer { margin-top: 14px; font-size: 11px; color: var(--color-text-tertiary); text-align: center; }
.ps-credit { margin-top: 8px; font-size: 11px; color: var(--color-text-tertiary); text-align: center; }
.ps-credit a { color: var(--color-text-secondary); text-decoration: underline; }
.ps-credit a:hover { color: var(--color-text-primary); }
.ps-kbd { font-family: var(--font-mono); font-size: 10px; background: var(--color-background-tertiary); padding: 1px 5px; border-radius: 3px; color: var(--color-text-secondary); }
.ps-search { display: none; width: 100%; padding: 6px 10px; margin-bottom: 10px; font-size: 12px; font-family: var(--font-sans); background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); color: var(--color-text-primary); }
.ps-search:focus { outline: none; border-color: var(--color-border-info); }
.ps-span.ps-span-pgstat-link { cursor: pointer; }
.ps-span.ps-span-pgstat-link:hover .ps-span-text { text-decoration: underline; }
.ps-drill-clear { color: var(--color-text-info); text-decoration: underline; cursor: pointer; margin-left: 8px; }
.ps-diff-section { margin-bottom: 18px; }
.ps-diff-section-header { font-size: 12px; font-weight: 500; margin-bottom: 8px; color: var(--color-text-primary); }
.ps-diff-section-header.red { color: var(--color-text-danger); }
.ps-diff-section-header.green { color: var(--color-text-success); }
.ps-quality-gate { margin: 12px 0 18px 0; }
.ps-section-title { font-size: 12px; font-weight: 500; margin: 0 0 8px 0; color: var(--color-text-primary); }
.ps-panel-toolbar { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.ps-export-btn { padding: 4px 10px; font-size: 11px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-info); background: transparent; cursor: pointer; color: var(--color-text-info); font-family: inherit; }
.ps-export-btn:hover { background: var(--color-background-info); }
.ps-copy-link-btn { padding: 4px 10px; font-size: 11px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-info); background: transparent; cursor: pointer; color: var(--color-text-info); font-family: inherit; margin-left: 6px; }
.ps-copy-link-btn:hover { background: var(--color-background-info); }
.ps-correlation-clickable { cursor: pointer; }
.ps-correlation-clickable:hover { background: var(--color-background-tertiary); }
.ps-show-more { margin-top: 10px; padding: 6px 12px; font-size: 11px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-tertiary); background: transparent; color: var(--color-text-secondary); cursor: pointer; font-family: inherit; width: 100%; }
.ps-show-more:hover { background: var(--color-background-secondary); border-color: var(--color-border-secondary); }
dialog.ps-modal { position: fixed; inset: 0; margin: auto; max-width: 520px; width: calc(100% - 40px); height: fit-content; max-height: calc(100vh - 40px); overflow-y: auto; background: var(--color-background-secondary); color: var(--color-text-primary); border-radius: var(--border-radius-lg); border: 0.5px solid var(--color-border-tertiary); padding: 18px 22px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
dialog.ps-modal::backdrop { background: rgba(0,0,0,0.6); }
dialog.ps-modal h2 { margin: 0 0 12px 0; font-size: 14px; font-weight: 500; }
.ps-modal-close { position: absolute; top: 8px; right: 10px; background: transparent; border: none; color: var(--color-text-secondary); font-family: inherit; font-size: 16px; cursor: pointer; line-height: 1; padding: 4px 8px; }
.ps-modal-close:hover { color: var(--color-text-primary); }
dialog.ps-modal table { width: 100%; border-collapse: collapse; }
dialog.ps-modal th { text-align: left; padding: 4px 6px; font-size: 10px; font-weight: 500; color: var(--color-text-secondary); border-bottom: 0.5px solid var(--color-border-tertiary); font-family: var(--font-sans); }
dialog.ps-modal th:first-child { width: 90px; }
dialog.ps-modal td { padding: 5px 6px; font-size: 12px; vertical-align: top; }
dialog.ps-modal td:first-child { width: 90px; white-space: nowrap; }
.ps-daemon-status { display: none; align-items: center; gap: 6px; font-size: 11px; color: var(--color-text-secondary); margin-right: 12px; }
body.ps-live .ps-daemon-status { display: inline-flex; }
.ps-daemon-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-text-tertiary); display: inline-block; }
.ps-daemon-dot.connected { background: #2ea043; }
.ps-daemon-dot.disconnected { background: #cf222e; }
.ps-daemon-dot.unauthorized { background: #d4a72c; }
.ps-refresh-btn { display: none; padding: 4px 10px; font-size: 11px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-tertiary); background: transparent; color: var(--color-text-secondary); cursor: pointer; font-family: inherit; margin-right: 8px; }
body.ps-live .ps-refresh-btn { display: inline-block; }
body.ps-live .ps-refresh-btn[hidden] { display: none; }
.ps-refresh-btn:hover { background: var(--color-background-secondary); border-color: var(--color-border-secondary); color: var(--color-text-primary); }
.ps-refresh-btn:disabled { cursor: not-allowed; opacity: 0.6; }
.ps-fin-actions { display: none; margin-left: 8px; }
body.ps-live .ps-fin-actions { display: inline-flex; gap: 6px; }
.ps-fin-action-btn { padding: 3px 10px; font-size: 11px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-info); background: transparent; color: var(--color-text-info); cursor: pointer; font-family: inherit; }
.ps-fin-action-btn:hover { background: var(--color-background-info); }
.ps-fin-action-btn.revoke { border-color: var(--color-border-tertiary); color: var(--color-text-secondary); }
.ps-fin-action-btn.revoke:hover { background: var(--color-background-secondary); }
.ps-fin-action-btn:disabled { cursor: not-allowed; opacity: 0.5; }
.ps-include-acked-wrap { display: none; align-items: center; gap: 6px; font-size: 11px; color: var(--color-text-secondary); margin-left: 8px; margin-right: 14px; cursor: pointer; }
body.ps-live .ps-include-acked-wrap { display: inline-flex; }
.ps-acks-footer { margin-top: 10px; font-size: 11px; color: var(--color-text-tertiary); }
dialog.ps-modal label { display: block; font-size: 11px; color: var(--color-text-secondary); margin: 10px 0 4px 0; }
dialog.ps-modal input[type="password"], dialog.ps-modal input[type="text"], dialog.ps-modal textarea, dialog.ps-modal select { width: 100%; box-sizing: border-box; padding: 6px 8px; font-size: 12px; font-family: inherit; background: var(--color-background-tertiary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); color: var(--color-text-primary); }
dialog.ps-modal textarea { min-height: 60px; resize: vertical; }
dialog.ps-modal .ps-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
dialog.ps-modal .ps-modal-btn { padding: 5px 14px; font-size: 11px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-tertiary); background: transparent; color: var(--color-text-secondary); cursor: pointer; font-family: inherit; }
dialog.ps-modal .ps-modal-btn:hover { background: var(--color-background-tertiary); color: var(--color-text-primary); }
dialog.ps-modal .ps-modal-btn.primary { border-color: var(--color-border-info); color: var(--color-text-info); }
dialog.ps-modal .ps-modal-btn.primary:hover { background: var(--color-background-info); }
dialog.ps-modal .ps-modal-error { display: none; margin-top: 10px; padding: 8px 10px; font-size: 11px; background: var(--color-background-danger); color: var(--color-text-danger); border-radius: var(--border-radius-md); }
.ps-toast { position: fixed; bottom: 20px; right: 20px; padding: 10px 16px; font-size: 12px; background: var(--color-background-secondary); color: var(--color-text-primary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); box-shadow: 0 4px 16px rgba(0,0,0,0.3); z-index: 9999; max-width: 380px; }
.ps-toast.error { border-color: var(--color-border-tertiary); background: var(--color-background-danger); color: var(--color-text-danger); }
.ps-toast.success { border-color: var(--color-border-tertiary); background: var(--color-background-tertiary); color: var(--color-text-primary); }
</style>
</head>
<body>
<div class="ps-container">
<div class="ps-header-ctrl">
<output class="ps-daemon-status" id="ps-daemon-status" aria-live="polite">
<span class="ps-daemon-dot" id="ps-daemon-dot"></span>
<span id="ps-daemon-status-text">Connecting...</span>
</output>
<button class="ps-refresh-btn" id="ps-refresh-btn" type="button" aria-label="Refresh daemon data">Refresh</button>
<button class="ps-refresh-btn" id="ps-logout-btn" type="button" aria-label="Forget key for this tab (clears the daemon API key from session storage)" hidden>Forget key</button>
<button class="ps-theme-toggle" id="theme-toggle" type="button">Toggle theme</button>
</div>
<div class="ps-topbar" id="topbar"></div>
<div class="ps-tabs" id="tabs" role="tablist" aria-label="Dashboard sections"></div>
<div id="panel-findings" role="tabpanel" aria-labelledby="tab-findings">
<div class="ps-banner" id="trim-banner" style="display: none;"></div>
<input type="search" class="ps-search" id="findings-search" placeholder="Filter findings by type, service, endpoint or template..." aria-label="Filter findings" />
<div class="ps-metrics" id="findings-metrics"></div>
<div class="ps-quality-gate" id="quality-gate-rules" style="display: none;"></div>
<div class="ps-filters" id="findings-filters"></div>
<div class="ps-panel-toolbar">
<label class="ps-include-acked-wrap" id="findings-include-acked-wrap">
<input type="checkbox" id="findings-include-acked" />
<span>Show acknowledged</span>
</label>
<button type="button" class="ps-export-btn" id="findings-export" data-export-tab="findings">Export CSV</button>
<button type="button" class="ps-copy-link-btn" id="findings-copy-link" data-copy-link-tab="findings">Copy link</button>
</div>
<div class="ps-list" id="findings-list"></div>
<button type="button" class="ps-show-more" id="findings-show-more" style="display: none;"></button>
<div class="ps-empty" id="findings-empty" style="display: none;">No findings in this trace set.</div>
</div>
<div id="panel-explain" role="tabpanel" aria-labelledby="tab-explain" style="display: none;">
<div class="ps-empty" id="explain-empty">Click a finding to view its trace tree here.</div>
<div id="explain-content" style="display: none;">
<div class="ps-tree-header" id="explain-breadcrumb"></div>
<div class="ps-tree" id="explain-tree"></div>
<div class="ps-drill" id="explain-fix" style="display: none;"></div>
</div>
<div class="ps-empty" id="explain-not-embedded" style="display: none;"></div>
</div>
<div id="panel-pgstat" role="tabpanel" aria-labelledby="tab-pgstat" style="display: none;">
<div class="ps-drill" id="pgstat-drill" style="display: none;">
<div class="ps-drill-label">Filtered from Explain</div>
<span id="pgstat-drill-text"></span>
<span class="ps-drill-clear" id="pgstat-drill-clear">clear</span>
</div>
<div class="ps-filters" id="pgstat-rankings"></div>
<input type="search" class="ps-search" id="pgstat-search" placeholder="Filter by SQL template..." aria-label="Filter pg_stat rows" />
<div class="ps-panel-toolbar">
<button type="button" class="ps-export-btn" id="pgstat-export" data-export-tab="pgstat">Export CSV</button>
<button type="button" class="ps-copy-link-btn" id="pgstat-copy-link" data-copy-link-tab="pgstat">Copy link</button>
</div>
<table class="ps-table" id="pgstat-table">
<thead><tr><th>Template</th><th style="width: 80px;">Calls</th><th style="width: 100px;">Total ms</th><th style="width: 90px;">Mean ms</th></tr></thead>
<tbody id="pgstat-body"></tbody>
</table>
<div class="ps-empty" id="pgstat-empty" style="display: none;">No pg_stat entries.</div>
</div>
<div id="panel-diff" role="tabpanel" aria-labelledby="tab-diff" style="display: none;">
<input type="search" class="ps-search" id="diff-search" placeholder="Filter diff findings by type, service, endpoint or template..." aria-label="Filter diff findings" />
<div class="ps-panel-toolbar">
<button type="button" class="ps-export-btn" id="diff-export" data-export-tab="diff">Export CSV</button>
<button type="button" class="ps-copy-link-btn" id="diff-copy-link" data-copy-link-tab="diff">Copy link</button>
</div>
<div class="ps-diff-section">
<div class="ps-diff-section-header red" id="diff-new-header">New findings (0)</div>
<div class="ps-list" id="diff-new-list"></div>
</div>
<div class="ps-diff-section">
<div class="ps-diff-section-header green" id="diff-resolved-header">Resolved findings (0)</div>
<div class="ps-list" id="diff-resolved-list"></div>
</div>
<div class="ps-diff-section">
<div class="ps-diff-section-header" id="diff-sev-header">Severity changes (0)</div>
<table class="ps-table" id="diff-sev-table" style="display: none;">
<thead><tr><th>Type</th><th>Service</th><th>Endpoint</th><th>Before</th><th>After</th></tr></thead>
<tbody id="diff-sev-body"></tbody>
</table>
</div>
<div class="ps-diff-section">
<div class="ps-diff-section-header" id="diff-endp-header">Endpoint metric deltas (0)</div>
<table class="ps-table" id="diff-endp-table" style="display: none;">
<thead><tr><th>Service</th><th>Endpoint</th><th>Before I/O</th><th>After I/O</th><th>Delta</th></tr></thead>
<tbody id="diff-endp-body"></tbody>
</table>
</div>
</div>
<div id="panel-correlations" role="tabpanel" aria-labelledby="tab-correlations" style="display: none;">
<input type="search" class="ps-search" id="correlations-search" placeholder="Filter correlations by service or type..." aria-label="Filter correlations" />
<div class="ps-panel-toolbar">
<button type="button" class="ps-export-btn" id="correlations-export" data-export-tab="correlations">Export CSV</button>
<button type="button" class="ps-copy-link-btn" id="correlations-copy-link" data-copy-link-tab="correlations">Copy link</button>
</div>
<div class="ps-list" id="correlations-list"></div>
</div>
<div id="panel-green" role="tabpanel" aria-labelledby="tab-green" style="display: none;">
<div class="ps-metrics ps-metrics-3" id="green-metrics"></div>
<div id="green-scoring-config" class="ps-scoring-bandeau" style="display: none;" aria-label="Carbon scoring configuration"></div>
<div class="ps-tree-header" id="green-regions-header" style="margin-top: 14px;"></div>
<table class="ps-table" id="green-regions-table" style="display: none;">
<thead><tr><th>Region</th><th>Intensity (gCO2/kWh)</th><th>I/O ops</th><th>CO2</th><th>Source</th><th>Estimated</th></tr></thead>
<tbody id="green-regions-body"></tbody>
</table>
<div class="ps-empty" id="green-regions-empty" style="display: none;">No region breakdown available.</div>
</div>
<div id="panel-acknowledgments" role="tabpanel" aria-labelledby="tab-acknowledgments" style="display: none;">
<table class="ps-table" id="acks-table">
<thead><tr><th>Signature</th><th style="width: 140px;">By</th><th>Reason</th><th style="width: 120px;">Expires</th><th style="width: 90px;"></th></tr></thead>
<tbody id="acks-body"></tbody>
</table>
<div class="ps-empty" id="acks-empty" style="display: none;">No daemon acknowledgments active.</div>
<div class="ps-acks-footer" id="acks-footer"></div>
</div>
<div class="ps-footer">
<span class="ps-kbd">j</span>/<span class="ps-kbd">k</span> navigate . <span class="ps-kbd">enter</span> open . <span class="ps-kbd">/</span> search . <span class="ps-kbd">esc</span> back . <span class="ps-kbd">?</span> shortcuts
</div>
<div class="ps-credit">
Powered by <a href="https://github.com/robintra/perf-sentinel" target="_blank" rel="noopener noreferrer">perf-sentinel</a>
</div>
</div>
<dialog id="cheatsheet" class="ps-modal" aria-labelledby="cheatsheet-title">
<button type="button" class="ps-modal-close" id="cheatsheet-close" aria-label="Close">x</button>
<h2 id="cheatsheet-title">Keyboard shortcuts</h2>
<table id="cheatsheet-table">
<thead><tr><th scope="col">Key</th><th scope="col">Action</th></tr></thead>
<tbody id="cheatsheet-body"></tbody>
</table>
</dialog>
<dialog id="auth-modal" class="ps-modal" aria-labelledby="auth-modal-title">
<button type="button" class="ps-modal-close" id="auth-modal-close" aria-label="Close">x</button>
<h2 id="auth-modal-title">Daemon authentication required</h2>
<p id="auth-modal-hint" style="margin: 0 0 8px 0; font-size: 11px; color: var(--color-text-warning);">The daemon rejected the request with 401. Enter the API key to retry. The key is held in browser memory only for this tab session and cleared when the tab closes.</p>
<form id="auth-modal-form">
<label for="auth-modal-key">API key</label>
<input type="password" id="auth-modal-key" autocomplete="off" />
<div class="ps-modal-error" id="auth-modal-error" role="alert"></div>
<div class="ps-modal-actions">
<button type="button" class="ps-modal-btn" id="auth-modal-cancel">Cancel</button>
<button type="submit" class="ps-modal-btn primary" id="auth-modal-submit">Submit</button>
</div>
</form>
</dialog>
<dialog id="ack-modal" class="ps-modal" aria-labelledby="ack-modal-title">
<button type="button" class="ps-modal-close" id="ack-modal-close" aria-label="Close">x</button>
<h2 id="ack-modal-title">Acknowledge finding</h2>
<form id="ack-modal-form">
<label for="ack-modal-sig">Signature</label>
<input type="text" id="ack-modal-sig" readonly />
<label for="ack-modal-reason">Reason</label>
<textarea id="ack-modal-reason" required></textarea>
<label for="ack-modal-expires">Expires</label>
<select id="ack-modal-expires">
<option value="">Never (permanent)</option>
<option value="24h">24 hours</option>
<option value="7d" selected>7 days</option>
<option value="30d">30 days</option>
</select>
<label for="ack-modal-by">Acknowledged by</label>
<input type="text" id="ack-modal-by" placeholder="optional" />
<div class="ps-modal-error" id="ack-modal-error" role="alert"></div>
<div class="ps-modal-actions">
<button type="button" class="ps-modal-btn" id="ack-modal-cancel">Cancel</button>
<button type="submit" class="ps-modal-btn primary" id="ack-modal-submit">Acknowledge</button>
</div>
</form>
</dialog>
<script id="report-data" type="application/json">
{{REPORT_JSON}}
</script>
<script>
(function () {
"use strict";
var STORAGE_KEYS = {
theme: "perf-sentinel:theme",
pgstatRanking: "perf-sentinel:pgstat-ranking"
};
function sessionGet(key) {
try {
return globalThis.sessionStorage.getItem(key);
} catch {
return null;
}
}
function sessionSet(key, value) {
try {
globalThis.sessionStorage.setItem(key, value);
} catch {
return;
}
}
var THEME_MODES = ["auto", "dark", "light"];
var prefersDarkMQ = null;
try {
prefersDarkMQ = globalThis.matchMedia
? globalThis.matchMedia("(prefers-color-scheme: dark)")
: null;
} catch {
prefersDarkMQ = null;
}
function currentThemeMode() {
var stored = sessionGet(STORAGE_KEYS.theme);
return THEME_MODES.includes(stored) ? stored : "auto";
}
function resolveThemeColor(mode) {
if (mode === "dark" || mode === "light") return mode;
return prefersDarkMQ && prefersDarkMQ.matches ? "dark" : "light";
}
function applyTheme(mode) {
document.documentElement.dataset.theme = resolveThemeColor(mode);
var btn = document.getElementById("theme-toggle");
if (btn) btn.textContent = "Theme: " + mode;
}
applyTheme(currentThemeMode());
if (prefersDarkMQ && typeof prefersDarkMQ.addEventListener === "function") {
try {
prefersDarkMQ.addEventListener("change", function () {
if (currentThemeMode() === "auto") applyTheme("auto");
});
} catch {
}
}
function loadPayload() {
var raw = document.getElementById("report-data").textContent || "";
try {
return JSON.parse(raw);
} catch (e) {
document.body.textContent = "Failed to parse embedded report data: " + e.message;
return null;
}
}
function indexTracesById(list) {
var map = Object.create(null);
list.forEach(function (trace) { map[trace.trace_id] = trace; });
return map;
}
var payload = loadPayload();
if (!payload) return;
var report = payload.report || {};
var findings = report.findings || [];
var greenSummary = report.green_summary || null;
var hasGreen = !!(greenSummary && greenSummary.co2);
var embeddedTraces = payload.embedded_traces || [];
var tracesById = indexTracesById(embeddedTraces);
function el(tag, cls, text) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (text !== undefined && text !== null) n.textContent = String(text);
return n;
}
function setAttr(node, name, value) { node.setAttribute(name, String(value)); }
function safeHttpsHref(url) {
if (typeof url !== "string") return null;
if (!url.startsWith("https://")) return null;
return url;
}
function metricCard(label, value, sub, smallValue) {
var wrap = el("div", "ps-metric");
wrap.appendChild(el("div", "ps-metric-label", label));
var v = el("div", "ps-metric-value", value);
if (smallValue) v.classList.add("ps-metric-value-sm");
wrap.appendChild(v);
wrap.appendChild(el("div", "ps-metric-sub", sub));
return wrap;
}
function formatGco2(grams) {
if (!Number.isFinite(grams)) return "-";
var abs = Math.abs(grams);
if (abs >= 1) return grams.toFixed(3) + " g";
if (abs >= 0.001) return (grams * 1000).toFixed(3) + " mg";
if (abs >= 0.000001) return (grams * 1000000).toFixed(3) + " ug";
if (abs === 0) return "0 g";
return grams.toExponential(2) + " g";
}
function formatDurationUs(us) {
if (!Number.isFinite(us)) return "-";
var ms = us / 1000;
if (ms < 1) return ms.toFixed(2) + " ms";
if (ms < 10) return ms.toFixed(2) + " ms";
return Math.round(ms) + " ms";
}
function truncate(s, max) {
if (typeof s !== "string") return "";
if (s.length <= max) return s;
return s.slice(0, max - 1) + "...";
}
function percent(num, denom) {
if (!denom) return "0%";
return Math.round((num * 100) / denom) + "%";
}
var SEV_CLASS = { critical: "ps-sev-crit", warning: "ps-sev-warn", info: "ps-sev-info" };
var SEV_LABEL = { critical: "CRIT", warning: "WARN", info: "INFO" };
var TYPE_LABEL = {
n_plus_one_sql: "N+1 SQL",
n_plus_one_http: "N+1 HTTP",
redundant_sql: "Redundant SQL",
redundant_http: "Redundant HTTP",
slow_sql: "Slow SQL",
slow_http: "Slow HTTP",
excessive_fanout: "Excessive fanout",
chatty_service: "Chatty service",
pool_saturation: "Pool saturation",
serialized_calls: "Serialized calls"
};
function typeLabel(t) { return TYPE_LABEL[t] || String(t); }
function renderTopbar() {
var bar = document.getElementById("topbar");
var brand = el("span", "ps-brand", "perf-sentinel");
var version = el("span", "ps-meta", "v" + (payload.version || ""));
var analysis = report.analysis || { events_processed: 0, traces_analyzed: 0 };
var meta = el(
"span",
"ps-meta",
(payload.input_label || "-") +
" . " + analysis.traces_analyzed + " traces" +
" . " + analysis.events_processed + " spans"
);
var gate = report.quality_gate || { passed: true };
var badge = el(
"span",
gate.passed ? "ps-gate-pass" : "ps-gate-fail",
gate.passed ? "Quality gate passed" : "Quality gate failed"
);
bar.appendChild(brand);
bar.appendChild(version);
bar.appendChild(meta);
bar.appendChild(badge);
}
var tabs = [];
var registeredTabs = Object.create(null);
function registerTab(key, label, countFn) {
tabs.push({ key: key, label: label, countFn: countFn });
registeredTabs[key] = true;
}
function tabIsRegistered(key) { return !!registeredTabs[key]; }
function renderTabs() {
var bar = document.getElementById("tabs");
tabs.forEach(function (t) {
var btn = el("button", "ps-tab", t.label);
btn.type = "button";
setAttr(btn, "role", "tab");
setAttr(btn, "data-tab", t.key);
setAttr(btn, "id", "tab-" + t.key);
setAttr(btn, "aria-controls", "panel-" + t.key);
setAttr(btn, "aria-selected", "false");
setAttr(btn, "tabindex", "-1");
if (t.countFn) {
var c = t.countFn();
if (c !== null && c !== undefined) {
var badge = el("span", "ps-badge", String(c));
btn.appendChild(document.createTextNode(" "));
btn.appendChild(badge);
}
}
btn.addEventListener("click", function () { switchTab(t.key); });
bar.appendChild(btn);
});
wireTablistKeyboardNav(bar);
if (tabs.length > 0) switchTab(tabs[0].key);
}
function wireTablistKeyboardNav(bar) {
bar.addEventListener("keydown", function (event) {
var target = event.target;
if (!target || target.getAttribute("role") !== "tab") return;
var buttons = Array.prototype.slice.call(
bar.querySelectorAll("button[role=\"tab\"]")
);
if (buttons.length === 0) return;
var idx = buttons.indexOf(target);
if (idx < 0) return;
var next = -1;
switch (event.key) {
case "ArrowRight":
case "ArrowDown":
next = (idx + 1) % buttons.length;
break;
case "ArrowLeft":
case "ArrowUp":
next = (idx - 1 + buttons.length) % buttons.length;
break;
case "Home":
next = 0;
break;
case "End":
next = buttons.length - 1;
break;
case "Enter":
case " ":
event.preventDefault();
switchTab(target.dataset.tab, true);
return;
default:
return;
}
event.preventDefault();
var targetBtn = buttons[next];
if (targetBtn) {
targetBtn.focus();
switchTab(targetBtn.dataset.tab, true);
}
});
}
var currentTab = null;
function switchTab(key, fromKeyboard) {
if (typeof clearAllSearchFilters === "function") clearAllSearchFilters();
currentTab = key;
document.querySelectorAll(".ps-tab").forEach(function (btn) {
var active = btn.dataset.tab === key;
btn.classList.toggle("active", active);
setAttr(btn, "aria-selected", active ? "true" : "false");
setAttr(btn, "tabindex", active ? "0" : "-1");
});
tabs.forEach(function (t) {
var panel = document.getElementById("panel-" + t.key);
if (panel) panel.style.display = t.key === key ? "" : "none";
});
if (fromKeyboard) {
var active = document.getElementById("tab-" + key);
if (active && typeof active.focus === "function") {
try { active.focus(); } catch { }
}
}
scheduleHashWrite();
}
function countBySeverity() {
var c = { critical: 0, warning: 0, info: 0 };
findings.forEach(function (f) { if (c[f.severity] !== undefined) c[f.severity] += 1; });
return c;
}
function renderFindingsMetrics() {
var root = document.getElementById("findings-metrics");
var sev = countBySeverity();
var total = findings.length;
root.appendChild(metricCard(
"Findings",
String(total),
sev.critical + " crit . " + sev.warning + " warn . " + sev.info + " info"
));
var avoidable = greenSummary ? (greenSummary.avoidable_io_ops || 0) : 0;
var ratio = greenSummary ? Math.round((greenSummary.io_waste_ratio || 0) * 100) : 0;
root.appendChild(metricCard(
"Avoidable I/O",
String(avoidable),
ratio + "% waste ratio"
));
if (hasGreen) {
var co2 = greenSummary.co2;
root.appendChild(metricCard(
"CO2 estimate",
formatGco2(co2.total.mid),
String(co2.total.model || "")
));
}
var offender = greenSummary && greenSummary.top_offenders && greenSummary.top_offenders[0];
if (offender) {
root.appendChild(metricCard(
"Top offender",
truncate(offender.service, 24),
"IIS " + (offender.io_intensity_score || 0).toFixed(1),
true
));
}
}
function formatRuleNumber(n) {
if (!Number.isFinite(n)) return "-";
if (Number.isInteger(n)) return String(n);
return n.toFixed(3);
}
function renderQualityGateRules() {
var host = document.getElementById("quality-gate-rules");
if (!host) return;
host.replaceChildren();
var rules = (report.quality_gate && report.quality_gate.rules) || [];
if (rules.length === 0) {
host.style.display = "none";
return;
}
host.style.display = "";
host.appendChild(el("h3", "ps-section-title", "Quality gate rules"));
var table = el("table", "ps-table");
var thead = el("thead");
var headRow = el("tr");
["Rule", "Threshold", "Actual", "Status"].forEach(function (label) {
headRow.appendChild(el("th", null, label));
});
thead.appendChild(headRow);
table.appendChild(thead);
var tbody = el("tbody");
rules.forEach(function (r) {
var tr = el("tr");
tr.appendChild(el("td", null, r.rule));
tr.appendChild(el("td", null, formatRuleNumber(r.threshold)));
tr.appendChild(el("td", null, formatRuleNumber(r.actual)));
var statusTd = el("td");
statusTd.appendChild(el(
"span",
r.passed ? "ps-gate-pass" : "ps-gate-fail",
r.passed ? "PASS" : "FAIL"
));
tr.appendChild(statusTd);
tbody.appendChild(tr);
});
table.appendChild(tbody);
host.appendChild(table);
}
var filterState = { severity: null, service: null };
function renderFilterChips() {
var root = document.getElementById("findings-filters");
root.textContent = "";
var severityGroup = el("span", "ps-chip-group", null);
setAttr(severityGroup, "role", "radiogroup");
setAttr(severityGroup, "aria-label", "Finding severity");
root.appendChild(severityGroup);
var serviceGroup = el("span", "ps-chip-group", null);
setAttr(serviceGroup, "role", "group");
setAttr(serviceGroup, "aria-label", "Finding service");
root.appendChild(serviceGroup);
function appendChip(parent, label, key, chipRole, stateAttr) {
var btn = el("button", "ps-chip", label);
btn.type = "button";
setAttr(btn, "role", chipRole);
setAttr(btn, "data-key", key);
setAttr(btn, stateAttr, "false");
btn.addEventListener("click", function () { onFilterClick(key); });
parent.appendChild(btn);
}
appendChip(severityGroup, "All", "all", "radio", "aria-checked");
var hasCrit = findings.some(function (f) { return f.severity === "critical"; });
var hasWarn = findings.some(function (f) { return f.severity === "warning"; });
if (hasCrit) appendChip(severityGroup, "Critical", "sev:critical", "radio", "aria-checked");
if (hasWarn) appendChip(severityGroup, "Warning", "sev:warning", "radio", "aria-checked");
var services = Object.create(null);
findings.forEach(function (f) { services[f.service] = true; });
Object.keys(services).sort().forEach(function (s) {
appendChip(serviceGroup, s, "svc:" + s, "button", "aria-pressed");
});
syncFilterChips();
}
function onFilterClick(key) {
if (key === "all") {
filterState.severity = null;
filterState.service = null;
} else if (key.startsWith("sev:")) {
var sv = key.slice(4);
filterState.severity = filterState.severity === sv ? null : sv;
} else if (key.startsWith("svc:")) {
var svc = key.slice(4);
filterState.service = filterState.service === svc ? null : svc;
}
syncFilterChips();
resetShownCount();
renderFindingsList();
scheduleHashWrite();
}
function syncFilterChips() {
var anyActive = !!(filterState.severity || filterState.service);
document.querySelectorAll("#findings-filters .ps-chip").forEach(function (chip) {
var key = chip.dataset.key;
var active = false;
if (key === "all") active = !anyActive;
else if (key === "sev:" + filterState.severity) active = true;
else if (key === "svc:" + filterState.service) active = true;
chip.classList.toggle("active", active);
var role = chip.getAttribute("role");
var stateAttr = role === "radio" ? "aria-checked" : "aria-pressed";
setAttr(chip, stateAttr, active ? "true" : "false");
});
}
var LIST_CAP = 500;
var visibleFindings = [];
var selectedIndex = -1;
var shownCount = LIST_CAP;
function applyFilters() {
return findings.filter(function (f) {
if (filterState.severity && f.severity !== filterState.severity) return false;
if (filterState.service && f.service !== filterState.service) return false;
return true;
});
}
function renderFindingsList() {
var root = document.getElementById("findings-list");
var empty = document.getElementById("findings-empty");
root.textContent = "";
var filtered = applyFilters();
visibleFindings = filtered.slice(0, shownCount);
selectedIndex = visibleFindings.length > 0 ? 0 : -1;
if (visibleFindings.length === 0) {
empty.style.display = "";
syncShowMore(filtered.length);
return;
}
empty.style.display = "none";
visibleFindings.forEach(function (f, idx) {
root.appendChild(buildFindingRow(f, idx));
});
syncSelection();
syncShowMore(filtered.length);
}
function syncShowMore(totalMatching) {
var btn = document.getElementById("findings-show-more");
if (!btn) return;
if (visibleFindings.length >= totalMatching) {
btn.style.display = "none";
return;
}
var remaining = totalMatching - visibleFindings.length;
var next = Math.min(LIST_CAP, remaining);
btn.style.display = "";
btn.textContent = "Show " + next + " more findings (" + remaining + " remaining)";
}
function resetShownCount() { shownCount = LIST_CAP; }
function buildFindingRow(f, idx) {
var row = el("div", "ps-row");
setAttr(row, "data-idx", idx);
if (f.signature) setAttr(row, "data-sig", f.signature);
var sev = el("div", SEV_CLASS[f.severity] || "ps-sev-info", SEV_LABEL[f.severity] || "INFO");
row.appendChild(sev);
var main = el("div", "ps-fin-main");
var typeRow = el("div", "ps-fin-type");
typeRow.appendChild(document.createTextNode(typeLabel(f.type || f.finding_type)));
var svc = el("span", "ps-fin-service", " in " + (f.service || "-"));
typeRow.appendChild(svc);
main.appendChild(typeRow);
var tpl = (f.pattern && f.pattern.template) || "";
main.appendChild(el("div", "ps-fin-detail", tpl));
row.appendChild(main);
var right = el("div", "ps-fin-right");
var occ = (f.pattern && f.pattern.occurrences) || 0;
right.appendChild(document.createTextNode(occ + " occ"));
right.appendChild(document.createElement("br"));
right.appendChild(el("span", "ps-fin-right-sub", truncate(f.source_endpoint || "-", 48)));
row.appendChild(right);
var actions = el("div", "ps-fin-actions");
var btn = el("button", "ps-fin-action-btn", "Ack");
btn.type = "button";
setAttr(btn, "data-action", "ack");
setAttr(btn, "aria-label", "Acknowledge this finding");
actions.appendChild(btn);
row.appendChild(actions);
row.addEventListener("click", function (event) {
var t = event.target;
if (t && (t.classList.contains("ps-fin-action-btn") || t.closest && t.closest(".ps-fin-actions"))) {
return;
}
selectedIndex = idx;
syncSelection();
openExplain(f);
});
return row;
}
function syncSelection() {
document.querySelectorAll("#findings-list .ps-row").forEach(function (row, idx) {
row.classList.toggle("selected", idx === selectedIndex);
});
}
function renderExplainEmpty(message) {
document.getElementById("explain-empty").style.display = "none";
var content = document.getElementById("explain-content");
var notEmbedded = document.getElementById("explain-not-embedded");
content.style.display = "none";
notEmbedded.style.display = "";
notEmbedded.textContent = message;
switchTab("explain");
}
function openExplain(f) {
document.getElementById("explain-empty").style.display = "none";
var content = document.getElementById("explain-content");
var notEmbedded = document.getElementById("explain-not-embedded");
notEmbedded.style.display = "none";
notEmbedded.textContent = "";
var trace = tracesById[f.trace_id];
if (!trace) {
renderExplainEmpty(
"Trace not embedded (cap reached). Rerun with --max-traces-embedded <higher> to include it."
);
return;
}
content.style.display = "";
var breadcrumb = document.getElementById("explain-breadcrumb");
var shortTraceId = (f.trace_id || "").slice(0, 12);
breadcrumb.textContent =
"trace_id = " + shortTraceId + " . " + (f.service || "-") + " . " + (f.source_endpoint || "-");
var treeRoot = document.getElementById("explain-tree");
treeRoot.textContent = "";
renderTree(treeRoot, trace, f);
var fix = document.getElementById("explain-fix");
fix.textContent = "";
if (f.suggested_fix) {
fix.style.display = "";
renderSuggestedFix(fix, f.suggested_fix);
} else {
fix.style.display = "none";
}
switchTab("explain");
}
function buildSpanTree(spans) {
var byId = Object.create(null);
spans.forEach(function (s) { byId[s.span_id] = s; });
var childrenByParent = Object.create(null);
var roots = [];
spans.forEach(function (s, idx) {
var parent = s.parent_span_id;
if (parent && byId[parent]) {
if (!childrenByParent[parent]) childrenByParent[parent] = [];
childrenByParent[parent].push(idx);
} else {
roots.push(idx);
}
});
return { roots: roots, childrenByParent: childrenByParent };
}
function emitSpanTreeRows(root, spans, tree, targetTpl) {
var stack = [];
for (var r = tree.roots.length - 1; r >= 0; r--) {
stack.push({ idx: tree.roots[r], depth: 0 });
}
var emitted = 0;
var HARD_SPAN_CAP = 2000;
var MAX_DEPTH = 64;
while (stack.length > 0 && emitted < HARD_SPAN_CAP) {
var frame = stack.pop();
if (frame.depth > MAX_DEPTH) continue;
var span = spans[frame.idx];
root.appendChild(buildSpanRow(span, frame.depth, targetTpl));
emitted += 1;
var kids = tree.childrenByParent[span.span_id] || [];
for (var k = kids.length - 1; k >= 0; k--) {
stack.push({ idx: kids[k], depth: frame.depth + 1 });
}
}
}
function renderTree(root, trace, finding) {
var spans = trace.spans || [];
if (spans.length === 0) {
root.appendChild(el("div", "ps-span dim", "(empty trace)"));
return;
}
var tree = buildSpanTree(spans);
var targetTpl = (finding.pattern && finding.pattern.template) || null;
emitSpanTreeRows(root, spans, tree, targetTpl);
}
function buildSpanRow(span, depth, targetTpl) {
var row = el("div", "ps-span");
var isFinding = targetTpl && span.template === targetTpl;
if (isFinding) row.classList.add("hilite");
else row.classList.add("dim");
row.style.paddingLeft = (depth * 20) + "px";
var pgStatMatch =
hasPgStat && span.event_type === "sql" && pgStatByTemplate[span.template]
? span.template
: null;
if (pgStatMatch) {
row.classList.add("ps-span-pgstat-link");
row.addEventListener("click", function () {
switchTab("pgstat");
showPgStatDrill(pgStatMatch);
});
}
var text = el("span", "ps-span-text");
if (isFinding) text.classList.add("ps-span-find");
text.textContent = describeSpan(span);
row.appendChild(text);
row.appendChild(el("span", "ps-span-dur", formatDurationUs(span.duration_us)));
return row;
}
function describeSpan(span) {
if (span.event_type === "sql") {
return span.template || span.target || span.operation || "(sql)";
}
if (span.event_type === "http_out") {
var op = (span.operation || "GET").toUpperCase();
return op + " " + (span.template || span.target || "");
}
return span.operation || span.target || "(span)";
}
function renderSuggestedFix(root, fix) {
var label = el("div", "ps-drill-label", "Suggested fix . " + (fix.framework || "-"));
root.appendChild(label);
root.appendChild(document.createTextNode(fix.recommendation || ""));
var href = safeHttpsHref(fix.reference_url);
if (href) {
root.appendChild(document.createTextNode(" "));
var a = el("a", "ps-drill-link", "See documentation");
setAttr(a, "href", href);
setAttr(a, "target", "_blank");
setAttr(a, "rel", "noopener noreferrer");
root.appendChild(a);
}
}
function renderGreenPanel() {
if (!hasGreen) return;
var co2 = greenSummary.co2;
var metrics = document.getElementById("green-metrics");
metrics.appendChild(metricCard(
"Operational CO2",
formatGco2(co2.operational_gco2),
"low " + formatGco2(co2.total.low) + " . high " + formatGco2(co2.total.high)
));
metrics.appendChild(metricCard(
"Embodied CO2",
formatGco2(co2.embodied_gco2),
"SCI v1.0 M term"
));
var avoidMid = co2.avoidable.mid;
var ratio = percent(avoidMid, co2.operational_gco2);
metrics.appendChild(metricCard(
"Avoidable",
formatGco2(avoidMid),
ratio + " of operational"
));
renderScoringConfigBandeau(greenSummary.scoring_config);
var regions = greenSummary.regions || [];
var header = document.getElementById("green-regions-header");
header.textContent = "Regions (" + regions.length + ")";
if (regions.length === 0) {
document.getElementById("green-regions-empty").style.display = "";
return;
}
document.getElementById("green-regions-table").style.display = "";
var tbody = document.getElementById("green-regions-body");
regions.forEach(function (r) {
var tr = document.createElement("tr");
tr.appendChild(el("td", null, r.region || "-"));
tr.appendChild(el("td", null, (r.grid_intensity_gco2_kwh || 0).toFixed(0)));
tr.appendChild(el("td", null, String(r.io_ops || 0)));
tr.appendChild(el("td", null, formatGco2(r.co2_gco2 || 0)));
tr.appendChild(el("td", null, r.intensity_source || "-"));
tr.appendChild(buildEstimatedCell(r));
tbody.appendChild(tr);
});
}
function renderScoringConfigBandeau(cfg) {
var bandeau = document.getElementById("green-scoring-config");
if (!bandeau) return;
if (!cfg) {
bandeau.style.display = "none";
return;
}
bandeau.replaceChildren(
el("span", "ps-scoring-label", "Carbon scoring:"),
buildApiVersionChip(cfg.api_version),
buildEmissionFactorChip(cfg.emission_factor_type),
buildTemporalGranularityChip(cfg.temporal_granularity)
);
bandeau.style.display = "";
}
function buildScoringChip(label, modifier, tooltip) {
var chip = el("span", "ps-scoring-chip ps-scoring-chip-" + modifier, label);
chip.setAttribute("title", tooltip);
return chip;
}
function buildApiVersionChip(version) {
if (version === "v3") {
return buildScoringChip(
"Electricity Maps v3",
"warning",
"v3 is in legacy mode. Migrate to v4 to silence the deprecation warning."
);
}
if (version === "v4") {
return buildScoringChip(
"Electricity Maps v4",
"neutral",
"API v4 is the current default."
);
}
if (version === "custom") {
return buildScoringChip(
"Electricity Maps custom",
"neutral",
"Custom endpoint without /vN suffix (proxy or mock)."
);
}
return buildScoringChip(
"Electricity Maps " + String(version),
"neutral",
"Unrecognized API version, displayed verbatim."
);
}
function buildEmissionFactorChip(factor) {
if (factor === "direct") {
return buildScoringChip(
"direct",
"accent",
"Direct emissions only. Used by some Scope 2 frameworks."
);
}
if (factor === "lifecycle") {
return buildScoringChip(
"lifecycle",
"neutral",
"Default. Includes upstream emissions (manufacturing, transport)."
);
}
return buildScoringChip(
String(factor),
"neutral",
"Unrecognized emission factor type, displayed verbatim."
);
}
function buildTemporalGranularityChip(granularity) {
if (granularity === "5_minutes" || granularity === "15_minutes") {
return buildScoringChip(
granularity,
"accent",
"Sub-hour granularity. Requires a paid Electricity Maps plan."
);
}
if (granularity === "hourly") {
return buildScoringChip(
"hourly",
"neutral",
"Default. Hourly average carbon intensity."
);
}
return buildScoringChip(
String(granularity),
"neutral",
"Unrecognized temporal granularity, displayed verbatim."
);
}
function buildEstimatedCell(r) {
var td = document.createElement("td");
if (r.intensity_estimated === true) {
var badge = el("span", "ps-badge-estimated", "Estimated");
var tip = r.intensity_estimation_method
? "Method: " + r.intensity_estimation_method +
". Estimated by Electricity Maps using their internal model. " +
"Less precise than measured data but reliable for trend analysis."
: "Estimated by Electricity Maps. Less precise than measured data " +
"but reliable for trend analysis.";
badge.setAttribute("title", tip);
td.appendChild(badge);
} else if (r.intensity_estimated === false) {
var measured = el("span", "ps-badge-measured", "Measured");
measured.setAttribute("title", "Source data measured directly from the grid operator.");
td.appendChild(measured);
} else {
td.textContent = "-";
}
return td;
}
var pgStat = payload.pg_stat || null;
var pgStatByTemplate = Object.create(null);
if (pgStat && Array.isArray(pgStat.rankings)) {
pgStat.rankings.forEach(function (r) {
(r.entries || []).forEach(function (entry) {
pgStatByTemplate[entry.normalized_template] = entry;
});
});
}
var hasPgStat = Object.keys(pgStatByTemplate).length > 0;
var diffData = payload.diff || null;
var hasDiff = !!diffData;
var correlations =
report.correlations && report.correlations.length > 0 ? report.correlations : [];
var hasCorrelations = correlations.length > 0;
var pgStatRankings =
pgStat && Array.isArray(pgStat.rankings) ? pgStat.rankings : [];
var activeRankingIndex = 0;
var pgStatHiliteTemplate = null;
var PGSTAT_LABELS = {
"top by total_exec_time": "Total time",
"top by calls": "Calls",
"top by mean_exec_time": "Mean time",
"top by shared_blks_total": "I/O blocks"
};
function activePgStatEntries() {
if (pgStatRankings.length === 0) return [];
var idx = Math.min(activeRankingIndex, pgStatRankings.length - 1);
return pgStatRankings[idx].entries || [];
}
function renderPgStatPanel() {
if (!hasPgStat) {
document.getElementById("pgstat-empty").style.display = "";
return;
}
renderPgStatRankings();
renderPgStatTable();
}
function renderPgStatRankings() {
var root = document.getElementById("pgstat-rankings");
root.textContent = "";
if (pgStatRankings.length <= 1) {
root.style.display = "none";
return;
}
root.style.display = "";
setAttr(root, "role", "radiogroup");
setAttr(root, "aria-label", "pg_stat ranking");
pgStatRankings.forEach(function (r, idx) {
var label = PGSTAT_LABELS[r.label] || r.label || ("Ranking " + (idx + 1));
var btn = el("button", "ps-chip", label);
btn.type = "button";
setAttr(btn, "role", "radio");
setAttr(btn, "data-ranking-index", String(idx));
var isActive = idx === activeRankingIndex;
setAttr(btn, "aria-checked", isActive ? "true" : "false");
if (isActive) btn.classList.add("active");
btn.addEventListener("click", function () {
selectPgStatRanking(idx);
});
root.appendChild(btn);
});
}
function selectPgStatRanking(idx) {
sessionSet(STORAGE_KEYS.pgstatRanking, rankingSlugForIndex(idx));
if (idx === activeRankingIndex) return;
activeRankingIndex = idx;
var chips = document.querySelectorAll("#pgstat-rankings .ps-chip");
for (var i = 0; i < chips.length; i++) {
var isActive = i === idx;
chips[i].classList.toggle("active", isActive);
setAttr(chips[i], "aria-checked", isActive ? "true" : "false");
}
renderPgStatTable();
scheduleHashWrite();
if (pgStatHiliteTemplate) {
var stillPresent = activePgStatEntries().some(function (entry) {
return entry.normalized_template === pgStatHiliteTemplate;
});
if (stillPresent) {
highlightPgStatRow(pgStatHiliteTemplate);
} else {
clearPgStatDrill();
}
}
if (SEARCHABLE_TABS && SEARCHABLE_TABS.pgstat) {
applySearchFilter(SEARCHABLE_TABS.pgstat);
}
}
function renderPgStatTable() {
var body = document.getElementById("pgstat-body");
body.textContent = "";
activePgStatEntries().forEach(function (entry) {
var tr = document.createElement("tr");
setAttr(tr, "data-template", entry.normalized_template);
tr.appendChild(el("td", null, entry.normalized_template));
tr.appendChild(el("td", null, String(entry.calls || 0)));
tr.appendChild(el("td", null, (entry.total_exec_time_ms || 0).toFixed(1)));
tr.appendChild(el("td", null, (entry.mean_exec_time_ms || 0).toFixed(2)));
body.appendChild(tr);
});
}
function showPgStatDrill(template) {
var drill = document.getElementById("pgstat-drill");
var txt = document.getElementById("pgstat-drill-text");
drill.style.display = "";
txt.textContent = template;
pgStatHiliteTemplate = template;
highlightPgStatRow(template);
}
function clearPgStatDrill() {
document.getElementById("pgstat-drill").style.display = "none";
pgStatHiliteTemplate = null;
document.querySelectorAll("#pgstat-body tr").forEach(function (row) {
row.classList.remove("hilite");
});
}
function highlightPgStatRow(template) {
var matched = null;
document.querySelectorAll("#pgstat-body tr").forEach(function (row) {
if (row.dataset.template === template) {
row.classList.add("hilite");
matched = row;
} else {
row.classList.remove("hilite");
}
});
if (matched) matched.scrollIntoView({ block: "nearest" });
}
function renderDiffPanel() {
if (!hasDiff) return;
renderDiffFindingsList(
"diff-new-list",
"diff-new-header",
"New findings",
diffData.new_findings || [],
true
);
renderDiffFindingsList(
"diff-resolved-list",
"diff-resolved-header",
"Resolved findings",
diffData.resolved_findings || [],
"resolved"
);
renderDiffSevTable(diffData.severity_changes || []);
renderDiffEndpTable(diffData.endpoint_metric_deltas || []);
}
function renderDiffFindingsList(listId, headerId, label, items, kind) {
var list = document.getElementById(listId);
var hdr = document.getElementById(headerId);
hdr.textContent = label + " (" + items.length + ")";
list.textContent = "";
items.forEach(function (f) {
list.appendChild(buildDiffFindingRow(f, kind));
});
}
function buildDiffFindingRow(f, kind) {
var row = el("div", "ps-row");
row.appendChild(
el("div", SEV_CLASS[f.severity] || "ps-sev-info", SEV_LABEL[f.severity] || "INFO")
);
var main = el("div", "ps-fin-main");
var typeRow = el("div", "ps-fin-type");
typeRow.appendChild(document.createTextNode(typeLabel(f.type || f.finding_type)));
typeRow.appendChild(el("span", "ps-fin-service", " in " + (f.service || "-")));
main.appendChild(typeRow);
main.appendChild(el("div", "ps-fin-detail", (f.pattern && f.pattern.template) || ""));
row.appendChild(main);
var right = el("div", "ps-fin-right");
right.appendChild(document.createTextNode(((f.pattern && f.pattern.occurrences) || 0) + " occ"));
right.appendChild(document.createElement("br"));
right.appendChild(el("span", "ps-fin-right-sub", truncate(f.source_endpoint || "-", 48)));
row.appendChild(right);
if (kind === "resolved") {
row.addEventListener("click", function () {
renderExplainEmpty(
"This finding was resolved. Its trace lives in the baseline report, not in the current run. Open the baseline report separately to explore its traces."
);
});
} else if (kind === true || kind === "new") {
row.addEventListener("click", function () {
openExplain(f);
});
} else {
row.style.cursor = "default";
}
return row;
}
function renderDiffSevTable(items) {
var hdr = document.getElementById("diff-sev-header");
var tbl = document.getElementById("diff-sev-table");
var body = document.getElementById("diff-sev-body");
hdr.textContent = "Severity changes (" + items.length + ")";
body.textContent = "";
if (items.length === 0) {
tbl.style.display = "none";
return;
}
tbl.style.display = "";
items.forEach(function (c) {
var f = c.finding || {};
var tr = document.createElement("tr");
tr.appendChild(el("td", null, typeLabel(f.type || f.finding_type)));
tr.appendChild(el("td", null, f.service || "-"));
tr.appendChild(el("td", null, f.source_endpoint || "-"));
tr.appendChild(el("td", null, SEV_LABEL[c.before_severity] || String(c.before_severity)));
tr.appendChild(el("td", null, SEV_LABEL[c.after_severity] || String(c.after_severity)));
body.appendChild(tr);
});
}
function renderDiffEndpTable(items) {
var hdr = document.getElementById("diff-endp-header");
var tbl = document.getElementById("diff-endp-table");
var body = document.getElementById("diff-endp-body");
hdr.textContent = "Endpoint metric deltas (" + items.length + ")";
body.textContent = "";
if (items.length === 0) {
tbl.style.display = "none";
return;
}
tbl.style.display = "";
items.forEach(function (d) {
var tr = document.createElement("tr");
tr.appendChild(el("td", null, d.service || "-"));
tr.appendChild(el("td", null, d.endpoint || "-"));
tr.appendChild(el("td", null, String(d.before_io_ops || 0)));
tr.appendChild(el("td", null, String(d.after_io_ops || 0)));
var delta = d.delta || 0;
tr.appendChild(el("td", null, (delta > 0 ? "+" : "") + String(delta)));
body.appendChild(tr);
});
}
function renderCorrelationsPanel() {
if (!hasCorrelations) return;
var list = document.getElementById("correlations-list");
list.textContent = "";
correlations.forEach(function (c) {
var src = c.source || {};
var tgt = c.target || {};
var row = el("div", "ps-row ps-correlation-row");
var leftLabel =
(src.service || "?") + ":" + typeLabel(src.finding_type || "?");
var rightLabel =
(tgt.service || "?") + ":" + typeLabel(tgt.finding_type || "?");
var main = el("div", "ps-fin-main");
main.appendChild(el("div", "ps-fin-type", leftLabel + " -> " + rightLabel));
var conf = Math.round((c.confidence || 0) * 100);
var lag = (c.median_lag_ms || 0).toFixed(0);
main.appendChild(
el(
"div",
"ps-fin-detail",
"confidence " + conf + "% . median lag " + lag + " ms"
)
);
row.appendChild(el("div", "ps-sev-info", "CORR"));
row.appendChild(main);
row.appendChild(
el("div", "ps-fin-right", String(c.co_occurrence_count || 0) + "x")
);
if (c.sample_trace_id) {
row.classList.add("ps-correlation-clickable");
row.addEventListener("click", function () {
openExplain(syntheticFindingFromCorrelation(c));
});
} else {
row.style.cursor = "default";
}
list.appendChild(row);
});
}
function syntheticFindingFromCorrelation(c) {
var tgt = c.target || {};
return {
trace_id: c.sample_trace_id,
service: tgt.service || "-",
source_endpoint: "-",
pattern: { template: tgt.template || "" }
};
}
function matchRowByText(row, q) {
return (row.textContent || "").toLowerCase().includes(q);
}
var SEARCHABLE_TABS = {
findings: {
inputId: "findings-search",
rowsSelector: "#findings-list .ps-row"
},
pgstat: {
inputId: "pgstat-search",
rowsSelector: "#pgstat-body tr"
},
diff: {
inputId: "diff-search",
rowsSelector: "#diff-new-list .ps-row, #diff-resolved-list .ps-row"
},
correlations: {
inputId: "correlations-search",
rowsSelector: "#correlations-list .ps-row"
}
};
function initSearchInputs() {
Object.keys(SEARCHABLE_TABS).forEach(function (tabKey) {
var cfg = SEARCHABLE_TABS[tabKey];
var input = document.getElementById(cfg.inputId);
if (!input) return;
input.addEventListener("input", function () {
applySearchFilter(cfg);
scheduleHashWrite();
});
});
}
function applySearchFilter(cfg) {
var input = document.getElementById(cfg.inputId);
var q = (input.value || "").toLowerCase();
document.querySelectorAll(cfg.rowsSelector).forEach(function (row) {
row.style.display = !q || matchRowByText(row, q) ? "" : "none";
});
}
function openSearchForActiveTab() {
var cfg = SEARCHABLE_TABS[currentTab];
if (!cfg) return;
var input = document.getElementById(cfg.inputId);
if (!input) return;
input.style.display = "block";
input.focus();
input.select();
}
function closeSearch(input) {
input.value = "";
applySearchFilter(SEARCHABLE_TABS[currentTab]);
input.style.display = "none";
input.blur();
}
function clearAllSearchFilters() {
Object.keys(SEARCHABLE_TABS).forEach(function (tabKey) {
var cfg = SEARCHABLE_TABS[tabKey];
var input = document.getElementById(cfg.inputId);
if (!input) return;
if (input.value) {
input.value = "";
applySearchFilter(cfg);
}
input.style.display = "none";
});
}
function renderTrimBanner() {
var t = payload.trimmed_traces;
if (!t) return;
var b = document.getElementById("trim-banner");
b.style.display = "";
b.textContent = "Showing " + t.kept + " of " + t.total +
" traces (trimmed for file size). Rerun with --max-traces-embedded <higher> to see more.";
}
document.getElementById("theme-toggle").addEventListener("click", function () {
var idx = THEME_MODES.indexOf(currentThemeMode());
var next = THEME_MODES[(idx + 1) % THEME_MODES.length];
sessionSet(STORAGE_KEYS.theme, next);
applyTheme(next);
});
function csvEscape(value) {
if (value === null || value === undefined) return "";
var s = String(value);
if (s.length > 0 && "=+-@\t".includes(s.charAt(0))) {
s = "'" + s;
}
if (s.includes(",") || s.includes("\"") ||
s.includes("\n") || s.includes("\r")) {
return "\"" + s.replaceAll("\"", "\"\"") + "\"";
}
return s;
}
function csvLine(cells) {
return cells.map(csvEscape).join(",");
}
function buildCsv(header, rows) {
var lines = [csvLine(header)];
rows.forEach(function (r) { lines.push(csvLine(r)); });
return lines.join("\r\n") + "\r\n";
}
function sanitizeLabelForFilename(s) {
if (!s) return "";
var cleaned = String(s).replaceAll(/[^A-Za-z0-9]+/g, "-");
cleaned = cleaned.replaceAll(/-+/g, "-").replaceAll(/^-|-$/g, "");
return cleaned;
}
function pad2(n) { return n < 10 ? "0" + n : String(n); }
function timestampForFilename() {
var d = new Date();
return d.getFullYear() +
pad2(d.getMonth() + 1) +
pad2(d.getDate()) + "-" +
pad2(d.getHours()) +
pad2(d.getMinutes()) +
pad2(d.getSeconds());
}
function buildCsvFilename(tabName) {
var label = sanitizeLabelForFilename(payload.input_label || "");
var prefix = label ? "perf-sentinel-" + label : "perf-sentinel";
return prefix + "-" + tabName + "-" + timestampForFilename() + ".csv";
}
function downloadCsv(tabName, csvText) {
var blob = new Blob([csvText], { type: "text/csv;charset=utf-8" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = buildCsvFilename(tabName);
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
}
function searchTermFor(tabKey) {
var cfg = SEARCHABLE_TABS && SEARCHABLE_TABS[tabKey];
if (!cfg) return "";
var input = document.getElementById(cfg.inputId);
if (!input) return "";
return (input.value || "").toLowerCase();
}
function matchesSearch(haystack, term) {
if (!term) return true;
return String(haystack || "").toLowerCase().includes(term);
}
function exportFindingsCsv() {
var term = searchTermFor("findings");
var header = [
"severity", "type", "service", "endpoint", "template",
"occurrences", "first_timestamp", "last_timestamp",
"code_function", "code_filepath", "code_lineno",
"suggested_fix_framework", "suggested_fix_recommendation",
"confidence"
];
var rows = applyFilters().filter(function (f) {
var blob = [
f.severity, f.type || f.finding_type, f.service, f.source_endpoint,
f.pattern && f.pattern.template
].join(" ");
return matchesSearch(blob, term);
}).map(function (f) {
var p = f.pattern || {};
var loc = f.code_location || {};
var fix = f.suggested_fix || {};
return [
f.severity || "",
f.type || f.finding_type || "",
f.service || "",
f.source_endpoint || "",
p.template || "",
p.occurrences ?? "",
f.first_timestamp || "",
f.last_timestamp || "",
loc.function || "",
loc.filepath || "",
loc.lineno ?? "",
fix.framework || "",
fix.recommendation || "",
f.confidence ?? ""
];
});
downloadCsv("findings", buildCsv(header, rows));
}
function exportPgStatCsv() {
var term = searchTermFor("pgstat");
var header = [
"template", "calls", "total_exec_time_ms", "mean_exec_time_ms",
"rows", "shared_blks_hit", "shared_blks_read", "seen_in_traces"
];
var rows = activePgStatEntries().filter(function (e) {
return matchesSearch(e.normalized_template, term);
}).map(function (e) {
return [
e.normalized_template || "",
e.calls ?? "",
e.total_exec_time_ms ?? "",
e.mean_exec_time_ms ?? "",
e.rows ?? "",
e.shared_blks_hit ?? "",
e.shared_blks_read ?? "",
e.seen_in_traces ? "true" : "false"
];
});
downloadCsv("pgstat", buildCsv(header, rows));
}
function diffFindingMatchBlob(f) {
return [
f.severity, f.type || f.finding_type, f.service, f.source_endpoint,
f.pattern && f.pattern.template
].join(" ");
}
function exportDiffCsv() {
if (!hasDiff) return;
var term = searchTermFor("diff");
var header = [
"section", "severity", "type", "service", "endpoint", "template",
"occurrences", "before_severity", "after_severity",
"before_io_ops", "after_io_ops", "delta"
];
var rows = [];
(diffData.new_findings || []).forEach(function (f) {
if (!matchesSearch(diffFindingMatchBlob(f), term)) return;
var p = f.pattern || {};
rows.push([
"new", f.severity || "", f.type || f.finding_type || "",
f.service || "", f.source_endpoint || "", p.template || "",
p.occurrences ?? "",
"", "", "", "", ""
]);
});
(diffData.resolved_findings || []).forEach(function (f) {
if (!matchesSearch(diffFindingMatchBlob(f), term)) return;
var p = f.pattern || {};
rows.push([
"resolved", f.severity || "", f.type || f.finding_type || "",
f.service || "", f.source_endpoint || "", p.template || "",
p.occurrences ?? "",
"", "", "", "", ""
]);
});
(diffData.severity_changes || []).forEach(function (c) {
var f = c.finding || {};
if (!matchesSearch(diffFindingMatchBlob(f), term)) return;
var p = f.pattern || {};
rows.push([
"severity_change", "", f.type || f.finding_type || "",
f.service || "", f.source_endpoint || "", p.template || "",
p.occurrences ?? "",
c.before_severity || "", c.after_severity || "",
"", "", ""
]);
});
(diffData.endpoint_metric_deltas || []).forEach(function (d) {
var blob = [d.service, d.endpoint].join(" ");
if (!matchesSearch(blob, term)) return;
rows.push([
"endpoint_delta", "", "",
d.service || "", d.endpoint || "", "",
"", "", "",
d.before_io_ops ?? "",
d.after_io_ops ?? "",
d.delta ?? ""
]);
});
downloadCsv("diff", buildCsv(header, rows));
}
function exportCorrelationsCsv() {
var term = searchTermFor("correlations");
var header = [
"service_a", "type_a", "template_a",
"service_b", "type_b", "template_b",
"confidence", "co_occurrence_count",
"source_total_occurrences", "median_lag_ms",
"first_seen", "last_seen"
];
var rows = correlations.filter(function (c) {
var src = c.source || {};
var tgt = c.target || {};
var blob = [
src.service, src.finding_type, tgt.service, tgt.finding_type
].join(" ");
return matchesSearch(blob, term);
}).map(function (c) {
var s = c.source || {};
var t = c.target || {};
return [
s.service || "", s.finding_type || "", s.template || "",
t.service || "", t.finding_type || "", t.template || "",
c.confidence ?? "",
c.co_occurrence_count ?? "",
c.source_total_occurrences ?? "",
c.median_lag_ms ?? "",
c.first_seen || "", c.last_seen || ""
];
});
downloadCsv("correlations", buildCsv(header, rows));
}
var RANKING_SLUGS = ["total_time", "calls", "mean_time", "io_blocks"];
function rankingSlugForIndex(idx) {
return RANKING_SLUGS[idx] || String(idx);
}
function rankingIndexForSlug(slug) {
if (RANKING_SLUGS.includes(slug)) return RANKING_SLUGS.indexOf(slug);
var n = Number.parseInt(slug, 10);
return Number.isFinite(n) ? n : -1;
}
var HASH_KEYS = new Set(["search", "ranking", "severity", "service"]);
function readHash() {
var raw = (location.hash || "").replace(/^#/, "");
if (!raw) return null;
var parts = raw.split("&");
var state = { tab: parts.shift() || null };
parts.forEach(function (p) {
if (!p) return;
var eq = p.indexOf("=");
if (eq < 0) return;
var k = p.slice(0, eq);
if (!HASH_KEYS.has(k)) return;
var v = p.slice(eq + 1);
try {
v = decodeURIComponent(v);
} catch {
return;
}
state[k] = v;
});
return state;
}
function currentStateForHash() {
var state = { tab: currentTab };
if (currentTab && SEARCHABLE_TABS[currentTab]) {
var term = searchTermFor(currentTab);
if (term) state.search = term;
}
if (currentTab === "findings") {
if (filterState.severity) state.severity = filterState.severity;
if (filterState.service) state.service = filterState.service;
}
if (currentTab === "pgstat") {
state.ranking = rankingSlugForIndex(activeRankingIndex);
}
return state;
}
function encodeHashFromState(state) {
if (!state || !state.tab) return "";
var parts = [state.tab];
["search", "ranking", "severity", "service"].forEach(function (k) {
var v = state[k];
if (v !== null && v !== undefined && v !== "") {
parts.push(k + "=" + encodeURIComponent(String(v)));
}
});
return parts.join("&");
}
var _lastWrittenHash = null;
function writeHash(state) {
var hashStr = encodeHashFromState(state);
var full = hashStr ? "#" + hashStr : "";
try {
history.replaceState(null, "", full || location.pathname + location.search);
_lastWrittenHash = hashStr;
} catch {
location.hash = hashStr;
_lastWrittenHash = hashStr;
}
}
var hashWriteTimer = null;
function scheduleHashWrite() {
if (hashWriteTimer) clearTimeout(hashWriteTimer);
hashWriteTimer = setTimeout(function () {
hashWriteTimer = null;
writeHash(currentStateForHash());
}, 200);
}
function applyHash(state) {
if (!state || !state.tab) return false;
if (!tabIsRegistered(state.tab)) return false;
if (state.tab === "findings") {
filterState.severity = null;
filterState.service = null;
if (state.severity === "critical" || state.severity === "warning") {
filterState.severity = state.severity;
}
if (state.service) filterState.service = state.service;
syncFilterChips();
resetShownCount();
renderFindingsList();
}
if (state.tab === "pgstat") {
var idx = rankingIndexForSlug(state.ranking || "");
if (idx >= 0 && idx < pgStatRankings.length) {
selectPgStatRanking(idx);
}
}
switchTab(state.tab);
if (state.search && SEARCHABLE_TABS[state.tab]) {
var cfg = SEARCHABLE_TABS[state.tab];
var input = document.getElementById(cfg.inputId);
if (input) {
input.style.display = "block";
input.value = state.search;
applySearchFilter(cfg);
}
}
return true;
}
var CHEATSHEET_ROWS = [
["j", "Move finding selection down"],
["k", "Move finding selection up"],
["Enter", "Open selected finding in Explain"],
["Esc", "Close help, close search, clear filters, back from Explain"],
["/", "Open filter search for active tab"],
["g f", "Go to Findings"],
["g e", "Go to Explain (if available)"],
["g p", "Go to pg_stat (if available)"],
["g d", "Go to Diff (if available)"],
["g c", "Go to Correlations (if available)"],
["g r", "Go to GreenOps (if available)"],
["?", "Show this cheatsheet"]
];
function populateCheatsheet() {
var body = document.getElementById("cheatsheet-body");
if (!body) return;
body.textContent = "";
CHEATSHEET_ROWS.forEach(function (row) {
var tr = document.createElement("tr");
var tdK = document.createElement("td");
var kbd = el("span", "ps-kbd", row[0]);
tdK.appendChild(kbd);
tr.appendChild(tdK);
tr.appendChild(el("td", null, row[1]));
body.appendChild(tr);
});
}
function isCheatsheetOpen() {
var dlg = document.getElementById("cheatsheet");
return !!(dlg && dlg.open);
}
var cheatsheetPriorFocus = null;
function openCheatsheet() {
var dlg = document.getElementById("cheatsheet");
if (!dlg || dlg.open) return;
cheatsheetPriorFocus = document.activeElement;
dlg.showModal();
var closeBtn = document.getElementById("cheatsheet-close");
if (closeBtn) closeBtn.focus();
}
function closeCheatsheet() {
var dlg = document.getElementById("cheatsheet");
if (dlg && dlg.open) dlg.close();
}
function toggleCheatsheet() {
if (isCheatsheetOpen()) closeCheatsheet();
else openCheatsheet();
}
function anyFilterChipActive() {
return !!(filterState.severity || filterState.service);
}
function clearFilterChips() {
filterState.severity = null;
filterState.service = null;
syncFilterChips();
resetShownCount();
renderFindingsList();
}
function isSearchInputFocused() {
var active = document.activeElement;
return !!(active && active.tagName === "INPUT" && active.classList.contains("ps-search"));
}
function isTextInputFocused() {
var a = document.activeElement;
if (!a) return false;
var tag = a.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return true;
return a.isContentEditable === true;
}
function handleEscape(ev) {
if (isCheatsheetOpen()) {
closeCheatsheet();
ev.preventDefault();
return;
}
if (isSearchInputFocused()) {
closeSearch(document.activeElement);
ev.preventDefault();
return;
}
if (currentTab === "explain") {
switchTab("findings");
return;
}
if (currentTab === "findings" && anyFilterChipActive()) {
clearFilterChips();
scheduleHashWrite();
ev.preventDefault();
}
}
function handleSlash(ev) {
if (isSearchInputFocused() || !SEARCHABLE_TABS[currentTab]) return;
openSearchForActiveTab();
ev.preventDefault();
}
function handleFindingsNav(ev) {
if (visibleFindings.length === 0) return;
if (ev.key === "j") {
selectedIndex = (selectedIndex + 1) % visibleFindings.length;
syncSelection();
scrollSelectedIntoView();
} else if (ev.key === "k") {
selectedIndex = (selectedIndex - 1 + visibleFindings.length) % visibleFindings.length;
syncSelection();
scrollSelectedIntoView();
} else if (ev.key === "Enter" && selectedIndex >= 0) {
openExplain(visibleFindings[selectedIndex]);
}
ev.preventDefault();
}
var pendingGTimer = null;
var G_MAPPING = {
f: "findings", e: "explain", p: "pgstat",
d: "diff", c: "correlations", r: "green"
};
function clearPendingG() {
if (pendingGTimer) {
clearTimeout(pendingGTimer);
pendingGTimer = null;
}
}
function handleGPrefix(ev) {
if (ev.repeat) return false;
if (ev.key === "g" && pendingGTimer === null) {
pendingGTimer = setTimeout(function () { pendingGTimer = null; }, 1000);
return true;
}
if (pendingGTimer !== null) {
clearPendingG();
var target = G_MAPPING[ev.key];
if (target && tabIsRegistered(target)) {
switchTab(target);
ev.preventDefault();
}
return true;
}
return false;
}
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") { handleEscape(ev); return; }
if (ev.key === "?" && !isTextInputFocused()) {
toggleCheatsheet();
ev.preventDefault();
return;
}
if (isCheatsheetOpen()) return;
if (ev.key === "/") { handleSlash(ev); return; }
if (isSearchInputFocused()) return;
if (isTextInputFocused()) return;
if (handleGPrefix(ev)) return;
if (currentTab !== "findings") return;
if (ev.key === "j" || ev.key === "k" || ev.key === "Enter") {
handleFindingsNav(ev);
}
});
function scrollSelectedIntoView() {
var rows = document.querySelectorAll("#findings-list .ps-row");
if (selectedIndex >= 0 && selectedIndex < rows.length) {
rows[selectedIndex].scrollIntoView({ block: "nearest" });
}
}
document.getElementById("pgstat-drill-clear").addEventListener("click", clearPgStatDrill);
function registerAllTabs() {
registerTab("findings", "Findings", function () { return findings.length; });
registerTab("explain", "Explain", null);
if (hasPgStat) {
registerTab("pgstat", "pg_stat", function () {
return activePgStatEntries().length;
});
}
if (hasDiff) {
registerTab("diff", "Diff", function () {
return (
(diffData.new_findings || []).length + (diffData.resolved_findings || []).length
);
});
}
if (hasCorrelations) {
registerTab("correlations", "Correlations", function () {
return correlations.length;
});
}
if (hasGreen) registerTab("green", "GreenOps", null);
if (payload.daemon && payload.daemon.url) {
registerTab("acknowledgments", "Acks", function () {
return liveAcksCount();
});
}
renderTabs();
}
function restorePersistedPgStatRanking() {
if (!hasPgStat) return;
var storedRanking = sessionGet(STORAGE_KEYS.pgstatRanking);
if (!storedRanking) return;
var storedIdx = rankingIndexForSlug(storedRanking);
if (storedIdx >= 0 && storedIdx < pgStatRankings.length) {
activeRankingIndex = storedIdx;
}
}
function renderAllPanels() {
renderTrimBanner();
renderFindingsMetrics();
renderQualityGateRules();
renderFilterChips();
restorePersistedPgStatRanking();
renderFindingsList();
if (hasPgStat) renderPgStatPanel();
if (hasDiff) renderDiffPanel();
if (hasCorrelations) renderCorrelationsPanel();
if (hasGreen) renderGreenPanel();
}
function wireCheatsheetDialog() {
var closeBtn = document.getElementById("cheatsheet-close");
if (closeBtn) closeBtn.addEventListener("click", closeCheatsheet);
var dlg = document.getElementById("cheatsheet");
if (!dlg) return;
dlg.addEventListener("close", restorePriorFocus);
dlg.addEventListener("click", function (ev) {
if (ev.target === dlg) closeCheatsheet();
});
}
function restorePriorFocus() {
var prior = cheatsheetPriorFocus;
cheatsheetPriorFocus = null;
if (!prior || typeof prior.focus !== "function") return;
try {
prior.focus();
} catch {
return;
}
}
function wireShowMoreButton() {
var btn = document.getElementById("findings-show-more");
if (!btn) return;
btn.addEventListener("click", function () {
shownCount += LIST_CAP;
renderFindingsList();
});
}
function wireExportButtons() {
var exportHandlers = {
findings: exportFindingsCsv,
pgstat: exportPgStatCsv,
diff: exportDiffCsv,
correlations: exportCorrelationsCsv
};
Object.keys(exportHandlers).forEach(function (tabKey) {
var btn = document.getElementById(tabKey + "-export");
if (btn) btn.addEventListener("click", exportHandlers[tabKey]);
});
}
function copyCurrentLink(btn) {
if (!btn.dataset.baseLabel) btn.dataset.baseLabel = btn.textContent;
var baseLabel = btn.dataset.baseLabel;
var href = location.href;
function flashCopied() {
btn.textContent = "Copied";
if (btn._copyFlashTimer) clearTimeout(btn._copyFlashTimer);
btn._copyFlashTimer = setTimeout(function () {
btn._copyFlashTimer = null;
btn.textContent = baseLabel;
}, 1500);
}
function fallbackCopy() {
try {
var ta = document.createElement("textarea");
ta.value = href;
ta.setAttribute("readonly", "");
ta.style.position = "absolute";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
var ok = document.execCommand("copy");
ta.remove();
if (ok) {
flashCopied();
} else {
console.error("copyCurrentLink: execCommand('copy') returned false");
}
} catch (err) {
console.error("copyCurrentLink fallback failed", err);
}
}
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
navigator.clipboard.writeText(href).then(flashCopied, function (err) {
console.error("navigator.clipboard.writeText rejected", err);
fallbackCopy();
});
} else {
fallbackCopy();
}
}
function wireCopyLinkButtons() {
["findings", "pgstat", "diff", "correlations"].forEach(function (tabKey) {
var btn = document.getElementById(tabKey + "-copy-link");
if (!btn) return;
btn.addEventListener("click", function () {
copyCurrentLink(btn);
});
});
}
function applyInitialHashOrWrite() {
var initialHash = readHash();
if (applyHash(initialHash)) return;
writeHash(currentStateForHash());
}
function wireHashChangeListener() {
globalThis.addEventListener("hashchange", function () {
var raw = (location.hash || "").replace(/^#/, "");
if (raw === _lastWrittenHash) return;
applyHash(readHash());
});
}
var DAEMON_API_KEY_STORAGE = "perf-sentinel.daemon.api-key";
var DAEMON_ACKS_CAP = 1000;
var DAEMON_FETCH_TIMEOUT_MS = 10000;
var liveState = {
enabled: false,
daemonUrl: null,
apiKey: null,
acks: [],
acksBySig: Object.create(null),
pendingAuthRetry: null,
activeFindingsAbort: null
};
function liveAcksCount() {
return liveState.acks.length;
}
function isLiveEnabled() {
return liveState.enabled;
}
function setApiKey(value) {
if (typeof value !== "string" || value.trim() === "") {
clearApiKey();
return;
}
liveState.apiKey = value;
sessionSet(DAEMON_API_KEY_STORAGE, value);
}
function clearApiKey() {
liveState.apiKey = null;
try {
globalThis.sessionStorage.removeItem(DAEMON_API_KEY_STORAGE);
} catch {
sessionSet(DAEMON_API_KEY_STORAGE, "");
}
}
function liveDaemonBase() {
return liveState.daemonUrl || "";
}
function fetchWithAuth(path, opts) {
var options = opts || {};
var headers = {};
if (options.headers) {
Object.keys(options.headers).forEach(function (k) {
headers[k] = options.headers[k];
});
}
headers["Accept"] = "application/json";
if (liveState.apiKey) {
headers["X-API-Key"] = liveState.apiKey;
}
var ctrl = (typeof AbortController === "function") ? new AbortController() : null;
var init = {
method: options.method || "GET",
headers: headers,
credentials: "omit",
mode: "cors"
};
if (options.body !== undefined && options.body !== null) {
init.body = options.body;
}
if (ctrl) {
init.signal = ctrl.signal;
} else if (options.signal) {
init.signal = options.signal;
}
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, DAEMON_FETCH_TIMEOUT_MS) : null;
return fetch(liveDaemonBase() + path, init).finally(function () {
if (timer) clearTimeout(timer);
});
}
function setDaemonStatus(state, label) {
var dot = document.getElementById("ps-daemon-dot");
var text = document.getElementById("ps-daemon-status-text");
if (dot) {
dot.classList.remove("connected", "disconnected", "unauthorized");
dot.classList.add(state);
}
if (text) text.textContent = label;
}
function showToast(kind, message) {
var existing = document.getElementById("ps-toast");
if (existing) existing.remove();
var t = el("div", "ps-toast " + (kind || ""), message);
setAttr(t, "id", "ps-toast");
setAttr(t, "role", kind === "error" ? "alert" : "status");
document.body.appendChild(t);
setTimeout(function () {
t.remove();
}, 5000);
}
function pingStatus() {
return fetchWithAuth("/api/status").then(function (r) {
if (r.status === 200) {
setDaemonStatus("connected", "Connected");
return true;
}
if (r.status === 401) {
if (liveState.apiKey) {
clearApiKey();
syncLogoutButtonVisibility();
}
setDaemonStatus("unauthorized", "Authentication required");
return false;
}
setDaemonStatus("disconnected", "HTTP " + r.status);
return false;
}).catch(function () {
setDaemonStatus("disconnected", "Unreachable");
return false;
});
}
function clearAcksState() {
liveState.acks = [];
liveState.acksBySig = Object.create(null);
}
function applyAcksPayload(data) {
var arr = Array.isArray(data) ? data : [];
liveState.acks = arr;
liveState.acksBySig = Object.create(null);
for (var a of arr) {
if (a && a.signature) liveState.acksBySig[a.signature] = a;
}
}
function handleAcksResponse(r) {
if (!r.ok) {
if (r.status === 401) {
setDaemonStatus("unauthorized", "Authentication required");
}
clearAcksState();
return null;
}
return r.json().then(applyAcksPayload);
}
function fetchAcks() {
return fetchWithAuth("/api/acks").then(handleAcksResponse).catch(clearAcksState);
}
function renderAcksPanel() {
var body = document.getElementById("acks-body");
var empty = document.getElementById("acks-empty");
var footer = document.getElementById("acks-footer");
if (!body) return;
body.textContent = "";
if (liveState.acks.length === 0) {
if (empty) empty.style.display = "";
if (footer) footer.textContent = "";
return;
}
if (empty) empty.style.display = "none";
liveState.acks.forEach(function (a) {
body.appendChild(buildAckRow(a));
});
if (footer) {
var note = liveState.acks.length + " active";
if (liveState.acks.length >= DAEMON_ACKS_CAP) {
note += " (showing up to " + DAEMON_ACKS_CAP + ", consult the daemon for the full list)";
}
note += ". TOML CI acks are not listed here, see .perf-sentinel-acknowledgments.toml.";
footer.textContent = note;
}
}
function buildAckRow(ack) {
var tr = document.createElement("tr");
var sig = document.createElement("td");
sig.textContent = ack.signature || "-";
tr.appendChild(sig);
var by = document.createElement("td");
by.textContent = ack.by || "-";
tr.appendChild(by);
var reason = document.createElement("td");
reason.textContent = ack.reason || "";
tr.appendChild(reason);
var expires = document.createElement("td");
expires.textContent = ack.expires_at ? ack.expires_at : "never";
tr.appendChild(expires);
var act = document.createElement("td");
var btn = el("button", "ps-fin-action-btn revoke", "Revoke");
btn.type = "button";
setAttr(btn, "data-sig", ack.signature || "");
btn.addEventListener("click", function () {
revokeAck(ack.signature);
});
act.appendChild(btn);
tr.appendChild(act);
return tr;
}
function syncFindingActionButtons() {
if (!isLiveEnabled()) return;
var rows = document.querySelectorAll("#findings-list .ps-row[data-sig]");
rows.forEach(function (row) {
var sig = row.dataset.sig || "";
if (!sig) return;
var btn = row.querySelector(".ps-fin-action-btn");
if (!btn) return;
var isAcked = !!liveState.acksBySig[sig];
btn.textContent = isAcked ? "Revoke" : "Ack";
setAttr(btn, "data-action", isAcked ? "revoke" : "ack");
btn.classList.toggle("revoke", isAcked);
btn.onclick = function (event) {
event.stopPropagation();
if (isAcked) revokeAck(sig);
else openAckModal(sig);
};
});
applyAckedFilter();
}
function openAckModal(sig) {
var dlg = document.getElementById("ack-modal");
var sigInput = document.getElementById("ack-modal-sig");
var reasonInput = document.getElementById("ack-modal-reason");
var byInput = document.getElementById("ack-modal-by");
var err = document.getElementById("ack-modal-error");
if (!dlg || !sigInput || !reasonInput) return;
sigInput.value = sig;
reasonInput.value = "";
if (byInput) byInput.value = "";
if (err) { err.style.display = "none"; err.textContent = ""; }
if (typeof dlg.showModal === "function") dlg.showModal();
}
function closeAckModal() {
var dlg = document.getElementById("ack-modal");
if (dlg && typeof dlg.close === "function") dlg.close();
}
function expiresIsoFromShortcut(value) {
if (!value) return null;
var now = new Date();
var hours = 0;
if (value === "24h") hours = 24;
else if (value === "7d") hours = 24 * 7;
else if (value === "30d") hours = 24 * 30;
else return null;
var dt = new Date(now.getTime() + hours * 3600 * 1000);
return dt.toISOString();
}
function submitAckModal(event) {
event.preventDefault();
var sig = (document.getElementById("ack-modal-sig") || {}).value || "";
var reason = ((document.getElementById("ack-modal-reason") || {}).value || "").trim();
var expiresValue = (document.getElementById("ack-modal-expires") || {}).value || "";
var by = ((document.getElementById("ack-modal-by") || {}).value || "").trim();
var err = document.getElementById("ack-modal-error");
if (!sig) return;
if (!reason) {
if (err) { err.textContent = "Reason is required."; err.style.display = "block"; }
return;
}
var body = { reason: reason };
if (by) body.by = by;
var expiresIso = expiresIsoFromShortcut(expiresValue);
if (expiresIso) body.expires_at = expiresIso;
postAck(sig, body, err);
}
function postAck(sig, body, errEl) {
var path = "/api/findings/" + encodeURIComponent(sig) + "/ack";
fetchWithAuth(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
}).then(function (r) {
if (r.status === 201) {
closeAckModal();
showToast("success", "Acknowledged.");
return refreshLiveState();
}
return handleAuthRetry(r, function () { postAck(sig, body, errEl); }, errEl, "create");
}).catch(function () {
if (errEl) { errEl.textContent = "Network error reaching the daemon."; errEl.style.display = "block"; }
});
}
function revokeAck(sig) {
if (!sig) return;
if (!globalThis.confirm || !globalThis.confirm("Revoke acknowledgment for " + sig + "?")) return;
var path = "/api/findings/" + encodeURIComponent(sig) + "/ack";
fetchWithAuth(path, { method: "DELETE" }).then(function (r) {
if (r.status === 204) {
showToast("success", "Acknowledgment revoked.");
return refreshLiveState();
}
return handleAuthRetry(r, function () { revokeAck(sig); }, null, "revoke");
}).catch(function () {
showToast("error", "Network error reaching the daemon.");
});
}
function handleAuthRetry(response, retry, errEl, opLabel) {
if (response.status === 401) {
liveState.pendingAuthRetry = retry;
var hadKey = liveState.apiKey !== null && liveState.apiKey !== "";
if (hadKey) {
clearApiKey();
syncLogoutButtonVisibility();
}
openAuthModal(hadKey ? "Invalid API key, please try again." : null);
return;
}
var msg = "HTTP " + response.status + " on " + opLabel;
if (errEl) { errEl.textContent = msg; errEl.style.display = "block"; }
else showToast("error", msg);
}
function openAuthModal(errorMessage) {
var dlg = document.getElementById("auth-modal");
var keyInput = document.getElementById("auth-modal-key");
var err = document.getElementById("auth-modal-error");
if (!dlg) return;
if (keyInput) keyInput.value = "";
if (err) {
if (errorMessage) {
err.textContent = errorMessage;
err.style.display = "block";
} else {
err.style.display = "none";
err.textContent = "";
}
}
if (typeof dlg.showModal === "function") dlg.showModal();
if (keyInput) {
setTimeout(function () { keyInput.focus(); }, 0);
}
}
function closeAuthModal() {
var dlg = document.getElementById("auth-modal");
if (dlg && typeof dlg.close === "function") dlg.close();
}
function submitAuthModal(event) {
event.preventDefault();
var keyInput = document.getElementById("auth-modal-key");
var err = document.getElementById("auth-modal-error");
var k = ((keyInput || {}).value || "").trim();
if (!k) {
if (err) { err.textContent = "API key is required."; err.style.display = "block"; }
return;
}
setApiKey(k);
syncLogoutButtonVisibility();
closeAuthModal();
var retry = liveState.pendingAuthRetry;
liveState.pendingAuthRetry = null;
pingStatus();
if (typeof retry === "function") retry();
}
function refreshLiveState() {
return Promise.all([pingStatus(), fetchAcks()]).then(function () {
renderAcksPanel();
syncFindingActionButtons();
syncAcksTabBadge();
});
}
function syncAcksTabBadge() {
var btn = document.getElementById("tab-acknowledgments");
if (!btn) return;
var count = liveAcksCount();
if (count === null || count === undefined) return;
var badge = btn.querySelector(".ps-badge");
if (badge) {
badge.textContent = String(count);
} else {
btn.appendChild(document.createTextNode(" "));
btn.appendChild(el("span", "ps-badge", String(count)));
}
}
function wireRefreshButton() {
var btn = document.getElementById("ps-refresh-btn");
if (!btn) return;
btn.addEventListener("click", function () {
btn.disabled = true;
refreshLiveState().then(function () { btn.disabled = false; });
});
}
function syncLogoutButtonVisibility() {
var btn = document.getElementById("ps-logout-btn");
if (!btn) return;
if (liveState.apiKey) btn.hidden = false;
else btn.hidden = true;
}
function wireLogoutButton() {
var btn = document.getElementById("ps-logout-btn");
if (!btn) return;
btn.addEventListener("click", function () {
clearApiKey();
syncLogoutButtonVisibility();
showToast("success", "API key cleared from this tab.");
pingStatus();
});
}
function applyAckedFilter() {
var input = document.getElementById("findings-include-acked");
if (!input) return;
var hideAcked = !input.checked;
var rows = document.querySelectorAll("#findings-list .ps-row[data-sig]");
rows.forEach(function (row) {
var sig = row.dataset.sig || "";
var isAcked = !!liveState.acksBySig[sig];
if (hideAcked && isAcked) row.style.display = "none";
else row.style.display = "";
});
}
function wireIncludeAckedToggle() {
var input = document.getElementById("findings-include-acked");
if (!input) return;
input.addEventListener("change", applyAckedFilter);
}
function wireAuthModal() {
var form = document.getElementById("auth-modal-form");
var cancel = document.getElementById("auth-modal-cancel");
var close = document.getElementById("auth-modal-close");
if (form) form.addEventListener("submit", submitAuthModal);
if (cancel) cancel.addEventListener("click", function () {
closeAuthModal();
liveState.pendingAuthRetry = null;
setDaemonStatus("unauthorized", "Authentication required");
});
if (close) close.addEventListener("click", function () {
closeAuthModal();
liveState.pendingAuthRetry = null;
});
}
function wireAckModal() {
var form = document.getElementById("ack-modal-form");
var cancel = document.getElementById("ack-modal-cancel");
var close = document.getElementById("ack-modal-close");
if (form) form.addEventListener("submit", submitAckModal);
if (cancel) cancel.addEventListener("click", closeAckModal);
if (close) close.addEventListener("click", closeAckModal);
}
function stripTrailingSlashes(s) {
var end = s.length;
while (end > 0 && s.codePointAt(end - 1) === 47) end--;
return s.substring(0, end);
}
function bootLiveMode() {
if (!payload.daemon || !payload.daemon.url) return;
liveState.enabled = true;
liveState.daemonUrl = stripTrailingSlashes(String(payload.daemon.url));
var stored = sessionGet(DAEMON_API_KEY_STORAGE);
if (stored) liveState.apiKey = stored;
document.body.classList.add("ps-live");
wireRefreshButton();
wireLogoutButton();
wireIncludeAckedToggle();
wireAuthModal();
wireAckModal();
syncLogoutButtonVisibility();
setDaemonStatus("disconnected", "Connecting...");
refreshLiveState();
}
renderTopbar();
registerAllTabs();
renderAllPanels();
initSearchInputs();
populateCheatsheet();
wireCheatsheetDialog();
wireShowMoreButton();
wireExportButtons();
wireCopyLinkButtons();
applyInitialHashOrWrite();
wireHashChangeListener();
bootLiveMode();
})();
</script>
</body>
</html>