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.
- package/README.md +17 -6
- package/dist/api.d.ts +165 -82
- package/dist/api.js +245 -262
- package/dist/bin/brakit.js +132 -210
- package/dist/mcp/server.js +65 -15
- package/dist/runtime/index.js +2192 -1858
- package/package.json +1 -1
package/dist/runtime/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
285
|
+
path,
|
|
227
286
|
startTime: performance.now(),
|
|
228
|
-
parentRequestId: ctx
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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/
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
};
|
|
1279
|
-
defaultLogStore = new LogStore();
|
|
1227
|
+
init_constants();
|
|
1228
|
+
init_limits();
|
|
1280
1229
|
}
|
|
1281
1230
|
});
|
|
1282
1231
|
|
|
1283
|
-
// src/
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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/
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
};
|
|
1303
|
-
defaultQueryStore = new QueryStore();
|
|
1453
|
+
init_limits();
|
|
1454
|
+
init_shared2();
|
|
1304
1455
|
}
|
|
1305
1456
|
});
|
|
1306
1457
|
|
|
1307
|
-
// src/
|
|
1308
|
-
function
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
|
1315
|
-
"src/
|
|
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/
|
|
1321
|
-
function
|
|
1322
|
-
return
|
|
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
|
|
1325
|
-
"src/
|
|
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/
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
|
1346
|
-
|
|
1347
|
-
"src/store/metrics/metrics-store.ts"() {
|
|
1537
|
+
var init_activity = __esm({
|
|
1538
|
+
"src/dashboard/api/activity.ts"() {
|
|
1348
1539
|
"use strict";
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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/
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
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/
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
|
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
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
|
1813
|
-
"src/dashboard/
|
|
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/
|
|
1824
|
-
function
|
|
1825
|
-
return
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
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
|
|
1946
|
-
"src/dashboard/
|
|
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/
|
|
1954
|
-
function
|
|
1955
|
-
return
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
}
|
|
1967
|
-
var
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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
|
-
|
|
2039
|
-
var
|
|
2040
|
-
|
|
2041
|
-
|
|
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
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
2115
|
-
|
|
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
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
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
|
|
2164
|
-
"src/dashboard/
|
|
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/
|
|
2173
|
-
function
|
|
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:
|
|
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:
|
|
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/
|
|
2730
|
-
import {
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
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
|
|
2736
|
-
"src/
|
|
2270
|
+
var init_fs = __esm({
|
|
2271
|
+
"src/utils/fs.ts"() {
|
|
2737
2272
|
"use strict";
|
|
2738
2273
|
}
|
|
2739
2274
|
});
|
|
2740
2275
|
|
|
2741
|
-
// src/
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
|
|
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
|
-
|
|
2387
|
+
init_atomic_writer();
|
|
2757
2388
|
init_finding_id();
|
|
2758
2389
|
FindingStore = class {
|
|
2759
2390
|
constructor(rootDir) {
|
|
2760
2391
|
this.rootDir = rootDir;
|
|
2761
|
-
|
|
2762
|
-
this.findingsPath =
|
|
2763
|
-
this.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2870
|
-
|
|
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
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
4346
|
-
this.
|
|
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
|
-
|
|
4360
|
-
boundRequestListener;
|
|
4361
|
-
boundQueryListener;
|
|
4362
|
-
boundErrorListener;
|
|
4363
|
-
boundLogListener;
|
|
4067
|
+
subs = new SubscriptionBag();
|
|
4364
4068
|
start() {
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
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
|
-
|
|
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 =
|
|
4402
|
-
const queries =
|
|
4403
|
-
const errors =
|
|
4404
|
-
const logs =
|
|
4405
|
-
const fetches =
|
|
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.
|
|
4109
|
+
if (this.registry.has("finding-store")) {
|
|
4110
|
+
const findingStore = this.registry.get("finding-store");
|
|
4409
4111
|
for (const finding of this.cachedFindings) {
|
|
4410
|
-
|
|
4112
|
+
findingStore.upsert(finding, "passive");
|
|
4411
4113
|
}
|
|
4412
|
-
|
|
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.
|
|
4122
|
+
previousMetrics: this.registry.get("metrics-store").getAll(),
|
|
4421
4123
|
securityFindings: this.cachedFindings
|
|
4422
4124
|
});
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
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.
|
|
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"
|
|
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"
|
|
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
|
|
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 (
|
|
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
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
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
|
-
|
|
6412
|
-
(
|
|
6413
|
-
|
|
6414
|
-
|
|
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
|
-
|
|
6417
|
-
|
|
6175
|
+
cards.appendChild(card);
|
|
6176
|
+
})(open[i]);
|
|
6177
|
+
}
|
|
6418
6178
|
|
|
6419
|
-
|
|
6420
|
-
|
|
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
|
-
|
|
6425
|
-
|
|
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(
|
|
6428
|
-
'<div class="ov-card-desc">' +
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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"
|
|
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 <
|
|
6506
|
-
|
|
6507
|
-
|
|
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">' +
|
|
6515
|
-
'<span class="sec-summary-label">issue' + (
|
|
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
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
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
|
-
|
|
6536
|
-
var
|
|
6537
|
-
var
|
|
6538
|
-
|
|
6539
|
-
|
|
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
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
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
|
|
6548
|
-
|
|
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
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
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
|
-
|
|
6577
|
-
|
|
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
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
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
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
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
|
-
|
|
6798
|
-
|
|
6974
|
+
init_telemetry_store();
|
|
6975
|
+
LogStore = class extends TelemetryStore {
|
|
6976
|
+
};
|
|
6799
6977
|
}
|
|
6800
6978
|
});
|
|
6801
6979
|
|
|
6802
|
-
// src/
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
(
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
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
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
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
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
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/
|
|
6853
|
-
function
|
|
6854
|
-
return
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
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
|
|
6869
|
-
"src/
|
|
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/
|
|
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
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
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
|
|
6896
|
-
var
|
|
6897
|
-
"src/
|
|
7030
|
+
var MetricsStore;
|
|
7031
|
+
var init_metrics_store = __esm({
|
|
7032
|
+
"src/store/metrics/metrics-store.ts"() {
|
|
6898
7033
|
"use strict";
|
|
6899
|
-
|
|
6900
|
-
|
|
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/
|
|
6905
|
-
import {
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
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
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
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/
|
|
6926
|
-
|
|
6927
|
-
|
|
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
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7018
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
7039
|
-
const
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
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
|
|
7049
|
-
if (
|
|
7050
|
-
const
|
|
7051
|
-
|
|
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
|
-
|
|
7340
|
+
newLines.push(formatConsoleLine(si.insight, suffix));
|
|
7055
7341
|
}
|
|
7056
|
-
if (
|
|
7342
|
+
if (newLines.length > 0) {
|
|
7057
7343
|
print("");
|
|
7058
|
-
for (const line of
|
|
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 ===
|
|
7148
|
-
if (encoding ===
|
|
7149
|
-
if (encoding ===
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
const
|
|
7306
|
-
|
|
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
|
-
|
|
7643
|
+
registry.register("finding-store", findingStore);
|
|
7644
|
+
const analysisEngine = new AnalysisEngine(registry);
|
|
7313
7645
|
analysisEngine.start();
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
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 (!
|
|
7336
|
-
|
|
7337
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
});
|