brakit 0.8.0 → 0.8.2

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.
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/constants/routes.ts
12
- var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS;
12
+ var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, VALID_TABS;
13
13
  var init_routes = __esm({
14
14
  "src/constants/routes.ts"() {
15
15
  "use strict";
@@ -30,11 +30,22 @@ var init_routes = __esm({
30
30
  DASHBOARD_API_SECURITY = "/__brakit/api/security";
31
31
  DASHBOARD_API_TAB = "/__brakit/api/tab";
32
32
  DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
33
+ VALID_TABS = /* @__PURE__ */ new Set([
34
+ "overview",
35
+ "actions",
36
+ "requests",
37
+ "fetches",
38
+ "queries",
39
+ "errors",
40
+ "logs",
41
+ "performance",
42
+ "security"
43
+ ]);
33
44
  }
34
45
  });
35
46
 
36
47
  // src/constants/limits.ts
37
- var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH;
48
+ var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS;
38
49
  var init_limits = __esm({
39
50
  "src/constants/limits.ts"() {
40
51
  "use strict";
@@ -43,11 +54,15 @@ var init_limits = __esm({
43
54
  DEFAULT_API_LIMIT = 500;
44
55
  MAX_TELEMETRY_ENTRIES = 1e3;
45
56
  MAX_TAB_NAME_LENGTH = 32;
57
+ MAX_INGEST_BYTES = 10 * 1024 * 1024;
58
+ TERMINAL_TRUNCATE_LENGTH = 80;
59
+ SENSITIVE_MASK_MIN_LENGTH = 8;
60
+ SENSITIVE_MASK_VISIBLE_CHARS = 4;
46
61
  }
47
62
  });
48
63
 
49
64
  // src/constants/thresholds.ts
50
- var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS;
65
+ var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, RESOLVE_AFTER_ABSENCES, RESOLVED_INSIGHT_TTL_MS;
51
66
  var init_thresholds = __esm({
52
67
  "src/constants/thresholds.ts"() {
53
68
  "use strict";
@@ -77,18 +92,27 @@ var init_thresholds = __esm({
77
92
  OVERFETCH_MANY_FIELDS = 12;
78
93
  OVERFETCH_UNWRAP_MIN_SIZE = 3;
79
94
  MAX_DUPLICATE_INSIGHTS = 3;
95
+ INSIGHT_WINDOW_PER_ENDPOINT = 2;
96
+ RESOLVE_AFTER_ABSENCES = 3;
97
+ RESOLVED_INSIGHT_TTL_MS = 18e5;
80
98
  }
81
99
  });
82
100
 
83
101
  // src/constants/transport.ts
84
- var SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS;
102
+ var SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS;
85
103
  var init_transport = __esm({
86
104
  "src/constants/transport.ts"() {
87
105
  "use strict";
88
106
  SSE_HEARTBEAT_INTERVAL_MS = 3e4;
89
107
  NOISE_HOSTS = [
90
108
  "registry.npmjs.org",
91
- "telemetry.nextjs.org"
109
+ "telemetry.nextjs.org",
110
+ "vitejs.dev"
111
+ ];
112
+ NOISE_PATH_PATTERNS = [
113
+ ".hot-update.",
114
+ "__webpack",
115
+ "__vite"
92
116
  ];
93
117
  }
94
118
  });
@@ -110,9 +134,18 @@ var init_metrics = __esm({
110
134
  });
111
135
 
112
136
  // src/constants/headers.ts
137
+ var SENSITIVE_HEADER_NAMES;
113
138
  var init_headers = __esm({
114
139
  "src/constants/headers.ts"() {
115
140
  "use strict";
141
+ SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
142
+ "authorization",
143
+ "cookie",
144
+ "set-cookie",
145
+ "proxy-authorization",
146
+ "x-api-key",
147
+ "x-auth-token"
148
+ ]);
116
149
  }
117
150
  });
118
151
 
@@ -159,6 +192,38 @@ var init_mcp = __esm({
159
192
  }
160
193
  });
161
194
 
195
+ // src/constants/encoding.ts
196
+ var CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE;
197
+ var init_encoding = __esm({
198
+ "src/constants/encoding.ts"() {
199
+ "use strict";
200
+ CONTENT_ENCODING_GZIP = "gzip";
201
+ CONTENT_ENCODING_BR = "br";
202
+ CONTENT_ENCODING_DEFLATE = "deflate";
203
+ }
204
+ });
205
+
206
+ // src/constants/severity.ts
207
+ var SEVERITY_ICON, SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
208
+ var init_severity = __esm({
209
+ "src/constants/severity.ts"() {
210
+ "use strict";
211
+ SEVERITY_ICON = {
212
+ critical: "\u2717",
213
+ warning: "\u26A0",
214
+ info: "\u2139"
215
+ };
216
+ SEVERITY_CRITICAL = "critical";
217
+ SEVERITY_WARNING = "warning";
218
+ SEVERITY_INFO = "info";
219
+ SEVERITY_ICON_MAP = {
220
+ [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
221
+ [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
222
+ [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
223
+ };
224
+ }
225
+ });
226
+
162
227
  // src/constants/index.ts
163
228
  var init_constants = __esm({
164
229
  "src/constants/index.ts"() {
@@ -171,21 +236,8 @@ var init_constants = __esm({
171
236
  init_headers();
172
237
  init_network();
173
238
  init_mcp();
174
- }
175
- });
176
-
177
- // src/instrument/transport.ts
178
- function setEmitter(fn) {
179
- emitter = fn;
180
- }
181
- function send(event) {
182
- emitter?.(event);
183
- }
184
- var emitter;
185
- var init_transport2 = __esm({
186
- "src/instrument/transport.ts"() {
187
- "use strict";
188
- emitter = null;
239
+ init_encoding();
240
+ init_severity();
189
241
  }
190
242
  });
191
243
 
@@ -205,7 +257,11 @@ var init_context = __esm({
205
257
 
206
258
  // src/instrument/hooks/fetch.ts
207
259
  import { subscribe } from "diagnostics_channel";
208
- function isNoise(origin) {
260
+ function setBrakitPort(port) {
261
+ brakitPort = port;
262
+ }
263
+ function isNoise(origin, path) {
264
+ if (NOISE_PATH_PATTERNS.some((p) => path.includes(p))) return true;
209
265
  try {
210
266
  const host = new URL(origin).hostname;
211
267
  return NOISE_HOSTS.some((h) => host === h || host.endsWith("." + h));
@@ -213,19 +269,22 @@ function isNoise(origin) {
213
269
  return false;
214
270
  }
215
271
  }
216
- function setupFetchHook() {
272
+ function setupFetchHook(emit) {
217
273
  subscribe("undici:request:create", (message) => {
218
274
  const msg = message;
219
275
  const req = msg.request;
220
276
  const origin = req.origin ?? "";
221
- if (isNoise(origin)) return;
277
+ const path = req.path ?? "/";
278
+ if (isNoise(origin, path)) return;
279
+ if (brakitPort && origin.includes(`localhost:${brakitPort}`)) return;
222
280
  const ctx = getRequestContext();
281
+ if (!ctx) return;
223
282
  pending.set(msg.request, {
224
283
  origin,
225
284
  method: req.method ?? "GET",
226
- path: req.path ?? "/",
285
+ path,
227
286
  startTime: performance.now(),
228
- parentRequestId: ctx?.requestId ?? null
287
+ parentRequestId: ctx.requestId
229
288
  });
230
289
  });
231
290
  subscribe("undici:request:headers", (message) => {
@@ -233,7 +292,7 @@ function setupFetchHook() {
233
292
  const info = pending.get(msg.request);
234
293
  if (!info) return;
235
294
  pending.delete(msg.request);
236
- send({
295
+ emit({
237
296
  type: "fetch",
238
297
  data: {
239
298
  url: info.origin + info.path,
@@ -250,32 +309,33 @@ function setupFetchHook() {
250
309
  pending.delete(msg.request);
251
310
  });
252
311
  }
253
- var pending;
312
+ var brakitPort, pending;
254
313
  var init_fetch = __esm({
255
314
  "src/instrument/hooks/fetch.ts"() {
256
315
  "use strict";
257
- init_transport2();
258
316
  init_context();
259
317
  init_constants();
318
+ brakitPort = 0;
260
319
  pending = /* @__PURE__ */ new WeakMap();
261
320
  }
262
321
  });
263
322
 
264
323
  // src/instrument/hooks/console.ts
265
324
  import { format } from "util";
266
- function setupConsoleHook() {
325
+ function setupConsoleHook(emit) {
267
326
  for (const level of LEVELS) {
268
327
  const original = originals[level];
269
328
  console[level] = (...args) => {
270
329
  original.apply(console, args);
271
330
  const ctx = getRequestContext();
331
+ if (!ctx) return;
272
332
  const message = format(...args);
273
333
  const timestamp = Date.now();
274
- const parentRequestId = ctx?.requestId ?? null;
334
+ const parentRequestId = ctx.requestId;
275
335
  if (level === "error") {
276
336
  const errorArg = args.find((a) => a instanceof Error);
277
337
  if (errorArg) {
278
- send({
338
+ emit({
279
339
  type: "error",
280
340
  data: {
281
341
  name: errorArg.name,
@@ -289,7 +349,7 @@ function setupConsoleHook() {
289
349
  }
290
350
  const match = message.match(/(\w*Error):\s+(.+)/s);
291
351
  if (match) {
292
- send({
352
+ emit({
293
353
  type: "error",
294
354
  data: {
295
355
  name: match[1],
@@ -302,7 +362,7 @@ function setupConsoleHook() {
302
362
  return;
303
363
  }
304
364
  }
305
- send({
365
+ emit({
306
366
  type: "log",
307
367
  data: { level, message, parentRequestId, timestamp }
308
368
  });
@@ -313,7 +373,6 @@ var LEVELS, originals;
313
373
  var init_console = __esm({
314
374
  "src/instrument/hooks/console.ts"() {
315
375
  "use strict";
316
- init_transport2();
317
376
  init_context();
318
377
  LEVELS = ["log", "warn", "error", "info", "debug"];
319
378
  originals = {
@@ -327,21 +386,24 @@ var init_console = __esm({
327
386
  });
328
387
 
329
388
  // src/instrument/hooks/errors.ts
330
- function captureError(err) {
331
- const error = err instanceof Error ? err : new Error(String(err));
332
- const ctx = getRequestContext();
333
- send({
334
- type: "error",
335
- data: {
336
- name: error.name,
337
- message: error.message,
338
- stack: error.stack ?? "",
339
- parentRequestId: ctx?.requestId ?? null,
340
- timestamp: Date.now()
341
- }
342
- });
389
+ function createCaptureError(emit) {
390
+ return (err) => {
391
+ const error = err instanceof Error ? err : new Error(String(err));
392
+ const ctx = getRequestContext();
393
+ emit({
394
+ type: "error",
395
+ data: {
396
+ name: error.name,
397
+ message: error.message,
398
+ stack: error.stack ?? "",
399
+ parentRequestId: ctx?.requestId ?? null,
400
+ timestamp: Date.now()
401
+ }
402
+ });
403
+ };
343
404
  }
344
- function setupErrorHook() {
405
+ function setupErrorHook(emit) {
406
+ const captureError = createCaptureError(emit);
345
407
  process.on("uncaughtException", (err) => {
346
408
  captureError(err);
347
409
  process.removeAllListeners("uncaughtException");
@@ -354,7 +416,6 @@ function setupErrorHook() {
354
416
  var init_errors = __esm({
355
417
  "src/instrument/hooks/errors.ts"() {
356
418
  "use strict";
357
- init_transport2();
358
419
  init_context();
359
420
  }
360
421
  });
@@ -727,115 +788,6 @@ var init_adapters = __esm({
727
788
  }
728
789
  });
729
790
 
730
- // src/utils/static-patterns.ts
731
- function isStaticPath(urlPath) {
732
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
733
- }
734
- var STATIC_PATTERNS;
735
- var init_static_patterns = __esm({
736
- "src/utils/static-patterns.ts"() {
737
- "use strict";
738
- STATIC_PATTERNS = [
739
- /^\/_next\//,
740
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
741
- /^\/favicon/,
742
- /^\/__nextjs/
743
- ];
744
- }
745
- });
746
-
747
- // src/store/request-store.ts
748
- function flattenHeaders(headers) {
749
- const flat = {};
750
- for (const [key, value] of Object.entries(headers)) {
751
- if (value === void 0) continue;
752
- flat[key] = Array.isArray(value) ? value.join(", ") : value;
753
- }
754
- return flat;
755
- }
756
- var RequestStore;
757
- var init_request_store = __esm({
758
- "src/store/request-store.ts"() {
759
- "use strict";
760
- init_constants();
761
- init_static_patterns();
762
- RequestStore = class {
763
- constructor(maxEntries = MAX_REQUEST_ENTRIES) {
764
- this.maxEntries = maxEntries;
765
- }
766
- requests = [];
767
- listeners = [];
768
- capture(input) {
769
- const url = input.url;
770
- const path = url.split("?")[0];
771
- let requestBodyStr = null;
772
- if (input.requestBody && input.requestBody.length > 0) {
773
- requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
774
- }
775
- let responseBodyStr = null;
776
- if (input.responseBody && input.responseBody.length > 0) {
777
- const ct = input.responseContentType;
778
- if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
779
- responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
780
- }
781
- }
782
- const entry = {
783
- id: input.requestId,
784
- method: input.method,
785
- url,
786
- path,
787
- headers: flattenHeaders(input.requestHeaders),
788
- requestBody: requestBodyStr,
789
- statusCode: input.statusCode,
790
- responseHeaders: flattenHeaders(input.responseHeaders),
791
- responseBody: responseBodyStr,
792
- startedAt: input.startTime,
793
- durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
794
- responseSize: input.responseBody?.length ?? 0,
795
- isStatic: isStaticPath(path)
796
- };
797
- this.requests.push(entry);
798
- if (this.requests.length > this.maxEntries) {
799
- this.requests.shift();
800
- }
801
- for (const fn of this.listeners) {
802
- fn(entry);
803
- }
804
- return entry;
805
- }
806
- getAll() {
807
- return this.requests;
808
- }
809
- clear() {
810
- this.requests.length = 0;
811
- }
812
- onRequest(fn) {
813
- this.listeners.push(fn);
814
- }
815
- offRequest(fn) {
816
- const idx = this.listeners.indexOf(fn);
817
- if (idx !== -1) this.listeners.splice(idx, 1);
818
- }
819
- };
820
- }
821
- });
822
-
823
- // src/store/request-log.ts
824
- var defaultStore, getRequests, clearRequests, onRequest, offRequest;
825
- var init_request_log = __esm({
826
- "src/store/request-log.ts"() {
827
- "use strict";
828
- init_request_store();
829
- init_static_patterns();
830
- init_request_store();
831
- defaultStore = new RequestStore();
832
- getRequests = () => defaultStore.getAll();
833
- clearRequests = () => defaultStore.clear();
834
- onRequest = (fn) => defaultStore.onRequest(fn);
835
- offRequest = (fn) => defaultStore.offRequest(fn);
836
- }
837
- });
838
-
839
791
  // src/analysis/categorize.ts
840
792
  function detectCategory(req) {
841
793
  const { method, url, statusCode, responseHeaders } = req;
@@ -1216,1195 +1168,748 @@ var init_group = __esm({
1216
1168
  }
1217
1169
  });
1218
1170
 
1219
- // src/store/telemetry-store.ts
1220
- import { randomUUID as randomUUID3 } from "crypto";
1221
- var TelemetryStore;
1222
- var init_telemetry_store = __esm({
1223
- "src/store/telemetry-store.ts"() {
1224
- "use strict";
1225
- init_constants();
1226
- TelemetryStore = class {
1227
- constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
1228
- this.maxEntries = maxEntries;
1229
- }
1230
- entries = [];
1231
- listeners = [];
1232
- add(data) {
1233
- const entry = { id: randomUUID3(), ...data };
1234
- this.entries.push(entry);
1235
- if (this.entries.length > this.maxEntries) this.entries.shift();
1236
- for (const fn of this.listeners) fn(entry);
1237
- return entry;
1238
- }
1239
- getAll() {
1240
- return this.entries;
1241
- }
1242
- getByRequest(requestId) {
1243
- return this.entries.filter((e) => e.parentRequestId === requestId);
1244
- }
1245
- clear() {
1246
- this.entries.length = 0;
1247
- }
1248
- onEntry(fn) {
1249
- this.listeners.push(fn);
1250
- }
1251
- offEntry(fn) {
1252
- const idx = this.listeners.indexOf(fn);
1253
- if (idx !== -1) this.listeners.splice(idx, 1);
1254
- }
1255
- };
1171
+ // src/dashboard/api/shared.ts
1172
+ function maskSensitiveHeaders(headers) {
1173
+ const masked = {};
1174
+ for (const [key, value] of Object.entries(headers)) {
1175
+ if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1176
+ const s = String(value);
1177
+ masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? "****" : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
1178
+ } else {
1179
+ masked[key] = value;
1180
+ }
1256
1181
  }
1257
- });
1258
-
1259
- // src/store/fetch-store.ts
1260
- var FetchStore, defaultFetchStore;
1261
- var init_fetch_store = __esm({
1262
- "src/store/fetch-store.ts"() {
1263
- "use strict";
1264
- init_telemetry_store();
1265
- FetchStore = class extends TelemetryStore {
1266
- };
1267
- defaultFetchStore = new FetchStore();
1182
+ return masked;
1183
+ }
1184
+ function getCorsOrigin(req) {
1185
+ const origin = req.headers.origin ?? "";
1186
+ try {
1187
+ const url = new URL(origin);
1188
+ if (LOCALHOST_HOSTNAMES.has(url.hostname)) {
1189
+ return origin;
1190
+ }
1191
+ } catch {
1268
1192
  }
1269
- });
1270
-
1271
- // src/store/log-store.ts
1272
- var LogStore, defaultLogStore;
1273
- var init_log_store = __esm({
1274
- "src/store/log-store.ts"() {
1193
+ return "";
1194
+ }
1195
+ function getJsonHeaders(req) {
1196
+ const corsOrigin = getCorsOrigin(req);
1197
+ const headers = {
1198
+ "content-type": "application/json",
1199
+ "cache-control": "no-cache"
1200
+ };
1201
+ if (corsOrigin) {
1202
+ headers["access-control-allow-origin"] = corsOrigin;
1203
+ }
1204
+ return headers;
1205
+ }
1206
+ function sendJson(req, res, status, data) {
1207
+ res.writeHead(status, getJsonHeaders(req));
1208
+ res.end(JSON.stringify(data));
1209
+ }
1210
+ function requireGet(req, res) {
1211
+ if (req.method !== "GET") {
1212
+ sendJson(req, res, 405, { error: "Method not allowed" });
1213
+ return false;
1214
+ }
1215
+ return true;
1216
+ }
1217
+ function handleTelemetryGet(req, res, store) {
1218
+ if (!requireGet(req, res)) return;
1219
+ const url = new URL(req.url ?? "/", "http://localhost");
1220
+ const requestId = url.searchParams.get("requestId");
1221
+ const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1222
+ sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1223
+ }
1224
+ var init_shared2 = __esm({
1225
+ "src/dashboard/api/shared.ts"() {
1275
1226
  "use strict";
1276
- init_telemetry_store();
1277
- LogStore = class extends TelemetryStore {
1278
- };
1279
- defaultLogStore = new LogStore();
1227
+ init_constants();
1228
+ init_limits();
1280
1229
  }
1281
1230
  });
1282
1231
 
1283
- // src/store/error-store.ts
1284
- var ErrorStore, defaultErrorStore;
1285
- var init_error_store = __esm({
1286
- "src/store/error-store.ts"() {
1287
- "use strict";
1288
- init_telemetry_store();
1289
- ErrorStore = class extends TelemetryStore {
1290
- };
1291
- defaultErrorStore = new ErrorStore();
1232
+ // src/dashboard/api/handlers.ts
1233
+ function sanitizeRequest(r) {
1234
+ return {
1235
+ ...r,
1236
+ headers: maskSensitiveHeaders(r.headers),
1237
+ responseHeaders: maskSensitiveHeaders(r.responseHeaders)
1238
+ };
1239
+ }
1240
+ function createRequestsHandler(registry) {
1241
+ return (req, res) => {
1242
+ if (!requireGet(req, res)) return;
1243
+ const url = new URL(req.url ?? "/", "http://localhost");
1244
+ const method = url.searchParams.get("method");
1245
+ const status = url.searchParams.get("status");
1246
+ const search = url.searchParams.get("search");
1247
+ const limit = parseInt(
1248
+ url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT),
1249
+ 10
1250
+ );
1251
+ const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
1252
+ let results = [...registry.get("request-store").getAll()].reverse();
1253
+ if (method) {
1254
+ results = results.filter((r) => r.method === method.toUpperCase());
1255
+ }
1256
+ if (status) {
1257
+ if (status.endsWith("xx")) {
1258
+ const prefix = parseInt(status[0], 10);
1259
+ results = results.filter(
1260
+ (r) => Math.floor(r.statusCode / 100) === prefix
1261
+ );
1262
+ } else {
1263
+ const code = parseInt(status, 10);
1264
+ results = results.filter((r) => r.statusCode === code);
1265
+ }
1266
+ }
1267
+ if (search) {
1268
+ const lower = search.toLowerCase();
1269
+ results = results.filter(
1270
+ (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1271
+ );
1272
+ }
1273
+ const total = results.length;
1274
+ results = results.slice(offset, offset + limit);
1275
+ const sanitized = results.map(sanitizeRequest);
1276
+ sendJson(req, res, 200, { total, requests: sanitized });
1277
+ };
1278
+ }
1279
+ function createFlowsHandler(registry) {
1280
+ return (req, res) => {
1281
+ if (!requireGet(req, res)) return;
1282
+ const flows = groupRequestsIntoFlows(registry.get("request-store").getAll()).reverse().map((flow) => ({
1283
+ ...flow,
1284
+ requests: flow.requests.map(sanitizeRequest)
1285
+ }));
1286
+ sendJson(req, res, 200, { total: flows.length, flows });
1287
+ };
1288
+ }
1289
+ function createClearHandler(registry) {
1290
+ return (req, res) => {
1291
+ if (req.method !== "POST") {
1292
+ sendJson(req, res, 405, { error: "Method not allowed" });
1293
+ return;
1294
+ }
1295
+ registry.get("request-store").clear();
1296
+ registry.get("fetch-store").clear();
1297
+ registry.get("log-store").clear();
1298
+ registry.get("error-store").clear();
1299
+ registry.get("query-store").clear();
1300
+ registry.get("metrics-store").reset();
1301
+ if (registry.has("finding-store")) registry.get("finding-store").clear();
1302
+ registry.get("event-bus").emit("store:cleared", void 0);
1303
+ sendJson(req, res, 200, { cleared: true });
1304
+ };
1305
+ }
1306
+ function createFetchesHandler(registry) {
1307
+ return (req, res) => handleTelemetryGet(req, res, registry.get("fetch-store"));
1308
+ }
1309
+ function createLogsHandler(registry) {
1310
+ return (req, res) => handleTelemetryGet(req, res, registry.get("log-store"));
1311
+ }
1312
+ function createErrorsHandler(registry) {
1313
+ return (req, res) => handleTelemetryGet(req, res, registry.get("error-store"));
1314
+ }
1315
+ function createQueriesHandler(registry) {
1316
+ return (req, res) => handleTelemetryGet(req, res, registry.get("query-store"));
1317
+ }
1318
+ var init_handlers = __esm({
1319
+ "src/dashboard/api/handlers.ts"() {
1320
+ "use strict";
1321
+ init_group();
1322
+ init_constants();
1323
+ init_shared2();
1292
1324
  }
1293
1325
  });
1294
1326
 
1295
- // src/store/query-store.ts
1296
- var QueryStore, defaultQueryStore;
1297
- var init_query_store = __esm({
1298
- "src/store/query-store.ts"() {
1327
+ // src/dashboard/api/ingest.ts
1328
+ function isBrakitBatch(msg) {
1329
+ return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
1330
+ }
1331
+ function isSDKPayload(msg) {
1332
+ return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
1333
+ }
1334
+ function createIngestHandler(registry) {
1335
+ const routeEvent = (event) => {
1336
+ switch (event.type) {
1337
+ case "fetch":
1338
+ registry.get("fetch-store").add(event.data);
1339
+ break;
1340
+ case "log":
1341
+ registry.get("log-store").add(event.data);
1342
+ break;
1343
+ case "error":
1344
+ registry.get("error-store").add(event.data);
1345
+ break;
1346
+ case "query":
1347
+ registry.get("query-store").add(event.data);
1348
+ break;
1349
+ }
1350
+ };
1351
+ const routeSDKEvent = (event) => {
1352
+ const ts = event.timestamp || Date.now();
1353
+ const parentRequestId = event.requestId ?? null;
1354
+ switch (event.type) {
1355
+ case "db.query":
1356
+ registry.get("query-store").add({
1357
+ driver: event.data.source ?? "sdk",
1358
+ source: event.data.source ?? "sdk",
1359
+ sql: event.data.sql,
1360
+ model: event.data.model,
1361
+ operation: event.data.operation,
1362
+ normalizedOp: event.data.normalizedOp ?? event.data.operation ?? "OTHER",
1363
+ table: event.data.table ?? "",
1364
+ durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1365
+ rowCount: event.data.rowCount,
1366
+ parentRequestId,
1367
+ timestamp: ts
1368
+ });
1369
+ break;
1370
+ case "fetch":
1371
+ registry.get("fetch-store").add({
1372
+ url: event.data.url ?? "",
1373
+ method: event.data.method ?? "GET",
1374
+ statusCode: event.data.statusCode ?? 0,
1375
+ durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1376
+ parentRequestId,
1377
+ timestamp: ts
1378
+ });
1379
+ break;
1380
+ case "log":
1381
+ registry.get("log-store").add({
1382
+ level: event.data.level ?? "log",
1383
+ message: event.data.message ?? "",
1384
+ parentRequestId,
1385
+ timestamp: ts
1386
+ });
1387
+ break;
1388
+ case "error":
1389
+ registry.get("error-store").add({
1390
+ name: event.data.name ?? "Error",
1391
+ message: event.data.message ?? "",
1392
+ stack: event.data.stack ?? "",
1393
+ parentRequestId,
1394
+ timestamp: ts
1395
+ });
1396
+ break;
1397
+ case "auth.check":
1398
+ registry.get("log-store").add({
1399
+ level: "info",
1400
+ message: `[auth] ${event.data.provider ?? "unknown"}: ${event.data.result ?? "check"}`,
1401
+ parentRequestId,
1402
+ timestamp: ts
1403
+ });
1404
+ break;
1405
+ }
1406
+ };
1407
+ return (req, res) => {
1408
+ if (req.method !== "POST") {
1409
+ sendJson(req, res, 405, { error: "Method not allowed" });
1410
+ return;
1411
+ }
1412
+ const chunks = [];
1413
+ let totalSize = 0;
1414
+ req.on("data", (chunk) => {
1415
+ totalSize += chunk.length;
1416
+ if (totalSize > MAX_INGEST_BYTES) {
1417
+ sendJson(req, res, 413, { error: "Payload too large" });
1418
+ req.destroy();
1419
+ return;
1420
+ }
1421
+ chunks.push(chunk);
1422
+ });
1423
+ req.on("end", () => {
1424
+ if (totalSize > MAX_INGEST_BYTES) return;
1425
+ try {
1426
+ const body = JSON.parse(Buffer.concat(chunks).toString());
1427
+ if (isSDKPayload(body)) {
1428
+ for (const event of body.events) {
1429
+ routeSDKEvent(event);
1430
+ }
1431
+ res.writeHead(204);
1432
+ res.end();
1433
+ return;
1434
+ }
1435
+ if (isBrakitBatch(body)) {
1436
+ for (const event of body.events) {
1437
+ routeEvent(event);
1438
+ }
1439
+ res.writeHead(204);
1440
+ res.end();
1441
+ return;
1442
+ }
1443
+ sendJson(req, res, 400, { error: "Invalid batch" });
1444
+ } catch {
1445
+ sendJson(req, res, 400, { error: "Invalid JSON" });
1446
+ }
1447
+ });
1448
+ };
1449
+ }
1450
+ var init_ingest = __esm({
1451
+ "src/dashboard/api/ingest.ts"() {
1299
1452
  "use strict";
1300
- init_telemetry_store();
1301
- QueryStore = class extends TelemetryStore {
1302
- };
1303
- defaultQueryStore = new QueryStore();
1453
+ init_limits();
1454
+ init_shared2();
1304
1455
  }
1305
1456
  });
1306
1457
 
1307
- // src/utils/math.ts
1308
- function percentile(values, p) {
1309
- if (values.length === 0) return 0;
1310
- const sorted = [...values].sort((a, b) => a - b);
1311
- const idx = Math.ceil(p * sorted.length) - 1;
1312
- return Math.round(sorted[Math.max(0, idx)]);
1458
+ // src/dashboard/api/metrics.ts
1459
+ function createMetricsHandler(metricsStore) {
1460
+ return (req, res) => {
1461
+ if (!requireGet(req, res)) return;
1462
+ const url = new URL(req.url ?? "/", "http://localhost");
1463
+ const endpoint = url.searchParams.get("endpoint");
1464
+ if (endpoint) {
1465
+ const ep = metricsStore.getEndpoint(endpoint);
1466
+ sendJson(req, res, 200, { endpoints: ep ? [ep] : [] });
1467
+ return;
1468
+ }
1469
+ sendJson(req, res, 200, { endpoints: metricsStore.getAll() });
1470
+ };
1313
1471
  }
1314
- var init_math = __esm({
1315
- "src/utils/math.ts"() {
1472
+ var init_metrics2 = __esm({
1473
+ "src/dashboard/api/metrics.ts"() {
1316
1474
  "use strict";
1475
+ init_shared2();
1317
1476
  }
1318
1477
  });
1319
1478
 
1320
- // src/utils/endpoint.ts
1321
- function getEndpointKey(method, path) {
1322
- return `${method} ${path}`;
1479
+ // src/dashboard/api/metrics-live.ts
1480
+ function createLiveMetricsHandler(metricsStore) {
1481
+ return (req, res) => {
1482
+ if (!requireGet(req, res)) return;
1483
+ sendJson(req, res, 200, { endpoints: metricsStore.getLiveEndpoints() });
1484
+ };
1323
1485
  }
1324
- var init_endpoint = __esm({
1325
- "src/utils/endpoint.ts"() {
1486
+ var init_metrics_live = __esm({
1487
+ "src/dashboard/api/metrics-live.ts"() {
1326
1488
  "use strict";
1489
+ init_shared2();
1327
1490
  }
1328
1491
  });
1329
1492
 
1330
- // src/store/metrics/metrics-store.ts
1331
- import { randomUUID as randomUUID4 } from "crypto";
1332
- function createAccumulator() {
1333
- return {
1334
- durations: [],
1335
- queryCounts: [],
1336
- errorCount: 0,
1337
- totalDurationSum: 0,
1338
- totalRequestCount: 0,
1339
- totalErrorCount: 0,
1340
- totalQuerySum: 0,
1341
- totalQueryTimeMs: 0,
1342
- totalFetchTimeMs: 0
1493
+ // src/dashboard/api/activity.ts
1494
+ function createActivityHandler(registry) {
1495
+ return (req, res) => {
1496
+ if (!requireGet(req, res)) return;
1497
+ try {
1498
+ const url = new URL(req.url ?? "/", "http://localhost");
1499
+ const requestId = url.searchParams.get("requestId");
1500
+ if (!requestId) {
1501
+ sendJson(req, res, 400, { error: "requestId parameter required" });
1502
+ return;
1503
+ }
1504
+ const fetches = registry.get("fetch-store").getByRequest(requestId);
1505
+ const logs = registry.get("log-store").getByRequest(requestId);
1506
+ const errors = registry.get("error-store").getByRequest(requestId);
1507
+ const queries = registry.get("query-store").getByRequest(requestId);
1508
+ const timeline = [];
1509
+ for (const f of fetches)
1510
+ timeline.push({ type: "fetch", timestamp: f.timestamp, data: { ...f } });
1511
+ for (const l of logs)
1512
+ timeline.push({ type: "log", timestamp: l.timestamp, data: { ...l } });
1513
+ for (const e of errors)
1514
+ timeline.push({ type: "error", timestamp: e.timestamp, data: { ...e } });
1515
+ for (const q of queries)
1516
+ timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
1517
+ timeline.sort((a, b) => a.timestamp - b.timestamp);
1518
+ sendJson(req, res, 200, {
1519
+ requestId,
1520
+ total: timeline.length,
1521
+ timeline,
1522
+ counts: {
1523
+ fetches: fetches.length,
1524
+ logs: logs.length,
1525
+ errors: errors.length,
1526
+ queries: queries.length
1527
+ }
1528
+ });
1529
+ } catch (err) {
1530
+ console.error("[brakit] activity handler error:", err);
1531
+ if (!res.headersSent) {
1532
+ sendJson(req, res, 500, { error: "Internal error" });
1533
+ }
1534
+ }
1343
1535
  };
1344
1536
  }
1345
- var MetricsStore;
1346
- var init_metrics_store = __esm({
1347
- "src/store/metrics/metrics-store.ts"() {
1537
+ var init_activity = __esm({
1538
+ "src/dashboard/api/activity.ts"() {
1348
1539
  "use strict";
1349
- init_constants();
1350
- init_math();
1351
- init_endpoint();
1352
- MetricsStore = class {
1353
- constructor(persistence) {
1354
- this.persistence = persistence;
1355
- this.data = persistence.load();
1356
- for (const ep of this.data.endpoints) {
1357
- this.endpointIndex.set(ep.endpoint, ep);
1358
- }
1359
- }
1360
- data;
1361
- endpointIndex = /* @__PURE__ */ new Map();
1362
- sessionId = randomUUID4();
1363
- sessionStart = Date.now();
1364
- flushTimer = null;
1365
- accumulators = /* @__PURE__ */ new Map();
1366
- pendingPoints = /* @__PURE__ */ new Map();
1367
- start() {
1368
- this.flushTimer = setInterval(
1369
- () => this.flush(),
1370
- METRICS_FLUSH_INTERVAL_MS
1371
- );
1372
- this.flushTimer.unref();
1540
+ init_shared2();
1541
+ }
1542
+ });
1543
+
1544
+ // src/dashboard/api/index.ts
1545
+ var init_api = __esm({
1546
+ "src/dashboard/api/index.ts"() {
1547
+ "use strict";
1548
+ init_handlers();
1549
+ init_ingest();
1550
+ init_metrics2();
1551
+ init_metrics_live();
1552
+ init_activity();
1553
+ }
1554
+ });
1555
+
1556
+ // src/dashboard/api/insights.ts
1557
+ function createInsightsHandler(engine) {
1558
+ return (req, res) => {
1559
+ if (!requireGet(req, res)) return;
1560
+ sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
1561
+ };
1562
+ }
1563
+ function createSecurityHandler(engine) {
1564
+ return (req, res) => {
1565
+ if (!requireGet(req, res)) return;
1566
+ sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
1567
+ };
1568
+ }
1569
+ var init_insights = __esm({
1570
+ "src/dashboard/api/insights.ts"() {
1571
+ "use strict";
1572
+ init_shared2();
1573
+ }
1574
+ });
1575
+
1576
+ // src/dashboard/api/findings.ts
1577
+ function createFindingsHandler(findingStore) {
1578
+ return (req, res) => {
1579
+ if (!requireGet(req, res)) return;
1580
+ const url = new URL(req.url ?? "/", "http://localhost");
1581
+ const stateParam = url.searchParams.get("state");
1582
+ let findings;
1583
+ if (stateParam && VALID_STATES.has(stateParam)) {
1584
+ findings = findingStore.getByState(stateParam);
1585
+ } else {
1586
+ findings = findingStore.getAll();
1587
+ }
1588
+ sendJson(req, res, 200, {
1589
+ total: findings.length,
1590
+ findings
1591
+ });
1592
+ };
1593
+ }
1594
+ var VALID_STATES;
1595
+ var init_findings = __esm({
1596
+ "src/dashboard/api/findings.ts"() {
1597
+ "use strict";
1598
+ init_shared2();
1599
+ VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
1600
+ }
1601
+ });
1602
+
1603
+ // src/core/disposable.ts
1604
+ var SubscriptionBag;
1605
+ var init_disposable = __esm({
1606
+ "src/core/disposable.ts"() {
1607
+ "use strict";
1608
+ SubscriptionBag = class {
1609
+ items = [];
1610
+ add(teardown) {
1611
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
1373
1612
  }
1374
- stop() {
1375
- if (this.flushTimer) {
1376
- clearInterval(this.flushTimer);
1377
- this.flushTimer = null;
1378
- }
1379
- this.flush(true);
1380
- }
1381
- recordRequest(req, metrics) {
1382
- if (req.isStatic) return;
1383
- const key = getEndpointKey(req.method, req.path);
1384
- let acc = this.accumulators.get(key);
1385
- if (!acc) {
1386
- acc = createAccumulator();
1387
- this.accumulators.set(key, acc);
1388
- }
1389
- acc.durations.push(req.durationMs);
1390
- acc.queryCounts.push(metrics.queryCount);
1391
- if (req.statusCode >= 400) acc.errorCount++;
1392
- acc.totalDurationSum += req.durationMs;
1393
- acc.totalRequestCount++;
1394
- acc.totalQuerySum += metrics.queryCount;
1395
- acc.totalQueryTimeMs += metrics.queryTimeMs;
1396
- acc.totalFetchTimeMs += metrics.fetchTimeMs;
1397
- if (req.statusCode >= 400) acc.totalErrorCount++;
1398
- const timestamp = Math.round(
1399
- Date.now() - (performance.now() - req.startedAt)
1400
- );
1401
- const point = {
1402
- timestamp,
1403
- durationMs: req.durationMs,
1404
- statusCode: req.statusCode,
1405
- queryCount: metrics.queryCount,
1406
- queryTimeMs: metrics.queryTimeMs,
1407
- fetchTimeMs: metrics.fetchTimeMs
1408
- };
1409
- let pending2 = this.pendingPoints.get(key);
1410
- if (!pending2) {
1411
- pending2 = [];
1412
- this.pendingPoints.set(key, pending2);
1413
- }
1414
- pending2.push(point);
1415
- }
1416
- getAll() {
1417
- return this.data.endpoints;
1418
- }
1419
- getEndpoint(endpoint) {
1420
- return this.endpointIndex.get(endpoint);
1421
- }
1422
- getLiveEndpoints() {
1423
- const merged = /* @__PURE__ */ new Map();
1424
- for (const ep of this.data.endpoints) {
1425
- if (ep.dataPoints && ep.dataPoints.length > 0) {
1426
- merged.set(ep.endpoint, ep.dataPoints);
1427
- }
1428
- }
1429
- for (const [endpoint, points] of this.pendingPoints) {
1430
- const existing = merged.get(endpoint);
1431
- merged.set(endpoint, existing ? existing.concat(points) : points);
1432
- }
1433
- const endpoints = [];
1434
- for (const [endpoint, requests] of merged) {
1435
- if (requests.length === 0) continue;
1436
- const durations = requests.map((r) => r.durationMs);
1437
- const errors = requests.filter((r) => r.statusCode >= 400).length;
1438
- const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
1439
- const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
1440
- const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
1441
- const n = requests.length;
1442
- const avgDurationMs = Math.round(durations.reduce((s, d) => s + d, 0) / n);
1443
- const avgQueryTimeMs = Math.round(totalQueryTime / n);
1444
- const avgFetchTimeMs = Math.round(totalFetchTime / n);
1445
- endpoints.push({
1446
- endpoint,
1447
- requests,
1448
- summary: {
1449
- p95Ms: percentile(durations, 0.95),
1450
- errorRate: errors / n,
1451
- avgQueryCount: Math.round(totalQueries / n),
1452
- totalRequests: n,
1453
- avgQueryTimeMs,
1454
- avgFetchTimeMs,
1455
- avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
1456
- }
1457
- });
1458
- }
1459
- endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
1460
- return endpoints;
1461
- }
1462
- reset() {
1463
- this.data = { version: 1, endpoints: [] };
1464
- this.endpointIndex.clear();
1465
- this.accumulators.clear();
1466
- this.pendingPoints.clear();
1467
- this.persistence.remove();
1468
- }
1469
- flush(sync = false) {
1470
- for (const [endpoint, acc] of this.accumulators) {
1471
- if (acc.durations.length === 0) continue;
1472
- const n = acc.totalRequestCount;
1473
- const session = {
1474
- sessionId: this.sessionId,
1475
- startedAt: this.sessionStart,
1476
- avgDurationMs: Math.round(acc.totalDurationSum / n),
1477
- p95DurationMs: percentile(acc.durations, 0.95),
1478
- requestCount: n,
1479
- errorCount: acc.totalErrorCount,
1480
- avgQueryCount: n > 0 ? Math.round(acc.totalQuerySum / n) : 0,
1481
- avgQueryTimeMs: n > 0 ? Math.round(acc.totalQueryTimeMs / n) : 0,
1482
- avgFetchTimeMs: n > 0 ? Math.round(acc.totalFetchTimeMs / n) : 0
1483
- };
1484
- const epMetrics = this.getOrCreateEndpoint(endpoint);
1485
- const existingIdx = epMetrics.sessions.findIndex(
1486
- (s) => s.sessionId === this.sessionId
1487
- );
1488
- if (existingIdx !== -1) {
1489
- epMetrics.sessions[existingIdx] = session;
1490
- } else {
1491
- epMetrics.sessions.push(session);
1492
- }
1493
- if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
1494
- epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
1495
- }
1496
- acc.durations.length = 0;
1497
- acc.queryCounts.length = 0;
1498
- acc.errorCount = 0;
1499
- }
1500
- for (const [endpoint, points] of this.pendingPoints) {
1501
- if (points.length === 0) continue;
1502
- const epMetrics = this.getOrCreateEndpoint(endpoint);
1503
- const existing = epMetrics.dataPoints ?? [];
1504
- epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
1505
- }
1506
- this.pendingPoints.clear();
1507
- if (sync) {
1508
- this.persistence.saveSync(this.data);
1509
- } else {
1510
- this.persistence.save(this.data);
1511
- }
1512
- }
1513
- getOrCreateEndpoint(endpoint) {
1514
- let ep = this.endpointIndex.get(endpoint);
1515
- if (!ep) {
1516
- ep = { endpoint, sessions: [] };
1517
- this.data.endpoints.push(ep);
1518
- this.endpointIndex.set(endpoint, ep);
1519
- }
1520
- return ep;
1613
+ dispose() {
1614
+ for (const d of this.items) d.dispose();
1615
+ this.items.length = 0;
1521
1616
  }
1522
1617
  };
1523
1618
  }
1524
1619
  });
1525
1620
 
1526
- // src/utils/fs.ts
1527
- import { access } from "fs/promises";
1528
- import { existsSync, readFileSync, writeFileSync } from "fs";
1529
- import { resolve } from "path";
1530
- function ensureGitignore(dir, entry) {
1531
- try {
1532
- const gitignorePath = resolve(dir, "../.gitignore");
1533
- if (existsSync(gitignorePath)) {
1534
- const content = readFileSync(gitignorePath, "utf-8");
1535
- if (content.split("\n").some((l) => l.trim() === entry)) return;
1536
- writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
1537
- } else {
1538
- writeFileSync(gitignorePath, entry + "\n");
1539
- }
1540
- } catch {
1541
- }
1542
- }
1543
- var init_fs = __esm({
1544
- "src/utils/fs.ts"() {
1545
- "use strict";
1546
- }
1547
- });
1621
+ // src/dashboard/sse.ts
1622
+ function createSSEHandler(registry) {
1623
+ return (req, res) => {
1624
+ res.writeHead(200, {
1625
+ "content-type": "text/event-stream",
1626
+ "cache-control": "no-cache",
1627
+ connection: "keep-alive",
1628
+ "access-control-allow-origin": "*"
1629
+ });
1630
+ res.write(":ok\n\n");
1631
+ const writeEvent = (eventType, data) => {
1632
+ if (res.destroyed) return;
1633
+ if (eventType) {
1634
+ res.write(`event: ${eventType}
1635
+ data: ${data}
1548
1636
 
1549
- // src/store/metrics/persistence.ts
1550
- import {
1551
- readFileSync as readFileSync2,
1552
- writeFileSync as writeFileSync2,
1553
- mkdirSync as mkdirSync2,
1554
- existsSync as existsSync2,
1555
- unlinkSync,
1556
- renameSync
1557
- } from "fs";
1558
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1559
- import { resolve as resolve2 } from "path";
1560
- var FileMetricsPersistence;
1561
- var init_persistence = __esm({
1562
- "src/store/metrics/persistence.ts"() {
1563
- "use strict";
1564
- init_constants();
1565
- init_fs();
1566
- FileMetricsPersistence = class {
1567
- metricsDir;
1568
- metricsPath;
1569
- tmpPath;
1570
- writing = false;
1571
- pendingData = null;
1572
- constructor(rootDir) {
1573
- this.metricsDir = resolve2(rootDir, METRICS_DIR);
1574
- this.metricsPath = resolve2(rootDir, METRICS_FILE);
1575
- this.tmpPath = this.metricsPath + ".tmp";
1576
- }
1577
- load() {
1578
- try {
1579
- if (existsSync2(this.metricsPath)) {
1580
- const raw = readFileSync2(this.metricsPath, "utf-8");
1581
- const parsed = JSON.parse(raw);
1582
- if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
1583
- return parsed;
1584
- }
1585
- }
1586
- } catch (err) {
1587
- process.stderr.write(`[brakit] failed to load metrics: ${err.message}
1588
- `);
1589
- }
1590
- return { version: 1, endpoints: [] };
1591
- }
1592
- save(data) {
1593
- if (this.writing) {
1594
- this.pendingData = data;
1595
- return;
1596
- }
1597
- this.writeAsync(data);
1598
- }
1599
- saveSync(data) {
1600
- try {
1601
- this.ensureDir();
1602
- writeFileSync2(this.tmpPath, JSON.stringify(data));
1603
- renameSync(this.tmpPath, this.metricsPath);
1604
- } catch (err) {
1605
- process.stderr.write(`[brakit] failed to save metrics: ${err.message}
1606
1637
  `);
1607
- }
1608
- }
1609
- remove() {
1610
- try {
1611
- if (existsSync2(this.metricsPath)) {
1612
- unlinkSync(this.metricsPath);
1613
- }
1614
- } catch {
1615
- }
1616
- }
1617
- async writeAsync(data) {
1618
- this.writing = true;
1619
- try {
1620
- if (!existsSync2(this.metricsDir)) {
1621
- await mkdir(this.metricsDir, { recursive: true });
1622
- ensureGitignore(this.metricsDir, METRICS_DIR);
1623
- }
1624
- await writeFile2(this.tmpPath, JSON.stringify(data));
1625
- await rename(this.tmpPath, this.metricsPath);
1626
- } catch (err) {
1627
- process.stderr.write(`[brakit] failed to save metrics: ${err.message}
1638
+ } else {
1639
+ res.write(`data: ${data}
1640
+
1628
1641
  `);
1629
- } finally {
1630
- this.writing = false;
1631
- if (this.pendingData) {
1632
- const next = this.pendingData;
1633
- this.pendingData = null;
1634
- this.writeAsync(next);
1635
- }
1636
- }
1637
- }
1638
- ensureDir() {
1639
- if (!existsSync2(this.metricsDir)) {
1640
- mkdirSync2(this.metricsDir, { recursive: true });
1641
- ensureGitignore(this.metricsDir, METRICS_DIR);
1642
- }
1643
1642
  }
1644
1643
  };
1644
+ const bus = registry.get("event-bus");
1645
+ const subs = new SubscriptionBag();
1646
+ subs.add(bus.on("request:completed", (r) => writeEvent(null, JSON.stringify(r))));
1647
+ subs.add(bus.on("telemetry:fetch", (e) => writeEvent("fetch", JSON.stringify(e))));
1648
+ subs.add(bus.on("telemetry:log", (e) => writeEvent("log", JSON.stringify(e))));
1649
+ subs.add(bus.on("telemetry:error", (e) => writeEvent("error_event", JSON.stringify(e))));
1650
+ subs.add(bus.on("telemetry:query", (e) => writeEvent("query", JSON.stringify(e))));
1651
+ subs.add(bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
1652
+ writeEvent("insights", JSON.stringify(statefulInsights));
1653
+ writeEvent("security", JSON.stringify(statefulFindings));
1654
+ }));
1655
+ const heartbeat = setInterval(() => {
1656
+ if (res.destroyed) {
1657
+ clearInterval(heartbeat);
1658
+ return;
1659
+ }
1660
+ res.write(":heartbeat\n\n");
1661
+ }, SSE_HEARTBEAT_INTERVAL_MS);
1662
+ req.on("close", () => {
1663
+ clearInterval(heartbeat);
1664
+ subs.dispose();
1665
+ });
1666
+ };
1667
+ }
1668
+ var init_sse = __esm({
1669
+ "src/dashboard/sse.ts"() {
1670
+ "use strict";
1671
+ init_disposable();
1672
+ init_constants();
1645
1673
  }
1646
1674
  });
1647
1675
 
1648
- // src/store/index.ts
1649
- var init_store = __esm({
1650
- "src/store/index.ts"() {
1651
- "use strict";
1652
- init_request_store();
1653
- init_telemetry_store();
1654
- init_fetch_store();
1655
- init_log_store();
1656
- init_error_store();
1657
- init_query_store();
1658
- init_metrics_store();
1659
- init_persistence();
1660
- }
1661
- });
1662
-
1663
- // src/dashboard/api/shared.ts
1664
- function maskSensitiveHeaders(headers) {
1665
- const masked = {};
1666
- for (const [key, value] of Object.entries(headers)) {
1667
- if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1668
- const s = String(value);
1669
- masked[key] = s.length <= 8 ? "****" : s.slice(0, 4) + "..." + s.slice(-4);
1670
- } else {
1671
- masked[key] = value;
1672
- }
1673
- }
1674
- return masked;
1675
- }
1676
- function getCorsOrigin(req) {
1677
- const origin = req.headers.origin ?? "";
1678
- try {
1679
- const url = new URL(origin);
1680
- if (LOCALHOST_HOSTNAMES.has(url.hostname)) {
1681
- return origin;
1682
- }
1683
- } catch {
1684
- }
1685
- return "";
1686
- }
1687
- function getJsonHeaders(req) {
1688
- const corsOrigin = getCorsOrigin(req);
1689
- const headers = {
1690
- "content-type": "application/json",
1691
- "cache-control": "no-cache"
1692
- };
1693
- if (corsOrigin) {
1694
- headers["access-control-allow-origin"] = corsOrigin;
1695
- }
1696
- return headers;
1697
- }
1698
- function sendJson(req, res, status, data) {
1699
- res.writeHead(status, getJsonHeaders(req));
1700
- res.end(JSON.stringify(data));
1701
- }
1702
- function requireGet(req, res) {
1703
- if (req.method !== "GET") {
1704
- sendJson(req, res, 405, { error: "Method not allowed" });
1705
- return false;
1706
- }
1707
- return true;
1708
- }
1709
- function handleTelemetryGet(req, res, store) {
1710
- if (!requireGet(req, res)) return;
1711
- const url = new URL(req.url ?? "/", "http://localhost");
1712
- const requestId = url.searchParams.get("requestId");
1713
- const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1714
- sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1676
+ // src/dashboard/styles/base.ts
1677
+ function getBaseStyles() {
1678
+ return `
1679
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
1680
+ :root{
1681
+ --bg:#ffffff;--bg-sidebar:#f8f8fa;--bg-card:#ffffff;--bg-hover:#f4f4f5;--bg-detail:#fafafa;
1682
+ --bg-active:#ede9fe;--bg-muted:#f4f4f5;
1683
+ --border:#e4e4e7;--border-light:#d4d4d8;--border-subtle:#f4f4f5;
1684
+ --text:#18181b;--text-dim:#52525b;--text-muted:#a1a1aa;
1685
+ --accent:#7c3aed;
1686
+ --green:#16a34a;
1687
+ --blue:#2563eb;
1688
+ --amber:#d97706;
1689
+ --red:#dc2626;
1690
+ --cyan:#0891b2;
1691
+ --green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
1692
+ --sidebar-width:232px;--header-height:52px;
1693
+ --radius:8px;--radius-sm:6px;
1694
+ --shadow-sm:0 1px 2px rgba(0,0,0,0.05);
1695
+ --shadow-md:0 1px 3px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.04);
1696
+ --shadow-lg:0 4px 12px rgba(0,0,0,0.08),0 1px 4px rgba(0,0,0,0.04);
1697
+ --breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
1698
+ --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
1699
+ --sans:Inter,system-ui,-apple-system,sans-serif;
1715
1700
  }
1716
- var SENSITIVE_HEADER_NAMES;
1717
- var init_shared2 = __esm({
1718
- "src/dashboard/api/shared.ts"() {
1719
- "use strict";
1720
- init_constants();
1721
- SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
1722
- "authorization",
1723
- "cookie",
1724
- "set-cookie",
1725
- "proxy-authorization",
1726
- "x-api-key",
1727
- "x-auth-token"
1728
- ]);
1729
- }
1730
- });
1701
+ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:15px;overflow:hidden;-webkit-font-smoothing:antialiased}
1731
1702
 
1732
- // src/dashboard/api/handlers.ts
1733
- function handleApiRequests(req, res) {
1734
- if (!requireGet(req, res)) return;
1735
- const url = new URL(req.url ?? "/", "http://localhost");
1736
- const method = url.searchParams.get("method");
1737
- const status = url.searchParams.get("status");
1738
- const search = url.searchParams.get("search");
1739
- const limit = parseInt(
1740
- url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT),
1741
- 10
1742
- );
1743
- const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
1744
- let results = [...getRequests()].reverse();
1745
- if (method) {
1746
- results = results.filter((r) => r.method === method.toUpperCase());
1747
- }
1748
- if (status) {
1749
- if (status.endsWith("xx")) {
1750
- const prefix = parseInt(status[0], 10);
1751
- results = results.filter(
1752
- (r) => Math.floor(r.statusCode / 100) === prefix
1753
- );
1754
- } else {
1755
- const code = parseInt(status, 10);
1756
- results = results.filter((r) => r.statusCode === code);
1757
- }
1758
- }
1759
- if (search) {
1760
- const lower = search.toLowerCase();
1761
- results = results.filter(
1762
- (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1763
- );
1764
- }
1765
- const total = results.length;
1766
- results = results.slice(offset, offset + limit);
1767
- const sanitized = results.map(sanitizeRequest);
1768
- sendJson(req, res, 200, { total, requests: sanitized });
1769
- }
1770
- function sanitizeRequest(r) {
1771
- return {
1772
- ...r,
1773
- headers: maskSensitiveHeaders(r.headers),
1774
- responseHeaders: maskSensitiveHeaders(r.responseHeaders)
1775
- };
1776
- }
1777
- function handleApiFlows(req, res) {
1778
- if (!requireGet(req, res)) return;
1779
- const flows = groupRequestsIntoFlows(getRequests()).reverse().map((flow) => ({
1780
- ...flow,
1781
- requests: flow.requests.map(sanitizeRequest)
1782
- }));
1783
- sendJson(req, res, 200, { total: flows.length, flows });
1784
- }
1785
- function createClearHandler(metricsStore) {
1786
- return (req, res) => {
1787
- if (req.method !== "POST") {
1788
- sendJson(req, res, 405, { error: "Method not allowed" });
1789
- return;
1790
- }
1791
- clearRequests();
1792
- defaultFetchStore.clear();
1793
- defaultLogStore.clear();
1794
- defaultErrorStore.clear();
1795
- defaultQueryStore.clear();
1796
- metricsStore.reset();
1797
- sendJson(req, res, 200, { cleared: true });
1798
- };
1799
- }
1800
- function handleApiFetches(req, res) {
1801
- handleTelemetryGet(req, res, defaultFetchStore);
1802
- }
1803
- function handleApiLogs(req, res) {
1804
- handleTelemetryGet(req, res, defaultLogStore);
1805
- }
1806
- function handleApiErrors(req, res) {
1807
- handleTelemetryGet(req, res, defaultErrorStore);
1808
- }
1809
- function handleApiQueries(req, res) {
1810
- handleTelemetryGet(req, res, defaultQueryStore);
1703
+ /* Scrollbar */
1704
+ ::-webkit-scrollbar{width:8px}
1705
+ ::-webkit-scrollbar-track{background:transparent}
1706
+ ::-webkit-scrollbar-thumb{background:#d4d4d8;border-radius:4px}
1707
+ ::-webkit-scrollbar-thumb:hover{background:#a1a1aa}
1708
+
1709
+ /* Tooltip */
1710
+ .tooltip{position:relative}
1711
+ .tooltip::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:#ffffff;border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-size:11px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;box-shadow:var(--shadow-lg)}
1712
+ .tooltip:hover::after{opacity:1}
1713
+
1714
+ /* Toast */
1715
+ .toast{position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-8px);background:#f0fdf4;border:1px solid #86efac;color:#15803d;padding:12px 24px;border-radius:10px;font-size:13px;font-weight:500;opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:100;box-shadow:var(--shadow-lg)}
1716
+ .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
1717
+
1718
+ /* Empty */
1719
+ .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:400px;color:var(--text-muted);gap:12px}
1720
+ .empty-title{font-size:19px;font-weight:600;color:var(--text-dim)}
1721
+ .empty-sub{font-size:14px}
1722
+
1723
+ /* View toggle */
1724
+ .view-flows{display:block}.view-requests{display:none}
1725
+ `;
1811
1726
  }
1812
- var init_handlers = __esm({
1813
- "src/dashboard/api/handlers.ts"() {
1727
+ var init_base = __esm({
1728
+ "src/dashboard/styles/base.ts"() {
1814
1729
  "use strict";
1815
- init_request_log();
1816
- init_group();
1817
- init_store();
1818
- init_constants();
1819
- init_shared2();
1820
1730
  }
1821
1731
  });
1822
1732
 
1823
- // src/dashboard/api/ingest.ts
1824
- function isBrakitBatch(msg) {
1825
- return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
1826
- }
1827
- function routeEvent(event) {
1828
- switch (event.type) {
1829
- case "fetch":
1830
- defaultFetchStore.add(event.data);
1831
- break;
1832
- case "log":
1833
- defaultLogStore.add(event.data);
1834
- break;
1835
- case "error":
1836
- defaultErrorStore.add(event.data);
1837
- break;
1838
- case "query":
1839
- defaultQueryStore.add(event.data);
1840
- break;
1841
- }
1842
- }
1843
- function isSDKPayload(msg) {
1844
- return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
1845
- }
1846
- function routeSDKEvent(event) {
1847
- const ts = event.timestamp || Date.now();
1848
- const parentRequestId = event.requestId ?? null;
1849
- switch (event.type) {
1850
- case "db.query":
1851
- defaultQueryStore.add({
1852
- driver: event.data.source ?? "sdk",
1853
- source: event.data.source ?? "sdk",
1854
- sql: event.data.sql,
1855
- model: event.data.model,
1856
- operation: event.data.operation,
1857
- normalizedOp: event.data.normalizedOp ?? event.data.operation ?? "OTHER",
1858
- table: event.data.table ?? "",
1859
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1860
- rowCount: event.data.rowCount,
1861
- parentRequestId,
1862
- timestamp: ts
1863
- });
1864
- break;
1865
- case "fetch":
1866
- defaultFetchStore.add({
1867
- url: event.data.url ?? "",
1868
- method: event.data.method ?? "GET",
1869
- statusCode: event.data.statusCode ?? 0,
1870
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1871
- parentRequestId,
1872
- timestamp: ts
1873
- });
1874
- break;
1875
- case "log":
1876
- defaultLogStore.add({
1877
- level: event.data.level ?? "log",
1878
- message: event.data.message ?? "",
1879
- parentRequestId,
1880
- timestamp: ts
1881
- });
1882
- break;
1883
- case "error":
1884
- defaultErrorStore.add({
1885
- name: event.data.name ?? "Error",
1886
- message: event.data.message ?? "",
1887
- stack: event.data.stack ?? "",
1888
- parentRequestId,
1889
- timestamp: ts
1890
- });
1891
- break;
1892
- case "auth.check":
1893
- defaultLogStore.add({
1894
- level: "info",
1895
- message: `[auth] ${event.data.provider ?? "unknown"}: ${event.data.result ?? "check"}`,
1896
- parentRequestId,
1897
- timestamp: ts
1898
- });
1899
- break;
1900
- }
1901
- }
1902
- function handleApiIngest(req, res) {
1903
- if (req.method !== "POST") {
1904
- sendJson(req, res, 405, { error: "Method not allowed" });
1905
- return;
1906
- }
1907
- const MAX_INGEST_BYTES = 10 * 1024 * 1024;
1908
- const chunks = [];
1909
- let totalSize = 0;
1910
- req.on("data", (chunk) => {
1911
- totalSize += chunk.length;
1912
- if (totalSize > MAX_INGEST_BYTES) {
1913
- sendJson(req, res, 413, { error: "Payload too large" });
1914
- req.destroy();
1915
- return;
1916
- }
1917
- chunks.push(chunk);
1918
- });
1919
- req.on("end", () => {
1920
- if (totalSize > MAX_INGEST_BYTES) return;
1921
- try {
1922
- const body = JSON.parse(Buffer.concat(chunks).toString());
1923
- if (isSDKPayload(body)) {
1924
- for (const event of body.events) {
1925
- routeSDKEvent(event);
1926
- }
1927
- res.writeHead(204);
1928
- res.end();
1929
- return;
1930
- }
1931
- if (isBrakitBatch(body)) {
1932
- for (const event of body.events) {
1933
- routeEvent(event);
1934
- }
1935
- res.writeHead(204);
1936
- res.end();
1937
- return;
1938
- }
1939
- sendJson(req, res, 400, { error: "Invalid batch" });
1940
- } catch {
1941
- sendJson(req, res, 400, { error: "Invalid JSON" });
1942
- }
1943
- });
1733
+ // src/dashboard/styles/layout.ts
1734
+ function getLayoutStyles() {
1735
+ return `
1736
+ /* Layout */
1737
+ .app{display:grid;grid-template-columns:var(--sidebar-width) 1fr;height:100vh;overflow:hidden}
1738
+ .main-panel{display:flex;flex-direction:column;overflow:hidden}
1739
+
1740
+ /* Sidebar */
1741
+ .sidebar{background:var(--bg-sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}
1742
+ .sidebar-logo{padding:20px 24px 24px;border-bottom:1px solid var(--border-subtle)}
1743
+ .sidebar-logo .logo-text{font-weight:800;font-size:21px;color:var(--accent);letter-spacing:-.5px}
1744
+ .sidebar-logo .logo-version{font-weight:400;font-size:11px;color:var(--text-muted);margin-left:8px;letter-spacing:0}
1745
+ .sidebar-nav{padding:12px;flex:1}
1746
+ .sidebar-section{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);padding:16px 12px 8px}
1747
+ .sidebar-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius);color:var(--text-dim);font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
1748
+ .sidebar-item:hover{background:var(--bg-hover);color:var(--text)}
1749
+ .sidebar-item.active{background:var(--bg-active);color:var(--accent)}
1750
+ .sidebar-item .item-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;opacity:.5}
1751
+ .sidebar-item.active .item-icon{opacity:1}
1752
+ .sidebar-item:hover .item-icon{opacity:.8}
1753
+ .sidebar-item .item-label{flex:1}
1754
+ .sidebar-item .item-count{font-size:12px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;min-width:24px;text-align:center}
1755
+ .sidebar-item.disabled{opacity:.35;cursor:default;pointer-events:none}
1756
+ .sidebar-item .coming-soon{font-size:10px;color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;font-weight:600;letter-spacing:.3px}
1757
+ .sidebar-footer{padding:16px 24px;border-top:1px solid var(--border-subtle);font-size:12px;color:var(--text-muted);font-family:var(--mono)}
1758
+
1759
+ /* Header */
1760
+ .header{display:flex;align-items:center;gap:16px;padding:0 28px;height:var(--header-height);border-bottom:1px solid var(--border);background:var(--bg);flex-shrink:0;box-shadow:0 1px 0 rgba(0,0,0,0.03)}
1761
+ .header-left{display:flex;flex-direction:column;justify-content:center}
1762
+ .header-title{font-weight:600;font-size:17px;color:var(--text);letter-spacing:-.2px;line-height:1.2}
1763
+ .header-sub{font-size:11px;color:var(--text-muted);line-height:1.2}
1764
+ .header-right{margin-left:auto;display:flex;gap:10px;align-items:center}
1765
+
1766
+ /* Segmented control */
1767
+ .segmented-control{display:flex;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:3px;gap:2px}
1768
+ .segmented-btn{background:transparent;border:none;color:var(--text-muted);padding:6px 14px;font-size:13px;cursor:pointer;transition:all .15s;font-family:var(--sans);font-weight:500;border-radius:var(--radius-sm)}
1769
+ .segmented-btn:hover{color:var(--text)}
1770
+ .segmented-btn.active{background:#ffffff;color:var(--text);box-shadow:var(--shadow-sm)}
1771
+
1772
+ .btn{background:#ffffff;border:1px solid var(--border);color:var(--text-dim);padding:7px 14px;border-radius:var(--radius);font-size:13px;cursor:pointer;transition:all .15s;font-family:var(--sans);font-weight:500;box-shadow:var(--shadow-sm)}
1773
+ .btn:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-light)}
1774
+ .btn-danger:hover{border-color:rgba(220,38,38,.3);color:var(--red);background:rgba(220,38,38,.05)}
1775
+
1776
+ /* Content */
1777
+ .main-content{flex:1;overflow-y:auto}
1778
+
1779
+ /* Column headers */
1780
+ .col-header{display:flex;align-items:center;gap:16px;padding:8px 28px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);border-bottom:1px solid var(--border);background:var(--bg-sidebar);position:sticky;top:0;z-index:2;font-family:var(--mono)}
1781
+
1782
+ /* Footer */
1783
+ .footer{padding:10px 28px;border-top:1px solid var(--border);font-size:13px;color:var(--text-muted);display:flex;gap:24px;font-family:var(--mono);flex-shrink:0;background:var(--bg-sidebar)}
1784
+ .footer .error-count{color:var(--red)}
1785
+ `;
1944
1786
  }
1945
- var init_ingest = __esm({
1946
- "src/dashboard/api/ingest.ts"() {
1787
+ var init_layout = __esm({
1788
+ "src/dashboard/styles/layout.ts"() {
1947
1789
  "use strict";
1948
- init_store();
1949
- init_shared2();
1950
1790
  }
1951
1791
  });
1952
1792
 
1953
- // src/dashboard/api/metrics.ts
1954
- function createMetricsHandler(metricsStore) {
1955
- return (req, res) => {
1956
- if (!requireGet(req, res)) return;
1957
- const url = new URL(req.url ?? "/", "http://localhost");
1958
- const endpoint = url.searchParams.get("endpoint");
1959
- if (endpoint) {
1960
- const ep = metricsStore.getEndpoint(endpoint);
1961
- sendJson(req, res, 200, { endpoints: ep ? [ep] : [] });
1962
- return;
1963
- }
1964
- sendJson(req, res, 200, { endpoints: metricsStore.getAll() });
1965
- };
1966
- }
1967
- var init_metrics2 = __esm({
1968
- "src/dashboard/api/metrics.ts"() {
1969
- "use strict";
1970
- init_shared2();
1971
- }
1972
- });
1793
+ // src/dashboard/styles/flows.ts
1794
+ function getFlowStyles() {
1795
+ return `
1796
+ /* Flow rows */
1797
+ .flow-row{padding:12px 28px;border-bottom:1px solid var(--border-subtle);cursor:pointer;transition:background .1s}
1798
+ .flow-row:hover{background:var(--bg-hover)}
1799
+ .flow-row.expanded{background:var(--bg-muted)}
1800
+ .flow-summary-row{display:flex;align-items:center;gap:14px;font-size:14px}
1801
+ .flow-status-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
1802
+ .flow-status-dot.dot-clean{background:var(--green)}
1803
+ .flow-status-dot.dot-warn{background:var(--amber)}
1804
+ .flow-status-dot.dot-error{background:var(--red)}
1805
+ .flow-label{font-weight:500;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
1806
+ .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
1807
+ .flow-badge-pill{font-size:11px;flex-shrink:0;font-family:var(--mono);font-weight:600;padding:2px 10px;border-radius:10px;text-align:center}
1808
+ .flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
1809
+ .flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
1810
+ .flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
1811
+ .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
1973
1812
 
1974
- // src/dashboard/api/metrics-live.ts
1975
- function createLiveMetricsHandler(metricsStore) {
1976
- return (req, res) => {
1977
- if (!requireGet(req, res)) return;
1978
- sendJson(req, res, 200, { endpoints: metricsStore.getLiveEndpoints() });
1979
- };
1980
- }
1981
- var init_metrics_live = __esm({
1982
- "src/dashboard/api/metrics-live.ts"() {
1983
- "use strict";
1984
- init_shared2();
1985
- }
1986
- });
1813
+ /* Flow expand panel */
1814
+ .flow-expand{display:none;padding:12px 28px 16px;border-bottom:1px solid var(--border);background:var(--bg-detail)}
1815
+ .flow-expand.open{display:block}
1987
1816
 
1988
- // src/dashboard/api/activity.ts
1989
- function handleApiActivity(req, res) {
1990
- if (!requireGet(req, res)) return;
1991
- try {
1992
- const url = new URL(req.url ?? "/", "http://localhost");
1993
- const requestId = url.searchParams.get("requestId");
1994
- if (!requestId) {
1995
- sendJson(req, res, 400, { error: "requestId parameter required" });
1996
- return;
1997
- }
1998
- const fetches = defaultFetchStore.getByRequest(requestId);
1999
- const logs = defaultLogStore.getByRequest(requestId);
2000
- const errors = defaultErrorStore.getByRequest(requestId);
2001
- const queries = defaultQueryStore.getByRequest(requestId);
2002
- const timeline = [];
2003
- for (const f of fetches)
2004
- timeline.push({ type: "fetch", timestamp: f.timestamp, data: { ...f } });
2005
- for (const l of logs)
2006
- timeline.push({ type: "log", timestamp: l.timestamp, data: { ...l } });
2007
- for (const e of errors)
2008
- timeline.push({ type: "error", timestamp: e.timestamp, data: { ...e } });
2009
- for (const q of queries)
2010
- timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
2011
- timeline.sort((a, b) => a.timestamp - b.timestamp);
2012
- sendJson(req, res, 200, {
2013
- requestId,
2014
- total: timeline.length,
2015
- timeline,
2016
- counts: {
2017
- fetches: fetches.length,
2018
- logs: logs.length,
2019
- errors: errors.length,
2020
- queries: queries.length
2021
- }
2022
- });
2023
- } catch (err) {
2024
- console.error("[brakit] activity handler error:", err);
2025
- if (!res.headersSent) {
2026
- sendJson(req, res, 500, { error: "Internal error" });
2027
- }
2028
- }
2029
- }
2030
- var init_activity = __esm({
2031
- "src/dashboard/api/activity.ts"() {
2032
- "use strict";
2033
- init_store();
2034
- init_shared2();
2035
- }
2036
- });
1817
+ /* Request cards in expanded flow */
1818
+ .traffic-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;margin-bottom:10px;box-shadow:var(--shadow-sm)}
1819
+ .traffic-card:last-child{margin-bottom:0}
2037
1820
 
2038
- // src/dashboard/api/index.ts
2039
- var init_api = __esm({
2040
- "src/dashboard/api/index.ts"() {
2041
- "use strict";
2042
- init_handlers();
2043
- init_ingest();
2044
- init_metrics2();
2045
- init_metrics_live();
2046
- init_activity();
2047
- }
2048
- });
1821
+ /* Simple mode traffic */
1822
+ .flow-traffic{padding:0;font-family:var(--mono);font-size:13px}
1823
+ .traffic-card-header{display:flex;align-items:center;gap:10px;margin-bottom:0}
1824
+ .traffic-card-header.has-details{margin-bottom:10px}
2049
1825
 
2050
- // src/dashboard/api/insights.ts
2051
- function createInsightsHandler(engine) {
2052
- return (req, res) => {
2053
- if (!requireGet(req, res)) return;
2054
- sendJson(req, res, 200, { insights: engine.getInsights() });
2055
- };
2056
- }
2057
- function createSecurityHandler(engine) {
2058
- return (req, res) => {
2059
- if (!requireGet(req, res)) return;
2060
- sendJson(req, res, 200, { findings: engine.getFindings() });
2061
- };
2062
- }
2063
- var init_insights = __esm({
2064
- "src/dashboard/api/insights.ts"() {
2065
- "use strict";
2066
- init_shared2();
2067
- }
2068
- });
1826
+ /* Method badges */
1827
+ .method-badge{display:inline-flex;align-items:center;justify-content:center;padding:3px 8px;border-radius:5px;font-size:10px;font-weight:700;font-family:var(--mono);letter-spacing:.3px;flex-shrink:0}
1828
+ .method-badge-GET{background:var(--green-bg);color:var(--green)}
1829
+ .method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
1830
+ .method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
1831
+ .method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
1832
+ .method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
2069
1833
 
2070
- // src/dashboard/api/findings.ts
2071
- function createFindingsHandler(findingStore) {
2072
- return (req, res) => {
2073
- if (!requireGet(req, res)) return;
2074
- const url = new URL(req.url ?? "/", "http://localhost");
2075
- const stateParam = url.searchParams.get("state");
2076
- let findings;
2077
- if (stateParam && VALID_STATES.has(stateParam)) {
2078
- findings = findingStore.getByState(stateParam);
2079
- } else {
2080
- findings = findingStore.getAll();
2081
- }
2082
- sendJson(req, res, 200, {
2083
- total: findings.length,
2084
- findings
2085
- });
2086
- };
2087
- }
2088
- var VALID_STATES;
2089
- var init_findings = __esm({
2090
- "src/dashboard/api/findings.ts"() {
2091
- "use strict";
2092
- init_shared2();
2093
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
2094
- }
2095
- });
1834
+ /* Status pills */
1835
+ .status-pill{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:600;font-family:var(--mono);flex-shrink:0}
1836
+ .status-pill-2xx{background:var(--green-bg);color:var(--green)}
1837
+ .status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
1838
+ .status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
1839
+ .status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
2096
1840
 
2097
- // src/dashboard/sse.ts
2098
- function createSSEHandler(engine) {
2099
- return (req, res) => {
2100
- res.writeHead(200, {
2101
- "content-type": "text/event-stream",
2102
- "cache-control": "no-cache",
2103
- connection: "keep-alive",
2104
- "access-control-allow-origin": "*"
2105
- });
2106
- res.write(":ok\n\n");
2107
- const writeEvent = (eventType, data) => {
2108
- if (res.destroyed) return;
2109
- if (eventType) {
2110
- res.write(`event: ${eventType}
2111
- data: ${data}
1841
+ .traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
1842
+ .traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
1843
+ .traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
1844
+ .traffic-card-size{color:var(--text-muted);font-size:11px;flex-shrink:0}
1845
+ .traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
2112
1846
 
2113
- `);
2114
- } else {
2115
- res.write(`data: ${data}
1847
+ /* Body toggles */
1848
+ .traffic-body{padding:0;margin-top:8px}
1849
+ .traffic-body-toggle{font-size:11px;color:var(--text-dim);display:inline-flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-muted);font-family:var(--mono);letter-spacing:.3px;transition:all .15s;margin-right:6px;margin-bottom:4px}
1850
+ .traffic-body-toggle:hover{border-color:var(--border-light);color:var(--text);background:var(--bg-hover)}
1851
+ .traffic-body-toggle .arrow-out{color:var(--blue)}
1852
+ .traffic-body-toggle .arrow-in{color:var(--green)}
1853
+ .traffic-body-toggle .chevron{font-size:9px;transition:transform .15s;display:inline-block}
1854
+ .traffic-body-toggle.open .chevron{transform:rotate(90deg)}
1855
+ .traffic-body pre{background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;font-family:var(--mono);font-size:12px;overflow-x:auto;max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;line-height:1.5;margin:6px 0 0;display:none}
1856
+ .traffic-body pre.open{display:block}
1857
+ .traffic-separator{height:0}
1858
+ .flow-divider{border-top:1px solid var(--border);margin:14px 0 10px}
1859
+ .flow-insights{padding:0;font-size:12px;line-height:1.8;color:var(--text-dim)}
1860
+ .flow-insights .insight-line{padding:3px 0}
1861
+ .flow-insights .insight-error{color:var(--red)}
1862
+ .flow-insights .insight-warn{color:var(--amber)}
1863
+ .flow-insights .insight-tip{margin-top:8px;color:var(--text-muted);font-size:11px;line-height:1.5}
2116
1864
 
2117
- `);
2118
- }
2119
- };
2120
- const requestListener = (traced) => {
2121
- writeEvent(null, JSON.stringify(traced));
2122
- };
2123
- const fetchListener = (entry) => {
2124
- writeEvent("fetch", JSON.stringify(entry));
2125
- };
2126
- const logListener = (entry) => {
2127
- writeEvent("log", JSON.stringify(entry));
2128
- };
2129
- const errorListener = (entry) => {
2130
- writeEvent("error_event", JSON.stringify(entry));
2131
- };
2132
- const queryListener = (entry) => {
2133
- writeEvent("query", JSON.stringify(entry));
2134
- };
2135
- const analysisListener = engine ? (insights, findings) => {
2136
- writeEvent("insights", JSON.stringify(insights));
2137
- writeEvent("security", JSON.stringify(findings));
2138
- } : void 0;
2139
- onRequest(requestListener);
2140
- defaultFetchStore.onEntry(fetchListener);
2141
- defaultLogStore.onEntry(logListener);
2142
- defaultErrorStore.onEntry(errorListener);
2143
- defaultQueryStore.onEntry(queryListener);
2144
- if (engine && analysisListener) engine.onUpdate(analysisListener);
2145
- const heartbeat = setInterval(() => {
2146
- if (res.destroyed) {
2147
- clearInterval(heartbeat);
2148
- return;
2149
- }
2150
- res.write(":heartbeat\n\n");
2151
- }, SSE_HEARTBEAT_INTERVAL_MS);
2152
- req.on("close", () => {
2153
- clearInterval(heartbeat);
2154
- offRequest(requestListener);
2155
- defaultFetchStore.offEntry(fetchListener);
2156
- defaultLogStore.offEntry(logListener);
2157
- defaultErrorStore.offEntry(errorListener);
2158
- defaultQueryStore.offEntry(queryListener);
2159
- if (engine && analysisListener) engine.offUpdate(analysisListener);
2160
- });
2161
- };
1865
+ /* Detailed mode sub-rows */
1866
+ .flow-subreqs{padding:4px 0;display:flex;flex-direction:column;gap:6px}
1867
+ .flow-subreq{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius);font-family:var(--mono);font-size:13px;cursor:pointer;transition:all .15s;background:var(--bg-card);box-shadow:var(--shadow-sm)}
1868
+ .flow-subreq:hover{border-color:var(--border-light);box-shadow:var(--shadow-md)}
1869
+ .flow-subreq .subreq-method{font-weight:700;flex-shrink:0;font-size:12px}
1870
+ .flow-subreq .subreq-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500}
1871
+ .flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
1872
+ .flow-subreq .subreq-status{flex-shrink:0}
1873
+ .flow-subreq .subreq-dur{color:var(--text-muted);font-size:12px;text-align:right;flex-shrink:0}
1874
+ .flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
1875
+ .flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
1876
+ .flow-subreq-detail.open{display:block}
1877
+
1878
+ /* Shared detail expand */
1879
+ .detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:14px}
1880
+ .detail-section h4{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:8px;font-weight:600}
1881
+ .detail-section pre{background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:14px;font-family:var(--mono);font-size:12px;overflow-x:auto;max-height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;line-height:1.6}
1882
+ .detail-meta{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:14px;font-family:var(--mono);font-size:12px;color:var(--text-dim);padding:12px 16px;background:var(--bg-muted);border-radius:var(--radius);border:1px solid var(--border)}
1883
+ .detail-meta span{display:flex;align-items:center;gap:6px}
1884
+ .detail-actions{margin-top:14px;display:flex;gap:8px}
1885
+
1886
+ /* Server activity */
1887
+ .server-activity{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}
1888
+ .server-activity-header{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600;margin-bottom:10px}
1889
+ .sa-section{margin-bottom:12px}
1890
+ .sa-label{font-size:10px;font-weight:600;color:var(--text-dim);margin-bottom:4px}
1891
+ .sa-row{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;padding:4px 0;color:var(--text)}
1892
+ .sa-method{width:40px;font-weight:600;flex-shrink:0}
1893
+ .sa-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim)}
1894
+ .sa-status{width:36px;text-align:right;font-weight:600}
1895
+ .sa-dur{width:60px;text-align:right;color:var(--text-muted)}
1896
+ .sa-level{width:50px;font-weight:600;flex-shrink:0;font-size:10px}
1897
+ .sa-msg{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:11px}
1898
+ .sa-err-name{width:100px;color:var(--red);font-weight:600;flex-shrink:0}
1899
+
1900
+ /* Strict Mode duplicate banner */
1901
+ .strict-mode-dupe{opacity:0.55}
1902
+ .strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
1903
+ `;
2162
1904
  }
2163
- var init_sse = __esm({
2164
- "src/dashboard/sse.ts"() {
1905
+ var init_flows = __esm({
1906
+ "src/dashboard/styles/flows.ts"() {
2165
1907
  "use strict";
2166
- init_request_log();
2167
- init_store();
2168
- init_constants();
2169
1908
  }
2170
1909
  });
2171
1910
 
2172
- // src/dashboard/styles/base.ts
2173
- function getBaseStyles() {
2174
- return `
2175
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
2176
- :root{
2177
- --bg:#ffffff;--bg-sidebar:#f8f8fa;--bg-card:#ffffff;--bg-hover:#f4f4f5;--bg-detail:#fafafa;
2178
- --bg-active:#ede9fe;--bg-muted:#f4f4f5;
2179
- --border:#e4e4e7;--border-light:#d4d4d8;--border-subtle:#f4f4f5;
2180
- --text:#18181b;--text-dim:#52525b;--text-muted:#a1a1aa;
2181
- --accent:#7c3aed;
2182
- --green:#16a34a;
2183
- --blue:#2563eb;
2184
- --amber:#d97706;
2185
- --red:#dc2626;
2186
- --cyan:#0891b2;
2187
- --sidebar-width:232px;--header-height:52px;
2188
- --radius:8px;--radius-sm:6px;
2189
- --shadow-sm:0 1px 2px rgba(0,0,0,0.05);
2190
- --shadow-md:0 1px 3px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.04);
2191
- --shadow-lg:0 4px 12px rgba(0,0,0,0.08),0 1px 4px rgba(0,0,0,0.04);
2192
- --breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
2193
- --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
2194
- --sans:Inter,system-ui,-apple-system,sans-serif;
2195
- }
2196
- html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:15px;overflow:hidden;-webkit-font-smoothing:antialiased}
2197
-
2198
- /* Scrollbar */
2199
- ::-webkit-scrollbar{width:8px}
2200
- ::-webkit-scrollbar-track{background:transparent}
2201
- ::-webkit-scrollbar-thumb{background:#d4d4d8;border-radius:4px}
2202
- ::-webkit-scrollbar-thumb:hover{background:#a1a1aa}
2203
-
2204
- /* Tooltip */
2205
- .tooltip{position:relative}
2206
- .tooltip::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:#ffffff;border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-size:11px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;box-shadow:var(--shadow-lg)}
2207
- .tooltip:hover::after{opacity:1}
2208
-
2209
- /* Toast */
2210
- .toast{position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-8px);background:#f0fdf4;border:1px solid #86efac;color:#15803d;padding:12px 24px;border-radius:10px;font-size:13px;font-weight:500;opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:100;box-shadow:var(--shadow-lg)}
2211
- .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
2212
-
2213
- /* Empty */
2214
- .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:400px;color:var(--text-muted);gap:12px}
2215
- .empty-title{font-size:19px;font-weight:600;color:var(--text-dim)}
2216
- .empty-sub{font-size:14px}
2217
-
2218
- /* View toggle */
2219
- .view-flows{display:block}.view-requests{display:none}
2220
- `;
2221
- }
2222
- var init_base = __esm({
2223
- "src/dashboard/styles/base.ts"() {
2224
- "use strict";
2225
- }
2226
- });
2227
-
2228
- // src/dashboard/styles/layout.ts
2229
- function getLayoutStyles() {
2230
- return `
2231
- /* Layout */
2232
- .app{display:grid;grid-template-columns:var(--sidebar-width) 1fr;height:100vh;overflow:hidden}
2233
- .main-panel{display:flex;flex-direction:column;overflow:hidden}
2234
-
2235
- /* Sidebar */
2236
- .sidebar{background:var(--bg-sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}
2237
- .sidebar-logo{padding:20px 24px 24px;border-bottom:1px solid var(--border-subtle)}
2238
- .sidebar-logo .logo-text{font-weight:800;font-size:21px;color:var(--accent);letter-spacing:-.5px}
2239
- .sidebar-logo .logo-version{font-weight:400;font-size:11px;color:var(--text-muted);margin-left:8px;letter-spacing:0}
2240
- .sidebar-nav{padding:12px;flex:1}
2241
- .sidebar-section{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);padding:16px 12px 8px}
2242
- .sidebar-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius);color:var(--text-dim);font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
2243
- .sidebar-item:hover{background:var(--bg-hover);color:var(--text)}
2244
- .sidebar-item.active{background:var(--bg-active);color:var(--accent)}
2245
- .sidebar-item .item-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;opacity:.5}
2246
- .sidebar-item.active .item-icon{opacity:1}
2247
- .sidebar-item:hover .item-icon{opacity:.8}
2248
- .sidebar-item .item-label{flex:1}
2249
- .sidebar-item .item-count{font-size:12px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;min-width:24px;text-align:center}
2250
- .sidebar-item.disabled{opacity:.35;cursor:default;pointer-events:none}
2251
- .sidebar-item .coming-soon{font-size:10px;color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;font-weight:600;letter-spacing:.3px}
2252
- .sidebar-footer{padding:16px 24px;border-top:1px solid var(--border-subtle);font-size:12px;color:var(--text-muted);font-family:var(--mono)}
2253
-
2254
- /* Header */
2255
- .header{display:flex;align-items:center;gap:16px;padding:0 28px;height:var(--header-height);border-bottom:1px solid var(--border);background:var(--bg);flex-shrink:0;box-shadow:0 1px 0 rgba(0,0,0,0.03)}
2256
- .header-left{display:flex;flex-direction:column;justify-content:center}
2257
- .header-title{font-weight:600;font-size:17px;color:var(--text);letter-spacing:-.2px;line-height:1.2}
2258
- .header-sub{font-size:11px;color:var(--text-muted);line-height:1.2}
2259
- .header-right{margin-left:auto;display:flex;gap:10px;align-items:center}
2260
-
2261
- /* Segmented control */
2262
- .segmented-control{display:flex;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:3px;gap:2px}
2263
- .segmented-btn{background:transparent;border:none;color:var(--text-muted);padding:6px 14px;font-size:13px;cursor:pointer;transition:all .15s;font-family:var(--sans);font-weight:500;border-radius:var(--radius-sm)}
2264
- .segmented-btn:hover{color:var(--text)}
2265
- .segmented-btn.active{background:#ffffff;color:var(--text);box-shadow:var(--shadow-sm)}
2266
-
2267
- .btn{background:#ffffff;border:1px solid var(--border);color:var(--text-dim);padding:7px 14px;border-radius:var(--radius);font-size:13px;cursor:pointer;transition:all .15s;font-family:var(--sans);font-weight:500;box-shadow:var(--shadow-sm)}
2268
- .btn:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-light)}
2269
- .btn-danger:hover{border-color:rgba(220,38,38,.3);color:var(--red);background:rgba(220,38,38,.05)}
2270
-
2271
- /* Content */
2272
- .main-content{flex:1;overflow-y:auto}
2273
-
2274
- /* Column headers */
2275
- .col-header{display:flex;align-items:center;gap:16px;padding:8px 28px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);border-bottom:1px solid var(--border);background:var(--bg-sidebar);position:sticky;top:0;z-index:2;font-family:var(--mono)}
2276
-
2277
- /* Footer */
2278
- .footer{padding:10px 28px;border-top:1px solid var(--border);font-size:13px;color:var(--text-muted);display:flex;gap:24px;font-family:var(--mono);flex-shrink:0;background:var(--bg-sidebar)}
2279
- .footer .error-count{color:var(--red)}
2280
- `;
2281
- }
2282
- var init_layout = __esm({
2283
- "src/dashboard/styles/layout.ts"() {
2284
- "use strict";
2285
- }
2286
- });
2287
-
2288
- // src/dashboard/styles/flows.ts
2289
- function getFlowStyles() {
2290
- return `
2291
- /* Flow rows */
2292
- .flow-row{padding:12px 28px;border-bottom:1px solid var(--border-subtle);cursor:pointer;transition:background .1s}
2293
- .flow-row:hover{background:var(--bg-hover)}
2294
- .flow-row.expanded{background:var(--bg-muted)}
2295
- .flow-summary-row{display:flex;align-items:center;gap:14px;font-size:14px}
2296
- .flow-status-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
2297
- .flow-status-dot.dot-clean{background:var(--green)}
2298
- .flow-status-dot.dot-warn{background:var(--amber)}
2299
- .flow-status-dot.dot-error{background:var(--red)}
2300
- .flow-label{font-weight:500;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
2301
- .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
2302
- .flow-badge-pill{font-size:11px;flex-shrink:0;font-family:var(--mono);font-weight:600;padding:2px 10px;border-radius:10px;text-align:center}
2303
- .flow-badge-pill.badge-clean{background:rgba(22,163,74,0.07);color:var(--green)}
2304
- .flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
2305
- .flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
2306
- .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
2307
-
2308
- /* Flow expand panel */
2309
- .flow-expand{display:none;padding:12px 28px 16px;border-bottom:1px solid var(--border);background:var(--bg-detail)}
2310
- .flow-expand.open{display:block}
2311
-
2312
- /* Request cards in expanded flow */
2313
- .traffic-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;margin-bottom:10px;box-shadow:var(--shadow-sm)}
2314
- .traffic-card:last-child{margin-bottom:0}
2315
-
2316
- /* Simple mode traffic */
2317
- .flow-traffic{padding:0;font-family:var(--mono);font-size:13px}
2318
- .traffic-card-header{display:flex;align-items:center;gap:10px;margin-bottom:0}
2319
- .traffic-card-header.has-details{margin-bottom:10px}
2320
-
2321
- /* Method badges */
2322
- .method-badge{display:inline-flex;align-items:center;justify-content:center;padding:3px 8px;border-radius:5px;font-size:10px;font-weight:700;font-family:var(--mono);letter-spacing:.3px;flex-shrink:0}
2323
- .method-badge-GET{background:rgba(22,163,74,0.08);color:var(--green)}
2324
- .method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
2325
- .method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
2326
- .method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
2327
- .method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
2328
-
2329
- /* Status pills */
2330
- .status-pill{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:600;font-family:var(--mono);flex-shrink:0}
2331
- .status-pill-2xx{background:rgba(22,163,74,0.07);color:var(--green)}
2332
- .status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
2333
- .status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
2334
- .status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
2335
-
2336
- .traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
2337
- .traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
2338
- .traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
2339
- .traffic-card-size{color:var(--text-muted);font-size:11px;flex-shrink:0}
2340
- .traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
2341
-
2342
- /* Body toggles */
2343
- .traffic-body{padding:0;margin-top:8px}
2344
- .traffic-body-toggle{font-size:11px;color:var(--text-dim);display:inline-flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-muted);font-family:var(--mono);letter-spacing:.3px;transition:all .15s;margin-right:6px;margin-bottom:4px}
2345
- .traffic-body-toggle:hover{border-color:var(--border-light);color:var(--text);background:var(--bg-hover)}
2346
- .traffic-body-toggle .arrow-out{color:var(--blue)}
2347
- .traffic-body-toggle .arrow-in{color:var(--green)}
2348
- .traffic-body-toggle .chevron{font-size:9px;transition:transform .15s;display:inline-block}
2349
- .traffic-body-toggle.open .chevron{transform:rotate(90deg)}
2350
- .traffic-body pre{background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;font-family:var(--mono);font-size:12px;overflow-x:auto;max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;line-height:1.5;margin:6px 0 0;display:none}
2351
- .traffic-body pre.open{display:block}
2352
- .traffic-separator{height:0}
2353
- .flow-divider{border-top:1px solid var(--border);margin:14px 0 10px}
2354
- .flow-insights{padding:0;font-size:12px;line-height:1.8;color:var(--text-dim)}
2355
- .flow-insights .insight-line{padding:3px 0}
2356
- .flow-insights .insight-error{color:var(--red)}
2357
- .flow-insights .insight-warn{color:var(--amber)}
2358
- .flow-insights .insight-tip{margin-top:8px;color:var(--text-muted);font-size:11px;line-height:1.5}
2359
-
2360
- /* Detailed mode sub-rows */
2361
- .flow-subreqs{padding:4px 0;display:flex;flex-direction:column;gap:6px}
2362
- .flow-subreq{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius);font-family:var(--mono);font-size:13px;cursor:pointer;transition:all .15s;background:var(--bg-card);box-shadow:var(--shadow-sm)}
2363
- .flow-subreq:hover{border-color:var(--border-light);box-shadow:var(--shadow-md)}
2364
- .flow-subreq .subreq-method{font-weight:700;flex-shrink:0;font-size:12px}
2365
- .flow-subreq .subreq-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500}
2366
- .flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
2367
- .flow-subreq .subreq-status{flex-shrink:0}
2368
- .flow-subreq .subreq-dur{color:var(--text-muted);font-size:12px;text-align:right;flex-shrink:0}
2369
- .flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
2370
- .flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
2371
- .flow-subreq-detail.open{display:block}
2372
-
2373
- /* Shared detail expand */
2374
- .detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:14px}
2375
- .detail-section h4{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:8px;font-weight:600}
2376
- .detail-section pre{background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:14px;font-family:var(--mono);font-size:12px;overflow-x:auto;max-height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;line-height:1.6}
2377
- .detail-meta{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:14px;font-family:var(--mono);font-size:12px;color:var(--text-dim);padding:12px 16px;background:var(--bg-muted);border-radius:var(--radius);border:1px solid var(--border)}
2378
- .detail-meta span{display:flex;align-items:center;gap:6px}
2379
- .detail-actions{margin-top:14px;display:flex;gap:8px}
2380
-
2381
- /* Server activity */
2382
- .server-activity{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}
2383
- .server-activity-header{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600;margin-bottom:10px}
2384
- .sa-section{margin-bottom:12px}
2385
- .sa-label{font-size:10px;font-weight:600;color:var(--text-dim);margin-bottom:4px}
2386
- .sa-row{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;padding:4px 0;color:var(--text)}
2387
- .sa-method{width:40px;font-weight:600;flex-shrink:0}
2388
- .sa-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim)}
2389
- .sa-status{width:36px;text-align:right;font-weight:600}
2390
- .sa-dur{width:60px;text-align:right;color:var(--text-muted)}
2391
- .sa-level{width:50px;font-weight:600;flex-shrink:0;font-size:10px}
2392
- .sa-msg{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:11px}
2393
- .sa-err-name{width:100px;color:var(--red);font-weight:600;flex-shrink:0}
2394
-
2395
- /* Strict Mode duplicate banner */
2396
- .strict-mode-dupe{opacity:0.55}
2397
- .strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
2398
- `;
2399
- }
2400
- var init_flows = __esm({
2401
- "src/dashboard/styles/flows.ts"() {
2402
- "use strict";
2403
- }
2404
- });
2405
-
2406
- // src/dashboard/styles/requests.ts
2407
- function getRequestStyles() {
1911
+ // src/dashboard/styles/requests.ts
1912
+ function getRequestStyles() {
2408
1913
  return `
2409
1914
  /* Request rows */
2410
1915
  .req-row{display:flex;align-items:center;gap:16px;padding:12px 28px;border-bottom:1px solid var(--border-subtle);cursor:pointer;transition:background .1s;font-family:var(--mono);font-size:14px}
@@ -2595,6 +2100,7 @@ function getOverviewStyles() {
2595
2100
  .ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
2596
2101
  .ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
2597
2102
  .ov-card-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
2103
+ .ov-card-icon.resolved{background:var(--green-bg);color:var(--green)}
2598
2104
  .ov-card-body{flex:1;min-width:0}
2599
2105
  .ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
2600
2106
  .ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
@@ -2611,8 +2117,13 @@ function getOverviewStyles() {
2611
2117
  .ov-detail-item{font-size:12px;color:var(--text);font-family:var(--mono);padding:2px 0}
2612
2118
 
2613
2119
  /* All-clear banner */
2614
- .ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:rgba(22,163,74,.06);border:1px solid rgba(22,163,74,.2);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
2120
+ .ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--green-bg-subtle);border:1px solid var(--green-border);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
2615
2121
  .ov-clear-icon{font-size:16px}
2122
+
2123
+ /* Resolved section */
2124
+ .ov-resolved-title{margin-top:24px}
2125
+ .ov-card-resolved{opacity:.7;border-color:var(--green-border);cursor:default}
2126
+ .ov-card-resolved:hover{opacity:1;box-shadow:var(--shadow-sm)}
2616
2127
  `;
2617
2128
  }
2618
2129
  var init_overview = __esm({
@@ -2628,7 +2139,7 @@ function getSecurityStyles() {
2628
2139
  .sec-container{padding:24px 28px}
2629
2140
 
2630
2141
  /* All-clear */
2631
- .sec-clear{display:flex;align-items:center;gap:16px;padding:20px 24px;background:rgba(22,163,74,.05);border:1px solid rgba(22,163,74,.15);border-radius:var(--radius);margin-bottom:24px}
2142
+ .sec-clear{display:flex;align-items:center;gap:16px;padding:20px 24px;background:var(--green-bg-subtle);border:1px solid var(--green-border-subtle);border-radius:var(--radius);margin-bottom:24px}
2632
2143
  .sec-clear-icon{font-size:24px;color:var(--green);flex-shrink:0}
2633
2144
  .sec-clear-title{font-size:15px;font-weight:600;color:var(--green);margin-bottom:2px}
2634
2145
  .sec-clear-sub{font-size:12px;color:var(--text-dim)}
@@ -2664,6 +2175,19 @@ function getSecurityStyles() {
2664
2175
  .sec-item-desc{color:var(--text-dim);line-height:1.5;flex:1;min-width:0}
2665
2176
  .sec-item-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
2666
2177
  .sec-item-count{font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0;margin-left:12px}
2178
+
2179
+ /* Resolved badge in summary */
2180
+ .sec-resolved-badge{font-size:11px;font-weight:600;padding:3px 10px;border-radius:10px;background:var(--green-bg);color:var(--green);margin-left:12px}
2181
+
2182
+ /* Resolved section */
2183
+ .sec-resolved-title{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--text-dim);margin:20px 0 8px 0}
2184
+ .sec-resolved-check{color:var(--green);font-size:14px}
2185
+ .sec-resolved-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 8px;border-radius:10px;border:1px solid var(--border)}
2186
+ .sec-group-resolved{opacity:.7;border-color:var(--green-border)}
2187
+ .sec-group-resolved:hover{opacity:1}
2188
+ .sec-item-resolved{color:var(--text-muted)}
2189
+ .sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
2190
+ .sec-resolved-item-icon{color:var(--green);font-size:12px;flex-shrink:0;margin-right:8px}
2667
2191
  `;
2668
2192
  }
2669
2193
  var init_security = __esm({
@@ -2726,50 +2250,160 @@ var init_styles = __esm({
2726
2250
  }
2727
2251
  });
2728
2252
 
2729
- // src/store/finding-id.ts
2730
- import { createHash } from "crypto";
2731
- function computeFindingId(finding) {
2732
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
2733
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
2253
+ // src/utils/fs.ts
2254
+ import { access } from "fs/promises";
2255
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2256
+ import { resolve } from "path";
2257
+ function ensureGitignore(dir, entry) {
2258
+ try {
2259
+ const gitignorePath = resolve(dir, "../.gitignore");
2260
+ if (existsSync(gitignorePath)) {
2261
+ const content = readFileSync(gitignorePath, "utf-8");
2262
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
2263
+ writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2264
+ } else {
2265
+ writeFileSync(gitignorePath, entry + "\n");
2266
+ }
2267
+ } catch {
2268
+ }
2734
2269
  }
2735
- var init_finding_id = __esm({
2736
- "src/store/finding-id.ts"() {
2270
+ var init_fs = __esm({
2271
+ "src/utils/fs.ts"() {
2737
2272
  "use strict";
2738
2273
  }
2739
2274
  });
2740
2275
 
2741
- // src/store/finding-store.ts
2742
- import {
2743
- readFileSync as readFileSync3,
2744
- writeFileSync as writeFileSync3,
2745
- existsSync as existsSync3,
2746
- mkdirSync as mkdirSync3,
2747
- renameSync as renameSync2
2748
- } from "fs";
2749
- import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
2750
- import { resolve as resolve3 } from "path";
2276
+ // src/utils/log.ts
2277
+ function brakitWarn(message) {
2278
+ process.stderr.write(`${PREFIX} ${message}
2279
+ `);
2280
+ }
2281
+ function brakitDebug(message) {
2282
+ if (process.env.DEBUG_BRAKIT) {
2283
+ process.stderr.write(`${PREFIX}:debug ${message}
2284
+ `);
2285
+ }
2286
+ }
2287
+ var PREFIX;
2288
+ var init_log = __esm({
2289
+ "src/utils/log.ts"() {
2290
+ "use strict";
2291
+ PREFIX = "[brakit]";
2292
+ }
2293
+ });
2294
+
2295
+ // src/utils/atomic-writer.ts
2296
+ import {
2297
+ writeFileSync as writeFileSync2,
2298
+ existsSync as existsSync2,
2299
+ mkdirSync as mkdirSync2,
2300
+ renameSync
2301
+ } from "fs";
2302
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
2303
+ var AtomicWriter;
2304
+ var init_atomic_writer = __esm({
2305
+ "src/utils/atomic-writer.ts"() {
2306
+ "use strict";
2307
+ init_fs();
2308
+ init_log();
2309
+ AtomicWriter = class {
2310
+ constructor(opts) {
2311
+ this.opts = opts;
2312
+ this.tmpPath = opts.filePath + ".tmp";
2313
+ }
2314
+ tmpPath;
2315
+ writing = false;
2316
+ pendingContent = null;
2317
+ writeSync(content) {
2318
+ try {
2319
+ this.ensureDir();
2320
+ writeFileSync2(this.tmpPath, content);
2321
+ renameSync(this.tmpPath, this.opts.filePath);
2322
+ } catch (err) {
2323
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2324
+ }
2325
+ }
2326
+ async writeAsync(content) {
2327
+ if (this.writing) {
2328
+ this.pendingContent = content;
2329
+ return;
2330
+ }
2331
+ this.writing = true;
2332
+ try {
2333
+ await this.ensureDirAsync();
2334
+ await writeFile2(this.tmpPath, content);
2335
+ await rename(this.tmpPath, this.opts.filePath);
2336
+ } catch (err) {
2337
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2338
+ } finally {
2339
+ this.writing = false;
2340
+ if (this.pendingContent !== null) {
2341
+ const next = this.pendingContent;
2342
+ this.pendingContent = null;
2343
+ this.writeAsync(next);
2344
+ }
2345
+ }
2346
+ }
2347
+ ensureDir() {
2348
+ if (!existsSync2(this.opts.dir)) {
2349
+ mkdirSync2(this.opts.dir, { recursive: true });
2350
+ if (this.opts.gitignoreEntry) {
2351
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2352
+ }
2353
+ }
2354
+ }
2355
+ async ensureDirAsync() {
2356
+ if (!existsSync2(this.opts.dir)) {
2357
+ await mkdir(this.opts.dir, { recursive: true });
2358
+ if (this.opts.gitignoreEntry) {
2359
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2360
+ }
2361
+ }
2362
+ }
2363
+ };
2364
+ }
2365
+ });
2366
+
2367
+ // src/store/finding-id.ts
2368
+ import { createHash } from "crypto";
2369
+ function computeFindingId(finding) {
2370
+ const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
2371
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
2372
+ }
2373
+ var init_finding_id = __esm({
2374
+ "src/store/finding-id.ts"() {
2375
+ "use strict";
2376
+ }
2377
+ });
2378
+
2379
+ // src/store/finding-store.ts
2380
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
2381
+ import { resolve as resolve2 } from "path";
2751
2382
  var FindingStore;
2752
2383
  var init_finding_store = __esm({
2753
2384
  "src/store/finding-store.ts"() {
2754
2385
  "use strict";
2755
2386
  init_constants();
2756
- init_fs();
2387
+ init_atomic_writer();
2757
2388
  init_finding_id();
2758
2389
  FindingStore = class {
2759
2390
  constructor(rootDir) {
2760
2391
  this.rootDir = rootDir;
2761
- this.metricsDir = resolve3(rootDir, METRICS_DIR);
2762
- this.findingsPath = resolve3(rootDir, FINDINGS_FILE);
2763
- this.tmpPath = this.findingsPath + ".tmp";
2392
+ const metricsDir = resolve2(rootDir, METRICS_DIR);
2393
+ this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
2394
+ this.writer = new AtomicWriter({
2395
+ dir: metricsDir,
2396
+ filePath: this.findingsPath,
2397
+ gitignoreEntry: METRICS_DIR,
2398
+ label: "findings"
2399
+ });
2764
2400
  this.load();
2765
2401
  }
2766
2402
  findings = /* @__PURE__ */ new Map();
2767
2403
  flushTimer = null;
2768
2404
  dirty = false;
2769
- writing = false;
2405
+ writer;
2770
2406
  findingsPath;
2771
- tmpPath;
2772
- metricsDir;
2773
2407
  start() {
2774
2408
  this.flushTimer = setInterval(
2775
2409
  () => this.flush(),
@@ -2823,6 +2457,15 @@ var init_finding_store = __esm({
2823
2457
  this.dirty = true;
2824
2458
  return true;
2825
2459
  }
2460
+ /**
2461
+ * Reconcile passive findings against the current analysis results.
2462
+ *
2463
+ * Passive findings are detected by continuous scanning (not user-triggered).
2464
+ * When a previously-seen finding is absent from the current results, it means
2465
+ * the issue has been fixed — transition it to "resolved" automatically.
2466
+ * Active findings (from MCP verify-fix) are not auto-resolved because they
2467
+ * require explicit verification.
2468
+ */
2826
2469
  reconcilePassive(currentFindings) {
2827
2470
  const currentIds = new Set(currentFindings.map(computeFindingId));
2828
2471
  for (const [id, stateful] of this.findings) {
@@ -2849,7 +2492,7 @@ var init_finding_store = __esm({
2849
2492
  load() {
2850
2493
  try {
2851
2494
  if (existsSync3(this.findingsPath)) {
2852
- const raw = readFileSync3(this.findingsPath, "utf-8");
2495
+ const raw = readFileSync2(this.findingsPath, "utf-8");
2853
2496
  const parsed = JSON.parse(raw);
2854
2497
  if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
2855
2498
  for (const f of parsed.findings) {
@@ -2862,56 +2505,20 @@ var init_finding_store = __esm({
2862
2505
  }
2863
2506
  flush() {
2864
2507
  if (!this.dirty) return;
2865
- this.writeAsync();
2508
+ this.writer.writeAsync(this.serialize());
2509
+ this.dirty = false;
2866
2510
  }
2867
2511
  flushSync() {
2868
2512
  if (!this.dirty) return;
2869
- try {
2870
- this.ensureDir();
2871
- const data = {
2872
- version: 1,
2873
- findings: [...this.findings.values()]
2874
- };
2875
- writeFileSync3(this.tmpPath, JSON.stringify(data));
2876
- renameSync2(this.tmpPath, this.findingsPath);
2877
- this.dirty = false;
2878
- } catch (err) {
2879
- process.stderr.write(
2880
- `[brakit] failed to save findings: ${err.message}
2881
- `
2882
- );
2883
- }
2884
- }
2885
- async writeAsync() {
2886
- if (this.writing) return;
2887
- this.writing = true;
2888
- try {
2889
- if (!existsSync3(this.metricsDir)) {
2890
- await mkdir2(this.metricsDir, { recursive: true });
2891
- ensureGitignore(this.metricsDir, METRICS_DIR);
2892
- }
2893
- const data = {
2894
- version: 1,
2895
- findings: [...this.findings.values()]
2896
- };
2897
- await writeFile3(this.tmpPath, JSON.stringify(data));
2898
- await rename2(this.tmpPath, this.findingsPath);
2899
- this.dirty = false;
2900
- } catch (err) {
2901
- process.stderr.write(
2902
- `[brakit] failed to save findings: ${err.message}
2903
- `
2904
- );
2905
- } finally {
2906
- this.writing = false;
2907
- if (this.dirty) this.writeAsync();
2908
- }
2513
+ this.writer.writeSync(this.serialize());
2514
+ this.dirty = false;
2909
2515
  }
2910
- ensureDir() {
2911
- if (!existsSync3(this.metricsDir)) {
2912
- mkdirSync3(this.metricsDir, { recursive: true });
2913
- ensureGitignore(this.metricsDir, METRICS_DIR);
2914
- }
2516
+ serialize() {
2517
+ const data = {
2518
+ version: 1,
2519
+ findings: [...this.findings.values()]
2520
+ };
2521
+ return JSON.stringify(data);
2915
2522
  }
2916
2523
  };
2917
2524
  }
@@ -3557,6 +3164,21 @@ var init_collections = __esm({
3557
3164
  }
3558
3165
  });
3559
3166
 
3167
+ // src/utils/endpoint.ts
3168
+ function getEndpointKey(method, path) {
3169
+ return `${method} ${path}`;
3170
+ }
3171
+ function extractEndpointFromDesc(desc) {
3172
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
3173
+ }
3174
+ var ENDPOINT_PREFIX_RE;
3175
+ var init_endpoint = __esm({
3176
+ "src/utils/endpoint.ts"() {
3177
+ "use strict";
3178
+ ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
3179
+ }
3180
+ });
3181
+
3560
3182
  // src/analysis/insights/query-helpers.ts
3561
3183
  function getQueryShape(q) {
3562
3184
  if (q.sql) return normalizeQueryParams(q.sql) ?? "";
@@ -3589,6 +3211,23 @@ function createEndpointGroup() {
3589
3211
  queryShapeDurations: /* @__PURE__ */ new Map()
3590
3212
  };
3591
3213
  }
3214
+ function windowByEndpoint(requests) {
3215
+ const byEndpoint = /* @__PURE__ */ new Map();
3216
+ for (const r of requests) {
3217
+ const ep = getEndpointKey(r.method, r.path);
3218
+ let list = byEndpoint.get(ep);
3219
+ if (!list) {
3220
+ list = [];
3221
+ byEndpoint.set(ep, list);
3222
+ }
3223
+ list.push(r);
3224
+ }
3225
+ const windowed = [];
3226
+ for (const [, reqs] of byEndpoint) {
3227
+ windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
3228
+ }
3229
+ return windowed;
3230
+ }
3592
3231
  function prepareContext(ctx) {
3593
3232
  const nonStatic = ctx.requests.filter(
3594
3233
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
@@ -3596,8 +3235,9 @@ function prepareContext(ctx) {
3596
3235
  const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
3597
3236
  const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
3598
3237
  const reqById = new Map(nonStatic.map((r) => [r.id, r]));
3238
+ const recent = windowByEndpoint(nonStatic);
3599
3239
  const endpointGroups = /* @__PURE__ */ new Map();
3600
- for (const r of nonStatic) {
3240
+ for (const r of recent) {
3601
3241
  const ep = getEndpointKey(r.method, r.path);
3602
3242
  let g = endpointGroups.get(ep);
3603
3243
  if (!g) {
@@ -3642,6 +3282,7 @@ var init_prepare = __esm({
3642
3282
  init_collections();
3643
3283
  init_endpoint();
3644
3284
  init_constants();
3285
+ init_thresholds();
3645
3286
  init_query_helpers();
3646
3287
  }
3647
3288
  });
@@ -3683,7 +3324,7 @@ var init_n1 = __esm({
3683
3324
  "use strict";
3684
3325
  init_query_helpers();
3685
3326
  init_endpoint();
3686
- init_thresholds();
3327
+ init_constants();
3687
3328
  n1Rule = {
3688
3329
  id: "n1",
3689
3330
  check(ctx) {
@@ -3733,7 +3374,7 @@ var init_cross_endpoint = __esm({
3733
3374
  "use strict";
3734
3375
  init_query_helpers();
3735
3376
  init_endpoint();
3736
- init_thresholds();
3377
+ init_constants();
3737
3378
  crossEndpointRule = {
3738
3379
  id: "cross-endpoint",
3739
3380
  check(ctx) {
@@ -3791,7 +3432,7 @@ var init_redundant_query = __esm({
3791
3432
  "use strict";
3792
3433
  init_query_helpers();
3793
3434
  init_endpoint();
3794
- init_thresholds();
3435
+ init_constants();
3795
3436
  redundantQueryRule = {
3796
3437
  id: "redundant-query",
3797
3438
  check(ctx) {
@@ -3870,7 +3511,7 @@ var errorHotspotRule;
3870
3511
  var init_error_hotspot = __esm({
3871
3512
  "src/analysis/insights/rules/error-hotspot.ts"() {
3872
3513
  "use strict";
3873
- init_thresholds();
3514
+ init_constants();
3874
3515
  errorHotspotRule = {
3875
3516
  id: "error-hotspot",
3876
3517
  check(ctx) {
@@ -3900,7 +3541,7 @@ var duplicateRule;
3900
3541
  var init_duplicate = __esm({
3901
3542
  "src/analysis/insights/rules/duplicate.ts"() {
3902
3543
  "use strict";
3903
- init_thresholds();
3544
+ init_constants();
3904
3545
  duplicateRule = {
3905
3546
  id: "duplicate",
3906
3547
  check(ctx) {
@@ -3963,7 +3604,7 @@ var init_slow = __esm({
3963
3604
  "src/analysis/insights/rules/slow.ts"() {
3964
3605
  "use strict";
3965
3606
  init_format();
3966
- init_thresholds();
3607
+ init_constants();
3967
3608
  slowRule = {
3968
3609
  id: "slow",
3969
3610
  check(ctx) {
@@ -4010,7 +3651,7 @@ var queryHeavyRule;
4010
3651
  var init_query_heavy = __esm({
4011
3652
  "src/analysis/insights/rules/query-heavy.ts"() {
4012
3653
  "use strict";
4013
- init_thresholds();
3654
+ init_constants();
4014
3655
  queryHeavyRule = {
4015
3656
  id: "query-heavy",
4016
3657
  check(ctx) {
@@ -4041,7 +3682,7 @@ var init_select_star = __esm({
4041
3682
  "src/analysis/insights/rules/select-star.ts"() {
4042
3683
  "use strict";
4043
3684
  init_query_helpers();
4044
- init_thresholds();
3685
+ init_constants();
4045
3686
  init_patterns();
4046
3687
  selectStarRule = {
4047
3688
  id: "select-star",
@@ -4081,7 +3722,7 @@ var init_high_rows = __esm({
4081
3722
  "src/analysis/insights/rules/high-rows.ts"() {
4082
3723
  "use strict";
4083
3724
  init_query_helpers();
4084
- init_thresholds();
3725
+ init_constants();
4085
3726
  highRowsRule = {
4086
3727
  id: "high-rows",
4087
3728
  check(ctx) {
@@ -4126,7 +3767,7 @@ var init_response_overfetch = __esm({
4126
3767
  init_endpoint();
4127
3768
  init_response();
4128
3769
  init_patterns();
4129
- init_thresholds();
3770
+ init_constants();
4130
3771
  responseOverfetchRule = {
4131
3772
  id: "response-overfetch",
4132
3773
  check(ctx) {
@@ -4185,7 +3826,7 @@ var init_large_response = __esm({
4185
3826
  "src/analysis/insights/rules/large-response.ts"() {
4186
3827
  "use strict";
4187
3828
  init_format();
4188
- init_thresholds();
3829
+ init_constants();
4189
3830
  largeResponseRule = {
4190
3831
  id: "large-response",
4191
3832
  check(ctx) {
@@ -4216,7 +3857,7 @@ var init_regression = __esm({
4216
3857
  "src/analysis/insights/rules/regression.ts"() {
4217
3858
  "use strict";
4218
3859
  init_format();
4219
- init_thresholds();
3860
+ init_constants();
4220
3861
  regressionRule = {
4221
3862
  id: "regression",
4222
3863
  check(ctx) {
@@ -4278,6 +3919,27 @@ var init_security2 = __esm({
4278
3919
  }
4279
3920
  });
4280
3921
 
3922
+ // src/analysis/insights/rules/index.ts
3923
+ var init_rules2 = __esm({
3924
+ "src/analysis/insights/rules/index.ts"() {
3925
+ "use strict";
3926
+ init_n1();
3927
+ init_cross_endpoint();
3928
+ init_redundant_query();
3929
+ init_error();
3930
+ init_error_hotspot();
3931
+ init_duplicate();
3932
+ init_slow();
3933
+ init_query_heavy();
3934
+ init_select_star();
3935
+ init_high_rows();
3936
+ init_response_overfetch();
3937
+ init_large_response();
3938
+ init_regression();
3939
+ init_security2();
3940
+ }
3941
+ });
3942
+
4281
3943
  // src/analysis/insights/index.ts
4282
3944
  function createDefaultInsightRunner() {
4283
3945
  const runner = new InsightRunner();
@@ -4305,20 +3967,7 @@ var init_insights2 = __esm({
4305
3967
  "use strict";
4306
3968
  init_runner();
4307
3969
  init_runner();
4308
- init_n1();
4309
- init_cross_endpoint();
4310
- init_redundant_query();
4311
- init_error();
4312
- init_error_hotspot();
4313
- init_duplicate();
4314
- init_slow();
4315
- init_query_heavy();
4316
- init_select_star();
4317
- init_high_rows();
4318
- init_response_overfetch();
4319
- init_large_response();
4320
- init_regression();
4321
- init_security2();
3970
+ init_rules2();
4322
3971
  }
4323
3972
  });
4324
3973
 
@@ -4330,66 +3979,118 @@ var init_insights3 = __esm({
4330
3979
  }
4331
3980
  });
4332
3981
 
3982
+ // src/analysis/insight-tracker.ts
3983
+ function computeInsightKey(insight) {
3984
+ const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
3985
+ return `${insight.type}:${identifier}`;
3986
+ }
3987
+ var InsightTracker;
3988
+ var init_insight_tracker = __esm({
3989
+ "src/analysis/insight-tracker.ts"() {
3990
+ "use strict";
3991
+ init_endpoint();
3992
+ init_thresholds();
3993
+ InsightTracker = class {
3994
+ tracked = /* @__PURE__ */ new Map();
3995
+ reconcile(current) {
3996
+ const currentKeys = /* @__PURE__ */ new Set();
3997
+ const now = Date.now();
3998
+ for (const insight of current) {
3999
+ const key = computeInsightKey(insight);
4000
+ currentKeys.add(key);
4001
+ const existing = this.tracked.get(key);
4002
+ if (existing) {
4003
+ existing.insight = insight;
4004
+ existing.lastSeenAt = now;
4005
+ existing.consecutiveAbsences = 0;
4006
+ if (existing.state === "resolved") {
4007
+ existing.state = "open";
4008
+ existing.resolvedAt = null;
4009
+ }
4010
+ } else {
4011
+ this.tracked.set(key, {
4012
+ key,
4013
+ state: "open",
4014
+ insight,
4015
+ firstSeenAt: now,
4016
+ lastSeenAt: now,
4017
+ resolvedAt: null,
4018
+ consecutiveAbsences: 0
4019
+ });
4020
+ }
4021
+ }
4022
+ for (const [key, stateful] of this.tracked) {
4023
+ if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
4024
+ stateful.consecutiveAbsences++;
4025
+ if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
4026
+ stateful.state = "resolved";
4027
+ stateful.resolvedAt = now;
4028
+ }
4029
+ } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
4030
+ this.tracked.delete(key);
4031
+ }
4032
+ }
4033
+ return [...this.tracked.values()];
4034
+ }
4035
+ getAll() {
4036
+ return [...this.tracked.values()];
4037
+ }
4038
+ clear() {
4039
+ this.tracked.clear();
4040
+ }
4041
+ };
4042
+ }
4043
+ });
4044
+
4333
4045
  // src/analysis/engine.ts
4334
4046
  var AnalysisEngine;
4335
4047
  var init_engine = __esm({
4336
4048
  "src/analysis/engine.ts"() {
4337
4049
  "use strict";
4338
- init_request_log();
4339
- init_store();
4340
- init_request_log();
4050
+ init_disposable();
4341
4051
  init_group();
4342
4052
  init_rules();
4343
4053
  init_insights3();
4054
+ init_insight_tracker();
4344
4055
  AnalysisEngine = class {
4345
- constructor(metricsStore, findingStore, debounceMs = 300) {
4346
- this.metricsStore = metricsStore;
4347
- this.findingStore = findingStore;
4056
+ constructor(registry, debounceMs = 300) {
4057
+ this.registry = registry;
4348
4058
  this.debounceMs = debounceMs;
4349
4059
  this.scanner = createDefaultScanner();
4350
- this.boundRequestListener = () => this.scheduleRecompute();
4351
- this.boundQueryListener = () => this.scheduleRecompute();
4352
- this.boundErrorListener = () => this.scheduleRecompute();
4353
- this.boundLogListener = () => this.scheduleRecompute();
4354
4060
  }
4355
4061
  scanner;
4062
+ insightTracker = new InsightTracker();
4356
4063
  cachedInsights = [];
4357
4064
  cachedFindings = [];
4065
+ cachedStatefulInsights = [];
4358
4066
  debounceTimer = null;
4359
- listeners = [];
4360
- boundRequestListener;
4361
- boundQueryListener;
4362
- boundErrorListener;
4363
- boundLogListener;
4067
+ subs = new SubscriptionBag();
4364
4068
  start() {
4365
- onRequest(this.boundRequestListener);
4366
- defaultQueryStore.onEntry(this.boundQueryListener);
4367
- defaultErrorStore.onEntry(this.boundErrorListener);
4368
- defaultLogStore.onEntry(this.boundLogListener);
4069
+ const bus = this.registry.get("event-bus");
4070
+ this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4071
+ this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4072
+ this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
4073
+ this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
4369
4074
  }
4370
4075
  stop() {
4371
- offRequest(this.boundRequestListener);
4372
- defaultQueryStore.offEntry(this.boundQueryListener);
4373
- defaultErrorStore.offEntry(this.boundErrorListener);
4374
- defaultLogStore.offEntry(this.boundLogListener);
4076
+ this.subs.dispose();
4375
4077
  if (this.debounceTimer) {
4376
4078
  clearTimeout(this.debounceTimer);
4377
4079
  this.debounceTimer = null;
4378
4080
  }
4379
4081
  }
4380
- onUpdate(fn) {
4381
- this.listeners.push(fn);
4382
- }
4383
- offUpdate(fn) {
4384
- const idx = this.listeners.indexOf(fn);
4385
- if (idx !== -1) this.listeners.splice(idx, 1);
4386
- }
4387
4082
  getInsights() {
4388
4083
  return this.cachedInsights;
4389
4084
  }
4390
4085
  getFindings() {
4391
4086
  return this.cachedFindings;
4392
4087
  }
4088
+ getStatefulFindings() {
4089
+ return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
4090
+ }
4091
+ getStatefulInsights() {
4092
+ return this.cachedStatefulInsights;
4093
+ }
4393
4094
  scheduleRecompute() {
4394
4095
  if (this.debounceTimer) return;
4395
4096
  this.debounceTimer = setTimeout(() => {
@@ -4398,18 +4099,19 @@ var init_engine = __esm({
4398
4099
  }, this.debounceMs);
4399
4100
  }
4400
4101
  recompute() {
4401
- const requests = getRequests();
4402
- const queries = defaultQueryStore.getAll();
4403
- const errors = defaultErrorStore.getAll();
4404
- const logs = defaultLogStore.getAll();
4405
- const fetches = defaultFetchStore.getAll();
4102
+ const requests = this.registry.get("request-store").getAll();
4103
+ const queries = this.registry.get("query-store").getAll();
4104
+ const errors = this.registry.get("error-store").getAll();
4105
+ const logs = this.registry.get("log-store").getAll();
4106
+ const fetches = this.registry.get("fetch-store").getAll();
4406
4107
  const flows = groupRequestsIntoFlows(requests);
4407
4108
  this.cachedFindings = this.scanner.scan({ requests, logs });
4408
- if (this.findingStore) {
4109
+ if (this.registry.has("finding-store")) {
4110
+ const findingStore = this.registry.get("finding-store");
4409
4111
  for (const finding of this.cachedFindings) {
4410
- this.findingStore.upsert(finding, "passive");
4112
+ findingStore.upsert(finding, "passive");
4411
4113
  }
4412
- this.findingStore.reconcilePassive(this.cachedFindings);
4114
+ findingStore.reconcilePassive(this.cachedFindings);
4413
4115
  }
4414
4116
  this.cachedInsights = computeInsights({
4415
4117
  requests,
@@ -4417,15 +4119,17 @@ var init_engine = __esm({
4417
4119
  errors,
4418
4120
  flows,
4419
4121
  fetches,
4420
- previousMetrics: this.metricsStore.getAll(),
4122
+ previousMetrics: this.registry.get("metrics-store").getAll(),
4421
4123
  securityFindings: this.cachedFindings
4422
4124
  });
4423
- for (const fn of this.listeners) {
4424
- try {
4425
- fn(this.cachedInsights, this.cachedFindings);
4426
- } catch {
4427
- }
4428
- }
4125
+ this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
4126
+ const update = {
4127
+ insights: this.cachedInsights,
4128
+ findings: this.cachedFindings,
4129
+ statefulFindings: this.getStatefulFindings(),
4130
+ statefulInsights: this.cachedStatefulInsights
4131
+ };
4132
+ this.registry.get("event-bus").emit("analysis:updated", update);
4429
4133
  }
4430
4134
  };
4431
4135
  }
@@ -4443,7 +4147,7 @@ var init_src = __esm({
4443
4147
  init_engine();
4444
4148
  init_insights3();
4445
4149
  init_insights2();
4446
- VERSION = "0.8.0";
4150
+ VERSION = "0.8.2";
4447
4151
  }
4448
4152
  });
4449
4153
 
@@ -4621,7 +4325,7 @@ var init_thresholds2 = __esm({
4621
4325
  });
4622
4326
 
4623
4327
  // src/dashboard/client/constants/display.ts
4624
- var QUERY_OP_COLORS, LOG_LEVEL_COLORS, GRAPH_COLORS, DOT_COLORS, HEALTH_GRADES, CHART_GRID_COLOR, CHART_LABEL_COLOR, CHART_FONT, CHART_FONT_SM, CHART_FONT_XS, CHART_PAD, TL_TYPE_COLORS, TL_TYPE_LABELS, SENSITIVE_HEADERS, HTTP_STATUS_MAP, NAV_LABELS, CURL_SKIP_HEADERS;
4328
+ var QUERY_OP_COLORS, LOG_LEVEL_COLORS, GRAPH_COLORS, DOT_COLORS, HEALTH_GRADES, CHART_GRID_COLOR, CHART_LABEL_COLOR, CHART_FONT, CHART_FONT_SM, CHART_FONT_XS, CHART_PAD, TL_TYPE_COLORS, TL_TYPE_LABELS, SENSITIVE_HEADERS, HTTP_STATUS_MAP, NAV_LABELS, CURL_SKIP_HEADERS, SEVERITY_MAP;
4625
4329
  var init_display = __esm({
4626
4330
  "src/dashboard/client/constants/display.ts"() {
4627
4331
  "use strict";
@@ -4649,6 +4353,7 @@ var init_display = __esm({
4649
4353
  HTTP_STATUS_MAP = `{400:'Bad Request',401:'Unauthorized',403:'Forbidden',404:'Not Found',405:'Method Not Allowed',408:'Timeout',409:'Conflict',422:'Unprocessable',429:'Too Many Requests',500:'Internal Server Error',502:'Bad Gateway',503:'Service Unavailable',504:'Gateway Timeout'}`;
4650
4354
  NAV_LABELS = `{ queries: 'Queries', requests: 'Requests', actions: 'Actions', errors: 'Errors', security: 'Security', fetches: 'Fetches', logs: 'Logs', performance: 'Performance' }`;
4651
4355
  CURL_SKIP_HEADERS = `['host', 'connection', 'accept-encoding']`;
4356
+ SEVERITY_MAP = `{ critical: { icon: '\\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\\u2139', cls: 'info', sort: 2 } }`;
4652
4357
  }
4653
4358
  });
4654
4359
 
@@ -5920,7 +5625,7 @@ function getGraphDetail() {
5920
5625
  function renderEndpointDetail(container) {
5921
5626
  var ep = graphData.find(function(e) { return e.endpoint === selectedEndpoint; });
5922
5627
  if (!ep || !ep.requests || ep.requests.length === 0) {
5923
- container.innerHTML += '<div class="empty" style="height:300px"><span class="empty-sub">No data for this endpoint</span></div>';
5628
+ container.innerHTML += '<div class="empty"><span class="empty-sub">No data for this endpoint</span></div>';
5924
5629
  return;
5925
5630
  }
5926
5631
 
@@ -6368,7 +6073,7 @@ function getOverviewRender() {
6368
6073
  var hasData = nonStatic.length > 0 || state.queries.length > 0 || state.errors.length > 0;
6369
6074
 
6370
6075
  if (!hasData) {
6371
- container.innerHTML = '<div class="empty" style="height:400px"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see insights here</span></div>';
6076
+ container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see insights here</span></div>';
6372
6077
  return;
6373
6078
  }
6374
6079
 
@@ -6388,9 +6093,11 @@ function getOverviewRender() {
6388
6093
  '<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
6389
6094
  container.appendChild(summary);
6390
6095
 
6391
- var insights = state.insights || [];
6096
+ var all = state.insights || [];
6097
+ var open = all.filter(function(si) { return si.state === 'open'; });
6098
+ var resolved = all.filter(function(si) { return si.state === 'resolved'; });
6392
6099
 
6393
- if (insights.length === 0) {
6100
+ if (open.length === 0 && resolved.length === 0) {
6394
6101
  var clear = document.createElement('div');
6395
6102
  clear.className = 'ov-clear';
6396
6103
  clear.innerHTML = '<span class="ov-clear-icon">\\u2713</span>All clear \u2014 no issues detected';
@@ -6398,67 +6105,104 @@ function getOverviewRender() {
6398
6105
  return;
6399
6106
  }
6400
6107
 
6401
- var title = document.createElement('div');
6402
- title.className = 'ov-section-title';
6403
- title.innerHTML = 'Issues Found <span class="ov-issue-count">' + insights.length + '</span>';
6404
- container.appendChild(title);
6405
-
6406
- var cards = document.createElement('div');
6407
- cards.className = 'ov-cards';
6108
+ if (open.length === 0 && resolved.length > 0) {
6109
+ var allFixed = document.createElement('div');
6110
+ allFixed.className = 'ov-clear';
6111
+ allFixed.innerHTML = '<span class="ov-clear-icon">\\u2713</span>All issues resolved \u2014 ' + resolved.length + ' finding' + (resolved.length !== 1 ? 's were' : ' was') + ' detected and fixed';
6112
+ container.appendChild(allFixed);
6113
+ }
6408
6114
 
6409
6115
  var NAV_LABELS = ${NAV_LABELS};
6116
+ var SEV = ${SEVERITY_MAP};
6410
6117
 
6411
- for (var i = 0; i < insights.length; i++) {
6412
- (function(insight) {
6413
- var card = document.createElement('div');
6414
- card.className = 'ov-card';
6118
+ if (open.length > 0) {
6119
+ var title = document.createElement('div');
6120
+ title.className = 'ov-section-title';
6121
+ title.innerHTML = 'Issues Found <span class="ov-issue-count">' + open.length + '</span>';
6122
+ container.appendChild(title);
6123
+
6124
+ var cards = document.createElement('div');
6125
+ cards.className = 'ov-cards';
6126
+
6127
+ for (var i = 0; i < open.length; i++) {
6128
+ (function(si) {
6129
+ var insight = si.insight;
6130
+ var card = document.createElement('div');
6131
+ card.className = 'ov-card';
6132
+
6133
+ var sevCfg = SEV[insight.severity];
6134
+ var iconCls = sevCfg.cls;
6135
+ var iconChar = sevCfg.icon;
6136
+
6137
+ var expandHtml = '';
6138
+ if (insight.detail) expandHtml += insight.detail;
6139
+ if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6140
+ expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6141
+
6142
+ card.innerHTML =
6143
+ '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6144
+ '<div class="ov-card-body">' +
6145
+ '<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
6146
+ '<div class="ov-card-desc">' + insight.desc + '</div>' +
6147
+ '<div class="ov-card-expand">' + expandHtml + '</div>' +
6148
+ '</div>' +
6149
+ '<span class="ov-card-arrow">\\u2192</span>';
6150
+
6151
+ card.addEventListener('click', function(e) {
6152
+ var target = e.target;
6153
+ while (target && target !== card) {
6154
+ if (target.classList && target.classList.contains('ov-card-link')) {
6155
+ var navView = target.getAttribute('data-nav');
6156
+ var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
6157
+ if (sidebarItem) sidebarItem.click();
6158
+ return;
6159
+ }
6160
+ target = target.parentElement;
6161
+ }
6162
+ var expand = card.querySelector('.ov-card-expand');
6163
+ var arrow = card.querySelector('.ov-card-arrow');
6164
+ if (card.classList.contains('expanded')) {
6165
+ card.classList.remove('expanded');
6166
+ expand.style.display = 'none';
6167
+ arrow.textContent = '\\u2192';
6168
+ } else {
6169
+ card.classList.add('expanded');
6170
+ expand.style.display = 'block';
6171
+ arrow.textContent = '\\u2193';
6172
+ }
6173
+ });
6415
6174
 
6416
- var iconCls = insight.severity === 'critical' ? 'critical' : insight.severity === 'info' ? 'info' : 'warning';
6417
- var iconChar = insight.severity === 'critical' ? '\\u2717' : insight.severity === 'info' ? '\\u2139' : '\\u26A0';
6175
+ cards.appendChild(card);
6176
+ })(open[i]);
6177
+ }
6418
6178
 
6419
- var expandHtml = '';
6420
- if (insight.detail) expandHtml += insight.detail;
6421
- if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6422
- expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6179
+ container.appendChild(cards);
6180
+ }
6423
6181
 
6424
- card.innerHTML =
6425
- '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6182
+ if (resolved.length > 0) {
6183
+ var resolvedTitle = document.createElement('div');
6184
+ resolvedTitle.className = 'ov-section-title ov-resolved-title';
6185
+ resolvedTitle.innerHTML = '<span style="color:var(--green)">\\u2713</span> Resolved <span class="ov-issue-count">' + resolved.length + '</span>';
6186
+ container.appendChild(resolvedTitle);
6187
+
6188
+ var resolvedCards = document.createElement('div');
6189
+ resolvedCards.className = 'ov-cards';
6190
+
6191
+ for (var ri = 0; ri < resolved.length; ri++) {
6192
+ var rInsight = resolved[ri].insight;
6193
+ var rCard = document.createElement('div');
6194
+ rCard.className = 'ov-card ov-card-resolved';
6195
+ rCard.innerHTML =
6196
+ '<span class="ov-card-icon resolved">\\u2713</span>' +
6426
6197
  '<div class="ov-card-body">' +
6427
- '<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
6428
- '<div class="ov-card-desc">' + insight.desc + '</div>' +
6429
- '<div class="ov-card-expand">' + expandHtml + '</div>' +
6430
- '</div>' +
6431
- '<span class="ov-card-arrow">\\u2192</span>';
6432
-
6433
- card.addEventListener('click', function(e) {
6434
- var target = e.target;
6435
- while (target && target !== card) {
6436
- if (target.classList && target.classList.contains('ov-card-link')) {
6437
- var navView = target.getAttribute('data-nav');
6438
- var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
6439
- if (sidebarItem) sidebarItem.click();
6440
- return;
6441
- }
6442
- target = target.parentElement;
6443
- }
6444
- var expand = card.querySelector('.ov-card-expand');
6445
- var arrow = card.querySelector('.ov-card-arrow');
6446
- if (card.classList.contains('expanded')) {
6447
- card.classList.remove('expanded');
6448
- expand.style.display = 'none';
6449
- arrow.textContent = '\\u2192';
6450
- } else {
6451
- card.classList.add('expanded');
6452
- expand.style.display = 'block';
6453
- arrow.textContent = '\\u2193';
6454
- }
6455
- });
6198
+ '<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rInsight.title) + '</div>' +
6199
+ '<div class="ov-card-desc">' + rInsight.desc + '</div>' +
6200
+ '</div>';
6201
+ resolvedCards.appendChild(rCard);
6202
+ }
6456
6203
 
6457
- cards.appendChild(card);
6458
- })(insights[i]);
6204
+ container.appendChild(resolvedCards);
6459
6205
  }
6460
-
6461
- container.appendChild(cards);
6462
6206
  }
6463
6207
  `;
6464
6208
  }
@@ -6488,13 +6232,16 @@ function getSecurityView() {
6488
6232
  var container = document.getElementById('security-content');
6489
6233
  if (!container) return;
6490
6234
  container.innerHTML = '';
6235
+ var SEV = ${SEVERITY_MAP};
6491
6236
 
6492
- var findings = state.findings || [];
6237
+ var all = state.findings || [];
6238
+ var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
6239
+ var resolved = all.filter(function(f) { return f.state === 'resolved'; });
6493
6240
 
6494
- if (findings.length === 0) {
6241
+ if (open.length === 0 && resolved.length === 0) {
6495
6242
  var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
6496
6243
  if (!hasData) {
6497
- container.innerHTML = '<div class="empty" style="height:400px"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
6244
+ container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
6498
6245
  } else {
6499
6246
  container.innerHTML = '<div class="sec-clear"><span class="sec-clear-icon">\\u2713</span><div class="sec-clear-text"><div class="sec-clear-title">All clear</div><div class="sec-clear-sub">No security or quality issues detected this session</div></div></div>';
6500
6247
  }
@@ -6502,17 +6249,20 @@ function getSecurityView() {
6502
6249
  }
6503
6250
 
6504
6251
  var critCount = 0, warnCount = 0, infoCount = 0;
6505
- for (var ci = 0; ci < findings.length; ci++) {
6506
- if (findings[ci].severity === 'critical') critCount++;
6507
- else if (findings[ci].severity === 'info') infoCount++;
6252
+ for (var ci = 0; ci < open.length; ci++) {
6253
+ var sev = open[ci].finding.severity;
6254
+ if (sev === 'critical') critCount++;
6255
+ else if (sev === 'info') infoCount++;
6508
6256
  else warnCount++;
6509
6257
  }
6258
+
6510
6259
  var summaryEl = document.createElement('div');
6511
6260
  summaryEl.className = 'sec-summary';
6512
6261
  summaryEl.innerHTML =
6513
6262
  '<div class="sec-summary-left">' +
6514
- '<span class="sec-summary-count">' + findings.length + '</span>' +
6515
- '<span class="sec-summary-label">issue' + (findings.length !== 1 ? 's' : '') + ' found</span>' +
6263
+ '<span class="sec-summary-count">' + open.length + '</span>' +
6264
+ '<span class="sec-summary-label">open issue' + (open.length !== 1 ? 's' : '') + '</span>' +
6265
+ (resolved.length > 0 ? '<span class="sec-resolved-badge">' + resolved.length + ' resolved</span>' : '') +
6516
6266
  '</div>' +
6517
6267
  '<div class="sec-summary-right">' +
6518
6268
  (critCount > 0 ? '<span class="sec-badge critical">' + critCount + ' critical</span>' : '') +
@@ -6521,60 +6271,93 @@ function getSecurityView() {
6521
6271
  '</div>';
6522
6272
  container.appendChild(summaryEl);
6523
6273
 
6524
- var groups = {};
6525
- var groupOrder = [];
6526
- for (var gi = 0; gi < findings.length; gi++) {
6527
- var f = findings[gi];
6528
- if (!groups[f.rule]) {
6529
- groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6530
- groupOrder.push(f.rule);
6531
- }
6532
- groups[f.rule].items.push(f);
6274
+ if (open.length === 0 && resolved.length > 0) {
6275
+ var allFixed = document.createElement('div');
6276
+ allFixed.className = 'sec-clear';
6277
+ allFixed.innerHTML = '<span class="sec-clear-icon">\\u2713</span><div class="sec-clear-text"><div class="sec-clear-title">All issues resolved</div><div class="sec-clear-sub">' + resolved.length + ' finding' + (resolved.length !== 1 ? 's were' : ' was') + ' detected and fixed</div></div>';
6278
+ container.appendChild(allFixed);
6533
6279
  }
6534
6280
 
6535
- groupOrder.sort(function(a, b) {
6536
- var sa = groups[a].severity === 'critical' ? 0 : groups[a].severity === 'warning' ? 1 : 2;
6537
- var sb = groups[b].severity === 'critical' ? 0 : groups[b].severity === 'warning' ? 1 : 2;
6538
- if (sa !== sb) return sa - sb;
6539
- return groups[b].items.length - groups[a].items.length;
6540
- });
6281
+ if (open.length > 0) {
6282
+ var groups = {};
6283
+ var groupOrder = [];
6284
+ for (var gi = 0; gi < open.length; gi++) {
6285
+ var f = open[gi].finding;
6286
+ if (!groups[f.rule]) {
6287
+ groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6288
+ groupOrder.push(f.rule);
6289
+ }
6290
+ groups[f.rule].items.push(f);
6291
+ }
6541
6292
 
6542
- for (var oi = 0; oi < groupOrder.length; oi++) {
6543
- var group = groups[groupOrder[oi]];
6544
- var section = document.createElement('div');
6545
- section.className = 'sec-group';
6293
+ groupOrder.sort(function(a, b) {
6294
+ var sa = SEV[groups[a].severity].sort;
6295
+ var sb = SEV[groups[b].severity].sort;
6296
+ if (sa !== sb) return sa - sb;
6297
+ return groups[b].items.length - groups[a].items.length;
6298
+ });
6546
6299
 
6547
- var iconCls = group.severity === 'critical' ? 'critical' : group.severity === 'info' ? 'info' : 'warning';
6548
- var iconChar = group.severity === 'critical' ? '\\u2717' : group.severity === 'info' ? '\\u2139' : '\\u26A0';
6300
+ for (var oi = 0; oi < groupOrder.length; oi++) {
6301
+ var group = groups[groupOrder[oi]];
6302
+ var section = document.createElement('div');
6303
+ section.className = 'sec-group';
6304
+
6305
+ var sevCfg = SEV[group.severity];
6306
+ var iconCls = sevCfg.cls;
6307
+ var iconChar = sevCfg.icon;
6308
+
6309
+ var header = document.createElement('div');
6310
+ header.className = 'sec-group-header';
6311
+ header.innerHTML =
6312
+ '<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
6313
+ '<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
6314
+ '<span class="sec-group-count">' + group.items.length + '</span>';
6315
+ section.appendChild(header);
6316
+
6317
+ if (group.hint) {
6318
+ var hintEl = document.createElement('div');
6319
+ hintEl.className = 'sec-hint';
6320
+ hintEl.textContent = group.hint;
6321
+ section.appendChild(hintEl);
6322
+ }
6549
6323
 
6550
- var header = document.createElement('div');
6551
- header.className = 'sec-group-header';
6552
- header.innerHTML =
6553
- '<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
6554
- '<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
6555
- '<span class="sec-group-count">' + group.items.length + '</span>';
6556
- section.appendChild(header);
6557
-
6558
- if (group.hint) {
6559
- var hintEl = document.createElement('div');
6560
- hintEl.className = 'sec-hint';
6561
- hintEl.textContent = group.hint;
6562
- section.appendChild(hintEl);
6563
- }
6564
-
6565
- var list = document.createElement('div');
6566
- list.className = 'sec-items';
6567
- for (var ii = 0; ii < group.items.length; ii++) {
6568
- var item = group.items[ii];
6569
- var row = document.createElement('div');
6570
- row.className = 'sec-item';
6571
- row.innerHTML =
6572
- '<div class="sec-item-desc">' + item.desc + '</div>' +
6573
- (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
6574
- list.appendChild(row);
6324
+ var list = document.createElement('div');
6325
+ list.className = 'sec-items';
6326
+ for (var ii = 0; ii < group.items.length; ii++) {
6327
+ var item = group.items[ii];
6328
+ var row = document.createElement('div');
6329
+ row.className = 'sec-item';
6330
+ row.innerHTML =
6331
+ '<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
6332
+ (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
6333
+ list.appendChild(row);
6334
+ }
6335
+ section.appendChild(list);
6336
+ container.appendChild(section);
6575
6337
  }
6576
- section.appendChild(list);
6577
- container.appendChild(section);
6338
+ }
6339
+
6340
+ if (resolved.length > 0) {
6341
+ var resolvedTitle = document.createElement('div');
6342
+ resolvedTitle.className = 'sec-resolved-title';
6343
+ resolvedTitle.innerHTML = '<span class="sec-resolved-check">\\u2713</span> Resolved <span class="sec-resolved-count">' + resolved.length + '</span>';
6344
+ container.appendChild(resolvedTitle);
6345
+
6346
+ var resolvedGroup = document.createElement('div');
6347
+ resolvedGroup.className = 'sec-group sec-group-resolved';
6348
+ var resolvedItems = document.createElement('div');
6349
+ resolvedItems.className = 'sec-items';
6350
+ for (var ri = 0; ri < resolved.length; ri++) {
6351
+ var rf = resolved[ri].finding;
6352
+ var rRow = document.createElement('div');
6353
+ rRow.className = 'sec-item sec-item-resolved';
6354
+ rRow.innerHTML =
6355
+ '<span class="sec-resolved-item-icon">\\u2713</span>' +
6356
+ '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>';
6357
+ resolvedItems.appendChild(rRow);
6358
+ }
6359
+ resolvedGroup.appendChild(resolvedItems);
6360
+ container.appendChild(resolvedGroup);
6578
6361
  }
6579
6362
  }
6580
6363
  `;
@@ -6582,6 +6365,7 @@ function getSecurityView() {
6582
6365
  var init_security3 = __esm({
6583
6366
  "src/dashboard/client/views/security.ts"() {
6584
6367
  "use strict";
6368
+ init_display();
6585
6369
  }
6586
6370
  });
6587
6371
 
@@ -6642,41 +6426,20 @@ function getApp() {
6642
6426
  }
6643
6427
  };
6644
6428
 
6645
- events.addEventListener('fetch', function(e) {
6646
- var f = JSON.parse(e.data);
6647
- state.fetches.unshift(f);
6648
- if (state.fetches.length > ${MAX_TELEMETRY_ENTRIES}) state.fetches.pop();
6649
- prependFetchRow(f);
6650
- updateStats();
6651
- if (f.parentRequestId) { invalidateTimelineCache(f.parentRequestId); refreshVisibleTimeline(f.parentRequestId); }
6652
- });
6653
-
6654
- events.addEventListener('log', function(e) {
6655
- var l = JSON.parse(e.data);
6656
- state.logs.unshift(l);
6657
- if (state.logs.length > ${MAX_TELEMETRY_ENTRIES}) state.logs.pop();
6658
- prependLogRow(l);
6659
- updateStats();
6660
- if (l.parentRequestId) { invalidateTimelineCache(l.parentRequestId); refreshVisibleTimeline(l.parentRequestId); }
6661
- });
6662
-
6663
- events.addEventListener('error_event', function(e) {
6664
- var err = JSON.parse(e.data);
6665
- state.errors.unshift(err);
6666
- if (state.errors.length > ${MAX_TELEMETRY_ENTRIES}) state.errors.pop();
6667
- prependErrorRow(err);
6668
- updateStats();
6669
- if (err.parentRequestId) { invalidateTimelineCache(err.parentRequestId); refreshVisibleTimeline(err.parentRequestId); }
6670
- });
6671
-
6672
- events.addEventListener('query', function(e) {
6673
- var q = JSON.parse(e.data);
6674
- state.queries.unshift(q);
6675
- if (state.queries.length > ${MAX_TELEMETRY_ENTRIES}) state.queries.pop();
6676
- prependQueryRow(q);
6677
- updateStats();
6678
- if (q.parentRequestId) { invalidateTimelineCache(q.parentRequestId); refreshVisibleTimeline(q.parentRequestId); }
6679
- });
6429
+ function registerTelemetryListener(eventName, stateKey, prependFn) {
6430
+ events.addEventListener(eventName, function(e) {
6431
+ var item = JSON.parse(e.data);
6432
+ state[stateKey].unshift(item);
6433
+ if (state[stateKey].length > ${MAX_TELEMETRY_ENTRIES}) state[stateKey].pop();
6434
+ prependFn(item);
6435
+ updateStats();
6436
+ if (item.parentRequestId) { invalidateTimelineCache(item.parentRequestId); refreshVisibleTimeline(item.parentRequestId); }
6437
+ });
6438
+ }
6439
+ registerTelemetryListener('fetch', 'fetches', prependFetchRow);
6440
+ registerTelemetryListener('log', 'logs', prependLogRow);
6441
+ registerTelemetryListener('error_event', 'errors', prependErrorRow);
6442
+ registerTelemetryListener('query', 'queries', prependQueryRow);
6680
6443
 
6681
6444
  events.addEventListener('insights', function(e) {
6682
6445
  state.insights = JSON.parse(e.data);
@@ -6689,6 +6452,12 @@ function getApp() {
6689
6452
  if (state.activeView === 'security') renderSecurity();
6690
6453
  updateStats();
6691
6454
  });
6455
+
6456
+ window.addEventListener('beforeunload', function() {
6457
+ events.close();
6458
+ clearTimeout(reloadTimer);
6459
+ clearTimeout(perfReloadTimer);
6460
+ });
6692
6461
  }
6693
6462
 
6694
6463
  async function reloadFlows() {
@@ -6762,244 +6531,749 @@ function getApp() {
6762
6531
  if (queryCount) queryCount.textContent = state.queries.length;
6763
6532
  var secCount = document.getElementById('sidebar-count-security');
6764
6533
  if (secCount) {
6765
- var numFindings = (state.findings || []).length;
6534
+ var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
6766
6535
  secCount.textContent = numFindings;
6767
6536
  secCount.style.display = numFindings > 0 ? '' : 'none';
6768
6537
  }
6769
6538
  }
6770
6539
 
6771
- function copyAsCurl(req) {
6772
- var headers = Object.entries(req.headers || {})
6773
- .filter(function(e) { return ${CURL_SKIP_HEADERS}.indexOf(e[0]) === -1; })
6774
- .map(function(e) { return "-H '" + e[0] + ": " + e[1] + "'"; })
6775
- .join(' ');
6776
- var body = req.requestBody ? " -d '" + req.requestBody.replace(/'/g, "'\\\\''") + "'" : '';
6777
- var curl = "curl -X " + req.method + " " + headers + body + " 'http://localhost:" + PORT + req.url + "'";
6778
- navigator.clipboard.writeText(curl).then(function() { showToast('Copied cURL command'); });
6540
+ function copyAsCurl(req) {
6541
+ var headers = Object.entries(req.headers || {})
6542
+ .filter(function(e) { return ${CURL_SKIP_HEADERS}.indexOf(e[0]) === -1; })
6543
+ .map(function(e) { return "-H '" + e[0] + ": " + e[1] + "'"; })
6544
+ .join(' ');
6545
+ var body = req.requestBody ? " -d '" + req.requestBody.replace(/'/g, "'\\\\''") + "'" : '';
6546
+ var curl = "curl -X " + req.method + " " + headers + body + " 'http://localhost:" + PORT + req.url + "'";
6547
+ navigator.clipboard.writeText(curl).then(function() { showToast('Copied cURL command'); });
6548
+ }
6549
+
6550
+ document.getElementById('clear-btn').addEventListener('click', async function() {
6551
+ if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
6552
+ await fetch('${DASHBOARD_API_CLEAR}', {method: 'POST'});
6553
+ state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
6554
+ state.insights = []; state.findings = [];
6555
+ graphData = []; selectedEndpoint = ${ALL_ENDPOINTS_SELECTOR}; timelineCache = {};
6556
+ renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
6557
+ showToast('Cleared');
6558
+ });
6559
+
6560
+ init();
6561
+ `;
6562
+ }
6563
+ var init_app2 = __esm({
6564
+ "src/dashboard/client/app.ts"() {
6565
+ "use strict";
6566
+ init_constants();
6567
+ init_constants2();
6568
+ }
6569
+ });
6570
+
6571
+ // src/dashboard/client/index.ts
6572
+ function getClientScript(config) {
6573
+ return `
6574
+ (function(){
6575
+ var PORT = ${config.proxyPort};
6576
+ var state = { flows: [], requests: [], fetches: [], errors: [], logs: [], queries: [], insights: [], findings: [], viewMode: 'simple', activeView: 'overview' };
6577
+
6578
+ var appEl = document.getElementById('app');
6579
+ var flowListEl = document.getElementById('flow-list');
6580
+ var reqListEl = document.getElementById('request-list');
6581
+ var emptyFlows = document.getElementById('empty-flows');
6582
+ var toastEl = document.getElementById('toast');
6583
+
6584
+ ${getHelpers()}
6585
+ ${getTelemetryViewHelpers()}
6586
+ ${getSqlUtils()}
6587
+ ${getFlowsView()}
6588
+ ${getRequestsView()}
6589
+ ${getFetchesView()}
6590
+ ${getErrorsView()}
6591
+ ${getLogsView()}
6592
+ ${getQueriesView()}
6593
+ ${getTimelineView()}
6594
+ ${getGraphView()}
6595
+ ${getOverviewView()}
6596
+ ${getSecurityView()}
6597
+ ${getApp()}
6598
+ })();
6599
+ `;
6600
+ }
6601
+ var init_client = __esm({
6602
+ "src/dashboard/client/index.ts"() {
6603
+ "use strict";
6604
+ init_helpers();
6605
+ init_view_helpers();
6606
+ init_sql_utils();
6607
+ init_flows2();
6608
+ init_requests2();
6609
+ init_fetches();
6610
+ init_errors2();
6611
+ init_logs();
6612
+ init_queries();
6613
+ init_timeline2();
6614
+ init_graph2();
6615
+ init_overview3();
6616
+ init_security3();
6617
+ init_app2();
6618
+ }
6619
+ });
6620
+
6621
+ // src/dashboard/page.ts
6622
+ function getDashboardHtml(config) {
6623
+ return `<!DOCTYPE html>
6624
+ <html lang="en">
6625
+ <head>
6626
+ <meta charset="UTF-8">
6627
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6628
+ <title>brakit</title>
6629
+ <style>${getStyles()}</style>
6630
+ </head>
6631
+ <body>
6632
+ ${getLayoutHtml(config)}
6633
+ <script>${getClientScript(config)}</script>
6634
+ </body>
6635
+ </html>`;
6636
+ }
6637
+ var init_page = __esm({
6638
+ "src/dashboard/page.ts"() {
6639
+ "use strict";
6640
+ init_styles();
6641
+ init_layout2();
6642
+ init_client();
6643
+ }
6644
+ });
6645
+
6646
+ // src/telemetry/config.ts
6647
+ import { homedir } from "os";
6648
+ import { join as join2 } from "path";
6649
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6650
+ import { randomUUID as randomUUID3 } from "crypto";
6651
+ function readConfig() {
6652
+ try {
6653
+ if (!existsSync4(CONFIG_PATH)) return null;
6654
+ return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
6655
+ } catch {
6656
+ return null;
6657
+ }
6658
+ }
6659
+ function isTelemetryEnabled() {
6660
+ const env = process.env.BRAKIT_TELEMETRY;
6661
+ if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
6662
+ return readConfig()?.telemetry ?? true;
6663
+ }
6664
+ var CONFIG_DIR, CONFIG_PATH;
6665
+ var init_config = __esm({
6666
+ "src/telemetry/config.ts"() {
6667
+ "use strict";
6668
+ CONFIG_DIR = join2(homedir(), ".brakit");
6669
+ CONFIG_PATH = join2(CONFIG_DIR, "config.json");
6670
+ }
6671
+ });
6672
+
6673
+ // src/telemetry/index.ts
6674
+ import { platform, release, arch } from "os";
6675
+ function recordTabViewed(tab) {
6676
+ tabsViewed.add(tab);
6677
+ }
6678
+ function recordDashboardOpened() {
6679
+ dashboardOpened = true;
6680
+ }
6681
+ var tabsViewed, dashboardOpened;
6682
+ var init_telemetry = __esm({
6683
+ "src/telemetry/index.ts"() {
6684
+ "use strict";
6685
+ init_src();
6686
+ init_config();
6687
+ init_config();
6688
+ tabsViewed = /* @__PURE__ */ new Set();
6689
+ dashboardOpened = false;
6690
+ }
6691
+ });
6692
+
6693
+ // src/dashboard/router.ts
6694
+ function isDashboardRequest(url) {
6695
+ return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
6696
+ }
6697
+ function createDashboardHandler(registry) {
6698
+ const metricsStore = registry.get("metrics-store");
6699
+ const analysisEngine = registry.has("analysis-engine") ? registry.get("analysis-engine") : void 0;
6700
+ const routes = {
6701
+ [DASHBOARD_API_REQUESTS]: createRequestsHandler(registry),
6702
+ [DASHBOARD_API_EVENTS]: createSSEHandler(registry),
6703
+ [DASHBOARD_API_FLOWS]: createFlowsHandler(registry),
6704
+ [DASHBOARD_API_CLEAR]: createClearHandler(registry),
6705
+ [DASHBOARD_API_LOGS]: createLogsHandler(registry),
6706
+ [DASHBOARD_API_FETCHES]: createFetchesHandler(registry),
6707
+ [DASHBOARD_API_ERRORS]: createErrorsHandler(registry),
6708
+ [DASHBOARD_API_QUERIES]: createQueriesHandler(registry),
6709
+ [DASHBOARD_API_METRICS]: createMetricsHandler(metricsStore),
6710
+ [DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(metricsStore),
6711
+ [DASHBOARD_API_INGEST]: createIngestHandler(registry),
6712
+ [DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
6713
+ };
6714
+ if (analysisEngine) {
6715
+ routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(analysisEngine);
6716
+ routes[DASHBOARD_API_SECURITY] = createSecurityHandler(analysisEngine);
6717
+ }
6718
+ if (registry.has("finding-store")) {
6719
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(registry.get("finding-store"));
6720
+ }
6721
+ routes[DASHBOARD_API_TAB] = (req, res) => {
6722
+ const raw = (req.url ?? "").split("tab=")[1];
6723
+ if (raw) {
6724
+ const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
6725
+ if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
6726
+ }
6727
+ res.writeHead(204);
6728
+ res.end();
6729
+ };
6730
+ return (req, res, config) => {
6731
+ const path = (req.url ?? "/").split("?")[0];
6732
+ const handler = routes[path];
6733
+ if (handler) {
6734
+ handler(req, res);
6735
+ return;
6736
+ }
6737
+ if (isTelemetryEnabled()) recordDashboardOpened();
6738
+ res.writeHead(200, {
6739
+ "content-type": "text/html; charset=utf-8",
6740
+ "cache-control": "no-cache",
6741
+ ...SECURITY_HEADERS
6742
+ });
6743
+ res.end(getDashboardHtml(config));
6744
+ };
6745
+ }
6746
+ var SECURITY_HEADERS;
6747
+ var init_router = __esm({
6748
+ "src/dashboard/router.ts"() {
6749
+ "use strict";
6750
+ init_constants();
6751
+ init_api();
6752
+ init_insights();
6753
+ init_findings();
6754
+ init_sse();
6755
+ init_page();
6756
+ init_telemetry();
6757
+ SECURITY_HEADERS = {
6758
+ "x-content-type-options": "nosniff",
6759
+ "x-frame-options": "DENY",
6760
+ "referrer-policy": "no-referrer",
6761
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
6762
+ };
6763
+ }
6764
+ });
6765
+
6766
+ // src/core/event-bus.ts
6767
+ var EventBus;
6768
+ var init_event_bus = __esm({
6769
+ "src/core/event-bus.ts"() {
6770
+ "use strict";
6771
+ EventBus = class {
6772
+ listeners = /* @__PURE__ */ new Map();
6773
+ emit(channel, data) {
6774
+ const set = this.listeners.get(channel);
6775
+ if (!set) return;
6776
+ for (const fn of set) {
6777
+ try {
6778
+ fn(data);
6779
+ } catch {
6780
+ }
6781
+ }
6782
+ }
6783
+ on(channel, fn) {
6784
+ let set = this.listeners.get(channel);
6785
+ if (!set) {
6786
+ set = /* @__PURE__ */ new Set();
6787
+ this.listeners.set(channel, set);
6788
+ }
6789
+ set.add(fn);
6790
+ return () => set.delete(fn);
6791
+ }
6792
+ off(channel, fn) {
6793
+ this.listeners.get(channel)?.delete(fn);
6794
+ }
6795
+ };
6796
+ }
6797
+ });
6798
+
6799
+ // src/core/service-registry.ts
6800
+ var ServiceRegistry;
6801
+ var init_service_registry = __esm({
6802
+ "src/core/service-registry.ts"() {
6803
+ "use strict";
6804
+ ServiceRegistry = class {
6805
+ services = /* @__PURE__ */ new Map();
6806
+ register(name, service) {
6807
+ this.services.set(name, service);
6808
+ }
6809
+ get(name) {
6810
+ const service = this.services.get(name);
6811
+ if (!service) throw new Error(`Service "${name}" not registered`);
6812
+ return service;
6813
+ }
6814
+ has(name) {
6815
+ return this.services.has(name);
6816
+ }
6817
+ };
6818
+ }
6819
+ });
6820
+
6821
+ // src/utils/static-patterns.ts
6822
+ function isStaticPath(urlPath) {
6823
+ return STATIC_PATTERNS.some((p) => p.test(urlPath));
6824
+ }
6825
+ var STATIC_PATTERNS;
6826
+ var init_static_patterns = __esm({
6827
+ "src/utils/static-patterns.ts"() {
6828
+ "use strict";
6829
+ STATIC_PATTERNS = [
6830
+ /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
6831
+ /^\/favicon/,
6832
+ /^\/node_modules\//,
6833
+ // Framework-specific static/internal paths
6834
+ /^\/_next\//,
6835
+ /^\/__nextjs/,
6836
+ /^\/@vite\//,
6837
+ /^\/__vite/
6838
+ ];
6839
+ }
6840
+ });
6841
+
6842
+ // src/store/request-store.ts
6843
+ function flattenHeaders(headers) {
6844
+ const flat = {};
6845
+ for (const [key, value] of Object.entries(headers)) {
6846
+ if (value === void 0) continue;
6847
+ flat[key] = Array.isArray(value) ? value.join(", ") : value;
6848
+ }
6849
+ return flat;
6850
+ }
6851
+ var RequestStore;
6852
+ var init_request_store = __esm({
6853
+ "src/store/request-store.ts"() {
6854
+ "use strict";
6855
+ init_constants();
6856
+ init_static_patterns();
6857
+ RequestStore = class {
6858
+ constructor(maxEntries = MAX_REQUEST_ENTRIES) {
6859
+ this.maxEntries = maxEntries;
6860
+ }
6861
+ requests = [];
6862
+ listeners = [];
6863
+ capture(input) {
6864
+ const url = input.url;
6865
+ const path = url.split("?")[0];
6866
+ let requestBodyStr = null;
6867
+ if (input.requestBody && input.requestBody.length > 0) {
6868
+ requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
6869
+ }
6870
+ let responseBodyStr = null;
6871
+ if (input.responseBody && input.responseBody.length > 0) {
6872
+ const ct = input.responseContentType;
6873
+ if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
6874
+ responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
6875
+ }
6876
+ }
6877
+ const entry = {
6878
+ id: input.requestId,
6879
+ method: input.method,
6880
+ url,
6881
+ path,
6882
+ headers: flattenHeaders(input.requestHeaders),
6883
+ requestBody: requestBodyStr,
6884
+ statusCode: input.statusCode,
6885
+ responseHeaders: flattenHeaders(input.responseHeaders),
6886
+ responseBody: responseBodyStr,
6887
+ startedAt: input.startTime,
6888
+ durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
6889
+ responseSize: input.responseBody?.length ?? 0,
6890
+ isStatic: isStaticPath(path)
6891
+ };
6892
+ this.requests.push(entry);
6893
+ if (this.requests.length > this.maxEntries) {
6894
+ this.requests.shift();
6895
+ }
6896
+ for (const fn of this.listeners) {
6897
+ fn(entry);
6898
+ }
6899
+ return entry;
6900
+ }
6901
+ getAll() {
6902
+ return this.requests;
6903
+ }
6904
+ clear() {
6905
+ this.requests.length = 0;
6906
+ }
6907
+ onRequest(fn) {
6908
+ this.listeners.push(fn);
6909
+ }
6910
+ offRequest(fn) {
6911
+ const idx = this.listeners.indexOf(fn);
6912
+ if (idx !== -1) this.listeners.splice(idx, 1);
6913
+ }
6914
+ };
6915
+ }
6916
+ });
6917
+
6918
+ // src/store/telemetry-store.ts
6919
+ import { randomUUID as randomUUID4 } from "crypto";
6920
+ var TelemetryStore;
6921
+ var init_telemetry_store = __esm({
6922
+ "src/store/telemetry-store.ts"() {
6923
+ "use strict";
6924
+ init_constants();
6925
+ TelemetryStore = class {
6926
+ constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
6927
+ this.maxEntries = maxEntries;
6928
+ }
6929
+ entries = [];
6930
+ listeners = [];
6931
+ add(data) {
6932
+ const entry = { id: randomUUID4(), ...data };
6933
+ this.entries.push(entry);
6934
+ if (this.entries.length > this.maxEntries) this.entries.shift();
6935
+ for (const fn of this.listeners) fn(entry);
6936
+ return entry;
6937
+ }
6938
+ getAll() {
6939
+ return this.entries;
6940
+ }
6941
+ getByRequest(requestId) {
6942
+ return this.entries.filter((e) => e.parentRequestId === requestId);
6943
+ }
6944
+ clear() {
6945
+ this.entries.length = 0;
6946
+ }
6947
+ onEntry(fn) {
6948
+ this.listeners.push(fn);
6949
+ }
6950
+ offEntry(fn) {
6951
+ const idx = this.listeners.indexOf(fn);
6952
+ if (idx !== -1) this.listeners.splice(idx, 1);
6953
+ }
6954
+ };
6955
+ }
6956
+ });
6957
+
6958
+ // src/store/fetch-store.ts
6959
+ var FetchStore;
6960
+ var init_fetch_store = __esm({
6961
+ "src/store/fetch-store.ts"() {
6962
+ "use strict";
6963
+ init_telemetry_store();
6964
+ FetchStore = class extends TelemetryStore {
6965
+ };
6779
6966
  }
6967
+ });
6780
6968
 
6781
- document.getElementById('clear-btn').addEventListener('click', async function() {
6782
- if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
6783
- await fetch('${DASHBOARD_API_CLEAR}', {method: 'POST'});
6784
- state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
6785
- state.insights = []; state.findings = [];
6786
- graphData = []; selectedEndpoint = ${ALL_ENDPOINTS_SELECTOR}; timelineCache = {};
6787
- renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
6788
- showToast('Cleared');
6789
- });
6790
-
6791
- init();
6792
- `;
6793
- }
6794
- var init_app2 = __esm({
6795
- "src/dashboard/client/app.ts"() {
6969
+ // src/store/log-store.ts
6970
+ var LogStore;
6971
+ var init_log_store = __esm({
6972
+ "src/store/log-store.ts"() {
6796
6973
  "use strict";
6797
- init_constants();
6798
- init_constants2();
6974
+ init_telemetry_store();
6975
+ LogStore = class extends TelemetryStore {
6976
+ };
6799
6977
  }
6800
6978
  });
6801
6979
 
6802
- // src/dashboard/client/index.ts
6803
- function getClientScript(config) {
6804
- return `
6805
- (function(){
6806
- var PORT = ${config.proxyPort};
6807
- var state = { flows: [], requests: [], fetches: [], errors: [], logs: [], queries: [], insights: [], findings: [], viewMode: 'simple', activeView: 'overview' };
6808
-
6809
- var appEl = document.getElementById('app');
6810
- var flowListEl = document.getElementById('flow-list');
6811
- var reqListEl = document.getElementById('request-list');
6812
- var emptyFlows = document.getElementById('empty-flows');
6813
- var toastEl = document.getElementById('toast');
6980
+ // src/store/error-store.ts
6981
+ var ErrorStore;
6982
+ var init_error_store = __esm({
6983
+ "src/store/error-store.ts"() {
6984
+ "use strict";
6985
+ init_telemetry_store();
6986
+ ErrorStore = class extends TelemetryStore {
6987
+ };
6988
+ }
6989
+ });
6814
6990
 
6815
- ${getHelpers()}
6816
- ${getTelemetryViewHelpers()}
6817
- ${getSqlUtils()}
6818
- ${getFlowsView()}
6819
- ${getRequestsView()}
6820
- ${getFetchesView()}
6821
- ${getErrorsView()}
6822
- ${getLogsView()}
6823
- ${getQueriesView()}
6824
- ${getTimelineView()}
6825
- ${getGraphView()}
6826
- ${getOverviewView()}
6827
- ${getSecurityView()}
6828
- ${getApp()}
6829
- })();
6830
- `;
6831
- }
6832
- var init_client = __esm({
6833
- "src/dashboard/client/index.ts"() {
6991
+ // src/store/query-store.ts
6992
+ var QueryStore;
6993
+ var init_query_store = __esm({
6994
+ "src/store/query-store.ts"() {
6834
6995
  "use strict";
6835
- init_helpers();
6836
- init_view_helpers();
6837
- init_sql_utils();
6838
- init_flows2();
6839
- init_requests2();
6840
- init_fetches();
6841
- init_errors2();
6842
- init_logs();
6843
- init_queries();
6844
- init_timeline2();
6845
- init_graph2();
6846
- init_overview3();
6847
- init_security3();
6848
- init_app2();
6996
+ init_telemetry_store();
6997
+ QueryStore = class extends TelemetryStore {
6998
+ };
6849
6999
  }
6850
7000
  });
6851
7001
 
6852
- // src/dashboard/page.ts
6853
- function getDashboardHtml(config) {
6854
- return `<!DOCTYPE html>
6855
- <html lang="en">
6856
- <head>
6857
- <meta charset="UTF-8">
6858
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6859
- <title>brakit</title>
6860
- <style>${getStyles()}</style>
6861
- </head>
6862
- <body>
6863
- ${getLayoutHtml(config)}
6864
- <script>${getClientScript(config)}</script>
6865
- </body>
6866
- </html>`;
7002
+ // src/utils/math.ts
7003
+ function percentile(values, p) {
7004
+ if (values.length === 0) return 0;
7005
+ const sorted = [...values].sort((a, b) => a - b);
7006
+ const idx = Math.ceil(p * sorted.length) - 1;
7007
+ return Math.round(sorted[Math.max(0, idx)]);
6867
7008
  }
6868
- var init_page = __esm({
6869
- "src/dashboard/page.ts"() {
7009
+ var init_math = __esm({
7010
+ "src/utils/math.ts"() {
6870
7011
  "use strict";
6871
- init_styles();
6872
- init_layout2();
6873
- init_client();
6874
7012
  }
6875
7013
  });
6876
7014
 
6877
- // src/telemetry/config.ts
6878
- import { homedir } from "os";
6879
- import { join as join2 } from "path";
6880
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
7015
+ // src/store/metrics/metrics-store.ts
6881
7016
  import { randomUUID as randomUUID5 } from "crypto";
6882
- function readConfig() {
6883
- try {
6884
- if (!existsSync4(CONFIG_PATH)) return null;
6885
- return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
6886
- } catch {
6887
- return null;
6888
- }
6889
- }
6890
- function isTelemetryEnabled() {
6891
- const env = process.env.BRAKIT_TELEMETRY;
6892
- if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
6893
- return readConfig()?.telemetry ?? true;
7017
+ function createAccumulator() {
7018
+ return {
7019
+ durations: [],
7020
+ queryCounts: [],
7021
+ errorCount: 0,
7022
+ totalDurationSum: 0,
7023
+ totalRequestCount: 0,
7024
+ totalErrorCount: 0,
7025
+ totalQuerySum: 0,
7026
+ totalQueryTimeMs: 0,
7027
+ totalFetchTimeMs: 0
7028
+ };
6894
7029
  }
6895
- var CONFIG_DIR, CONFIG_PATH;
6896
- var init_config = __esm({
6897
- "src/telemetry/config.ts"() {
7030
+ var MetricsStore;
7031
+ var init_metrics_store = __esm({
7032
+ "src/store/metrics/metrics-store.ts"() {
6898
7033
  "use strict";
6899
- CONFIG_DIR = join2(homedir(), ".brakit");
6900
- CONFIG_PATH = join2(CONFIG_DIR, "config.json");
7034
+ init_constants();
7035
+ init_math();
7036
+ init_endpoint();
7037
+ MetricsStore = class {
7038
+ constructor(persistence) {
7039
+ this.persistence = persistence;
7040
+ this.data = persistence.load();
7041
+ for (const ep of this.data.endpoints) {
7042
+ this.endpointIndex.set(ep.endpoint, ep);
7043
+ }
7044
+ }
7045
+ data;
7046
+ endpointIndex = /* @__PURE__ */ new Map();
7047
+ sessionId = randomUUID5();
7048
+ sessionStart = Date.now();
7049
+ flushTimer = null;
7050
+ accumulators = /* @__PURE__ */ new Map();
7051
+ pendingPoints = /* @__PURE__ */ new Map();
7052
+ start() {
7053
+ this.flushTimer = setInterval(
7054
+ () => this.flush(),
7055
+ METRICS_FLUSH_INTERVAL_MS
7056
+ );
7057
+ this.flushTimer.unref();
7058
+ }
7059
+ stop() {
7060
+ if (this.flushTimer) {
7061
+ clearInterval(this.flushTimer);
7062
+ this.flushTimer = null;
7063
+ }
7064
+ this.flush(true);
7065
+ }
7066
+ recordRequest(req, metrics) {
7067
+ if (req.isStatic) return;
7068
+ const key = getEndpointKey(req.method, req.path);
7069
+ let acc = this.accumulators.get(key);
7070
+ if (!acc) {
7071
+ acc = createAccumulator();
7072
+ this.accumulators.set(key, acc);
7073
+ }
7074
+ acc.durations.push(req.durationMs);
7075
+ acc.queryCounts.push(metrics.queryCount);
7076
+ if (req.statusCode >= 400) acc.errorCount++;
7077
+ acc.totalDurationSum += req.durationMs;
7078
+ acc.totalRequestCount++;
7079
+ acc.totalQuerySum += metrics.queryCount;
7080
+ acc.totalQueryTimeMs += metrics.queryTimeMs;
7081
+ acc.totalFetchTimeMs += metrics.fetchTimeMs;
7082
+ if (req.statusCode >= 400) acc.totalErrorCount++;
7083
+ const timestamp = Math.round(
7084
+ Date.now() - (performance.now() - req.startedAt)
7085
+ );
7086
+ const point = {
7087
+ timestamp,
7088
+ durationMs: req.durationMs,
7089
+ statusCode: req.statusCode,
7090
+ queryCount: metrics.queryCount,
7091
+ queryTimeMs: metrics.queryTimeMs,
7092
+ fetchTimeMs: metrics.fetchTimeMs
7093
+ };
7094
+ let pending2 = this.pendingPoints.get(key);
7095
+ if (!pending2) {
7096
+ pending2 = [];
7097
+ this.pendingPoints.set(key, pending2);
7098
+ }
7099
+ pending2.push(point);
7100
+ }
7101
+ getAll() {
7102
+ return this.data.endpoints;
7103
+ }
7104
+ getEndpoint(endpoint) {
7105
+ return this.endpointIndex.get(endpoint);
7106
+ }
7107
+ getLiveEndpoints() {
7108
+ const merged = /* @__PURE__ */ new Map();
7109
+ for (const ep of this.data.endpoints) {
7110
+ if (ep.dataPoints && ep.dataPoints.length > 0) {
7111
+ merged.set(ep.endpoint, ep.dataPoints);
7112
+ }
7113
+ }
7114
+ for (const [endpoint, points] of this.pendingPoints) {
7115
+ const existing = merged.get(endpoint);
7116
+ merged.set(endpoint, existing ? existing.concat(points) : points);
7117
+ }
7118
+ const endpoints = [];
7119
+ for (const [endpoint, requests] of merged) {
7120
+ if (requests.length === 0) continue;
7121
+ const durations = requests.map((r) => r.durationMs);
7122
+ const errors = requests.filter((r) => r.statusCode >= 400).length;
7123
+ const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
7124
+ const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
7125
+ const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
7126
+ const n = requests.length;
7127
+ const avgDurationMs = Math.round(durations.reduce((s, d) => s + d, 0) / n);
7128
+ const avgQueryTimeMs = Math.round(totalQueryTime / n);
7129
+ const avgFetchTimeMs = Math.round(totalFetchTime / n);
7130
+ endpoints.push({
7131
+ endpoint,
7132
+ requests,
7133
+ summary: {
7134
+ p95Ms: percentile(durations, 0.95),
7135
+ errorRate: errors / n,
7136
+ avgQueryCount: Math.round(totalQueries / n),
7137
+ totalRequests: n,
7138
+ avgQueryTimeMs,
7139
+ avgFetchTimeMs,
7140
+ avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
7141
+ }
7142
+ });
7143
+ }
7144
+ endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
7145
+ return endpoints;
7146
+ }
7147
+ reset() {
7148
+ this.data = { version: 1, endpoints: [] };
7149
+ this.endpointIndex.clear();
7150
+ this.accumulators.clear();
7151
+ this.pendingPoints.clear();
7152
+ this.persistence.remove();
7153
+ }
7154
+ flush(sync = false) {
7155
+ for (const [endpoint, acc] of this.accumulators) {
7156
+ if (acc.durations.length === 0) continue;
7157
+ const n = acc.totalRequestCount;
7158
+ const session = {
7159
+ sessionId: this.sessionId,
7160
+ startedAt: this.sessionStart,
7161
+ avgDurationMs: Math.round(acc.totalDurationSum / n),
7162
+ p95DurationMs: percentile(acc.durations, 0.95),
7163
+ requestCount: n,
7164
+ errorCount: acc.totalErrorCount,
7165
+ avgQueryCount: n > 0 ? Math.round(acc.totalQuerySum / n) : 0,
7166
+ avgQueryTimeMs: n > 0 ? Math.round(acc.totalQueryTimeMs / n) : 0,
7167
+ avgFetchTimeMs: n > 0 ? Math.round(acc.totalFetchTimeMs / n) : 0
7168
+ };
7169
+ const epMetrics = this.getOrCreateEndpoint(endpoint);
7170
+ const existingIdx = epMetrics.sessions.findIndex(
7171
+ (s) => s.sessionId === this.sessionId
7172
+ );
7173
+ if (existingIdx !== -1) {
7174
+ epMetrics.sessions[existingIdx] = session;
7175
+ } else {
7176
+ epMetrics.sessions.push(session);
7177
+ }
7178
+ if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
7179
+ epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
7180
+ }
7181
+ acc.durations.length = 0;
7182
+ acc.queryCounts.length = 0;
7183
+ acc.errorCount = 0;
7184
+ }
7185
+ for (const [endpoint, points] of this.pendingPoints) {
7186
+ if (points.length === 0) continue;
7187
+ const epMetrics = this.getOrCreateEndpoint(endpoint);
7188
+ const existing = epMetrics.dataPoints ?? [];
7189
+ epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
7190
+ }
7191
+ this.pendingPoints.clear();
7192
+ if (sync) {
7193
+ this.persistence.saveSync(this.data);
7194
+ } else {
7195
+ this.persistence.save(this.data);
7196
+ }
7197
+ }
7198
+ getOrCreateEndpoint(endpoint) {
7199
+ let ep = this.endpointIndex.get(endpoint);
7200
+ if (!ep) {
7201
+ ep = { endpoint, sessions: [] };
7202
+ this.data.endpoints.push(ep);
7203
+ this.endpointIndex.set(endpoint, ep);
7204
+ }
7205
+ return ep;
7206
+ }
7207
+ };
6901
7208
  }
6902
7209
  });
6903
7210
 
6904
- // src/telemetry/index.ts
6905
- import { platform, release, arch } from "os";
6906
- function recordTabViewed(tab) {
6907
- tabsViewed.add(tab);
6908
- }
6909
- function recordDashboardOpened() {
6910
- dashboardOpened = true;
6911
- }
6912
- var tabsViewed, dashboardOpened;
6913
- var init_telemetry = __esm({
6914
- "src/telemetry/index.ts"() {
7211
+ // src/store/metrics/persistence.ts
7212
+ import { readFileSync as readFileSync4, existsSync as existsSync5, unlinkSync } from "fs";
7213
+ import { resolve as resolve3 } from "path";
7214
+ var FileMetricsPersistence;
7215
+ var init_persistence = __esm({
7216
+ "src/store/metrics/persistence.ts"() {
6915
7217
  "use strict";
6916
- init_src();
6917
- init_store();
6918
- init_config();
6919
- init_config();
6920
- tabsViewed = /* @__PURE__ */ new Set();
6921
- dashboardOpened = false;
7218
+ init_constants();
7219
+ init_atomic_writer();
7220
+ init_log();
7221
+ FileMetricsPersistence = class {
7222
+ metricsPath;
7223
+ writer;
7224
+ constructor(rootDir) {
7225
+ this.metricsPath = resolve3(rootDir, METRICS_FILE);
7226
+ this.writer = new AtomicWriter({
7227
+ dir: resolve3(rootDir, METRICS_DIR),
7228
+ filePath: this.metricsPath,
7229
+ gitignoreEntry: METRICS_DIR,
7230
+ label: "metrics"
7231
+ });
7232
+ }
7233
+ load() {
7234
+ try {
7235
+ if (existsSync5(this.metricsPath)) {
7236
+ const raw = readFileSync4(this.metricsPath, "utf-8");
7237
+ const parsed = JSON.parse(raw);
7238
+ if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
7239
+ return parsed;
7240
+ }
7241
+ }
7242
+ } catch (err) {
7243
+ brakitWarn(`failed to load metrics: ${err.message}`);
7244
+ }
7245
+ return { version: 1, endpoints: [] };
7246
+ }
7247
+ save(data) {
7248
+ this.writer.writeAsync(JSON.stringify(data));
7249
+ }
7250
+ saveSync(data) {
7251
+ this.writer.writeSync(JSON.stringify(data));
7252
+ }
7253
+ remove() {
7254
+ try {
7255
+ if (existsSync5(this.metricsPath)) {
7256
+ unlinkSync(this.metricsPath);
7257
+ }
7258
+ } catch {
7259
+ }
7260
+ }
7261
+ };
6922
7262
  }
6923
7263
  });
6924
7264
 
6925
- // src/dashboard/router.ts
6926
- function isDashboardRequest(url) {
6927
- return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
6928
- }
6929
- function createDashboardHandler(deps) {
6930
- const routes = {
6931
- [DASHBOARD_API_REQUESTS]: handleApiRequests,
6932
- [DASHBOARD_API_EVENTS]: createSSEHandler(deps.analysisEngine),
6933
- [DASHBOARD_API_FLOWS]: handleApiFlows,
6934
- [DASHBOARD_API_CLEAR]: createClearHandler(deps.metricsStore),
6935
- [DASHBOARD_API_LOGS]: handleApiLogs,
6936
- [DASHBOARD_API_FETCHES]: handleApiFetches,
6937
- [DASHBOARD_API_ERRORS]: handleApiErrors,
6938
- [DASHBOARD_API_QUERIES]: handleApiQueries,
6939
- [DASHBOARD_API_METRICS]: createMetricsHandler(deps.metricsStore),
6940
- [DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(deps.metricsStore),
6941
- [DASHBOARD_API_INGEST]: handleApiIngest,
6942
- [DASHBOARD_API_ACTIVITY]: handleApiActivity
6943
- };
6944
- if (deps.analysisEngine) {
6945
- routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(deps.analysisEngine);
6946
- routes[DASHBOARD_API_SECURITY] = createSecurityHandler(deps.analysisEngine);
6947
- }
6948
- if (deps.findingStore) {
6949
- routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(deps.findingStore);
6950
- }
6951
- routes[DASHBOARD_API_TAB] = (req, res) => {
6952
- const raw = (req.url ?? "").split("tab=")[1];
6953
- if (raw) {
6954
- const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
6955
- if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
6956
- }
6957
- res.writeHead(204);
6958
- res.end();
6959
- };
6960
- return (req, res, config) => {
6961
- const path = (req.url ?? "/").split("?")[0];
6962
- const handler = routes[path];
6963
- if (handler) {
6964
- handler(req, res);
6965
- return;
6966
- }
6967
- if (isTelemetryEnabled()) recordDashboardOpened();
6968
- res.writeHead(200, {
6969
- "content-type": "text/html; charset=utf-8",
6970
- "cache-control": "no-cache",
6971
- ...SECURITY_HEADERS
6972
- });
6973
- res.end(getDashboardHtml(config));
6974
- };
6975
- }
6976
- var VALID_TABS, SECURITY_HEADERS;
6977
- var init_router = __esm({
6978
- "src/dashboard/router.ts"() {
7265
+ // src/store/index.ts
7266
+ var init_store = __esm({
7267
+ "src/store/index.ts"() {
6979
7268
  "use strict";
6980
- init_constants();
6981
- init_api();
6982
- init_insights();
6983
- init_findings();
6984
- init_sse();
6985
- init_page();
6986
- init_telemetry();
6987
- VALID_TABS = /* @__PURE__ */ new Set([
6988
- "overview",
6989
- "actions",
6990
- "requests",
6991
- "fetches",
6992
- "queries",
6993
- "errors",
6994
- "logs",
6995
- "performance",
6996
- "security"
6997
- ]);
6998
- SECURITY_HEADERS = {
6999
- "x-content-type-options": "nosniff",
7000
- "x-frame-options": "DENY",
7001
- "referrer-policy": "no-referrer"
7002
- };
7269
+ init_request_store();
7270
+ init_telemetry_store();
7271
+ init_fetch_store();
7272
+ init_log_store();
7273
+ init_error_store();
7274
+ init_query_store();
7275
+ init_metrics_store();
7276
+ init_persistence();
7003
7277
  }
7004
7278
  });
7005
7279
 
@@ -7009,16 +7283,13 @@ function print(line) {
7009
7283
  process.stdout.write(line + "\n");
7010
7284
  }
7011
7285
  function severityIcon(severity) {
7012
- if (severity === "critical") return pc.red("\u2717");
7013
- if (severity === "warning") return pc.yellow("\u26A0");
7014
- return pc.dim("\u25CB");
7286
+ return SEVERITY_COLOR[severity](SEVERITY_ICON[severity]);
7015
7287
  }
7016
7288
  function colorTitle(severity, text) {
7017
- if (severity === "critical") return pc.red(pc.bold(text));
7018
- if (severity === "warning") return pc.yellow(pc.bold(text));
7019
- return pc.dim(text);
7289
+ const color = SEVERITY_COLOR[severity];
7290
+ return severity === "info" ? color(text) : color(pc.bold(text));
7020
7291
  }
7021
- function truncate(s, max = 80) {
7292
+ function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
7022
7293
  return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
7023
7294
  }
7024
7295
  function formatConsoleLine(insight, suffix) {
@@ -7032,40 +7303,70 @@ function formatConsoleLine(insight, suffix) {
7032
7303
  }
7033
7304
  return line;
7034
7305
  }
7035
- function createConsoleInsightListener(proxyPort, metricsStore) {
7306
+ function startTerminalInsights(registry, proxyPort) {
7307
+ const bus = registry.get("event-bus");
7308
+ const metricsStore = registry.get("metrics-store");
7036
7309
  const printedKeys = /* @__PURE__ */ new Set();
7310
+ const resolvedKeys = /* @__PURE__ */ new Set();
7037
7311
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
7038
- return (insights) => {
7039
- const lines = [];
7040
- for (const insight of insights) {
7041
- if (insight.severity === "info") continue;
7042
- const endpoint = insight.desc.match(/^(\S+\s+\S+)/)?.[1] ?? insight.desc;
7043
- const key = `${insight.type}:${endpoint}`;
7044
- if (printedKeys.has(key)) continue;
7045
- printedKeys.add(key);
7312
+ return bus.on("analysis:updated", ({ statefulInsights }) => {
7313
+ const newLines = [];
7314
+ const resolvedLines = [];
7315
+ for (const si of statefulInsights) {
7316
+ if (si.state === "resolved") {
7317
+ if (resolvedKeys.has(si.key)) continue;
7318
+ resolvedKeys.add(si.key);
7319
+ printedKeys.delete(si.key);
7320
+ const title = pc.green(pc.bold(`\u2713 ${si.insight.title}`));
7321
+ const desc = pc.dim(truncate(si.insight.desc));
7322
+ resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
7323
+ continue;
7324
+ }
7325
+ resolvedKeys.delete(si.key);
7326
+ if (si.insight.severity === "info") continue;
7327
+ if (printedKeys.has(si.key)) continue;
7328
+ printedKeys.add(si.key);
7046
7329
  let suffix;
7047
- if (insight.type === "slow") {
7048
- const ep = metricsStore.getEndpoint(endpoint);
7049
- if (ep && ep.sessions.length > 1) {
7050
- const prev = ep.sessions[ep.sessions.length - 2];
7051
- suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
7330
+ if (si.insight.type === "slow") {
7331
+ const endpoint = extractEndpointFromDesc(si.insight.desc);
7332
+ if (endpoint) {
7333
+ const ep = metricsStore.getEndpoint(endpoint);
7334
+ if (ep && ep.sessions.length > 1) {
7335
+ const prev = ep.sessions[ep.sessions.length - 2];
7336
+ suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
7337
+ }
7052
7338
  }
7053
7339
  }
7054
- lines.push(formatConsoleLine(insight, suffix));
7340
+ newLines.push(formatConsoleLine(si.insight, suffix));
7055
7341
  }
7056
- if (lines.length > 0) {
7342
+ if (newLines.length > 0) {
7057
7343
  print("");
7058
- for (const line of lines) print(line);
7344
+ for (const line of newLines) print(line);
7059
7345
  print("");
7060
7346
  print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.dim("Dashboard:")} ${pc.underline(`http://${dashUrl}`)} ${pc.dim("or ask your AI:")} ${pc.bold('"Fix brakit findings"')}`);
7061
7347
  }
7062
- };
7348
+ if (resolvedLines.length > 0) {
7349
+ print("");
7350
+ for (const line of resolvedLines) print(line);
7351
+ print("");
7352
+ print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.green("Issues fixed!")}`);
7353
+ }
7354
+ });
7063
7355
  }
7356
+ var SEVERITY_COLOR;
7064
7357
  var init_terminal = __esm({
7065
7358
  "src/output/terminal.ts"() {
7066
7359
  "use strict";
7067
7360
  init_src();
7068
7361
  init_constants();
7362
+ init_limits();
7363
+ init_severity();
7364
+ init_endpoint();
7365
+ SEVERITY_COLOR = {
7366
+ critical: pc.red,
7367
+ warning: pc.yellow,
7368
+ info: pc.dim
7369
+ };
7069
7370
  }
7070
7371
  });
7071
7372
 
@@ -7144,10 +7445,11 @@ function outgoingToIncoming(headers) {
7144
7445
  }
7145
7446
  function decompress(body, encoding) {
7146
7447
  try {
7147
- if (encoding === "gzip") return gunzipSync(body);
7148
- if (encoding === "br") return brotliDecompressSync(body);
7149
- if (encoding === "deflate") return inflateSync(body);
7150
- } catch {
7448
+ if (encoding === CONTENT_ENCODING_GZIP) return gunzipSync(body);
7449
+ if (encoding === CONTENT_ENCODING_BR) return brotliDecompressSync(body);
7450
+ if (encoding === CONTENT_ENCODING_DEFLATE) return inflateSync(body);
7451
+ } catch (e) {
7452
+ brakitDebug(`decompress failed: ${e.message}`);
7151
7453
  }
7152
7454
  return body;
7153
7455
  }
@@ -7157,7 +7459,7 @@ function toBuffer(chunk) {
7157
7459
  if (typeof chunk === "string") return Buffer.from(chunk);
7158
7460
  return null;
7159
7461
  }
7160
- function captureInProcess(req, res, requestId) {
7462
+ function captureInProcess(req, res, requestId, requestStore) {
7161
7463
  const startTime = performance.now();
7162
7464
  const method = req.method ?? "GET";
7163
7465
  const resChunks = [];
@@ -7174,7 +7476,8 @@ function captureInProcess(req, res, requestId) {
7174
7476
  resSize += buf.length;
7175
7477
  }
7176
7478
  }
7177
- } catch {
7479
+ } catch (e) {
7480
+ brakitDebug(`capture write: ${e.message}`);
7178
7481
  }
7179
7482
  return originalWrite.apply(this, args);
7180
7483
  };
@@ -7187,7 +7490,8 @@ function captureInProcess(req, res, requestId) {
7187
7490
  resChunks.push(buf);
7188
7491
  }
7189
7492
  }
7190
- } catch {
7493
+ } catch (e) {
7494
+ brakitDebug(`capture end: ${e.message}`);
7191
7495
  }
7192
7496
  const result = originalEnd.apply(this, args);
7193
7497
  const endTime = performance.now();
@@ -7197,7 +7501,7 @@ function captureInProcess(req, res, requestId) {
7197
7501
  if (body && encoding) {
7198
7502
  body = decompress(body, encoding);
7199
7503
  }
7200
- defaultStore.capture({
7504
+ requestStore.capture({
7201
7505
  requestId,
7202
7506
  method,
7203
7507
  url: req.url ?? "/",
@@ -7211,7 +7515,8 @@ function captureInProcess(req, res, requestId) {
7211
7515
  endTime,
7212
7516
  config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
7213
7517
  });
7214
- } catch {
7518
+ } catch (e) {
7519
+ brakitDebug(`capture store: ${e.message}`);
7215
7520
  }
7216
7521
  return result;
7217
7522
  };
@@ -7219,8 +7524,8 @@ function captureInProcess(req, res, requestId) {
7219
7524
  var init_capture = __esm({
7220
7525
  "src/runtime/capture.ts"() {
7221
7526
  "use strict";
7222
- init_request_log();
7223
7527
  init_constants();
7528
+ init_log();
7224
7529
  }
7225
7530
  });
7226
7531
 
@@ -7246,6 +7551,10 @@ function installInterceptor(deps) {
7246
7551
  deps.onFirstRequest(port);
7247
7552
  }
7248
7553
  }
7554
+ const localPort = req.socket.localPort;
7555
+ if (bannerPrinted && localPort && deps.config.proxyPort && localPort !== deps.config.proxyPort) {
7556
+ return original.apply(this, [event, ...args]);
7557
+ }
7249
7558
  if (isDashboardRequest(url)) {
7250
7559
  if (!isLocalRequest(req)) {
7251
7560
  res.writeHead(404);
@@ -7261,7 +7570,7 @@ function installInterceptor(deps) {
7261
7570
  url,
7262
7571
  method: req.method ?? "GET"
7263
7572
  };
7264
- captureInProcess(req, res, requestId);
7573
+ captureInProcess(req, res, requestId, deps.requestStore);
7265
7574
  return storage.run(
7266
7575
  ctx,
7267
7576
  () => original.apply(this, [event, ...args])
@@ -7293,91 +7602,115 @@ var setup_exports = {};
7293
7602
  __export(setup_exports, {
7294
7603
  setup: () => setup
7295
7604
  });
7296
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
7605
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
7297
7606
  import { resolve as resolve4 } from "path";
7298
7607
  function setup() {
7299
7608
  if (initialized) return;
7300
7609
  initialized = true;
7301
- setEmitter(routeEvent2);
7302
- setupFetchHook();
7303
- setupConsoleHook();
7304
- setupErrorHook();
7305
- const registry = createDefaultRegistry();
7306
- registry.patchAll(routeEvent2);
7610
+ const bus = new EventBus();
7611
+ const registry = new ServiceRegistry();
7612
+ const requestStore = new RequestStore();
7613
+ const fetchStore = new FetchStore();
7614
+ const logStore = new LogStore();
7615
+ const errorStore = new ErrorStore();
7616
+ const queryStore = new QueryStore();
7617
+ registry.register("event-bus", bus);
7618
+ registry.register("request-store", requestStore);
7619
+ registry.register("fetch-store", fetchStore);
7620
+ registry.register("log-store", logStore);
7621
+ registry.register("error-store", errorStore);
7622
+ registry.register("query-store", queryStore);
7623
+ bus.on("telemetry:fetch", (data) => fetchStore.add(data));
7624
+ bus.on("telemetry:query", (data) => queryStore.add(data));
7625
+ bus.on("telemetry:log", (data) => logStore.add(data));
7626
+ bus.on("telemetry:error", (data) => errorStore.add(data));
7627
+ requestStore.onRequest((req) => bus.emit("request:completed", req));
7628
+ const telemetryEmit = (event) => {
7629
+ const channel = `telemetry:${event.type}`;
7630
+ bus.emit(channel, event.data);
7631
+ };
7632
+ setupFetchHook(telemetryEmit);
7633
+ setupConsoleHook(telemetryEmit);
7634
+ setupErrorHook(telemetryEmit);
7635
+ const adapterRegistry = createDefaultRegistry();
7636
+ adapterRegistry.patchAll(telemetryEmit);
7307
7637
  const cwd = process.cwd();
7308
7638
  const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
7309
7639
  metricsStore.start();
7640
+ registry.register("metrics-store", metricsStore);
7310
7641
  const findingStore = new FindingStore(cwd);
7311
7642
  findingStore.start();
7312
- const analysisEngine = new AnalysisEngine(metricsStore, findingStore);
7643
+ registry.register("finding-store", findingStore);
7644
+ const analysisEngine = new AnalysisEngine(registry);
7313
7645
  analysisEngine.start();
7314
- const config = {
7315
- proxyPort: 0,
7316
- targetPort: 0,
7317
- showStatic: false,
7318
- maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
7319
- };
7320
- const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine, findingStore });
7321
- onRequest((req) => {
7322
- const queries = defaultQueryStore.getByRequest(req.id);
7323
- const fetches = defaultFetchStore.getByRequest(req.id);
7646
+ registry.register("analysis-engine", analysisEngine);
7647
+ bus.on("request:completed", (req) => {
7648
+ const queries = queryStore.getByRequest(req.id);
7649
+ const fetches = fetchStore.getByRequest(req.id);
7324
7650
  metricsStore.recordRequest(req, {
7325
7651
  queryCount: queries.length,
7326
7652
  queryTimeMs: queries.reduce((s, q) => s + q.durationMs, 0),
7327
7653
  fetchTimeMs: fetches.reduce((s, f) => s + f.durationMs, 0)
7328
7654
  });
7329
7655
  });
7656
+ const config = {
7657
+ proxyPort: 0,
7658
+ targetPort: 0,
7659
+ showStatic: false,
7660
+ maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
7661
+ };
7662
+ const handleDashboard = createDashboardHandler(registry);
7663
+ let terminalDispose = null;
7330
7664
  installInterceptor({
7331
7665
  handleDashboard,
7332
7666
  config,
7667
+ requestStore,
7333
7668
  onFirstRequest(port) {
7669
+ setBrakitPort(port);
7334
7670
  const dir = resolve4(cwd, METRICS_DIR);
7335
- if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
7336
- writeFileSync5(resolve4(cwd, PORT_FILE), String(port));
7337
- analysisEngine.onUpdate(createConsoleInsightListener(port, metricsStore));
7671
+ if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
7672
+ const portPath = resolve4(cwd, PORT_FILE);
7673
+ if (existsSync6(portPath)) {
7674
+ const old = readFileSync5(portPath, "utf-8").trim();
7675
+ if (old && old !== String(port)) {
7676
+ brakitDebug(`Overwriting stale port file (was ${old}, now ${port})`);
7677
+ }
7678
+ }
7679
+ writeFileSync4(portPath, String(port));
7680
+ terminalDispose = startTerminalInsights(registry, port);
7338
7681
  process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
7339
7682
  `);
7340
7683
  }
7341
7684
  });
7342
7685
  health.setTeardown(() => {
7343
7686
  uninstallInterceptor();
7687
+ terminalDispose?.();
7344
7688
  analysisEngine.stop();
7345
7689
  findingStore.stop();
7346
7690
  metricsStore.stop();
7347
7691
  try {
7348
7692
  const portPath = resolve4(cwd, PORT_FILE);
7349
- if (existsSync5(portPath)) unlinkSync2(portPath);
7693
+ if (existsSync6(portPath)) unlinkSync2(portPath);
7350
7694
  } catch {
7351
7695
  }
7352
7696
  });
7353
7697
  }
7354
- function routeEvent2(event) {
7355
- switch (event.type) {
7356
- case "fetch":
7357
- defaultFetchStore.add(event.data);
7358
- break;
7359
- case "log":
7360
- defaultLogStore.add(event.data);
7361
- break;
7362
- case "error":
7363
- defaultErrorStore.add(event.data);
7364
- break;
7365
- case "query":
7366
- defaultQueryStore.add(event.data);
7367
- break;
7368
- }
7369
- }
7370
7698
  var initialized;
7371
7699
  var init_setup = __esm({
7372
7700
  "src/runtime/setup.ts"() {
7373
7701
  "use strict";
7374
- init_transport2();
7375
7702
  init_fetch();
7376
7703
  init_console();
7377
7704
  init_errors();
7378
7705
  init_adapters();
7379
7706
  init_router();
7380
- init_request_log();
7707
+ init_event_bus();
7708
+ init_service_registry();
7709
+ init_request_store();
7710
+ init_fetch_store();
7711
+ init_log_store();
7712
+ init_error_store();
7713
+ init_query_store();
7381
7714
  init_store();
7382
7715
  init_finding_store();
7383
7716
  init_engine();
@@ -7386,6 +7719,7 @@ var init_setup = __esm({
7386
7719
  init_constants();
7387
7720
  init_health2();
7388
7721
  init_interceptor();
7722
+ init_log();
7389
7723
  initialized = false;
7390
7724
  }
7391
7725
  });