brakit 0.8.0 → 0.8.1

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.
@@ -34,7 +34,7 @@ var init_routes = __esm({
34
34
  });
35
35
 
36
36
  // src/constants/limits.ts
37
- var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH;
37
+ 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
38
  var init_limits = __esm({
39
39
  "src/constants/limits.ts"() {
40
40
  "use strict";
@@ -43,11 +43,15 @@ var init_limits = __esm({
43
43
  DEFAULT_API_LIMIT = 500;
44
44
  MAX_TELEMETRY_ENTRIES = 1e3;
45
45
  MAX_TAB_NAME_LENGTH = 32;
46
+ MAX_INGEST_BYTES = 10 * 1024 * 1024;
47
+ TERMINAL_TRUNCATE_LENGTH = 80;
48
+ SENSITIVE_MASK_MIN_LENGTH = 8;
49
+ SENSITIVE_MASK_VISIBLE_CHARS = 4;
46
50
  }
47
51
  });
48
52
 
49
53
  // 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;
54
+ 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
55
  var init_thresholds = __esm({
52
56
  "src/constants/thresholds.ts"() {
53
57
  "use strict";
@@ -77,6 +81,9 @@ var init_thresholds = __esm({
77
81
  OVERFETCH_MANY_FIELDS = 12;
78
82
  OVERFETCH_UNWRAP_MIN_SIZE = 3;
79
83
  MAX_DUPLICATE_INSIGHTS = 3;
84
+ INSIGHT_WINDOW_PER_ENDPOINT = 2;
85
+ RESOLVE_AFTER_ABSENCES = 3;
86
+ RESOLVED_INSIGHT_TTL_MS = 18e5;
80
87
  }
81
88
  });
82
89
 
@@ -110,9 +117,18 @@ var init_metrics = __esm({
110
117
  });
111
118
 
112
119
  // src/constants/headers.ts
120
+ var SENSITIVE_HEADER_NAMES;
113
121
  var init_headers = __esm({
114
122
  "src/constants/headers.ts"() {
115
123
  "use strict";
124
+ SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
125
+ "authorization",
126
+ "cookie",
127
+ "set-cookie",
128
+ "proxy-authorization",
129
+ "x-api-key",
130
+ "x-auth-token"
131
+ ]);
116
132
  }
117
133
  });
118
134
 
@@ -1321,9 +1337,14 @@ var init_math = __esm({
1321
1337
  function getEndpointKey(method, path) {
1322
1338
  return `${method} ${path}`;
1323
1339
  }
1340
+ function extractEndpointFromDesc(desc) {
1341
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1342
+ }
1343
+ var ENDPOINT_PREFIX_RE;
1324
1344
  var init_endpoint = __esm({
1325
1345
  "src/utils/endpoint.ts"() {
1326
1346
  "use strict";
1347
+ ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1327
1348
  }
1328
1349
  });
1329
1350
 
@@ -1666,7 +1687,7 @@ function maskSensitiveHeaders(headers) {
1666
1687
  for (const [key, value] of Object.entries(headers)) {
1667
1688
  if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1668
1689
  const s = String(value);
1669
- masked[key] = s.length <= 8 ? "****" : s.slice(0, 4) + "..." + s.slice(-4);
1690
+ masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? "****" : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
1670
1691
  } else {
1671
1692
  masked[key] = value;
1672
1693
  }
@@ -1713,19 +1734,11 @@ function handleTelemetryGet(req, res, store) {
1713
1734
  const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1714
1735
  sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1715
1736
  }
1716
- var SENSITIVE_HEADER_NAMES;
1717
1737
  var init_shared2 = __esm({
1718
1738
  "src/dashboard/api/shared.ts"() {
1719
1739
  "use strict";
1720
1740
  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
- ]);
1741
+ init_limits();
1729
1742
  }
1730
1743
  });
1731
1744
 
@@ -1782,7 +1795,7 @@ function handleApiFlows(req, res) {
1782
1795
  }));
1783
1796
  sendJson(req, res, 200, { total: flows.length, flows });
1784
1797
  }
