@highflame/overwatch 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +337 -0
- package/bin/overwatch +12 -0
- package/dist/auth/cli-oauth.d.ts +13 -0
- package/dist/auth/cli-oauth.d.ts.map +1 -0
- package/dist/auth/html-utils.d.ts +20 -0
- package/dist/auth/html-utils.d.ts.map +1 -0
- package/dist/auth/index.d.ts +10 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/oauth.d.ts +81 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/pkce.d.ts +26 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/token-store.d.ts +44 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/bin/overwatch +12 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +5449 -0
- package/dist/cli.js.map +7 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/manager.d.ts +54 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +6004 -0
- package/dist/daemon.js.map +7 -0
- package/dist/data/ingestor.d.ts +31 -0
- package/dist/data/ingestor.d.ts.map +1 -0
- package/dist/data/processor.d.ts +96 -0
- package/dist/data/processor.d.ts.map +1 -0
- package/dist/data/reader.d.ts +24 -0
- package/dist/data/reader.d.ts.map +1 -0
- package/dist/data/recorder.d.ts +12 -0
- package/dist/data/recorder.d.ts.map +1 -0
- package/dist/engines/cedar.d.ts +41 -0
- package/dist/engines/cedar.d.ts.map +1 -0
- package/dist/engines/remote.d.ts +21 -0
- package/dist/engines/remote.d.ts.map +1 -0
- package/dist/engines/yara.d.ts +12 -0
- package/dist/engines/yara.d.ts.map +1 -0
- package/dist/handlers/dashboard-handler.d.ts +7 -0
- package/dist/handlers/dashboard-handler.d.ts.map +1 -0
- package/dist/handlers/hook-handler.d.ts +23 -0
- package/dist/handlers/hook-handler.d.ts.map +1 -0
- package/dist/handlers/oauth-handler.d.ts +12 -0
- package/dist/handlers/oauth-handler.d.ts.map +1 -0
- package/dist/handlers/scan-handler.d.ts +13 -0
- package/dist/handlers/scan-handler.d.ts.map +1 -0
- package/dist/handlers/utils.d.ts +11 -0
- package/dist/handlers/utils.d.ts.map +1 -0
- package/dist/hooks/claudecode/hooks.json.template +20 -0
- package/dist/hooks/cursor/hooks.json.template +74 -0
- package/dist/hooks/universal-hook.sh +36 -0
- package/dist/http/server.d.ts +38 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5941 -0
- package/dist/index.js.map +7 -0
- package/dist/installer.d.ts +25 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/javelin/admin-client.d.ts +75 -0
- package/dist/javelin/admin-client.d.ts.map +1 -0
- package/dist/javelin/client.d.ts +30 -0
- package/dist/javelin/client.d.ts.map +1 -0
- package/dist/javelin/config-reader.d.ts +70 -0
- package/dist/javelin/config-reader.d.ts.map +1 -0
- package/dist/javelin/index.d.ts +5 -0
- package/dist/javelin/index.d.ts.map +1 -0
- package/dist/javelin/types.d.ts +81 -0
- package/dist/javelin/types.d.ts.map +1 -0
- package/dist/lib/policy-engine.d.ts +34 -0
- package/dist/lib/policy-engine.d.ts.map +1 -0
- package/dist/lib/policy-manager.d.ts +86 -0
- package/dist/lib/policy-manager.d.ts.map +1 -0
- package/dist/module.d.ts +52 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/pipeline/context-mapper.d.ts +16 -0
- package/dist/pipeline/context-mapper.d.ts.map +1 -0
- package/dist/pipeline/extractors/claude-extractor.d.ts +48 -0
- package/dist/pipeline/extractors/claude-extractor.d.ts.map +1 -0
- package/dist/pipeline/extractors/cursor-extractor.d.ts +44 -0
- package/dist/pipeline/extractors/cursor-extractor.d.ts.map +1 -0
- package/dist/pipeline/extractors/github-copilot-extractor.d.ts +49 -0
- package/dist/pipeline/extractors/github-copilot-extractor.d.ts.map +1 -0
- package/dist/pipeline/extractors/index.d.ts +47 -0
- package/dist/pipeline/extractors/index.d.ts.map +1 -0
- package/dist/pipeline/extractors/registry.d.ts +38 -0
- package/dist/pipeline/extractors/registry.d.ts.map +1 -0
- package/dist/pipeline/hook-pipeline.d.ts +25 -0
- package/dist/pipeline/hook-pipeline.d.ts.map +1 -0
- package/dist/policy.cedar +783 -0
- package/dist/rules/pre/command_injection.yar +60 -0
- package/dist/rules/pre/cross_origin_escalation.yar +106 -0
- package/dist/rules/pre/mcp_config_risk.yar +35 -0
- package/dist/rules/pre/path_traversal.yar +50 -0
- package/dist/rules/pre/prompt_injection.yar +101 -0
- package/dist/rules/pre/secrets_leakage.yar +100 -0
- package/dist/rules/pre/sql_injection.yar +65 -0
- package/dist/scanner.d.ts +80 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/service.d.ts +18 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/services/interface.d.ts +11 -0
- package/dist/services/interface.d.ts.map +1 -0
- package/dist/services/launchd.d.ts +12 -0
- package/dist/services/launchd.d.ts.map +1 -0
- package/dist/services/systemd.d.ts +12 -0
- package/dist/services/systemd.d.ts.map +1 -0
- package/dist/services/windows.d.ts +7 -0
- package/dist/services/windows.d.ts.map +1 -0
- package/dist/skills/index.d.ts +7 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/scanner.d.ts +44 -0
- package/dist/skills/scanner.d.ts.map +1 -0
- package/dist/skills/types.d.ts +29 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/types/config.d.ts +165 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/events.d.ts +225 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/remote-policy.d.ts +129 -0
- package/dist/types/remote-policy.d.ts.map +1 -0
- package/dist/types/requests.d.ts +45 -0
- package/dist/types/requests.d.ts.map +1 -0
- package/dist/types/responses.d.ts +60 -0
- package/dist/types/responses.d.ts.map +1 -0
- package/dist/ui/images/highflame-mono.png +0 -0
- package/dist/ui/views/dashboard.ejs +301 -0
- package/dist/ui/views/dashboard.js +785 -0
- package/dist/ui/views/partials/commands-table.ejs +54 -0
- package/dist/ui/views/partials/events-table.ejs +36 -0
- package/dist/ui/views/partials/filter-dropdown.ejs +12 -0
- package/dist/ui/views/partials/overview-charts.ejs +149 -0
- package/dist/ui/views/partials/scans-table.ejs +136 -0
- package/dist/ui/views/partials/sessions-table.ejs +50 -0
- package/dist/ui/views/partials/stats-grid.ejs +23 -0
- package/dist/ui/views/partials/threats-table.ejs +60 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/performance.d.ts +26 -0
- package/dist/utils/performance.d.ts.map +1 -0
- package/dist/utils/port-manager.d.ts +6 -0
- package/dist/utils/port-manager.d.ts.map +1 -0
- package/dist/yara/engine.d.ts +58 -0
- package/dist/yara/engine.d.ts.map +1 -0
- package/dist/yara/index.d.ts +5 -0
- package/dist/yara/index.d.ts.map +1 -0
- package/lib/platform-loader.js +210 -0
- package/package.json +63 -0
- package/scripts/postinstall.js +121 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Client-Side Logic
|
|
3
|
+
*
|
|
4
|
+
* This module handles all client-side interactivity for the Guardian dashboard.
|
|
5
|
+
* It's designed to be easily testable - all functions are pure or have clear side effects.
|
|
6
|
+
*
|
|
7
|
+
* Debug Mode: Add ?debug=1 to URL to enable console logging
|
|
8
|
+
*
|
|
9
|
+
* @module DashboardClient
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
(function (window) {
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Configuration & State
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
const DEBUG = new URLSearchParams(window.location.search).has("debug");
|
|
20
|
+
|
|
21
|
+
/** @type {object} Centralized state - avoids globals scattered everywhere */
|
|
22
|
+
const state = {
|
|
23
|
+
sessions: [],
|
|
24
|
+
sessionsWithEvents: [],
|
|
25
|
+
rawData: { threats: [], commands: [], events: [] },
|
|
26
|
+
activeTab: "overview",
|
|
27
|
+
chart: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// Debug Utilities
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
function log(...args) {
|
|
35
|
+
if (DEBUG) console.log("[Dashboard]", ...args);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function logTable(label, data) {
|
|
39
|
+
if (DEBUG) {
|
|
40
|
+
console.group(`[Dashboard] ${label}`);
|
|
41
|
+
console.table(data);
|
|
42
|
+
console.groupEnd();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Expose state for debugging in console */
|
|
47
|
+
function exposeDebugAPI() {
|
|
48
|
+
window.__DASHBOARD_DEBUG__ = {
|
|
49
|
+
getState: () => state,
|
|
50
|
+
getSessions: () => state.sessions,
|
|
51
|
+
getEvents: () => state.rawData.events,
|
|
52
|
+
getThreats: () => state.rawData.threats,
|
|
53
|
+
getCommands: () => state.rawData.commands,
|
|
54
|
+
inspectSession: (id) => state.sessions.find((s) => s.id === id),
|
|
55
|
+
inspectEvent: (type, idx) => state.rawData[type]?.[idx],
|
|
56
|
+
logState: () => console.log(JSON.parse(JSON.stringify(state))),
|
|
57
|
+
};
|
|
58
|
+
log("Debug API exposed at window.__DASHBOARD_DEBUG__");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================
|
|
62
|
+
// Data Initialization
|
|
63
|
+
// ============================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initialize state with server-provided data
|
|
67
|
+
* Called once on page load with pre-escaped JSON from server
|
|
68
|
+
*/
|
|
69
|
+
function initializeData(sessions, rawData, sessionsWithEvents) {
|
|
70
|
+
state.sessions = sessions || [];
|
|
71
|
+
state.sessionsWithEvents = sessionsWithEvents || [];
|
|
72
|
+
state.rawData = rawData || { threats: [], commands: [], events: [] };
|
|
73
|
+
|
|
74
|
+
log("State initialized:", {
|
|
75
|
+
sessions: state.sessions.length,
|
|
76
|
+
sessionsWithEvents: state.sessionsWithEvents.length,
|
|
77
|
+
events: state.rawData.events.length,
|
|
78
|
+
threats: state.rawData.threats.length,
|
|
79
|
+
commands: state.rawData.commands.length,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
logTable("Sessions", state.sessions);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================
|
|
86
|
+
// DOM Utilities
|
|
87
|
+
// ============================================================
|
|
88
|
+
|
|
89
|
+
const $ = (sel) => document.querySelector(sel);
|
|
90
|
+
const $$ = (sel) => document.querySelectorAll(sel);
|
|
91
|
+
|
|
92
|
+
function createElement(tag, attrs = {}, children = []) {
|
|
93
|
+
const el = document.createElement(tag);
|
|
94
|
+
Object.entries(attrs).forEach(([key, val]) => {
|
|
95
|
+
if (key === "className") el.className = val;
|
|
96
|
+
else if (key === "textContent") el.textContent = val;
|
|
97
|
+
else if (key.startsWith("on"))
|
|
98
|
+
el.addEventListener(key.slice(2).toLowerCase(), val);
|
|
99
|
+
else el.setAttribute(key, val);
|
|
100
|
+
});
|
|
101
|
+
children.forEach((child) => {
|
|
102
|
+
if (typeof child === "string")
|
|
103
|
+
el.appendChild(document.createTextNode(child));
|
|
104
|
+
else if (child) el.appendChild(child);
|
|
105
|
+
});
|
|
106
|
+
return el;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================
|
|
110
|
+
// Escape Utilities (Security)
|
|
111
|
+
// ============================================================
|
|
112
|
+
|
|
113
|
+
const HTML_ENTITIES = {
|
|
114
|
+
"&": "&",
|
|
115
|
+
"<": "<",
|
|
116
|
+
">": ">",
|
|
117
|
+
'"': """,
|
|
118
|
+
"'": "'",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function escapeHtml(text) {
|
|
122
|
+
if (typeof text !== "string") return String(text);
|
|
123
|
+
return text.replace(/[&<>"']/g, (m) => HTML_ENTITIES[m]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function escapeAttr(str) {
|
|
127
|
+
return escapeHtml(str).replace(/\n/g, " ");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================
|
|
131
|
+
// Tab Navigation
|
|
132
|
+
// ============================================================
|
|
133
|
+
|
|
134
|
+
const TAB_TITLES = {
|
|
135
|
+
overview: "Overview",
|
|
136
|
+
sessions: "Sessions Explorer",
|
|
137
|
+
threats: "Security Threats",
|
|
138
|
+
commands: "Command Analysis",
|
|
139
|
+
events: "Event Log",
|
|
140
|
+
scans: "MCP Scans",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function showTab(tabId) {
|
|
144
|
+
log("Switching to tab:", tabId);
|
|
145
|
+
state.activeTab = tabId;
|
|
146
|
+
|
|
147
|
+
$$(".tab-content").forEach((tab) => tab.classList.remove("active"));
|
|
148
|
+
$("#" + tabId)?.classList.add("active");
|
|
149
|
+
|
|
150
|
+
$$(".tab-btn").forEach((btn) => btn.classList.remove("active"));
|
|
151
|
+
$(`.tab-btn[data-tab="${tabId}"]`)?.classList.add("active");
|
|
152
|
+
|
|
153
|
+
const titleEl = $("#view-title");
|
|
154
|
+
if (titleEl) titleEl.textContent = TAB_TITLES[tabId] || tabId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================================
|
|
158
|
+
// Table Filtering
|
|
159
|
+
// ============================================================
|
|
160
|
+
|
|
161
|
+
function filterTable(tableId, source) {
|
|
162
|
+
log("Filtering table:", tableId, "by source:", source || "all");
|
|
163
|
+
const table = $("#" + tableId);
|
|
164
|
+
if (!table) return;
|
|
165
|
+
|
|
166
|
+
const rows = table.querySelectorAll("tbody tr");
|
|
167
|
+
rows.forEach((row) => {
|
|
168
|
+
const rowSource = row.getAttribute("data-source");
|
|
169
|
+
row.style.display = !source || rowSource === source ? "" : "none";
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// Modal System
|
|
175
|
+
// ============================================================
|
|
176
|
+
|
|
177
|
+
function openModal() {
|
|
178
|
+
$("#modal-container")?.classList.remove("hidden");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function closeModal() {
|
|
182
|
+
$("#modal-container")?.classList.add("hidden");
|
|
183
|
+
log("Modal closed");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function setModalContent(contentElement) {
|
|
187
|
+
const body = $("#modal-body");
|
|
188
|
+
if (body) {
|
|
189
|
+
body.innerHTML = "";
|
|
190
|
+
body.appendChild(contentElement);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function openEventDetailModal() {
|
|
195
|
+
$("#modal-event-detail")?.classList.remove("hidden");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function closeEventDetailModal() {
|
|
199
|
+
$("#modal-event-detail")?.classList.add("hidden");
|
|
200
|
+
log("Event detail modal closed");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setEventDetailModalContent(contentElement) {
|
|
204
|
+
const body = $("#modal-body-event");
|
|
205
|
+
if (body) {
|
|
206
|
+
body.innerHTML = "";
|
|
207
|
+
body.appendChild(contentElement);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================
|
|
212
|
+
// Session Detail View
|
|
213
|
+
// ============================================================
|
|
214
|
+
|
|
215
|
+
function openSession(sessionId) {
|
|
216
|
+
log("Opening session:", sessionId);
|
|
217
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
218
|
+
if (!session) {
|
|
219
|
+
log("Session not found:", sessionId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let sessionEvents = [];
|
|
224
|
+
const full = state.sessionsWithEvents.find((s) => s.id === sessionId);
|
|
225
|
+
if (full && Array.isArray(full.events)) {
|
|
226
|
+
sessionEvents = full.events;
|
|
227
|
+
log("Session events (server):", sessionEvents.length);
|
|
228
|
+
} else {
|
|
229
|
+
const payload = (e) =>
|
|
230
|
+
(e.input && e.input.input != null ? e.input.input : e.input) || {};
|
|
231
|
+
const sessionKey = (p) =>
|
|
232
|
+
p.conversation_id ??
|
|
233
|
+
p.conversationId ??
|
|
234
|
+
p.session_id ??
|
|
235
|
+
p.sessionId ??
|
|
236
|
+
"unknown";
|
|
237
|
+
const [sid, src] = sessionId.includes("::")
|
|
238
|
+
? sessionId.split("::")
|
|
239
|
+
: [sessionId, null];
|
|
240
|
+
sessionEvents = state.rawData.events.filter((e) => {
|
|
241
|
+
const p = payload(e);
|
|
242
|
+
const k =
|
|
243
|
+
(typeof sessionKey(p) === "string" && sessionKey(p)) || "unknown";
|
|
244
|
+
const matchId = k === sid;
|
|
245
|
+
const matchSource = !src || e.source === src;
|
|
246
|
+
return matchId && matchSource;
|
|
247
|
+
});
|
|
248
|
+
// Sort in reverse chronological order
|
|
249
|
+
sessionEvents.sort(
|
|
250
|
+
(a, b) =>
|
|
251
|
+
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
|
252
|
+
);
|
|
253
|
+
log("Session events (client filter):", sessionEvents.length);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const container = buildSessionModal(session, sessionEvents);
|
|
257
|
+
setModalContent(container);
|
|
258
|
+
openModal();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildSessionModal(session, events) {
|
|
262
|
+
const container = createElement("div", {
|
|
263
|
+
className: "h-full flex flex-col",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Header
|
|
267
|
+
const header = createElement(
|
|
268
|
+
"div",
|
|
269
|
+
{
|
|
270
|
+
className:
|
|
271
|
+
"p-6 pr-16 border-b border-zinc-800 flex justify-between items-start bg-zinc-950",
|
|
272
|
+
},
|
|
273
|
+
[
|
|
274
|
+
createElement("div", {}, [
|
|
275
|
+
createElement("div", { className: "flex items-center gap-3 mb-2" }, [
|
|
276
|
+
createElement("span", {
|
|
277
|
+
className:
|
|
278
|
+
"text-[10px] font-bold px-2 py-1 rounded bg-zinc-800 uppercase tracking-widest",
|
|
279
|
+
textContent: session.source,
|
|
280
|
+
}),
|
|
281
|
+
createElement("span", {
|
|
282
|
+
className: "text-xs text-zinc-500 font-mono",
|
|
283
|
+
textContent: session.id,
|
|
284
|
+
}),
|
|
285
|
+
]),
|
|
286
|
+
createElement("h2", {
|
|
287
|
+
className: "text-xl font-bold text-white",
|
|
288
|
+
textContent: "Session Transcript",
|
|
289
|
+
}),
|
|
290
|
+
]),
|
|
291
|
+
createElement("div", { className: "text-right" }, [
|
|
292
|
+
createElement("div", {
|
|
293
|
+
className: "text-xs text-zinc-400 mb-1",
|
|
294
|
+
textContent: "Started",
|
|
295
|
+
}),
|
|
296
|
+
createElement("div", {
|
|
297
|
+
className: "font-mono text-sm",
|
|
298
|
+
textContent: new Date(session.startTime).toLocaleString(),
|
|
299
|
+
}),
|
|
300
|
+
]),
|
|
301
|
+
],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Timeline
|
|
305
|
+
const timeline = createElement("div", {
|
|
306
|
+
className:
|
|
307
|
+
"flex-1 overflow-y-auto custom-scrollbar p-6 space-y-6 bg-black",
|
|
308
|
+
});
|
|
309
|
+
events.forEach((event) => {
|
|
310
|
+
timeline.appendChild(buildEventBubble(event));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
container.appendChild(header);
|
|
314
|
+
container.appendChild(timeline);
|
|
315
|
+
return container;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildEventBubble(event) {
|
|
319
|
+
const isUser =
|
|
320
|
+
event.event === "beforeSubmitPrompt" ||
|
|
321
|
+
event.event === "UserPromptSubmit" ||
|
|
322
|
+
event.event === "userPromptSubmitted";
|
|
323
|
+
|
|
324
|
+
const payload =
|
|
325
|
+
(event.input && event.input.input != null
|
|
326
|
+
? event.input.input
|
|
327
|
+
: event.input) || {};
|
|
328
|
+
const wrapper = createElement("div", {
|
|
329
|
+
className: `flex flex-col ${isUser ? "items-end" : "items-start"} w-full group`,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const label = isUser
|
|
333
|
+
? "User"
|
|
334
|
+
: (payload.tool_name ?? payload.toolName ?? payload.name ?? event.event);
|
|
335
|
+
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
336
|
+
wrapper.appendChild(
|
|
337
|
+
createElement(
|
|
338
|
+
"div",
|
|
339
|
+
{ className: "text-[10px] text-zinc-500 mb-1 px-1 flex gap-2" },
|
|
340
|
+
[
|
|
341
|
+
createElement("span", {
|
|
342
|
+
className: "font-bold uppercase tracking-wider",
|
|
343
|
+
textContent: String(label),
|
|
344
|
+
}),
|
|
345
|
+
createElement("span", { className: "font-mono", textContent: time }),
|
|
346
|
+
],
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
let content =
|
|
351
|
+
payload.prompt ??
|
|
352
|
+
payload.command ??
|
|
353
|
+
payload.tool_name ??
|
|
354
|
+
payload.toolName ??
|
|
355
|
+
payload.toolArgs ??
|
|
356
|
+
`Event: ${event.event}`;
|
|
357
|
+
if (typeof content !== "string") content = JSON.stringify(content, null, 2);
|
|
358
|
+
const bubbleClass = isUser
|
|
359
|
+
? "bg-zinc-800 text-zinc-100 user rounded-tr-none"
|
|
360
|
+
: "bg-zinc-900 border border-zinc-800 text-zinc-300 agent rounded-tl-none";
|
|
361
|
+
|
|
362
|
+
const bubble = createElement("div", {
|
|
363
|
+
className: `chat-bubble ${bubbleClass} p-4 rounded-xl shadow-sm text-sm leading-relaxed max-w-[80%] cursor-pointer hover:ring-1 hover:ring-zinc-600 transition-all`,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (
|
|
367
|
+
!isUser &&
|
|
368
|
+
(payload.tool_input ?? payload.toolArgs ?? payload.command)
|
|
369
|
+
) {
|
|
370
|
+
const code = createElement("code", {
|
|
371
|
+
className: "font-mono text-xs block whitespace-pre-wrap",
|
|
372
|
+
textContent: content,
|
|
373
|
+
});
|
|
374
|
+
bubble.appendChild(code);
|
|
375
|
+
} else {
|
|
376
|
+
bubble.textContent = content;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Threat badge
|
|
380
|
+
if (event.threat_summary?.total_count > 0) {
|
|
381
|
+
const badge = createElement("div", {
|
|
382
|
+
className:
|
|
383
|
+
"mt-2 text-[10px] font-bold text-orange-500 bg-orange-500/10 px-2 py-1 rounded inline-block border border-orange-500/20",
|
|
384
|
+
textContent: `⚠️ ${event.threat_summary.total_count} Threat(s) Detected`,
|
|
385
|
+
});
|
|
386
|
+
bubble.appendChild(badge);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Click to inspect
|
|
390
|
+
bubble.addEventListener("click", () => showJsonInspector(event));
|
|
391
|
+
wrapper.appendChild(bubble);
|
|
392
|
+
|
|
393
|
+
return wrapper;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ============================================================
|
|
397
|
+
// JSON Inspector
|
|
398
|
+
// ============================================================
|
|
399
|
+
|
|
400
|
+
function openEventDetail(type, index) {
|
|
401
|
+
const eventData = state.rawData[type]?.[index];
|
|
402
|
+
if (!eventData) {
|
|
403
|
+
log("Event not found:", type, index);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
showJsonInspector(eventData);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function showJsonInspector(data) {
|
|
410
|
+
log("Inspecting JSON:", data);
|
|
411
|
+
|
|
412
|
+
const container = createElement("div", {
|
|
413
|
+
className: "h-full flex flex-col bg-zinc-950",
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Header
|
|
417
|
+
const header = createElement(
|
|
418
|
+
"div",
|
|
419
|
+
{
|
|
420
|
+
className:
|
|
421
|
+
"p-4 pr-16 border-b border-zinc-800 bg-zinc-900/50 flex justify-between items-center",
|
|
422
|
+
},
|
|
423
|
+
[
|
|
424
|
+
createElement("h3", {
|
|
425
|
+
className: "font-bold text-sm text-zinc-200",
|
|
426
|
+
textContent: "Event Details",
|
|
427
|
+
}),
|
|
428
|
+
createElement("div", { className: "flex gap-2" }, [
|
|
429
|
+
createElement("button", {
|
|
430
|
+
className:
|
|
431
|
+
"text-xs bg-zinc-800 hover:bg-zinc-700 px-3 py-1.5 rounded transition-colors",
|
|
432
|
+
textContent: "Copy JSON",
|
|
433
|
+
onClick: () => {
|
|
434
|
+
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
|
435
|
+
log("JSON copied to clipboard");
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
]),
|
|
439
|
+
],
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// JSON content with syntax highlighting
|
|
443
|
+
const content = createElement("div", {
|
|
444
|
+
className: "flex-1 overflow-auto custom-scrollbar p-4 bg-[#0d0d0d]",
|
|
445
|
+
});
|
|
446
|
+
const pre = createElement("pre", {
|
|
447
|
+
className: "font-mono text-xs text-green-400 whitespace-pre-wrap",
|
|
448
|
+
});
|
|
449
|
+
pre.innerHTML = syntaxHighlight(data);
|
|
450
|
+
content.appendChild(pre);
|
|
451
|
+
|
|
452
|
+
container.appendChild(header);
|
|
453
|
+
container.appendChild(content);
|
|
454
|
+
|
|
455
|
+
setEventDetailModalContent(container);
|
|
456
|
+
openEventDetailModal();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function syntaxHighlight(json) {
|
|
460
|
+
const str = typeof json === "string" ? json : JSON.stringify(json, null, 2);
|
|
461
|
+
return escapeHtml(str).replace(
|
|
462
|
+
/("(\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
|
|
463
|
+
(match) => {
|
|
464
|
+
let cls = "text-orange-300"; // number
|
|
465
|
+
if (/^"/.test(match)) {
|
|
466
|
+
cls = /:$/.test(match) ? "text-blue-300" : "text-green-300"; // key or string
|
|
467
|
+
} else if (/true|false/.test(match)) {
|
|
468
|
+
cls = "text-purple-300";
|
|
469
|
+
} else if (/null/.test(match)) {
|
|
470
|
+
cls = "text-gray-400";
|
|
471
|
+
}
|
|
472
|
+
return `<span class="${cls}">${match}</span>`;
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ============================================================
|
|
478
|
+
// Chart Initialization
|
|
479
|
+
// ============================================================
|
|
480
|
+
|
|
481
|
+
function initializeChart(labels, cursorData, claudeData, githubCopilotData) {
|
|
482
|
+
const ctx = $("#activityChart")?.getContext("2d");
|
|
483
|
+
if (!ctx) {
|
|
484
|
+
log("Chart canvas not found");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
log("Initializing chart with", labels.length, "data points");
|
|
489
|
+
|
|
490
|
+
const isDark = getTheme() === "dark";
|
|
491
|
+
const cursorColor = "#2563eb"; // Blue - works in both themes
|
|
492
|
+
const claudeColor = isDark ? "#ffffff" : "#09090b"; // White in dark, black in light
|
|
493
|
+
const copilotColor = "#8b5cf6"; // Violet - works in both themes
|
|
494
|
+
|
|
495
|
+
state.chart = new Chart(ctx, {
|
|
496
|
+
type: "line",
|
|
497
|
+
data: {
|
|
498
|
+
labels,
|
|
499
|
+
datasets: [
|
|
500
|
+
{
|
|
501
|
+
label: "Cursor",
|
|
502
|
+
data: cursorData,
|
|
503
|
+
borderColor: cursorColor,
|
|
504
|
+
backgroundColor: isDark
|
|
505
|
+
? "rgba(37, 99, 235, 0.1)"
|
|
506
|
+
: "rgba(37, 99, 235, 0.05)",
|
|
507
|
+
borderWidth: 2,
|
|
508
|
+
tension: 0.4,
|
|
509
|
+
fill: true,
|
|
510
|
+
pointRadius: 0,
|
|
511
|
+
pointHoverRadius: 4,
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
label: "Claude",
|
|
515
|
+
data: claudeData,
|
|
516
|
+
borderColor: claudeColor,
|
|
517
|
+
backgroundColor: isDark
|
|
518
|
+
? "rgba(255, 255, 255, 0.05)"
|
|
519
|
+
: "rgba(9, 9, 11, 0.05)",
|
|
520
|
+
borderWidth: 2,
|
|
521
|
+
tension: 0.4,
|
|
522
|
+
fill: true,
|
|
523
|
+
pointRadius: 0,
|
|
524
|
+
pointHoverRadius: 4,
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
label: "GitHub Copilot",
|
|
528
|
+
data: githubCopilotData || [],
|
|
529
|
+
borderColor: copilotColor,
|
|
530
|
+
backgroundColor: isDark
|
|
531
|
+
? "rgba(139, 92, 246, 0.1)"
|
|
532
|
+
: "rgba(139, 92, 246, 0.05)",
|
|
533
|
+
borderWidth: 2,
|
|
534
|
+
tension: 0.4,
|
|
535
|
+
fill: true,
|
|
536
|
+
pointRadius: 0,
|
|
537
|
+
pointHoverRadius: 4,
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
options: {
|
|
542
|
+
responsive: true,
|
|
543
|
+
maintainAspectRatio: false,
|
|
544
|
+
interaction: { mode: "index", intersect: false },
|
|
545
|
+
plugins: {
|
|
546
|
+
legend: { display: false },
|
|
547
|
+
tooltip: {
|
|
548
|
+
backgroundColor: isDark ? "#181818" : "#ffffff",
|
|
549
|
+
titleColor: isDark ? "#fff" : "#09090b",
|
|
550
|
+
bodyColor: isDark ? "#a1a1aa" : "#52525b",
|
|
551
|
+
borderColor: isDark ? "#27272a" : "#e4e4e7",
|
|
552
|
+
borderWidth: 1,
|
|
553
|
+
padding: 10,
|
|
554
|
+
displayColors: true,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
scales: {
|
|
558
|
+
x: {
|
|
559
|
+
grid: { display: false },
|
|
560
|
+
ticks: {
|
|
561
|
+
color: "#71717a",
|
|
562
|
+
font: { size: 10 },
|
|
563
|
+
maxTicksLimit: 7,
|
|
564
|
+
autoSkip: true,
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
y: {
|
|
568
|
+
beginAtZero: true,
|
|
569
|
+
grid: { color: isDark ? "#27272a" : "#e4e4e7" },
|
|
570
|
+
ticks: { color: "#71717a", font: { size: 10 }, stepSize: 5 },
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ============================================================
|
|
578
|
+
// Event Delegation (Single Handler)
|
|
579
|
+
// ============================================================
|
|
580
|
+
|
|
581
|
+
function setupEventDelegation() {
|
|
582
|
+
document.addEventListener("click", (e) => {
|
|
583
|
+
// Session row click
|
|
584
|
+
const sessionRow = e.target.closest(".session-row");
|
|
585
|
+
if (sessionRow) {
|
|
586
|
+
const sessionId = sessionRow.getAttribute("data-session-id");
|
|
587
|
+
if (sessionId) openSession(sessionId);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Event row click
|
|
592
|
+
const eventRow = e.target.closest(".event-row");
|
|
593
|
+
if (eventRow) {
|
|
594
|
+
const eventType = eventRow.getAttribute("data-event-type");
|
|
595
|
+
const eventIndex = parseInt(
|
|
596
|
+
eventRow.getAttribute("data-event-index"),
|
|
597
|
+
10,
|
|
598
|
+
);
|
|
599
|
+
if (eventType && !isNaN(eventIndex))
|
|
600
|
+
openEventDetail(eventType, eventIndex);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Tab button click
|
|
605
|
+
const tabBtn = e.target.closest(".tab-btn");
|
|
606
|
+
if (tabBtn) {
|
|
607
|
+
const tabId = tabBtn.getAttribute("data-tab");
|
|
608
|
+
if (tabId) showTab(tabId);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Modal close on overlay click
|
|
613
|
+
if (e.target.id === "modal-container") {
|
|
614
|
+
closeModal();
|
|
615
|
+
}
|
|
616
|
+
if (e.target.id === "modal-event-detail") {
|
|
617
|
+
closeEventDetailModal();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Keyboard: Escape to close modal
|
|
622
|
+
document.addEventListener("keydown", (e) => {
|
|
623
|
+
if (e.key === "Escape") closeEventDetailModal();
|
|
624
|
+
if (e.key === "Escape") closeModal();
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
log("Event delegation set up");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ============================================================
|
|
631
|
+
// Theme Management
|
|
632
|
+
// ============================================================
|
|
633
|
+
|
|
634
|
+
function getTheme() {
|
|
635
|
+
return localStorage.getItem("theme") || "dark";
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function setTheme(theme) {
|
|
639
|
+
const html = document.documentElement;
|
|
640
|
+
if (theme === "dark") {
|
|
641
|
+
html.classList.add("dark");
|
|
642
|
+
} else {
|
|
643
|
+
html.classList.remove("dark");
|
|
644
|
+
}
|
|
645
|
+
localStorage.setItem("theme", theme);
|
|
646
|
+
updateThemeIcon(theme);
|
|
647
|
+
updateChartTheme(theme);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function toggleTheme() {
|
|
651
|
+
const currentTheme = getTheme();
|
|
652
|
+
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
|
653
|
+
setTheme(newTheme);
|
|
654
|
+
log("Theme toggled to:", newTheme);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function updateThemeIcon(theme) {
|
|
658
|
+
const lightIcon = $("#theme-icon-light");
|
|
659
|
+
const darkIcon = $("#theme-icon-dark");
|
|
660
|
+
if (lightIcon && darkIcon) {
|
|
661
|
+
if (theme === "dark") {
|
|
662
|
+
lightIcon.classList.remove("hidden");
|
|
663
|
+
lightIcon.classList.add("block");
|
|
664
|
+
darkIcon.classList.remove("block");
|
|
665
|
+
darkIcon.classList.add("hidden");
|
|
666
|
+
} else {
|
|
667
|
+
lightIcon.classList.remove("block");
|
|
668
|
+
lightIcon.classList.add("hidden");
|
|
669
|
+
darkIcon.classList.remove("hidden");
|
|
670
|
+
darkIcon.classList.add("block");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function updateChartTheme(theme) {
|
|
676
|
+
if (!state.chart) return;
|
|
677
|
+
|
|
678
|
+
const isDark = theme === "dark";
|
|
679
|
+
const claudeColor = isDark ? "#ffffff" : "#09090b";
|
|
680
|
+
|
|
681
|
+
// Update Claude dataset color
|
|
682
|
+
if (state.chart.data.datasets[1]) {
|
|
683
|
+
state.chart.data.datasets[1].borderColor = claudeColor;
|
|
684
|
+
state.chart.data.datasets[1].backgroundColor = isDark
|
|
685
|
+
? "rgba(255, 255, 255, 0.05)"
|
|
686
|
+
: "rgba(9, 9, 11, 0.05)";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Update tooltip colors
|
|
690
|
+
state.chart.options.plugins.tooltip.backgroundColor = isDark
|
|
691
|
+
? "#181818"
|
|
692
|
+
: "#ffffff";
|
|
693
|
+
state.chart.options.plugins.tooltip.titleColor = isDark
|
|
694
|
+
? "#fff"
|
|
695
|
+
: "#09090b";
|
|
696
|
+
state.chart.options.plugins.tooltip.bodyColor = isDark
|
|
697
|
+
? "#a1a1aa"
|
|
698
|
+
: "#52525b";
|
|
699
|
+
state.chart.options.plugins.tooltip.borderColor = isDark
|
|
700
|
+
? "#27272a"
|
|
701
|
+
: "#e4e4e7";
|
|
702
|
+
|
|
703
|
+
// Update scales
|
|
704
|
+
state.chart.options.scales.x.ticks.color = "#71717a";
|
|
705
|
+
state.chart.options.scales.y.grid.color = isDark ? "#27272a" : "#e4e4e7";
|
|
706
|
+
state.chart.options.scales.y.ticks.color = "#71717a";
|
|
707
|
+
|
|
708
|
+
state.chart.update("none");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ============================================================
|
|
712
|
+
// Page Initialization
|
|
713
|
+
// ============================================================
|
|
714
|
+
|
|
715
|
+
function initializePage() {
|
|
716
|
+
// Initialize theme
|
|
717
|
+
const savedTheme = getTheme();
|
|
718
|
+
setTheme(savedTheme);
|
|
719
|
+
|
|
720
|
+
// Set up theme toggle button
|
|
721
|
+
const themeToggle = $("#theme-toggle");
|
|
722
|
+
if (themeToggle) {
|
|
723
|
+
themeToggle.addEventListener("click", toggleTheme);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Set current date
|
|
727
|
+
const dateEl = $("#current-date");
|
|
728
|
+
if (dateEl) {
|
|
729
|
+
dateEl.textContent = new Date().toLocaleDateString(undefined, {
|
|
730
|
+
weekday: "long",
|
|
731
|
+
year: "numeric",
|
|
732
|
+
month: "long",
|
|
733
|
+
day: "numeric",
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Mark initial active tab
|
|
738
|
+
const activeTabBtn = $(`.tab-btn[data-tab="${state.activeTab}"]`);
|
|
739
|
+
if (activeTabBtn) activeTabBtn.classList.add("active");
|
|
740
|
+
|
|
741
|
+
log("Page initialized");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ============================================================
|
|
745
|
+
// Public API
|
|
746
|
+
// ============================================================
|
|
747
|
+
|
|
748
|
+
window.Dashboard = {
|
|
749
|
+
// Core initialization
|
|
750
|
+
init: function (sessions, rawData, chartConfig, sessionsWithEvents) {
|
|
751
|
+
log("Dashboard.init() called");
|
|
752
|
+
initializeData(sessions, rawData, sessionsWithEvents);
|
|
753
|
+
setupEventDelegation();
|
|
754
|
+
initializePage();
|
|
755
|
+
|
|
756
|
+
if (chartConfig) {
|
|
757
|
+
initializeChart(
|
|
758
|
+
chartConfig.labels,
|
|
759
|
+
chartConfig.cursor,
|
|
760
|
+
chartConfig.claude,
|
|
761
|
+
chartConfig.github_copilot,
|
|
762
|
+
);
|
|
763
|
+
// Update chart theme after initialization
|
|
764
|
+
updateChartTheme(getTheme());
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (DEBUG) exposeDebugAPI();
|
|
768
|
+
log("Dashboard ready");
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
// Exposed for inline handlers if needed (prefer data-tab attributes)
|
|
772
|
+
showTab,
|
|
773
|
+
filterTable,
|
|
774
|
+
closeModal,
|
|
775
|
+
closeEventDetailModal,
|
|
776
|
+
|
|
777
|
+
// For testing
|
|
778
|
+
__test__: {
|
|
779
|
+
escapeHtml,
|
|
780
|
+
syntaxHighlight,
|
|
781
|
+
buildEventBubble,
|
|
782
|
+
getState: () => state,
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
})(window);
|