mergen-server 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- package/sdk/webpack-plugin.js +106 -0
|
@@ -0,0 +1,1407 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { Router } from "express";
|
|
3
|
+
import { getStores } from "../storage/store-registry.js";
|
|
4
|
+
import { postmortemStore } from "../intelligence/postmortem-store.js";
|
|
5
|
+
function createDashboardRouter(serverVersion, localSecret) {
|
|
6
|
+
const router = Router();
|
|
7
|
+
function checkAuth(req, res) {
|
|
8
|
+
if (!localSecret) return true;
|
|
9
|
+
const presented = req.headers["x-mergen-secret"] ?? req.query["secret"];
|
|
10
|
+
if (presented !== localSecret) {
|
|
11
|
+
res.status(401).send("Unauthorized \u2014 pass x-mergen-secret header or ?secret= query param");
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
router.get("/dashboard", (req, res) => {
|
|
17
|
+
if (!checkAuth(req, res)) return;
|
|
18
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
19
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
20
|
+
res.setHeader(
|
|
21
|
+
"Content-Security-Policy",
|
|
22
|
+
`default-src 'self'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; connect-src 'self'`
|
|
23
|
+
);
|
|
24
|
+
res.send(buildDashboardHtml(serverVersion, nonce));
|
|
25
|
+
});
|
|
26
|
+
router.get("/dashboard/incidents/:pid", async (req, res) => {
|
|
27
|
+
if (!checkAuth(req, res)) return;
|
|
28
|
+
const { pid } = req.params;
|
|
29
|
+
const inc = await getStores().incidents.get(pid, req.tenantId);
|
|
30
|
+
if (!inc) {
|
|
31
|
+
res.status(404).send("<h1>Incident not found</h1>");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const pms = postmortemStore.list(200);
|
|
35
|
+
const pm = pms.find((p) => p.pid === pid) ?? null;
|
|
36
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
37
|
+
const fmtMs = (ms) => ms < 6e4 ? `${Math.round(ms / 1e3)}s` : ms < 36e5 ? `${Math.round(ms / 6e4)}m` : `${(ms / 36e5).toFixed(1)}h`;
|
|
38
|
+
const mttrMs = inc.resolvedAt ? inc.resolvedAt - inc.createdAt : null;
|
|
39
|
+
const pct = inc.confidence != null ? Math.round(inc.confidence * 100) : null;
|
|
40
|
+
const statusColor = inc.status === "resolved" ? "#22c55e" : inc.status === "acknowledged" ? "#f59e0b" : "#ef4444";
|
|
41
|
+
const html = `<!DOCTYPE html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
45
|
+
<title>Incident \xB7 ${esc(inc.service ?? "unknown")} \xB7 Mergen</title>
|
|
46
|
+
<style>
|
|
47
|
+
:root{--bg:#0f1117;--surface:#1a1d26;--border:#2a2d3a;--text:#e2e8f0;--muted:#64748b;
|
|
48
|
+
--green:#22c55e;--yellow:#f59e0b;--red:#ef4444;--blue:#3b82f6;}
|
|
49
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
50
|
+
body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;font-size:13px;line-height:1.6;padding:32px 24px;max-width:800px;margin:0 auto}
|
|
51
|
+
h1{font-size:20px;font-weight:700;margin-bottom:4px}
|
|
52
|
+
.badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:11px;font-weight:600;color:#fff;background:${statusColor};margin-left:8px;vertical-align:middle}
|
|
53
|
+
.meta{color:var(--muted);font-size:12px;margin:6px 0 24px}
|
|
54
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px;margin-bottom:20px}
|
|
55
|
+
.card-title{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:14px}
|
|
56
|
+
table{width:100%;border-collapse:collapse}
|
|
57
|
+
td{padding:7px 0;border-bottom:1px solid var(--border);font-size:12px;vertical-align:top}
|
|
58
|
+
td:first-child{font-weight:600;width:180px;color:var(--muted)}
|
|
59
|
+
tr:last-child td{border-bottom:none}
|
|
60
|
+
pre{background:rgba(255,255,255,.04);border-radius:4px;padding:12px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-all;margin-top:8px;border:1px solid var(--border)}
|
|
61
|
+
.back{font-size:12px;color:var(--blue);text-decoration:none;display:inline-block;margin-bottom:20px}
|
|
62
|
+
.green{color:var(--green)} .yellow{color:var(--yellow)} .red{color:var(--red)} .muted{color:var(--muted)}
|
|
63
|
+
.verdict-pass{color:var(--green)} .verdict-partial{color:var(--yellow)} .verdict-fail{color:var(--red)}
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<a href="/dashboard" class="back">\u2190 Dashboard</a>
|
|
68
|
+
<h1>${esc(inc.service ?? "Unknown")} <span class="badge">${inc.status}</span></h1>
|
|
69
|
+
<div class="meta">PID: ${esc(pid)} \xB7 ${new Date(inc.createdAt).toUTCString()}</div>
|
|
70
|
+
|
|
71
|
+
<div class="card">
|
|
72
|
+
<div class="card-title">Incident Summary</div>
|
|
73
|
+
<table>
|
|
74
|
+
${inc.hypothesis ? `<tr><td>Root cause</td><td>${esc(inc.hypothesis)}</td></tr>` : ""}
|
|
75
|
+
${inc.tag ? `<tr><td>Pattern tag</td><td><code>${esc(inc.tag)}</code></td></tr>` : ""}
|
|
76
|
+
${pct != null ? `<tr><td>Confidence</td><td class="${pct >= 85 ? "green" : pct >= 70 ? "yellow" : "red"}">${pct}%</td></tr>` : ""}
|
|
77
|
+
${inc.environment ? `<tr><td>Environment</td><td>${esc(inc.environment)}</td></tr>` : ""}
|
|
78
|
+
${inc.sha ? `<tr><td>Blamed commit</td><td><code>${esc(inc.sha.slice(0, 8))}</code></td></tr>` : ""}
|
|
79
|
+
${mttrMs != null ? `<tr><td>MTTR</td><td class="${mttrMs < 3e5 ? "green" : mttrMs < 18e5 ? "yellow" : "red"}">${fmtMs(mttrMs)}</td></tr>` : ""}
|
|
80
|
+
<tr><td>Resolution</td><td>${inc.resolvedAutonomously ? '<span class="green">Autonomous (Mergen)</span>' : inc.assignee ? esc(inc.assignee) : '<span class="muted">Manual</span>'}</td></tr>
|
|
81
|
+
${inc.acknowledgedBy ? `<tr><td>Acknowledged by</td><td>${esc(inc.acknowledgedBy)}</td></tr>` : ""}
|
|
82
|
+
${inc.notes.length > 0 ? `<tr><td>Notes</td><td>${inc.notes.map(esc).join("<br>")}</td></tr>` : ""}
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
${pm ? `
|
|
87
|
+
<div class="card">
|
|
88
|
+
<div class="card-title">Postmortem</div>
|
|
89
|
+
<table>
|
|
90
|
+
${pm.rootCause ? `<tr><td>Root cause</td><td>${esc(pm.rootCause)}</td></tr>` : ""}
|
|
91
|
+
${pm.fixCommand ? `<tr><td>Fix applied</td><td><code>${esc(pm.fixCommand)}</code></td></tr>` : ""}
|
|
92
|
+
${pm.mttrMs != null ? `<tr><td>MTTR</td><td>${fmtMs(pm.mttrMs)}</td></tr>` : ""}
|
|
93
|
+
<tr><td>Causally verified</td><td class="${pm.causallyCorrect ? "green" : "muted"}">${pm.causallyCorrect ? "Yes" : "No"}</td></tr>
|
|
94
|
+
</table>
|
|
95
|
+
${pm.body ? `<pre>${esc(pm.body.slice(0, 3e3))}${pm.body.length > 3e3 ? "\n\u2026[truncated]" : ""}</pre>` : ""}
|
|
96
|
+
</div>` : ""}
|
|
97
|
+
|
|
98
|
+
<div style="margin-top:8px;font-size:11px;color:var(--muted)">
|
|
99
|
+
<a href="/incidents/${esc(pid)}/brief" style="color:var(--blue)">Shareable brief</a> \xB7
|
|
100
|
+
<a href="/incidents/impact-report?format=html" style="color:var(--blue)">Impact report</a> \xB7
|
|
101
|
+
Generated ${(/* @__PURE__ */ new Date()).toUTCString()}
|
|
102
|
+
</div>
|
|
103
|
+
</body>
|
|
104
|
+
</html>`;
|
|
105
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
106
|
+
res.send(html);
|
|
107
|
+
});
|
|
108
|
+
router.get("/", (req, res) => {
|
|
109
|
+
if (!checkAuth(req, res)) return;
|
|
110
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
111
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
112
|
+
res.setHeader(
|
|
113
|
+
"Content-Security-Policy",
|
|
114
|
+
`default-src 'self'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; connect-src 'self'`
|
|
115
|
+
);
|
|
116
|
+
res.send(buildDashboardHtml(serverVersion, nonce));
|
|
117
|
+
});
|
|
118
|
+
return router;
|
|
119
|
+
}
|
|
120
|
+
function buildDashboardHtml(version, nonce) {
|
|
121
|
+
const isShadow = process.env.MERGEN_SHADOW_MODE === "true";
|
|
122
|
+
const shadowBadge = isShadow ? `<span class="badge" style="background:rgba(245,158,11,.15);color:var(--yellow);margin-left:8px">Trial Mode: Shadow (Read-only)</span>` : "";
|
|
123
|
+
return (
|
|
124
|
+
/* html */
|
|
125
|
+
`<!DOCTYPE html>
|
|
126
|
+
<html lang="en">
|
|
127
|
+
<head>
|
|
128
|
+
<meta charset="UTF-8">
|
|
129
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
130
|
+
<title>Mergen Dashboard</title>
|
|
131
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
132
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
133
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
134
|
+
<style>
|
|
135
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
136
|
+
:root{
|
|
137
|
+
--bg:#070c14;--surface:rgba(20, 26, 40, 0.6);--border:rgba(255, 255, 255, 0.08);
|
|
138
|
+
--text:#f1f5f9;--muted:#64748b;--red:#ef4444;--yellow:#f59e0b;
|
|
139
|
+
--green:#10b981;--blue:#4f46e5;--orange:#f97316;
|
|
140
|
+
--radius:12px;--font:'Outfit', system-ui, -apple-system, sans-serif;
|
|
141
|
+
}
|
|
142
|
+
body{
|
|
143
|
+
background: radial-gradient(circle at 50% 0%, #0f172a 0%, #020617 100%);
|
|
144
|
+
color:var(--text);font-family:var(--font);font-size:13px;line-height:1.5;min-height:100vh;
|
|
145
|
+
-webkit-font-smoothing: antialiased;
|
|
146
|
+
}
|
|
147
|
+
header{
|
|
148
|
+
background: rgba(15, 23, 42, 0.7);
|
|
149
|
+
backdrop-filter: blur(12px);
|
|
150
|
+
-webkit-backdrop-filter: blur(12px);
|
|
151
|
+
border-bottom: 1px solid var(--border);
|
|
152
|
+
padding:14px 24px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10;
|
|
153
|
+
}
|
|
154
|
+
.logo{
|
|
155
|
+
font-weight:800;font-size:18px;letter-spacing:-0.02em;
|
|
156
|
+
background: linear-gradient(135deg, #a5b4fc 0%, #c084fc 100%);
|
|
157
|
+
-webkit-background-clip: text;
|
|
158
|
+
-webkit-text-fill-color: transparent;
|
|
159
|
+
}
|
|
160
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--muted);flex-shrink:0}
|
|
161
|
+
.dot.ok{background:var(--green);box-shadow:0 0 8px var(--green)}
|
|
162
|
+
.dot.err{background:var(--red);box-shadow:0 0 8px var(--red)}
|
|
163
|
+
.header-stats{display:flex;gap:16px;margin-left:auto;font-size:12px;color:var(--muted)}
|
|
164
|
+
.header-stats span{color:var(--text);font-weight: 500;}
|
|
165
|
+
.badge{font-size:11px;padding:2px 8px;border-radius:6px;font-weight:600}
|
|
166
|
+
.badge-red{background:rgba(239,68,68,.12);color:var(--red);border:1px solid rgba(239,68,68,.2)}
|
|
167
|
+
.badge-green{background:rgba(16,185,129,.12);color:var(--green);border:1px solid rgba(16,185,129,.2)}
|
|
168
|
+
.badge-muted{background:rgba(100,116,139,.12);color:var(--muted);border:1px solid rgba(100,116,139,.2)}
|
|
169
|
+
main{max-width:1100px;margin:0 auto;padding:24px}
|
|
170
|
+
.grid{display:grid;grid-template-columns:1fr 340px;gap:20px;align-items:start}
|
|
171
|
+
@media(max-width:768px){.grid{grid-template-columns:1fr}}
|
|
172
|
+
.card{
|
|
173
|
+
background:var(--surface);
|
|
174
|
+
backdrop-filter:blur(8px);
|
|
175
|
+
-webkit-backdrop-filter:blur(8px);
|
|
176
|
+
border:1px solid var(--border);
|
|
177
|
+
border-radius:var(--radius);
|
|
178
|
+
padding:18px;
|
|
179
|
+
margin-bottom:16px;
|
|
180
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
|
181
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
182
|
+
}
|
|
183
|
+
.card:hover {
|
|
184
|
+
border-color: rgba(99, 102, 241, 0.25);
|
|
185
|
+
box-shadow: 0 6px 24px rgba(99, 102, 241, 0.06);
|
|
186
|
+
}
|
|
187
|
+
.card-title{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:12px}
|
|
188
|
+
/* Root cause */
|
|
189
|
+
.rc{
|
|
190
|
+
border-left:3px solid var(--red);
|
|
191
|
+
background:rgba(239,68,68,.04);
|
|
192
|
+
padding:14px 16px;
|
|
193
|
+
border-radius:0 var(--radius) var(--radius) 0;
|
|
194
|
+
margin-bottom:16px;
|
|
195
|
+
box-shadow: inset 0 0 10px rgba(239,68,68,0.02);
|
|
196
|
+
}
|
|
197
|
+
.rc-label{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--red);margin-bottom:4px}
|
|
198
|
+
.rc-hyp{font-size:14px;font-weight:600;color:var(--text);line-height:1.4;letter-spacing:-0.01em;}
|
|
199
|
+
.rc-fix{font-size:12px;color:var(--muted);margin-top:6px}
|
|
200
|
+
.rc-conf{font-size:11px;color:var(--red);margin-top:4px;font-weight:600}
|
|
201
|
+
.rc-feedback{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:11px;color:var(--muted)}
|
|
202
|
+
.btn-verdict{
|
|
203
|
+
padding:3px 10px;border:1px solid rgba(255,255,255,.08);border-radius:6px;cursor:pointer;
|
|
204
|
+
font-size:11px;font-family:inherit;background:rgba(255,255,255,0.03);color:var(--text);
|
|
205
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
206
|
+
}
|
|
207
|
+
.btn-verdict:hover{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,0.2);}
|
|
208
|
+
.btn-verdict:active{transform:scale(0.96)}
|
|
209
|
+
.btn-verdict.yes{color:var(--green)}.btn-verdict.no{color:var(--red)}.btn-verdict.partial{color:var(--yellow)}
|
|
210
|
+
/* Timeline */
|
|
211
|
+
.tl-row{display:flex;align-items:baseline;gap:8px;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.03);font-size:12px;transition:background 0.15s;}
|
|
212
|
+
.tl-row:hover{background:rgba(255,255,255,0.01)}
|
|
213
|
+
.tl-row:last-child{border-bottom:none}
|
|
214
|
+
.tl-time{flex-shrink:0;color:var(--muted);font-variant-numeric:tabular-nums;width:60px;font-family:'JetBrains Mono',monospace;}
|
|
215
|
+
.tl-icon{flex-shrink:0;width:18px;text-align:center}
|
|
216
|
+
.tl-src{flex-shrink:0;font-size:9px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;padding:1px 5px;border-radius:4px;background:rgba(100,116,139,.12);color:var(--muted)}
|
|
217
|
+
.tl-src.ci{background:rgba(59,130,246,.12);color:#60a5fa}
|
|
218
|
+
.tl-src.deploy{background:rgba(16,185,129,.12);color:#34d399}
|
|
219
|
+
.tl-src.backend{background:rgba(249,115,22,.12);color:#fb923c}
|
|
220
|
+
.tl-src.node{background:rgba(16,185,129,.12);color:#34d399}
|
|
221
|
+
.tl-src.python{background:rgba(245,158,11,.12);color:#fbbf24}
|
|
222
|
+
.tl-joined{font-size:9px;color:var(--green);flex-shrink:0;cursor:pointer;text-decoration:underline}
|
|
223
|
+
/* Trace detail panel */
|
|
224
|
+
#trace-panel{display:none;margin-bottom:16px}
|
|
225
|
+
.trace-section{margin-bottom:10px}
|
|
226
|
+
.trace-section-title{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
|
|
227
|
+
.trace-row{font-size:11px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03);font-family:'JetBrains Mono',monospace;color:#cbd5e1;}
|
|
228
|
+
.trace-row:last-child{border-bottom:none}
|
|
229
|
+
/* SDK card */
|
|
230
|
+
.sdk-row{display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03);font-size:12px}
|
|
231
|
+
.sdk-row:last-child{border-bottom:none}
|
|
232
|
+
.sdk-badge{font-size:10px;font-weight:700;padding:1px 6px;border-radius:4px}
|
|
233
|
+
.sdk-badge.node{background:rgba(16,185,129,.12);color:#34d399}
|
|
234
|
+
.sdk-badge.python{background:rgba(245,158,11,.12);color:#fbbf24}
|
|
235
|
+
.tl-summary{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
236
|
+
.tl-sha{flex-shrink:0;font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;}
|
|
237
|
+
/* Side stats */
|
|
238
|
+
.stat{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03)}
|
|
239
|
+
.stat:last-child{border-bottom:none}
|
|
240
|
+
.stat-label{color:var(--muted)}
|
|
241
|
+
.stat-val{font-weight:600}
|
|
242
|
+
.stat-val.red{color:var(--red)}
|
|
243
|
+
.stat-val.green{color:var(--green)}
|
|
244
|
+
.stat-val.yellow{color:var(--yellow)}
|
|
245
|
+
/* CI/deploy table */
|
|
246
|
+
.ci-row{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03);font-size:12px}
|
|
247
|
+
.ci-row:last-child{border-bottom:none}
|
|
248
|
+
.ci-icon{flex-shrink:0;font-size:14px}
|
|
249
|
+
.ci-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
250
|
+
.ci-sha{font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;flex-shrink:0}
|
|
251
|
+
/* Refresh bar */
|
|
252
|
+
.refresh{
|
|
253
|
+
position:fixed;bottom:16px;right:16px;font-size:11px;color:var(--muted);
|
|
254
|
+
background:rgba(15,23,42,0.85);backdrop-filter:blur(8px);
|
|
255
|
+
border:1px solid var(--border);border-radius:20px;padding:4px 12px;
|
|
256
|
+
}
|
|
257
|
+
.empty{text-align:center;padding:32px;color:var(--muted);font-size:12px}
|
|
258
|
+
/* Fix validation */
|
|
259
|
+
.vld-status{font-weight:700;font-size:13px}
|
|
260
|
+
.vld-status.resolved{color:var(--green)}.vld-status.partial{color:var(--yellow)}.vld-status.wrong{color:var(--red)}
|
|
261
|
+
.vld-watch{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);margin-bottom:8px}
|
|
262
|
+
.vld-watch-dot{width:6px;height:6px;border-radius:50%;background:#3b82f6;box-shadow:0 0 6px #3b82f6;animation:pulse 2s infinite}
|
|
263
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
264
|
+
a{color:#818cf8;text-decoration:none;transition:color 0.15s;}a:hover{color:#a5b4fc;text-decoration:underline}
|
|
265
|
+
/* Signal status bar */
|
|
266
|
+
.signals-bar{
|
|
267
|
+
display:flex;gap:16px;padding:10px 24px;background:rgba(15,23,42,0.4);
|
|
268
|
+
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
|
|
269
|
+
border-bottom:1px solid var(--border);font-size:11px;flex-wrap:wrap;align-items:center;
|
|
270
|
+
}
|
|
271
|
+
.sig{display:flex;align-items:center;gap:5px}
|
|
272
|
+
.sig-dot{width:6px;height:6px;border-radius:50%;background:var(--muted);flex-shrink:0}
|
|
273
|
+
.sig-dot.on{background:var(--green);box-shadow:0 0 6px var(--green)}
|
|
274
|
+
.sig-lbl{color:var(--muted)}
|
|
275
|
+
.sig-lbl.on{color:var(--text)}
|
|
276
|
+
.sig-setup{margin-left:auto;font-size:10px;color:var(--muted);cursor:pointer;text-decoration:underline}
|
|
277
|
+
/* Incident actions */
|
|
278
|
+
.inc-bar{
|
|
279
|
+
display:flex;align-items:center;gap:8px;padding:10px 14px;
|
|
280
|
+
background:rgba(79,70,229,.05);border-left:3px solid var(--blue);
|
|
281
|
+
border-radius:0 var(--radius) var(--radius) 0;margin-bottom:16px;
|
|
282
|
+
border:1px solid rgba(79,70,229,0.15);border-left-width:3px;
|
|
283
|
+
}
|
|
284
|
+
.inc-bar.acked{border-left-color:var(--yellow);background:rgba(245,158,11,.04);border-color:rgba(245,158,11,0.15);border-left-width:3px;}
|
|
285
|
+
.inc-bar.resolved{border-left-color:var(--green);background:rgba(16,185,129,.04);border-color:rgba(16,185,129,0.15);border-left-width:3px;}
|
|
286
|
+
.inc-status{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0}
|
|
287
|
+
.inc-status.open{color:#818cf8}.inc-status.acknowledged{color:var(--yellow)}.inc-status.resolved{color:var(--green)}
|
|
288
|
+
.inc-actions{display:flex;gap:6px;margin-left:auto;flex-wrap:wrap}
|
|
289
|
+
.btn{
|
|
290
|
+
padding:6px 12px;border:1px solid transparent;border-radius:6px;cursor:pointer;
|
|
291
|
+
font-size:11px;font-weight:600;font-family:inherit;
|
|
292
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
293
|
+
}
|
|
294
|
+
.btn:active{transform:scale(0.97)}
|
|
295
|
+
.btn-ack{background:rgba(245,158,11,.15);color:var(--yellow);border-color:rgba(245,158,11,0.25)}
|
|
296
|
+
.btn-ack:hover{background:rgba(245,158,11,.25)}
|
|
297
|
+
.btn-resolve{background:rgba(16,185,129,.15);color:var(--green);border-color:rgba(16,185,129,0.25)}
|
|
298
|
+
.btn-resolve:hover{background:rgba(16,185,129,.25)}
|
|
299
|
+
.btn-note{background:rgba(255,255,255,.05);color:#cbd5e1;border-color:rgba(255,255,255,0.08)}
|
|
300
|
+
.btn-note:hover{background:rgba(255,255,255,.1)}
|
|
301
|
+
.btn-assign{background:rgba(79,70,229,.15);color:#a5b4fc;border-color:rgba(79,70,229,0.25)}
|
|
302
|
+
.btn-assign:hover{background:rgba(79,70,229,.25)}
|
|
303
|
+
.note-input{
|
|
304
|
+
background:rgba(15,23,42,0.6);border:1px solid var(--border);color:var(--text);
|
|
305
|
+
border-radius:6px;padding:6px 12px;font-size:11px;font-family:inherit;width:200px;
|
|
306
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
307
|
+
}
|
|
308
|
+
.note-input:focus{
|
|
309
|
+
outline:none;border-color:rgba(99,102,241,0.5);
|
|
310
|
+
box-shadow:0 0 0 2px rgba(99,102,241,0.25);
|
|
311
|
+
background:rgba(15,23,42,0.85);
|
|
312
|
+
}
|
|
313
|
+
/* War room */
|
|
314
|
+
#war-room{display:none;margin-bottom:20px}
|
|
315
|
+
.wr-grid{display:grid;grid-template-columns:1fr 1fr 300px;gap:16px;align-items:start}
|
|
316
|
+
@media(max-width:900px){.wr-grid{grid-template-columns:1fr 1fr}}
|
|
317
|
+
@media(max-width:600px){.wr-grid{grid-template-columns:1fr}}
|
|
318
|
+
.wr-active{
|
|
319
|
+
border-left:3px solid var(--red);background:rgba(239,68,68,.04);
|
|
320
|
+
padding:12px 14px;border-radius:0 var(--radius) var(--radius) 0;margin-bottom:12px;
|
|
321
|
+
border:1px solid rgba(239,68,68,0.15);border-left-width:3px;
|
|
322
|
+
}
|
|
323
|
+
.wr-active-title{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--red);margin-bottom:4px}
|
|
324
|
+
.wr-active-body{font-size:13px;font-weight:600;line-height:1.4}
|
|
325
|
+
.wr-active-meta{font-size:11px;color:var(--muted);margin-top:4px}
|
|
326
|
+
.conf-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700;letter-spacing:.04em}
|
|
327
|
+
.conf-badge.HIGH{background:rgba(16,185,129,.15);color:var(--green)}
|
|
328
|
+
.conf-badge.MEDIUM{background:rgba(245,158,11,.15);color:var(--yellow)}
|
|
329
|
+
.conf-badge.LOW{background:rgba(100,116,139,.15);color:var(--muted)}
|
|
330
|
+
.acc-table{width:100%;border-collapse:collapse;font-size:12px}
|
|
331
|
+
.acc-table th{text-align:left;font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);padding:6px 0;border-bottom:1px solid var(--border)}
|
|
332
|
+
.acc-table td{padding:8px 0;border-bottom:1px solid rgba(255,255,255,.03);vertical-align:middle}
|
|
333
|
+
.acc-table tr:last-child td{border-bottom:none}
|
|
334
|
+
.acc-bar-wrap{background:rgba(255,255,255,.05);border-radius:4px;height:6px;width:80px;overflow:hidden;display:inline-block;vertical-align:middle;margin-left:6px}
|
|
335
|
+
.acc-bar{height:6px;border-radius:4px;background:var(--green)}
|
|
336
|
+
.inc-open-row{display:flex;align-items:baseline;gap:8px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03);font-size:12px}
|
|
337
|
+
.inc-open-row:last-child{border-bottom:none}
|
|
338
|
+
.inc-open-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
339
|
+
.inc-open-svc{flex-shrink:0;font-size:9px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;padding:1px 5px;border-radius:4px;background:rgba(239,68,68,.12);color:var(--red);border:1px solid rgba(239,68,68,0.2)}
|
|
340
|
+
.blast-stat{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03);font-size:12px}
|
|
341
|
+
.blast-stat:last-child{border-bottom:none}
|
|
342
|
+
.blast-val{font-weight:600}
|
|
343
|
+
</style>
|
|
344
|
+
</head>
|
|
345
|
+
<body>
|
|
346
|
+
<header>
|
|
347
|
+
<span class="dot" id="dot"></span>
|
|
348
|
+
<span class="logo">\u2B21 Mergen</span>
|
|
349
|
+
<span style="color:var(--muted);font-size:11px">v${version}</span>
|
|
350
|
+
${shadowBadge}
|
|
351
|
+
<div class="header-stats">
|
|
352
|
+
<div>Errors: <span id="h-errors" class="badge badge-muted">\u2014</span></div>
|
|
353
|
+
<div>Warnings: <span id="h-warns" class="badge badge-muted">\u2014</span></div>
|
|
354
|
+
<div>Net errors: <span id="h-net" class="badge badge-muted">\u2014</span></div>
|
|
355
|
+
<div>Captured: <span id="h-buf" style="color:var(--text)">\u2014</span></div>
|
|
356
|
+
</div>
|
|
357
|
+
</header>
|
|
358
|
+
<div class="signals-bar">
|
|
359
|
+
<span class="sig"><span class="sig-dot" id="sig-browser"></span><span class="sig-lbl" id="sig-browser-lbl">Browser</span></span>
|
|
360
|
+
<span class="sig"><span class="sig-dot" id="sig-backend"></span><span class="sig-lbl" id="sig-backend-lbl">Backend</span></span>
|
|
361
|
+
<span class="sig"><span class="sig-dot" id="sig-ci"></span><span class="sig-lbl" id="sig-ci-lbl">CI/CD</span></span>
|
|
362
|
+
<span class="sig"><span class="sig-dot" id="sig-process"></span><span class="sig-lbl" id="sig-process-lbl">Process</span></span>
|
|
363
|
+
<span class="sig-setup" id="sig-setup-hint" style="display:none" onclick="window.open('http://127.0.0.1:3000/setup','_blank')"></span>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<div id="war-room" style="background:var(--surface);border-bottom:1px solid var(--border);padding:16px 24px">
|
|
367
|
+
<div style="max-width:1100px;margin:0 auto">
|
|
368
|
+
<div style="font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:12px">\u2694 War Room</div>
|
|
369
|
+
<div id="wr-active-box"></div>
|
|
370
|
+
<div class="wr-grid">
|
|
371
|
+
<div>
|
|
372
|
+
<div style="font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:8px">Open Incidents</div>
|
|
373
|
+
<div id="wr-open-list"><span style="font-size:11px;color:var(--muted)">No open incidents</span></div>
|
|
374
|
+
</div>
|
|
375
|
+
<div>
|
|
376
|
+
<div style="font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:8px">Attribution Accuracy</div>
|
|
377
|
+
<table class="acc-table">
|
|
378
|
+
<thead><tr><th>Band</th><th>Correct</th><th>Total</th><th>Rate</th></tr></thead>
|
|
379
|
+
<tbody id="wr-accuracy-body"><tr><td colspan="4" style="color:var(--muted);padding:8px 0">No validated incidents yet</td></tr></tbody>
|
|
380
|
+
</table>
|
|
381
|
+
</div>
|
|
382
|
+
<div>
|
|
383
|
+
<div style="font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:8px">Blast Radius</div>
|
|
384
|
+
<div id="wr-blast-list"><span style="font-size:11px;color:var(--muted)">No active incident</span></div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<main>
|
|
391
|
+
<div id="rc-box" style="display:none" class="rc">
|
|
392
|
+
<div class="rc-label">Root Cause \xB7 <span id="rc-pct"></span></div>
|
|
393
|
+
<div class="rc-hyp" id="rc-hyp"></div>
|
|
394
|
+
<div class="rc-fix" id="rc-fix"></div>
|
|
395
|
+
<div id="rc-feedback" class="rc-feedback" style="display:none">
|
|
396
|
+
Did this fix it?
|
|
397
|
+
<button class="btn-verdict yes" data-verdict="correct">\u2713 Yes</button>
|
|
398
|
+
<button class="btn-verdict partial" data-verdict="partial">~ Partially</button>
|
|
399
|
+
<button class="btn-verdict no" data-verdict="wrong">\u2717 No</button>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<div id="inc-bar" style="display:none" class="inc-bar">
|
|
404
|
+
<span class="inc-status" id="inc-status">OPEN</span>
|
|
405
|
+
<span id="inc-assignee" style="font-size:11px;color:var(--muted)"></span>
|
|
406
|
+
<div class="inc-actions">
|
|
407
|
+
<button class="btn btn-ack" id="btn-ack">Acknowledge</button>
|
|
408
|
+
<button class="btn btn-assign" id="btn-assign">Assign to me</button>
|
|
409
|
+
<button class="btn btn-resolve" id="btn-resolve">Resolve</button>
|
|
410
|
+
<input class="note-input" id="note-input" placeholder="Add note\u2026" style="display:none">
|
|
411
|
+
<button class="btn btn-note" id="btn-note">+ Note</button>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div class="grid">
|
|
416
|
+
<div>
|
|
417
|
+
<div class="card" id="trace-panel">
|
|
418
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
|
419
|
+
<div class="card-title" style="margin:0">Trace Detail <span id="trace-id-label" style="font-family:monospace;font-size:10px;font-weight:400;letter-spacing:0;text-transform:none"></span></div>
|
|
420
|
+
<button onclick="closeTrace()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:14px">\u2715</button>
|
|
421
|
+
</div>
|
|
422
|
+
<div id="trace-content"></div>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div class="card">
|
|
426
|
+
<div class="card-title">Unified Timeline <span style="font-weight:400;text-transform:none;letter-spacing:0" id="tl-window"></span></div>
|
|
427
|
+
<div id="tl-list"><div class="empty">Connecting\u2026</div></div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div>
|
|
432
|
+
<div class="card">
|
|
433
|
+
<div class="card-title">Buffer</div>
|
|
434
|
+
<div id="stats-list"></div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<div class="card" id="ci-card" style="display:none">
|
|
438
|
+
<div class="card-title">CI / Deployments</div>
|
|
439
|
+
<div id="ci-list"></div>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<div class="card" id="sdk-card">
|
|
443
|
+
<div class="card-title">Backend SDKs</div>
|
|
444
|
+
<div id="sdk-list"><div style="font-size:11px;color:var(--muted)">No SDK connections yet.</div></div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div class="card" id="validate-card" style="display:none">
|
|
448
|
+
<div class="card-title">Fix Validation</div>
|
|
449
|
+
<div id="validate-list"></div>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div class="card" id="calib-card">
|
|
453
|
+
<div class="card-title">Calibration Health</div>
|
|
454
|
+
<div id="calib-list"><div style="font-size:11px;color:var(--muted)">Loading\u2026</div></div>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<div class="card" id="mttr-card">
|
|
458
|
+
<div class="card-title">MTTR / Autonomous Resolution</div>
|
|
459
|
+
<div id="mttr-content"><div style="font-size:11px;color:var(--muted)">Loading\u2026</div></div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div class="card" id="blunder-card">
|
|
463
|
+
<div class="card-title">Agent Blunder Log (Prevented Outages)</div>
|
|
464
|
+
<div id="blunder-content"><div style="font-size:11px;color:var(--muted)">Loading\u2026</div></div>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div class="card" id="activity-feed-card">
|
|
468
|
+
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
|
469
|
+
<span>Agent Activity Feed</span>
|
|
470
|
+
<a href="/policies" style="font-size:10px;color:var(--blue);font-weight:400;text-transform:none;letter-spacing:0">Policy Editor \u2192</a>
|
|
471
|
+
</div>
|
|
472
|
+
<div id="activity-feed-content"><div style="font-size:11px;color:var(--muted)">Loading\u2026</div></div>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<div class="card" id="corpus-card">
|
|
476
|
+
<div class="card-title">Accuracy Gate</div>
|
|
477
|
+
<div id="corpus-progress">
|
|
478
|
+
<div style="font-size:11px;color:var(--muted);margin-bottom:8px">
|
|
479
|
+
HIGH-confidence verdicts toward partner corpus target
|
|
480
|
+
</div>
|
|
481
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
|
482
|
+
<div style="flex:1;background:rgba(255,255,255,.06);border-radius:3px;height:6px;overflow:hidden">
|
|
483
|
+
<div id="corpus-bar" style="height:6px;border-radius:3px;background:var(--green);width:0%;transition:width .4s"></div>
|
|
484
|
+
</div>
|
|
485
|
+
<span id="corpus-count" style="font-size:12px;font-weight:600;color:var(--text);white-space:nowrap">0 / 20</span>
|
|
486
|
+
</div>
|
|
487
|
+
<div id="corpus-status" style="font-size:11px;color:var(--muted)">Loading\u2026</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<div class="card" id="service-memory-card">
|
|
492
|
+
<div class="card-title">Service Memory</div>
|
|
493
|
+
<div style="display:flex;gap:6px;margin-bottom:10px;align-items:center">
|
|
494
|
+
<input id="sm-service-input" type="text" placeholder="service name (e.g. api)"
|
|
495
|
+
style="flex:1;background:rgba(255,255,255,.06);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px;color:var(--text);outline:none"
|
|
496
|
+
oninput="debouncePollServiceMemory()"/>
|
|
497
|
+
<span style="font-size:10px;color:var(--muted)">filter</span>
|
|
498
|
+
</div>
|
|
499
|
+
<div id="sm-summary" style="font-size:11px;color:var(--muted);margin-bottom:8px"></div>
|
|
500
|
+
<div id="sm-edges" style="font-size:11px"></div>
|
|
501
|
+
<div id="sm-incidents" style="font-size:11px;margin-top:8px"></div>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
<div class="card" id="recent-incidents-card">
|
|
505
|
+
<div class="card-title">Recent Incidents</div>
|
|
506
|
+
<div id="recent-incidents-list"><div style="font-size:11px;color:var(--muted)">Loading\u2026</div></div>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<div class="card" id="runbooks-card">
|
|
510
|
+
<div class="card-title">Runbook Library</div>
|
|
511
|
+
<div id="runbooks-list"><div style="font-size:11px;color:var(--muted)">Loading\u2026</div></div>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div class="card">
|
|
515
|
+
<div class="card-title">Connect more signals</div>
|
|
516
|
+
<div style="font-size:11px;color:var(--muted);line-height:1.8">
|
|
517
|
+
Node.js: <code style="color:var(--text)">npm install mergen-node</code><br>
|
|
518
|
+
Python: <code style="color:var(--text)">pip install mergen-python</code><br>
|
|
519
|
+
Stream backend: <code style="color:var(--text)">mergen-server watch npm start</code><br>
|
|
520
|
+
CI results: <code style="color:var(--text)">POST /ci/github</code><br>
|
|
521
|
+
Docker logs: <code style="color:var(--text)">MERGEN_DOCKER_LOGS=true</code><br>
|
|
522
|
+
Auto-learn from Slack: <code style="color:var(--text)">MERGEN_SLACK_OVERRIDE_LOOP=true</code>
|
|
523
|
+
</div>
|
|
524
|
+
<div style="margin-top:12px;font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:6px">GitHub Actions \u2014 Corpus Gate</div>
|
|
525
|
+
<pre style="font-size:9px;color:var(--text);background:rgba(255,255,255,.04);border-radius:4px;padding:8px;overflow-x:auto;margin:0;line-height:1.5"># .github/workflows/ci.yml
|
|
526
|
+
- uses: mergen-server/mergen@v1
|
|
527
|
+
with:
|
|
528
|
+
mergen-url: http://your-mergen-server:3000
|
|
529
|
+
mergen-secret: ${{ secrets.MERGEN_SECRET }}</pre>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
</main>
|
|
534
|
+
|
|
535
|
+
<div class="refresh" id="refresh-bar">Refreshing\u2026</div>
|
|
536
|
+
|
|
537
|
+
<script nonce="${nonce}">
|
|
538
|
+
// Fetch local secret once \u2014 used for mutating endpoints (/feedback, /clear).
|
|
539
|
+
// The /local-secret endpoint is protected by the Host-header check so only
|
|
540
|
+
// callers on 127.0.0.1:<port> can read it.
|
|
541
|
+
let _localSecret = '';
|
|
542
|
+
let _sdkServices = {};
|
|
543
|
+
|
|
544
|
+
function setSignal(id, on, label) {
|
|
545
|
+
const dot = document.getElementById('sig-' + id);
|
|
546
|
+
const lbl = document.getElementById('sig-' + id + '-lbl');
|
|
547
|
+
if (!dot || !lbl) return;
|
|
548
|
+
dot.className = 'sig-dot' + (on ? ' on' : '');
|
|
549
|
+
lbl.className = 'sig-lbl' + (on ? ' on' : '');
|
|
550
|
+
lbl.textContent = label;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function updateSignalStatus(health, sdkServices, rows) {
|
|
554
|
+
const browserOn = health && (health.buffered > 0 || (health.lastEventAt && Date.now() - health.lastEventAt < 300000));
|
|
555
|
+
setSignal('browser', browserOn, 'Browser' + (browserOn ? '' : ' \u2014 inactive'));
|
|
556
|
+
|
|
557
|
+
const backendKeys = Object.keys(sdkServices || {});
|
|
558
|
+
const backendOn = backendKeys.length > 0;
|
|
559
|
+
setSignal('backend', backendOn, backendOn ? 'Backend (' + backendKeys.length + ')' : 'Backend \u2014 not connected');
|
|
560
|
+
|
|
561
|
+
const ciOn = (rows || []).some(r => r.kind === 'ci_failure' || r.kind === 'ci_success' || r.kind === 'deployment');
|
|
562
|
+
setSignal('ci', ciOn, ciOn ? 'CI/CD' : 'CI/CD \u2014 not connected');
|
|
563
|
+
|
|
564
|
+
const processOn = (rows || []).some(r => r.kind === 'terminal');
|
|
565
|
+
setSignal('process', processOn, processOn ? 'Process' : 'Process \u2014 not connected');
|
|
566
|
+
|
|
567
|
+
const inactive = [];
|
|
568
|
+
if (!backendOn) inactive.push('backend');
|
|
569
|
+
if (!ciOn) inactive.push('CI/CD');
|
|
570
|
+
if (!processOn) inactive.push('process');
|
|
571
|
+
const hint = document.getElementById('sig-setup-hint');
|
|
572
|
+
if (inactive.length > 0) {
|
|
573
|
+
hint.textContent = 'Connect: ' + inactive.join(', ') + ' \u2192';
|
|
574
|
+
hint.style.display = '';
|
|
575
|
+
} else {
|
|
576
|
+
hint.style.display = 'none';
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
fetch('/local-secret').then(r=>r.json()).then(d=>{ _localSecret=d.secret||''; }).catch(()=>{});
|
|
580
|
+
|
|
581
|
+
// Track which pids the user has already rated to avoid double-submission.
|
|
582
|
+
const _ratedPids = new Set(JSON.parse(localStorage.getItem('mergenRated')||'[]'));
|
|
583
|
+
|
|
584
|
+
document.getElementById('rc-feedback').addEventListener('click', async (e) => {
|
|
585
|
+
const btn = e.target.closest('[data-verdict]');
|
|
586
|
+
if (!btn || !_currentPid || _ratedPids.has(_currentPid)) return;
|
|
587
|
+
const verdict = btn.dataset.verdict;
|
|
588
|
+
try {
|
|
589
|
+
const r = await fetch('/feedback', {
|
|
590
|
+
method:'POST',
|
|
591
|
+
headers:{'Content-Type':'application/json','x-mergen-secret':_localSecret},
|
|
592
|
+
body: JSON.stringify({ pid: _currentPid, verdict }),
|
|
593
|
+
});
|
|
594
|
+
if (r.ok || r.status === 207) {
|
|
595
|
+
_ratedPids.add(_currentPid);
|
|
596
|
+
localStorage.setItem('mergenRated', JSON.stringify([..._ratedPids]));
|
|
597
|
+
const fb = document.getElementById('rc-feedback');
|
|
598
|
+
fb.innerHTML = '<span style="color:var(--muted)">Thanks \u2014 this improves future diagnoses.</span>';
|
|
599
|
+
}
|
|
600
|
+
} catch {}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const ICON = {
|
|
604
|
+
error:'\u{1F534}',warn:'\u{1F7E1}',log:'\u2B1C',request:'\u{1F7E0}',context:'\u2B1C',
|
|
605
|
+
terminal:'\u{1F4BB}',process_exit:'\u{1F4A5}',ci_failure:'\u274C',ci_success:'\u2705',deployment:'\u{1F680}',
|
|
606
|
+
backend_span:'\u{1F537}',
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
function esc(s){
|
|
610
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function rel(ms){
|
|
614
|
+
const d=Date.now()-ms;
|
|
615
|
+
if(d<60000) return Math.max(1,Math.floor(d/1000))+'s';
|
|
616
|
+
if(d<3600000) return Math.floor(d/60000)+'m';
|
|
617
|
+
return Math.floor(d/3600000)+'h';
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function time(iso){return iso.slice(11,19);}
|
|
621
|
+
|
|
622
|
+
async function pollSdkStatus(){
|
|
623
|
+
try{
|
|
624
|
+
const {services} = await fetch('/sdk-status').then(r=>r.json());
|
|
625
|
+
_sdkServices = services || {};
|
|
626
|
+
updateSignalStatus(null, _sdkServices, null);
|
|
627
|
+
const keys = Object.keys(services||{});
|
|
628
|
+
const el = document.getElementById('sdk-list');
|
|
629
|
+
if(keys.length===0){
|
|
630
|
+
el.innerHTML='<div style="font-size:11px;color:var(--muted)">No SDK connections yet.<br>Install <code>mergen-node</code> or <code>mergen-python</code>.</div>';
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
el.innerHTML=keys.map(k=>{
|
|
634
|
+
const s=services[k];
|
|
635
|
+
const ago=rel(s.lastSeen);
|
|
636
|
+
const errBadge=s.errorCount>0?'<span class="badge badge-red">'+s.errorCount+' err</span>':'';
|
|
637
|
+
return '<div class="sdk-row">'+
|
|
638
|
+
'<span><span class="sdk-badge '+s.sdk+'">'+s.sdk+'</span> '+esc(k.split('/')[1]||k)+'</span>'+
|
|
639
|
+
'<span style="display:flex;align-items:center;gap:6px">'+errBadge+'<span style="font-size:10px;color:var(--muted)">'+ago+' ago</span></span>'+
|
|
640
|
+
'</div>';
|
|
641
|
+
}).join('');
|
|
642
|
+
}catch(e){}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function showTrace(traceId){
|
|
646
|
+
try{
|
|
647
|
+
const data = await fetch('/trace/'+traceId).then(r=>r.json());
|
|
648
|
+
const panel = document.getElementById('trace-panel');
|
|
649
|
+
const content = document.getElementById('trace-content');
|
|
650
|
+
document.getElementById('trace-id-label').textContent = traceId;
|
|
651
|
+
panel.style.display='block';
|
|
652
|
+
|
|
653
|
+
let html='';
|
|
654
|
+
if(data.browserNet&&data.browserNet.length>0){
|
|
655
|
+
html+='<div class="trace-section"><div class="trace-section-title">Browser (fetch/XHR)</div>';
|
|
656
|
+
html+=data.browserNet.map(n=>'<div class="trace-row">'+esc(n.method)+' '+esc(n.url)+' \u2192 '+n.status+' ('+n.duration+'ms)'+(n.error?' \u2014 '+esc(n.error):'')+'</div>').join('');
|
|
657
|
+
html+='</div>';
|
|
658
|
+
}
|
|
659
|
+
if(data.backendSpans&&data.backendSpans.length>0){
|
|
660
|
+
html+='<div class="trace-section"><div class="trace-section-title">Backend Spans</div>';
|
|
661
|
+
html+=data.backendSpans.map(s=>'<div class="trace-row"><span class="sdk-badge '+s.sdk+'" style="margin-right:4px">'+s.sdk+'</span>'+esc(s.service)+' \u2014 '+esc(s.method)+' '+esc(s.route)+' \u2192 '+s.statusCode+' ('+s.durationMs+'ms)'+(s.error?' \u2014 '+esc(s.error):'')+'</div>').join('');
|
|
662
|
+
html+='</div>';
|
|
663
|
+
}
|
|
664
|
+
if(data.backendLogs&&data.backendLogs.length>0){
|
|
665
|
+
html+='<div class="trace-section"><div class="trace-section-title">Backend Logs (traceId in stdout)</div>';
|
|
666
|
+
html+=data.backendLogs.map(t=>'<div class="trace-row">'+esc('['+t.terminalName+'] '+t.data.slice(0,200))+'</div>').join('');
|
|
667
|
+
html+='</div>';
|
|
668
|
+
}
|
|
669
|
+
const joined = (data.browserNet&&data.browserNet.length>0)&&(data.backendSpans&&data.backendSpans.length>0);
|
|
670
|
+
html+='<div style="font-size:11px;margin-top:8px;color:'+(joined?'var(--green)':'var(--yellow)')+'">'+
|
|
671
|
+
(joined?'\u2705 EXACT JOIN \u2014 browser request matched to backend span':'\u26A0 Partial \u2014 check SDK instrumentation')+'</div>';
|
|
672
|
+
content.innerHTML=html;
|
|
673
|
+
}catch(e){ console.warn('trace fetch failed',e); }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function closeTrace(){
|
|
677
|
+
document.getElementById('trace-panel').style.display='none';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function poll(){
|
|
681
|
+
try{
|
|
682
|
+
const [unified,health]=await Promise.all([
|
|
683
|
+
fetch('/timeline/unified?seconds=300&limit=30').then(r=>r.json()),
|
|
684
|
+
fetch('/health').then(r=>r.json()),
|
|
685
|
+
]);
|
|
686
|
+
|
|
687
|
+
// Header
|
|
688
|
+
const dot=document.getElementById('dot');
|
|
689
|
+
dot.className='dot ok';
|
|
690
|
+
document.getElementById('h-errors').textContent=health.errors??0;
|
|
691
|
+
document.getElementById('h-errors').className='badge '+(health.errors>0?'badge-red':'badge-muted');
|
|
692
|
+
document.getElementById('h-warns').textContent=health.warnings??0;
|
|
693
|
+
document.getElementById('h-net').textContent=health.networkErrors??0;
|
|
694
|
+
document.getElementById('h-buf').textContent=health.buffered??0;
|
|
695
|
+
|
|
696
|
+
// Root cause
|
|
697
|
+
const rc=unified.rootCause;
|
|
698
|
+
const rcBox=document.getElementById('rc-box');
|
|
699
|
+
if(rc&&rc.confidence>=0.45){
|
|
700
|
+
rcBox.style.display='block';
|
|
701
|
+
const lowConf=rc.confidence<0.7;
|
|
702
|
+
rcBox.style.opacity=lowConf?'0.6':'1';
|
|
703
|
+
document.getElementById('rc-pct').textContent=Math.round(rc.confidence*100)+'% confidence'+(lowConf?' (low)':'');
|
|
704
|
+
document.getElementById('rc-hyp').textContent=rc.hypothesis;
|
|
705
|
+
const fix=document.getElementById('rc-fix');
|
|
706
|
+
if(rc.fixHint){fix.textContent='\u{1F4A1} '+rc.fixHint;fix.style.display='block';}else{fix.style.display='none';}
|
|
707
|
+
// Show feedback buttons for new pids; hide if already rated
|
|
708
|
+
const fb=document.getElementById('rc-feedback');
|
|
709
|
+
if(rc.pid && !_ratedPids.has(rc.pid)){
|
|
710
|
+
fb.style.display='flex';
|
|
711
|
+
const hypSnippet=rc.hypothesis?rc.hypothesis.slice(0,60)+(rc.hypothesis.length>60?'\u2026':''):'';
|
|
712
|
+
fb.innerHTML='Did this fix it?'+(hypSnippet?' <span style="color:var(--muted);font-size:10px">('+esc(hypSnippet)+')</span>':'')+' <button class="btn-verdict yes" data-verdict="correct">\u2713 Yes</button><button class="btn-verdict partial" data-verdict="partial">~ Partially</button><button class="btn-verdict no" data-verdict="wrong">\u2717 No</button>';
|
|
713
|
+
}else if(rc.pid){
|
|
714
|
+
fb.style.display='none';
|
|
715
|
+
}
|
|
716
|
+
if(!lowConf)ensureIncident(rc);
|
|
717
|
+
}else{rcBox.style.display='none';}
|
|
718
|
+
|
|
719
|
+
// Timeline
|
|
720
|
+
const rows=unified.rows??[];
|
|
721
|
+
const tlList=document.getElementById('tl-list');
|
|
722
|
+
document.getElementById('tl-window').textContent='\xB7 last 5m \xB7 '+rows.length+' events';
|
|
723
|
+
if(rows.length===0){
|
|
724
|
+
tlList.innerHTML='<div class="empty">No events yet. Send telemetry via OpenTelemetry (:4318/v1/traces), trigger a PagerDuty test, or stream Docker logs. See <a href="/setup" style="color:var(--blue)">setup guide</a>.</div>';
|
|
725
|
+
}else{
|
|
726
|
+
tlList.innerHTML=rows.slice(-30).reverse().map(r=>{
|
|
727
|
+
const src=r.source||'';
|
|
728
|
+
const sha=r.sha?'<span class="tl-sha">['+esc(r.sha)+']</span>':'';
|
|
729
|
+
// For backend spans, show sdk badge instead of generic "backend"
|
|
730
|
+
let srcBadge='';
|
|
731
|
+
if(r.kind==='backend_span'&&r.summary){
|
|
732
|
+
const sdkMatch=r.summary.match(/^[(node|python):/);
|
|
733
|
+
const sdkName=sdkMatch?sdkMatch[1]:null;
|
|
734
|
+
srcBadge=sdkName?'<span class="tl-src '+sdkName+'">'+sdkName+'</span>':'<span class="tl-src backend">backend</span>';
|
|
735
|
+
}else if(src){
|
|
736
|
+
srcBadge='<span class="tl-src '+src+'">'+src+'</span>';
|
|
737
|
+
}
|
|
738
|
+
const confBadge=r.confidence>=1.0
|
|
739
|
+
?'<span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(34,197,94,.15);color:var(--green);flex-shrink:0">EXACT</span>'
|
|
740
|
+
:r.confidence>=0.8
|
|
741
|
+
?'<span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(59,130,246,.12);color:var(--blue);flex-shrink:0">LINKED</span>'
|
|
742
|
+
:'';
|
|
743
|
+
const joinedLink=r.traceId&&r.confidence>=1.0
|
|
744
|
+
?'<span class="tl-joined" onclick="showTrace(''+esc(r.traceId)+'')">trace</span>'
|
|
745
|
+
:r.traceId
|
|
746
|
+
?'<span class="tl-joined" style="color:var(--muted)" onclick="showTrace(''+esc(r.traceId)+'')">trace</span>'
|
|
747
|
+
:'';
|
|
748
|
+
return '<div class="tl-row">'+
|
|
749
|
+
'<span class="tl-time">'+time(r.isoTs)+'</span>'+
|
|
750
|
+
'<span class="tl-icon">'+(ICON[r.kind]||'\u2B1C')+'</span>'+
|
|
751
|
+
srcBadge+
|
|
752
|
+
'<span class="tl-summary">'+esc(r.summary)+'</span>'+
|
|
753
|
+
sha+confBadge+joinedLink+
|
|
754
|
+
'</div>';
|
|
755
|
+
}).join('');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Buffer stats
|
|
759
|
+
document.getElementById('stats-list').innerHTML=[
|
|
760
|
+
['Errors',health.errors??0,'red'],
|
|
761
|
+
['Warnings',health.warnings??0,'yellow'],
|
|
762
|
+
['Net errors',health.networkErrors??0,'red'],
|
|
763
|
+
['Captured',health.buffered??0,''],
|
|
764
|
+
['Last event',health.lastEventAt?rel(health.lastEventAt)+' ago':'\u2014',''],
|
|
765
|
+
].map(([l,v,c])=>'<div class="stat"><span class="stat-label">'+l+'</span><span class="stat-val '+(c||'')+'">'+v+'</span></div>').join('');
|
|
766
|
+
|
|
767
|
+
// CI/deployments from unified rows
|
|
768
|
+
const ciRows=rows.filter(r=>r.kind==='ci_failure'||r.kind==='ci_success'||r.kind==='deployment').slice(-6).reverse();
|
|
769
|
+
const ciCard=document.getElementById('ci-card');
|
|
770
|
+
if(ciRows.length>0){
|
|
771
|
+
ciCard.style.display='block';
|
|
772
|
+
document.getElementById('ci-list').innerHTML=ciRows.map(r=>{
|
|
773
|
+
const sha=r.sha?'<span class="ci-sha">'+esc(r.sha)+'</span>':'';
|
|
774
|
+
return '<div class="ci-row">'+
|
|
775
|
+
'<span class="ci-icon">'+(ICON[r.kind]||'\u2B1C')+'</span>'+
|
|
776
|
+
'<span class="ci-label">'+esc(r.summary)+'</span>'+
|
|
777
|
+
sha+
|
|
778
|
+
'</div>';
|
|
779
|
+
}).join('');
|
|
780
|
+
}else{ciCard.style.display='none';}
|
|
781
|
+
|
|
782
|
+
updateSignalStatus(health, _sdkServices, rows);
|
|
783
|
+
document.getElementById('refresh-bar').textContent='Updated '+new Date().toLocaleTimeString();
|
|
784
|
+
}catch(e){
|
|
785
|
+
document.getElementById('dot').className='dot err';
|
|
786
|
+
document.getElementById('refresh-bar').textContent='Server unreachable \u2014 run: cd server && npm start';
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// \u2500\u2500 Incident actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
791
|
+
let _currentPid = null;
|
|
792
|
+
let _userName = localStorage.getItem('mergenUser') || '';
|
|
793
|
+
|
|
794
|
+
async function incidentAction(action, body={}) {
|
|
795
|
+
if (!_currentPid) return;
|
|
796
|
+
try {
|
|
797
|
+
await fetch('/incidents/'+encodeURIComponent(_currentPid)+'/'+action, {
|
|
798
|
+
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),
|
|
799
|
+
});
|
|
800
|
+
await refreshIncident();
|
|
801
|
+
} catch(e) { console.warn('incident action failed', e); }
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function refreshIncident() {
|
|
805
|
+
if (!_currentPid) return;
|
|
806
|
+
try {
|
|
807
|
+
const r = await fetch('/incidents/'+encodeURIComponent(_currentPid));
|
|
808
|
+
if (!r.ok) return;
|
|
809
|
+
const {incident} = await r.json();
|
|
810
|
+
renderIncidentBar(incident);
|
|
811
|
+
} catch {}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function renderIncidentBar(inc) {
|
|
815
|
+
const bar = document.getElementById('inc-bar');
|
|
816
|
+
if (!inc || !bar) return;
|
|
817
|
+
bar.style.display='flex';
|
|
818
|
+
bar.className='inc-bar'+(inc.status==='acknowledged'?' acked':inc.status==='resolved'?' resolved':'');
|
|
819
|
+
document.getElementById('inc-status').textContent=inc.status.toUpperCase();
|
|
820
|
+
document.getElementById('inc-status').className='inc-status '+inc.status;
|
|
821
|
+
const aEl = document.getElementById('inc-assignee');
|
|
822
|
+
aEl.textContent = inc.assignee ? 'Assigned: '+inc.assignee : (inc.acknowledgedBy ? 'Acked by: '+inc.acknowledgedBy : '');
|
|
823
|
+
document.getElementById('btn-ack').style.display = inc.status==='open' ? '' : 'none';
|
|
824
|
+
document.getElementById('btn-assign').style.display = inc.status==='resolved' ? 'none' : '';
|
|
825
|
+
document.getElementById('btn-resolve').style.display = inc.status==='resolved' ? 'none' : '';
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
document.getElementById('btn-ack').addEventListener('click', async () => {
|
|
829
|
+
const by = _userName || prompt('Your name (optional):') || '';
|
|
830
|
+
if (by && !_userName) { _userName=by; localStorage.setItem('mergenUser',by); }
|
|
831
|
+
await incidentAction('acknowledge', {by});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
document.getElementById('btn-assign').addEventListener('click', async () => {
|
|
835
|
+
const to = _userName || prompt('Assign to (name/email):');
|
|
836
|
+
if (!to) return;
|
|
837
|
+
if (!_userName) { _userName=to; localStorage.setItem('mergenUser',to); }
|
|
838
|
+
await incidentAction('assign', {to});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
document.getElementById('btn-resolve').addEventListener('click', async () => {
|
|
842
|
+
const note = document.getElementById('note-input').value.trim();
|
|
843
|
+
await incidentAction('resolve', {by: _userName, note: note||undefined});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
document.getElementById('btn-note').addEventListener('click', () => {
|
|
847
|
+
const inp = document.getElementById('note-input');
|
|
848
|
+
if (inp.style.display==='none') { inp.style.display=''; inp.focus(); return; }
|
|
849
|
+
const text = inp.value.trim();
|
|
850
|
+
if (!text) { inp.style.display='none'; return; }
|
|
851
|
+
incidentAction('note', {text, author: _userName}).then(()=>{ inp.value=''; inp.style.display='none'; });
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// \u2500\u2500 Wire incident bar to root-cause hypothesis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
855
|
+
async function ensureIncident(rc) {
|
|
856
|
+
if (!rc || !rc.pid) return;
|
|
857
|
+
if (_currentPid === rc.pid) return;
|
|
858
|
+
_currentPid = rc.pid;
|
|
859
|
+
// Upsert the incident record
|
|
860
|
+
try {
|
|
861
|
+
await fetch('/incidents', {
|
|
862
|
+
method:'POST', headers:{'Content-Type':'application/json'},
|
|
863
|
+
body: JSON.stringify({ pid: rc.pid, hypothesis: rc.hypothesis, tag: rc.tag, confidence: rc.confidence }),
|
|
864
|
+
});
|
|
865
|
+
await refreshIncident();
|
|
866
|
+
} catch {}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function pollValidateState() {
|
|
870
|
+
try {
|
|
871
|
+
const state = await fetch('/validate/state').then(r=>r.json());
|
|
872
|
+
const card = document.getElementById('validate-card');
|
|
873
|
+
const list = document.getElementById('validate-list');
|
|
874
|
+
if (!state.watching && !state.lastValidation) { card.style.display='none'; return; }
|
|
875
|
+
card.style.display = 'block';
|
|
876
|
+
let html = '';
|
|
877
|
+
if (state.watching) {
|
|
878
|
+
const n = state.paths ? state.paths.length : 0;
|
|
879
|
+
html += '<div class="vld-watch"><span class="vld-watch-dot"></span>Watching '+n+' file'+(n!==1?'s':'')+'\u2026</div>';
|
|
880
|
+
}
|
|
881
|
+
if (state.lastValidation) {
|
|
882
|
+
const v = state.lastValidation;
|
|
883
|
+
const cls = v.verdict==='correct'?'resolved':v.verdict==='partial'?'partial':'wrong';
|
|
884
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'+
|
|
885
|
+
'<span class="vld-status '+cls+'">'+esc(v.status)+'</span>'+
|
|
886
|
+
'<span style="font-size:10px;color:var(--muted)">'+rel(v.timestamp)+' ago</span>'+
|
|
887
|
+
'</div>'+
|
|
888
|
+
'<div style="font-size:11px;color:var(--muted)">'+
|
|
889
|
+
'Before: <strong>'+v.errsBefore+'</strong> error'+(v.errsBefore!==1?'s':'')+
|
|
890
|
+
' \u2192 After: <strong>'+v.errsAfter+'</strong>'+
|
|
891
|
+
'</div>';
|
|
892
|
+
}
|
|
893
|
+
list.innerHTML = html;
|
|
894
|
+
} catch(e) {}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async function pollCalibrationHealth() {
|
|
898
|
+
try {
|
|
899
|
+
const [sm, cal, unc] = await Promise.all([
|
|
900
|
+
fetch('/session-metrics').then(r => r.json()),
|
|
901
|
+
fetch('/calibration').then(r => r.json()),
|
|
902
|
+
fetch('/calibration/unclassified?minCount=3').then(r => r.json()),
|
|
903
|
+
]);
|
|
904
|
+
const el = document.getElementById('calib-list');
|
|
905
|
+
if (!el) return;
|
|
906
|
+
let html = '';
|
|
907
|
+
|
|
908
|
+
// First-attempt fix success rate
|
|
909
|
+
const rate = sm.firstAttemptSuccessRate;
|
|
910
|
+
const rateStr = rate !== null && rate !== undefined
|
|
911
|
+
? Math.round(rate * 100) + '%'
|
|
912
|
+
: sm.withOutcome < 3 ? 'n/a (' + sm.withOutcome + ' sessions)' : '\u2014';
|
|
913
|
+
const rateColor = rate === null ? 'var(--muted)' : rate >= 0.6 ? 'var(--green)' : rate >= 0.4 ? 'var(--yellow)' : 'var(--red)';
|
|
914
|
+
html += '<div class="stat"><span class="stat-label">1st-attempt success</span><span class="stat-val" style="color:' + rateColor + '">' + esc(rateStr) + '</span></div>';
|
|
915
|
+
html += '<div class="stat"><span class="stat-label">Total sessions</span><span class="stat-val">' + (sm.total || 0) + '</span></div>';
|
|
916
|
+
|
|
917
|
+
// corpusSeeded = all records are built-in seeds (zero real verdicts recorded yet).
|
|
918
|
+
// During this phase, showing live accuracy or "trusted detectors" would be misleading \u2014
|
|
919
|
+
// every seeded tag trivially exceeds MIN_SAMPLES, making all detectors appear trusted
|
|
920
|
+
// before any real data exists. Show live stats only once real verdicts arrive.
|
|
921
|
+
const isWarmingUp = !!cal.corpusSeeded;
|
|
922
|
+
const realN = cal.realVerdictCount || 0;
|
|
923
|
+
|
|
924
|
+
if (!isWarmingUp) {
|
|
925
|
+
if (cal.overallAccuracy !== null && cal.overallAccuracy !== undefined) {
|
|
926
|
+
const acc = Math.round(cal.overallAccuracy * 100);
|
|
927
|
+
const accColor = acc >= 70 ? 'var(--green)' : acc >= 50 ? 'var(--yellow)' : 'var(--red)';
|
|
928
|
+
html += '<div class="stat"><span class="stat-label">Live accuracy</span><span class="stat-val" style="color:' + accColor + '">' + acc + '%</span></div>';
|
|
929
|
+
}
|
|
930
|
+
html += '<div class="stat"><span class="stat-label">Trusted detectors</span><span class="stat-val">' + (cal.trustedDetectors || 0) + ' / ' + (cal.totalDetectors || 0) + '</span></div>';
|
|
931
|
+
} else {
|
|
932
|
+
html += '<div class="stat"><span class="stat-label">Live accuracy</span><span class="stat-val" style="color:var(--muted)">\u2014 (initializing)</span></div>';
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Warm-up progress
|
|
936
|
+
if (isWarmingUp) {
|
|
937
|
+
const warmPct = Math.min(100, Math.round((realN / 10) * 100));
|
|
938
|
+
html += '<div style="margin-top:10px;padding:8px;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);border-radius:4px">'
|
|
939
|
+
+ '<div style="font-size:10px;font-weight:700;color:var(--yellow);margin-bottom:6px;letter-spacing:.05em;text-transform:uppercase">Collecting real verdicts</div>'
|
|
940
|
+
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">'
|
|
941
|
+
+ '<div style="flex:1;background:rgba(255,255,255,.06);border-radius:3px;height:5px;overflow:hidden">'
|
|
942
|
+
+ '<div style="height:5px;border-radius:3px;background:var(--yellow);width:' + warmPct + '%;transition:width .4s"></div></div>'
|
|
943
|
+
+ '<span style="font-size:11px;font-weight:600;color:var(--yellow);white-space:nowrap">' + realN + ' / 10</span></div>'
|
|
944
|
+
+ '<div style="font-size:10px;color:var(--muted)">Rate each analysis to build your accuracy corpus \xB7 live accuracy shown after 10 verdicts</div>'
|
|
945
|
+
+ '</div>';
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Unclassified clusters
|
|
949
|
+
if (unc.total > 0) {
|
|
950
|
+
html += '<div class="stat"><span class="stat-label" style="color:var(--yellow)">Unclassified patterns</span><span class="stat-val" style="color:var(--yellow)">' + unc.total + '</span></div>';
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Per-detector breakdown \u2014 live when real verdicts exist, priors when still warming up
|
|
954
|
+
if (!isWarmingUp) {
|
|
955
|
+
const liveDetectors = (cal.perDetector || []).filter(d => d.trusted).slice(0, 5);
|
|
956
|
+
if (liveDetectors.length > 0) {
|
|
957
|
+
html += '<div style="margin-top:8px;font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:4px">Detectors</div>';
|
|
958
|
+
for (const d of liveDetectors) {
|
|
959
|
+
const acc = Math.round(d.accuracy * 100);
|
|
960
|
+
const trend = d.trendDelta !== null ? (d.trendDelta > 0 ? ' \u2191' : d.trendDelta < 0 ? ' \u2193' : '') : '';
|
|
961
|
+
const color = acc >= 70 ? 'var(--green)' : acc >= 50 ? 'var(--yellow)' : 'var(--red)';
|
|
962
|
+
html += '<div style="display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:11px">'
|
|
963
|
+
+ '<span style="color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' + esc(d.tag) + '">' + esc(d.tag.replace(/_/g,' ')) + '</span>'
|
|
964
|
+
+ '<span style="color:' + color + ';flex-shrink:0">' + acc + '%' + esc(trend) + '</span>'
|
|
965
|
+
+ '</div>';
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
const priors = (cal.perDetector || []).slice(0, 5);
|
|
970
|
+
if (priors.length > 0) {
|
|
971
|
+
html += '<div style="margin-top:8px;font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:2px;opacity:.55">Calibration priors</div>'
|
|
972
|
+
+ '<div style="font-size:9px;color:var(--muted);margin-bottom:4px;opacity:.55">Synthetic defaults \xB7 replaced by your data as verdicts come in</div>';
|
|
973
|
+
for (const d of priors) {
|
|
974
|
+
const acc = Math.round(d.accuracy * 100);
|
|
975
|
+
html += '<div style="display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:11px;opacity:.45">'
|
|
976
|
+
+ '<span style="color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' + esc(d.tag) + '">' + esc(d.tag.replace(/_/g,' ')) + '</span>'
|
|
977
|
+
+ '<span style="color:var(--muted);flex-shrink:0">' + acc + '%</span>'
|
|
978
|
+
+ '</div>';
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
el.innerHTML = html;
|
|
984
|
+
} catch(e) {}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// \u2500\u2500 War Room \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
988
|
+
function fmtRelTime(ts) {
|
|
989
|
+
if (!ts) return '\u2014';
|
|
990
|
+
const diff = Date.now() - ts;
|
|
991
|
+
if (diff < 60000) return 'just now';
|
|
992
|
+
if (diff < 3600000) return Math.round(diff/60000)+'m ago';
|
|
993
|
+
return Math.round(diff/3600000)+'h ago';
|
|
994
|
+
}
|
|
995
|
+
function fmtDurationMs(ms) {
|
|
996
|
+
if (!ms) return '\u2014';
|
|
997
|
+
if (ms < 60000) return Math.round(ms/1000)+'s';
|
|
998
|
+
if (ms < 3600000) return Math.round(ms/60000)+'m';
|
|
999
|
+
return (ms/3600000).toFixed(1)+'h';
|
|
1000
|
+
}
|
|
1001
|
+
async function pollWarRoom() {
|
|
1002
|
+
try {
|
|
1003
|
+
const d = await fetch('/api/war-room').then(r=>r.json());
|
|
1004
|
+
if (!d.ok) return;
|
|
1005
|
+
|
|
1006
|
+
const wr = document.getElementById('war-room');
|
|
1007
|
+
if (!wr) return;
|
|
1008
|
+
|
|
1009
|
+
const hasActivity = d.activeIncident || (d.openIncidents && d.openIncidents.length > 0)
|
|
1010
|
+
|| (d.mttrHistory && d.mttrHistory.length > 0);
|
|
1011
|
+
wr.style.display = hasActivity ? '' : 'none';
|
|
1012
|
+
|
|
1013
|
+
// Active incident banner
|
|
1014
|
+
const activeBox = document.getElementById('wr-active-box');
|
|
1015
|
+
if (d.activeIncident && activeBox) {
|
|
1016
|
+
const a = d.activeIncident;
|
|
1017
|
+
const conf = a.blameConfidence !== null ? Math.round(a.blameConfidence*100)+'%' : null;
|
|
1018
|
+
const label = a.blameLabel || '';
|
|
1019
|
+
activeBox.innerHTML = '<div class="wr-active">'
|
|
1020
|
+
+ '<div class="wr-active-title">\u{1F534} Active \u2014 ' + esc(a.service) + '</div>'
|
|
1021
|
+
+ '<div class="wr-active-body">' + esc(a.alertTitle) + '</div>'
|
|
1022
|
+
+ '<div class="wr-active-meta">Fired '+ fmtRelTime(a.firedAt)
|
|
1023
|
+
+ (a.blameSha ? ' \xB7 Deploy <code style="font-family:monospace">'+esc(a.blameSha)+'</code>' : '')
|
|
1024
|
+
+ (conf ? ' \xB7 <span class="conf-badge '+esc(label)+'">'+conf+' '+esc(label)+'</span>' : '')
|
|
1025
|
+
+ (a.blameExplanation ? '<br><span style="font-size:10px;color:var(--muted)">'+esc(a.blameExplanation)+'</span>' : '')
|
|
1026
|
+
+ '</div></div>';
|
|
1027
|
+
} else if (activeBox) {
|
|
1028
|
+
activeBox.innerHTML = '';
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Open incidents
|
|
1032
|
+
const openList = document.getElementById('wr-open-list');
|
|
1033
|
+
if (openList) {
|
|
1034
|
+
if (!d.openIncidents || d.openIncidents.length === 0) {
|
|
1035
|
+
openList.innerHTML = '<span style="font-size:11px;color:var(--muted)">No open incidents \u2713</span>';
|
|
1036
|
+
} else {
|
|
1037
|
+
openList.innerHTML = d.openIncidents.map(inc => {
|
|
1038
|
+
const conf = inc.attributionConfidence !== null
|
|
1039
|
+
? '<span class="conf-badge '+(inc.attributionConfidence>=.8?'HIGH':inc.attributionConfidence>=.6?'MEDIUM':'LOW')+'">'
|
|
1040
|
+
+Math.round(inc.attributionConfidence*100)+'%</span>' : '';
|
|
1041
|
+
return '<div class="inc-open-row">'
|
|
1042
|
+
+ '<span class="inc-open-svc">'+esc(inc.service)+'</span>'
|
|
1043
|
+
+ '<span class="inc-open-title" title="'+esc(inc.alertTitle)+'">'+esc(inc.alertTitle)+'</span>'
|
|
1044
|
+
+ conf
|
|
1045
|
+
+ '<span style="font-size:10px;color:var(--muted);flex-shrink:0">'+fmtRelTime(inc.firedAt)+'</span>'
|
|
1046
|
+
+ '</div>';
|
|
1047
|
+
}).join('');
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Attribution accuracy table
|
|
1052
|
+
const accBody = document.getElementById('wr-accuracy-body');
|
|
1053
|
+
if (accBody && d.attributionAccuracy) {
|
|
1054
|
+
const acc = d.attributionAccuracy;
|
|
1055
|
+
const bands = [['HIGH','high','var(--green)'],['MEDIUM','medium','var(--yellow)'],['LOW','low','var(--muted)']];
|
|
1056
|
+
const rows = bands.map(([label,key,color]) => {
|
|
1057
|
+
const b = acc[key] || {correct:0,total:0,pct:null};
|
|
1058
|
+
if (b.total === 0) return '<tr><td><span class="conf-badge '+label+'">'+label+'</span></td><td style="color:var(--muted)">\u2014</td><td style="color:var(--muted)">0</td><td style="color:var(--muted)">\u2014</td></tr>';
|
|
1059
|
+
const pct = b.pct ?? 0;
|
|
1060
|
+
return '<tr><td><span class="conf-badge '+label+'">'+label+'</span></td>'
|
|
1061
|
+
+'<td style="color:'+color+'">'+b.correct+'</td>'
|
|
1062
|
+
+'<td style="color:var(--muted)">'+b.total+'</td>'
|
|
1063
|
+
+'<td><span style="color:'+color+'">'+pct+'%</span>'
|
|
1064
|
+
+'<span class="acc-bar-wrap"><span class="acc-bar" style="width:'+pct+'%;background:'+color+'"></span></span></td></tr>';
|
|
1065
|
+
});
|
|
1066
|
+
accBody.innerHTML = rows.join('');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Blast radius panel
|
|
1070
|
+
const blastEl = document.getElementById('wr-blast-list');
|
|
1071
|
+
if (blastEl && d.blastRadius) {
|
|
1072
|
+
const br = d.blastRadius;
|
|
1073
|
+
if (!br.errorCount) {
|
|
1074
|
+
blastEl.innerHTML = '<span style="font-size:11px;color:var(--muted)">No active errors</span>';
|
|
1075
|
+
} else {
|
|
1076
|
+
let html = '';
|
|
1077
|
+
if (br.affectedSessions > 0) html += '<div class="blast-stat"><span style="color:var(--muted)">Sessions</span><span class="blast-val" style="color:var(--red)">'+br.affectedSessions+'</span></div>';
|
|
1078
|
+
if (br.affectedUsers > 0) html += '<div class="blast-stat"><span style="color:var(--muted)">Users</span><span class="blast-val">'+br.affectedUsers+'</span></div>';
|
|
1079
|
+
html += '<div class="blast-stat"><span style="color:var(--muted)">Errors</span><span class="blast-val">'+br.errorCount+'</span></div>';
|
|
1080
|
+
if (br.durationMs) html += '<div class="blast-stat"><span style="color:var(--muted)">Duration</span><span class="blast-val">'+fmtDurationMs(br.durationMs)+'</span></div>';
|
|
1081
|
+
if (br.browserSegments) {
|
|
1082
|
+
const topBrowsers = Object.entries(br.browserSegments).sort((a,b)=>b[1]-a[1]).slice(0,3);
|
|
1083
|
+
if (topBrowsers.length > 0) html += '<div class="blast-stat"><span style="color:var(--muted)">Browsers</span><span class="blast-val" style="font-size:10px">'+topBrowsers.map(([b,n])=>b+':'+n).join(' ')+'</span></div>';
|
|
1084
|
+
}
|
|
1085
|
+
blastEl.innerHTML = html;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
} catch(e) {}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function pollMttr() {
|
|
1092
|
+
try {
|
|
1093
|
+
const d = await fetch('/incidents/impact-report').then(r => r.json());
|
|
1094
|
+
if (!d.ok) return;
|
|
1095
|
+
const el = document.getElementById('mttr-content');
|
|
1096
|
+
if (!el) return;
|
|
1097
|
+
|
|
1098
|
+
if (d.totalResolved === 0) {
|
|
1099
|
+
el.innerHTML = '<div style="font-size:11px;color:var(--muted)">No resolved incidents yet.</div>';
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const isShadow = d.isShadowMode;
|
|
1104
|
+
const rateLabel = isShadow ? 'Autonomous rate (would execute)' : 'Autonomous rate';
|
|
1105
|
+
const autoLabel = isShadow ? 'Autonomous (Would Execute)' : 'Autonomous';
|
|
1106
|
+
const mttrAutoLabel = isShadow ? 'MTTR autonomous (would execute)' : 'MTTR autonomous';
|
|
1107
|
+
|
|
1108
|
+
const autoRate = d.autonomousRate ?? 0;
|
|
1109
|
+
const barColor = autoRate >= 50 ? 'var(--green)' : autoRate >= 25 ? 'var(--yellow)' : 'var(--muted)';
|
|
1110
|
+
|
|
1111
|
+
function fmtMs(ms) {
|
|
1112
|
+
if (!ms) return '\u2014';
|
|
1113
|
+
if (ms < 60000) return Math.round(ms / 1000) + 's';
|
|
1114
|
+
if (ms < 3600000) return Math.round(ms / 60000) + 'm';
|
|
1115
|
+
return (ms / 3600000).toFixed(1) + 'h';
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
let html = '';
|
|
1119
|
+
|
|
1120
|
+
// Hero stat: time saved \u2014 the board-deck number
|
|
1121
|
+
if (d.report && d.report.timeSavedLabel) {
|
|
1122
|
+
html += '<div style="text-align:center;padding:10px 0 14px">'
|
|
1123
|
+
+ '<div style="font-size:22px;font-weight:700;color:var(--green);line-height:1">' + esc(d.report.timeSavedLabel) + '</div>'
|
|
1124
|
+
+ (d.report.hoursPerIncident ? '<div style="font-size:10px;color:var(--muted);margin-top:4px">' + d.report.hoursPerIncident + 'h per incident on average</div>' : '')
|
|
1125
|
+
+ '</div>';
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Autonomous rate bar
|
|
1129
|
+
html += '<div style="margin-bottom:10px">'
|
|
1130
|
+
+ '<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:4px">'
|
|
1131
|
+
+ '<span style="color:var(--muted)">' + rateLabel + '</span>'
|
|
1132
|
+
+ '<span style="font-weight:700;color:' + barColor + '">' + autoRate + '%</span>'
|
|
1133
|
+
+ '</div>'
|
|
1134
|
+
+ '<div style="background:rgba(255,255,255,.06);border-radius:3px;height:6px;overflow:hidden">'
|
|
1135
|
+
+ '<div style="height:6px;border-radius:3px;background:' + barColor + ';width:' + autoRate + '%;transition:width .4s"></div>'
|
|
1136
|
+
+ '</div>'
|
|
1137
|
+
+ '</div>';
|
|
1138
|
+
|
|
1139
|
+
// MTTR comparison
|
|
1140
|
+
html += '<div class="stat"><span class="stat-label">Resolved total</span><span class="stat-val">' + d.totalResolved + '</span></div>';
|
|
1141
|
+
html += '<div class="stat"><span class="stat-label">' + autoLabel + '</span><span class="stat-val" style="color:var(--green)">' + d.autonomousResolutions + '</span></div>';
|
|
1142
|
+
html += '<div class="stat"><span class="stat-label">Manual</span><span class="stat-val" style="color:var(--muted)">' + d.manualResolutions + '</span></div>';
|
|
1143
|
+
|
|
1144
|
+
if (d.mttr) {
|
|
1145
|
+
if (d.mttr.overallMs) html += '<div class="stat"><span class="stat-label">MTTR overall</span><span class="stat-val">' + fmtMs(d.mttr.overallMs) + '</span></div>';
|
|
1146
|
+
if (d.mttr.autonomousMs) html += '<div class="stat"><span class="stat-label">' + mttrAutoLabel + '</span><span class="stat-val" style="color:var(--green)">' + fmtMs(d.mttr.autonomousMs) + '</span></div>';
|
|
1147
|
+
if (d.mttr.manualMs) html += '<div class="stat"><span class="stat-label">MTTR manual</span><span class="stat-val" style="color:var(--muted)">' + fmtMs(d.mttr.manualMs) + '</span></div>';
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Recent resolutions
|
|
1151
|
+
if (d.recentResolutions && d.recentResolutions.length > 0) {
|
|
1152
|
+
html += '<div style="margin-top:8px;font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:4px">Recent</div>';
|
|
1153
|
+
for (const r of d.recentResolutions.slice(0, 4)) {
|
|
1154
|
+
const icon = r.resolvedAutonomously ? '\u{1F916}' : '\u{1F464}';
|
|
1155
|
+
const mttr = r.mttrMs ? fmtMs(r.mttrMs) : '\u2014';
|
|
1156
|
+
html += '<div style="display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:11px">'
|
|
1157
|
+
+ '<span>' + icon + ' ' + esc(r.tag ? r.tag.replace(/_/g,' ') : r.pid.slice(0,12)) + '</span>'
|
|
1158
|
+
+ '<span style="color:var(--muted)">' + mttr + '</span>'
|
|
1159
|
+
+ '</div>';
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
el.innerHTML = html;
|
|
1164
|
+
} catch(e) {}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async function pollBlunders() {
|
|
1168
|
+
try {
|
|
1169
|
+
const d = await fetch('/agent-blunders').then(r => r.json());
|
|
1170
|
+
const el = document.getElementById('blunder-content');
|
|
1171
|
+
if (!el || !d) return;
|
|
1172
|
+
|
|
1173
|
+
if (!d.prevented) {
|
|
1174
|
+
el.innerHTML = '<div style="font-size:11px;color:var(--muted)">0 autonomous actions blocked. Prod is running fine.</div>';
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
let html = '';
|
|
1179
|
+
html += '<div class="stat"><span class="stat-label">Prevented Outages</span><span class="stat-val" style="color:var(--green)">' + d.prevented + '</span></div>';
|
|
1180
|
+
|
|
1181
|
+
const types = d.byType || {};
|
|
1182
|
+
for (const [key, val] of Object.entries(types)) {
|
|
1183
|
+
if (val > 0) {
|
|
1184
|
+
const name = key.replace(/_/g, ' ');
|
|
1185
|
+
html += '<div class="stat"><span class="stat-label" style="font-size:11px;padding-left:8px">\u2022 ' + name + '</span><span class="stat-val" style="font-size:11px">' + val + '</span></div>';
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (d.recent && d.recent.length > 0) {
|
|
1190
|
+
html += '<div style="margin-top:8px;font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:4px">Recent Interceptions</div>';
|
|
1191
|
+
for (const b of d.recent.slice(0, 3)) {
|
|
1192
|
+
const reason = b.blockReason || b.reason || 'Safety block';
|
|
1193
|
+
html += '<div style="padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:11px">'
|
|
1194
|
+
+ '<div style="font-weight:600;color:var(--red);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(b.command) + '">\u274C ' + esc(b.command) + '</div>'
|
|
1195
|
+
+ '<div style="color:var(--muted);font-size:10px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(reason) + '">' + esc(reason) + '</div>'
|
|
1196
|
+
+ '</div>';
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
el.innerHTML = html;
|
|
1201
|
+
} catch(e) {}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
async function pollCorpusProgress() {
|
|
1205
|
+
try {
|
|
1206
|
+
const d = await fetch('/calibration/corpus-progress').then(r => r.json());
|
|
1207
|
+
if (!d.ok) return;
|
|
1208
|
+
const bar = document.getElementById('corpus-bar');
|
|
1209
|
+
const count = document.getElementById('corpus-count');
|
|
1210
|
+
const status = document.getElementById('corpus-status');
|
|
1211
|
+
if (bar) bar.style.width = d.pct + '%';
|
|
1212
|
+
if (count) count.textContent = d.highConfidentCorrect + ' / ' + d.target;
|
|
1213
|
+
if (status) {
|
|
1214
|
+
if (d.targetReached) {
|
|
1215
|
+
status.textContent = '\u2705 Gate reached \u2014 corpus ready to publish';
|
|
1216
|
+
status.style.color = 'var(--green)';
|
|
1217
|
+
} else {
|
|
1218
|
+
const remaining = d.target - d.highConfidentCorrect;
|
|
1219
|
+
status.textContent = remaining + ' more needed \xB7 ' + d.trustedDetectors + ' trusted detector' + (d.trustedDetectors !== 1 ? 's' : '') + ' \xB7 ' + d.totalVerdicts + ' total verdicts';
|
|
1220
|
+
status.style.color = 'var(--muted)';
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
} catch {}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
let _smDebounce = null;
|
|
1227
|
+
function debouncePollServiceMemory() {
|
|
1228
|
+
if (_smDebounce) clearTimeout(_smDebounce);
|
|
1229
|
+
_smDebounce = setTimeout(pollServiceMemory, 400);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
async function pollServiceMemory() {
|
|
1233
|
+
const input = document.getElementById('sm-service-input');
|
|
1234
|
+
const service = input ? input.value.trim() : '';
|
|
1235
|
+
const summaryEl = document.getElementById('sm-summary');
|
|
1236
|
+
const edgesEl = document.getElementById('sm-edges');
|
|
1237
|
+
const incEl = document.getElementById('sm-incidents');
|
|
1238
|
+
if (!summaryEl || !edgesEl || !incEl) return;
|
|
1239
|
+
try {
|
|
1240
|
+
const qs = service ? '?service=' + encodeURIComponent(service) : '';
|
|
1241
|
+
const [intData, incData] = await Promise.all([
|
|
1242
|
+
fetch('/services/interactions' + qs).then(r => r.json()),
|
|
1243
|
+
fetch('/incidents' + (service ? '?service=' + encodeURIComponent(service) : '')).then(r => r.json()),
|
|
1244
|
+
]);
|
|
1245
|
+
|
|
1246
|
+
if (intData.ok) {
|
|
1247
|
+
const edges = intData.edges || [];
|
|
1248
|
+
if (edges.length === 0) {
|
|
1249
|
+
edgesEl.innerHTML = '<span style="color:var(--muted)">No interaction edges yet \u2014 resolved incidents build this graph automatically.</span>';
|
|
1250
|
+
summaryEl.textContent = '';
|
|
1251
|
+
} else {
|
|
1252
|
+
summaryEl.textContent = edges.length + ' co-occurrence edge' + (edges.length !== 1 ? 's' : '') + ' across ' + intData.services.length + ' service' + (intData.services.length !== 1 ? 's' : '');
|
|
1253
|
+
const topEdges = edges.slice(0, 8);
|
|
1254
|
+
edgesEl.innerHTML = '<div style="color:var(--muted);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Interaction Map</div>' +
|
|
1255
|
+
topEdges.map(e => {
|
|
1256
|
+
const w = e.weight > 5 ? '\u{1F534}' : e.weight > 2 ? '\u{1F7E1}' : '\u26AA';
|
|
1257
|
+
return '<div style="display:flex;gap:6px;align-items:center;padding:2px 0">' +
|
|
1258
|
+
'<code style="font-size:10px;color:var(--text)">' + e.source + '</code>' +
|
|
1259
|
+
'<span style="color:var(--muted)">\u2194</span>' +
|
|
1260
|
+
'<code style="font-size:10px;color:var(--text)">' + e.target + '</code>' +
|
|
1261
|
+
'<span style="color:var(--muted);margin-left:auto">' + w + ' ' + e.weight + 'x</span>' +
|
|
1262
|
+
'</div>';
|
|
1263
|
+
}).join('');
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const incidents = Array.isArray(incData) ? incData : (incData.incidents || []);
|
|
1268
|
+
const filtered = service ? incidents.filter(i => i.service === service) : incidents;
|
|
1269
|
+
const recent = filtered.slice(0, 6);
|
|
1270
|
+
const resolved = filtered.filter(i => i.status === 'resolved');
|
|
1271
|
+
const mttrVals = resolved.filter(i => i.resolvedAt).map(i => i.resolvedAt - i.createdAt);
|
|
1272
|
+
const avgMttr = mttrVals.length > 0 ? Math.round(mttrVals.reduce((a,b)=>a+b,0) / mttrVals.length / 1000) : null;
|
|
1273
|
+
const autoRate = resolved.length > 0 ? Math.round(resolved.filter(i=>i.resolvedAutonomously).length / resolved.length * 100) : null;
|
|
1274
|
+
|
|
1275
|
+
if (recent.length === 0) {
|
|
1276
|
+
incEl.innerHTML = '<span style="color:var(--muted)">No incident history' + (service ? ' for "' + service + '"' : '') + '</span>';
|
|
1277
|
+
} else {
|
|
1278
|
+
const statsLine = [
|
|
1279
|
+
resolved.length + ' resolved',
|
|
1280
|
+
avgMttr !== null ? 'avg MTTR ' + (avgMttr >= 60 ? Math.round(avgMttr/60) + 'm' : avgMttr + 's') : null,
|
|
1281
|
+
autoRate !== null ? autoRate + '% autonomous' : null,
|
|
1282
|
+
].filter(Boolean).join(' \xB7 ');
|
|
1283
|
+
incEl.innerHTML = '<div style="color:var(--muted);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Recent Incidents <span style="font-weight:400;text-transform:none;letter-spacing:0">' + statsLine + '</span></div>' +
|
|
1284
|
+
recent.map(i => {
|
|
1285
|
+
const icon = i.status === 'resolved' ? (i.resolvedAutonomously ? '\u{1F916}' : '\u2705') : '\u{1F534}';
|
|
1286
|
+
const tag = (i.tag || 'unknown').replace(/^infra_/, '');
|
|
1287
|
+
const svc = i.service ? ' <span style="color:var(--muted)">[' + i.service + ']</span>' : '';
|
|
1288
|
+
return '<div style="padding:2px 0">' + icon + ' ' + tag + svc + ' <span style="color:var(--muted);float:right">' + Math.round(i.confidence*100) + '%</span></div>';
|
|
1289
|
+
}).join('');
|
|
1290
|
+
}
|
|
1291
|
+
} catch {}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async function pollRecentIncidents() {
|
|
1295
|
+
try {
|
|
1296
|
+
const d = await fetch('/incidents').then(r => r.json());
|
|
1297
|
+
const el = document.getElementById('recent-incidents-list');
|
|
1298
|
+
if (!el) return;
|
|
1299
|
+
const items = (d.incidents || []).slice(0, 10);
|
|
1300
|
+
if (items.length === 0) { el.innerHTML = '<div style="font-size:11px;color:var(--muted)">No incidents recorded yet</div>'; return; }
|
|
1301
|
+
let html = '<table style="width:100%;border-collapse:collapse;font-size:11px">'
|
|
1302
|
+
+ '<thead><tr style="color:var(--muted);font-size:10px;text-align:left">'
|
|
1303
|
+
+ '<th style="padding:2px 6px 4px 0">Service</th><th style="padding:2px 6px 4px 0">Status</th>'
|
|
1304
|
+
+ '<th style="padding:2px 6px 4px 0">MTTR</th><th style="padding:2px 0 4px 0">Fired</th></tr></thead><tbody>';
|
|
1305
|
+
for (const inc of items) {
|
|
1306
|
+
const statusColor = inc.status === 'resolved' ? 'var(--green)' : inc.status === 'acknowledged' ? 'var(--yellow)' : 'var(--red)';
|
|
1307
|
+
const mttrMin = inc.resolvedAt && inc.createdAt ? Math.round((inc.resolvedAt - inc.createdAt) / 60000) : null;
|
|
1308
|
+
const firedAgo = inc.createdAt ? fmtRelTime(inc.createdAt) : '\u2014';
|
|
1309
|
+
const briefUrl = '/dashboard/incidents/' + encodeURIComponent(inc.pid || inc.id || '');
|
|
1310
|
+
html += '<tr style="border-bottom:1px solid rgba(255,255,255,.04)">'
|
|
1311
|
+
+ '<td style="padding:4px 6px 4px 0;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'
|
|
1312
|
+
+ '<a href="' + briefUrl + '" target="_blank" style="color:var(--blue);text-decoration:none">' + esc(inc.service || '\u2014') + '</a></td>'
|
|
1313
|
+
+ '<td style="padding:4px 6px 4px 0"><span style="color:' + statusColor + '">' + esc(inc.status || '\u2014') + '</span>'
|
|
1314
|
+
+ (inc.resolvedAutonomously ? ' <span style="font-size:9px;color:var(--muted)">(auto)</span>' : '') + '</td>'
|
|
1315
|
+
+ '<td style="padding:4px 6px 4px 0;color:var(--muted)">' + (mttrMin != null ? mttrMin + 'm' : '\u2014') + '</td>'
|
|
1316
|
+
+ '<td style="padding:4px 0;color:var(--muted)">' + firedAgo + '</td>'
|
|
1317
|
+
+ '</tr>';
|
|
1318
|
+
}
|
|
1319
|
+
html += '</tbody></table>';
|
|
1320
|
+
el.innerHTML = html;
|
|
1321
|
+
} catch(e) {}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function pollRunbooks() {
|
|
1325
|
+
try {
|
|
1326
|
+
const d = await fetch('/runbooks').then(r => r.json());
|
|
1327
|
+
const el = document.getElementById('runbooks-list');
|
|
1328
|
+
if (!el) return;
|
|
1329
|
+
const items = d.runbooks || [];
|
|
1330
|
+
if (items.length === 0) {
|
|
1331
|
+
el.innerHTML = '<div style="font-size:11px;color:var(--muted)">No runbooks yet. Create one:<br><code style="font-size:10px">POST /runbooks { name, incidentTag, steps[], riskTier, requiredApprovers }</code></div>';
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
let html = '';
|
|
1335
|
+
for (const rb of items) {
|
|
1336
|
+
const approvedColor = rb.approved ? 'var(--green)' : 'var(--yellow)';
|
|
1337
|
+
const approvedLabel = rb.approved ? '\u2705 Approved' : rb.approvals.length + '/' + rb.requiredApprovers + ' approvals';
|
|
1338
|
+
html += '<div style="padding:6px 0;border-bottom:1px solid rgba(255,255,255,.04)">'
|
|
1339
|
+
+ '<div style="display:flex;justify-content:space-between;align-items:baseline">'
|
|
1340
|
+
+ '<span style="font-size:11px;font-weight:600;color:var(--text)">' + esc(rb.name) + '</span>'
|
|
1341
|
+
+ '<span style="font-size:10px;color:' + approvedColor + '">' + approvedLabel + '</span>'
|
|
1342
|
+
+ '</div>'
|
|
1343
|
+
+ '<div style="font-size:10px;color:var(--muted);margin-top:2px">Tag: <code>' + esc(rb.incidentTag) + '</code> \xB7 Risk: ' + esc(rb.riskTier) + ' \xB7 ' + (rb.steps.length) + ' step(s)</div>'
|
|
1344
|
+
+ '</div>';
|
|
1345
|
+
}
|
|
1346
|
+
el.innerHTML = html;
|
|
1347
|
+
} catch(e) {}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
async function pollActivityFeed() {
|
|
1351
|
+
try {
|
|
1352
|
+
const d = await fetch('/activity-feed?limit=10').then(r => r.json());
|
|
1353
|
+
const el = document.getElementById('activity-feed-content');
|
|
1354
|
+
if (!el || !d || !d.events) return;
|
|
1355
|
+
if (!d.events.length) {
|
|
1356
|
+
el.innerHTML = '<div style="font-size:11px;color:var(--muted)">No agent activity yet.</div>';
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const verdictColor = {PASS:'var(--green)',BLOCK:'var(--red)',HOLD:'var(--yellow)'};
|
|
1360
|
+
const verdictIcon = {PASS:'\u25B6',BLOCK:'\u{1F6AB}',HOLD:'\u23F8'};
|
|
1361
|
+
el.innerHTML = d.events.map(ev => {
|
|
1362
|
+
const col = verdictColor[ev.verdict] || 'var(--muted)';
|
|
1363
|
+
const icon = verdictIcon[ev.verdict] || '?';
|
|
1364
|
+
const t = new Date(ev.timestamp).toLocaleTimeString();
|
|
1365
|
+
const rules = ev.ruleNames && ev.ruleNames.length ? ' \xB7 ' + esc(ev.ruleNames.join(', ')) : '';
|
|
1366
|
+
return '<div style="display:flex;gap:8px;align-items:flex-start;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)">'
|
|
1367
|
+
+ '<span style="color:'+col+';font-size:11px;min-width:14px">'+icon+'</span>'
|
|
1368
|
+
+ '<div style="flex:1;min-width:0">'
|
|
1369
|
+
+ '<div style="font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(ev.toolName)+(ev.commandArg?' <span style="font-weight:400;color:var(--muted)">'+esc(ev.commandArg.slice(0,60))+'</span>':'')+'</div>'
|
|
1370
|
+
+ '<div style="font-size:10px;color:var(--muted)">'+t+rules+'</div>'
|
|
1371
|
+
+ '</div>'
|
|
1372
|
+
+ '</div>';
|
|
1373
|
+
}).join('');
|
|
1374
|
+
} catch(e) {}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
poll();
|
|
1378
|
+
pollSdkStatus();
|
|
1379
|
+
pollValidateState();
|
|
1380
|
+
pollCalibrationHealth();
|
|
1381
|
+
pollWarRoom();
|
|
1382
|
+
pollCorpusProgress();
|
|
1383
|
+
pollMttr();
|
|
1384
|
+
pollBlunders();
|
|
1385
|
+
pollActivityFeed();
|
|
1386
|
+
pollServiceMemory();
|
|
1387
|
+
pollRecentIncidents();
|
|
1388
|
+
pollRunbooks();
|
|
1389
|
+
setInterval(poll,5000);
|
|
1390
|
+
setInterval(pollSdkStatus,10000);
|
|
1391
|
+
setInterval(pollValidateState,5000);
|
|
1392
|
+
setInterval(pollCalibrationHealth,30000);
|
|
1393
|
+
setInterval(pollWarRoom,10000);
|
|
1394
|
+
setInterval(pollCorpusProgress,30000);
|
|
1395
|
+
setInterval(pollMttr,30000);
|
|
1396
|
+
setInterval(pollBlunders,30000);
|
|
1397
|
+
setInterval(pollActivityFeed,3000);
|
|
1398
|
+
setInterval(pollServiceMemory,30000);
|
|
1399
|
+
setInterval(pollRecentIncidents,30000);
|
|
1400
|
+
setInterval(pollRunbooks,30000);
|
|
1401
|
+
</script>
|
|
1402
|
+
</body></html>`
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
export {
|
|
1406
|
+
createDashboardRouter
|
|
1407
|
+
};
|