1785
- function createClearHandler(metricsStore) {
1798
+ function createClearHandler(metricsStore, findingStore) {
1786
1799
  return (req, res) => {
1787
1800
  if (req.method !== "POST") {
1788
1801
  sendJson(req, res, 405, { error: "Method not allowed" });
@@ -1794,6 +1807,7 @@ function createClearHandler(metricsStore) {
1794
1807
  defaultErrorStore.clear();
1795
1808
  defaultQueryStore.clear();
1796
1809
  metricsStore.reset();
1810
+ findingStore?.clear();
1797
1811
  sendJson(req, res, 200, { cleared: true });
1798
1812
  };
1799
1813
  }
@@ -1904,7 +1918,6 @@ function handleApiIngest(req, res) {
1904
1918
  sendJson(req, res, 405, { error: "Method not allowed" });
1905
1919
  return;
1906
1920
  }
1907
- const MAX_INGEST_BYTES = 10 * 1024 * 1024;
1908
1921
  const chunks = [];
1909
1922
  let totalSize = 0;
1910
1923
  req.on("data", (chunk) => {
@@ -1946,6 +1959,7 @@ var init_ingest = __esm({
1946
1959
  "src/dashboard/api/ingest.ts"() {
1947
1960
  "use strict";
1948
1961
  init_store();
1962
+ init_limits();
1949
1963
  init_shared2();
1950
1964
  }
1951
1965
  });
@@ -2051,13 +2065,13 @@ var init_api = __esm({
2051
2065
  function createInsightsHandler(engine) {
2052
2066
  return (req, res) => {
2053
2067
  if (!requireGet(req, res)) return;
2054
- sendJson(req, res, 200, { insights: engine.getInsights() });
2068
+ sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
2055
2069
  };
2056
2070
  }
2057
2071
  function createSecurityHandler(engine) {
2058
2072
  return (req, res) => {
2059
2073
  if (!requireGet(req, res)) return;
2060
- sendJson(req, res, 200, { findings: engine.getFindings() });
2074
+ sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
2061
2075
  };
2062
2076
  }
2063
2077
  var init_insights = __esm({
@@ -2132,9 +2146,9 @@ data: ${data}
2132
2146
  const queryListener = (entry) => {
2133
2147
  writeEvent("query", JSON.stringify(entry));
2134
2148
  };
2135
- const analysisListener = engine ? (insights, findings) => {
2136
- writeEvent("insights", JSON.stringify(insights));
2137
- writeEvent("security", JSON.stringify(findings));
2149
+ const analysisListener = engine ? ({ statefulInsights, statefulFindings }) => {
2150
+ writeEvent("insights", JSON.stringify(statefulInsights));
2151
+ writeEvent("security", JSON.stringify(statefulFindings));
2138
2152
  } : void 0;
2139
2153
  onRequest(requestListener);
2140
2154
  defaultFetchStore.onEntry(fetchListener);
@@ -2184,6 +2198,7 @@ function getBaseStyles() {
2184
2198
  --amber:#d97706;
2185
2199
  --red:#dc2626;
2186
2200
  --cyan:#0891b2;
2201
+ --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);
2187
2202
  --sidebar-width:232px;--header-height:52px;
2188
2203
  --radius:8px;--radius-sm:6px;
2189
2204
  --shadow-sm:0 1px 2px rgba(0,0,0,0.05);
@@ -2300,7 +2315,7 @@ function getFlowStyles() {
2300
2315
  .flow-label{font-weight:500;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
2301
2316
  .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
2302
2317
  .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)}
2318
+ .flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
2304
2319
  .flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
2305
2320
  .flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
2306
2321
  .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
@@ -2320,7 +2335,7 @@ function getFlowStyles() {
2320
2335
 
2321
2336
  /* Method badges */
2322
2337
  .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)}
2338
+ .method-badge-GET{background:var(--green-bg);color:var(--green)}
2324
2339
  .method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
2325
2340
  .method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
2326
2341
  .method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
@@ -2328,7 +2343,7 @@ function getFlowStyles() {
2328
2343
 
2329
2344
  /* Status pills */
2330
2345
  .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)}
2346
+ .status-pill-2xx{background:var(--green-bg);color:var(--green)}
2332
2347
  .status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
2333
2348
  .status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
2334
2349
  .status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
@@ -2595,6 +2610,7 @@ function getOverviewStyles() {
2595
2610
  .ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
2596
2611
  .ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
2597
2612
  .ov-card-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
2613
+ .ov-card-icon.resolved{background:var(--green-bg);color:var(--green)}
2598
2614
  .ov-card-body{flex:1;min-width:0}
2599
2615
  .ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
2600
2616
  .ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
@@ -2611,8 +2627,13 @@ function getOverviewStyles() {
2611
2627
  .ov-detail-item{font-size:12px;color:var(--text);font-family:var(--mono);padding:2px 0}
2612
2628
 
2613
2629
  /* All-clear banner */
2614
- .ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:rgba(22,163,74,.06);border:1px solid rgba(22,163,74,.2);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
2630
+ .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
2631
  .ov-clear-icon{font-size:16px}
2632
+
2633
+ /* Resolved section */
2634
+ .ov-resolved-title{margin-top:24px}
2635
+ .ov-card-resolved{opacity:.7;border-color:var(--green-border);cursor:default}
2636
+ .ov-card-resolved:hover{opacity:1;box-shadow:var(--shadow-sm)}
2616
2637
  `;
2617
2638
  }
2618
2639
  var init_overview = __esm({
@@ -2628,7 +2649,7 @@ function getSecurityStyles() {
2628
2649
  .sec-container{padding:24px 28px}
2629
2650
 
2630
2651
  /* All-clear */
2631
- .sec-clear{display:flex;align-items:center;gap:16px;padding:20px 24px;background:rgba(22,163,74,.05);border:1px solid rgba(22,163,74,.15);border-radius:var(--radius);margin-bottom:24px}
2652
+ .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
2653
  .sec-clear-icon{font-size:24px;color:var(--green);flex-shrink:0}
2633
2654
  .sec-clear-title{font-size:15px;font-weight:600;color:var(--green);margin-bottom:2px}
2634
2655
  .sec-clear-sub{font-size:12px;color:var(--text-dim)}
@@ -2664,6 +2685,19 @@ function getSecurityStyles() {
2664
2685
  .sec-item-desc{color:var(--text-dim);line-height:1.5;flex:1;min-width:0}
2665
2686
  .sec-item-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
2666
2687
  .sec-item-count{font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0;margin-left:12px}
2688
+
2689
+ /* Resolved badge in summary */
2690
+ .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}
2691
+
2692
+ /* Resolved section */
2693
+ .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}
2694
+ .sec-resolved-check{color:var(--green);font-size:14px}
2695
+ .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)}
2696
+ .sec-group-resolved{opacity:.7;border-color:var(--green-border)}
2697
+ .sec-group-resolved:hover{opacity:1}
2698
+ .sec-item-resolved{color:var(--text-muted)}
2699
+ .sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
2700
+ .sec-resolved-item-icon{color:var(--green);font-size:12px;flex-shrink:0;margin-right:8px}
2667
2701
  `;
2668
2702
  }
2669
2703
  var init_security = __esm({
@@ -3589,6 +3623,23 @@ function createEndpointGroup() {
3589
3623
  queryShapeDurations: /* @__PURE__ */ new Map()
3590
3624
  };
3591
3625
  }
3626
+ function windowByEndpoint(requests) {
3627
+ const byEndpoint = /* @__PURE__ */ new Map();
3628
+ for (const r of requests) {
3629
+ const ep = getEndpointKey(r.method, r.path);
3630
+ let list = byEndpoint.get(ep);
3631
+ if (!list) {
3632
+ list = [];
3633
+ byEndpoint.set(ep, list);
3634
+ }
3635
+ list.push(r);
3636
+ }
3637
+ const windowed = [];
3638
+ for (const [, reqs] of byEndpoint) {
3639
+ windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
3640
+ }
3641
+ return windowed;
3642
+ }
3592
3643
  function prepareContext(ctx) {
3593
3644
  const nonStatic = ctx.requests.filter(
3594
3645
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
@@ -3596,8 +3647,9 @@ function prepareContext(ctx) {
3596
3647
  const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
3597
3648
  const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
3598
3649
  const reqById = new Map(nonStatic.map((r) => [r.id, r]));
3650
+ const recent = windowByEndpoint(nonStatic);
3599
3651
  const endpointGroups = /* @__PURE__ */ new Map();
3600
- for (const r of nonStatic) {
3652
+ for (const r of recent) {
3601
3653
  const ep = getEndpointKey(r.method, r.path);
3602
3654
  let g = endpointGroups.get(ep);
3603
3655
  if (!g) {
@@ -3642,6 +3694,7 @@ var init_prepare = __esm({
3642
3694
  init_collections();
3643
3695
  init_endpoint();
3644
3696
  init_constants();
3697
+ init_thresholds();
3645
3698
  init_query_helpers();
3646
3699
  }
3647
3700
  });
@@ -4330,6 +4383,69 @@ var init_insights3 = __esm({
4330
4383
  }
4331
4384
  });
4332
4385
 
4386
+ // src/analysis/insight-tracker.ts
4387
+ function computeInsightKey(insight) {
4388
+ const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
4389
+ return `${insight.type}:${identifier}`;
4390
+ }
4391
+ var InsightTracker;
4392
+ var init_insight_tracker = __esm({
4393
+ "src/analysis/insight-tracker.ts"() {
4394
+ "use strict";
4395
+ init_endpoint();
4396
+ init_thresholds();
4397
+ InsightTracker = class {
4398
+ tracked = /* @__PURE__ */ new Map();
4399
+ reconcile(current) {
4400
+ const currentKeys = /* @__PURE__ */ new Set();
4401
+ const now = Date.now();
4402
+ for (const insight of current) {
4403
+ const key = computeInsightKey(insight);
4404
+ currentKeys.add(key);
4405
+ const existing = this.tracked.get(key);
4406
+ if (existing) {
4407
+ existing.insight = insight;
4408
+ existing.lastSeenAt = now;
4409
+ existing.consecutiveAbsences = 0;
4410
+ if (existing.state === "resolved") {
4411
+ existing.state = "open";
4412
+ existing.resolvedAt = null;
4413
+ }
4414
+ } else {
4415
+ this.tracked.set(key, {
4416
+ key,
4417
+ state: "open",
4418
+ insight,
4419
+ firstSeenAt: now,
4420
+ lastSeenAt: now,
4421
+ resolvedAt: null,
4422
+ consecutiveAbsences: 0
4423
+ });
4424
+ }
4425
+ }
4426
+ for (const [key, stateful] of this.tracked) {
4427
+ if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
4428
+ stateful.consecutiveAbsences++;
4429
+ if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
4430
+ stateful.state = "resolved";
4431
+ stateful.resolvedAt = now;
4432
+ }
4433
+ } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
4434
+ this.tracked.delete(key);
4435
+ }
4436
+ }
4437
+ return [...this.tracked.values()];
4438
+ }
4439
+ getAll() {
4440
+ return [...this.tracked.values()];
4441
+ }
4442
+ clear() {
4443
+ this.tracked.clear();
4444
+ }
4445
+ };
4446
+ }
4447
+ });
4448
+
4333
4449
  // src/analysis/engine.ts
4334
4450
  var AnalysisEngine;
4335
4451
  var init_engine = __esm({
@@ -4341,6 +4457,7 @@ var init_engine = __esm({
4341
4457
  init_group();
4342
4458
  init_rules();
4343
4459
  init_insights3();
4460
+ init_insight_tracker();
4344
4461
  AnalysisEngine = class {
4345
4462
  constructor(metricsStore, findingStore, debounceMs = 300) {
4346
4463
  this.metricsStore = metricsStore;
@@ -4353,8 +4470,10 @@ var init_engine = __esm({
4353
4470
  this.boundLogListener = () => this.scheduleRecompute();
4354
4471
  }
4355
4472
  scanner;
4473
+ insightTracker = new InsightTracker();
4356
4474
  cachedInsights = [];
4357
4475
  cachedFindings = [];
4476
+ cachedStatefulInsights = [];
4358
4477
  debounceTimer = null;
4359
4478
  listeners = [];
4360
4479
  boundRequestListener;
@@ -4390,6 +4509,12 @@ var init_engine = __esm({
4390
4509
  getFindings() {
4391
4510
  return this.cachedFindings;
4392
4511
  }
4512
+ getStatefulFindings() {
4513
+ return this.findingStore?.getAll() ?? [];
4514
+ }
4515
+ getStatefulInsights() {
4516
+ return this.cachedStatefulInsights;
4517
+ }
4393
4518
  scheduleRecompute() {
4394
4519
  if (this.debounceTimer) return;
4395
4520
  this.debounceTimer = setTimeout(() => {
@@ -4420,9 +4545,16 @@ var init_engine = __esm({
4420
4545
  previousMetrics: this.metricsStore.getAll(),
4421
4546
  securityFindings: this.cachedFindings
4422
4547
  });
4548
+ this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
4549
+ const update = {
4550
+ insights: this.cachedInsights,
4551
+ findings: this.cachedFindings,
4552
+ statefulFindings: this.getStatefulFindings(),
4553
+ statefulInsights: this.cachedStatefulInsights
4554
+ };
4423
4555
  for (const fn of this.listeners) {
4424
4556
  try {
4425
- fn(this.cachedInsights, this.cachedFindings);
4557
+ fn(update);
4426
4558
  } catch {
4427
4559
  }
4428
4560
  }
@@ -4443,7 +4575,7 @@ var init_src = __esm({
4443
4575
  init_engine();
4444
4576
  init_insights3();
4445
4577
  init_insights2();
4446
- VERSION = "0.8.0";
4578
+ VERSION = "0.8.1";
4447
4579
  }
4448
4580
  });
4449
4581
 
@@ -4621,7 +4753,7 @@ var init_thresholds2 = __esm({
4621
4753
  });
4622
4754
 
4623
4755
  // 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;
4756
+ 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
4757
  var init_display = __esm({
4626
4758
  "src/dashboard/client/constants/display.ts"() {
4627
4759
  "use strict";
@@ -4649,6 +4781,7 @@ var init_display = __esm({
4649
4781
  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
4782
  NAV_LABELS = `{ queries: 'Queries', requests: 'Requests', actions: 'Actions', errors: 'Errors', security: 'Security', fetches: 'Fetches', logs: 'Logs', performance: 'Performance' }`;
4651
4783
  CURL_SKIP_HEADERS = `['host', 'connection', 'accept-encoding']`;
4784
+ SEVERITY_MAP = `{ critical: { icon: '\\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\\u2139', cls: 'info', sort: 2 } }`;
4652
4785
  }
4653
4786
  });
4654
4787
 
@@ -6388,9 +6521,11 @@ function getOverviewRender() {
6388
6521
  '<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
6389
6522
  container.appendChild(summary);
6390
6523
 
6391
- var insights = state.insights || [];
6524
+ var all = state.insights || [];
6525
+ var open = all.filter(function(si) { return si.state === 'open'; });
6526
+ var resolved = all.filter(function(si) { return si.state === 'resolved'; });
6392
6527
 
6393
- if (insights.length === 0) {
6528
+ if (open.length === 0 && resolved.length === 0) {
6394
6529
  var clear = document.createElement('div');
6395
6530
  clear.className = 'ov-clear';
6396
6531
  clear.innerHTML = '<span class="ov-clear-icon">\\u2713</span>All clear \u2014 no issues detected';
@@ -6398,67 +6533,104 @@ function getOverviewRender() {
6398
6533
  return;
6399
6534
  }
6400
6535
 
6401
- var title = document.createElement('div');
6402
- title.className = 'ov-section-title';
6403
- title.innerHTML = 'Issues Found <span class="ov-issue-count">' + insights.length + '</span>';
6404
- container.appendChild(title);
6405
-
6406
- var cards = document.createElement('div');
6407
- cards.className = 'ov-cards';
6536
+ if (open.length === 0 && resolved.length > 0) {
6537
+ var allFixed = document.createElement('div');
6538
+ allFixed.className = 'ov-clear';
6539
+ 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';
6540
+ container.appendChild(allFixed);
6541
+ }
6408
6542
 
6409
6543
  var NAV_LABELS = ${NAV_LABELS};
6544
+ var SEV = ${SEVERITY_MAP};
6410
6545
 
6411
- for (var i = 0; i < insights.length; i++) {
6412
- (function(insight) {
6413
- var card = document.createElement('div');
6414
- card.className = 'ov-card';
6546
+ if (open.length > 0) {
6547
+ var title = document.createElement('div');
6548
+ title.className = 'ov-section-title';
6549
+ title.innerHTML = 'Issues Found <span class="ov-issue-count">' + open.length + '</span>';
6550
+ container.appendChild(title);
6551
+
6552
+ var cards = document.createElement('div');
6553
+ cards.className = 'ov-cards';
6554
+
6555
+ for (var i = 0; i < open.length; i++) {
6556
+ (function(si) {
6557
+ var insight = si.insight;
6558
+ var card = document.createElement('div');
6559
+ card.className = 'ov-card';
6560
+
6561
+ var sevCfg = SEV[insight.severity];
6562
+ var iconCls = sevCfg.cls;
6563
+ var iconChar = sevCfg.icon;
6564
+
6565
+ var expandHtml = '';
6566
+ if (insight.detail) expandHtml += insight.detail;
6567
+ if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6568
+ expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6569
+
6570
+ card.innerHTML =
6571
+ '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6572
+ '<div class="ov-card-body">' +
6573
+ '<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
6574
+ '<div class="ov-card-desc">' + insight.desc + '</div>' +
6575
+ '<div class="ov-card-expand">' + expandHtml + '</div>' +
6576
+ '</div>' +
6577
+ '<span class="ov-card-arrow">\\u2192</span>';
6578
+
6579
+ card.addEventListener('click', function(e) {
6580
+ var target = e.target;
6581
+ while (target && target !== card) {
6582
+ if (target.classList && target.classList.contains('ov-card-link')) {
6583
+ var navView = target.getAttribute('data-nav');
6584
+ var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
6585
+ if (sidebarItem) sidebarItem.click();
6586
+ return;
6587
+ }
6588
+ target = target.parentElement;
6589
+ }
6590
+ var expand = card.querySelector('.ov-card-expand');
6591
+ var arrow = card.querySelector('.ov-card-arrow');
6592
+ if (card.classList.contains('expanded')) {
6593
+ card.classList.remove('expanded');
6594
+ expand.style.display = 'none';
6595
+ arrow.textContent = '\\u2192';
6596
+ } else {
6597
+ card.classList.add('expanded');
6598
+ expand.style.display = 'block';
6599
+ arrow.textContent = '\\u2193';
6600
+ }
6601
+ });
6415
6602
 
6416
- var iconCls = insight.severity === 'critical' ? 'critical' : insight.severity === 'info' ? 'info' : 'warning';
6417
- var iconChar = insight.severity === 'critical' ? '\\u2717' : insight.severity === 'info' ? '\\u2139' : '\\u26A0';
6603
+ cards.appendChild(card);
6604
+ })(open[i]);
6605
+ }
6418
6606
 
6419
- var expandHtml = '';
6420
- if (insight.detail) expandHtml += insight.detail;
6421
- if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6422
- expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6607
+ container.appendChild(cards);
6608
+ }
6423
6609
 
6424
- card.innerHTML =
6425
- '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6610
+ if (resolved.length > 0) {
6611
+ var resolvedTitle = document.createElement('div');
6612
+ resolvedTitle.className = 'ov-section-title ov-resolved-title';
6613
+ resolvedTitle.innerHTML = '<span style="color:var(--green)">\\u2713</span> Resolved <span class="ov-issue-count">' + resolved.length + '</span>';
6614
+ container.appendChild(resolvedTitle);
6615
+
6616
+ var resolvedCards = document.createElement('div');
6617
+ resolvedCards.className = 'ov-cards';
6618
+
6619
+ for (var ri = 0; ri < resolved.length; ri++) {
6620
+ var rInsight = resolved[ri].insight;
6621
+ var rCard = document.createElement('div');
6622
+ rCard.className = 'ov-card ov-card-resolved';
6623
+ rCard.innerHTML =
6624
+ '<span class="ov-card-icon resolved">\\u2713</span>' +
6426
6625
  '<div class="ov-card-body">' +
6427
- '<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
6428
- '<div class="ov-card-desc">' + insight.desc + '</div>' +
6429
- '<div class="ov-card-expand">' + expandHtml + '</div>' +
6430
- '</div>' +
6431
- '<span class="ov-card-arrow">\\u2192</span>';
6432
-
6433
- card.addEventListener('click', function(e) {
6434
- var target = e.target;
6435
- while (target && target !== card) {
6436
- if (target.classList && target.classList.contains('ov-card-link')) {
6437
- var navView = target.getAttribute('data-nav');
6438
- var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
6439
- if (sidebarItem) sidebarItem.click();
6440
- return;
6441
- }
6442
- target = target.parentElement;
6443
- }
6444
- var expand = card.querySelector('.ov-card-expand');
6445
- var arrow = card.querySelector('.ov-card-arrow');
6446
- if (card.classList.contains('expanded')) {
6447
- card.classList.remove('expanded');
6448
- expand.style.display = 'none';
6449
- arrow.textContent = '\\u2192';
6450
- } else {
6451
- card.classList.add('expanded');
6452
- expand.style.display = 'block';
6453
- arrow.textContent = '\\u2193';
6454
- }
6455
- });
6626
+ '<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rInsight.title) + '</div>' +
6627
+ '<div class="ov-card-desc">' + rInsight.desc + '</div>' +
6628
+ '</div>';
6629
+ resolvedCards.appendChild(rCard);
6630
+ }
6456
6631
 
6457
- cards.appendChild(card);
6458
- })(insights[i]);
6632
+ container.appendChild(resolvedCards);
6459
6633
  }
6460
-
6461
- container.appendChild(cards);
6462
6634
  }
6463
6635
  `;
6464
6636
  }
@@ -6488,10 +6660,13 @@ function getSecurityView() {
6488
6660
  var container = document.getElementById('security-content');
6489
6661
  if (!container) return;
6490
6662
  container.innerHTML = '';
6663
+ var SEV = ${SEVERITY_MAP};
6491
6664
 
6492
- var findings = state.findings || [];
6665
+ var all = state.findings || [];
6666
+ var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
6667
+ var resolved = all.filter(function(f) { return f.state === 'resolved'; });
6493
6668
 
6494
- if (findings.length === 0) {
6669
+ if (open.length === 0 && resolved.length === 0) {
6495
6670
  var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
6496
6671
  if (!hasData) {
6497
6672
  container.innerHTML = '<div class="empty" style="height:400px"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
@@ -6502,17 +6677,20 @@ function getSecurityView() {
6502
6677
  }
6503
6678
 
6504
6679
  var critCount = 0, warnCount = 0, infoCount = 0;
6505
- for (var ci = 0; ci < findings.length; ci++) {
6506
- if (findings[ci].severity === 'critical') critCount++;
6507
- else if (findings[ci].severity === 'info') infoCount++;
6680
+ for (var ci = 0; ci < open.length; ci++) {
6681
+ var sev = open[ci].finding.severity;
6682
+ if (sev === 'critical') critCount++;
6683
+ else if (sev === 'info') infoCount++;
6508
6684
  else warnCount++;
6509
6685
  }
6686
+
6510
6687
  var summaryEl = document.createElement('div');
6511
6688
  summaryEl.className = 'sec-summary';
6512
6689
  summaryEl.innerHTML =
6513
6690
  '<div class="sec-summary-left">' +
6514
- '<span class="sec-summary-count">' + findings.length + '</span>' +
6515
- '<span class="sec-summary-label">issue' + (findings.length !== 1 ? 's' : '') + ' found</span>' +
6691
+ '<span class="sec-summary-count">' + open.length + '</span>' +
6692
+ '<span class="sec-summary-label">open issue' + (open.length !== 1 ? 's' : '') + '</span>' +
6693
+ (resolved.length > 0 ? '<span class="sec-resolved-badge">' + resolved.length + ' resolved</span>' : '') +
6516
6694
  '</div>' +
6517
6695
  '<div class="sec-summary-right">' +
6518
6696
  (critCount > 0 ? '<span class="sec-badge critical">' + critCount + ' critical</span>' : '') +
@@ -6521,60 +6699,93 @@ function getSecurityView() {
6521
6699
  '</div>';
6522
6700
  container.appendChild(summaryEl);
6523
6701
 
6524
- var groups = {};
6525
- var groupOrder = [];
6526
- for (var gi = 0; gi < findings.length; gi++) {
6527
- var f = findings[gi];
6528
- if (!groups[f.rule]) {
6529
- groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6530
- groupOrder.push(f.rule);
6531
- }
6532
- groups[f.rule].items.push(f);
6702
+ if (open.length === 0 && resolved.length > 0) {
6703
+ var allFixed = document.createElement('div');
6704
+ allFixed.className = 'sec-clear';
6705
+ 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>';
6706
+ container.appendChild(allFixed);
6533
6707
  }
6534
6708
 
6535
- groupOrder.sort(function(a, b) {
6536
- var sa = groups[a].severity === 'critical' ? 0 : groups[a].severity === 'warning' ? 1 : 2;
6537
- var sb = groups[b].severity === 'critical' ? 0 : groups[b].severity === 'warning' ? 1 : 2;
6538
- if (sa !== sb) return sa - sb;
6539
- return groups[b].items.length - groups[a].items.length;
6540
- });
6709
+ if (open.length > 0) {
6710
+ var groups = {};
6711
+ var groupOrder = [];
6712
+ for (var gi = 0; gi < open.length; gi++) {
6713
+ var f = open[gi].finding;
6714
+ if (!groups[f.rule]) {
6715
+ groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6716
+ groupOrder.push(f.rule);
6717
+ }
6718
+ groups[f.rule].items.push(f);
6719
+ }
6541
6720
 
6542
- for (var oi = 0; oi < groupOrder.length; oi++) {
6543
- var group = groups[groupOrder[oi]];
6544
- var section = document.createElement('div');
6545
- section.className = 'sec-group';
6721
+ groupOrder.sort(function(a, b) {
6722
+ var sa = SEV[groups[a].severity].sort;
6723
+ var sb = SEV[groups[b].severity].sort;
6724
+ if (sa !== sb) return sa - sb;
6725
+ return groups[b].items.length - groups[a].items.length;
6726
+ });
6546
6727
 
6547
- var iconCls = group.severity === 'critical' ? 'critical' : group.severity === 'info' ? 'info' : 'warning';
6548
- var iconChar = group.severity === 'critical' ? '\\u2717' : group.severity === 'info' ? '\\u2139' : '\\u26A0';
6728
+ for (var oi = 0; oi < groupOrder.length; oi++) {
6729
+ var group = groups[groupOrder[oi]];
6730
+ var section = document.createElement('div');
6731
+ section.className = 'sec-group';
6732
+
6733
+ var sevCfg = SEV[group.severity];
6734
+ var iconCls = sevCfg.cls;
6735
+ var iconChar = sevCfg.icon;
6736
+
6737
+ var header = document.createElement('div');
6738
+ header.className = 'sec-group-header';
6739
+ header.innerHTML =
6740
+ '<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
6741
+ '<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
6742
+ '<span class="sec-group-count">' + group.items.length + '</span>';
6743
+ section.appendChild(header);
6744
+
6745
+ if (group.hint) {
6746
+ var hintEl = document.createElement('div');
6747
+ hintEl.className = 'sec-hint';
6748
+ hintEl.textContent = group.hint;
6749
+ section.appendChild(hintEl);
6750
+ }
6549
6751
 
6550
- var header = document.createElement('div');
6551
- header.className = 'sec-group-header';
6552
- header.innerHTML =
6553
- '<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
6554
- '<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
6555
- '<span class="sec-group-count">' + group.items.length + '</span>';
6556
- section.appendChild(header);
6557
-
6558
- if (group.hint) {
6559
- var hintEl = document.createElement('div');
6560
- hintEl.className = 'sec-hint';
6561
- hintEl.textContent = group.hint;
6562
- section.appendChild(hintEl);
6563
- }
6564
-
6565
- var list = document.createElement('div');
6566
- list.className = 'sec-items';
6567
- for (var ii = 0; ii < group.items.length; ii++) {
6568
- var item = group.items[ii];
6569
- var row = document.createElement('div');
6570
- row.className = 'sec-item';
6571
- row.innerHTML =
6572
- '<div class="sec-item-desc">' + item.desc + '</div>' +
6573
- (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
6574
- list.appendChild(row);
6752
+ var list = document.createElement('div');
6753
+ list.className = 'sec-items';
6754
+ for (var ii = 0; ii < group.items.length; ii++) {
6755
+ var item = group.items[ii];
6756
+ var row = document.createElement('div');
6757
+ row.className = 'sec-item';
6758
+ row.innerHTML =
6759
+ '<div class="sec-item-desc">' + item.desc + '</div>' +
6760
+ (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
6761
+ list.appendChild(row);
6762
+ }
6763
+ section.appendChild(list);
6764
+ container.appendChild(section);
6575
6765
  }
6576
- section.appendChild(list);
6577
- container.appendChild(section);
6766
+ }
6767
+
6768
+ if (resolved.length > 0) {
6769
+ var resolvedTitle = document.createElement('div');
6770
+ resolvedTitle.className = 'sec-resolved-title';
6771
+ resolvedTitle.innerHTML = '<span class="sec-resolved-check">\\u2713</span> Resolved <span class="sec-resolved-count">' + resolved.length + '</span>';
6772
+ container.appendChild(resolvedTitle);
6773
+
6774
+ var resolvedGroup = document.createElement('div');
6775
+ resolvedGroup.className = 'sec-group sec-group-resolved';
6776
+ var resolvedItems = document.createElement('div');
6777
+ resolvedItems.className = 'sec-items';
6778
+ for (var ri = 0; ri < resolved.length; ri++) {
6779
+ var rf = resolved[ri].finding;
6780
+ var rRow = document.createElement('div');
6781
+ rRow.className = 'sec-item sec-item-resolved';
6782
+ rRow.innerHTML =
6783
+ '<span class="sec-resolved-item-icon">\\u2713</span>' +
6784
+ '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>';
6785
+ resolvedItems.appendChild(rRow);
6786
+ }
6787
+ resolvedGroup.appendChild(resolvedItems);
6788
+ container.appendChild(resolvedGroup);
6578
6789
  }
6579
6790
  }
6580
6791
  `;
@@ -6582,6 +6793,7 @@ function getSecurityView() {
6582
6793
  var init_security3 = __esm({
6583
6794
  "src/dashboard/client/views/security.ts"() {
6584
6795
  "use strict";
6796
+ init_display();
6585
6797
  }
6586
6798
  });
6587
6799
 
@@ -6762,7 +6974,7 @@ function getApp() {
6762
6974
  if (queryCount) queryCount.textContent = state.queries.length;
6763
6975
  var secCount = document.getElementById('sidebar-count-security');
6764
6976
  if (secCount) {
6765
- var numFindings = (state.findings || []).length;
6977
+ var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
6766
6978
  secCount.textContent = numFindings;
6767
6979
  secCount.style.display = numFindings > 0 ? '' : 'none';
6768
6980
  }
@@ -6931,7 +7143,7 @@ function createDashboardHandler(deps) {
6931
7143
  [DASHBOARD_API_REQUESTS]: handleApiRequests,
6932
7144
  [DASHBOARD_API_EVENTS]: createSSEHandler(deps.analysisEngine),
6933
7145
  [DASHBOARD_API_FLOWS]: handleApiFlows,
6934
- [DASHBOARD_API_CLEAR]: createClearHandler(deps.metricsStore),
7146
+ [DASHBOARD_API_CLEAR]: createClearHandler(deps.metricsStore, deps.findingStore),
6935
7147
  [DASHBOARD_API_LOGS]: handleApiLogs,
6936
7148
  [DASHBOARD_API_FETCHES]: handleApiFetches,
6937
7149
  [DASHBOARD_API_ERRORS]: handleApiErrors,
@@ -7003,22 +7215,32 @@ var init_router = __esm({
7003
7215
  }
7004
7216
  });
7005
7217
 
7218
+ // src/constants/severity.ts
7219
+ var SEVERITY_ICON;
7220
+ var init_severity = __esm({
7221
+ "src/constants/severity.ts"() {
7222
+ "use strict";
7223
+ SEVERITY_ICON = {
7224
+ critical: "\u2717",
7225
+ warning: "\u26A0",
7226
+ info: "\u2139"
7227
+ };
7228
+ }
7229
+ });
7230
+
7006
7231
  // src/output/terminal.ts
7007
7232
  import pc from "picocolors";
7008
7233
  function print(line) {
7009
7234
  process.stdout.write(line + "\n");
7010
7235
  }
7011
7236
  function severityIcon(severity) {
7012
- if (severity === "critical") return pc.red("\u2717");
7013
- if (severity === "warning") return pc.yellow("\u26A0");
7014
- return pc.dim("\u25CB");
7237
+ return SEVERITY_COLOR[severity](SEVERITY_ICON[severity]);
7015
7238
  }
7016
7239
  function colorTitle(severity, text) {
7017
- if (severity === "critical") return pc.red(pc.bold(text));
7018
- if (severity === "warning") return pc.yellow(pc.bold(text));
7019
- return pc.dim(text);
7240
+ const color = SEVERITY_COLOR[severity];
7241
+ return severity === "info" ? color(text) : color(pc.bold(text));
7020
7242
  }
7021
- function truncate(s, max = 80) {
7243
+ function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
7022
7244
  return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
7023
7245
  }
7024
7246
  function formatConsoleLine(insight, suffix) {
@@ -7034,38 +7256,66 @@ function formatConsoleLine(insight, suffix) {
7034
7256
  }
7035
7257
  function createConsoleInsightListener(proxyPort, metricsStore) {
7036
7258
  const printedKeys = /* @__PURE__ */ new Set();
7259
+ const resolvedKeys = /* @__PURE__ */ new Set();
7037
7260
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
7038
- return (insights) => {
7039
- const lines = [];
7040
- for (const insight of insights) {
7041
- if (insight.severity === "info") continue;
7042
- const endpoint = insight.desc.match(/^(\S+\s+\S+)/)?.[1] ?? insight.desc;
7043
- const key = `${insight.type}:${endpoint}`;
7044
- if (printedKeys.has(key)) continue;
7045
- printedKeys.add(key);
7261
+ return ({ statefulInsights }) => {
7262
+ const newLines = [];
7263
+ const resolvedLines = [];
7264
+ for (const si of statefulInsights) {
7265
+ if (si.state === "resolved") {
7266
+ if (resolvedKeys.has(si.key)) continue;
7267
+ resolvedKeys.add(si.key);
7268
+ printedKeys.delete(si.key);
7269
+ const title = pc.green(pc.bold(`\u2713 ${si.insight.title}`));
7270
+ const desc = pc.dim(truncate(si.insight.desc));
7271
+ resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
7272
+ continue;
7273
+ }
7274
+ resolvedKeys.delete(si.key);
7275
+ if (si.insight.severity === "info") continue;
7276
+ if (printedKeys.has(si.key)) continue;
7277
+ printedKeys.add(si.key);
7046
7278
  let suffix;
7047
- if (insight.type === "slow") {
7048
- const ep = metricsStore.getEndpoint(endpoint);
7049
- if (ep && ep.sessions.length > 1) {
7050
- const prev = ep.sessions[ep.sessions.length - 2];
7051
- suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
7279
+ if (si.insight.type === "slow") {
7280
+ const endpoint = extractEndpointFromDesc(si.insight.desc);
7281
+ if (endpoint) {
7282
+ const ep = metricsStore.getEndpoint(endpoint);
7283
+ if (ep && ep.sessions.length > 1) {
7284
+ const prev = ep.sessions[ep.sessions.length - 2];
7285
+ suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
7286
+ }
7052
7287
  }
7053
7288
  }
7054
- lines.push(formatConsoleLine(insight, suffix));
7289
+ newLines.push(formatConsoleLine(si.insight, suffix));
7055
7290
  }
7056
- if (lines.length > 0) {
7291
+ if (newLines.length > 0) {
7057
7292
  print("");
7058
- for (const line of lines) print(line);
7293
+ for (const line of newLines) print(line);
7059
7294
  print("");
7060
7295
  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
7296
  }
7297
+ if (resolvedLines.length > 0) {
7298
+ print("");
7299
+ for (const line of resolvedLines) print(line);
7300
+ print("");
7301
+ print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.green("Issues fixed!")}`);
7302
+ }
7062
7303
  };
7063
7304
  }
7305
+ var SEVERITY_COLOR;
7064
7306
  var init_terminal = __esm({
7065
7307
  "src/output/terminal.ts"() {
7066
7308
  "use strict";
7067
7309
  init_src();
7068
7310
  init_constants();
7311
+ init_limits();
7312
+ init_severity();
7313
+ init_endpoint();
7314
+ SEVERITY_COLOR = {
7315
+ critical: pc.red,
7316
+ warning: pc.yellow,
7317
+ info: pc.dim
7318
+ };
7069
7319
  }
7070
7320
  });
7071
7321