nat-engine 1__py3-none-any.whl
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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2538 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
NeuroAgentTest (NAT) — Multi-Agent Neural Network Framework
|
|
4
|
+
Copyright (C) 2026 NAT Contributors
|
|
5
|
+
AGPL-3.0-or-later — see LICENSE for details
|
|
6
|
+
For commercial licensing: licensing@nat-testing.io
|
|
7
|
+
-->
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="UTF-8" />
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
12
|
+
<title>NAT Dashboard — NeuroAgentTest</title>
|
|
13
|
+
|
|
14
|
+
<!-- PWA / mobile meta -->
|
|
15
|
+
<meta name="theme-color" content="#1f6feb" />
|
|
16
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
17
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
18
|
+
<meta name="apple-mobile-web-app-title" content="NAT" />
|
|
19
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
20
|
+
<link rel="manifest" href="/manifest.json" />
|
|
21
|
+
<link rel="apple-touch-icon" href="/pwa-icon-192.png" />
|
|
22
|
+
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
|
|
24
|
+
<style>
|
|
25
|
+
/* ─────────────────────────────────────────────
|
|
26
|
+
CSS Reset & Design Tokens
|
|
27
|
+
───────────────────────────────────────────── */
|
|
28
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
29
|
+
|
|
30
|
+
:root {
|
|
31
|
+
--bg-base: #0d1117;
|
|
32
|
+
--bg-card: #161b22;
|
|
33
|
+
--bg-card2: #1c2128;
|
|
34
|
+
--bg-hover: #21262d;
|
|
35
|
+
--border: #30363d;
|
|
36
|
+
--text-primary: #e6edf3;
|
|
37
|
+
--text-secondary: #8b949e;
|
|
38
|
+
--text-muted: #484f58;
|
|
39
|
+
--accent: #58a6ff;
|
|
40
|
+
--accent-dim: #1f6feb;
|
|
41
|
+
--green: #3fb950;
|
|
42
|
+
--yellow: #d29922;
|
|
43
|
+
--orange: #f0883e;
|
|
44
|
+
--red: #f85149;
|
|
45
|
+
--purple: #bc8cff;
|
|
46
|
+
--radius: 8px;
|
|
47
|
+
--shadow: 0 4px 16px rgba(0,0,0,.4);
|
|
48
|
+
--transition: .15s ease;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
body {
|
|
52
|
+
background: var(--bg-base);
|
|
53
|
+
color: var(--text-primary);
|
|
54
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
55
|
+
font-size: 14px;
|
|
56
|
+
line-height: 1.5;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
a { color: var(--accent); text-decoration: none; }
|
|
60
|
+
a:hover { text-decoration: underline; }
|
|
61
|
+
|
|
62
|
+
/* ─────────────────────────────────────────────
|
|
63
|
+
Layout
|
|
64
|
+
───────────────────────────────────────────── */
|
|
65
|
+
.app { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
|
|
66
|
+
|
|
67
|
+
.topbar {
|
|
68
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
69
|
+
padding: 12px 24px;
|
|
70
|
+
background: var(--bg-card);
|
|
71
|
+
border-bottom: 1px solid var(--border);
|
|
72
|
+
position: sticky; top: 0; z-index: 100;
|
|
73
|
+
}
|
|
74
|
+
.topbar-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 16px; }
|
|
75
|
+
.topbar-brand .logo { width: 28px; height: 28px; background: var(--accent-dim);
|
|
76
|
+
border-radius: 6px; display: flex; align-items: center; justify-content: center;
|
|
77
|
+
font-weight: 900; font-size: 16px; color: #fff; }
|
|
78
|
+
.topbar-meta { display: flex; align-items: center; gap: 16px; color: var(--text-secondary); font-size: 12px; }
|
|
79
|
+
.ws-badge {
|
|
80
|
+
display: flex; align-items: center; gap: 5px; padding: 3px 8px;
|
|
81
|
+
border-radius: 999px; background: var(--bg-card2); border: 1px solid var(--border);
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
}
|
|
84
|
+
.ws-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); transition: var(--transition); }
|
|
85
|
+
.ws-dot.connected { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
86
|
+
|
|
87
|
+
.main { padding: 20px 24px; overflow-y: auto; }
|
|
88
|
+
|
|
89
|
+
/* ─────────────────────────────────────────────
|
|
90
|
+
Summary Bar
|
|
91
|
+
───────────────────────────────────────────── */
|
|
92
|
+
.summary-bar {
|
|
93
|
+
display: grid;
|
|
94
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
95
|
+
gap: 12px;
|
|
96
|
+
margin-bottom: 20px;
|
|
97
|
+
}
|
|
98
|
+
.stat-card {
|
|
99
|
+
background: var(--bg-card);
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
border-radius: var(--radius);
|
|
102
|
+
padding: 16px 18px;
|
|
103
|
+
transition: var(--transition);
|
|
104
|
+
}
|
|
105
|
+
.stat-card:hover { border-color: var(--accent-dim); }
|
|
106
|
+
.stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-secondary); margin-bottom: 6px; }
|
|
107
|
+
.stat-value { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
108
|
+
.stat-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
109
|
+
.health-good { color: var(--green); }
|
|
110
|
+
.health-warn { color: var(--yellow); }
|
|
111
|
+
.health-bad { color: var(--orange); }
|
|
112
|
+
.health-crit { color: var(--red); }
|
|
113
|
+
|
|
114
|
+
/* ─────────────────────────────────────────────
|
|
115
|
+
Section layout
|
|
116
|
+
───────────────────────────────────────────── */
|
|
117
|
+
.section-header {
|
|
118
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
119
|
+
margin-bottom: 12px;
|
|
120
|
+
}
|
|
121
|
+
.section-title { font-size: 15px; font-weight: 600; }
|
|
122
|
+
.refresh-btn {
|
|
123
|
+
background: none; border: 1px solid var(--border); color: var(--text-secondary);
|
|
124
|
+
padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 12px;
|
|
125
|
+
transition: var(--transition);
|
|
126
|
+
}
|
|
127
|
+
.refresh-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
128
|
+
|
|
129
|
+
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
|
|
130
|
+
.full-col { margin-bottom: 20px; }
|
|
131
|
+
|
|
132
|
+
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
|
|
133
|
+
|
|
134
|
+
/* ─────────────────────────────────────────────
|
|
135
|
+
Cards (general)
|
|
136
|
+
───────────────────────────────────────────── */
|
|
137
|
+
.card {
|
|
138
|
+
background: var(--bg-card);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
border-radius: var(--radius);
|
|
141
|
+
padding: 16px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ─────────────────────────────────────────────
|
|
145
|
+
Risk Heatmap
|
|
146
|
+
───────────────────────────────────────────── */
|
|
147
|
+
.heatmap-grid {
|
|
148
|
+
display: grid;
|
|
149
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
150
|
+
gap: 8px;
|
|
151
|
+
}
|
|
152
|
+
.endpoint-cell {
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
padding: 10px 12px;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
transition: var(--transition);
|
|
157
|
+
border: 2px solid transparent;
|
|
158
|
+
position: relative;
|
|
159
|
+
}
|
|
160
|
+
.endpoint-cell:hover { filter: brightness(1.15); }
|
|
161
|
+
.endpoint-cell.selected { border-color: var(--accent); }
|
|
162
|
+
.endpoint-cell .ep-method { font-size: 10px; font-weight: 700; letter-spacing: .05em; opacity: .85; }
|
|
163
|
+
.endpoint-cell .ep-path { font-size: 12px; font-weight: 600; word-break: break-all; margin: 2px 0; }
|
|
164
|
+
.endpoint-cell .ep-score { font-size: 18px; font-weight: 700; }
|
|
165
|
+
.endpoint-cell .ep-trend { position: absolute; top: 8px; right: 10px; font-size: 16px; }
|
|
166
|
+
.endpoint-cell .ep-badge { font-size: 10px; margin-top: 3px; opacity: .8; }
|
|
167
|
+
|
|
168
|
+
/* Risk color bands */
|
|
169
|
+
.risk-green { background: rgba(63,185,80,.18); border-color: rgba(63,185,80,.3) !important; color: #3fb950; }
|
|
170
|
+
.risk-yellow { background: rgba(210,153,34,.18); border-color: rgba(210,153,34,.3) !important; color: #d29922; }
|
|
171
|
+
.risk-orange { background: rgba(240,136,62,.18); border-color: rgba(240,136,62,.3) !important; color: #f0883e; }
|
|
172
|
+
.risk-red { background: rgba(248,81,73,.18); border-color: rgba(248,81,73,.3) !important; color: #f85149; }
|
|
173
|
+
|
|
174
|
+
/* ─────────────────────────────────────────────
|
|
175
|
+
Endpoint Detail Panel
|
|
176
|
+
───────────────────────────────────────────── */
|
|
177
|
+
#detail-panel { display: none; }
|
|
178
|
+
.detail-grid {
|
|
179
|
+
display: grid;
|
|
180
|
+
grid-template-columns: 1fr 1fr;
|
|
181
|
+
gap: 12px;
|
|
182
|
+
}
|
|
183
|
+
@media (max-width: 700px) { .detail-grid { grid-template-columns: 1fr; } }
|
|
184
|
+
.detail-row { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid var(--border); }
|
|
185
|
+
.detail-row:last-child { border-bottom: none; }
|
|
186
|
+
.detail-key { color: var(--text-secondary); }
|
|
187
|
+
.detail-val { font-weight: 600; }
|
|
188
|
+
.progress-bar { height: 6px; border-radius: 3px; background: var(--bg-card2); overflow: hidden; }
|
|
189
|
+
.progress-fill { height: 100%; border-radius: 3px; transition: width .4s ease; }
|
|
190
|
+
|
|
191
|
+
/* ─────────────────────────────────────────────
|
|
192
|
+
Anomaly Feed
|
|
193
|
+
───────────────────────────────────────────── */
|
|
194
|
+
.anomaly-list { display: flex; flex-direction: column; gap: 6px; }
|
|
195
|
+
.anomaly-entry {
|
|
196
|
+
display: grid;
|
|
197
|
+
grid-template-columns: auto 1fr auto;
|
|
198
|
+
gap: 10px;
|
|
199
|
+
align-items: start;
|
|
200
|
+
padding: 10px 12px;
|
|
201
|
+
background: var(--bg-card2);
|
|
202
|
+
border-radius: 6px;
|
|
203
|
+
border-left: 3px solid var(--border);
|
|
204
|
+
transition: var(--transition);
|
|
205
|
+
}
|
|
206
|
+
.anomaly-entry:hover { background: var(--bg-hover); }
|
|
207
|
+
.sev-info { border-left-color: var(--accent); }
|
|
208
|
+
.sev-warning { border-left-color: var(--yellow); }
|
|
209
|
+
.sev-critical { border-left-color: var(--red); }
|
|
210
|
+
.sev-badge {
|
|
211
|
+
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 999px;
|
|
212
|
+
text-transform: uppercase; white-space: nowrap;
|
|
213
|
+
}
|
|
214
|
+
.sev-badge.info { background: rgba(88,166,255,.15); color: var(--accent); }
|
|
215
|
+
.sev-badge.warning { background: rgba(210,153,34,.2); color: var(--yellow); }
|
|
216
|
+
.sev-badge.critical { background: rgba(248,81,73,.2); color: var(--red); }
|
|
217
|
+
.anomaly-body .ep-name { font-weight: 600; font-size: 13px; }
|
|
218
|
+
.anomaly-body .anom-desc { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }
|
|
219
|
+
.anomaly-ts { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
|
220
|
+
|
|
221
|
+
.filter-bar { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
222
|
+
.filter-btn {
|
|
223
|
+
background: var(--bg-card2); border: 1px solid var(--border);
|
|
224
|
+
color: var(--text-secondary); padding: 4px 10px; border-radius: 999px;
|
|
225
|
+
cursor: pointer; font-size: 12px; transition: var(--transition);
|
|
226
|
+
}
|
|
227
|
+
.filter-btn.active { background: var(--accent-dim); border-color: var(--accent); color: #fff; }
|
|
228
|
+
.filter-btn:hover:not(.active) { border-color: var(--accent); color: var(--accent); }
|
|
229
|
+
|
|
230
|
+
/* ─────────────────────────────────────────────
|
|
231
|
+
Agent Activity Panel
|
|
232
|
+
───────────────────────────────────────────── */
|
|
233
|
+
.agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
|
|
234
|
+
.agent-card {
|
|
235
|
+
background: var(--bg-card2); border: 1px solid var(--border); border-radius: 6px; padding: 12px;
|
|
236
|
+
}
|
|
237
|
+
.agent-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
238
|
+
.agent-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
239
|
+
.agent-dot.active { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
240
|
+
.agent-dot.inactive { background: var(--text-muted); }
|
|
241
|
+
.agent-role { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--accent); }
|
|
242
|
+
.agent-name { font-weight: 600; font-size: 13px; }
|
|
243
|
+
.agent-task { font-size: 11px; color: var(--text-secondary); margin-top: 4px; font-style: italic; }
|
|
244
|
+
.belief-row { display: flex; justify-content: space-between; align-items: center; margin-top: 6px; font-size: 12px; }
|
|
245
|
+
.belief-key { color: var(--text-muted); }
|
|
246
|
+
|
|
247
|
+
/* ─────────────────────────────────────────────
|
|
248
|
+
Scan History
|
|
249
|
+
───────────────────────────────────────────── */
|
|
250
|
+
.scan-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
251
|
+
.scan-table th {
|
|
252
|
+
text-align: left; padding: 8px 10px; color: var(--text-secondary);
|
|
253
|
+
border-bottom: 1px solid var(--border); font-weight: 600; font-size: 11px;
|
|
254
|
+
text-transform: uppercase; letter-spacing: .05em;
|
|
255
|
+
}
|
|
256
|
+
.scan-table td { padding: 8px 10px; border-bottom: 1px solid rgba(48,54,61,.5); }
|
|
257
|
+
.scan-table tr:hover td { background: var(--bg-hover); }
|
|
258
|
+
.scan-table tr:last-child td { border-bottom: none; }
|
|
259
|
+
.status-badge {
|
|
260
|
+
font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
|
|
261
|
+
text-transform: uppercase;
|
|
262
|
+
}
|
|
263
|
+
.status-pending { background: rgba(88,166,255,.15); color: var(--accent); }
|
|
264
|
+
.status-running { background: rgba(188,140,255,.15); color: var(--purple); }
|
|
265
|
+
.status-completed { background: rgba(63,185,80,.15); color: var(--green); }
|
|
266
|
+
.status-failed { background: rgba(248,81,73,.15); color: var(--red); }
|
|
267
|
+
|
|
268
|
+
/* ─────────────────────────────────────────────
|
|
269
|
+
Chart container
|
|
270
|
+
───────────────────────────────────────────── */
|
|
271
|
+
.chart-wrap { position: relative; height: 160px; }
|
|
272
|
+
|
|
273
|
+
/* ─────────────────────────────────────────────
|
|
274
|
+
Empty state
|
|
275
|
+
───────────────────────────────────────────── */
|
|
276
|
+
.empty-state {
|
|
277
|
+
text-align: center; padding: 32px 16px; color: var(--text-muted); font-size: 13px;
|
|
278
|
+
}
|
|
279
|
+
.empty-state .emoji { font-size: 32px; display: block; margin-bottom: 8px; }
|
|
280
|
+
|
|
281
|
+
/* ─────────────────────────────────────────────
|
|
282
|
+
Toast notifications
|
|
283
|
+
───────────────────────────────────────────── */
|
|
284
|
+
#toast-container {
|
|
285
|
+
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
|
|
286
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
287
|
+
}
|
|
288
|
+
.toast {
|
|
289
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
|
|
290
|
+
padding: 10px 14px; min-width: 240px; max-width: 360px;
|
|
291
|
+
box-shadow: var(--shadow); font-size: 13px;
|
|
292
|
+
animation: slideIn .2s ease;
|
|
293
|
+
}
|
|
294
|
+
@keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
295
|
+
|
|
296
|
+
/* Scrollbar */
|
|
297
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
298
|
+
::-webkit-scrollbar-track { background: var(--bg-base); }
|
|
299
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
300
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
|
301
|
+
|
|
302
|
+
/* Pulse animation for live indicators */
|
|
303
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
|
|
304
|
+
.pulse { animation: pulse 2s infinite; }
|
|
305
|
+
|
|
306
|
+
/* ─────────────────────────────────────────────
|
|
307
|
+
Tab Navigation
|
|
308
|
+
───────────────────────────────────────────── */
|
|
309
|
+
.tab-nav {
|
|
310
|
+
display: flex; gap: 0; margin-bottom: 20px;
|
|
311
|
+
border-bottom: 1px solid var(--border);
|
|
312
|
+
}
|
|
313
|
+
.tab-btn {
|
|
314
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
315
|
+
color: var(--text-secondary); padding: 10px 18px; cursor: pointer;
|
|
316
|
+
font-size: 13px; font-weight: 500; transition: var(--transition);
|
|
317
|
+
margin-bottom: -1px;
|
|
318
|
+
}
|
|
319
|
+
.tab-btn:hover { color: var(--text-primary); }
|
|
320
|
+
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
321
|
+
|
|
322
|
+
.tab-panel { display: none; }
|
|
323
|
+
.tab-panel.active { display: block; }
|
|
324
|
+
|
|
325
|
+
/* ─────────────────────────────────────────────
|
|
326
|
+
Security Tab
|
|
327
|
+
───────────────────────────────────────────── */
|
|
328
|
+
.sec-controls-grid {
|
|
329
|
+
display: grid;
|
|
330
|
+
grid-template-columns: 1fr 1fr auto auto;
|
|
331
|
+
gap: 10px; align-items: end; flex-wrap: wrap;
|
|
332
|
+
}
|
|
333
|
+
@media (max-width: 900px) { .sec-controls-grid { grid-template-columns: 1fr 1fr; } }
|
|
334
|
+
.form-group { display: flex; flex-direction: column; gap: 4px; }
|
|
335
|
+
.form-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-secondary); }
|
|
336
|
+
.form-input, .form-select {
|
|
337
|
+
background: var(--bg-card2); border: 1px solid var(--border); border-radius: 6px;
|
|
338
|
+
color: var(--text-primary); padding: 7px 10px; font-size: 13px; outline: none;
|
|
339
|
+
transition: var(--transition);
|
|
340
|
+
}
|
|
341
|
+
.form-input:focus, .form-select:focus { border-color: var(--accent); }
|
|
342
|
+
.form-select option { background: var(--bg-card2); }
|
|
343
|
+
|
|
344
|
+
.sec-summary-cards {
|
|
345
|
+
display: grid;
|
|
346
|
+
grid-template-columns: repeat(5, 1fr);
|
|
347
|
+
gap: 10px; margin: 16px 0;
|
|
348
|
+
}
|
|
349
|
+
@media (max-width: 700px) { .sec-summary-cards { grid-template-columns: repeat(3, 1fr); } }
|
|
350
|
+
.sec-count-card {
|
|
351
|
+
background: var(--bg-card2); border: 1px solid var(--border); border-radius: var(--radius);
|
|
352
|
+
padding: 12px 14px; text-align: center;
|
|
353
|
+
}
|
|
354
|
+
.sec-count-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-secondary); margin-bottom: 4px; }
|
|
355
|
+
.sec-count-value { font-size: 26px; font-weight: 700; line-height: 1; }
|
|
356
|
+
|
|
357
|
+
.findings-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
358
|
+
.findings-table th {
|
|
359
|
+
text-align: left; padding: 8px 10px; color: var(--text-secondary);
|
|
360
|
+
border-bottom: 1px solid var(--border); font-weight: 600; font-size: 11px;
|
|
361
|
+
text-transform: uppercase; letter-spacing: .05em;
|
|
362
|
+
}
|
|
363
|
+
.findings-table td { padding: 8px 10px; border-bottom: 1px solid rgba(48,54,61,.5); }
|
|
364
|
+
.findings-table tr.clickable { cursor: pointer; }
|
|
365
|
+
.findings-table tr.clickable:hover td { background: var(--bg-hover); }
|
|
366
|
+
.findings-table tr:last-child td { border-bottom: none; }
|
|
367
|
+
.findings-table tr.prio-high td { background: rgba(248,81,73,.06); }
|
|
368
|
+
.findings-table tr.prio-medium td { background: rgba(210,153,34,.06); }
|
|
369
|
+
.findings-table tr.prio-low td { background: rgba(88,166,255,.04); }
|
|
370
|
+
|
|
371
|
+
.sev-pill {
|
|
372
|
+
font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
|
|
373
|
+
text-transform: uppercase; white-space: nowrap; display: inline-block;
|
|
374
|
+
}
|
|
375
|
+
.sev-critical { background: rgba(248,81,73,.2); color: #f85149; }
|
|
376
|
+
.sev-high { background: rgba(240,136,62,.2); color: #f0883e; }
|
|
377
|
+
.sev-medium { background: rgba(210,153,34,.2); color: #d29922; }
|
|
378
|
+
.sev-low { background: rgba(88,166,255,.2); color: #58a6ff; }
|
|
379
|
+
.sev-info { background: rgba(139,148,158,.2); color: #8b949e; }
|
|
380
|
+
|
|
381
|
+
.prio-badge {
|
|
382
|
+
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
|
|
383
|
+
white-space: nowrap; display: inline-block; font-family: ui-monospace, SFMono-Regular, monospace;
|
|
384
|
+
}
|
|
385
|
+
.prio-badge-high { background: rgba(248,81,73,.2); color: #f85149; }
|
|
386
|
+
.prio-badge-medium { background: rgba(210,153,34,.2); color: #d29922; }
|
|
387
|
+
.prio-badge-low { background: rgba(88,166,255,.2); color: #58a6ff; }
|
|
388
|
+
|
|
389
|
+
#sec-finding-detail { display: none; }
|
|
390
|
+
.finding-detail-grid {
|
|
391
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
|
|
392
|
+
}
|
|
393
|
+
@media (max-width: 700px) { .finding-detail-grid { grid-template-columns: 1fr; } }
|
|
394
|
+
.finding-meta-row {
|
|
395
|
+
display: flex; justify-content: space-between; padding: 5px 0;
|
|
396
|
+
border-bottom: 1px solid var(--border);
|
|
397
|
+
}
|
|
398
|
+
.finding-meta-row:last-child { border-bottom: none; }
|
|
399
|
+
.finding-meta-key { color: var(--text-secondary); }
|
|
400
|
+
.finding-meta-val { font-weight: 600; text-align: right; max-width: 60%; word-break: break-all; }
|
|
401
|
+
.finding-evidence {
|
|
402
|
+
background: var(--bg-base); border: 1px solid var(--border); border-radius: 6px;
|
|
403
|
+
padding: 10px; font-family: ui-monospace, SFMono-Regular, monospace; font-size: 12px;
|
|
404
|
+
white-space: pre-wrap; word-break: break-all; overflow-x: auto;
|
|
405
|
+
color: var(--text-secondary); max-height: 200px; overflow-y: auto;
|
|
406
|
+
}
|
|
407
|
+
#sec-scan-status { font-size: 12px; color: var(--text-secondary); }
|
|
408
|
+
|
|
409
|
+
/* ─────────────────────────────────────────────
|
|
410
|
+
Agents Tab
|
|
411
|
+
───────────────────────────────────────────── */
|
|
412
|
+
.agents-cards-grid {
|
|
413
|
+
display: grid;
|
|
414
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
415
|
+
gap: 12px;
|
|
416
|
+
}
|
|
417
|
+
.agent-full-card {
|
|
418
|
+
background: var(--bg-card2); border: 1px solid var(--border);
|
|
419
|
+
border-radius: var(--radius); padding: 14px;
|
|
420
|
+
cursor: pointer; transition: var(--transition);
|
|
421
|
+
}
|
|
422
|
+
.agent-full-card:hover { border-color: var(--accent-dim); background: var(--bg-hover); }
|
|
423
|
+
.agent-full-card.selected { border-color: var(--accent); }
|
|
424
|
+
.agent-fc-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
425
|
+
.agent-fc-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
|
426
|
+
.agent-fc-dot.active { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
427
|
+
.agent-fc-dot.idle { background: var(--text-muted); }
|
|
428
|
+
.agent-fc-role {
|
|
429
|
+
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
|
|
430
|
+
padding: 1px 6px; border-radius: 999px; background: rgba(88,166,255,.15); color: var(--accent);
|
|
431
|
+
}
|
|
432
|
+
.agent-fc-id { font-weight: 600; font-size: 13px; margin-top: 2px; }
|
|
433
|
+
.agent-fc-status { margin-left: auto; font-size: 11px; color: var(--text-muted); }
|
|
434
|
+
.agent-fc-task { font-size: 11px; color: var(--text-secondary); font-style: italic; margin-bottom: 10px; }
|
|
435
|
+
.agent-fc-stats { display: flex; flex-direction: column; gap: 6px; }
|
|
436
|
+
.agent-fc-stat-row { display: flex; align-items: center; justify-content: space-between; font-size: 12px; }
|
|
437
|
+
.agent-fc-stat-label { color: var(--text-muted); }
|
|
438
|
+
.agent-fc-stat-val { font-weight: 600; }
|
|
439
|
+
.conf-bar-wrap { flex: 1; margin: 0 8px; }
|
|
440
|
+
.conf-bar { height: 5px; border-radius: 3px; background: var(--bg-card); overflow: hidden; }
|
|
441
|
+
.conf-fill { height: 100%; border-radius: 3px; background: var(--accent); transition: width .4s ease; }
|
|
442
|
+
.agent-fc-endpoints { font-size: 11px; color: var(--text-muted); margin-top: 6px; }
|
|
443
|
+
|
|
444
|
+
/* Agent Drilldown Panel */
|
|
445
|
+
#agent-drilldown-panel { display: none; }
|
|
446
|
+
.drilldown-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
447
|
+
.drilldown-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
448
|
+
@media (max-width: 900px) { .drilldown-grid { grid-template-columns: 1fr; } }
|
|
449
|
+
.drilldown-section-title {
|
|
450
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
|
|
451
|
+
color: var(--text-secondary); margin-bottom: 8px;
|
|
452
|
+
}
|
|
453
|
+
.beliefs-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
454
|
+
.beliefs-table th {
|
|
455
|
+
text-align: left; padding: 6px 8px; color: var(--text-secondary);
|
|
456
|
+
border-bottom: 1px solid var(--border); font-weight: 600; font-size: 11px;
|
|
457
|
+
text-transform: uppercase; letter-spacing: .05em; cursor: pointer; user-select: none;
|
|
458
|
+
}
|
|
459
|
+
.beliefs-table th:hover { color: var(--accent); }
|
|
460
|
+
.beliefs-table td { padding: 6px 8px; border-bottom: 1px solid rgba(48,54,61,.4); }
|
|
461
|
+
.beliefs-table tr:last-child td { border-bottom: none; }
|
|
462
|
+
.beliefs-table tr:hover td { background: var(--bg-hover); }
|
|
463
|
+
.belief-bar { width: 60px; height: 5px; border-radius: 3px; background: var(--bg-card); overflow: hidden; display: inline-block; vertical-align: middle; margin-right: 6px; }
|
|
464
|
+
.belief-fill { height: 100%; border-radius: 3px; transition: width .3s ease; }
|
|
465
|
+
.intent-list { display: flex; flex-direction: column; gap: 5px; }
|
|
466
|
+
.intent-item {
|
|
467
|
+
display: flex; justify-content: space-between; align-items: flex-start;
|
|
468
|
+
padding: 6px 8px; background: var(--bg-card2); border-radius: 5px; font-size: 12px;
|
|
469
|
+
}
|
|
470
|
+
.intent-action { font-weight: 600; }
|
|
471
|
+
.intent-target { color: var(--text-secondary); font-size: 11px; margin-top: 2px; }
|
|
472
|
+
.intent-score { color: var(--accent); font-weight: 700; white-space: nowrap; margin-left: 8px; }
|
|
473
|
+
.desire-list { display: flex; flex-direction: column; gap: 4px; }
|
|
474
|
+
.desire-item { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
475
|
+
.desire-name { color: var(--text-secondary); flex: 1; }
|
|
476
|
+
.desire-weight { font-weight: 600; width: 40px; text-align: right; }
|
|
477
|
+
.desire-bar { flex: 1; height: 5px; border-radius: 3px; background: var(--bg-card); overflow: hidden; }
|
|
478
|
+
.desire-fill { height: 100%; border-radius: 3px; background: var(--purple); }
|
|
479
|
+
.chart-wrap-tall { position: relative; height: 220px; }
|
|
480
|
+
|
|
481
|
+
/* ECNP Timeline */
|
|
482
|
+
.ecnp-timeline { display: flex; flex-direction: column; gap: 8px; max-height: 440px; overflow-y: auto; }
|
|
483
|
+
.ecnp-event {
|
|
484
|
+
display: grid; grid-template-columns: auto 1fr auto;
|
|
485
|
+
gap: 10px; align-items: start;
|
|
486
|
+
padding: 10px 12px; background: var(--bg-card2);
|
|
487
|
+
border-radius: 6px; border-left: 3px solid var(--border);
|
|
488
|
+
animation: slideIn .25s ease;
|
|
489
|
+
}
|
|
490
|
+
.ecnp-event.offer { border-left-color: var(--accent); }
|
|
491
|
+
.ecnp-event.bid { border-left-color: var(--yellow); }
|
|
492
|
+
.ecnp-event.award { border-left-color: var(--green); }
|
|
493
|
+
.ecnp-badge {
|
|
494
|
+
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 999px;
|
|
495
|
+
text-transform: uppercase; white-space: nowrap;
|
|
496
|
+
}
|
|
497
|
+
.ecnp-badge.offer { background: rgba(88,166,255,.15); color: var(--accent); }
|
|
498
|
+
.ecnp-badge.bid { background: rgba(210,153,34,.2); color: var(--yellow); }
|
|
499
|
+
.ecnp-badge.award { background: rgba(63,185,80,.2); color: var(--green); }
|
|
500
|
+
.ecnp-body { font-size: 12px; min-width: 0; }
|
|
501
|
+
.ecnp-sender { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
502
|
+
.ecnp-contract { color: var(--text-secondary); font-size: 11px; font-family: ui-monospace, monospace; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
503
|
+
.ecnp-details { color: var(--text-muted); font-size: 11px; margin-top: 4px; }
|
|
504
|
+
.ecnp-ts { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
|
505
|
+
|
|
506
|
+
/* Belief Update Feed */
|
|
507
|
+
.belief-feed { display: flex; flex-direction: column; gap: 4px; max-height: 440px; overflow-y: auto; }
|
|
508
|
+
.belief-feed-entry {
|
|
509
|
+
font-size: 11px; font-family: ui-monospace, SFMono-Regular, monospace;
|
|
510
|
+
padding: 5px 8px; border-radius: 4px; background: var(--bg-card2);
|
|
511
|
+
border-left: 3px solid var(--border);
|
|
512
|
+
animation: slideIn .2s ease;
|
|
513
|
+
}
|
|
514
|
+
.belief-feed-entry.improving { border-left-color: var(--green); }
|
|
515
|
+
.belief-feed-entry.worsening { border-left-color: var(--red); }
|
|
516
|
+
.belief-ts { color: var(--text-muted); }
|
|
517
|
+
.belief-agent { color: var(--accent); font-weight: 700; }
|
|
518
|
+
.belief-ep { color: var(--text-secondary); }
|
|
519
|
+
.belief-old { color: var(--text-muted); }
|
|
520
|
+
.belief-new-good { color: var(--green); font-weight: 700; }
|
|
521
|
+
.belief-new-bad { color: var(--red); font-weight: 700; }
|
|
522
|
+
.belief-ev { color: var(--text-muted); }
|
|
523
|
+
|
|
524
|
+
/* All-Agents / Per-Agent chart section */
|
|
525
|
+
.agents-chart-grid {
|
|
526
|
+
display: grid;
|
|
527
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
528
|
+
gap: 16px; margin-top: 12px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* ─────────────────────────────────────────────
|
|
532
|
+
Queue Panel
|
|
533
|
+
───────────────────────────────────────────── */
|
|
534
|
+
.queue-stat { font-size: 13px; color: var(--text-secondary); }
|
|
535
|
+
.queue-stat span { font-weight: 700; color: var(--text-primary); }
|
|
536
|
+
.queue-item {
|
|
537
|
+
display: flex; align-items: center; gap: 10px;
|
|
538
|
+
padding: 8px 12px; background: var(--bg-card2);
|
|
539
|
+
border-radius: 6px; margin-bottom: 6px;
|
|
540
|
+
border-left: 3px solid var(--border);
|
|
541
|
+
}
|
|
542
|
+
.queue-item.running { border-left-color: var(--purple); }
|
|
543
|
+
.queue-item.pending { border-left-color: var(--accent); }
|
|
544
|
+
.queue-item.completed { border-left-color: var(--green); }
|
|
545
|
+
.queue-item.failed { border-left-color: var(--red); }
|
|
546
|
+
.queue-progress {
|
|
547
|
+
flex: 1; height: 4px; border-radius: 2px; background: var(--bg-card); overflow: hidden;
|
|
548
|
+
}
|
|
549
|
+
.queue-progress-fill { height: 100%; border-radius: 2px; background: var(--purple); animation: pulse 1.5s infinite; }
|
|
550
|
+
|
|
551
|
+
/* ─────────────────────────────────────────────
|
|
552
|
+
Export Buttons
|
|
553
|
+
───────────────────────────────────────────── */
|
|
554
|
+
.export-btn {
|
|
555
|
+
background: none; border: 1px solid var(--border); color: var(--text-secondary);
|
|
556
|
+
padding: 2px 7px; border-radius: 4px; cursor: pointer; font-size: 11px;
|
|
557
|
+
transition: var(--transition); margin-right: 3px;
|
|
558
|
+
}
|
|
559
|
+
.export-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
560
|
+
|
|
561
|
+
/* ─────────────────────────────────────────────
|
|
562
|
+
History Tab
|
|
563
|
+
───────────────────────────────────────────── */
|
|
564
|
+
.history-detail-row { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
|
|
565
|
+
.history-detail-row:last-child { border-bottom: none; }
|
|
566
|
+
.history-detail-key { color: var(--text-secondary); }
|
|
567
|
+
.history-detail-val { font-weight: 600; }
|
|
568
|
+
.page-btn {
|
|
569
|
+
background: var(--bg-card2); border: 1px solid var(--border); color: var(--text-secondary);
|
|
570
|
+
padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 12px;
|
|
571
|
+
transition: var(--transition);
|
|
572
|
+
}
|
|
573
|
+
.page-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
|
574
|
+
.page-btn:disabled { opacity: .4; cursor: default; }
|
|
575
|
+
.page-btn.active { background: var(--accent-dim); border-color: var(--accent); color: #fff; }
|
|
576
|
+
|
|
577
|
+
/* ─────────────────────────────────────────────
|
|
578
|
+
Settings Tab
|
|
579
|
+
───────────────────────────────────────────── */
|
|
580
|
+
.settings-ws-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--text-muted); }
|
|
581
|
+
.settings-ws-dot.connected { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
582
|
+
|
|
583
|
+
/* ─────────────────────────────────────────────
|
|
584
|
+
Error Banner
|
|
585
|
+
───────────────────────────────────────────── */
|
|
586
|
+
#error-banner {
|
|
587
|
+
display: none; padding: 10px 20px; background: rgba(248,81,73,.12);
|
|
588
|
+
border-bottom: 1px solid rgba(248,81,73,.3); font-size: 13px;
|
|
589
|
+
color: #f85149; text-align: center;
|
|
590
|
+
}
|
|
591
|
+
#error-banner a { color: #f85149; text-decoration: underline; }
|
|
592
|
+
|
|
593
|
+
/* ─────────────────────────────────────────────
|
|
594
|
+
Hamburger / Mobile Nav
|
|
595
|
+
───────────────────────────────────────────── */
|
|
596
|
+
.hamburger-btn {
|
|
597
|
+
display: none; background: none; border: 1px solid var(--border);
|
|
598
|
+
color: var(--text-secondary); padding: 5px 10px; border-radius: 6px;
|
|
599
|
+
cursor: pointer; font-size: 18px; line-height: 1;
|
|
600
|
+
}
|
|
601
|
+
@media (max-width: 640px) {
|
|
602
|
+
.hamburger-btn { display: block; }
|
|
603
|
+
.tab-nav { flex-wrap: wrap; max-height: 0; overflow: hidden; transition: max-height .3s ease; }
|
|
604
|
+
.tab-nav.open { max-height: 300px; }
|
|
605
|
+
.tab-btn { flex: 1 1 100%; border-bottom: none; border-top: 1px solid var(--border); }
|
|
606
|
+
.tab-btn.active { border-left: 3px solid var(--accent); }
|
|
607
|
+
.topbar-brand span:last-child { display: none; }
|
|
608
|
+
}
|
|
609
|
+
@media (min-width: 641px) {
|
|
610
|
+
.tab-nav { max-height: none !important; overflow: visible !important; }
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* ─────────────────────────────────────────────
|
|
614
|
+
Dark / Light Mode Toggle
|
|
615
|
+
───────────────────────────────────────────── */
|
|
616
|
+
.theme-btn {
|
|
617
|
+
background: none; border: 1px solid var(--border); color: var(--text-secondary);
|
|
618
|
+
padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 14px;
|
|
619
|
+
transition: var(--transition);
|
|
620
|
+
}
|
|
621
|
+
.theme-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
622
|
+
|
|
623
|
+
/* Light mode override */
|
|
624
|
+
body.light-mode {
|
|
625
|
+
--bg-base: #f6f8fa;
|
|
626
|
+
--bg-card: #ffffff;
|
|
627
|
+
--bg-card2: #f0f2f5;
|
|
628
|
+
--bg-hover: #e8ecf0;
|
|
629
|
+
--border: #d0d7de;
|
|
630
|
+
--text-primary: #1f2328;
|
|
631
|
+
--text-secondary: #57606a;
|
|
632
|
+
--text-muted: #8c959f;
|
|
633
|
+
--shadow: 0 4px 16px rgba(0,0,0,.08);
|
|
634
|
+
}
|
|
635
|
+
</style>
|
|
636
|
+
</head>
|
|
637
|
+
<body>
|
|
638
|
+
<div class="app">
|
|
639
|
+
<!-- ─── Top Bar ─────────────────────────────────────── -->
|
|
640
|
+
<header class="topbar">
|
|
641
|
+
<div class="topbar-brand">
|
|
642
|
+
<div class="logo">N</div>
|
|
643
|
+
<span>NeuroAgentTest</span>
|
|
644
|
+
<span style="color:var(--text-muted);font-weight:400;">Dashboard</span>
|
|
645
|
+
</div>
|
|
646
|
+
<div class="topbar-meta">
|
|
647
|
+
<span id="last-refresh" style="font-size:11px;"></span>
|
|
648
|
+
<button class="theme-btn" id="theme-btn" onclick="toggleTheme()" title="Toggle light/dark mode">☀️</button>
|
|
649
|
+
<div class="ws-badge">
|
|
650
|
+
<div class="ws-dot" id="ws-dot"></div>
|
|
651
|
+
<span id="ws-label">Connecting…</span>
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
<button class="hamburger-btn" id="hamburger-btn" onclick="toggleMobileMenu()" aria-label="Toggle navigation">☰</button>
|
|
655
|
+
</header>
|
|
656
|
+
|
|
657
|
+
<!-- ─── Main Content ────────────────────────────────── -->
|
|
658
|
+
<main class="main">
|
|
659
|
+
|
|
660
|
+
<!-- Tab Navigation -->
|
|
661
|
+
<nav class="tab-nav" id="tab-nav">
|
|
662
|
+
<button class="tab-btn active" data-tab="overview" onclick="switchTab('overview')">📊 Overview</button>
|
|
663
|
+
<button class="tab-btn" data-tab="agents" onclick="switchTab('agents')">🤖 Agents</button>
|
|
664
|
+
<button class="tab-btn" data-tab="security" onclick="switchTab('security')">🔒 Security</button>
|
|
665
|
+
<button class="tab-btn" data-tab="history" onclick="switchTab('history')">📜 History</button>
|
|
666
|
+
<button class="tab-btn" data-tab="settings" onclick="switchTab('settings')">⚙️ Settings</button>
|
|
667
|
+
</nav>
|
|
668
|
+
|
|
669
|
+
<!-- ═══ Overview Tab ═══════════════════════════════ -->
|
|
670
|
+
<div id="tab-overview" class="tab-panel active">
|
|
671
|
+
|
|
672
|
+
<!-- Summary Bar -->
|
|
673
|
+
<div class="summary-bar" id="summary-bar">
|
|
674
|
+
<div class="stat-card">
|
|
675
|
+
<div class="stat-label">Total Scans</div>
|
|
676
|
+
<div class="stat-value" id="s-scans">—</div>
|
|
677
|
+
</div>
|
|
678
|
+
<div class="stat-card">
|
|
679
|
+
<div class="stat-label">Endpoints Discovered</div>
|
|
680
|
+
<div class="stat-value" id="s-endpoints">—</div>
|
|
681
|
+
</div>
|
|
682
|
+
<div class="stat-card">
|
|
683
|
+
<div class="stat-label">Tests Run</div>
|
|
684
|
+
<div class="stat-value" id="s-tests">—</div>
|
|
685
|
+
<div class="stat-sub" id="s-passrate"></div>
|
|
686
|
+
</div>
|
|
687
|
+
<div class="stat-card">
|
|
688
|
+
<div class="stat-label">Pass / Fail</div>
|
|
689
|
+
<div class="stat-value" id="s-passed"><span style="color:var(--green)">—</span></div>
|
|
690
|
+
<div class="stat-sub" id="s-failed-sub"></div>
|
|
691
|
+
</div>
|
|
692
|
+
<div class="stat-card">
|
|
693
|
+
<div class="stat-label">Unique Defects</div>
|
|
694
|
+
<div class="stat-value" id="s-defects">—</div>
|
|
695
|
+
<div class="stat-sub" id="s-defects-sub">by endpoint</div>
|
|
696
|
+
</div>
|
|
697
|
+
<div class="stat-card">
|
|
698
|
+
<div class="stat-label">Active Anomalies</div>
|
|
699
|
+
<div class="stat-value" id="s-anomalies">—</div>
|
|
700
|
+
</div>
|
|
701
|
+
<div class="stat-card">
|
|
702
|
+
<div class="stat-label">Health Score</div>
|
|
703
|
+
<div class="stat-value" id="s-health">—</div>
|
|
704
|
+
<div class="stat-sub" id="s-avgrisk"></div>
|
|
705
|
+
</div>
|
|
706
|
+
<div class="stat-card">
|
|
707
|
+
<div class="stat-label">Last Scan</div>
|
|
708
|
+
<div class="stat-value" style="font-size:14px;padding-top:4px;" id="s-lastscan">—</div>
|
|
709
|
+
</div>
|
|
710
|
+
<div class="stat-card" id="s-llm-card" style="display:none;">
|
|
711
|
+
<div class="stat-label">🤖 LLM Cases</div>
|
|
712
|
+
<div class="stat-value" id="s-llm-count">—</div>
|
|
713
|
+
<div class="stat-sub" id="s-llm-rate"></div>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
<!-- Queue Panel -->
|
|
718
|
+
<div class="full-col card" id="queue-panel" style="margin-bottom:16px;">
|
|
719
|
+
<div class="section-header">
|
|
720
|
+
<span class="section-title">🚦 Scan Queue</span>
|
|
721
|
+
<button class="refresh-btn" onclick="loadQueue()">↻ Refresh</button>
|
|
722
|
+
</div>
|
|
723
|
+
<div id="queue-stats" style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap;">
|
|
724
|
+
<span class="queue-stat"><span id="q-running">0</span> running</span>
|
|
725
|
+
<span class="queue-stat"><span id="q-queued">0</span> queued</span>
|
|
726
|
+
<span class="queue-stat text-muted">capacity: <span id="q-capacity">4</span></span>
|
|
727
|
+
</div>
|
|
728
|
+
<div id="queue-list">
|
|
729
|
+
<div class="empty-state"><span class="emoji">✅</span>No active scans</div>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<!-- Risk Heatmap -->
|
|
734
|
+
<div class="full-col card">
|
|
735
|
+
<div class="section-header">
|
|
736
|
+
<span class="section-title">🔥 Risk Heatmap</span>
|
|
737
|
+
<button class="refresh-btn" onclick="loadEndpoints()">↻ Refresh</button>
|
|
738
|
+
</div>
|
|
739
|
+
<div class="heatmap-grid" id="heatmap-grid">
|
|
740
|
+
<div class="empty-state"><span class="emoji">📡</span>Run a scan to see endpoints</div>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
|
|
744
|
+
<!-- Endpoint Detail Panel -->
|
|
745
|
+
<div class="full-col card" id="detail-panel">
|
|
746
|
+
<div class="section-header">
|
|
747
|
+
<span class="section-title" id="detail-title">Endpoint Detail</span>
|
|
748
|
+
<button class="refresh-btn" onclick="closeDetail()">✕ Close</button>
|
|
749
|
+
</div>
|
|
750
|
+
<div class="detail-grid">
|
|
751
|
+
<!-- Risk Breakdown -->
|
|
752
|
+
<div>
|
|
753
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:.06em;">Risk Breakdown</div>
|
|
754
|
+
<div class="detail-row"><span class="detail-key">Overall Risk</span><span class="detail-val" id="d-risk">—</span></div>
|
|
755
|
+
<div class="detail-row"><span class="detail-key">Fault Rate</span><span class="detail-val" id="d-fault">—</span></div>
|
|
756
|
+
<div style="margin:4px 0 8px;"><div class="progress-bar"><div class="progress-fill" id="pb-fault" style="background:var(--red);width:0%"></div></div></div>
|
|
757
|
+
<div class="detail-row"><span class="detail-key">Latency Signal</span><span class="detail-val" id="d-latency">—</span></div>
|
|
758
|
+
<div style="margin:4px 0 8px;"><div class="progress-bar"><div class="progress-fill" id="pb-latency" style="background:var(--orange);width:0%"></div></div></div>
|
|
759
|
+
<div class="detail-row"><span class="detail-key">Diversity Signal</span><span class="detail-val" id="d-diversity">—</span></div>
|
|
760
|
+
<div style="margin:4px 0 8px;"><div class="progress-bar"><div class="progress-fill" id="pb-diversity" style="background:var(--yellow);width:0%"></div></div></div>
|
|
761
|
+
<div class="detail-row"><span class="detail-key">Belief Signal</span><span class="detail-val" id="d-belief">—</span></div>
|
|
762
|
+
<div class="detail-row"><span class="detail-key">Run Count</span><span class="detail-val" id="d-runs">—</span></div>
|
|
763
|
+
<div class="detail-row"><span class="detail-key">Fault Count</span><span class="detail-val" id="d-faults">—</span></div>
|
|
764
|
+
<div class="detail-row"><span class="detail-key">Trend</span><span class="detail-val" id="d-trend">—</span></div>
|
|
765
|
+
<div class="detail-row"><span class="detail-key">Anomalies</span><span class="detail-val" id="d-anoms">—</span></div>
|
|
766
|
+
</div>
|
|
767
|
+
<!-- Chart placeholder -->
|
|
768
|
+
<div>
|
|
769
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:.06em;">Risk Signal Breakdown</div>
|
|
770
|
+
<div class="chart-wrap"><canvas id="detail-chart"></canvas></div>
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
<!-- Healing Summary (shown when healing data is present) -->
|
|
775
|
+
<div id="d-healing-section" style="display:none;margin-top:16px;padding-top:16px;border-top:1px solid var(--border);">
|
|
776
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:.06em;">🩹 Self-Healing Summary</div>
|
|
777
|
+
<div class="detail-row"><span class="detail-key">Tests Regenerated</span><span class="detail-val" id="d-heal-regenerated">—</span></div>
|
|
778
|
+
<div class="detail-row"><span class="detail-key">Tests Removed</span><span class="detail-val" id="d-heal-removed">—</span></div>
|
|
779
|
+
<div class="detail-row"><span class="detail-key">Tests Kept</span><span class="detail-val" id="d-heal-kept">—</span></div>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
783
|
+
<div class="two-col">
|
|
784
|
+
<!-- Anomaly Feed -->
|
|
785
|
+
<div class="card">
|
|
786
|
+
<div class="section-header">
|
|
787
|
+
<span class="section-title">⚠️ Anomaly Feed</span>
|
|
788
|
+
<button class="refresh-btn" onclick="loadAnomalies()">↻ Refresh</button>
|
|
789
|
+
</div>
|
|
790
|
+
<div class="filter-bar" id="anomaly-filters">
|
|
791
|
+
<button class="filter-btn active" data-sev="all" onclick="filterAnomalies(this)">All</button>
|
|
792
|
+
<button class="filter-btn" data-sev="critical" onclick="filterAnomalies(this)">Critical</button>
|
|
793
|
+
<button class="filter-btn" data-sev="warning" onclick="filterAnomalies(this)">Warning</button>
|
|
794
|
+
<button class="filter-btn" data-sev="info" onclick="filterAnomalies(this)">Info</button>
|
|
795
|
+
</div>
|
|
796
|
+
<div class="anomaly-list" id="anomaly-list">
|
|
797
|
+
<div class="empty-state"><span class="emoji">✅</span>No anomalies detected</div>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
|
|
801
|
+
<!-- Agent Activity -->
|
|
802
|
+
<div class="card">
|
|
803
|
+
<div class="section-header">
|
|
804
|
+
<span class="section-title">🧠 Agent Activity</span>
|
|
805
|
+
<button class="refresh-btn" onclick="renderAgents()">↻ Refresh</button>
|
|
806
|
+
</div>
|
|
807
|
+
<div class="agent-grid" id="agent-grid">
|
|
808
|
+
<!-- Populated by renderAgents() -->
|
|
809
|
+
</div>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
|
|
813
|
+
<!-- Scan History -->
|
|
814
|
+
<div class="full-col card">
|
|
815
|
+
<div class="section-header">
|
|
816
|
+
<span class="section-title">📋 Scan History</span>
|
|
817
|
+
<button class="refresh-btn" onclick="loadScans()">↻ Refresh</button>
|
|
818
|
+
</div>
|
|
819
|
+
<div style="overflow-x:auto;">
|
|
820
|
+
<table class="scan-table" id="scan-table">
|
|
821
|
+
<thead>
|
|
822
|
+
<tr>
|
|
823
|
+
<th>Scan ID</th>
|
|
824
|
+
<th>Status</th>
|
|
825
|
+
<th>Spec</th>
|
|
826
|
+
<th>Base URL</th>
|
|
827
|
+
<th>Tests</th>
|
|
828
|
+
<th>Pass Rate</th>
|
|
829
|
+
<th>Healing</th>
|
|
830
|
+
<th>Started</th>
|
|
831
|
+
<th>Completed</th>
|
|
832
|
+
<th>Export</th>
|
|
833
|
+
</tr>
|
|
834
|
+
</thead>
|
|
835
|
+
<tbody id="scan-tbody">
|
|
836
|
+
<tr><td colspan="10" class="empty-state"><span class="emoji">🔍</span>No scans yet</td></tr>
|
|
837
|
+
</tbody>
|
|
838
|
+
</table>
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
</div><!-- /tab-overview -->
|
|
843
|
+
|
|
844
|
+
<!-- ═══ Security Tab ════════════════════════════════ -->
|
|
845
|
+
<div id="tab-security" class="tab-panel">
|
|
846
|
+
|
|
847
|
+
<!-- Security Scan Controls -->
|
|
848
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
849
|
+
<div class="section-header">
|
|
850
|
+
<span class="section-title">🔍 Security Scan Controls</span>
|
|
851
|
+
<button class="refresh-btn" onclick="loadSecurityData()">↻ Refresh</button>
|
|
852
|
+
</div>
|
|
853
|
+
<div class="sec-controls-grid">
|
|
854
|
+
<div class="form-group">
|
|
855
|
+
<label class="form-label" for="sec-spec-path">Spec Path</label>
|
|
856
|
+
<input class="form-input" id="sec-spec-path" type="text" placeholder="openapi.yaml" />
|
|
857
|
+
</div>
|
|
858
|
+
<div class="form-group">
|
|
859
|
+
<label class="form-label" for="sec-base-url">Base URL</label>
|
|
860
|
+
<input class="form-input" id="sec-base-url" type="text" placeholder="https://api.example.com" />
|
|
861
|
+
</div>
|
|
862
|
+
<div class="form-group">
|
|
863
|
+
<label class="form-label" for="sec-severity">Severity Threshold</label>
|
|
864
|
+
<select class="form-select" id="sec-severity">
|
|
865
|
+
<option value="low">Low</option>
|
|
866
|
+
<option value="medium" selected>Medium</option>
|
|
867
|
+
<option value="high">High</option>
|
|
868
|
+
<option value="critical">Critical</option>
|
|
869
|
+
</select>
|
|
870
|
+
</div>
|
|
871
|
+
<div class="form-group" style="justify-content:flex-end;">
|
|
872
|
+
<button class="refresh-btn" id="run-sec-scan-btn" onclick="runSecurityScan()" style="padding:8px 16px;font-size:13px;">▶ Run Security Scan</button>
|
|
873
|
+
<span id="sec-scan-status"></span>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
<!-- Security Summary -->
|
|
879
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
880
|
+
<div class="section-header">
|
|
881
|
+
<span class="section-title">📊 Security Summary</span>
|
|
882
|
+
</div>
|
|
883
|
+
<div class="sec-summary-cards">
|
|
884
|
+
<div class="sec-count-card">
|
|
885
|
+
<div class="sec-count-label">Critical 🔴</div>
|
|
886
|
+
<div class="sec-count-value sev-critical" id="sec-critical-count">0</div>
|
|
887
|
+
</div>
|
|
888
|
+
<div class="sec-count-card">
|
|
889
|
+
<div class="sec-count-label">High 🟠</div>
|
|
890
|
+
<div class="sec-count-value sev-high" id="sec-high-count">0</div>
|
|
891
|
+
</div>
|
|
892
|
+
<div class="sec-count-card">
|
|
893
|
+
<div class="sec-count-label">Medium 🟡</div>
|
|
894
|
+
<div class="sec-count-value sev-medium" id="sec-medium-count">0</div>
|
|
895
|
+
</div>
|
|
896
|
+
<div class="sec-count-card">
|
|
897
|
+
<div class="sec-count-label">Low 🔵</div>
|
|
898
|
+
<div class="sec-count-value sev-low" id="sec-low-count">0</div>
|
|
899
|
+
</div>
|
|
900
|
+
<div class="sec-count-card">
|
|
901
|
+
<div class="sec-count-label">Info ℹ️</div>
|
|
902
|
+
<div class="sec-count-value sev-info" id="sec-info-count">0</div>
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
|
|
907
|
+
<!-- Security Findings Table -->
|
|
908
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
909
|
+
<div class="section-header">
|
|
910
|
+
<span class="section-title">🛡️ Security Findings</span>
|
|
911
|
+
</div>
|
|
912
|
+
<div style="overflow-x:auto;">
|
|
913
|
+
<table class="findings-table" id="sec-findings-table">
|
|
914
|
+
<thead>
|
|
915
|
+
<tr>
|
|
916
|
+
<th>Severity</th>
|
|
917
|
+
<th>Check ID</th>
|
|
918
|
+
<th>Endpoint</th>
|
|
919
|
+
<th>Title</th>
|
|
920
|
+
<th>Risk Priority</th>
|
|
921
|
+
<th>Confidence</th>
|
|
922
|
+
</tr>
|
|
923
|
+
</thead>
|
|
924
|
+
<tbody id="sec-findings-tbody">
|
|
925
|
+
<tr><td colspan="6"><div class="empty-state"><span class="emoji">🔒</span>Run a security scan to see findings</div></td></tr>
|
|
926
|
+
</tbody>
|
|
927
|
+
</table>
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
|
|
931
|
+
<!-- Prioritization Info Card -->
|
|
932
|
+
<div class="full-col card" id="sec-prioritization-card" style="margin-bottom:16px;display:none;">
|
|
933
|
+
<div class="section-header">
|
|
934
|
+
<span class="section-title">🎯 Belief-Guided Prioritization</span>
|
|
935
|
+
</div>
|
|
936
|
+
<div id="sec-prioritization-summary" style="font-size:13px;color:var(--text-secondary);margin-bottom:10px;"></div>
|
|
937
|
+
<div style="overflow-x:auto;">
|
|
938
|
+
<table class="findings-table" id="sec-prio-table">
|
|
939
|
+
<thead>
|
|
940
|
+
<tr>
|
|
941
|
+
<th>Endpoint</th>
|
|
942
|
+
<th>Priority Score</th>
|
|
943
|
+
<th>Belief Score</th>
|
|
944
|
+
<th>Risk Score</th>
|
|
945
|
+
<th>Checks Run</th>
|
|
946
|
+
</tr>
|
|
947
|
+
</thead>
|
|
948
|
+
<tbody id="sec-prio-tbody"></tbody>
|
|
949
|
+
</table>
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
|
|
953
|
+
<!-- Finding Detail Panel -->
|
|
954
|
+
<div class="full-col card" id="sec-finding-detail">
|
|
955
|
+
<div class="section-header">
|
|
956
|
+
<span class="section-title" id="sec-detail-title">Finding Detail</span>
|
|
957
|
+
<button class="refresh-btn" onclick="closeSecurityDetail()">✕ Close</button>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="finding-detail-grid">
|
|
960
|
+
<div>
|
|
961
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:.06em;">Metadata</div>
|
|
962
|
+
<div class="finding-meta-row"><span class="finding-meta-key">Check ID</span><span class="finding-meta-val" id="fd-check-id">—</span></div>
|
|
963
|
+
<div class="finding-meta-row"><span class="finding-meta-key">Endpoint</span><span class="finding-meta-val" id="fd-endpoint">—</span></div>
|
|
964
|
+
<div class="finding-meta-row"><span class="finding-meta-key">Severity</span><span class="finding-meta-val" id="fd-severity">—</span></div>
|
|
965
|
+
<div class="finding-meta-row"><span class="finding-meta-key">Title</span><span class="finding-meta-val" id="fd-title">—</span></div>
|
|
966
|
+
<div class="finding-meta-row"><span class="finding-meta-key">CWE ID</span><span class="finding-meta-val" id="fd-cwe-id">—</span></div>
|
|
967
|
+
<div style="margin-top:12px;">
|
|
968
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.06em;">Description</div>
|
|
969
|
+
<p id="fd-description" style="font-size:13px;color:var(--text-secondary);line-height:1.6;"></p>
|
|
970
|
+
</div>
|
|
971
|
+
<div style="margin-top:12px;">
|
|
972
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.06em;">Remediation</div>
|
|
973
|
+
<p id="fd-remediation" style="font-size:13px;color:var(--text-secondary);line-height:1.6;"></p>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
<div>
|
|
977
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.06em;">Evidence</div>
|
|
978
|
+
<pre class="finding-evidence" id="fd-evidence"></pre>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
|
|
983
|
+
</div><!-- /tab-security -->
|
|
984
|
+
|
|
985
|
+
<!-- ═══ Agents Tab ════════════════════════════════ -->
|
|
986
|
+
<div id="tab-agents" class="tab-panel">
|
|
987
|
+
|
|
988
|
+
<!-- Agent Cards -->
|
|
989
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
990
|
+
<div class="section-header">
|
|
991
|
+
<span class="section-title">🤖 Active Agents</span>
|
|
992
|
+
<button class="refresh-btn" onclick="loadAgents()">↻ Refresh</button>
|
|
993
|
+
</div>
|
|
994
|
+
<div class="agents-cards-grid" id="agents-cards-grid">
|
|
995
|
+
<div class="empty-state"><span class="emoji">🤖</span>No agents active — start a scan to see live agent data</div>
|
|
996
|
+
</div>
|
|
997
|
+
</div>
|
|
998
|
+
|
|
999
|
+
<!-- Agent Drilldown Panel -->
|
|
1000
|
+
<div class="full-col card" id="agent-drilldown-panel" style="margin-bottom:16px;">
|
|
1001
|
+
<div class="drilldown-header">
|
|
1002
|
+
<span class="section-title" id="dd-agent-title">Agent Detail</span>
|
|
1003
|
+
<button class="refresh-btn" onclick="closeAgentDrilldown()">✕ Close</button>
|
|
1004
|
+
</div>
|
|
1005
|
+
<div class="drilldown-grid">
|
|
1006
|
+
<!-- Left: Belief state table, intentions, desires -->
|
|
1007
|
+
<div>
|
|
1008
|
+
<div class="drilldown-section-title">Belief State</div>
|
|
1009
|
+
<div style="overflow-x:auto;margin-bottom:14px;">
|
|
1010
|
+
<table class="beliefs-table" id="dd-beliefs-table">
|
|
1011
|
+
<thead>
|
|
1012
|
+
<tr>
|
|
1013
|
+
<th onclick="sortBeliefs('endpoint')">Endpoint ⇅</th>
|
|
1014
|
+
<th onclick="sortBeliefs('likelihood')">Fault Likelihood ⇅</th>
|
|
1015
|
+
<th onclick="sortBeliefs('confidence')">Confidence ⇅</th>
|
|
1016
|
+
</tr>
|
|
1017
|
+
</thead>
|
|
1018
|
+
<tbody id="dd-beliefs-tbody"></tbody>
|
|
1019
|
+
</table>
|
|
1020
|
+
</div>
|
|
1021
|
+
<div class="drilldown-section-title">Intentions</div>
|
|
1022
|
+
<div class="intent-list" id="dd-intent-list" style="margin-bottom:14px;"></div>
|
|
1023
|
+
<div class="drilldown-section-title">Desires</div>
|
|
1024
|
+
<div class="desire-list" id="dd-desire-list"></div>
|
|
1025
|
+
</div>
|
|
1026
|
+
<!-- Right: Belief trace chart for this agent -->
|
|
1027
|
+
<div>
|
|
1028
|
+
<div class="drilldown-section-title">Belief Trace</div>
|
|
1029
|
+
<div class="chart-wrap-tall"><canvas id="dd-belief-chart"></canvas></div>
|
|
1030
|
+
</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
|
|
1034
|
+
<!-- ECNP Timeline + Belief Update Feed -->
|
|
1035
|
+
<div class="two-col" style="margin-bottom:20px;">
|
|
1036
|
+
<!-- ECNP Timeline -->
|
|
1037
|
+
<div class="card">
|
|
1038
|
+
<div class="section-header">
|
|
1039
|
+
<span class="section-title">🤝 ECNP Message Flow</span>
|
|
1040
|
+
<button class="refresh-btn" onclick="clearEcnpTimeline()">✕ Clear</button>
|
|
1041
|
+
</div>
|
|
1042
|
+
<div class="ecnp-timeline" id="ecnp-timeline">
|
|
1043
|
+
<div class="empty-state"><span class="emoji">🤝</span>Awaiting ECNP events…</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
|
|
1047
|
+
<!-- Belief Update Feed -->
|
|
1048
|
+
<div class="card">
|
|
1049
|
+
<div class="section-header">
|
|
1050
|
+
<span class="section-title">📡 Belief Update Feed</span>
|
|
1051
|
+
<button class="refresh-btn" onclick="clearBeliefFeed()">✕ Clear</button>
|
|
1052
|
+
</div>
|
|
1053
|
+
<div class="belief-feed" id="belief-feed">
|
|
1054
|
+
<div class="empty-state"><span class="emoji">📡</span>Awaiting belief updates…</div>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
|
|
1059
|
+
<!-- Belief Trace Charts Section -->
|
|
1060
|
+
<div class="full-col card" id="belief-chart-section">
|
|
1061
|
+
<div class="section-header">
|
|
1062
|
+
<span class="section-title">📈 Belief Traces</span>
|
|
1063
|
+
<div style="display:flex;gap:8px;">
|
|
1064
|
+
<button class="filter-btn active" id="chart-view-all" onclick="switchChartView('all')">All Agents</button>
|
|
1065
|
+
<button class="filter-btn" id="chart-view-per" onclick="switchChartView('per')">Per Agent</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
</div>
|
|
1068
|
+
<!-- All Agents overlay chart -->
|
|
1069
|
+
<div id="chart-all-agents" style="margin-top:12px;">
|
|
1070
|
+
<div class="chart-wrap-tall"><canvas id="all-agents-chart"></canvas></div>
|
|
1071
|
+
</div>
|
|
1072
|
+
<!-- Per-agent charts grid -->
|
|
1073
|
+
<div id="chart-per-agent" style="display:none;margin-top:12px;">
|
|
1074
|
+
<div class="agents-chart-grid" id="per-agent-charts-grid">
|
|
1075
|
+
<div class="empty-state"><span class="emoji">📈</span>No agent data yet</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
</div><!-- /tab-agents -->
|
|
1081
|
+
|
|
1082
|
+
<!-- ═══ History Tab ═══════════════════════════════ -->
|
|
1083
|
+
<div id="tab-history" class="tab-panel">
|
|
1084
|
+
|
|
1085
|
+
<!-- History Filters -->
|
|
1086
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
1087
|
+
<div class="section-header">
|
|
1088
|
+
<span class="section-title">📜 Scan History</span>
|
|
1089
|
+
<button class="refresh-btn" onclick="loadHistory()">↻ Refresh</button>
|
|
1090
|
+
</div>
|
|
1091
|
+
<div class="filter-bar" id="history-status-filters" style="margin-bottom:12px;">
|
|
1092
|
+
<button class="filter-btn active" data-status="all" onclick="filterHistory(this)">All</button>
|
|
1093
|
+
<button class="filter-btn" data-status="completed" onclick="filterHistory(this)">Completed</button>
|
|
1094
|
+
<button class="filter-btn" data-status="failed" onclick="filterHistory(this)">Failed</button>
|
|
1095
|
+
<button class="filter-btn" data-status="running" onclick="filterHistory(this)">Running</button>
|
|
1096
|
+
</div>
|
|
1097
|
+
<div style="overflow-x:auto;">
|
|
1098
|
+
<table class="scan-table" id="history-table">
|
|
1099
|
+
<thead>
|
|
1100
|
+
<tr>
|
|
1101
|
+
<th>Scan ID</th>
|
|
1102
|
+
<th>Status</th>
|
|
1103
|
+
<th>Spec</th>
|
|
1104
|
+
<th>Base URL</th>
|
|
1105
|
+
<th>Tests</th>
|
|
1106
|
+
<th>Pass Rate</th>
|
|
1107
|
+
<th>Started</th>
|
|
1108
|
+
<th>Duration</th>
|
|
1109
|
+
<th>Export</th>
|
|
1110
|
+
</tr>
|
|
1111
|
+
</thead>
|
|
1112
|
+
<tbody id="history-tbody">
|
|
1113
|
+
<tr><td colspan="9" class="empty-state"><span class="emoji">🔍</span>No scans yet</td></tr>
|
|
1114
|
+
</tbody>
|
|
1115
|
+
</table>
|
|
1116
|
+
</div>
|
|
1117
|
+
<div id="history-pagination" style="display:flex;gap:8px;justify-content:center;margin-top:12px;"></div>
|
|
1118
|
+
</div>
|
|
1119
|
+
|
|
1120
|
+
<!-- Expanded Scan Detail -->
|
|
1121
|
+
<div class="full-col card" id="history-detail-panel" style="display:none;margin-bottom:16px;">
|
|
1122
|
+
<div class="section-header">
|
|
1123
|
+
<span class="section-title" id="history-detail-title">Scan Detail</span>
|
|
1124
|
+
<button class="refresh-btn" onclick="closeHistoryDetail()">✕ Close</button>
|
|
1125
|
+
</div>
|
|
1126
|
+
<div id="history-detail-content"></div>
|
|
1127
|
+
</div>
|
|
1128
|
+
|
|
1129
|
+
</div><!-- /tab-history -->
|
|
1130
|
+
|
|
1131
|
+
<!-- ═══ Settings Tab ════════════════════════════════ -->
|
|
1132
|
+
<div id="tab-settings" class="tab-panel">
|
|
1133
|
+
|
|
1134
|
+
<!-- Connection Status -->
|
|
1135
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
1136
|
+
<div class="section-header">
|
|
1137
|
+
<span class="section-title">🔌 Connection Status</span>
|
|
1138
|
+
<button class="refresh-btn" onclick="loadSettings()">↻ Refresh</button>
|
|
1139
|
+
</div>
|
|
1140
|
+
<div id="settings-connection-status" style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
|
1141
|
+
<div class="ws-dot" id="settings-ws-dot"></div>
|
|
1142
|
+
<span id="settings-ws-label">Checking…</span>
|
|
1143
|
+
</div>
|
|
1144
|
+
</div>
|
|
1145
|
+
|
|
1146
|
+
<!-- API Key & Auth -->
|
|
1147
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
1148
|
+
<div class="section-header">
|
|
1149
|
+
<span class="section-title">🔑 API Key & Authentication</span>
|
|
1150
|
+
</div>
|
|
1151
|
+
<div id="settings-auth-content">
|
|
1152
|
+
<div class="detail-row"><span class="detail-key">API Key Configured</span><span class="detail-val" id="cfg-api-key-configured">—</span></div>
|
|
1153
|
+
<div class="detail-row"><span class="detail-key">API Key Preview</span><span class="detail-val" id="cfg-api-key-preview" style="font-family:monospace;">—</span></div>
|
|
1154
|
+
</div>
|
|
1155
|
+
<div style="margin-top:12px;padding:12px;background:var(--bg-card2);border-radius:6px;font-size:12px;color:var(--text-secondary);">
|
|
1156
|
+
<div style="font-weight:600;margin-bottom:4px;">Using the API Key in CI/CD</div>
|
|
1157
|
+
<code>curl -H "X-API-Key: <your-key>" http://localhost:8080/api/v1/scan</code>
|
|
1158
|
+
</div>
|
|
1159
|
+
</div>
|
|
1160
|
+
|
|
1161
|
+
<!-- Server Configuration -->
|
|
1162
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
1163
|
+
<div class="section-header">
|
|
1164
|
+
<span class="section-title">⚙️ Server Configuration</span>
|
|
1165
|
+
</div>
|
|
1166
|
+
<div id="settings-server-content">
|
|
1167
|
+
<div class="detail-row"><span class="detail-key">Host</span><span class="detail-val" id="cfg-host">—</span></div>
|
|
1168
|
+
<div class="detail-row"><span class="detail-key">Port</span><span class="detail-val" id="cfg-port">—</span></div>
|
|
1169
|
+
<div class="detail-row"><span class="detail-key">Workers</span><span class="detail-val" id="cfg-workers">—</span></div>
|
|
1170
|
+
<div class="detail-row"><span class="detail-key">Log Level</span><span class="detail-val" id="cfg-log-level">—</span></div>
|
|
1171
|
+
<div class="detail-row"><span class="detail-key">Rate Limit</span><span class="detail-val" id="cfg-rate-limit">—</span></div>
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
|
|
1175
|
+
<!-- Queue Configuration -->
|
|
1176
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
1177
|
+
<div class="section-header">
|
|
1178
|
+
<span class="section-title">🚦 Queue Configuration</span>
|
|
1179
|
+
</div>
|
|
1180
|
+
<div id="settings-queue-content">
|
|
1181
|
+
<div class="detail-row"><span class="detail-key">Max Concurrent Scans</span><span class="detail-val" id="cfg-max-concurrent">—</span></div>
|
|
1182
|
+
<div class="detail-row"><span class="detail-key">Max Queue Depth</span><span class="detail-val" id="cfg-max-queue">—</span></div>
|
|
1183
|
+
<div class="detail-row"><span class="detail-key">Scan Timeout (s)</span><span class="detail-val" id="cfg-timeout">—</span></div>
|
|
1184
|
+
</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
|
|
1187
|
+
<!-- Webhook Configuration -->
|
|
1188
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
1189
|
+
<div class="section-header">
|
|
1190
|
+
<span class="section-title">🪝 Webhook Integration</span>
|
|
1191
|
+
</div>
|
|
1192
|
+
<div id="settings-webhook-content">
|
|
1193
|
+
<div class="detail-row"><span class="detail-key">Default Webhook URL</span><span class="detail-val" id="cfg-webhook-url">—</span></div>
|
|
1194
|
+
<div class="detail-row"><span class="detail-key">HMAC Signing Configured</span><span class="detail-val" id="cfg-hmac">—</span></div>
|
|
1195
|
+
</div>
|
|
1196
|
+
<div style="margin-top:12px;padding:12px;background:var(--bg-card2);border-radius:6px;font-size:12px;color:var(--text-secondary);">
|
|
1197
|
+
<div style="font-weight:600;margin-bottom:4px;">Webhook Payload Example</div>
|
|
1198
|
+
<code>POST <webhook_url><br>X-NAT-Signature: sha256=<hmac><br>{"event":"scan.completed","scan_id":"…"}</code>
|
|
1199
|
+
</div>
|
|
1200
|
+
</div>
|
|
1201
|
+
|
|
1202
|
+
<!-- CI/CD Integration Guide -->
|
|
1203
|
+
<div class="full-col card" style="margin-bottom:16px;">
|
|
1204
|
+
<div class="section-header">
|
|
1205
|
+
<span class="section-title">🚀 CI/CD Integration Guide</span>
|
|
1206
|
+
</div>
|
|
1207
|
+
<div style="font-size:13px;color:var(--text-secondary);line-height:1.7;">
|
|
1208
|
+
<p style="margin-bottom:8px;"><strong>GitHub Actions</strong></p>
|
|
1209
|
+
<pre style="background:var(--bg-base);border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px;overflow-x:auto;">- uses: bg-playground/nat-action@v1
|
|
1210
|
+
with:
|
|
1211
|
+
spec: openapi.yaml
|
|
1212
|
+
base-url: https://api.example.com
|
|
1213
|
+
api-key: ${{ secrets.NAT_API_KEY }}</pre>
|
|
1214
|
+
<p style="margin-top:12px;margin-bottom:8px;"><strong>Docker</strong></p>
|
|
1215
|
+
<pre style="background:var(--bg-base);border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px;overflow-x:auto;">docker run --rm ghcr.io/bg-playground/nat-engine:latest \
|
|
1216
|
+
nat scan --spec openapi.yaml --base-url https://api.example.com</pre>
|
|
1217
|
+
</div>
|
|
1218
|
+
</div>
|
|
1219
|
+
|
|
1220
|
+
</div><!-- /tab-settings -->
|
|
1221
|
+
|
|
1222
|
+
</main>
|
|
1223
|
+
</div>
|
|
1224
|
+
|
|
1225
|
+
<div id="error-banner">
|
|
1226
|
+
⚠️ <span id="error-banner-msg">Connection lost</span>
|
|
1227
|
+
— <a href="https://github.com/bg-playground/MultiAgent-Neural-Network-Framework/blob/main/docs/guides/dashboard.md" target="_blank">Troubleshooting Guide</a>
|
|
1228
|
+
<button onclick="document.getElementById('error-banner').style.display='none'" style="margin-left:12px;background:none;border:none;color:#f85149;cursor:pointer;font-size:14px;">✕</button>
|
|
1229
|
+
</div>
|
|
1230
|
+
|
|
1231
|
+
<!-- Toast container -->
|
|
1232
|
+
<div id="toast-container"></div>
|
|
1233
|
+
|
|
1234
|
+
<script>
|
|
1235
|
+
/* ────────────────────────────────────────────────────────────────
|
|
1236
|
+
NAT Dashboard — vanilla JS SPA
|
|
1237
|
+
──────────────────────────────────────────────────────────────── */
|
|
1238
|
+
|
|
1239
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
1240
|
+
let _summary = null;
|
|
1241
|
+
let _endpoints = [];
|
|
1242
|
+
let _anomalies = [];
|
|
1243
|
+
let _scans = [];
|
|
1244
|
+
let _selectedEndpoint = null;
|
|
1245
|
+
let _anomalyFilter = 'all';
|
|
1246
|
+
let _detailChart = null;
|
|
1247
|
+
let _ws = null;
|
|
1248
|
+
let _refreshTimer = null;
|
|
1249
|
+
let _secFindings = [];
|
|
1250
|
+
let _secPrioritization = [];
|
|
1251
|
+
let _activeTab = 'overview';
|
|
1252
|
+
let _secScanPollTimer = null;
|
|
1253
|
+
let _historyPage = 1;
|
|
1254
|
+
let _historyPerPage = 20;
|
|
1255
|
+
let _historyStatusFilter = 'all';
|
|
1256
|
+
let _historyScans = [];
|
|
1257
|
+
let _settings = null;
|
|
1258
|
+
let _queueData = null;
|
|
1259
|
+
|
|
1260
|
+
// ─── Agents Tab State ────────────────────────────────────────────
|
|
1261
|
+
let _agentStates = {}; // agent_id → last agent_state payload
|
|
1262
|
+
let _ecnpEvents = []; // array of ecnp_event payloads (capped at 100)
|
|
1263
|
+
let _beliefUpdates = []; // array of belief_update payloads (capped at 50)
|
|
1264
|
+
let _beliefCharts = {}; // agent_id → Chart.js instance (per-agent trace)
|
|
1265
|
+
let _allAgentsChart = null; // Chart.js instance for All Agents overlay
|
|
1266
|
+
let _ddChart = null; // Chart.js instance inside drilldown panel
|
|
1267
|
+
let _selectedAgentId = null;
|
|
1268
|
+
let _beliefTraceData = {}; // agent_id → { labels: [...], endpoints: { ep: [...] } }
|
|
1269
|
+
let _chartUpdateTimer = null;
|
|
1270
|
+
let _chartView = 'all'; // 'all' | 'per'
|
|
1271
|
+
let _beliefsSortKey = 'likelihood';
|
|
1272
|
+
let _beliefsSortAsc = false;
|
|
1273
|
+
let _agentColors = {}; // agent_id → colour string
|
|
1274
|
+
let _paletteIdx = 0;
|
|
1275
|
+
const AGENT_PALETTE = ['#58a6ff','#3fb950','#d29922','#f0883e','#bc8cff','#f85149','#56d364','#e3b341'];
|
|
1276
|
+
const MAX_TRACE_POINTS = 60;
|
|
1277
|
+
const MAX_ECNP_EVENTS = 100;
|
|
1278
|
+
const MAX_BELIEF_UPDATES = 50;
|
|
1279
|
+
|
|
1280
|
+
// ─── Utility ────────────────────────────────────────────────────
|
|
1281
|
+
function riskClass(score) {
|
|
1282
|
+
if (score < 0.3) return 'risk-green';
|
|
1283
|
+
if (score < 0.6) return 'risk-yellow';
|
|
1284
|
+
if (score < 0.8) return 'risk-orange';
|
|
1285
|
+
return 'risk-red';
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function healthClass(score) {
|
|
1289
|
+
if (score >= 0.8) return 'health-good';
|
|
1290
|
+
if (score >= 0.6) return 'health-warn';
|
|
1291
|
+
if (score >= 0.4) return 'health-bad';
|
|
1292
|
+
return 'health-crit';
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function trendArrow(trend) {
|
|
1296
|
+
if (!trend) return '→';
|
|
1297
|
+
const t = trend.toLowerCase();
|
|
1298
|
+
if (t === 'up' || t === 'increasing') return '↑';
|
|
1299
|
+
if (t === 'down' || t === 'decreasing') return '↓';
|
|
1300
|
+
return '→';
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function pct(v) { return (v * 100).toFixed(1) + '%'; }
|
|
1304
|
+
function fmt2(v) { return parseFloat(v).toFixed(3); }
|
|
1305
|
+
|
|
1306
|
+
function relTime(iso) {
|
|
1307
|
+
if (!iso) return '—';
|
|
1308
|
+
const diff = (Date.now() - new Date(iso).getTime()) / 1000;
|
|
1309
|
+
if (diff < 60) return Math.round(diff) + 's ago';
|
|
1310
|
+
if (diff < 3600) return Math.round(diff / 60) + 'm ago';
|
|
1311
|
+
if (diff < 86400) return Math.round(diff / 3600) + 'h ago';
|
|
1312
|
+
return new Date(iso).toLocaleDateString();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function formatDuration(startIso, endIso) {
|
|
1316
|
+
if (!startIso || !endIso) return '—';
|
|
1317
|
+
const durationSec = Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 1000);
|
|
1318
|
+
if (durationSec < 0) return '—';
|
|
1319
|
+
if (durationSec >= 60) return Math.round(durationSec / 60) + 'm ' + (durationSec % 60) + 's';
|
|
1320
|
+
return durationSec + 's';
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function shortId(id) {
|
|
1324
|
+
return id ? id.substring(0, 8) + '…' : '—';
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function toast(msg, duration = 4000) {
|
|
1328
|
+
const el = document.createElement('div');
|
|
1329
|
+
el.className = 'toast';
|
|
1330
|
+
el.textContent = msg;
|
|
1331
|
+
document.getElementById('toast-container').appendChild(el);
|
|
1332
|
+
setTimeout(() => el.remove(), duration);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async function apiFetch(path) {
|
|
1336
|
+
const res = await fetch(path);
|
|
1337
|
+
if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
|
|
1338
|
+
return res.json();
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// ─── Summary Bar ────────────────────────────────────────────────
|
|
1342
|
+
async function loadSummary() {
|
|
1343
|
+
try {
|
|
1344
|
+
_summary = await apiFetch('/api/v1/dashboard/summary');
|
|
1345
|
+
renderSummary();
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
console.warn('Summary fetch failed', e);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function renderSummary() {
|
|
1352
|
+
if (!_summary) return;
|
|
1353
|
+
const s = _summary;
|
|
1354
|
+
document.getElementById('s-scans').textContent = s.total_scans;
|
|
1355
|
+
document.getElementById('s-endpoints').textContent = s.endpoints_discovered;
|
|
1356
|
+
document.getElementById('s-tests').textContent = s.total_tests_run;
|
|
1357
|
+
document.getElementById('s-passrate').textContent = s.total_tests_run > 0
|
|
1358
|
+
? 'Pass rate: ' + pct(s.pass_rate) : '';
|
|
1359
|
+
document.getElementById('s-anomalies').textContent = s.active_anomalies;
|
|
1360
|
+
const hEl = document.getElementById('s-health');
|
|
1361
|
+
hEl.textContent = pct(s.health_score);
|
|
1362
|
+
hEl.className = 'stat-value ' + healthClass(s.health_score);
|
|
1363
|
+
document.getElementById('s-avgrisk').textContent = 'Avg risk: ' + fmt2(s.avg_risk_score);
|
|
1364
|
+
document.getElementById('s-lastscan').textContent = relTime(s.last_scan_at);
|
|
1365
|
+
document.getElementById('last-refresh').textContent = 'Updated ' + new Date().toLocaleTimeString();
|
|
1366
|
+
// Pass/Fail card
|
|
1367
|
+
const passedEl = document.getElementById('s-passed');
|
|
1368
|
+
if (passedEl) {
|
|
1369
|
+
const passedCount = s.total_tests_run > 0 ? Math.round(s.total_tests_run * s.pass_rate) : 0;
|
|
1370
|
+
const failedCount = s.total_tests_run - passedCount;
|
|
1371
|
+
passedEl.innerHTML = `<span style="color:var(--green)">${passedCount}</span>`;
|
|
1372
|
+
const failedSub = document.getElementById('s-failed-sub');
|
|
1373
|
+
if (failedSub) failedSub.textContent = `${failedCount} failed`;
|
|
1374
|
+
}
|
|
1375
|
+
// Unique defects (anomalies by endpoint count)
|
|
1376
|
+
const defectsEl = document.getElementById('s-defects');
|
|
1377
|
+
if (defectsEl) defectsEl.textContent = s.anomalies_found || 0;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// ─── Heatmap ────────────────────────────────────────────────────
|
|
1381
|
+
async function loadEndpoints() {
|
|
1382
|
+
try {
|
|
1383
|
+
_endpoints = await apiFetch('/api/v1/dashboard/endpoints');
|
|
1384
|
+
renderHeatmap();
|
|
1385
|
+
} catch (e) {
|
|
1386
|
+
console.warn('Endpoints fetch failed', e);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function renderHeatmap() {
|
|
1391
|
+
const grid = document.getElementById('heatmap-grid');
|
|
1392
|
+
if (!_endpoints.length) {
|
|
1393
|
+
grid.innerHTML = '<div class="empty-state"><span class="emoji">📡</span>Run a scan to see endpoints</div>';
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
grid.innerHTML = _endpoints.map((ep, i) => {
|
|
1397
|
+
const rc = riskClass(ep.risk_score);
|
|
1398
|
+
const parts = ep.endpoint.split(' ');
|
|
1399
|
+
const method = parts.length > 1 ? parts[0] : 'GET';
|
|
1400
|
+
const path = parts.length > 1 ? parts.slice(1).join(' ') : ep.endpoint;
|
|
1401
|
+
const sel = _selectedEndpoint === ep.endpoint ? ' selected' : '';
|
|
1402
|
+
return `<div class="endpoint-cell ${rc}${sel}" onclick="selectEndpoint(${i})" title="${ep.endpoint}">
|
|
1403
|
+
<div class="ep-trend">${trendArrow(ep.trend)}</div>
|
|
1404
|
+
<div class="ep-method">${escHtml(method)}</div>
|
|
1405
|
+
<div class="ep-path">${escHtml(path)}</div>
|
|
1406
|
+
<div class="ep-score">${fmt2(ep.risk_score)}</div>
|
|
1407
|
+
<div class="ep-badge">${ep.run_count} runs · ${ep.anomaly_count} anomalies</div>
|
|
1408
|
+
</div>`;
|
|
1409
|
+
}).join('');
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function escHtml(s) {
|
|
1413
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// ─── Detail Panel ───────────────────────────────────────────────
|
|
1417
|
+
function selectEndpoint(idx) {
|
|
1418
|
+
const ep = _endpoints[idx];
|
|
1419
|
+
if (!ep) return;
|
|
1420
|
+
_selectedEndpoint = ep.endpoint;
|
|
1421
|
+
renderHeatmap(); // re-highlight
|
|
1422
|
+
showDetail(ep);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function showDetail(ep) {
|
|
1426
|
+
document.getElementById('detail-panel').style.display = 'block';
|
|
1427
|
+
document.getElementById('detail-title').textContent = ep.endpoint;
|
|
1428
|
+
|
|
1429
|
+
const rc = riskClass(ep.risk_score);
|
|
1430
|
+
const color = { 'risk-green': '#3fb950', 'risk-yellow': '#d29922', 'risk-orange': '#f0883e', 'risk-red': '#f85149' }[rc] || '#58a6ff';
|
|
1431
|
+
|
|
1432
|
+
document.getElementById('d-risk').textContent = fmt2(ep.risk_score);
|
|
1433
|
+
document.getElementById('d-risk').style.color = color;
|
|
1434
|
+
document.getElementById('d-fault').textContent = pct(ep.fault_rate);
|
|
1435
|
+
document.getElementById('d-latency').textContent = fmt2(ep.latency_signal);
|
|
1436
|
+
document.getElementById('d-diversity').textContent = fmt2(ep.diversity_signal);
|
|
1437
|
+
document.getElementById('d-belief').textContent = fmt2(ep.belief_signal);
|
|
1438
|
+
document.getElementById('d-runs').textContent = ep.run_count;
|
|
1439
|
+
document.getElementById('d-faults').textContent = ep.fault_count;
|
|
1440
|
+
document.getElementById('d-trend').textContent = trendArrow(ep.trend) + ' ' + ep.trend;
|
|
1441
|
+
document.getElementById('d-anoms').textContent = ep.anomaly_count;
|
|
1442
|
+
|
|
1443
|
+
document.getElementById('pb-fault').style.width = pct(Math.min(ep.fault_rate, 1));
|
|
1444
|
+
document.getElementById('pb-latency').style.width = pct(Math.min(ep.latency_signal, 1));
|
|
1445
|
+
document.getElementById('pb-diversity').style.width = pct(Math.min(ep.diversity_signal, 1));
|
|
1446
|
+
|
|
1447
|
+
// Radar/doughnut chart
|
|
1448
|
+
if (_detailChart) { _detailChart.destroy(); _detailChart = null; }
|
|
1449
|
+
const ctx = document.getElementById('detail-chart').getContext('2d');
|
|
1450
|
+
_detailChart = new Chart(ctx, {
|
|
1451
|
+
type: 'doughnut',
|
|
1452
|
+
data: {
|
|
1453
|
+
labels: ['Fault Rate', 'Latency', 'Diversity', 'Belief'],
|
|
1454
|
+
datasets: [{
|
|
1455
|
+
data: [
|
|
1456
|
+
ep.fault_rate,
|
|
1457
|
+
ep.latency_signal,
|
|
1458
|
+
ep.diversity_signal,
|
|
1459
|
+
ep.belief_signal
|
|
1460
|
+
],
|
|
1461
|
+
backgroundColor: ['rgba(248,81,73,.7)','rgba(240,136,62,.7)','rgba(210,153,34,.7)','rgba(88,166,255,.7)'],
|
|
1462
|
+
borderColor: 'transparent',
|
|
1463
|
+
borderWidth: 0,
|
|
1464
|
+
}]
|
|
1465
|
+
},
|
|
1466
|
+
options: {
|
|
1467
|
+
responsive: true,
|
|
1468
|
+
maintainAspectRatio: false,
|
|
1469
|
+
plugins: {
|
|
1470
|
+
legend: { position: 'bottom', labels: { color: '#8b949e', font: { size: 11 }, padding: 8, boxWidth: 10 } }
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
document.getElementById('detail-panel').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function closeDetail() {
|
|
1479
|
+
_selectedEndpoint = null;
|
|
1480
|
+
document.getElementById('detail-panel').style.display = 'none';
|
|
1481
|
+
renderHeatmap();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// ─── Anomaly Feed ────────────────────────────────────────────────
|
|
1485
|
+
async function loadAnomalies() {
|
|
1486
|
+
try {
|
|
1487
|
+
_anomalies = await apiFetch('/api/v1/dashboard/anomalies');
|
|
1488
|
+
renderAnomalies();
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
console.warn('Anomaly fetch failed', e);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function filterAnomalies(btn) {
|
|
1495
|
+
document.querySelectorAll('#anomaly-filters .filter-btn').forEach(b => b.classList.remove('active'));
|
|
1496
|
+
btn.classList.add('active');
|
|
1497
|
+
_anomalyFilter = btn.dataset.sev;
|
|
1498
|
+
renderAnomalies();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function renderAnomalies() {
|
|
1502
|
+
const list = document.getElementById('anomaly-list');
|
|
1503
|
+
let filtered = _anomalies;
|
|
1504
|
+
if (_anomalyFilter !== 'all') {
|
|
1505
|
+
filtered = _anomalies.filter(a => a.severity === _anomalyFilter);
|
|
1506
|
+
}
|
|
1507
|
+
if (!filtered.length) {
|
|
1508
|
+
list.innerHTML = '<div class="empty-state"><span class="emoji">✅</span>No anomalies' +
|
|
1509
|
+
(_anomalyFilter !== 'all' ? ' for this filter' : ' detected') + '</div>';
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
list.innerHTML = filtered.slice(0, 50).map(a => `
|
|
1513
|
+
<div class="anomaly-entry sev-${escHtml(a.severity)}">
|
|
1514
|
+
<span class="sev-badge ${escHtml(a.severity)}">${escHtml(a.severity)}</span>
|
|
1515
|
+
<div class="anomaly-body">
|
|
1516
|
+
<div class="ep-name">${escHtml(a.endpoint)}</div>
|
|
1517
|
+
<div class="anom-desc">${escHtml(a.anomaly_type)} — ${escHtml(a.description)}</div>
|
|
1518
|
+
</div>
|
|
1519
|
+
<span class="anomaly-ts">${relTime(a.detected_at)}</span>
|
|
1520
|
+
</div>`).join('');
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ─── Agent Activity ──────────────────────────────────────────────
|
|
1524
|
+
function renderAgents() {
|
|
1525
|
+
// Build representative agents based on BDI architecture (NAT roles)
|
|
1526
|
+
const roles = [
|
|
1527
|
+
{ id: 'planner-1', role: 'Planner', task: 'Analyzing spec and generating test plan', beliefs: { fault_prior: 0.2, confidence: 0.85 } },
|
|
1528
|
+
{ id: 'executor-1', role: 'Executor', task: 'Executing prioritized test cases', beliefs: { throughput: 0.92, error_rate: 0.05 } },
|
|
1529
|
+
{ id: 'executor-2', role: 'Executor', task: 'Awaiting task assignment', beliefs: { throughput: 0.0, error_rate: 0.0 } },
|
|
1530
|
+
{ id: 'analyzer-1', role: 'Analyzer', task: 'Scoring risk and detecting anomalies', beliefs: { avg_risk: _summary ? _summary.avg_risk_score : 0, anomalies: _summary ? _summary.anomalies_found : 0 } },
|
|
1531
|
+
{ id: 'coordinator-1', role: 'Coordinator', task: 'Allocating ECNP contracts to executors', beliefs: { active_contracts: _endpoints.length, capacity: 4 } },
|
|
1532
|
+
];
|
|
1533
|
+
|
|
1534
|
+
const active = _scans.some(s => s.status === 'running');
|
|
1535
|
+
|
|
1536
|
+
document.getElementById('agent-grid').innerHTML = roles.map(ag => {
|
|
1537
|
+
const isActive = active || ag.role === 'Analyzer' || ag.role === 'Coordinator';
|
|
1538
|
+
return `<div class="agent-card">
|
|
1539
|
+
<div class="agent-header">
|
|
1540
|
+
<div class="agent-dot ${isActive ? 'active' : 'inactive'}"></div>
|
|
1541
|
+
<div>
|
|
1542
|
+
<div class="agent-role">${escHtml(ag.role)}</div>
|
|
1543
|
+
<div class="agent-name">${escHtml(ag.id)}</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1546
|
+
<div class="agent-task">${escHtml(ag.task)}</div>
|
|
1547
|
+
${Object.entries(ag.beliefs).map(([k, v]) =>
|
|
1548
|
+
`<div class="belief-row"><span class="belief-key">${escHtml(k)}</span><span>${typeof v === 'number' ? fmt2(v) : escHtml(String(v))}</span></div>`
|
|
1549
|
+
).join('')}
|
|
1550
|
+
</div>`;
|
|
1551
|
+
}).join('');
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// ─── Queue Panel ────────────────────────────────────────────────
|
|
1555
|
+
async function loadQueue() {
|
|
1556
|
+
try {
|
|
1557
|
+
_queueData = await apiFetch('/api/v1/queue');
|
|
1558
|
+
renderQueue();
|
|
1559
|
+
} catch (e) {
|
|
1560
|
+
console.warn('Queue fetch failed', e);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function renderQueue() {
|
|
1565
|
+
if (!_queueData) return;
|
|
1566
|
+
document.getElementById('q-running').textContent = _queueData.running || 0;
|
|
1567
|
+
document.getElementById('q-queued').textContent = _queueData.queued || 0;
|
|
1568
|
+
document.getElementById('q-capacity').textContent = _queueData.capacity || 4;
|
|
1569
|
+
|
|
1570
|
+
const listEl = document.getElementById('queue-list');
|
|
1571
|
+
if (!listEl) return;
|
|
1572
|
+
const running = _queueData.running || 0;
|
|
1573
|
+
const queued = _queueData.queued || 0;
|
|
1574
|
+
if (running === 0 && queued === 0) {
|
|
1575
|
+
listEl.innerHTML = '<div class="empty-state"><span class="emoji">✅</span>No active scans</div>';
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
// Build queue display from the locally cached _scans list
|
|
1579
|
+
let html = '';
|
|
1580
|
+
_scans.filter(s => s.status === 'running').forEach(s => {
|
|
1581
|
+
html += `<div class="queue-item running">
|
|
1582
|
+
<span class="status-badge status-running">running</span>
|
|
1583
|
+
<span style="font-family:monospace;font-size:12px;">${shortId(s.scan_id)}</span>
|
|
1584
|
+
<span style="color:var(--text-muted);font-size:11px;">${escHtml(s.spec_name || '—')}</span>
|
|
1585
|
+
<div class="queue-progress"><div class="queue-progress-fill" style="width:100%"></div></div>
|
|
1586
|
+
</div>`;
|
|
1587
|
+
});
|
|
1588
|
+
_scans.filter(s => s.status === 'pending').forEach((s, i) => {
|
|
1589
|
+
html += `<div class="queue-item pending">
|
|
1590
|
+
<span class="status-badge status-pending">queued #${i + 1}</span>
|
|
1591
|
+
<span style="font-family:monospace;font-size:12px;">${shortId(s.scan_id)}</span>
|
|
1592
|
+
<span style="color:var(--text-muted);font-size:11px;">${escHtml(s.spec_name || '—')}</span>
|
|
1593
|
+
</div>`;
|
|
1594
|
+
});
|
|
1595
|
+
listEl.innerHTML = html || '<div class="empty-state"><span class="emoji">✅</span>No active scans</div>';
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// ─── Export ─────────────────────────────────────────────────────
|
|
1599
|
+
function exportScan(scanId, format) {
|
|
1600
|
+
const a = document.createElement('a');
|
|
1601
|
+
a.href = `/api/v1/scan/${encodeURIComponent(scanId)}/export?format=${encodeURIComponent(format)}`;
|
|
1602
|
+
a.download = `scan-${scanId}.${format}`;
|
|
1603
|
+
document.body.appendChild(a);
|
|
1604
|
+
a.click();
|
|
1605
|
+
document.body.removeChild(a);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// ─── History Tab ─────────────────────────────────────────────────
|
|
1609
|
+
async function loadHistory() {
|
|
1610
|
+
try {
|
|
1611
|
+
const qs = `per_page=${_historyPerPage}&page=${_historyPage}` +
|
|
1612
|
+
(_historyStatusFilter !== 'all' ? `&status=${encodeURIComponent(_historyStatusFilter)}` : '');
|
|
1613
|
+
_historyScans = await apiFetch(`/api/v1/scans?${qs}`);
|
|
1614
|
+
renderHistory();
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
console.warn('History fetch failed', e);
|
|
1617
|
+
toast('⚠️ Failed to load scan history');
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function filterHistory(btn) {
|
|
1622
|
+
document.querySelectorAll('#history-status-filters .filter-btn').forEach(b => b.classList.remove('active'));
|
|
1623
|
+
btn.classList.add('active');
|
|
1624
|
+
_historyStatusFilter = btn.dataset.status;
|
|
1625
|
+
_historyPage = 1;
|
|
1626
|
+
loadHistory();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function renderHistory() {
|
|
1630
|
+
const tbody = document.getElementById('history-tbody');
|
|
1631
|
+
if (!tbody) return;
|
|
1632
|
+
if (!_historyScans.length) {
|
|
1633
|
+
tbody.innerHTML = '<tr><td colspan="9"><div class="empty-state"><span class="emoji">🔍</span>No scans found</div></td></tr>';
|
|
1634
|
+
renderHistoryPagination();
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
tbody.innerHTML = _historyScans.map((s, i) => {
|
|
1638
|
+
const durationStr = formatDuration(s.created_at, s.completed_at);
|
|
1639
|
+
return `<tr style="cursor:pointer;" onclick="showHistoryDetail(${i})">
|
|
1640
|
+
<td style="font-family:monospace;font-size:12px;" title="${escHtml(s.scan_id)}">${shortId(s.scan_id)}</td>
|
|
1641
|
+
<td><span class="status-badge status-${escHtml(s.status)}">${escHtml(s.status)}</span></td>
|
|
1642
|
+
<td>${escHtml(s.spec_name || '—')}</td>
|
|
1643
|
+
<td style="max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(s.base_url||'')}">${escHtml(s.base_url || '—')}</td>
|
|
1644
|
+
<td>${s.total || 0}</td>
|
|
1645
|
+
<td>${s.total > 0 ? pct(s.pass_rate) : '—'}</td>
|
|
1646
|
+
<td>${relTime(s.created_at)}</td>
|
|
1647
|
+
<td>${durationStr}</td>
|
|
1648
|
+
<td>
|
|
1649
|
+
<button class="export-btn" onclick="event.stopPropagation();exportScan('${escHtml(s.scan_id)}','json')">⬇ JSON</button>
|
|
1650
|
+
<button class="export-btn" onclick="event.stopPropagation();exportScan('${escHtml(s.scan_id)}','csv')">⬇ CSV</button>
|
|
1651
|
+
<button class="export-btn" onclick="event.stopPropagation();exportScan('${escHtml(s.scan_id)}','html')">⬇ HTML</button>
|
|
1652
|
+
</td>
|
|
1653
|
+
</tr>`;
|
|
1654
|
+
}).join('');
|
|
1655
|
+
renderHistoryPagination();
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function renderHistoryPagination() {
|
|
1659
|
+
const el = document.getElementById('history-pagination');
|
|
1660
|
+
if (!el) return;
|
|
1661
|
+
const prevDisabled = _historyPage <= 1 ? 'disabled' : '';
|
|
1662
|
+
const nextDisabled = _historyScans.length < _historyPerPage ? 'disabled' : '';
|
|
1663
|
+
el.innerHTML = `
|
|
1664
|
+
<button class="page-btn" ${prevDisabled} onclick="changeHistoryPage(${_historyPage - 1})">← Prev</button>
|
|
1665
|
+
<span class="page-btn active">Page ${_historyPage}</span>
|
|
1666
|
+
<button class="page-btn" ${nextDisabled} onclick="changeHistoryPage(${_historyPage + 1})">Next →</button>`;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function changeHistoryPage(page) {
|
|
1670
|
+
if (page < 1) return;
|
|
1671
|
+
_historyPage = page;
|
|
1672
|
+
loadHistory();
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function showHistoryDetail(idx) {
|
|
1676
|
+
const s = _historyScans[idx];
|
|
1677
|
+
if (!s) return;
|
|
1678
|
+
const panel = document.getElementById('history-detail-panel');
|
|
1679
|
+
const content = document.getElementById('history-detail-content');
|
|
1680
|
+
const title = document.getElementById('history-detail-title');
|
|
1681
|
+
if (!panel || !content || !title) return;
|
|
1682
|
+
title.textContent = 'Scan ' + s.scan_id;
|
|
1683
|
+
const durationStr = formatDuration(s.created_at, s.completed_at);
|
|
1684
|
+
content.innerHTML = `
|
|
1685
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
|
1686
|
+
<div>
|
|
1687
|
+
<div class="history-detail-row"><span class="history-detail-key">Scan ID</span><span class="history-detail-val" style="font-family:monospace;font-size:12px;">${escHtml(s.scan_id)}</span></div>
|
|
1688
|
+
<div class="history-detail-row"><span class="history-detail-key">Status</span><span class="history-detail-val"><span class="status-badge status-${escHtml(s.status)}">${escHtml(s.status)}</span></span></div>
|
|
1689
|
+
<div class="history-detail-row"><span class="history-detail-key">Spec</span><span class="history-detail-val">${escHtml(s.spec_name || '—')}</span></div>
|
|
1690
|
+
<div class="history-detail-row"><span class="history-detail-key">Base URL</span><span class="history-detail-val">${escHtml(s.base_url || '—')}</span></div>
|
|
1691
|
+
<div class="history-detail-row"><span class="history-detail-key">Started</span><span class="history-detail-val">${escHtml(s.created_at || '—')}</span></div>
|
|
1692
|
+
<div class="history-detail-row"><span class="history-detail-key">Completed</span><span class="history-detail-val">${escHtml(s.completed_at || '—')}</span></div>
|
|
1693
|
+
<div class="history-detail-row"><span class="history-detail-key">Duration</span><span class="history-detail-val">${durationStr}</span></div>
|
|
1694
|
+
</div>
|
|
1695
|
+
<div>
|
|
1696
|
+
<div class="history-detail-row"><span class="history-detail-key">Total Tests</span><span class="history-detail-val">${s.total || 0}</span></div>
|
|
1697
|
+
<div class="history-detail-row"><span class="history-detail-key">Passed</span><span class="history-detail-val" style="color:var(--green);">${s.passed || 0}</span></div>
|
|
1698
|
+
<div class="history-detail-row"><span class="history-detail-key">Failed</span><span class="history-detail-val" style="color:var(--red);">${s.failed || 0}</span></div>
|
|
1699
|
+
<div class="history-detail-row"><span class="history-detail-key">Pass Rate</span><span class="history-detail-val">${s.total > 0 ? pct(s.pass_rate) : '—'}</span></div>
|
|
1700
|
+
${s.llm_cases_generated > 0 ? `<div class="history-detail-row"><span class="history-detail-key">LLM Cases</span><span class="history-detail-val" style="color:var(--purple);">${s.llm_cases_generated}</span></div>` : ''}
|
|
1701
|
+
</div>
|
|
1702
|
+
</div>
|
|
1703
|
+
<div style="display:flex;gap:8px;margin-top:14px;">
|
|
1704
|
+
<button class="export-btn" style="font-size:12px;padding:4px 10px;" onclick="exportScan('${escHtml(s.scan_id)}','json')">⬇ Export JSON</button>
|
|
1705
|
+
<button class="export-btn" style="font-size:12px;padding:4px 10px;" onclick="exportScan('${escHtml(s.scan_id)}','csv')">⬇ Export CSV</button>
|
|
1706
|
+
<button class="export-btn" style="font-size:12px;padding:4px 10px;" onclick="exportScan('${escHtml(s.scan_id)}','html')">⬇ Export HTML</button>
|
|
1707
|
+
</div>`;
|
|
1708
|
+
panel.style.display = 'block';
|
|
1709
|
+
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function closeHistoryDetail() {
|
|
1713
|
+
const panel = document.getElementById('history-detail-panel');
|
|
1714
|
+
if (panel) panel.style.display = 'none';
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// ─── Settings Tab ────────────────────────────────────────────────
|
|
1718
|
+
async function loadSettings() {
|
|
1719
|
+
try {
|
|
1720
|
+
_settings = await apiFetch('/api/v1/dashboard/settings');
|
|
1721
|
+
renderSettings();
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
console.warn('Settings fetch failed', e);
|
|
1724
|
+
toast('⚠️ Failed to load settings');
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function renderSettings() {
|
|
1729
|
+
if (!_settings) return;
|
|
1730
|
+
const s = _settings.server || {};
|
|
1731
|
+
const a = _settings.auth || {};
|
|
1732
|
+
const q = _settings.queue || {};
|
|
1733
|
+
const w = _settings.webhook || {};
|
|
1734
|
+
|
|
1735
|
+
_setText('cfg-host', s.host || '—');
|
|
1736
|
+
_setText('cfg-port', s.port != null ? String(s.port) : '—');
|
|
1737
|
+
_setText('cfg-workers', s.workers != null ? String(s.workers) : '—');
|
|
1738
|
+
_setText('cfg-log-level', s.log_level || '—');
|
|
1739
|
+
_setText('cfg-rate-limit', _settings.rate_limit || 'unlimited');
|
|
1740
|
+
|
|
1741
|
+
_setText('cfg-api-key-configured', a.api_key_configured ? '✅ Yes' : '⚠️ Not set');
|
|
1742
|
+
_setText('cfg-api-key-preview', a.api_key_preview || '(none)');
|
|
1743
|
+
|
|
1744
|
+
_setText('cfg-max-concurrent', String(q.max_concurrent_scans || 4));
|
|
1745
|
+
_setText('cfg-max-queue', String(q.max_queue_depth || 20));
|
|
1746
|
+
_setText('cfg-timeout', String(q.scan_timeout_seconds || 300));
|
|
1747
|
+
|
|
1748
|
+
_setText('cfg-webhook-url', w.default_url || '(not configured)');
|
|
1749
|
+
_setText('cfg-hmac', w.hmac_configured ? '✅ Yes' : '⚠️ No');
|
|
1750
|
+
|
|
1751
|
+
const settingsWsDot = document.getElementById('settings-ws-dot');
|
|
1752
|
+
const settingsWsLabel = document.getElementById('settings-ws-label');
|
|
1753
|
+
if (settingsWsDot && settingsWsLabel) {
|
|
1754
|
+
const connected = _ws && _ws.readyState === WebSocket.OPEN;
|
|
1755
|
+
settingsWsDot.className = 'ws-dot' + (connected ? ' connected' : '');
|
|
1756
|
+
settingsWsLabel.textContent = connected ? '🟢 Connected' : '🔴 Disconnected';
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function _setText(id, text) {
|
|
1761
|
+
const el = document.getElementById(id);
|
|
1762
|
+
if (el) el.textContent = text;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// ─── Agents Tab ─────────────────────────────────────────────────
|
|
1766
|
+
function agentColor(id) {
|
|
1767
|
+
if (!_agentColors[id]) {
|
|
1768
|
+
_agentColors[id] = AGENT_PALETTE[_paletteIdx % AGENT_PALETTE.length];
|
|
1769
|
+
_paletteIdx++;
|
|
1770
|
+
}
|
|
1771
|
+
return _agentColors[id];
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
async function loadAgents() {
|
|
1775
|
+
try {
|
|
1776
|
+
const agents = await apiFetch('/api/v1/dashboard/agents');
|
|
1777
|
+
agents.forEach(a => { _agentStates[a.agent_id] = a; });
|
|
1778
|
+
renderAgentCards();
|
|
1779
|
+
scheduleChartUpdate();
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
console.warn('Agents fetch failed', e);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function renderAgentCards() {
|
|
1786
|
+
const grid = document.getElementById('agents-cards-grid');
|
|
1787
|
+
const agents = Object.values(_agentStates);
|
|
1788
|
+
if (!agents.length) {
|
|
1789
|
+
grid.innerHTML = '<div class="empty-state"><span class="emoji">🤖</span>No agents active — start a scan to see live agent data</div>';
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
grid.innerHTML = agents.map(ag => {
|
|
1793
|
+
const role = ag.agent_role || 'Unknown';
|
|
1794
|
+
const confVals = Object.values(ag.confidence || {});
|
|
1795
|
+
const confidence = confVals.length ? confVals.reduce((a, b) => a + b, 0) / confVals.length : 0;
|
|
1796
|
+
const confPct = Math.round(confidence * 100);
|
|
1797
|
+
const endpointCount = Object.keys(ag.beliefs || {}).length;
|
|
1798
|
+
const firstIntent = (ag.intentions || [])[0];
|
|
1799
|
+
const taskDesc = firstIntent
|
|
1800
|
+
? (firstIntent.action || '') + (firstIntent.target ? ': ' + firstIntent.target : '')
|
|
1801
|
+
: 'Idle';
|
|
1802
|
+
const sel = _selectedAgentId === ag.agent_id ? ' selected' : '';
|
|
1803
|
+
return `<div class="agent-full-card${sel}" onclick="openAgentDrilldown('${escHtml(ag.agent_id)}')" data-agent-id="${escHtml(ag.agent_id)}">
|
|
1804
|
+
<div class="agent-fc-header">
|
|
1805
|
+
<div class="agent-fc-dot active"></div>
|
|
1806
|
+
<div>
|
|
1807
|
+
<div class="agent-fc-role">${escHtml(role)}</div>
|
|
1808
|
+
<div class="agent-fc-id">${escHtml(ag.agent_id)}</div>
|
|
1809
|
+
</div>
|
|
1810
|
+
<span class="agent-fc-status">● active</span>
|
|
1811
|
+
</div>
|
|
1812
|
+
<div class="agent-fc-task">${escHtml(taskDesc)}</div>
|
|
1813
|
+
<div class="agent-fc-stats">
|
|
1814
|
+
<div class="agent-fc-stat-row">
|
|
1815
|
+
<span class="agent-fc-stat-label">Confidence</span>
|
|
1816
|
+
<div class="conf-bar-wrap"><div class="conf-bar"><div class="conf-fill" style="width:${confPct}%"></div></div></div>
|
|
1817
|
+
<span class="agent-fc-stat-val">${confPct}%</span>
|
|
1818
|
+
</div>
|
|
1819
|
+
<div class="agent-fc-stat-row">
|
|
1820
|
+
<span class="agent-fc-stat-label">Entropy</span>
|
|
1821
|
+
<span class="agent-fc-stat-val">${fmt2(ag.entropy || 0)}</span>
|
|
1822
|
+
</div>
|
|
1823
|
+
</div>
|
|
1824
|
+
<div class="agent-fc-endpoints">${endpointCount} endpoint${endpointCount !== 1 ? 's' : ''} tracked</div>
|
|
1825
|
+
</div>`;
|
|
1826
|
+
}).join('');
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function openAgentDrilldown(agentId) {
|
|
1830
|
+
const ag = _agentStates[agentId];
|
|
1831
|
+
if (!ag) return;
|
|
1832
|
+
_selectedAgentId = agentId;
|
|
1833
|
+
renderAgentCards();
|
|
1834
|
+
document.getElementById('dd-agent-title').textContent = (ag.agent_role || 'Agent') + ' · ' + agentId;
|
|
1835
|
+
document.getElementById('agent-drilldown-panel').style.display = 'block';
|
|
1836
|
+
|
|
1837
|
+
_beliefsSortKey = 'likelihood';
|
|
1838
|
+
_beliefsSortAsc = false;
|
|
1839
|
+
renderDrilldownBeliefs(ag);
|
|
1840
|
+
|
|
1841
|
+
const intentList = document.getElementById('dd-intent-list');
|
|
1842
|
+
const intentions = ag.intentions || [];
|
|
1843
|
+
if (!intentions.length) {
|
|
1844
|
+
intentList.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No active intentions</div>';
|
|
1845
|
+
} else {
|
|
1846
|
+
intentList.innerHTML = intentions.map(i => `
|
|
1847
|
+
<div class="intent-item">
|
|
1848
|
+
<div>
|
|
1849
|
+
<div class="intent-action">${escHtml(i.action || '—')}</div>
|
|
1850
|
+
<div class="intent-target">${escHtml(i.target || '—')}</div>
|
|
1851
|
+
</div>
|
|
1852
|
+
<div class="intent-score">${fmt2(i.score || 0)}</div>
|
|
1853
|
+
</div>`).join('');
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
const desireList = document.getElementById('dd-desire-list');
|
|
1857
|
+
const desireEntries = Object.entries(ag.desires || {});
|
|
1858
|
+
if (!desireEntries.length) {
|
|
1859
|
+
desireList.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No desires</div>';
|
|
1860
|
+
} else {
|
|
1861
|
+
desireList.innerHTML = desireEntries.map(([k, v]) => {
|
|
1862
|
+
const p = Math.round(v * 100);
|
|
1863
|
+
return `<div class="desire-item">
|
|
1864
|
+
<span class="desire-name">${escHtml(k)}</span>
|
|
1865
|
+
<div class="desire-bar"><div class="desire-fill" style="width:${p}%"></div></div>
|
|
1866
|
+
<span class="desire-weight">${fmt2(v)}</span>
|
|
1867
|
+
</div>`;
|
|
1868
|
+
}).join('');
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
renderDrilldownChart(agentId);
|
|
1872
|
+
document.getElementById('agent-drilldown-panel').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function renderDrilldownBeliefs(ag) {
|
|
1876
|
+
const tbody = document.getElementById('dd-beliefs-tbody');
|
|
1877
|
+
const beliefs = ag.beliefs || {};
|
|
1878
|
+
const confidence = ag.confidence || {};
|
|
1879
|
+
let rows = Object.entries(beliefs).map(([ep, fl]) => ({
|
|
1880
|
+
endpoint: ep, likelihood: fl, confidence: confidence[ep] || 0
|
|
1881
|
+
}));
|
|
1882
|
+
rows.sort((a, b) => {
|
|
1883
|
+
const va = a[_beliefsSortKey], vb = b[_beliefsSortKey];
|
|
1884
|
+
if (typeof va === 'string') return _beliefsSortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
1885
|
+
return _beliefsSortAsc ? va - vb : vb - va;
|
|
1886
|
+
});
|
|
1887
|
+
if (!rows.length) {
|
|
1888
|
+
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:var(--text-muted);padding:12px;">No beliefs yet</td></tr>';
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
tbody.innerHTML = rows.map(r => {
|
|
1892
|
+
const flPct = Math.round(r.likelihood * 100);
|
|
1893
|
+
const flColor = r.likelihood > 0.6 ? '#f85149' : r.likelihood > 0.35 ? '#f0883e' : '#3fb950';
|
|
1894
|
+
return `<tr>
|
|
1895
|
+
<td style="font-family:monospace;font-size:11px;">${escHtml(r.endpoint)}</td>
|
|
1896
|
+
<td><span class="belief-bar"><span class="belief-fill" style="width:${flPct}%;background:${flColor}"></span></span>${fmt2(r.likelihood)}</td>
|
|
1897
|
+
<td>${Math.round(r.confidence * 100)}%</td>
|
|
1898
|
+
</tr>`;
|
|
1899
|
+
}).join('');
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function sortBeliefs(key) {
|
|
1903
|
+
if (_beliefsSortKey === key) {
|
|
1904
|
+
_beliefsSortAsc = !_beliefsSortAsc;
|
|
1905
|
+
} else {
|
|
1906
|
+
_beliefsSortKey = key;
|
|
1907
|
+
_beliefsSortAsc = false;
|
|
1908
|
+
}
|
|
1909
|
+
if (_selectedAgentId && _agentStates[_selectedAgentId]) {
|
|
1910
|
+
renderDrilldownBeliefs(_agentStates[_selectedAgentId]);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function closeAgentDrilldown() {
|
|
1915
|
+
_selectedAgentId = null;
|
|
1916
|
+
document.getElementById('agent-drilldown-panel').style.display = 'none';
|
|
1917
|
+
if (_ddChart) { _ddChart.destroy(); _ddChart = null; }
|
|
1918
|
+
renderAgentCards();
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// ─── Belief Trace Data ──────────────────────────────────────────
|
|
1922
|
+
function updateBeliefTrace(agentId, beliefs, timestamp) {
|
|
1923
|
+
if (!_beliefTraceData[agentId]) {
|
|
1924
|
+
_beliefTraceData[agentId] = { labels: [], endpoints: {} };
|
|
1925
|
+
}
|
|
1926
|
+
const trace = _beliefTraceData[agentId];
|
|
1927
|
+
const label = new Date(timestamp).toLocaleTimeString();
|
|
1928
|
+
trace.labels.push(label);
|
|
1929
|
+
if (trace.labels.length > MAX_TRACE_POINTS) trace.labels.shift();
|
|
1930
|
+
Object.entries(beliefs).forEach(([ep, val]) => {
|
|
1931
|
+
if (!trace.endpoints[ep]) trace.endpoints[ep] = [];
|
|
1932
|
+
trace.endpoints[ep].push(val);
|
|
1933
|
+
if (trace.endpoints[ep].length > MAX_TRACE_POINTS) trace.endpoints[ep].shift();
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function _makeBeliefChartConfig(labels, datasets) {
|
|
1938
|
+
return {
|
|
1939
|
+
type: 'line',
|
|
1940
|
+
data: { labels, datasets },
|
|
1941
|
+
options: {
|
|
1942
|
+
responsive: true,
|
|
1943
|
+
maintainAspectRatio: false,
|
|
1944
|
+
animation: false,
|
|
1945
|
+
scales: {
|
|
1946
|
+
x: {
|
|
1947
|
+
ticks: { color: '#8b949e', font: { size: 10 }, maxTicksLimit: 8 },
|
|
1948
|
+
grid: { color: 'rgba(48,54,61,.5)' },
|
|
1949
|
+
},
|
|
1950
|
+
y: {
|
|
1951
|
+
min: 0, max: 1,
|
|
1952
|
+
ticks: { color: '#8b949e', font: { size: 10 } },
|
|
1953
|
+
grid: { color: 'rgba(48,54,61,.5)' },
|
|
1954
|
+
title: { display: true, text: 'Fault Likelihood', color: '#8b949e', font: { size: 10 } },
|
|
1955
|
+
},
|
|
1956
|
+
},
|
|
1957
|
+
plugins: {
|
|
1958
|
+
legend: { position: 'bottom', labels: { color: '#8b949e', font: { size: 10 }, padding: 6, boxWidth: 10 } },
|
|
1959
|
+
},
|
|
1960
|
+
},
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function renderDrilldownChart(agentId) {
|
|
1965
|
+
if (_ddChart) { _ddChart.destroy(); _ddChart = null; }
|
|
1966
|
+
const ctx = document.getElementById('dd-belief-chart').getContext('2d');
|
|
1967
|
+
const trace = _beliefTraceData[agentId];
|
|
1968
|
+
if (!trace || !trace.labels.length) {
|
|
1969
|
+
_ddChart = new Chart(ctx, _makeBeliefChartConfig([], []));
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
const datasets = Object.entries(trace.endpoints).map(([ep, vals], i) => ({
|
|
1973
|
+
label: ep,
|
|
1974
|
+
data: vals,
|
|
1975
|
+
borderColor: AGENT_PALETTE[i % AGENT_PALETTE.length],
|
|
1976
|
+
backgroundColor: 'transparent',
|
|
1977
|
+
tension: 0.3,
|
|
1978
|
+
borderWidth: 1.5,
|
|
1979
|
+
pointRadius: 2,
|
|
1980
|
+
}));
|
|
1981
|
+
_ddChart = new Chart(ctx, _makeBeliefChartConfig(trace.labels, datasets));
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// ─── Chart Update (throttled ~1 fps) ────────────────────────────
|
|
1985
|
+
function scheduleChartUpdate() {
|
|
1986
|
+
if (_chartUpdateTimer) return;
|
|
1987
|
+
_chartUpdateTimer = setTimeout(() => {
|
|
1988
|
+
_chartUpdateTimer = null;
|
|
1989
|
+
_doChartUpdate();
|
|
1990
|
+
}, 1000);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function _doChartUpdate() {
|
|
1994
|
+
if (_chartView === 'all') {
|
|
1995
|
+
_updateAllAgentsChart();
|
|
1996
|
+
} else {
|
|
1997
|
+
_updatePerAgentCharts();
|
|
1998
|
+
}
|
|
1999
|
+
if (_selectedAgentId) {
|
|
2000
|
+
renderDrilldownChart(_selectedAgentId);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function _updateAllAgentsChart() {
|
|
2005
|
+
const ctx = document.getElementById('all-agents-chart').getContext('2d');
|
|
2006
|
+
const agentIds = Object.keys(_beliefTraceData);
|
|
2007
|
+
const datasets = agentIds.map(id => {
|
|
2008
|
+
const trace = _beliefTraceData[id];
|
|
2009
|
+
const eps = Object.values(trace.endpoints);
|
|
2010
|
+
const avgData = trace.labels.map((_, ti) => {
|
|
2011
|
+
const vals = eps.map(arr => arr[ti]).filter(v => v !== undefined);
|
|
2012
|
+
return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
|
|
2013
|
+
});
|
|
2014
|
+
return {
|
|
2015
|
+
label: id,
|
|
2016
|
+
data: avgData,
|
|
2017
|
+
borderColor: agentColor(id),
|
|
2018
|
+
backgroundColor: 'transparent',
|
|
2019
|
+
tension: 0.3,
|
|
2020
|
+
borderWidth: 2,
|
|
2021
|
+
pointRadius: 2,
|
|
2022
|
+
};
|
|
2023
|
+
});
|
|
2024
|
+
const labels = agentIds.length ? (_beliefTraceData[agentIds[0]].labels || []) : [];
|
|
2025
|
+
if (_allAgentsChart) {
|
|
2026
|
+
_allAgentsChart.data.labels = labels;
|
|
2027
|
+
_allAgentsChart.data.datasets = datasets;
|
|
2028
|
+
_allAgentsChart.update('none');
|
|
2029
|
+
} else {
|
|
2030
|
+
_allAgentsChart = new Chart(ctx, _makeBeliefChartConfig(labels, datasets));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function _updatePerAgentCharts() {
|
|
2035
|
+
const grid = document.getElementById('per-agent-charts-grid');
|
|
2036
|
+
const agents = Object.keys(_agentStates);
|
|
2037
|
+
if (!agents.length) {
|
|
2038
|
+
grid.innerHTML = '<div class="empty-state"><span class="emoji">📈</span>No agent data yet</div>';
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
const placeholder = grid.querySelector('.empty-state');
|
|
2042
|
+
if (placeholder) placeholder.remove();
|
|
2043
|
+
|
|
2044
|
+
agents.forEach(id => {
|
|
2045
|
+
const safeId = id.replace(/[^a-z0-9]/gi, '-');
|
|
2046
|
+
let wrapper = document.getElementById('chart-agent-' + safeId);
|
|
2047
|
+
if (!wrapper) {
|
|
2048
|
+
wrapper = document.createElement('div');
|
|
2049
|
+
wrapper.id = 'chart-agent-' + safeId;
|
|
2050
|
+
wrapper.className = 'card';
|
|
2051
|
+
wrapper.innerHTML = '<div style="font-size:12px;font-weight:600;margin-bottom:8px;">' + escHtml(id) + '</div>'
|
|
2052
|
+
+ '<div class="chart-wrap-tall"><canvas id="canvas-agent-' + safeId + '"></canvas></div>';
|
|
2053
|
+
grid.appendChild(wrapper);
|
|
2054
|
+
}
|
|
2055
|
+
const trace = _beliefTraceData[id];
|
|
2056
|
+
if (!trace) return;
|
|
2057
|
+
const datasets = Object.entries(trace.endpoints).map(([ep, vals], i) => ({
|
|
2058
|
+
label: ep,
|
|
2059
|
+
data: vals,
|
|
2060
|
+
borderColor: AGENT_PALETTE[i % AGENT_PALETTE.length],
|
|
2061
|
+
backgroundColor: 'transparent',
|
|
2062
|
+
tension: 0.3,
|
|
2063
|
+
borderWidth: 1.5,
|
|
2064
|
+
pointRadius: 2,
|
|
2065
|
+
}));
|
|
2066
|
+
if (_beliefCharts[id]) {
|
|
2067
|
+
_beliefCharts[id].data.labels = trace.labels;
|
|
2068
|
+
_beliefCharts[id].data.datasets = datasets;
|
|
2069
|
+
_beliefCharts[id].update('none');
|
|
2070
|
+
} else {
|
|
2071
|
+
const canvas = document.getElementById('canvas-agent-' + safeId);
|
|
2072
|
+
if (canvas) {
|
|
2073
|
+
_beliefCharts[id] = new Chart(canvas.getContext('2d'), _makeBeliefChartConfig(trace.labels, datasets));
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
function switchChartView(view) {
|
|
2080
|
+
_chartView = view;
|
|
2081
|
+
document.getElementById('chart-all-agents').style.display = view === 'all' ? 'block' : 'none';
|
|
2082
|
+
document.getElementById('chart-per-agent').style.display = view === 'per' ? 'block' : 'none';
|
|
2083
|
+
document.getElementById('chart-view-all').classList.toggle('active', view === 'all');
|
|
2084
|
+
document.getElementById('chart-view-per').classList.toggle('active', view === 'per');
|
|
2085
|
+
scheduleChartUpdate();
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// ─── ECNP Timeline ──────────────────────────────────────────────
|
|
2089
|
+
function appendEcnpEvent(ev) {
|
|
2090
|
+
_ecnpEvents.unshift(ev);
|
|
2091
|
+
if (_ecnpEvents.length > MAX_ECNP_EVENTS) _ecnpEvents.pop();
|
|
2092
|
+
_renderEcnpTimeline();
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function _renderEcnpTimeline() {
|
|
2096
|
+
const el = document.getElementById('ecnp-timeline');
|
|
2097
|
+
if (!_ecnpEvents.length) {
|
|
2098
|
+
el.innerHTML = '<div class="empty-state"><span class="emoji">🤝</span>Awaiting ECNP events…</div>';
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
el.innerHTML = _ecnpEvents.slice(0, MAX_ECNP_EVENTS).map(ev => {
|
|
2102
|
+
const kind = (ev.event_kind || 'offer').toLowerCase();
|
|
2103
|
+
const detailStr = Object.entries(ev.details || {})
|
|
2104
|
+
.map(([k, v]) => escHtml(k) + ': ' + escHtml(typeof v === 'object' ? JSON.stringify(v) : String(v)))
|
|
2105
|
+
.slice(0, 3).join(' · ');
|
|
2106
|
+
return `<div class="ecnp-event ${escHtml(kind)}">
|
|
2107
|
+
<span class="ecnp-badge ${escHtml(kind)}">${escHtml(kind)}</span>
|
|
2108
|
+
<div class="ecnp-body">
|
|
2109
|
+
<div class="ecnp-sender">${escHtml(ev.sender_id || '—')}</div>
|
|
2110
|
+
<div class="ecnp-contract">${escHtml(ev.contract_id || '—')}</div>
|
|
2111
|
+
${detailStr ? '<div class="ecnp-details">' + detailStr + '</div>' : ''}
|
|
2112
|
+
</div>
|
|
2113
|
+
<span class="ecnp-ts">${relTime(ev.timestamp)}</span>
|
|
2114
|
+
</div>`;
|
|
2115
|
+
}).join('');
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function clearEcnpTimeline() {
|
|
2119
|
+
_ecnpEvents = [];
|
|
2120
|
+
_renderEcnpTimeline();
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// ─── Belief Update Feed ─────────────────────────────────────────
|
|
2124
|
+
function appendBeliefUpdate(upd) {
|
|
2125
|
+
_beliefUpdates.unshift(upd);
|
|
2126
|
+
if (_beliefUpdates.length > MAX_BELIEF_UPDATES) _beliefUpdates.pop();
|
|
2127
|
+
_renderBeliefFeed();
|
|
2128
|
+
if (_agentStates[upd.agent_id]) {
|
|
2129
|
+
updateBeliefTrace(upd.agent_id, { [upd.endpoint]: upd.new_value }, upd.timestamp);
|
|
2130
|
+
scheduleChartUpdate();
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function _renderBeliefFeed() {
|
|
2135
|
+
const el = document.getElementById('belief-feed');
|
|
2136
|
+
if (!_beliefUpdates.length) {
|
|
2137
|
+
el.innerHTML = '<div class="empty-state"><span class="emoji">📡</span>Awaiting belief updates…</div>';
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
el.innerHTML = _beliefUpdates.map(upd => {
|
|
2141
|
+
const improving = upd.new_value < upd.old_value;
|
|
2142
|
+
const cls = improving ? 'improving' : 'worsening';
|
|
2143
|
+
const newCls = improving ? 'belief-new-good' : 'belief-new-bad';
|
|
2144
|
+
const ts = new Date(upd.timestamp).toLocaleTimeString();
|
|
2145
|
+
return `<div class="belief-feed-entry ${cls}"><span class="belief-ts">[${escHtml(ts)}]</span> <span class="belief-agent">${escHtml(upd.agent_id)}</span>: <span class="belief-ep">${escHtml(upd.endpoint)}</span> <span class="belief-old">${fmt2(upd.old_value)}</span> → <span class="${newCls}">${fmt2(upd.new_value)}</span> <span class="belief-ev">(ev: ${fmt2(upd.evidence)})</span></div>`;
|
|
2146
|
+
}).join('');
|
|
2147
|
+
el.scrollTop = 0;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
function clearBeliefFeed() {
|
|
2151
|
+
_beliefUpdates = [];
|
|
2152
|
+
_renderBeliefFeed();
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// ─── Scan History ───────────────────────────────────────────────
|
|
2156
|
+
async function loadScans() {
|
|
2157
|
+
try {
|
|
2158
|
+
_scans = await apiFetch('/api/v1/scans?limit=30');
|
|
2159
|
+
renderScans();
|
|
2160
|
+
} catch (e) {
|
|
2161
|
+
console.warn('Scans fetch failed', e);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
function renderScans() {
|
|
2166
|
+
const tbody = document.getElementById('scan-tbody');
|
|
2167
|
+
if (!_scans.length) {
|
|
2168
|
+
tbody.innerHTML = '<tr><td colspan="10"><div class="empty-state"><span class="emoji">🔍</span>No scans yet</div></td></tr>';
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
tbody.innerHTML = _scans.map(s => {
|
|
2172
|
+
const h = s.healing_summary;
|
|
2173
|
+
const healingCell = h
|
|
2174
|
+
? `<span title="Regenerated: ${h.regenerated||0} | Removed: ${h.removed||0} | Kept: ${h.kept||0}" style="font-size:11px;color:var(--text-secondary);">🩹 ${(h.regenerated||0)+(h.removed||0)} changes</span>`
|
|
2175
|
+
: '<span style="color:var(--text-muted);font-size:11px;">—</span>';
|
|
2176
|
+
const llmBadge = s.llm_cases_generated > 0
|
|
2177
|
+
? `<span title="${s.llm_cases_generated} LLM-generated test cases" style="margin-left:4px;font-size:10px;background:rgba(188,140,255,.2);color:#bc8cff;border-radius:4px;padding:1px 5px;">🤖 LLM</span>`
|
|
2178
|
+
: '';
|
|
2179
|
+
return `
|
|
2180
|
+
<tr>
|
|
2181
|
+
<td style="font-family:monospace;font-size:12px;" title="${escHtml(s.scan_id)}">${shortId(s.scan_id)}</td>
|
|
2182
|
+
<td><span class="status-badge status-${escHtml(s.status)}">${escHtml(s.status)}</span></td>
|
|
2183
|
+
<td>${escHtml(s.spec_name || '—')}${llmBadge}</td>
|
|
2184
|
+
<td style="max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(s.base_url||'')}">${escHtml(s.base_url || '—')}</td>
|
|
2185
|
+
<td>${s.total || 0}</td>
|
|
2186
|
+
<td>${s.total > 0 ? pct(s.pass_rate) : '—'}</td>
|
|
2187
|
+
<td>${healingCell}</td>
|
|
2188
|
+
<td>${relTime(s.created_at)}</td>
|
|
2189
|
+
<td>${relTime(s.completed_at)}</td>
|
|
2190
|
+
<td>
|
|
2191
|
+
<button class="export-btn" onclick="exportScan('${escHtml(s.scan_id)}','json')" title="Export JSON">⬇ JSON</button>
|
|
2192
|
+
<button class="export-btn" onclick="exportScan('${escHtml(s.scan_id)}','csv')" title="Export CSV">⬇ CSV</button>
|
|
2193
|
+
<button class="export-btn" onclick="exportScan('${escHtml(s.scan_id)}','html')" title="Export HTML">⬇ HTML</button>
|
|
2194
|
+
</td>
|
|
2195
|
+
</tr>`;
|
|
2196
|
+
}).join('');
|
|
2197
|
+
|
|
2198
|
+
// Update LLM Impact card
|
|
2199
|
+
const totalLlm = _scans.reduce((acc, s) => acc + (s.llm_cases_generated || 0), 0);
|
|
2200
|
+
const llmCard = document.getElementById('s-llm-card');
|
|
2201
|
+
if (totalLlm > 0 && llmCard) {
|
|
2202
|
+
llmCard.style.display = '';
|
|
2203
|
+
document.getElementById('s-llm-count').textContent = totalLlm;
|
|
2204
|
+
document.getElementById('s-llm-rate').textContent = 'across all scans';
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// ─── WebSocket ──────────────────────────────────────────────────
|
|
2209
|
+
function connectWs() {
|
|
2210
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
2211
|
+
const url = `${proto}//${location.host}/ws/live`;
|
|
2212
|
+
_ws = new WebSocket(url);
|
|
2213
|
+
|
|
2214
|
+
_ws.onopen = () => {
|
|
2215
|
+
document.getElementById('ws-dot').classList.add('connected');
|
|
2216
|
+
document.getElementById('ws-label').textContent = 'Live';
|
|
2217
|
+
const banner = document.getElementById('error-banner');
|
|
2218
|
+
if (banner) banner.style.display = 'none';
|
|
2219
|
+
};
|
|
2220
|
+
_ws.onclose = () => {
|
|
2221
|
+
document.getElementById('ws-dot').classList.remove('connected');
|
|
2222
|
+
document.getElementById('ws-label').textContent = 'Reconnecting…';
|
|
2223
|
+
const banner = document.getElementById('error-banner');
|
|
2224
|
+
const bannerMsg = document.getElementById('error-banner-msg');
|
|
2225
|
+
if (banner && bannerMsg) {
|
|
2226
|
+
bannerMsg.textContent = 'WebSocket disconnected — attempting to reconnect in 5s…';
|
|
2227
|
+
banner.style.display = 'block';
|
|
2228
|
+
}
|
|
2229
|
+
setTimeout(connectWs, 5000);
|
|
2230
|
+
};
|
|
2231
|
+
_ws.onerror = () => {
|
|
2232
|
+
document.getElementById('ws-dot').classList.remove('connected');
|
|
2233
|
+
document.getElementById('ws-label').textContent = 'Disconnected';
|
|
2234
|
+
};
|
|
2235
|
+
_ws.onmessage = (ev) => {
|
|
2236
|
+
try {
|
|
2237
|
+
const msg = JSON.parse(ev.data);
|
|
2238
|
+
if (msg.type === 'anomaly') {
|
|
2239
|
+
toast('⚠️ New anomaly: ' + (msg.data?.endpoint || ''));
|
|
2240
|
+
loadAnomalies();
|
|
2241
|
+
loadSummary();
|
|
2242
|
+
} else if (msg.type === 'scan_update') {
|
|
2243
|
+
loadScans();
|
|
2244
|
+
loadSummary();
|
|
2245
|
+
} else if (msg.type === 'summary') {
|
|
2246
|
+
_summary = msg.data;
|
|
2247
|
+
renderSummary();
|
|
2248
|
+
} else if (msg.type === 'agent_state') {
|
|
2249
|
+
_agentStates[msg.agent_id] = msg;
|
|
2250
|
+
updateBeliefTrace(msg.agent_id, msg.beliefs || {}, msg.timestamp);
|
|
2251
|
+
if (_activeTab === 'agents') {
|
|
2252
|
+
renderAgentCards();
|
|
2253
|
+
if (_selectedAgentId === msg.agent_id) {
|
|
2254
|
+
renderDrilldownBeliefs(msg);
|
|
2255
|
+
}
|
|
2256
|
+
scheduleChartUpdate();
|
|
2257
|
+
}
|
|
2258
|
+
} else if (msg.type === 'ecnp_event') {
|
|
2259
|
+
appendEcnpEvent(msg);
|
|
2260
|
+
} else if (msg.type === 'belief_update') {
|
|
2261
|
+
appendBeliefUpdate(msg);
|
|
2262
|
+
}
|
|
2263
|
+
} catch (_) {}
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// ─── Auto-refresh ───────────────────────────────────────────────
|
|
2268
|
+
function startAutoRefresh() {
|
|
2269
|
+
if (_refreshTimer) clearInterval(_refreshTimer);
|
|
2270
|
+
_refreshTimer = setInterval(async () => {
|
|
2271
|
+
await Promise.all([
|
|
2272
|
+
loadSummary(),
|
|
2273
|
+
loadEndpoints(),
|
|
2274
|
+
loadAnomalies(),
|
|
2275
|
+
loadScans(),
|
|
2276
|
+
loadQueue(),
|
|
2277
|
+
]);
|
|
2278
|
+
renderAgents();
|
|
2279
|
+
if (_activeTab === 'history') loadHistory();
|
|
2280
|
+
}, 8000);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// ─── Init ───────────────────────────────────────────────────────
|
|
2284
|
+
async function init() {
|
|
2285
|
+
await Promise.all([
|
|
2286
|
+
loadSummary(),
|
|
2287
|
+
loadEndpoints(),
|
|
2288
|
+
loadAnomalies(),
|
|
2289
|
+
loadScans(),
|
|
2290
|
+
loadAgents(),
|
|
2291
|
+
loadQueue(),
|
|
2292
|
+
]);
|
|
2293
|
+
renderAgents();
|
|
2294
|
+
connectWs();
|
|
2295
|
+
startAutoRefresh();
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// ─── Tab Switching ───────────────────────────────────────────────
|
|
2299
|
+
function switchTab(tabName) {
|
|
2300
|
+
_activeTab = tabName;
|
|
2301
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
2302
|
+
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
2303
|
+
});
|
|
2304
|
+
document.querySelectorAll('.tab-panel').forEach(panel => {
|
|
2305
|
+
panel.classList.toggle('active', panel.id === 'tab-' + tabName);
|
|
2306
|
+
});
|
|
2307
|
+
// Close mobile nav on tab switch
|
|
2308
|
+
const nav = document.getElementById('tab-nav');
|
|
2309
|
+
if (nav) nav.classList.remove('open');
|
|
2310
|
+
if (tabName === 'security') loadSecurityData();
|
|
2311
|
+
if (tabName === 'agents') loadAgents();
|
|
2312
|
+
if (tabName === 'history') loadHistory();
|
|
2313
|
+
if (tabName === 'settings') loadSettings();
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// ─── Security ────────────────────────────────────────────────────
|
|
2317
|
+
const SEV_COLORS = {
|
|
2318
|
+
critical: '#f85149',
|
|
2319
|
+
high: '#f0883e',
|
|
2320
|
+
medium: '#d29922',
|
|
2321
|
+
low: '#58a6ff',
|
|
2322
|
+
info: '#8b949e',
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
async function loadSecurityData() {
|
|
2326
|
+
try {
|
|
2327
|
+
const data = await apiFetch('/api/v1/security-findings');
|
|
2328
|
+
_secFindings = Array.isArray(data) ? data : (data.findings || []);
|
|
2329
|
+
renderSecuritySummary();
|
|
2330
|
+
renderSecurityFindings();
|
|
2331
|
+
} catch (e) {
|
|
2332
|
+
console.warn('Security findings fetch failed', e);
|
|
2333
|
+
}
|
|
2334
|
+
// Load prioritization data from the latest completed scan if available
|
|
2335
|
+
try {
|
|
2336
|
+
const report = await apiFetch('/api/v1/security-report');
|
|
2337
|
+
if (report && report.prioritization) {
|
|
2338
|
+
_secPrioritization = report.prioritization;
|
|
2339
|
+
renderPrioritization();
|
|
2340
|
+
renderSecurityFindings(); // re-render with prio badges
|
|
2341
|
+
}
|
|
2342
|
+
} catch (e) {
|
|
2343
|
+
// prioritization data is optional — ignore errors
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
function renderSecuritySummary() {
|
|
2348
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
2349
|
+
_secFindings.forEach(f => {
|
|
2350
|
+
const sev = (f.severity || 'info').toLowerCase();
|
|
2351
|
+
if (sev in counts) counts[sev]++;
|
|
2352
|
+
});
|
|
2353
|
+
document.getElementById('sec-critical-count').textContent = counts.critical;
|
|
2354
|
+
document.getElementById('sec-high-count').textContent = counts.high;
|
|
2355
|
+
document.getElementById('sec-medium-count').textContent = counts.medium;
|
|
2356
|
+
document.getElementById('sec-low-count').textContent = counts.low;
|
|
2357
|
+
document.getElementById('sec-info-count').textContent = counts.info;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
function _prioBadge(endpoint) {
|
|
2361
|
+
const entry = _secPrioritization.find(p => p.endpoint === endpoint || p.endpoint && p.endpoint.endsWith(endpoint));
|
|
2362
|
+
if (!entry) return '—';
|
|
2363
|
+
const score = entry.priority_score || 0;
|
|
2364
|
+
let cls = 'prio-badge-low';
|
|
2365
|
+
if (score > 0.7) cls = 'prio-badge-high';
|
|
2366
|
+
else if (score > 0.4) cls = 'prio-badge-medium';
|
|
2367
|
+
return `<span class="prio-badge ${cls}">${score.toFixed(2)}</span>`;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function _prioRowClass(endpoint) {
|
|
2371
|
+
const entry = _secPrioritization.find(p => p.endpoint === endpoint || p.endpoint && p.endpoint.endsWith(endpoint));
|
|
2372
|
+
if (!entry) return '';
|
|
2373
|
+
const score = entry.priority_score || 0;
|
|
2374
|
+
if (score > 0.7) return 'prio-high';
|
|
2375
|
+
if (score > 0.4) return 'prio-medium';
|
|
2376
|
+
return 'prio-low';
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
function renderSecurityFindings() {
|
|
2380
|
+
const tbody = document.getElementById('sec-findings-tbody');
|
|
2381
|
+
if (!_secFindings.length) {
|
|
2382
|
+
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><span class="emoji">🔒</span>No security findings</div></td></tr>';
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
tbody.innerHTML = _secFindings.map((f, i) => {
|
|
2386
|
+
const sev = (f.severity || 'info').toLowerCase();
|
|
2387
|
+
const rowCls = _prioRowClass(f.endpoint || '');
|
|
2388
|
+
return `<tr class="clickable ${rowCls}" onclick="showSecurityFinding(${i})">
|
|
2389
|
+
<td><span class="sev-pill sev-${escHtml(sev)}">${escHtml(sev)}</span></td>
|
|
2390
|
+
<td style="font-family:monospace;font-size:12px;">${escHtml(f.check_id || '—')}</td>
|
|
2391
|
+
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(f.endpoint||'')}">${escHtml(f.endpoint || '—')}</td>
|
|
2392
|
+
<td>${escHtml(f.title || '—')}</td>
|
|
2393
|
+
<td>${_prioBadge(f.endpoint || '')}</td>
|
|
2394
|
+
<td>${escHtml(f.confidence != null ? String(f.confidence) : '—')}</td>
|
|
2395
|
+
</tr>`;
|
|
2396
|
+
}).join('');
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function renderPrioritization() {
|
|
2400
|
+
const card = document.getElementById('sec-prioritization-card');
|
|
2401
|
+
const tbody = document.getElementById('sec-prio-tbody');
|
|
2402
|
+
const summary = document.getElementById('sec-prioritization-summary');
|
|
2403
|
+
if (!_secPrioritization.length) {
|
|
2404
|
+
card.style.display = 'none';
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
card.style.display = '';
|
|
2408
|
+
const high = _secPrioritization.filter(p => (p.priority_score || 0) > 0.7).length;
|
|
2409
|
+
const total = _secPrioritization.length;
|
|
2410
|
+
summary.textContent = `${total} endpoints prioritized — ${high} flagged high-risk (score > 0.7) and received full check coverage.`;
|
|
2411
|
+
tbody.innerHTML = _secPrioritization.map(p => {
|
|
2412
|
+
const score = p.priority_score || 0;
|
|
2413
|
+
let cls = 'prio-badge-low';
|
|
2414
|
+
if (score > 0.7) cls = 'prio-badge-high';
|
|
2415
|
+
else if (score > 0.4) cls = 'prio-badge-medium';
|
|
2416
|
+
const checksRunCount = (p.checks_run || []).length;
|
|
2417
|
+
return `<tr>
|
|
2418
|
+
<td style="font-family:monospace;font-size:12px;">${escHtml(p.endpoint || '—')}</td>
|
|
2419
|
+
<td><span class="prio-badge ${cls}">${score.toFixed(2)}</span></td>
|
|
2420
|
+
<td>${(p.belief_score || 0).toFixed(2)}</td>
|
|
2421
|
+
<td>${(p.risk_score || 0).toFixed(2)}</td>
|
|
2422
|
+
<td style="font-size:12px;">${checksRunCount} checks</td>
|
|
2423
|
+
</tr>`;
|
|
2424
|
+
}).join('');
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function showSecurityFinding(idx) {
|
|
2428
|
+
const f = _secFindings[idx];
|
|
2429
|
+
if (!f) return;
|
|
2430
|
+
const sev = (f.severity || 'info').toLowerCase();
|
|
2431
|
+
document.getElementById('sec-finding-detail').style.display = 'block';
|
|
2432
|
+
document.getElementById('sec-detail-title').textContent = f.title || 'Finding Detail';
|
|
2433
|
+
document.getElementById('fd-check-id').textContent = f.check_id || '—';
|
|
2434
|
+
document.getElementById('fd-endpoint').textContent = f.endpoint || '—';
|
|
2435
|
+
document.getElementById('fd-severity').innerHTML = `<span class="sev-pill sev-${escHtml(sev)}">${escHtml(sev)}</span>`;
|
|
2436
|
+
document.getElementById('fd-title').textContent = f.title || '—';
|
|
2437
|
+
document.getElementById('fd-cwe-id').textContent = f.cwe_id || '—';
|
|
2438
|
+
document.getElementById('fd-description').textContent = f.description || '—';
|
|
2439
|
+
document.getElementById('fd-remediation').textContent = f.remediation || '—';
|
|
2440
|
+
document.getElementById('fd-evidence').textContent = typeof f.evidence === 'object'
|
|
2441
|
+
? JSON.stringify(f.evidence, null, 2) : (f.evidence || '—');
|
|
2442
|
+
document.getElementById('sec-finding-detail').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
function closeSecurityDetail() {
|
|
2446
|
+
document.getElementById('sec-finding-detail').style.display = 'none';
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
async function runSecurityScan() {
|
|
2450
|
+
const spec = document.getElementById('sec-spec-path').value.trim();
|
|
2451
|
+
const baseUrl = document.getElementById('sec-base-url').value.trim();
|
|
2452
|
+
const severity = document.getElementById('sec-severity').value;
|
|
2453
|
+
const statusEl = document.getElementById('sec-scan-status');
|
|
2454
|
+
const btn = document.getElementById('run-sec-scan-btn');
|
|
2455
|
+
|
|
2456
|
+
if (!spec || !baseUrl) {
|
|
2457
|
+
statusEl.textContent = '⚠️ Spec path and Base URL are required.';
|
|
2458
|
+
statusEl.style.color = '#f0883e';
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
btn.disabled = true;
|
|
2463
|
+
statusEl.textContent = '⏳ Starting scan…';
|
|
2464
|
+
statusEl.style.color = '#8b949e';
|
|
2465
|
+
|
|
2466
|
+
try {
|
|
2467
|
+
const res = await fetch('/api/v1/security-scan', {
|
|
2468
|
+
method: 'POST',
|
|
2469
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2470
|
+
body: JSON.stringify({ spec, base_url: baseUrl, severity_threshold: severity }),
|
|
2471
|
+
});
|
|
2472
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2473
|
+
const data = await res.json();
|
|
2474
|
+
const scanId = data.scan_id;
|
|
2475
|
+
statusEl.textContent = '⏳ Scan running…';
|
|
2476
|
+
_pollSecurityScan(scanId, statusEl, btn);
|
|
2477
|
+
} catch (e) {
|
|
2478
|
+
statusEl.textContent = '❌ Failed to start scan: ' + e.message;
|
|
2479
|
+
statusEl.style.color = '#f85149';
|
|
2480
|
+
btn.disabled = false;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function _pollSecurityScan(scanId, statusEl, btn) {
|
|
2485
|
+
if (_secScanPollTimer) clearInterval(_secScanPollTimer);
|
|
2486
|
+
_secScanPollTimer = setInterval(async () => {
|
|
2487
|
+
try {
|
|
2488
|
+
const data = await apiFetch(`/api/v1/security-scan/${encodeURIComponent(scanId)}`);
|
|
2489
|
+
const status = (data.status || '').toLowerCase();
|
|
2490
|
+
if (status === 'completed' || status === 'done') {
|
|
2491
|
+
clearInterval(_secScanPollTimer);
|
|
2492
|
+
statusEl.textContent = '✅ Scan complete';
|
|
2493
|
+
statusEl.style.color = '#3fb950';
|
|
2494
|
+
btn.disabled = false;
|
|
2495
|
+
await loadSecurityData();
|
|
2496
|
+
} else if (status === 'failed' || status === 'error') {
|
|
2497
|
+
clearInterval(_secScanPollTimer);
|
|
2498
|
+
statusEl.textContent = '❌ Scan failed';
|
|
2499
|
+
statusEl.style.color = '#f85149';
|
|
2500
|
+
btn.disabled = false;
|
|
2501
|
+
} else {
|
|
2502
|
+
statusEl.textContent = `⏳ ${status || 'running'}…`;
|
|
2503
|
+
}
|
|
2504
|
+
} catch (e) {
|
|
2505
|
+
clearInterval(_secScanPollTimer);
|
|
2506
|
+
statusEl.textContent = '❌ Poll error: ' + e.message;
|
|
2507
|
+
statusEl.style.color = '#f85149';
|
|
2508
|
+
btn.disabled = false;
|
|
2509
|
+
}
|
|
2510
|
+
}, 3000);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// ─── Mobile Menu ─────────────────────────────────────────────────
|
|
2514
|
+
function toggleMobileMenu() {
|
|
2515
|
+
document.getElementById('tab-nav').classList.toggle('open');
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// ─── Theme Toggle ────────────────────────────────────────────────
|
|
2519
|
+
function toggleTheme() {
|
|
2520
|
+
const body = document.body;
|
|
2521
|
+
const btn = document.getElementById('theme-btn');
|
|
2522
|
+
body.classList.toggle('light-mode');
|
|
2523
|
+
if (btn) btn.textContent = body.classList.contains('light-mode') ? '🌙' : '☀️';
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
2527
|
+
|
|
2528
|
+
// ─── Service Worker Registration (PWA) ───────────────────────────────────────
|
|
2529
|
+
if ('serviceWorker' in navigator) {
|
|
2530
|
+
window.addEventListener('load', () => {
|
|
2531
|
+
navigator.serviceWorker.register('/sw.js').catch((err) => {
|
|
2532
|
+
console.warn('Service worker registration failed:', err);
|
|
2533
|
+
});
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
</script>
|
|
2537
|
+
</body>
|
|
2538
|
+
</html>
|