@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.
Files changed (156) hide show
  1. package/README.md +337 -0
  2. package/bin/overwatch +12 -0
  3. package/dist/auth/cli-oauth.d.ts +13 -0
  4. package/dist/auth/cli-oauth.d.ts.map +1 -0
  5. package/dist/auth/html-utils.d.ts +20 -0
  6. package/dist/auth/html-utils.d.ts.map +1 -0
  7. package/dist/auth/index.d.ts +10 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/oauth.d.ts +81 -0
  10. package/dist/auth/oauth.d.ts.map +1 -0
  11. package/dist/auth/pkce.d.ts +26 -0
  12. package/dist/auth/pkce.d.ts.map +1 -0
  13. package/dist/auth/token-store.d.ts +44 -0
  14. package/dist/auth/token-store.d.ts.map +1 -0
  15. package/dist/bin/overwatch +12 -0
  16. package/dist/cli.d.ts +6 -0
  17. package/dist/cli.d.ts.map +1 -0
  18. package/dist/cli.js +5449 -0
  19. package/dist/cli.js.map +7 -0
  20. package/dist/config/index.d.ts +5 -0
  21. package/dist/config/index.d.ts.map +1 -0
  22. package/dist/config/manager.d.ts +54 -0
  23. package/dist/config/manager.d.ts.map +1 -0
  24. package/dist/daemon.d.ts +11 -0
  25. package/dist/daemon.d.ts.map +1 -0
  26. package/dist/daemon.js +6004 -0
  27. package/dist/daemon.js.map +7 -0
  28. package/dist/data/ingestor.d.ts +31 -0
  29. package/dist/data/ingestor.d.ts.map +1 -0
  30. package/dist/data/processor.d.ts +96 -0
  31. package/dist/data/processor.d.ts.map +1 -0
  32. package/dist/data/reader.d.ts +24 -0
  33. package/dist/data/reader.d.ts.map +1 -0
  34. package/dist/data/recorder.d.ts +12 -0
  35. package/dist/data/recorder.d.ts.map +1 -0
  36. package/dist/engines/cedar.d.ts +41 -0
  37. package/dist/engines/cedar.d.ts.map +1 -0
  38. package/dist/engines/remote.d.ts +21 -0
  39. package/dist/engines/remote.d.ts.map +1 -0
  40. package/dist/engines/yara.d.ts +12 -0
  41. package/dist/engines/yara.d.ts.map +1 -0
  42. package/dist/handlers/dashboard-handler.d.ts +7 -0
  43. package/dist/handlers/dashboard-handler.d.ts.map +1 -0
  44. package/dist/handlers/hook-handler.d.ts +23 -0
  45. package/dist/handlers/hook-handler.d.ts.map +1 -0
  46. package/dist/handlers/oauth-handler.d.ts +12 -0
  47. package/dist/handlers/oauth-handler.d.ts.map +1 -0
  48. package/dist/handlers/scan-handler.d.ts +13 -0
  49. package/dist/handlers/scan-handler.d.ts.map +1 -0
  50. package/dist/handlers/utils.d.ts +11 -0
  51. package/dist/handlers/utils.d.ts.map +1 -0
  52. package/dist/hooks/claudecode/hooks.json.template +20 -0
  53. package/dist/hooks/cursor/hooks.json.template +74 -0
  54. package/dist/hooks/universal-hook.sh +36 -0
  55. package/dist/http/server.d.ts +38 -0
  56. package/dist/http/server.d.ts.map +1 -0
  57. package/dist/index.d.ts +8 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +5941 -0
  60. package/dist/index.js.map +7 -0
  61. package/dist/installer.d.ts +25 -0
  62. package/dist/installer.d.ts.map +1 -0
  63. package/dist/javelin/admin-client.d.ts +75 -0
  64. package/dist/javelin/admin-client.d.ts.map +1 -0
  65. package/dist/javelin/client.d.ts +30 -0
  66. package/dist/javelin/client.d.ts.map +1 -0
  67. package/dist/javelin/config-reader.d.ts +70 -0
  68. package/dist/javelin/config-reader.d.ts.map +1 -0
  69. package/dist/javelin/index.d.ts +5 -0
  70. package/dist/javelin/index.d.ts.map +1 -0
  71. package/dist/javelin/types.d.ts +81 -0
  72. package/dist/javelin/types.d.ts.map +1 -0
  73. package/dist/lib/policy-engine.d.ts +34 -0
  74. package/dist/lib/policy-engine.d.ts.map +1 -0
  75. package/dist/lib/policy-manager.d.ts +86 -0
  76. package/dist/lib/policy-manager.d.ts.map +1 -0
  77. package/dist/module.d.ts +52 -0
  78. package/dist/module.d.ts.map +1 -0
  79. package/dist/pipeline/context-mapper.d.ts +16 -0
  80. package/dist/pipeline/context-mapper.d.ts.map +1 -0
  81. package/dist/pipeline/extractors/claude-extractor.d.ts +48 -0
  82. package/dist/pipeline/extractors/claude-extractor.d.ts.map +1 -0
  83. package/dist/pipeline/extractors/cursor-extractor.d.ts +44 -0
  84. package/dist/pipeline/extractors/cursor-extractor.d.ts.map +1 -0
  85. package/dist/pipeline/extractors/github-copilot-extractor.d.ts +49 -0
  86. package/dist/pipeline/extractors/github-copilot-extractor.d.ts.map +1 -0
  87. package/dist/pipeline/extractors/index.d.ts +47 -0
  88. package/dist/pipeline/extractors/index.d.ts.map +1 -0
  89. package/dist/pipeline/extractors/registry.d.ts +38 -0
  90. package/dist/pipeline/extractors/registry.d.ts.map +1 -0
  91. package/dist/pipeline/hook-pipeline.d.ts +25 -0
  92. package/dist/pipeline/hook-pipeline.d.ts.map +1 -0
  93. package/dist/policy.cedar +783 -0
  94. package/dist/rules/pre/command_injection.yar +60 -0
  95. package/dist/rules/pre/cross_origin_escalation.yar +106 -0
  96. package/dist/rules/pre/mcp_config_risk.yar +35 -0
  97. package/dist/rules/pre/path_traversal.yar +50 -0
  98. package/dist/rules/pre/prompt_injection.yar +101 -0
  99. package/dist/rules/pre/secrets_leakage.yar +100 -0
  100. package/dist/rules/pre/sql_injection.yar +65 -0
  101. package/dist/scanner.d.ts +80 -0
  102. package/dist/scanner.d.ts.map +1 -0
  103. package/dist/service.d.ts +18 -0
  104. package/dist/service.d.ts.map +1 -0
  105. package/dist/services/interface.d.ts +11 -0
  106. package/dist/services/interface.d.ts.map +1 -0
  107. package/dist/services/launchd.d.ts +12 -0
  108. package/dist/services/launchd.d.ts.map +1 -0
  109. package/dist/services/systemd.d.ts +12 -0
  110. package/dist/services/systemd.d.ts.map +1 -0
  111. package/dist/services/windows.d.ts +7 -0
  112. package/dist/services/windows.d.ts.map +1 -0
  113. package/dist/skills/index.d.ts +7 -0
  114. package/dist/skills/index.d.ts.map +1 -0
  115. package/dist/skills/scanner.d.ts +44 -0
  116. package/dist/skills/scanner.d.ts.map +1 -0
  117. package/dist/skills/types.d.ts +29 -0
  118. package/dist/skills/types.d.ts.map +1 -0
  119. package/dist/types/config.d.ts +165 -0
  120. package/dist/types/config.d.ts.map +1 -0
  121. package/dist/types/events.d.ts +225 -0
  122. package/dist/types/events.d.ts.map +1 -0
  123. package/dist/types/index.d.ts +6 -0
  124. package/dist/types/index.d.ts.map +1 -0
  125. package/dist/types/remote-policy.d.ts +129 -0
  126. package/dist/types/remote-policy.d.ts.map +1 -0
  127. package/dist/types/requests.d.ts +45 -0
  128. package/dist/types/requests.d.ts.map +1 -0
  129. package/dist/types/responses.d.ts +60 -0
  130. package/dist/types/responses.d.ts.map +1 -0
  131. package/dist/ui/images/highflame-mono.png +0 -0
  132. package/dist/ui/views/dashboard.ejs +301 -0
  133. package/dist/ui/views/dashboard.js +785 -0
  134. package/dist/ui/views/partials/commands-table.ejs +54 -0
  135. package/dist/ui/views/partials/events-table.ejs +36 -0
  136. package/dist/ui/views/partials/filter-dropdown.ejs +12 -0
  137. package/dist/ui/views/partials/overview-charts.ejs +149 -0
  138. package/dist/ui/views/partials/scans-table.ejs +136 -0
  139. package/dist/ui/views/partials/sessions-table.ejs +50 -0
  140. package/dist/ui/views/partials/stats-grid.ejs +23 -0
  141. package/dist/ui/views/partials/threats-table.ejs +60 -0
  142. package/dist/utils/index.d.ts +3 -0
  143. package/dist/utils/index.d.ts.map +1 -0
  144. package/dist/utils/logger.d.ts +28 -0
  145. package/dist/utils/logger.d.ts.map +1 -0
  146. package/dist/utils/performance.d.ts +26 -0
  147. package/dist/utils/performance.d.ts.map +1 -0
  148. package/dist/utils/port-manager.d.ts +6 -0
  149. package/dist/utils/port-manager.d.ts.map +1 -0
  150. package/dist/yara/engine.d.ts +58 -0
  151. package/dist/yara/engine.d.ts.map +1 -0
  152. package/dist/yara/index.d.ts +5 -0
  153. package/dist/yara/index.d.ts.map +1 -0
  154. package/lib/platform-loader.js +210 -0
  155. package/package.json +63 -0
  156. 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
+ "<": "&lt;",
116
+ ">": "&gt;",
117
+ '"': "&quot;",
118
+ "'": "&#039;",
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, "&#10;");
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);