brakit 0.8.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,11 +18,13 @@
18
18
  --red:#dc2626;
19
19
  --cyan:#0891b2;
20
20
  --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);
21
+ --amber-bg:rgba(217,119,6,0.07);--red-bg:rgba(220,38,38,0.07);--blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
21
22
  --sidebar-width:232px;--header-height:52px;
22
23
  --radius:8px;--radius-sm:6px;
23
- --shadow-sm:0 1px 2px rgba(0,0,0,0.05);
24
- --shadow-md:0 1px 3px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.04);
25
- --shadow-lg:0 4px 12px rgba(0,0,0,0.08),0 1px 4px rgba(0,0,0,0.04);
24
+ --shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
25
+ --shadow-md:0 2px 6px rgba(0,0,0,0.08),0 1px 3px rgba(0,0,0,0.04);
26
+ --shadow-lg:0 4px 16px rgba(0,0,0,0.1),0 2px 6px rgba(0,0,0,0.05);
27
+ --transition:0.15s ease;
26
28
  --breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
27
29
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
28
30
  --sans:Inter,system-ui,-apple-system,sans-serif;
@@ -94,6 +96,11 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
94
96
 
95
97
  /* Content */
96
98
  .main-content{flex:1;overflow-y:auto}
99
+ bk-dashboard{display:contents}
100
+ bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-timeline-panel,bk-empty-state{display:block}
101
+ bk-method-badge,bk-status-pill,bk-duration-label,bk-copy-button{display:inline-flex;flex-shrink:0}
102
+ bk-stat-card{display:inline-flex}
103
+ bk-toast{display:block;position:fixed;top:0;left:0;right:0;z-index:100;pointer-events:none}
97
104
 
98
105
  /* Column headers */
99
106
  .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)}
@@ -115,8 +122,8 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
115
122
  .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
116
123
  .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}
117
124
  .flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
118
- .flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
119
- .flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
125
+ .flow-badge-pill.badge-warn{background:var(--amber-bg);color:var(--amber)}
126
+ .flow-badge-pill.badge-error{background:var(--red-bg);color:var(--red)}
120
127
  .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
121
128
 
122
129
  /* Flow expand panel */
@@ -135,23 +142,23 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
135
142
  /* Method badges */
136
143
  .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}
137
144
  .method-badge-GET{background:var(--green-bg);color:var(--green)}
138
- .method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
139
- .method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
140
- .method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
145
+ .method-badge-POST{background:var(--blue-bg);color:var(--blue)}
146
+ .method-badge-PUT,.method-badge-PATCH{background:var(--amber-bg);color:var(--amber)}
147
+ .method-badge-DELETE{background:var(--red-bg);color:var(--red)}
141
148
  .method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
142
149
 
143
150
  /* Status pills */
144
151
  .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}
145
152
  .status-pill-2xx{background:var(--green-bg);color:var(--green)}
146
- .status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
147
- .status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
148
- .status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
153
+ .status-pill-3xx{background:var(--cyan-bg);color:var(--cyan)}
154
+ .status-pill-4xx{background:var(--amber-bg);color:var(--amber)}
155
+ .status-pill-5xx{background:var(--red-bg);color:var(--red)}
149
156
 
150
157
  .traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
151
158
  .traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
152
159
  .traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
153
160
  .traffic-card-size{color:var(--text-muted);font-size:11px;flex-shrink:0}
154
- .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}
161
+ .traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
155
162
 
156
163
  /* Body toggles */
157
164
  .traffic-body{padding:0;margin-top:8px}
@@ -180,7 +187,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
180
187
  .flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
181
188
  .flow-subreq .subreq-status{flex-shrink:0}
182
189
  .flow-subreq .subreq-dur{color:var(--text-muted);font-size:12px;text-align:right;flex-shrink:0}
183
- .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}
190
+ .flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
184
191
  .flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
185
192
  .flow-subreq-detail.open{display:block}
186
193
 
@@ -210,6 +217,41 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
210
217
  .strict-mode-dupe{opacity:0.55}
211
218
  .strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
212
219
 
220
+ /* Flow detail tabs */
221
+ .flow-detail-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
222
+ .flow-tab{padding:8px 16px;font-size:12px;font-family:var(--mono);font-weight:600;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;letter-spacing:.3px}
223
+ .flow-tab:hover{color:var(--text)}
224
+ .flow-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
225
+
226
+ /* Waterfall chart — request bars on time axis, sub-events as text rows */
227
+ .flow-waterfall{padding:0;font-family:var(--mono);font-size:11px}
228
+ .wf-time-axis{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);padding:0 0 6px;margin-left:180px;margin-right:56px;border-bottom:1px solid var(--border);margin-bottom:2px}
229
+ .wf-rows{display:flex;flex-direction:column;gap:0}
230
+
231
+ /* Request group — request bar + its sub-events */
232
+ .wf-request-group{border-bottom:1px solid var(--border-subtle);padding:2px 0}
233
+ .wf-request-group:last-child{border-bottom:none}
234
+
235
+ /* Request row — label | bar on time axis | duration */
236
+ .wf-req-row{display:flex;align-items:center;gap:0;height:24px;transition:background .1s}
237
+ .wf-req-row:hover{background:var(--bg-hover)}
238
+ .wf-req-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;padding:0 10px 0 0;font-size:11px}
239
+ .wf-bar-track{flex:1;position:relative;height:14px;min-width:0;overflow:hidden}
240
+ .wf-bar{position:absolute;top:1px;height:12px;border-radius:3px;opacity:0.8;min-width:3px}
241
+ .wf-req-row:hover .wf-bar{opacity:1}
242
+ .wf-req-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-muted);font-size:10px;padding-left:8px}
243
+
244
+ /* Sub-event rows — same layout as request rows: label | bar track | duration */
245
+ .wf-sub-row{display:flex;align-items:center;gap:0;height:20px;transition:background .1s}
246
+ .wf-sub-row:hover{background:var(--bg-hover)}
247
+ .wf-sub-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:10px;padding-left:14px;display:flex;align-items:center;gap:6px}
248
+ .wf-sub-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
249
+ .wf-sub-bar-sized{height:8px !important;top:3px !important;opacity:0.65}
250
+ .wf-sub-row:hover .wf-sub-bar-sized{opacity:0.9}
251
+ .wf-sub-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-dim);font-size:9px;padding-left:8px}
252
+
253
+ .wf-loading{color:var(--text-muted);padding:12px 0;font-size:11px;font-family:var(--mono)}
254
+
213
255
  /* Request rows */
214
256
  .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}
215
257
  .req-row:hover{background:var(--bg-hover)}
@@ -267,17 +309,23 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
267
309
  .fetch-stat-value{font-size:17px;font-weight:700;font-family:var(--mono);color:var(--text)}
268
310
  .fetch-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
269
311
  .fetch-groups-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
270
- .fetch-groups{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}
271
- .fetch-group{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;box-shadow:var(--shadow-sm);transition:all .15s}
312
+ .fetch-groups{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}
313
+ .fetch-group{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:10px 16px;box-shadow:var(--shadow-sm);transition:all .15s}
272
314
  .fetch-group:hover{border-color:var(--border-light);box-shadow:var(--shadow-md)}
273
315
  .fetch-group-header{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:13px}
274
316
  .fetch-group-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;color:var(--text)}
275
317
  .fetch-group-count{font-size:12px;color:var(--text-muted);flex-shrink:0;background:var(--bg-muted);padding:2px 8px;border-radius:10px}
276
- .fetch-group-meta{display:flex;gap:16px;margin-top:8px;font-size:11px;color:var(--text-dim);font-family:var(--mono)}
318
+ .fetch-group-meta{display:flex;align-items:center;gap:8px;margin-top:6px;font-size:11px;color:var(--text-dim);font-family:var(--mono)}
277
319
  .fetch-group-meta span{display:flex;align-items:center;gap:4px}
278
- .fetch-group-callers{margin-top:6px;font-size:11px;color:var(--text-muted)}
279
- .fetch-group-callers strong{color:var(--text-dim);font-weight:500}
320
+ .fetch-group-sep{color:var(--text-muted);font-size:9px}
321
+ .fetch-group-ok{color:var(--green)}
280
322
  .fetch-group-err{color:var(--red)}
323
+ .fetch-group-timeline{display:flex;align-items:center;gap:6px;margin-top:4px;font-size:10px;color:var(--text-muted);font-family:var(--mono)}
324
+ .fetch-group-timeline-dot{width:6px;height:6px;border-radius:50%;background:var(--blue);opacity:.5;flex-shrink:0}
325
+ .fetch-group-timeline-range{letter-spacing:.3px}
326
+ .fetch-group-callers{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:10px;flex-wrap:wrap}
327
+ .fetch-group-callers-label{color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}
328
+ .fetch-group-caller-pill{background:var(--bg-muted);border:1px solid var(--border);color:var(--text-dim);padding:1px 8px;border-radius:10px;font-family:var(--mono);font-size:10px;white-space:nowrap}
281
329
 
282
330
  /* Performance tab */
283
331
  .perf-selector{display:flex;gap:6px;flex-wrap:wrap;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
@@ -291,22 +339,39 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
291
339
  .perf-badge-lg{padding:4px 12px;font-size:13px;border-radius:var(--radius-sm)}
292
340
  .perf-badge-sm{padding:1px 6px;font-size:9px}
293
341
 
294
- /* Overview: endpoint list with inline scatter charts */
295
- .perf-endpoint-list{padding:16px 28px;display:flex;flex-direction:column;gap:8px}
296
- .perf-endpoint-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 20px 8px;cursor:pointer;transition:all .15s;box-shadow:var(--shadow-sm)}
297
- .perf-endpoint-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md)}
298
- .perf-ep-header{display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap}
299
- .perf-ep-name{flex:1;font-family:var(--mono);font-size:13px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:120px}
300
- .perf-ep-stats{display:flex;align-items:center;gap:14px;flex-shrink:0}
301
- .perf-ep-stat{font-size:11px;font-family:var(--mono);color:var(--text-muted)}
302
- .perf-ep-stat-err{color:var(--red)}
303
- .perf-ep-stat-warn{color:var(--amber)}
304
- .perf-ep-stat-muted{color:var(--text-dim)}
305
- .perf-inline-canvas{width:100%;height:88px;border-radius:var(--radius-sm);background:var(--bg-muted);border:1px solid var(--border);display:block}
342
+ /* Overview: summary cards */
343
+ .perf-overview{padding:16px 28px}
344
+ .perf-summary-row{display:flex;gap:8px;margin-bottom:16px}
345
+ .perf-summary-card{flex:1;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--shadow-sm)}
346
+ .perf-summary-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
347
+ .perf-summary-value{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text)}
348
+ .perf-summary-value-sm{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
349
+
350
+ /* Shared table styles */
351
+ .perf-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px}
352
+ .perf-table thead th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600;font-family:var(--sans);padding:10px 14px;border-bottom:2px solid var(--border);white-space:nowrap}
353
+ .perf-table tbody td{padding:11px 14px;border-bottom:1px solid var(--border-subtle);color:var(--text)}
354
+ .perf-table-row{cursor:pointer;transition:background var(--transition, .15s ease)}
355
+ .perf-table-row:hover{background:var(--bg-hover)}
356
+ .perf-table tbody tr:last-child td{border-bottom:none}
357
+ .perf-th-right{text-align:right !important}
358
+ .perf-th-center{text-align:center !important}
359
+ .perf-td-right{text-align:right}
360
+ .perf-td-center{text-align:center}
361
+ .perf-td-name{font-weight:600;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
362
+ .perf-td-muted{color:var(--text-dim)}
363
+ .perf-row-err{background:var(--red-bg)}
364
+ .perf-row-err:hover{background:rgba(220,38,38,0.1)}
365
+
366
+ /* Heat map table wrapper */
367
+ .perf-heatmap{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm)}
368
+ .perf-hm-p95{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid}
369
+ .perf-hm-split-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--bg-muted);width:100%;min-width:80px}
306
370
 
307
371
  /* Detail view */
308
372
  .perf-detail-header{padding:20px 28px 16px;border-bottom:1px solid var(--border-subtle)}
309
373
  .perf-detail-title{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:600;color:var(--text);font-family:var(--mono)}
374
+ .perf-baseline-hint{font-size:11px;font-weight:400;color:var(--text-muted);padding:2px 8px;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm)}
310
375
  .perf-metric-row{display:flex;gap:4px;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
311
376
  .perf-metric-card{flex:1;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 16px;display:flex;flex-direction:column;gap:4px}
312
377
  .perf-metric-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
@@ -341,30 +406,51 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
341
406
  .perf-canvas{border-radius:var(--radius);background:var(--bg-muted);border:1px solid var(--border)}
342
407
  .perf-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
343
408
 
344
- /* Request history table */
409
+ /* Request history */
345
410
  .perf-history-wrap{padding:0 28px 20px}
346
- .perf-history-wrap .col-header{padding:8px 0;margin:0;position:static;background:var(--bg);gap:0}
347
- .perf-hist-row{display:flex;align-items:center;padding:10px 0;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
348
- .perf-hist-row:hover{background:var(--bg-hover);margin:0 -28px;padding-left:28px;padding-right:28px}
349
- .perf-hist-row-err{background:rgba(220,38,38,0.04)}
350
- .perf-hist-row-err:hover{background:rgba(220,38,38,0.08)}
351
- .perf-hist-row-hl{background:rgba(37,99,235,0.1);margin:0 -28px;padding-left:28px;padding-right:28px;border-left:3px solid #4ade80}
352
- .perf-hist-row-hl.perf-hist-row-err{background:rgba(220,38,38,0.12);border-left-color:#f87171}
353
- .perf-col{flex-shrink:0;border-right:1px solid var(--border-subtle);padding-right:16px;margin-right:16px}
354
- .perf-col:last-child{border-right:none;padding-right:0;margin-right:0}
355
- .perf-col-date{width:100px;color:var(--text-dim)}
356
- .perf-col-health{width:60px;display:flex;align-items:center}
357
- .perf-col-avg{width:70px;color:var(--text)}
358
- .perf-col-status{width:50px;text-align:center}
359
- .perf-col-qpr{width:60px;text-align:right;color:var(--text-dim)}
411
+ .perf-hist-row-hl{background:rgba(37,99,235,0.1) !important;border-left:3px solid #4ade80}
412
+
413
+ /* Callers section */
414
+ .perf-callers{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
415
+ .perf-callers-list{display:flex;flex-direction:column;gap:0}
416
+ .perf-caller-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
417
+ .perf-caller-row:last-child{border-bottom:none}
418
+ .perf-caller-name{flex:1;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
419
+ .perf-caller-count{color:var(--text-muted);font-size:11px;flex-shrink:0}
420
+ .perf-caller-avg{color:var(--text-dim);font-size:11px;flex-shrink:0}
421
+
422
+ /* Query breakdown section */
423
+ .perf-queries{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
424
+ .perf-queries-loading{font-size:11px;color:var(--text-muted);font-family:var(--mono)}
425
+ .perf-queries-list{display:flex;flex-direction:column;gap:0}
426
+ .perf-query-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
427
+ .perf-query-row:last-child{border-bottom:none}
428
+ .perf-query-label{flex:1;font-weight:500;color:var(--accent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
429
+ .perf-query-avg{color:var(--text-muted);font-size:11px;flex-shrink:0}
430
+ .perf-query-count{color:var(--text-dim);font-size:11px;flex-shrink:0}
431
+
432
+ /* Session trends */
433
+ .perf-trends{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
434
+ .perf-trends-list{display:flex;flex-direction:column;gap:0}
435
+ .perf-trend-row{display:flex;align-items:center;gap:14px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
436
+ .perf-trend-row:last-child{border-bottom:none}
437
+ .perf-trend-current{background:var(--bg-muted);border-radius:var(--radius-sm);font-weight:600}
438
+ .perf-trend-time{width:80px;color:var(--text-dim);font-size:11px;flex-shrink:0}
439
+ .perf-trend-p95{flex-shrink:0}
440
+ .perf-trend-reqs{color:var(--text-muted);font-size:11px;flex-shrink:0}
441
+ .perf-trend-errs{font-size:11px;flex-shrink:0}
442
+ .perf-trend-arrow{font-size:10px;font-weight:600;flex-shrink:0}
443
+ .perf-trend-slower{color:var(--red)}
444
+ .perf-trend-faster{color:var(--green)}
360
445
 
361
446
  /* Overview */
362
447
  .ov-container{padding:24px 28px}
363
448
 
364
449
  /* Summary banner */
365
- .ov-summary{display:flex;gap:24px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:24px;flex-wrap:wrap;box-shadow:var(--shadow-sm)}
366
- .ov-stat{display:flex;flex-direction:column;gap:2px}
367
- .ov-stat-value{font-size:19px;font-weight:700;font-family:var(--mono);color:var(--text)}
450
+ .ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
451
+ .ov-stat{display:flex;flex-direction:column;gap:4px;flex:1;min-width:100px;padding:16px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-sm);transition:box-shadow var(--transition, .15s ease)}
452
+ .ov-stat:hover{box-shadow:var(--shadow-md)}
453
+ .ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
368
454
  .ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
369
455
 
370
456
  /* Section header */
@@ -373,8 +459,8 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
373
459
 
374
460
  /* Insight cards */
375
461
  .ov-cards{display:flex;flex-direction:column;gap:8px}
376
- .ov-card{display:flex;align-items:flex-start;gap:14px;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all .15s;box-shadow:var(--shadow-sm)}
377
- .ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md)}
462
+ .ov-card{display:flex;align-items:flex-start;gap:14px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all var(--transition, .15s ease);box-shadow:var(--shadow-sm)}
463
+ .ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
378
464
  .ov-card-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;border-radius:50%;margin-top:2px}
379
465
  .ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
380
466
  .ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
@@ -383,6 +469,7 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
383
469
  .ov-card-body{flex:1;min-width:0}
384
470
  .ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
385
471
  .ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
472
+ .ov-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:8px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);line-height:1.5}
386
473
  .ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
387
474
  .ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
388
475
 
@@ -438,9 +525,8 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
438
525
  .sec-hint{padding:8px 16px;font-size:11px;color:var(--text-muted);background:var(--bg-muted);border-bottom:1px solid var(--border)}
439
526
 
440
527
  /* Items */
441
- .sec-items{padding:4px 0}
442
- .sec-item{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;font-size:12px;transition:background .1s}
443
- .sec-item:hover{background:var(--bg-hover)}
528
+ .sec-items{padding:2px 0}
529
+ .sec-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:12px;flex-wrap:wrap}
444
530
  .sec-item-desc{color:var(--text-dim);line-height:1.5;flex:1;min-width:0}
445
531
  .sec-item-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
446
532
  .sec-item-count{font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0;margin-left:12px}
@@ -465,2189 +551,1103 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
465
551
  .sec-ai-verified{background:rgba(22,163,74,.1);color:var(--green)}
466
552
  .sec-ai-notes{font-size:11px;color:var(--text-muted);font-style:italic;margin-top:2px;padding-left:0}
467
553
 
468
- /* Timeline */
469
- .request-timeline{margin-top:8px;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;overflow:hidden}
470
- .request-timeline.tl-hidden{display:none}
471
- .tl-header{display:flex;align-items:center;justify-content:space-between;padding:0 0 8px}
472
- .tl-title{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
473
- .tl-counts{display:flex;gap:10px;font-family:var(--mono);font-size:10px}
474
- .tl-count{color:var(--text-dim)}
475
- .tl-count-query{color:var(--accent)}
476
- .tl-count-fetch{color:var(--blue)}
477
- .tl-count-error{color:var(--red)}
478
- .tl-count-log{color:var(--text-muted)}
479
- .tl-loading{color:var(--text-muted);padding:4px 0;font-size:10px;font-family:var(--mono)}
480
- .tl-events{position:relative;padding-left:4px}
481
- .tl-event{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;padding:4px 0 4px 12px;border-left:2px solid var(--border);position:relative;flex-wrap:wrap}
482
- .tl-event-time{width:48px;color:var(--text-muted);font-size:10px;flex-shrink:0;text-align:right}
483
- .tl-event-type{width:44px;font-weight:700;font-size:9px;letter-spacing:.5px;flex-shrink:0}
484
- .tl-event-summary{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
485
- .tl-event-status{width:32px;text-align:right;font-weight:600;flex-shrink:0}
486
- .tl-event-dur{width:48px;text-align:right;color:var(--text-muted);flex-shrink:0;font-size:10px}
487
- .tl-event.tl-clickable{cursor:pointer;border-radius:6px;margin-left:-4px;padding:6px 12px;border-left:none;border:1px solid var(--border);background:var(--bg-card)}
488
- .tl-event.tl-clickable:hover{background:var(--bg-hover);border-color:var(--border-light)}
489
- .tl-event-sql{width:100%;display:none;margin:4px 0 2px 0;padding:8px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);font-size:10px;line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--text-dim);overflow-x:auto;max-height:150px;overflow-y:auto;position:relative}
490
- .tl-event-sql.open{display:block}
491
- .tl-sql-copy{position:absolute;top:6px;right:6px;padding:2px 8px;font-size:9px;font-family:var(--mono);background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-muted);cursor:pointer;transition:all .15s}
492
- .tl-sql-copy:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-light)}
554
+ .request-timeline {
555
+ margin-top: 8px;
556
+ background: var(--bg-muted);
557
+ border: 1px solid var(--border);
558
+ border-radius: var(--radius);
559
+ padding: 10px 14px;
560
+ overflow: hidden;
561
+ }
562
+
563
+ .request-timeline.tl-hidden { display: none; }
564
+
565
+ .tl-header {
566
+ display: flex;
567
+ align-items: center;
568
+ justify-content: space-between;
569
+ padding: 0 0 8px;
570
+ }
571
+
572
+ .tl-title {
573
+ font-size: 10px;
574
+ text-transform: uppercase;
575
+ letter-spacing: .8px;
576
+ color: var(--text-muted);
577
+ font-weight: 600;
578
+ }
579
+
580
+ .tl-counts {
581
+ display: flex;
582
+ gap: 10px;
583
+ font-family: var(--mono);
584
+ font-size: 10px;
585
+ }
586
+
587
+ .tl-count { color: var(--text-dim); }
588
+ .tl-count-query { color: var(--accent); }
589
+ .tl-count-fetch { color: var(--blue); }
590
+ .tl-count-error { color: var(--red); }
591
+ .tl-count-log { color: var(--text-muted); }
592
+
593
+ .tl-loading {
594
+ color: var(--text-muted);
595
+ padding: 4px 0;
596
+ font-size: 10px;
597
+ font-family: var(--mono);
598
+ }
599
+
600
+ .tl-events {
601
+ position: relative;
602
+ padding-left: 4px;
603
+ }
604
+
605
+ .tl-event {
606
+ display: flex;
607
+ align-items: center;
608
+ gap: 10px;
609
+ font-family: var(--mono);
610
+ font-size: 11px;
611
+ padding: 4px 0 4px 12px;
612
+ border-left: 2px solid var(--border);
613
+ position: relative;
614
+ flex-wrap: wrap;
615
+ }
616
+
617
+ .tl-event-time {
618
+ width: 48px;
619
+ color: var(--text-muted);
620
+ font-size: 10px;
621
+ flex-shrink: 0;
622
+ text-align: right;
623
+ }
624
+
625
+ .tl-event-type {
626
+ width: 44px;
627
+ font-weight: 700;
628
+ font-size: 9px;
629
+ letter-spacing: .5px;
630
+ flex-shrink: 0;
631
+ }
632
+
633
+ .tl-event-summary {
634
+ flex: 1;
635
+ overflow: hidden;
636
+ text-overflow: ellipsis;
637
+ white-space: nowrap;
638
+ color: var(--text);
639
+ }
640
+
641
+ .tl-event-status {
642
+ width: 32px;
643
+ text-align: right;
644
+ font-weight: 600;
645
+ flex-shrink: 0;
646
+ }
647
+
648
+ .tl-event-dur {
649
+ width: 48px;
650
+ text-align: right;
651
+ color: var(--text-muted);
652
+ flex-shrink: 0;
653
+ font-size: 10px;
654
+ }
655
+
656
+ .tl-event.tl-clickable {
657
+ cursor: pointer;
658
+ border-radius: 6px;
659
+ margin-left: -4px;
660
+ padding: 6px 12px;
661
+ border-left: none;
662
+ border: 1px solid var(--border);
663
+ background: var(--bg-card);
664
+ }
665
+
666
+ .tl-event.tl-clickable:hover {
667
+ background: var(--bg-hover);
668
+ border-color: var(--border-light);
669
+ }
670
+
671
+ .tl-event-sql {
672
+ width: 100%;
673
+ display: none;
674
+ margin: 4px 0 2px 0;
675
+ padding: 8px 10px;
676
+ background: var(--bg-card);
677
+ border: 1px solid var(--border);
678
+ border-radius: var(--radius-sm);
679
+ font-size: 10px;
680
+ line-height: 1.5;
681
+ white-space: pre-wrap;
682
+ word-break: break-word;
683
+ color: var(--text-dim);
684
+ overflow-x: auto;
685
+ max-height: 150px;
686
+ overflow-y: auto;
687
+ position: relative;
688
+ }
689
+
690
+ .tl-event-sql.open { display: block; }
691
+
692
+ .tl-sql-copy {
693
+ position: absolute;
694
+ top: 6px;
695
+ right: 6px;
696
+ padding: 2px 8px;
697
+ font-size: 9px;
698
+ font-family: var(--mono);
699
+ background: var(--bg-muted);
700
+ border: 1px solid var(--border);
701
+ border-radius: var(--radius-sm);
702
+ color: var(--text-muted);
703
+ cursor: pointer;
704
+ transition: all .15s;
705
+ }
706
+
707
+ .tl-sql-copy:hover {
708
+ background: var(--bg-hover);
709
+ color: var(--text);
710
+ border-color: var(--border-light);
711
+ }
712
+
713
+ .tl-nested {
714
+ margin-left: 24px;
715
+ padding-left: 10px;
716
+ border-left: 2px dashed var(--blue);
717
+ margin-top: 2px;
718
+ margin-bottom: 2px;
719
+ position: relative;
720
+ }
721
+
722
+ .tl-nested::before {
723
+ content: "";
724
+ position: absolute;
725
+ top: 12px;
726
+ left: -2px;
727
+ width: 8px;
728
+ height: 0;
729
+ border-top: 2px dashed var(--blue);
730
+ }
731
+
732
+ .tl-nested-label {
733
+ display: block;
734
+ font-family: var(--mono);
735
+ font-size: 9px;
736
+ color: var(--blue);
737
+ letter-spacing: .3px;
738
+ padding: 2px 0 4px;
739
+ opacity: .7;
740
+ }
741
+
742
+ .tl-nested-event {
743
+ opacity: .9;
744
+ font-size: 10px;
745
+ }
493
746
  </style>
494
747
  </head>
495
748
  <body>
496
-
497
- <div class="app" id="app">
498
- <aside class="sidebar">
499
- <div class="sidebar-logo">
500
- <span class="logo-text">brakit</span>
501
- <span class="logo-version">v0.0.0</span>
502
- </div>
503
- <nav class="sidebar-nav">
504
- <button class="sidebar-item active" data-view="overview">
505
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
506
- <span class="item-label">Overview</span>
507
- </button>
508
- <div class="sidebar-section">Monitor</div>
509
- <button class="sidebar-item" data-view="actions">
510
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span>
511
- <span class="item-label">Actions</span>
512
- <span class="item-count" id="sidebar-count-actions">0</span>
513
- </button>
514
- <button class="sidebar-item" data-view="requests">
515
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
516
- <span class="item-label">Requests</span>
517
- <span class="item-count" id="sidebar-count-requests">0</span>
518
- </button>
519
- <button class="sidebar-item" data-view="fetches">
520
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span>
521
- <span class="item-label">Fetches</span>
522
- <span class="item-count" id="sidebar-count-fetches">0</span>
523
- </button>
524
- <div class="sidebar-section">Insights</div>
525
- <button class="sidebar-item" data-view="queries">
526
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg></span>
527
- <span class="item-label">Queries</span>
528
- <span class="item-count" id="sidebar-count-queries">0</span>
529
- </button>
530
- <button class="sidebar-item" data-view="errors">
531
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg></span>
532
- <span class="item-label">Errors</span>
533
- <span class="item-count" id="sidebar-count-errors">0</span>
534
- </button>
535
- <button class="sidebar-item" data-view="logs">
536
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>
537
- <span class="item-label">Logs</span>
538
- <span class="item-count" id="sidebar-count-logs">0</span>
539
- </button>
540
- <button class="sidebar-item" data-view="security">
541
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span>
542
- <span class="item-label">Security</span>
543
- <span class="item-count" id="sidebar-count-security" style="display:none">0</span>
544
- </button>
545
- <button class="sidebar-item" data-view="performance">
546
- <span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></span>
547
- <span class="item-label">Performance</span>
548
- </button>
549
- </nav>
550
- <div class="sidebar-footer">:{{PORT}}</div>
551
- </aside>
552
- <div class="main-panel">
553
- <div class="header">
554
- <div class="header-left">
555
- <span class="header-title" id="header-title">Overview</span>
556
- <span class="header-sub" id="header-sub">Live summary of your application</span>
749
+ <bk-dashboard></bk-dashboard>
750
+ <script>window.__BRAKIT_CONFIG__={port:{{PORT}},version:"{{VERSION}}"};</script>
751
+ <script>(function(){'use strict';var Rs=Object.defineProperty;var ws=Object.getOwnPropertyDescriptor;var u=(i,e,t,s)=>{for(var r=s>1?void 0:s?ws(e,t):e,o=i.length-1,n;o>=0;o--)(n=i[o])&&(r=(s?n(e,t,r):n(r))||r);return s&&r&&Rs(e,t,r),r};var Mt=globalThis,Ot=Mt.ShadowRoot&&(Mt.ShadyCSS===void 0||Mt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,xe=Symbol(),Te=new WeakMap,Lt=class{constructor(e,t,s){if(this._$cssResult$=true,s!==xe)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t;}get styleSheet(){let e=this.o,t=this.t;if(Ot&&e===void 0){let s=t!==void 0&&t.length===1;s&&(e=Te.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),s&&Te.set(t,e));}return e}toString(){return this.cssText}},Re=i=>new Lt(typeof i=="string"?i:i+"",void 0,xe);var we=(i,e)=>{if(Ot)i.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let s=document.createElement("style"),r=Mt.litNonce;r!==void 0&&s.setAttribute("nonce",r),s.textContent=t.cssText,i.appendChild(s);}},Qt=Ot?i=>i:i=>i instanceof CSSStyleSheet?(e=>{let t="";for(let s of e.cssRules)t+=s.cssText;return Re(t)})(i):i;var{is:As,defineProperty:Cs,getOwnPropertyDescriptor:Is,getOwnPropertyNames:Ms,getOwnPropertySymbols:Ls,getPrototypeOf:Os}=Object,N=globalThis,Ae=N.trustedTypes,ks=Ae?Ae.emptyScript:"",Ns=N.reactiveElementPolyfillSupport,ht=(i,e)=>i,mt={toAttribute(i,e){switch(e){case Boolean:i=i?ks:null;break;case Object:case Array:i=i==null?i:JSON.stringify(i);}return i},fromAttribute(i,e){let t=i;switch(e){case Boolean:t=i!==null;break;case Number:t=i===null?null:Number(i);break;case Object:case Array:try{t=JSON.parse(i);}catch{t=null;}}return t}},kt=(i,e)=>!As(i,e),Ce={attribute:true,type:String,converter:mt,reflect:false,useDefault:false,hasChanged:kt};Symbol.metadata??(Symbol.metadata=Symbol("metadata")),N.litPropertyMetadata??(N.litPropertyMetadata=new WeakMap);var O=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??(this.l=[])).push(e);}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=Ce){if(t.state&&(t.attribute=false),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=true),this.elementProperties.set(e,t),!t.noAccessor){let s=Symbol(),r=this.getPropertyDescriptor(e,s,t);r!==void 0&&Cs(this.prototype,e,r);}}static getPropertyDescriptor(e,t,s){let{get:r,set:o}=Is(this.prototype,e)??{get(){return this[t]},set(n){this[t]=n;}};return {get:r,set(n){let l=r?.call(this);o?.call(this,n),this.requestUpdate(e,l,s);},configurable:true,enumerable:true}}static getPropertyOptions(e){return this.elementProperties.get(e)??Ce}static _$Ei(){if(this.hasOwnProperty(ht("elementProperties")))return;let e=Os(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties);}static finalize(){if(this.hasOwnProperty(ht("finalized")))return;if(this.finalized=true,this._$Ei(),this.hasOwnProperty(ht("properties"))){let t=this.properties,s=[...Ms(t),...Ls(t)];for(let r of s)this.createProperty(r,t[r]);}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[s,r]of t)this.elementProperties.set(s,r);}this._$Eh=new Map;for(let[t,s]of this.elementProperties){let r=this._$Eu(t,s);r!==void 0&&this._$Eh.set(r,t);}this.elementStyles=this.finalizeStyles(this.styles);}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let s=new Set(e.flat(1/0).reverse());for(let r of s)t.unshift(Qt(r));}else e!==void 0&&t.push(Qt(e));return t}static _$Eu(e,t){let s=t.attribute;return s===false?void 0:typeof s=="string"?s:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=false,this.hasUpdated=false,this._$Em=null,this._$Ev();}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this));}addController(e){(this._$EO??(this._$EO=new Set)).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.();}removeController(e){this._$EO?.delete(e);}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let s of t.keys())this.hasOwnProperty(s)&&(e.set(s,this[s]),delete this[s]);e.size>0&&(this._$Ep=e);}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return we(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??(this.renderRoot=this.createRenderRoot()),this.enableUpdating(true),this._$EO?.forEach(e=>e.hostConnected?.());}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.());}attributeChangedCallback(e,t,s){this._$AK(e,s);}_$ET(e,t){let s=this.constructor.elementProperties.get(e),r=this.constructor._$Eu(e,s);if(r!==void 0&&s.reflect===true){let o=(s.converter?.toAttribute!==void 0?s.converter:mt).toAttribute(t,s.type);this._$Em=e,o==null?this.removeAttribute(r):this.setAttribute(r,o),this._$Em=null;}}_$AK(e,t){let s=this.constructor,r=s._$Eh.get(e);if(r!==void 0&&this._$Em!==r){let o=s.getPropertyOptions(r),n=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:mt;this._$Em=r;let l=n.fromAttribute(t,o.type);this[r]=l??this._$Ej?.get(r)??l,this._$Em=null;}}requestUpdate(e,t,s,r=false,o){if(e!==void 0){let n=this.constructor;if(r===false&&(o=this[e]),s??(s=n.getPropertyOptions(e)),!((s.hasChanged??kt)(o,t)||s.useDefault&&s.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,s))))return;this.C(e,t,s);}this.isUpdatePending===false&&(this._$ES=this._$EP());}C(e,t,{useDefault:s,reflect:r,wrapped:o},n){s&&!(this._$Ej??(this._$Ej=new Map)).has(e)&&(this._$Ej.set(e,n??t??this[e]),o!==true||n!==void 0)||(this._$AL.has(e)||(this.hasUpdated||s||(t=void 0),this._$AL.set(e,t)),r===true&&this._$Em!==e&&(this._$Eq??(this._$Eq=new Set)).add(e));}async _$EP(){this.isUpdatePending=true;try{await this._$ES;}catch(t){Promise.reject(t);}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??(this.renderRoot=this.createRenderRoot()),this._$Ep){for(let[r,o]of this._$Ep)this[r]=o;this._$Ep=void 0;}let s=this.constructor.elementProperties;if(s.size>0)for(let[r,o]of s){let{wrapped:n}=o,l=this[r];n!==true||this._$AL.has(r)||l===void 0||this.C(r,void 0,o,l);}}let e=false,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(s=>s.hostUpdate?.()),this.update(t)):this._$EM();}catch(s){throw e=false,this._$EM(),s}e&&this._$AE(t);}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=true,this.firstUpdated(e)),this.updated(e);}_$EM(){this._$AL=new Map,this.isUpdatePending=false;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return true}update(e){this._$Eq&&(this._$Eq=this._$Eq.forEach(t=>this._$ET(t,this[t]))),this._$EM();}updated(e){}firstUpdated(e){}};O.elementStyles=[],O.shadowRootOptions={mode:"open"},O[ht("elementProperties")]=new Map,O[ht("finalized")]=new Map,Ns?.({ReactiveElement:O}),(N.reactiveElementVersions??(N.reactiveElementVersions=[])).push("2.1.2");var ft=globalThis,Ie=i=>i,Nt=ft.trustedTypes,Me=Nt?Nt.createPolicy("lit-html",{createHTML:i=>i}):void 0,He="$lit$",D=`lit$${Math.random().toFixed(9).slice(2)}$`,qe="?"+D,Ds=`<${qe}>`,W=document,gt=()=>W.createComment(""),bt=i=>i===null||typeof i!="object"&&typeof i!="function",Zt=Array.isArray,Hs=i=>Zt(i)||typeof i?.[Symbol.iterator]=="function",Vt=`[
752
+ \f\r]`,vt=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Le=/-->/g,Oe=/>/g,B=RegExp(`>|${Vt}(?:([^\\s"'>=/]+)(${Vt}*=${Vt}*(?:[^
753
+ \f\r"'\`<>=]|("|')|))|$)`,"g"),ke=/'/g,Ne=/"/g,Pe=/^(?:script|style|textarea|title)$/i,te=i=>(e,...t)=>({_$litType$:i,strings:e,values:t}),a=te(1),j=Symbol.for("lit-noChange"),d=Symbol.for("lit-nothing"),De=new WeakMap,G=W.createTreeWalker(W,129);function Ue(i,e){if(!Zt(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return Me!==void 0?Me.createHTML(e):e}var qs=(i,e)=>{let t=i.length-1,s=[],r,o=e===2?"<svg>":e===3?"<math>":"",n=vt;for(let l=0;l<t;l++){let c=i[l],p,h,m=-1,b=0;for(;b<c.length&&(n.lastIndex=b,h=n.exec(c),h!==null);)b=n.lastIndex,n===vt?h[1]==="!--"?n=Le:h[1]!==void 0?n=Oe:h[2]!==void 0?(Pe.test(h[2])&&(r=RegExp("</"+h[2],"g")),n=B):h[3]!==void 0&&(n=B):n===B?h[0]===">"?(n=r??vt,m=-1):h[1]===void 0?m=-2:(m=n.lastIndex-h[2].length,p=h[1],n=h[3]===void 0?B:h[3]==='"'?Ne:ke):n===Ne||n===ke?n=B:n===Le||n===Oe?n=vt:(n=B,r=void 0);let $=n===B&&i[l+1].startsWith("/>")?" ":"";o+=n===vt?c+Ds:m>=0?(s.push(p),c.slice(0,m)+He+c.slice(m)+D+$):c+D+(m===-2?l:$);}return [Ue(i,o+(i[t]||"<?>")+(e===2?"</svg>":e===3?"</math>":"")),s]},Et=class i{constructor({strings:e,_$litType$:t},s){let r;this.parts=[];let o=0,n=0,l=e.length-1,c=this.parts,[p,h]=qs(e,t);if(this.el=i.createElement(p,s),G.currentNode=this.el.content,t===2||t===3){let m=this.el.content.firstChild;m.replaceWith(...m.childNodes);}for(;(r=G.nextNode())!==null&&c.length<l;){if(r.nodeType===1){if(r.hasAttributes())for(let m of r.getAttributeNames())if(m.endsWith(He)){let b=h[n++],$=r.getAttribute(m).split(D),E=/([.?@])?(.*)/.exec(b);c.push({type:1,index:o,name:E[2],strings:$,ctor:E[1]==="."?Xt:E[1]==="?"?Kt:E[1]==="@"?zt:z}),r.removeAttribute(m);}else m.startsWith(D)&&(c.push({type:6,index:o}),r.removeAttribute(m));if(Pe.test(r.tagName)){let m=r.textContent.split(D),b=m.length-1;if(b>0){r.textContent=Nt?Nt.emptyScript:"";for(let $=0;$<b;$++)r.append(m[$],gt()),G.nextNode(),c.push({type:2,index:++o});r.append(m[b],gt());}}}else if(r.nodeType===8)if(r.data===qe)c.push({type:2,index:o});else {let m=-1;for(;(m=r.data.indexOf(D,m+1))!==-1;)c.push({type:7,index:o}),m+=D.length-1;}o++;}}static createElement(e,t){let s=W.createElement("template");return s.innerHTML=e,s}};function K(i,e,t=i,s){if(e===j)return e;let r=s!==void 0?t._$Co?.[s]:t._$Cl,o=bt(e)?void 0:e._$litDirective$;return r?.constructor!==o&&(r?._$AO?.(false),o===void 0?r=void 0:(r=new o(i),r._$AT(i,t,s)),s!==void 0?(t._$Co??(t._$Co=[]))[s]=r:t._$Cl=r),r!==void 0&&(e=K(i,r._$AS(i,e.values),r,s)),e}var Yt=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:s}=this._$AD,r=(e?.creationScope??W).importNode(t,true);G.currentNode=r;let o=G.nextNode(),n=0,l=0,c=s[0];for(;c!==void 0;){if(n===c.index){let p;c.type===2?p=new _t(o,o.nextSibling,this,e):c.type===1?p=new c.ctor(o,c.name,c.strings,this,e):c.type===6&&(p=new Jt(o,this,e)),this._$AV.push(p),c=s[++l];}n!==c?.index&&(o=G.nextNode(),n++);}return G.currentNode=W,r}p(e){let t=0;for(let s of this._$AV)s!==void 0&&(s.strings!==void 0?(s._$AI(e,s,t),t+=s.strings.length-2):s._$AI(e[t])),t++;}},_t=class i{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,s,r){this.type=2,this._$AH=d,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=s,this.options=r,this._$Cv=r?.isConnected??true;}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=K(this,e,t),bt(e)?e===d||e==null||e===""?(this._$AH!==d&&this._$AR(),this._$AH=d):e!==this._$AH&&e!==j&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):Hs(e)?this.k(e):this._(e);}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e));}_(e){this._$AH!==d&&bt(this._$AH)?this._$AA.nextSibling.data=e:this.T(W.createTextNode(e)),this._$AH=e;}$(e){let{values:t,_$litType$:s}=e,r=typeof s=="number"?this._$AC(e):(s.el===void 0&&(s.el=Et.createElement(Ue(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===r)this._$AH.p(t);else {let o=new Yt(r,this),n=o.u(this.options);o.p(t),this.T(n),this._$AH=o;}}_$AC(e){let t=De.get(e.strings);return t===void 0&&De.set(e.strings,t=new Et(e)),t}k(e){Zt(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,s,r=0;for(let o of e)r===t.length?t.push(s=new i(this.O(gt()),this.O(gt()),this,this.options)):s=t[r],s._$AI(o),r++;r<t.length&&(this._$AR(s&&s._$AB.nextSibling,r),t.length=r);}_$AR(e=this._$AA.nextSibling,t){for(this._$AP?.(false,true,t);e!==this._$AB;){let s=Ie(e).nextSibling;Ie(e).remove(),e=s;}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e));}},z=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,s,r,o){this.type=1,this._$AH=d,this._$AN=void 0,this.element=e,this.name=t,this._$AM=r,this.options=o,s.length>2||s[0]!==""||s[1]!==""?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=d;}_$AI(e,t=this,s,r){let o=this.strings,n=false;if(o===void 0)e=K(this,e,t,0),n=!bt(e)||e!==this._$AH&&e!==j,n&&(this._$AH=e);else {let l=e,c,p;for(e=o[0],c=0;c<o.length-1;c++)p=K(this,l[s+c],t,c),p===j&&(p=this._$AH[c]),n||(n=!bt(p)||p!==this._$AH[c]),p===d?e=d:e!==d&&(e+=(p??"")+o[c+1]),this._$AH[c]=p;}n&&!r&&this.j(e);}j(e){e===d?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??"");}},Xt=class extends z{constructor(){super(...arguments),this.type=3;}j(e){this.element[this.name]=e===d?void 0:e;}},Kt=class extends z{constructor(){super(...arguments),this.type=4;}j(e){this.element.toggleAttribute(this.name,!!e&&e!==d);}},zt=class extends z{constructor(e,t,s,r,o){super(e,t,s,r,o),this.type=5;}_$AI(e,t=this){if((e=K(this,e,t,0)??d)===j)return;let s=this._$AH,r=e===d&&s!==d||e.capture!==s.capture||e.once!==s.once||e.passive!==s.passive,o=e!==d&&(s===d||r);r&&this.element.removeEventListener(this.name,this,s),o&&this.element.addEventListener(this.name,this,e),this._$AH=e;}handleEvent(e){typeof this._$AH=="function"?this._$AH.call(this.options?.host??this.element,e):this._$AH.handleEvent(e);}},Jt=class{constructor(e,t,s){this.element=e,this.type=6,this._$AN=void 0,this._$AM=t,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(e){K(this,e);}};var Ps=ft.litHtmlPolyfillSupport;Ps?.(Et,_t),(ft.litHtmlVersions??(ft.litHtmlVersions=[])).push("3.3.2");var Fe=(i,e,t)=>{let s=t?.renderBefore??e,r=s._$litPart$;if(r===void 0){let o=t?.renderBefore??null;s._$litPart$=r=new _t(e.insertBefore(gt(),o),o,void 0,t??{});}return r._$AI(i),r};var St=globalThis,f=class extends O{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){var t;let e=super.createRenderRoot();return (t=this.renderOptions).renderBefore??(t.renderBefore=e.firstChild),e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=Fe(t,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return j}};f._$litElement$=true,f.finalized=true,St.litElementHydrateSupport?.({LitElement:f});var Us=St.litElementPolyfillSupport;Us?.({LitElement:f});(St.litElementVersions??(St.litElementVersions=[])).push("4.2.2");var g=i=>(e,t)=>{t!==void 0?t.addInitializer(()=>{customElements.define(i,e);}):customElements.define(i,e);};var Fs={attribute:true,type:String,converter:mt,reflect:false,hasChanged:kt},Bs=(i=Fs,e,t)=>{let{kind:s,metadata:r}=t,o=globalThis.litPropertyMetadata.get(r);if(o===void 0&&globalThis.litPropertyMetadata.set(r,o=new Map),s==="setter"&&((i=Object.create(i)).wrapped=true),o.set(t.name,i),s==="accessor"){let{name:n}=t;return {set(l){let c=e.get.call(this);e.set.call(this,l),this.requestUpdate(n,c,i,true,l);},init(l){return l!==void 0&&this.C(n,void 0,i,l),l}}}if(s==="setter"){let{name:n}=t;return function(l){let c=this[n];e.call(this,l),this.requestUpdate(n,c,i,true,l);}}throw Error("Unsupported decorator location: "+s)};function T(i){return (e,t)=>typeof t=="object"?Bs(i,e,t):((s,r,o)=>{let n=r.hasOwnProperty(o);return r.constructor.createProperty(o,s),n?Object.getOwnPropertyDescriptor(r,o):void 0})(i,e,t)}function _(i){return T({...i,state:true,attribute:false})}var $t=class extends f{constructor(){super(...arguments);this.method="";}createRenderRoot(){return this}render(){let t=this.method.toUpperCase();return a`<span class="method-badge method-badge-${t}">${t}</span>`}};u([T()],$t.prototype,"method",2),$t=u([g("bk-method-badge")],$t);var I="/__brakit/api",k="/__brakit",y={flows:`${I}/flows`,requests:`${I}/requests`,events:`${I}/events`,clear:`${I}/clear`,fetches:`${I}/fetches`,errors:`${I}/errors`,logs:`${I}/logs`,queries:`${I}/queries`,metricsLive:`${I}/metrics/live`,insights:`${I}/insights`,tab:`${I}/tab`,activity:`${I}/activity`};var J="polling",Ht="static",Gs="auth-handshake",Ws="auth-check",js="middleware",yt={[Gs]:1,[Ws]:1,[js]:1};var ee="fetch";var se="error_event",re="query",oe="issues";var ie={overview:"Overview",actions:"Actions",requests:"Requests",fetches:"Server Fetches",queries:"Queries",errors:"Errors",logs:"Logs",performance:"Performance",security:"Security"},ne={overview:"Live summary of your application",actions:"User actions captured as sequences of HTTP requests",requests:"All HTTP requests proxied through brakit",fetches:"Outbound HTTP calls made by your server to external services",queries:"Database queries executed during request handling",errors:"Unhandled exceptions and errors thrown by your application",logs:"Console output from your application",performance:"Endpoint health and response time trends",security:"Security findings and recommendations"};var le=100,Z=300,tt=800,ce=2e3,de=100,pe=50,ue=500;var H="__all__",Ft={SELECT:"var(--blue)",INSERT:"var(--green)",UPDATE:"var(--amber)",DELETE:"var(--red)",COUNT:"var(--text-muted)"},Ke={error:"var(--red)",warn:"var(--amber)",info:"var(--blue)",debug:"var(--text-muted)",log:"var(--text-dim)"},me=["#2563eb","#7c3aed","#16a34a","#d97706","#dc2626","#0891b2","#ea580c","#c026d3","#059669","#db2777"],Tt={green:"#4ade80",amber:"#fbbf24",red:"#f87171"},et=[{max:le,label:"Fast",color:"var(--green)",bg:"rgba(22,163,74,0.08)",border:"rgba(22,163,74,0.2)"},{max:Z,label:"Good",color:"var(--green)",bg:"rgba(22,163,74,0.06)",border:"rgba(22,163,74,0.15)"},{max:tt,label:"OK",color:"var(--amber)",bg:"rgba(217,119,6,0.06)",border:"rgba(217,119,6,0.15)"},{max:ce,label:"Slow",color:"var(--red)",bg:"rgba(220,38,38,0.06)",border:"rgba(220,38,38,0.15)"},{max:1/0,label:"Critical",color:"var(--red)",bg:"rgba(220,38,38,0.08)",border:"rgba(220,38,38,0.2)"}],ze="rgba(228,228,231,0.8)",ve="rgba(113,113,122,0.7)",Je="10px monospace",fe="9px monospace";var Ze={top:16,right:16,bottom:28,left:52},ts={fetch:"var(--blue)",log:"var(--text-muted)",error:"var(--red)",query:"var(--accent)"},es={fetch:"FETCH",log:"LOG",error:"ERROR",query:"QUERY"},ss=new Set(["cookie","set-cookie","authorization","proxy-authorization","x-api-key","x-auth-token"]),rs={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"},os=new Set(["host","connection","accept-encoding"]),q={critical:{icon:"\u2717",cls:"critical",sort:0},warning:{icon:"\u26A0",cls:"warning",sort:1},info:{icon:"\u2139",cls:"info",sort:2}};function v(i){return i<1e3?i+"ms":(i/1e3).toFixed(1)+"s"}function U(i){return !i||i===0?"":i<1024?i+"b":(i/1024).toFixed(1)+"kb"}function P(i){return i?i.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}function st(i){return i>=500?"status-pill-5xx":i>=400?"status-pill-4xx":i>=300?"status-pill-3xx":"status-pill-2xx"}function is(i){return rs[i]||(i>=500?"Server Error":i>=400?"Client Error":"OK")}function Qs(i,e){if(ss.has(i.toLowerCase())){let t=String(e);return t.length<=8?"****":t.slice(0,4)+"..."+t.slice(-4)+" ("+t.length+" chars)"}return String(e)}function rt(i){return !i||Object.keys(i).length===0?'<span style="color:var(--text-muted)">No headers</span>':Object.entries(i).map(([e,t])=>'<span class="json-key">'+P(e)+"</span>: "+P(Qs(e,t))).join(`
754
+ `)}function Q(i){if(!i)return '<span style="color:var(--text-muted)">No body</span>';try{let e=JSON.parse(i);return Vs(JSON.stringify(e,null,2))}catch{return P(i)}}function Vs(i){return P(i).replace(/("(?:[^"\\]|\\.)*")(\s*:)?|\b(true|false)\b|\bnull\b|(-?\d+\.?\d*(?:[eE][+-]?\d+)?)/g,(e,t,s,r,o)=>t?s?'<span class="json-key">'+t+"</span>"+s:'<span class="json-str">'+t+"</span>":r?'<span class="json-bool">'+e+"</span>":o?'<span class="json-num">'+e+"</span>":e==="null"?'<span class="json-null">null</span>':e)}var xt=class extends f{constructor(){super(...arguments);this.code=0;}createRenderRoot(){return this}render(){let t=st(this.code);return a`<span class="status-pill ${t}">${this.code}</span>`}};u([T({type:Number})],xt.prototype,"code",2),xt=u([g("bk-status-pill")],xt);var Rt=class extends f{constructor(){super(...arguments);this.ms=0;}createRenderRoot(){return this}render(){return a`<span class="req-duration">${v(this.ms)}</span>`}};u([T({type:Number})],Rt.prototype,"ms",2),Rt=u([g("bk-duration-label")],Rt);var ot=class extends f{constructor(){super(...arguments);this.title="";this.subtitle="";}createRenderRoot(){return this}render(){return a`
755
+ <div class="empty">
756
+ <span class="empty-title">${this.title}</span>
757
+ <span class="empty-sub">${this.subtitle}</span>
758
+ </div>
759
+ `}};u([T()],ot.prototype,"title",2),u([T()],ot.prototype,"subtitle",2),ot=u([g("bk-empty-state")],ot);var C=class extends f{constructor(){super(...arguments);this.message="";this.visible=false;}createRenderRoot(){return this}static show(t){let s=document.querySelector("bk-toast");s&&s.showMessage(t);}showMessage(t){this.hideTimer&&clearTimeout(this.hideTimer),this.message=t,this.visible=true,this.hideTimer=setTimeout(()=>{this.visible=false;},2e3);}render(){return a`<div class="toast ${this.visible?"show":""}">${this.message}</div>`}};u([_()],C.prototype,"message",2),u([_()],C.prototype,"visible",2),C=u([g("bk-toast")],C);var V=class extends f{constructor(){super(...arguments);this.text="";this.label="Copy";this.toastMessage="Copied";}createRenderRoot(){return this}async copy(t){t.stopPropagation();try{await navigator.clipboard.writeText(this.text),C.show(this.toastMessage);}catch{}}render(){return a`<button class="query-detail-copy" @click=${this.copy}>${this.label}</button>`}};u([T()],V.prototype,"text",2),u([T()],V.prototype,"label",2),u([T({attribute:"toast-message"})],V.prototype,"toastMessage",2),V=u([g("bk-copy-button")],V);var Y=class extends f{constructor(){super(...arguments);this.value="";this.label="";this.color="";}createRenderRoot(){return this}render(){return a`
760
+ <div class="fetch-stat">
761
+ <span class="fetch-stat-value" style="color:${this.color}">${this.value}</span>
762
+ <span class="fetch-stat-label">${this.label}</span>
763
+ </div>
764
+ `}};u([T()],Y.prototype,"value",2),u([T()],Y.prototype,"label",2),u([T()],Y.prototype,"color",2),Y=u([g("bk-stat-card")],Y);var F=class extends Event{constructor(e,t,s,r){super("context-request",{bubbles:true,composed:true}),this.context=e,this.contextTarget=t,this.callback=s,this.subscribe=r??false;}};var it=class{constructor(e,t,s,r){if(this.subscribe=false,this.provided=false,this.value=void 0,this.t=(o,n)=>{this.unsubscribe&&(this.unsubscribe!==n&&(this.provided=false,this.unsubscribe()),this.subscribe||this.unsubscribe()),this.value=o,this.host.requestUpdate(),this.provided&&!this.subscribe||(this.provided=true,this.callback&&this.callback(o,n)),this.unsubscribe=n;},this.host=e,t.context!==void 0){let o=t;this.context=o.context,this.callback=o.callback,this.subscribe=o.subscribe??false;}else this.context=t,this.callback=s,this.subscribe=r??false;this.host.addController(this);}hostConnected(){this.dispatchRequest();}hostDisconnected(){this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=void 0);}dispatchRequest(){this.host.dispatchEvent(new F(this.context,this.host,this.t,this.subscribe));}};var Bt=class{get value(){return this.o}set value(e){this.setValue(e);}setValue(e,t=false){let s=t||!Object.is(e,this.o);this.o=e,s&&this.updateObservers();}constructor(e){this.subscriptions=new Map,this.updateObservers=()=>{for(let[t,{disposer:s}]of this.subscriptions)t(this.o,s);},e!==void 0&&(this.value=e);}addCallback(e,t,s){if(!s)return void e(this.value);this.subscriptions.has(e)||this.subscriptions.set(e,{disposer:()=>{this.subscriptions.delete(e);},consumerHost:t});let{disposer:r}=this.subscriptions.get(e);e(this.value,r);}clearCallbacks(){this.subscriptions.clear();}};var ge=class extends Event{constructor(e,t){super("context-provider",{bubbles:true,composed:true}),this.context=e,this.contextTarget=t;}},nt=class extends Bt{constructor(e,t,s){super(t.context!==void 0?t.initialValue:s),this.onContextRequest=r=>{if(r.context!==this.context)return;let o=r.contextTarget??r.composedPath()[0];o!==this.host&&(r.stopPropagation(),this.addCallback(r.callback,o,r.subscribe));},this.onProviderRequest=r=>{if(r.context!==this.context||(r.contextTarget??r.composedPath()[0])===this.host)return;let o=new Set;for(let[n,{consumerHost:l}]of this.subscriptions)o.has(n)||(o.add(n),l.dispatchEvent(new F(this.context,l,n,true)));r.stopPropagation();},this.host=e,t.context!==void 0?this.context=t.context:this.context=t,this.attachListeners(),this.host.addController?.(this);}attachListeners(){this.host.addEventListener("context-request",this.onContextRequest),this.host.addEventListener("context-provider",this.onProviderRequest);}hostConnected(){this.host.dispatchEvent(new ge(this.context,this.host));}};function be({context:i}){return (e,t)=>{let s=new WeakMap;if(typeof t=="object")return {get(){return e.get.call(this)},set(r){return s.get(this).setValue(r),e.set.call(this,r)},init(r){return s.set(this,new nt(this,{context:i,initialValue:r})),r}};{e.constructor.addInitializer((n=>{s.set(n,new nt(n,{context:i}));}));let r=Object.getOwnPropertyDescriptor(e,t),o;if(r===void 0){let n=new WeakMap;o={get(){return n.get(this)},set(l){s.get(this).setValue(l),n.set(this,l);},configurable:true,enumerable:true};}else {let n=r.set;o={...r,set(l){s.get(this).setValue(l),n?.call(this,l);}};}return void Object.defineProperty(e,t,o)}}}function R({context:i,subscribe:e}){return (t,s)=>{typeof s=="object"?s.addInitializer((function(){new it(this,{context:i,callback:r=>{t.set.call(this,r);},subscribe:e});})):t.constructor.addInitializer((r=>{new it(r,{context:i,callback:o=>{r[s]=o;},subscribe:e});}));}}var x="dashboard-store",Gt=class extends EventTarget{constructor(){super(...arguments);this._state={flows:[],requests:[],fetches:[],errors:[],logs:[],queries:[],issues:[],metrics:[],viewMode:"simple",activeView:"overview"};}get state(){return this._state}setFlows(t){this._state={...this._state,flows:t},this.notify("flows");}setRequests(t){this._state={...this._state,requests:t},this.notify("requests");}setFetches(t){this._state={...this._state,fetches:t},this.notify("fetches");}setErrors(t){this._state={...this._state,errors:t},this.notify("errors");}setLogs(t){this._state={...this._state,logs:t},this.notify("logs");}setQueries(t){this._state={...this._state,queries:t},this.notify("queries");}setIssues(t){this._state={...this._state,issues:t},this.notify("issues");}setMetrics(t){this._state={...this._state,metrics:t},this.notify("metrics");}prependRequest(t){let s=[t,...this._state.requests.slice(0,999)];this._state={...this._state,requests:s},this.notify("requests");}prependFetch(t){this._state={...this._state,fetches:[t,...this._state.fetches]},this.notify("fetches");}prependError(t){this._state={...this._state,errors:[t,...this._state.errors]},this.notify("errors");}prependLog(t){this._state={...this._state,logs:[t,...this._state.logs]},this.notify("logs");}prependQuery(t){this._state={...this._state,queries:[t,...this._state.queries]},this.notify("queries");}setActiveView(t){this._state={...this._state,activeView:t},this.notify("activeView");}setViewMode(t){this._state={...this._state,viewMode:t},this.notify("viewMode");}clearAll(){this._state={...this._state,flows:[],requests:[],fetches:[],errors:[],logs:[],queries:[],issues:[],metrics:[]},this.notify("all");}notify(t){this.dispatchEvent(new CustomEvent("state-changed",{detail:t}));}};var at=class extends f{constructor(){super(...arguments);this.expandedIdx=-1;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}toggleError(t){this.expandedIdx=this.expandedIdx===t?-1:t;}renderErrorRow(t,s){let r=new Date(t.timestamp).toLocaleTimeString(),o=this.expandedIdx===s;return a`
765
+ <div
766
+ class="req-row tel-clickable ${o?"expanded":""}"
767
+ @click=${()=>this.toggleError(s)}
768
+ >
769
+ <span class="tel-error-name" title=${t.name}>${t.name}</span>
770
+ <span class="tel-message" title=${t.message}>${t.message}</span>
771
+ <span class="tel-timestamp">${r}</span>
772
+ </div>
773
+ ${o&&t.stack?a`<div class="error-stack">${t.stack}</div>`:d}
774
+ `}render(){let t=this.store.state.errors;return t.length===0?a`<bk-empty-state
775
+ title="No errors"
776
+ subtitle="No errors have been captured yet"
777
+ ></bk-empty-state>`:a`
778
+ <div class="col-header">
779
+ <span style="width:180px">Type</span>
780
+ <span style="flex:1">Message</span>
781
+ <span style="width:130px;text-align:right">Time</span>
782
+ </div>
783
+ <div id="error-list">
784
+ ${t.map((s,r)=>this.renderErrorRow(s,r))}
785
+ </div>
786
+ `}};u([R({context:x})],at.prototype,"store",2),u([_()],at.prototype,"expandedIdx",2),at=u([g("bk-errors-view")],at);var wt=class extends f{createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}renderAnalysis(e){if(e.length===0)return d;let t={error:0,warn:0,info:0,debug:0,log:0};for(let s of e)t[s.level]!==void 0&&t[s.level]++;return a`
787
+ <div id="log-analysis">
788
+ <div class="fetch-summary">
789
+ <bk-stat-card value=${String(e.length)} label="Total Logs"></bk-stat-card>
790
+ ${t.error>0?a`<bk-stat-card value=${String(t.error)} label="Errors" color="var(--red)"></bk-stat-card>`:d}
791
+ ${t.warn>0?a`<bk-stat-card value=${String(t.warn)} label="Warnings" color="var(--amber)"></bk-stat-card>`:d}
792
+ <bk-stat-card value=${String(t.info)} label="Info"></bk-stat-card>
793
+ ${t.debug>0?a`<bk-stat-card value=${String(t.debug)} label="Debug"></bk-stat-card>`:d}
794
+ ${t.log>0?a`<bk-stat-card value=${String(t.log)} label="Log"></bk-stat-card>`:d}
795
+ </div>
796
+ </div>
797
+ `}renderLogRow(e){let t=new Date(e.timestamp).toLocaleTimeString();return a`
798
+ <div class="req-row">
799
+ <span class="tel-level tel-level-${e.level}">${e.level.toUpperCase()}</span>
800
+ <span class="tel-message tel-mono" title=${e.message}>${e.message}</span>
801
+ <span class="tel-timestamp">${t}</span>
802
+ </div>
803
+ `}render(){let e=this.store.state.logs;return e.length===0?a`<bk-empty-state
804
+ title="No logs"
805
+ subtitle="No console output has been captured yet"
806
+ ></bk-empty-state>`:a`
807
+ ${this.renderAnalysis(e)}
808
+ <div class="col-header">
809
+ <span style="width:52px">Level</span>
810
+ <span style="flex:1">Message</span>
811
+ <span style="width:130px;text-align:right">Time</span>
812
+ </div>
813
+ <div id="log-list">
814
+ ${e.map(t=>this.renderLogRow(t))}
815
+ </div>
816
+ `}};u([R({context:x})],wt.prototype,"store",2),wt=u([g("bk-logs-view")],wt);var Xs=new Set(["SELECT","FROM","WHERE","AND","OR","INSERT","INTO","VALUES","UPDATE","SET","DELETE","JOIN","LEFT","RIGHT","INNER","OUTER","ON","GROUP","BY","ORDER","HAVING","LIMIT","OFFSET","AS","IN","NOT","NULL","IS","LIKE","BETWEEN","EXISTS","CASE","WHEN","THEN","ELSE","END","COUNT","SUM","AVG","MIN","MAX","DISTINCT","UNION","ALL","CREATE","TABLE","ALTER","DROP","INDEX","RETURNING","WITH","RECURSIVE","OVER","PARTITION","WINDOW","FETCH","NEXT","ROWS","ONLY","CAST","COALESCE","NULLIF","EXTRACT","INTERVAL","TRUE","FALSE","ASC","DESC","USING","NATURAL","CROSS","FULL","ROLLBACK","COMMIT","BEGIN","TRANSACTION","SAVEPOINT","RELEASE"]);function ns(i){let e=i.trim().match(/^(\w+)/);return e?e[1].toUpperCase():"?"}function as(i){let e=i.replace(/\s+/g," ").trim(),t=e.match(/\bFROM\s+["'`]?(\w+)["'`]?/i);if(t)return t[1];let s=e.match(/\bINTO\s+["'`]?(\w+)["'`]?/i);if(s)return s[1];let r=e.match(/\bUPDATE\s+["'`]?(\w+)["'`]?/i);return r?r[1]:""}function ls(i){return P(i).replace(/\b\w+\b/g,e=>Xs.has(e.toUpperCase())?'<span class="sql-kw">'+e+"</span>":e)}var lt=class extends f{constructor(){super(...arguments);this.expandedIdx=-1;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}toggleQuery(t){this.expandedIdx=this.expandedIdx===t?-1:t;}queryDuration(t){return t===0?"<1ms":v(t)}getQueryInfo(t){let s=(t.normalizedOp||t.operation||(t.sql?ns(t.sql):"?")).toUpperCase(),r=t.table||t.model||(t.sql?as(t.sql):""),o=t.sql||s+" "+r;return {op:s,table:r,sqlText:o}}renderQueryRow(t,s){let{op:r,table:o,sqlText:n}=this.getQueryInfo(t),l=Ft[r]||"var(--text-dim)",c=t.durationMs>de,p=t.sql||r+" "+o,h=this.expandedIdx===s;return a`
817
+ <div>
818
+ <div
819
+ class="req-row query-row tel-clickable ${h?"expanded":""}"
820
+ @click=${()=>this.toggleQuery(s)}
821
+ >
822
+ <span class="query-op" title=${r} style="color:${l}">${r}</span>
823
+ <span class="query-table" title=${o}>${o}</span>
824
+ <span class="query-preview" title=${p}>${p}</span>
825
+ <span class="query-dur${c?" query-slow":""}">${this.queryDuration(t.durationMs)}</span>
826
+ </div>
827
+ <div class="query-detail ${h?"open":""}">
828
+ ${h?a`
829
+ <pre class="query-detail-sql" .innerHTML=${ls(n)}></pre>
830
+ <bk-copy-button .text=${n} label="Copy"></bk-copy-button>
831
+ `:d}
832
+ </div>
833
+ </div>
834
+ `}render(){let t=this.store.state.queries;return t.length===0?a`<bk-empty-state
835
+ title="No queries"
836
+ subtitle="No database queries have been captured yet"
837
+ ></bk-empty-state>`:a`
838
+ <div class="col-header">
839
+ <span style="width:70px;border-right:1px solid var(--border);padding-right:16px">Operation</span>
840
+ <span style="width:170px;border-right:1px solid var(--border);padding-right:16px">Table</span>
841
+ <span style="flex:1;border-right:1px solid var(--border);padding-right:16px">Query</span>
842
+ <span style="width:60px;text-align:right">Time</span>
843
+ </div>
844
+ <div id="query-list">
845
+ ${t.map((s,r)=>this.renderQueryRow(s,r))}
846
+ </div>
847
+ `}};u([R({context:x})],lt.prototype,"store",2),u([_()],lt.prototype,"expandedIdx",2),lt=u([g("bk-queries-view")],lt);function Ee(i){return i.replace(/'/g,"'\\''")}function Ks(i,e){let t=Object.entries(i.headers||{}).filter(([o])=>!os.has(o)).map(([o,n])=>`-H '${Ee(o)}: ${Ee(n)}'`).join(" "),s=i.requestBody?` -d '${Ee(i.requestBody)}'`:"",r=e?`http://localhost:${e}`:"";return `curl -X ${i.method} ${t}${s} '${r}${i.url}'`}function ct(i){let e=window.__BRAKIT_CONFIG__?.port??"",t=Ks(i,e);navigator.clipboard.writeText(t).then(()=>C.show("Copied cURL command"));}var dt=class extends f{constructor(){super(...arguments);this.expandedId=null;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}toggleRequest(t){this.expandedId=this.expandedId===t?null:t;}handleCopyAsCurl(t,s){s.stopPropagation(),ct(t);}renderDetail(t){return a`
848
+ <div class="detail-meta">
849
+ <span><bk-method-badge .method=${t.method}></bk-method-badge> ${t.url}</span>
850
+ <span><bk-status-pill .code=${t.statusCode}></bk-status-pill></span>
851
+ <span>${t.durationMs}ms</span>
852
+ ${t.responseSize?a`<span>${U(t.responseSize)}</span>`:d}
853
+ </div>
854
+ <div class="request-timeline tl-hidden" data-request-id=${t.id} data-request-started=${String(t.startedAt)}></div>
855
+ <div class="detail-grid">
856
+ <div class="detail-section"><h4>Request Headers</h4><pre .innerHTML=${rt(t.headers)}></pre></div>
857
+ <div class="detail-section"><h4>Response Headers</h4><pre .innerHTML=${rt(t.responseHeaders)}></pre></div>
858
+ <div class="detail-section"><h4>Request Body</h4><pre .innerHTML=${Q(t.requestBody)}></pre></div>
859
+ <div class="detail-section"><h4>Response Body</h4><pre .innerHTML=${Q(t.responseBody)}></pre></div>
860
+ </div>
861
+ <div class="detail-actions">
862
+ <button class="btn btn-curl" @click=${s=>this.handleCopyAsCurl(t,s)}>Copy cURL</button>
863
+ </div>
864
+ `}renderRequestRow(t){let s=this.expandedId===t.id;return a`
865
+ <div class="req-row ${s?"expanded":""}" @click=${()=>this.toggleRequest(t.id)}>
866
+ <div class="req-summary">
867
+ <bk-method-badge .method=${t.method}></bk-method-badge>
868
+ <span class="req-url">${t.url}</span>
869
+ <bk-status-pill .code=${t.statusCode}></bk-status-pill>
870
+ <bk-duration-label .ms=${t.durationMs}></bk-duration-label>
871
+ <span class="req-size">${U(t.responseSize)}</span>
872
+ </div>
557
873
  </div>
558
- <div class="header-right">
559
- <div class="segmented-control" id="mode-toggle" style="display:none">
560
- <button class="segmented-btn active" id="mode-simple">Quick</button>
561
- <button class="segmented-btn" id="mode-detailed">Detailed</button>
874
+ <div class="req-detail ${s?"open":""}">${s?this.renderDetail(t):d}</div>
875
+ `}render(){let t=this.store.state.requests.filter(s=>!s.path?.startsWith(k));return t.length===0?a`<bk-empty-state title="No requests" subtitle="No HTTP requests have been captured yet"></bk-empty-state>`:a`
876
+ <div class="col-header">
877
+ <span style="width:60px">Method</span>
878
+ <span style="flex:1">URL</span>
879
+ <span style="width:36px;text-align:right">Status</span>
880
+ <span style="width:70px;text-align:right">Time</span>
881
+ <span style="width:60px;text-align:right">Size</span>
882
+ </div>
883
+ <div id="request-list">${t.map(s=>this.renderRequestRow(s))}</div>
884
+ `}};u([R({context:x})],dt.prototype,"store",2),u([_()],dt.prototype,"expandedId",2),dt=u([g("bk-requests-view")],dt);var At=class extends f{createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}buildGroups(e,t){let s=new Map;for(let o of t)s.set(o.id,o);let r={};for(let o of e){let n=o.method+" "+o.url;r[n]||(r[n]={method:o.method,url:o.url,count:0,totalDur:0,maxDur:0,errors:0,callers:{},statusCodes:{},firstTs:o.timestamp,lastTs:o.timestamp});let l=r[n];if(l.count++,l.totalDur+=o.durationMs,o.durationMs>l.maxDur&&(l.maxDur=o.durationMs),o.statusCode>=400&&l.errors++,l.statusCodes[o.statusCode]=(l.statusCodes[o.statusCode]||0)+1,o.timestamp<l.firstTs&&(l.firstTs=o.timestamp),o.timestamp>l.lastTs&&(l.lastTs=o.timestamp),o.parentRequestId){let c=s.get(o.parentRequestId);c&&(l.callers[c.method+" "+(c.path||c.url)]=1);}}return Object.values(r).sort((o,n)=>n.count-o.count)}renderSummary(e){let t=new Set,s=0,r=0;for(let n of e)t.add(n.url),n.statusCode>=400&&s++,r+=n.durationMs;let o=Math.round(r/e.length);return a`
885
+ <div class="fetch-summary">
886
+ <bk-stat-card value=${String(e.length)} label="Total Fetches"></bk-stat-card>
887
+ <bk-stat-card value=${String(t.size)} label="Unique URLs"></bk-stat-card>
888
+ <bk-stat-card value=${String(s)} label="Errors" color=${s>0?"var(--red)":""}></bk-stat-card>
889
+ <bk-stat-card value=${v(o)} label="Avg Duration"></bk-stat-card>
890
+ </div>
891
+ `}formatTime(e){return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}renderGroup(e){let t=Math.round(e.totalDur/e.count),s=e.count>0?Math.round(e.errors/e.count*100):0,r=Object.keys(e.callers),o=Object.entries(e.statusCodes),n=o.length>0?Number(o.sort((l,c)=>c[1]-l[1])[0][0]):0;return a`
892
+ <div class="fetch-group">
893
+ <div class="fetch-group-header">
894
+ <bk-method-badge .method=${e.method}></bk-method-badge>
895
+ <span class="fetch-group-url" title=${e.url}>${e.url}</span>
896
+ ${n>0?a`<bk-status-pill .code=${n}></bk-status-pill>`:d}
897
+ <span class="fetch-group-count">${e.count}x</span>
898
+ </div>
899
+ <div class="fetch-group-meta">
900
+ <span>avg ${v(t)}</span>
901
+ <span class="fetch-group-sep">\u00b7</span>
902
+ <span>max ${v(e.maxDur)}</span>
903
+ <span class="fetch-group-sep">\u00b7</span>
904
+ ${s>0?a`<span class="fetch-group-err">${s}% errors</span>`:a`<span class="fetch-group-ok">0% errors</span>`}
905
+ </div>
906
+ ${e.firstTs>0?a`
907
+ <div class="fetch-group-timeline">
908
+ <span class="fetch-group-timeline-dot"></span>
909
+ <span class="fetch-group-timeline-range">
910
+ ${this.formatTime(e.firstTs)}${e.firstTs!==e.lastTs?a` \u2192 ${this.formatTime(e.lastTs)}`:d}
911
+ </span>
912
+ </div>`:d}
913
+ ${r.length>0?a`
914
+ <div class="fetch-group-callers">
915
+ <span class="fetch-group-callers-label">Called by</span>
916
+ ${r.map(l=>a`<span class="fetch-group-caller-pill">${l}</span>`)}
917
+ </div>`:d}
918
+ </div>
919
+ `}render(){let e=this.store.state.fetches,t=this.store.state.requests;if(e.length===0)return a`<bk-empty-state title="No fetches" subtitle="No outbound HTTP calls have been captured yet"></bk-empty-state>`;let s=this.buildGroups(e,t);return a`
920
+ <div class="fetch-analysis" id="fetch-analysis">
921
+ ${this.renderSummary(e)}
922
+ ${s.length>0?a`
923
+ <div class="fetch-groups-title">Grouped by URL (${s.length})</div>
924
+ <div class="fetch-groups">${s.map(r=>this.renderGroup(r))}</div>
925
+ `:d}
926
+ </div>
927
+ `}};u([R({context:x})],At.prototype,"store",2),At=u([g("bk-fetches-view")],At);function cs(i,e){if(e>=400)return "var(--red)";switch(i){case "GET":return "var(--green)";case "POST":return "var(--blue)";case "PUT":case "PATCH":return "var(--amber)";case "DELETE":return "var(--red)";default:return "var(--text-muted)"}}function _e(i){return i==="query"?"var(--accent)":"var(--cyan)"}function tr(i){return i.type==="query"||i.type==="fetch"}function er(i){let e=(i.normalizedOp||i.operation||"QUERY").toUpperCase(),t=i.table||i.model||"";return {label:`${e} ${t}`,tooltip:i.sql||`${e} ${t}`}}function sr(i){return {label:`${i.method} ${i.url}`,tooltip:`${i.method} ${i.url}`}}function rr(i,e,t,s,r,o){let n=i.data.durationMs||0,l,c;if(o){let p=Math.max(i.timestamp-s,0);l=Math.min(p/r*100,95),c=Math.max(n/r*100,1.5);}else {let p=t[0].timestamp,m=t[t.length-1].timestamp-p;l=m>0?(i.timestamp-p)/m*85:e/Math.max(t.length-1,1)*85,c=Math.max(n/r*100,1.5);}return l+c>100&&(c=Math.max(100-l,1.5)),{leftPct:l,widthPct:c}}function or(i,e){let t=i.timeline.filter(tr);if(t.length===0)return [];let s=e.startedAt,r=e.durationMs||1,o=Math.abs(t[0].timestamp-s)<r*10;return t.map((n,l)=>{let c=n.data.durationMs||0,{leftPct:p,widthPct:h}=rr(n,l,t,s,r,o),m=n.type==="query"?er(n.data):sr(n.data);return {type:n.type,label:m.label,durMs:c,durLabel:v(c),tooltip:m.tooltip,leftPct:p,widthPct:h}})}function ps(i,e){let t=i.requests.filter(l=>!l.isStrictModeDupe);if(t.length===0)return {rows:[],totalMs:0};let s=Math.min(...t.map(l=>l.startedAt)),o=Math.max(...t.map(l=>l.startedAt+l.durationMs))-s;return o===0?{rows:[],totalMs:0}:{rows:t.map(l=>{let c=(l.startedAt-s)/o*100,p=Math.max(l.durationMs/o*100,.5),h=e?.activities?.[l.id],m=h?or(h,l):[];return {label:`${l.method} ${l.label}`,leftPct:c,widthPct:p,color:cs(l.method,l.statusCode),durMs:l.durationMs,durLabel:v(l.durationMs),tooltip:`${l.method} ${l.label} (${v(l.durationMs)})`,subEvents:m}}),totalMs:o}}function us(i){let e=i.requests,t=[],s=[],r=[],o=[],n=new Map;for(let p of e){let h=p.label,m=p.pollingDurationMs||p.durationMs;if(!yt[p.category||""]){if(p.isDuplicate){let b=n.get(h);b?(b.count++,b.wastedMs+=m):n.set(h,{name:h,count:2,wastedMs:m});continue}if(p.statusCode>=400){s.push(h+" ("+is(p.statusCode)+")");continue}p.responseSize>51200&&r.push("Large response: "+h+" returned "+U(p.responseSize)),t.push(h);}}for(let p of n.values())o.push(p);let l="";if(o.length>0){let p=o.map(m=>m.name).join(", "),h=o.reduce((m,b)=>m+b.wastedMs,0);l="Your app fetches "+p+" multiple times on this page. This wastes ~"+v(h)+". Try caching these calls, deduplicating with React Query/SWR, or moving them to a shared layout.";}else s.length>0&&(l="Some requests are failing. Check your API routes and make sure the endpoints exist.");let c=e.filter(p=>p.durationMs>2e3&&p.category!==J);return c.length>0&&!l&&(l=c.map(p=>p.label).join(", ")+` is taking over ${v(2e3)}. Consider adding caching or optimizing the backend query.`),{successes:t,errors:s,warnings:r,duplicates:o,tip:l}}var M=class extends f{constructor(){super(...arguments);this.expandedFlowIdx=-1;this.expandedSubReqIdx=-1;this.flowDetailTab="insights";this.flowTimeline=null;this.flowTimelineLoading=false;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}get flows(){return this.store.state.flows}get viewMode(){return this.store.state.viewMode}flowDotClass(t){return t.hasErrors?"dot-error":t.redundancyPct>0?"dot-warn":"dot-clean"}flowBadgeInfo(t){if(t.hasErrors){let s=t.requests.filter(r=>r.statusCode>=400).length;return {text:s+" error"+(s!==1?"s":""),cls:"badge-error"}}return t.redundancyPct>0?{text:t.redundancyPct+"% redundant",cls:"badge-warn"}:{text:"clean",cls:"badge-clean"}}toggleFlow(t){this.expandedFlowIdx===t?this.expandedFlowIdx=-1:(this.expandedFlowIdx=t,this.expandedSubReqIdx=-1,this.flowDetailTab="insights",this.flowTimeline=null);}toggleSubReq(t,s){s.stopPropagation(),this.expandedSubReqIdx=this.expandedSubReqIdx===t?-1:t;}toggleBodyBlock(t){t.stopPropagation();let s=t.currentTarget,r=s.parentElement;if(!r)return;s.classList.toggle("open");let o=r.querySelector("pre");o&&o.classList.toggle("open");}switchTab(t,s,r){r.stopPropagation(),this.flowDetailTab=t,t==="timeline"&&!this.flowTimeline&&this.loadFlowTimeline(s);}async loadFlowTimeline(t){if(this.flowTimelineLoading)return;let s=t.requests.map(r=>r.id).filter(Boolean);if(s.length!==0){this.flowTimelineLoading=true;try{let r=await fetch(`${y.activity}?requestIds=${s.join(",")}`);if(!r.ok){this.flowTimelineLoading=!1;return}this.flowTimeline=await r.json();}catch{}this.flowTimelineLoading=false;}}loadTimelineForContainer(t){let s=t.querySelectorAll(".request-timeline");for(let r of s){let o=r.getAttribute("data-request-id");if(o&&!r.hasAttribute("data-loaded")){r.setAttribute("data-loaded","1");let n=document.createElement("bk-timeline-panel");n.setAttribute("request-id",o),n.setAttribute("request-started",r.getAttribute("data-request-started")||"0"),r.appendChild(n),r.classList.remove("tl-hidden");}}}updated(){if(this.expandedFlowIdx>=0&&this.flowDetailTab==="insights"){let t=this.querySelector(".flow-expand.open");t&&this.loadTimelineForContainer(t);}}render(){let t=this.flows;return t.length===0?a`<bk-empty-state
928
+ title="No actions yet"
929
+ subtitle="Start using your app to see user action flows here"
930
+ ></bk-empty-state>`:a`
931
+ <div id="flow-col-header" class="col-header">
932
+ <span style="width:8px"></span>
933
+ <span style="flex:1">Action</span>
934
+ <span style="width:60px;text-align:right">Reqs</span>
935
+ <span style="width:120px;text-align:right">Status</span>
936
+ <span style="width:70px;text-align:right">Time</span>
937
+ </div>
938
+ <div id="flow-list">
939
+ ${t.map((s,r)=>this.renderFlowRow(s,r))}
940
+ </div>
941
+ `}renderFlowRow(t,s){let r=this.expandedFlowIdx===s,o=this.flowDotClass(t),n=this.flowBadgeInfo(t);return a`
942
+ <div
943
+ class="flow-row ${r?"expanded":""}"
944
+ @click=${()=>this.toggleFlow(s)}
945
+ >
946
+ <div class="flow-summary-row">
947
+ <span class="flow-status-dot ${o}"></span>
948
+ <span class="flow-label">${t.label}</span>
949
+ <span class="flow-req-count"
950
+ >${t.requests.length}
951
+ req${t.requests.length!==1?"s":""}</span
952
+ >
953
+ <span class="flow-badge-pill ${n.cls}">${n.text}</span>
954
+ <span class="flow-duration"
955
+ >${v(t.totalDurationMs)}</span
956
+ >
957
+ </div>
958
+ </div>
959
+ <div class="flow-expand ${r?"open":""}">
960
+ ${r?this.renderFlowDetail(t):d}
961
+ </div>
962
+ `}renderFlowDetail(t){let s=this.viewMode==="simple"?"Insights":"Details";return a`
963
+ <div class="flow-detail-tabs">
964
+ <button
965
+ class="flow-tab ${this.flowDetailTab==="insights"?"active":""}"
966
+ @click=${r=>this.switchTab("insights",t,r)}
967
+ >
968
+ ${s}
969
+ </button>
970
+ <button
971
+ class="flow-tab ${this.flowDetailTab==="timeline"?"active":""}"
972
+ @click=${r=>this.switchTab("timeline",t,r)}
973
+ >
974
+ Timeline
975
+ </button>
976
+ </div>
977
+ ${this.flowDetailTab==="insights"?this.viewMode==="simple"?this.renderFlowInsights(t):this.renderFlowSubReqs(t):this.renderFlowWaterfall(t)}
978
+ `}renderFlowWaterfall(t){if(this.flowTimelineLoading)return a`<div class="wf-loading">Loading timeline...</div>`;let{rows:s,totalMs:r}=ps(t,this.flowTimeline);if(s.length===0)return d;let o=[];for(let n=0;n<=5;n++)o.push(v(r/5*n));return a`
979
+ <div class="flow-waterfall">
980
+ <div class="wf-time-axis">
981
+ ${o.map(n=>a`<span>${n}</span>`)}
982
+ </div>
983
+ <div class="wf-rows">
984
+ ${s.map(n=>this.renderWaterfallGroup(n))}
985
+ </div>
986
+ </div>
987
+ `}renderWaterfallGroup(t){return a`
988
+ <div class="wf-request-group">
989
+ <div class="wf-req-row" title="${t.tooltip}">
990
+ <div class="wf-req-label">${t.label}</div>
991
+ <div class="wf-bar-track">
992
+ <div
993
+ class="wf-bar"
994
+ style="left:${t.leftPct}%;width:${t.widthPct}%;background:${t.color}"
995
+ ></div>
996
+ </div>
997
+ <div class="wf-req-dur">${t.durLabel}</div>
998
+ </div>
999
+ ${t.subEvents.length>0?t.subEvents.map(s=>a`
1000
+ <div class="wf-sub-row" title="${s.tooltip}">
1001
+ <div class="wf-sub-label">
1002
+ <span
1003
+ class="wf-sub-dot"
1004
+ style="background:${_e(s.type)}"
1005
+ ></span>
1006
+ ${s.label}
1007
+ </div>
1008
+ <div class="wf-bar-track">
1009
+ <div
1010
+ class="wf-bar wf-sub-bar-sized"
1011
+ style="left:${t.leftPct+s.leftPct/100*t.widthPct}%;width:${s.widthPct/100*t.widthPct}%;background:${_e(s.type)}"
1012
+ ></div>
1013
+ </div>
1014
+ <div class="wf-sub-dur">${s.durLabel}</div>
1015
+ </div>
1016
+ `):d}
1017
+ </div>
1018
+ `}renderFlowInsights(t){let s=us(t),r=s.errors.length>0||s.duplicates.length>0||s.warnings.length>0||!!s.tip;return a`
1019
+ <div>
1020
+ <div class="flow-traffic">
1021
+ ${t.requests.map(o=>this.renderTrafficCard(o))}
1022
+ </div>
1023
+ ${r?a`
1024
+ <div class="flow-divider"></div>
1025
+ <div class="flow-insights">
1026
+ ${s.errors.map(o=>a`<div class="insight-line insight-error">
1027
+ ✗ ${o}
1028
+ </div>`)}
1029
+ ${s.duplicates.map(o=>a`<div class="insight-line insight-warn">
1030
+ ⚠ ${o.name} — loaded ${o.count}x (wasting
1031
+ ~${v(o.wastedMs)})
1032
+ </div>`)}
1033
+ ${s.warnings.map(o=>a`<div class="insight-line insight-warn">⚠ ${o}</div>`)}
1034
+ ${s.tip?a`<div class="insight-line insight-tip">
1035
+ Tip: ${s.tip}
1036
+ </div>`:d}
1037
+ </div>
1038
+ `:d}
1039
+ </div>
1040
+ `}renderTrafficCard(t){if(yt[t.category||""])return d;let s=st(t.statusCode),r=v(t.pollingDurationMs||t.durationMs),o=!t.isDuplicate&&t.category!==Ht&&t.category!==J||t.requestBody&&t.method!=="GET"||!!t.responseBody;return a`
1041
+ <div
1042
+ class="traffic-card ${t.isStrictModeDupe?"strict-mode-dupe":""}"
1043
+ >
1044
+ <div class="traffic-card-header ${o?"has-details":""}">
1045
+ <bk-method-badge .method=${t.method}></bk-method-badge>
1046
+ <span class="traffic-card-path ${t.isDuplicate?"is-dup":""}"
1047
+ >${t.label}</span
1048
+ >
1049
+ <span class="status-pill ${s}">${t.statusCode}</span>
1050
+ <span class="traffic-card-dur">${r}</span>
1051
+ ${t.isDuplicate?a`<span class="traffic-card-dup">duplicate</span>`:a`<span class="traffic-card-size"
1052
+ >${U(t.responseSize)}</span
1053
+ >`}
1054
+ </div>
1055
+ ${t.isStrictModeDupe?a`<div class="strict-mode-banner">
1056
+ React Strict Mode duplicate — does not happen in production
1057
+ </div>`:d}
1058
+ ${!t.isDuplicate&&t.category!==Ht&&t.category!==J?a`<div
1059
+ class="request-timeline tl-hidden"
1060
+ data-request-id=${t.id}
1061
+ data-request-started=${String(t.startedAt)}
1062
+ ></div>`:d}
1063
+ ${t.requestBody&&t.method!=="GET"?this.renderBodyToggle("out","Request Body",t.requestBody):d}
1064
+ ${t.responseBody?this.renderBodyToggle("in","Response Body",t.responseBody):d}
1065
+ </div>
1066
+ `}renderBodyToggle(t,s,r){let o=t==="out"?"\u2192":"\u2190";return a`
1067
+ <div class="traffic-body">
1068
+ <button class="traffic-body-toggle" @click=${this.toggleBodyBlock}>
1069
+ <span class="chevron">▸</span
1070
+ ><span class="arrow-${t}">${o}</span> ${s}
1071
+ </button>
1072
+ <pre .innerHTML=${Q(r)}></pre>
1073
+ </div>
1074
+ `}renderFlowSubReqs(t){return a`<div class="flow-subreqs">
1075
+ ${t.requests.map((s,r)=>this.renderSubReqRow(s,r))}
1076
+ </div>`}renderSubReqRow(t,s){let r=this.expandedSubReqIdx===s,o=st(t.statusCode),n=t.pollingDurationMs?v(t.pollingDurationMs):v(t.durationMs);return a`
1077
+ <div
1078
+ class="flow-subreq ${r?"expanded":""}"
1079
+ @click=${l=>this.toggleSubReq(s,l)}
1080
+ >
1081
+ <bk-method-badge .method=${t.method}></bk-method-badge>
1082
+ <span class="subreq-label ${t.isDuplicate?"is-dup":""}"
1083
+ >${t.path||t.url}</span
1084
+ >
1085
+ ${t.isDuplicate?a`<span class="subreq-dup-tag">duplicate</span>`:d}
1086
+ <span class="status-pill ${o}">${t.statusCode}</span>
1087
+ <span class="subreq-dur">${n}</span>
1088
+ </div>
1089
+ <div class="flow-subreq-detail ${r?"open":""}">
1090
+ ${r?this.renderSubReqDetail(t):d}
1091
+ </div>
1092
+ `}renderSubReqDetail(t){let s=st(t.statusCode);return a`
1093
+ <div class="detail-meta">
1094
+ <span
1095
+ ><bk-method-badge .method=${t.method}></bk-method-badge> ${P(t.url)}</span
1096
+ >
1097
+ <span
1098
+ ><span class="status-pill ${s}">${t.statusCode}</span></span
1099
+ >
1100
+ <span>${t.durationMs}ms</span>
1101
+ ${t.responseSize?a`<span>${U(t.responseSize)}</span>`:d}
1102
+ </div>
1103
+ <div
1104
+ class="request-timeline tl-hidden"
1105
+ data-request-id=${t.id}
1106
+ data-request-started=${String(t.startedAt)}
1107
+ ></div>
1108
+ <div class="detail-grid">
1109
+ <div class="detail-section">
1110
+ <h4>Request Headers</h4>
1111
+ <pre .innerHTML=${rt(t.headers)}></pre>
1112
+ </div>
1113
+ <div class="detail-section">
1114
+ <h4>Response Headers</h4>
1115
+ <pre .innerHTML=${rt(t.responseHeaders)}></pre>
1116
+ </div>
1117
+ <div class="detail-section">
1118
+ <h4>Request Body</h4>
1119
+ <pre .innerHTML=${Q(t.requestBody)}></pre>
1120
+ </div>
1121
+ <div class="detail-section">
1122
+ <h4>Response Body</h4>
1123
+ <pre .innerHTML=${Q(t.responseBody)}></pre>
1124
+ </div>
1125
+ </div>
1126
+ <div class="detail-actions">
1127
+ <button
1128
+ class="btn btn-curl"
1129
+ @click=${r=>{r.stopPropagation(),ct(t);}}
1130
+ >
1131
+ Copy cURL
1132
+ </button>
1133
+ </div>
1134
+ `}};u([R({context:x})],M.prototype,"store",2),u([_()],M.prototype,"expandedFlowIdx",2),u([_()],M.prototype,"expandedSubReqIdx",2),u([_()],M.prototype,"flowDetailTab",2),u([_()],M.prototype,"flowTimeline",2),u([_()],M.prototype,"flowTimelineLoading",2),M=u([g("bk-flows-view")],M);var Ct=class extends f{createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}render(){let e=(this.store.state.issues||[]).slice(),t=e.filter(l=>l.state==="open"||l.state==="fixing"||l.state==="regressed"),s=e.filter(l=>l.state==="resolved");if(t.length===0&&s.length===0)return this.store.state.requests.length>0||this.store.state.logs.length>0||this.store.state.queries.length>0?a`
1135
+ <div class="sec-clear">
1136
+ <span class="sec-clear-icon">\u2713</span>
1137
+ <div class="sec-clear-text">
1138
+ <div class="sec-clear-title">All clear</div>
1139
+ <div class="sec-clear-sub">No security or quality issues detected this session</div>
1140
+ </div>
1141
+ </div>
1142
+ `:a`<bk-empty-state title="Waiting for requests..." subtitle="Start using your app to see security findings here"></bk-empty-state>`;let r=0,o=0,n=0;for(let l of t){let c=l.issue.severity;c==="critical"?r++:c==="info"?n++:o++;}return a`
1143
+ <div id="security-content">
1144
+ ${this.renderSummary(t.length,s.length,r,o,n)}
1145
+ ${t.length===0&&s.length>0?a`
1146
+ <div class="sec-clear">
1147
+ <span class="sec-clear-icon">\u2713</span>
1148
+ <div class="sec-clear-text">
1149
+ <div class="sec-clear-title">All issues resolved</div>
1150
+ <div class="sec-clear-sub">${s.length} finding${s.length!==1?"s were":" was"} detected and fixed</div>
1151
+ </div>
1152
+ </div>
1153
+ `:d}
1154
+ ${t.length>0?this.renderOpenGroups(t):d}
1155
+ ${s.length>0?this.renderResolved(s):d}
1156
+ </div>
1157
+ `}renderSummary(e,t,s,r,o){return a`
1158
+ <div class="sec-summary">
1159
+ <div class="sec-summary-left">
1160
+ <span class="sec-summary-count">${e}</span>
1161
+ <span class="sec-summary-label">open issue${e!==1?"s":""}</span>
1162
+ ${t>0?a`<span class="sec-resolved-badge">${t} resolved</span>`:d}
562
1163
  </div>
563
- <button class="btn btn-danger" id="clear-btn">Clear</button>
1164
+ <div class="sec-summary-right">
1165
+ ${s>0?a`<span class="sec-badge critical">${s} critical</span>`:d}
1166
+ ${r>0?a`<span class="sec-badge warning">${r} warning</span>`:d}
1167
+ ${o>0?a`<span class="sec-badge info">${o} info</span>`:d}
1168
+ </div>
1169
+ </div>
1170
+ `}renderOpenGroups(e){let t={},s=[];for(let r of e){let o=r.issue,n=o.rule||o.type;t[n]||(t[n]={rule:n,title:o.title,severity:o.severity,hint:o.hint,items:[]},s.push(n)),t[n].items.push(r);}return s.sort((r,o)=>{let n=q[t[r].severity]?.sort??2,l=q[t[o].severity]?.sort??2;return n!==l?n-l:t[o].items.length-t[r].items.length}),a`${s.map(r=>this.renderGroup(t[r]))}`}renderGroup(e){let t=q[e.severity]||q.info;return a`
1171
+ <div class="sec-group">
1172
+ <div class="sec-group-header">
1173
+ <span class="sec-group-icon ${t.cls}">${t.icon}</span>
1174
+ <span class="sec-group-title">${e.title}</span>
1175
+ <span class="sec-group-count">${e.items.length}</span>
1176
+ </div>
1177
+ ${e.hint?a`<div class="sec-hint">${e.hint}</div>`:d}
1178
+ <div class="sec-items">${e.items.map(s=>this.renderIssueItem(s))}</div>
564
1179
  </div>
565
- </div>
566
- <div class="main-content">
567
- <div id="overview-container">
568
- <div class="ov-container" id="overview-content"></div>
1180
+ `}renderIssueItem(e){let t=e.issue;return a`
1181
+ <div class="sec-item">
1182
+ <div class="sec-item-desc">${t.desc}</div>
1183
+ ${e.occurrences>1?a`<span class="sec-item-count">${e.occurrences}x</span>`:d}
1184
+ ${e.state==="fixing"&&e.aiStatus==="fixed"?a`<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>`:e.aiStatus==="wont_fix"?a`<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>`:e.state==="regressed"?a`<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>`:d}
1185
+ ${e.aiNotes?a`<div class="sec-ai-notes">${e.aiNotes}</div>`:d}
569
1186
  </div>
570
- <div class="view-flows" id="flow-container" style="display:none">
571
- <div class="col-header" id="flow-col-header">
572
- <span style="width:8px"></span>
573
- <span style="flex:1">Action</span>
574
- <span style="width:60px;text-align:right">Reqs</span>
575
- <span style="width:120px;text-align:right">Status</span>
576
- <span style="width:70px;text-align:right">Time</span>
1187
+ `}renderResolved(e){return a`
1188
+ <div class="sec-resolved-title">
1189
+ <span class="sec-resolved-check">\u2713</span> Resolved
1190
+ <span class="sec-resolved-count">${e.length}</span>
1191
+ </div>
1192
+ <div class="sec-group sec-group-resolved">
1193
+ <div class="sec-items">
1194
+ ${e.map(t=>a`
1195
+ <div class="sec-item sec-item-resolved">
1196
+ <span class="sec-resolved-item-icon">\u2713</span>
1197
+ <div class="sec-item-desc">${t.issue.title} \u2014 ${t.issue.endpoint||"global"}</div>
1198
+ ${t.aiStatus==="fixed"?a`<span class="sec-ai-badge sec-ai-verified">Verified fix</span>`:d}
1199
+ ${t.aiNotes?a`<div class="sec-ai-notes">${t.aiNotes}</div>`:d}
1200
+ </div>
1201
+ `)}
577
1202
  </div>
578
- <div id="flow-list">
579
- <div class="empty" id="empty-flows">
580
- <span class="empty-title">Waiting for requests...</span>
581
- <span class="empty-sub">Use your app and actions will appear here</span>
1203
+ </div>
1204
+ `}};u([R({context:x})],Ct.prototype,"store",2),Ct=u([g("bk-security-view")],Ct);var pt=class extends f{constructor(){super(...arguments);this.expandedCardIdx=-1;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}toggleCard(t){this.expandedCardIdx=this.expandedCardIdx===t?-1:t;}render(){let t=this.store.state,s=t.requests.filter(E=>!E.isStatic&&!E.isHealthCheck&&(!E.path||E.path.indexOf(k)!==0));if(!(s.length>0||t.queries.length>0||t.errors.length>0))return a`<bk-empty-state
1205
+ title="Waiting for requests..."
1206
+ subtitle="Start using your app to see insights here"
1207
+ ></bk-empty-state>`;let o=s.filter(E=>E.statusCode>=400).length,n=new Set(["data-fetch","api-call","server-action","page-load"]),l=s.filter(E=>E.category&&n.has(E.category)),c=l.length>0?l:s,p=c.length>0?Math.round(c.reduce((E,It)=>E+It.durationMs,0)/c.length):0,h=t.issues||[],m=h.filter(E=>E.state==="open"||E.state==="regressed"),b=h.filter(E=>E.state==="fixing"),$=h.filter(E=>E.state==="resolved");return a`
1208
+ <div class="ov-container" id="overview-content">
1209
+ ${this.renderSummary(s.length,t.flows.length,p,t.queries.length,o,t.fetches.length)}
1210
+ ${m.length===0&&b.length===0&&$.length===0?a`<div class="ov-clear">
1211
+ <span class="ov-clear-icon">\u2713</span>All clear \u2014 no issues detected
1212
+ </div>`:d}
1213
+ ${m.length===0&&$.length>0?a`<div class="ov-clear">
1214
+ <span class="ov-clear-icon">\u2713</span>All issues resolved \u2014
1215
+ ${$.length} finding${$.length!==1?"s were":" was"} detected and
1216
+ fixed
1217
+ </div>`:d}
1218
+ ${m.length>0?this.renderOpenIssues(m):d}
1219
+ ${b.length>0?this.renderVerifying(b):d}
1220
+ ${$.length>0?this.renderResolvedIssues($):d}
1221
+ </div>
1222
+ `}renderSummary(t,s,r,o,n,l){return a`
1223
+ <div class="ov-summary">
1224
+ <div class="ov-stat"><span class="ov-stat-value">${t}</span><span class="ov-stat-label">Requests</span></div>
1225
+ <div class="ov-stat"><span class="ov-stat-value">${s}</span><span class="ov-stat-label">Actions</span></div>
1226
+ <div class="ov-stat"><span class="ov-stat-value">${v(r)}</span><span class="ov-stat-label">Avg Response</span></div>
1227
+ <div class="ov-stat"><span class="ov-stat-value">${o}</span><span class="ov-stat-label">Queries</span></div>
1228
+ <div class="ov-stat"><span class="ov-stat-value" style="color:${n>0?"var(--red)":"var(--green)"}">${n}</span><span class="ov-stat-label">Errors</span></div>
1229
+ <div class="ov-stat"><span class="ov-stat-value">${l}</span><span class="ov-stat-label">Fetches</span></div>
1230
+ </div>
1231
+ `}renderOpenIssues(t){return a`
1232
+ <div class="ov-section-title">Issues Found <span class="ov-issue-count">${t.length}</span></div>
1233
+ <div class="ov-cards">${t.map((s,r)=>this.renderIssueCard(s,r))}</div>
1234
+ `}renderIssueCard(t,s){let r=t.issue,o=q[r.severity]||q.info,n=this.expandedCardIdx===s,l=t.aiStatus==="wont_fix"?a`<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>`:t.state==="regressed"?a`<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>`:d,c=t.cleanHitsSinceLastSeen>0?a`<div class="ov-card-resolving">Resolving\u2026 ${t.cleanHitsSinceLastSeen}/${5} clean requests</div>`:d;return a`
1235
+ <div class="ov-card ${n?"expanded":""}" @click=${()=>this.toggleCard(s)}>
1236
+ <span class="ov-card-icon ${o.cls}">${o.icon}</span>
1237
+ <div class="ov-card-body">
1238
+ <div class="ov-card-title">${r.title}${l}</div>
1239
+ <div class="ov-card-desc">${r.desc}</div>
1240
+ ${r.detail?a`<div class="ov-card-detail">${r.detail}</div>`:d}
1241
+ ${c}
1242
+ ${n&&r.hint?a`<div class="ov-card-hint">${r.hint}</div>`:d}
1243
+ </div>
1244
+ ${r.hint?a`<span class="ov-card-arrow">${n?"\u2193":"\u2192"}</span>`:d}
1245
+ </div>
1246
+ `}renderVerifying(t){return a`
1247
+ <div class="ov-section-title ov-resolved-title">
1248
+ <span style="color:var(--yellow,#f5a623)">\u29d7</span> Awaiting Verification
1249
+ <span class="ov-issue-count">${t.length}</span>
1250
+ </div>
1251
+ <div class="ov-cards">
1252
+ ${t.map(s=>{let r=s.issue,o=s.cleanHitsSinceLastSeen>0?a`<div class="ov-card-resolving">Verifying\u2026 ${s.cleanHitsSinceLastSeen}/${5} clean requests</div>`:d;return a`
1253
+ <div class="ov-card ov-card-resolved">
1254
+ <span class="ov-card-icon resolved">\u29d7</span>
1255
+ <div class="ov-card-body">
1256
+ <div class="ov-card-title" style="color:var(--text-muted)">
1257
+ ${r.title}
1258
+ <span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>
1259
+ </div>
1260
+ <div class="ov-card-desc">${r.desc}</div>
1261
+ ${o}
1262
+ </div>
1263
+ </div>
1264
+ `})}
1265
+ </div>
1266
+ `}renderResolvedIssues(t){return a`
1267
+ <div class="ov-section-title ov-resolved-title">
1268
+ <span style="color:var(--green)">\u2713</span> Resolved
1269
+ <span class="ov-issue-count">${t.length}</span>
1270
+ </div>
1271
+ <div class="ov-cards">
1272
+ ${t.map(s=>a`
1273
+ <div class="ov-card ov-card-resolved">
1274
+ <span class="ov-card-icon resolved">\u2713</span>
1275
+ <div class="ov-card-body">
1276
+ <div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">${s.issue.title}</div>
1277
+ <div class="ov-card-desc">${s.issue.desc}</div>
1278
+ </div>
1279
+ </div>
1280
+ `)}
1281
+ </div>
1282
+ `}};u([R({context:x})],pt.prototype,"store",2),u([_()],pt.prototype,"expandedCardIdx",2),pt=u([g("bk-overview-view")],pt);function hs(i){return i<1?"<1ms":i<1e3?Math.round(i)+"ms":(i/1e3).toFixed(1)+"s"}function ir(i){return i<Z?Tt.green:i<tt?Tt.amber:Tt.red}function nr(i){return i.statusCode>=400?Tt.red:ir(i.durationMs)}function ms(i){return [parseInt(i.slice(1,3),16),parseInt(i.slice(3,5),16),parseInt(i.slice(5,7),16)]}function ar(i){let e=i.getContext("2d");if(!e)return null;let t=window.devicePixelRatio||1,s=i.clientWidth,r=i.clientHeight;return i.width=s*t,i.height=r*t,e.scale(t,t),{ctx:e,w:s,h:r}}function lr(i,e,t,s,r){let[o,n,l]=ms(r);i.beginPath(),i.arc(e,t,s+2,0,Math.PI*2),i.fillStyle=`rgba(${o},${n},${l},0.25)`,i.fill(),i.beginPath(),i.arc(e,t,s,0,Math.PI*2),i.fillStyle=r,i.fill();}function cr(i,e,t,s,r,o){let[n,l,c]=ms(r);i.strokeStyle=`rgba(${n},${l},${c},0.3)`,i.lineWidth=o+2,i.beginPath(),i.moveTo(e-s,t-s),i.lineTo(e+s,t+s),i.moveTo(e+s,t-s),i.lineTo(e-s,t+s),i.stroke(),i.strokeStyle=r,i.lineWidth=o,i.beginPath(),i.moveTo(e-s,t-s),i.lineTo(e+s,t+s),i.moveTo(e+s,t-s),i.lineTo(e-s,t+s),i.stroke();}function vs(i,e){let t=[],s=ar(i);if(!s||e.length===0)return t;let{ctx:r,w:o,h:n}=s,l=Ze,c=o-l.left-l.right,p=n-l.top-l.bottom,h=0,m=e[0].timestamp,b=e[0].timestamp;for(let S of e)S.durationMs>h&&(h=S.durationMs),S.timestamp<m&&(m=S.timestamp),S.timestamp>b&&(b=S.timestamp);h=Math.max(h,10),h=Math.ceil(h*1.15/10)*10;let $=b-m||1;r.strokeStyle=ze,r.lineWidth=1;let E=4;for(let S=0;S<=E;S++){let A=l.top+p-S/E*p;r.beginPath(),r.moveTo(l.left,A),r.lineTo(l.left+c,A),r.stroke(),r.fillStyle=ve,r.font=Je,r.textAlign="right",r.fillText(hs(Math.round(S/E*h)),l.left-8,A+3);}for(let S of [{ms:Z},{ms:tt}]){if(S.ms>=h)continue;let A=l.top+p-S.ms/h*p;r.beginPath(),r.setLineDash([4,4]),r.strokeStyle="rgba(113,113,122,0.3)",r.lineWidth=1,r.moveTo(l.left,A),r.lineTo(l.left+c,A),r.stroke(),r.setLineDash([]),r.fillStyle="rgba(113,113,122,0.5)",r.font=fe,r.textAlign="left",r.fillText(hs(S.ms),l.left+c+2,A+3);}for(let S=0;S<e.length;S++){let A=e[S],ut=e.length===1?l.left+c/2:l.left+(A.timestamp-m)/$*c,jt=l.top+p-A.durationMs/h*p,ye=nr(A);t.push({x:ut,y:jt,idx:S,r:A}),A.statusCode>=400?cr(r,ut,jt,4,ye,2):lr(r,ut,jt,4,ye);}r.fillStyle=ve,r.font=fe,r.textAlign="center";let It=[m,m+$/2,b];for(let S=0;S<It.length;S++){let A=l.left+S/2*c,ut=new Date(It[S]);r.fillText(ut.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),A,l.top+p+14);}return t}var vr={max:1/0,label:"Pending",color:"var(--text-muted)",bg:"var(--bg-muted)",border:"var(--border)"};function fs(i,e,t){return t>=20?i:e}function $e(i,e){if(!e||e<=0)return vr;let t=i/e;return t<.7?et[0]:t<1.2?et[1]:t<2?et[2]:t<3?et[3]:et[4]}var L=class extends f{constructor(){super(...arguments);this.selectedEndpoint=H;this.graphData=[];this.loadError=false;this.queryBreakdown=[];this.queryBreakdownLoading=false;this.scatterDots=[];}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate()),this.loadMetrics();}async loadMetrics(){try{let s=await(await fetch(y.metricsLive)).json();this.graphData=s.endpoints||[],this.loadError=!1,(!this.selectedEndpoint||this.selectedEndpoint===H)&&(this.selectedEndpoint=H);}catch{this.loadError=true;}}healthGradeForEndpoint(t){let s=fs(t.summary.p95Ms,t.summary.medianMs,t.summary.totalRequests);return $e(s,t.baselineP95Ms)}healthGradeForDuration(t,s){return $e(t,s)}getCallers(t){let s=this.store.state.flows,r=new Map;for(let o of s)for(let n of o.requests)if(`${n.method} ${n.path}`===t||this.normalizeEndpoint(n)===t){let c=r.get(o.label);c?(c.count++,c.totalMs+=n.durationMs):r.set(o.label,{count:1,totalMs:n.durationMs});}return [...r.entries()].map(([o,n])=>({label:o,count:n.count,avgMs:Math.round(n.totalMs/n.count)})).sort((o,n)=>n.count-o.count)}normalizeEndpoint(t){let s=t.path.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,"/:id").replace(/\/\d+/g,"/:id");return `${t.method} ${s}`}async loadQueryBreakdown(t){if(this.queryBreakdownLoading)return;let r=this.store.state.requests.filter(o=>`${o.method} ${o.path}`===t||this.normalizeEndpoint(o)===t).slice(-20).map(o=>o.id).filter(Boolean);if(r.length===0){this.queryBreakdown=[];return}this.queryBreakdownLoading=true;try{let o=await fetch(`${y.activity}?requestIds=${r.join(",")}`);if(!o.ok){this.queryBreakdownLoading=!1;return}let n=await o.json(),l=new Map;for(let c of Object.values(n.activities))for(let p of c.timeline){if(p.type!=="query")continue;let h=p.data,m=(h.normalizedOp||h.operation||"QUERY").toUpperCase(),b=h.table||h.model||"",$=`${m} ${b}`.trim(),E=l.get($);E?(E.totalMs+=h.durationMs,E.count++):l.set($,{label:$,totalMs:h.durationMs,count:1});}this.queryBreakdown=[...l.values()].map(c=>({...c,avgMs:Math.round(c.totalMs/c.count)})).sort((c,p)=>p.totalMs-c.totalMs);}catch{}this.queryBreakdownLoading=false;}renderScatterChart(t,s){this.scatterDots=vs(t,s),t.style.cursor="pointer",t.onclick=r=>{let o=t.getBoundingClientRect(),n=r.clientX-o.left,l=r.clientY-o.top,c=null,p=1/0;for(let h of this.scatterDots){let m=Math.sqrt((h.x-n)**2+(h.y-l)**2);m<p&&(p=m,c=h);}c&&p<16&&this.highlightRow(c.idx);};}highlightRow(t){let s=this.querySelector(".perf-hist-row-hl");s&&s.classList.remove("perf-hist-row-hl");let r=this.querySelector(`[data-req-idx="${t}"]`);r&&(r.classList.add("perf-hist-row-hl"),r.scrollIntoView({behavior:"smooth",block:"center"}));}updated(){if(this.selectedEndpoint===H)return;let t=this.querySelector("#perf-detail-canvas");if(t){let s=this.graphData.find(r=>r.endpoint===this.selectedEndpoint);s&&this.renderScatterChart(t,s.requests);}}render(){return !this.graphData||this.graphData.length===0?a`<bk-empty-state title="No performance data yet" subtitle="Hit some endpoints and data will appear here"></bk-empty-state>`:a`
1283
+ <div id="graph-content">
1284
+ ${this.renderSelector()}
1285
+ ${this.selectedEndpoint===H?this.renderOverview():this.renderDetail()}
1286
+ </div>
1287
+ `}renderSelector(){return a`
1288
+ <div class="perf-selector">
1289
+ <button class="perf-selector-btn ${this.selectedEndpoint===H?"active":""}"
1290
+ @click=${()=>{this.selectedEndpoint=H;}}>Overview</button>
1291
+ ${this.graphData.map((t,s)=>a`
1292
+ <button class="perf-selector-btn ${t.endpoint===this.selectedEndpoint?"active":""}"
1293
+ @click=${()=>{this.selectedEndpoint=t.endpoint,this.queryBreakdown=[],this.loadQueryBreakdown(t.endpoint);}}>
1294
+ <span class="perf-dot" style="background:${me[s%me.length]}"></span>${t.endpoint}
1295
+ </button>
1296
+ `)}
1297
+ </div>
1298
+ `}renderOverview(){let t=this.graphData.filter(c=>c.requests.length>0);if(t.length===0)return d;let s=t.reduce((c,p)=>c+p.summary.totalRequests,0),r=s>0?Math.round(t.reduce((c,p)=>c+p.summary.p95Ms*p.summary.totalRequests,0)/s):0,o=t.reduce((c,p)=>c+Math.round(p.summary.errorRate*p.summary.totalRequests),0),n=s>0?o/s:0,l=t[0];return a`
1299
+ <div class="perf-overview">
1300
+ <div class="perf-summary-row">
1301
+ <div class="perf-summary-card">
1302
+ <span class="perf-summary-label">Total Requests</span>
1303
+ <span class="perf-summary-value">${s}</span>
1304
+ </div>
1305
+ <div class="perf-summary-card">
1306
+ <span class="perf-summary-label">Avg P95</span>
1307
+ <span class="perf-summary-value" style="color:${this.healthGradeForDuration(r).color}">${v(r)}</span>
1308
+ </div>
1309
+ <div class="perf-summary-card">
1310
+ <span class="perf-summary-label">Error Rate</span>
1311
+ <span class="perf-summary-value" style="color:${o>0?"var(--red)":"var(--green)"}">${Math.round(n*100)}%</span>
1312
+ </div>
1313
+ <div class="perf-summary-card">
1314
+ <span class="perf-summary-label">Slowest</span>
1315
+ <span class="perf-summary-value perf-summary-value-sm">${l?.endpoint??"-"}</span>
582
1316
  </div>
583
1317
  </div>
1318
+
1319
+ <table class="perf-table perf-heatmap">
1320
+ <thead>
1321
+ <tr>
1322
+ <th>Endpoint</th>
1323
+ <th class="perf-th-right">Calls</th>
1324
+ <th class="perf-th-center">P95</th>
1325
+ <th class="perf-th-center">Errors</th>
1326
+ <th class="perf-th-center">Q/Req</th>
1327
+ <th>Time Split</th>
1328
+ </tr>
1329
+ </thead>
1330
+ <tbody>
1331
+ ${t.map(c=>this.renderHeatmapRow(c))}
1332
+ </tbody>
1333
+ </table>
584
1334
  </div>
585
- <div class="view-requests" id="request-container">
586
- <div class="col-header">
587
- <span style="width:60px">Method</span>
588
- <span style="flex:1">URL</span>
589
- <span style="width:36px;text-align:right">Status</span>
590
- <span style="width:70px;text-align:right">Time</span>
591
- <span style="width:60px;text-align:right">Size</span>
1335
+ `}renderHeatmapRow(t){let s=t.summary,r=this.healthGradeForEndpoint(t),o=Math.round(s.errorRate*s.totalRequests),n=(s.avgQueryTimeMs||0)+(s.avgFetchTimeMs||0)+(s.avgAppTimeMs||0),l=0,c=0,p=100;return n>0&&(l=Math.round((s.avgQueryTimeMs||0)/n*100),c=Math.round((s.avgFetchTimeMs||0)/n*100),p=Math.max(0,100-l-c)),a`
1336
+ <tr class="perf-table-row" @click=${()=>{this.selectedEndpoint=t.endpoint,this.queryBreakdown=[],this.loadQueryBreakdown(t.endpoint);}}>
1337
+ <td class="perf-td-name">${t.endpoint}</td>
1338
+ <td class="perf-td-right">${s.totalRequests}</td>
1339
+ <td class="perf-td-center">
1340
+ <span class="perf-hm-p95" style="color:${r.color};background:${r.bg};border-color:${r.border}">${v(s.p95Ms)}</span>
1341
+ </td>
1342
+ <td class="perf-td-center" style="color:${o>0?"var(--red)":"var(--text-muted)"}">${o>0?o:"-"}</td>
1343
+ <td class="perf-td-center" style="color:${s.avgQueryCount>5?"var(--amber)":"var(--text-muted)"}">${s.avgQueryCount}</td>
1344
+ <td>
1345
+ <span class="perf-hm-split-bar">
1346
+ ${l>0?a`<span class="perf-breakdown-seg perf-breakdown-db" style="width:${l}%"></span>`:d}
1347
+ ${c>0?a`<span class="perf-breakdown-seg perf-breakdown-fetch" style="width:${c}%"></span>`:d}
1348
+ ${p>0?a`<span class="perf-breakdown-seg perf-breakdown-app" style="width:${p}%"></span>`:d}
1349
+ </span>
1350
+ </td>
1351
+ </tr>
1352
+ `}renderDetail(){let t=this.graphData.find(n=>n.endpoint===this.selectedEndpoint);if(!t?.requests?.length)return a`<bk-empty-state subtitle="No data for this endpoint"></bk-empty-state>`;let s=t.summary,r=this.healthGradeForEndpoint(t),o=Math.round(s.errorRate*s.totalRequests);return a`
1353
+ ${this.renderDetailHeader(t,r)}
1354
+ ${this.renderDetailMetrics(s,r,o)}
1355
+ ${this.renderDetailBreakdown(s)}
1356
+ ${this.renderCallers(t.endpoint)}
1357
+ ${this.renderQueryBreakdown()}
1358
+ ${this.renderTrends(t)}
1359
+ ${this.renderDetailChart()}
1360
+ ${this.renderDetailHistory(t)}
1361
+ `}renderDetailHeader(t,s){return a`
1362
+ <div class="perf-detail-header">
1363
+ <div class="perf-detail-title">
1364
+ <span class="perf-badge perf-badge-lg" style="color:${s.color};background:${s.bg};border-color:${s.border}">${s.label}</span>
1365
+ <span>${t.endpoint}</span>
1366
+ ${t.baselineP95Ms?a`<span class="perf-baseline-hint">Baseline: ${v(t.baselineP95Ms)}</span>`:d}
592
1367
  </div>
593
- <div id="request-list"></div>
594
1368
  </div>
595
- <div class="view-telemetry" id="fetch-container" style="display:none">
596
- <div class="fetch-analysis" id="fetch-analysis"></div>
1369
+ `}renderDetailMetrics(t,s,r){return a`
1370
+ <div class="perf-metric-row">
1371
+ <div class="perf-metric-card">
1372
+ <span class="perf-metric-label">P95</span>
1373
+ <span class="perf-metric-value" style="color:${s.color}">${v(t.p95Ms)}</span>
1374
+ </div>
1375
+ <div class="perf-metric-card">
1376
+ <span class="perf-metric-label">Errors</span>
1377
+ <span class="perf-metric-value" style="color:${r>0?"var(--red)":"var(--green)"}">
1378
+ ${r>0?r+" ("+Math.round(t.errorRate*100)+"%)":"0"}
1379
+ </span>
1380
+ </div>
1381
+ <div class="perf-metric-card">
1382
+ <span class="perf-metric-label">Queries/req</span>
1383
+ <span class="perf-metric-value" style="color:${t.avgQueryCount>5?"var(--amber)":"var(--text)"}">${t.avgQueryCount}</span>
1384
+ </div>
597
1385
  </div>
598
- <div class="view-telemetry" id="query-container" style="display:none">
599
- <div class="col-header">
600
- <span style="width:70px;border-right:1px solid var(--border);padding-right:16px">Operation</span>
601
- <span style="width:170px;border-right:1px solid var(--border);padding-right:16px">Table</span>
602
- <span style="flex:1;border-right:1px solid var(--border);padding-right:16px">Query</span>
603
- <span style="width:60px;text-align:right">Time</span>
1386
+ `}renderDetailBreakdown(t){let s=(t.avgQueryTimeMs||0)+(t.avgFetchTimeMs||0)+(t.avgAppTimeMs||0);if(s<=0)return d;let r=Math.round((t.avgQueryTimeMs||0)/s*100),o=Math.round((t.avgFetchTimeMs||0)/s*100),n=Math.max(0,100-r-o);return a`
1387
+ <div class="perf-breakdown">
1388
+ <div class="perf-section-title">Time Breakdown</div>
1389
+ <div class="perf-breakdown-bar">
1390
+ ${r>0?a`<div class="perf-breakdown-seg perf-breakdown-db" style="width:${r}%"></div>`:d}
1391
+ ${o>0?a`<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:${o}%"></div>`:d}
1392
+ ${n>0?a`<div class="perf-breakdown-seg perf-breakdown-app" style="width:${n}%"></div>`:d}
1393
+ </div>
1394
+ <div class="perf-breakdown-legend">
1395
+ <span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-db"></span>DB ${v(t.avgQueryTimeMs||0)} (${r}%)</span>
1396
+ <span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>Fetch ${v(t.avgFetchTimeMs||0)} (${o}%)</span>
1397
+ <span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-app"></span>App ${v(t.avgAppTimeMs||0)} (${n}%)</span>
1398
+ </div>
1399
+ </div>
1400
+ `}renderCallers(t){let s=this.getCallers(t);return s.length===0?d:a`
1401
+ <div class="perf-callers">
1402
+ <div class="perf-section-title">Called By</div>
1403
+ <div class="perf-callers-list">
1404
+ ${s.map(r=>a`
1405
+ <div class="perf-caller-row">
1406
+ <span class="perf-caller-name">${r.label}</span>
1407
+ <span class="perf-caller-count">${r.count} call${r.count!==1?"s":""}</span>
1408
+ <span class="perf-caller-avg">avg ${v(r.avgMs)}</span>
1409
+ </div>
1410
+ `)}
604
1411
  </div>
605
- <div id="query-list"></div>
606
1412
  </div>
607
- <div class="view-telemetry" id="error-container" style="display:none">
608
- <div class="col-header">
609
- <span style="width:180px">Type</span>
610
- <span style="flex:1">Message</span>
611
- <span style="width:130px;text-align:right">Time</span>
1413
+ `}renderQueryBreakdown(){return this.queryBreakdownLoading?a`<div class="perf-queries"><div class="perf-section-title">DB Queries</div><div class="perf-queries-loading">Loading...</div></div>`:this.queryBreakdown.length===0?d:a`
1414
+ <div class="perf-queries">
1415
+ <div class="perf-section-title">DB Queries</div>
1416
+ <div class="perf-queries-list">
1417
+ ${this.queryBreakdown.map(t=>a`
1418
+ <div class="perf-query-row">
1419
+ <span class="perf-query-label">${t.label}</span>
1420
+ <span class="perf-query-avg">avg ${v(t.avgMs)}</span>
1421
+ <span class="perf-query-count">${t.count} call${t.count!==1?"s":""}</span>
1422
+ </div>
1423
+ `)}
612
1424
  </div>
613
- <div id="error-list"></div>
614
1425
  </div>
615
- <div class="view-telemetry" id="log-container" style="display:none">
616
- <div id="log-analysis"></div>
617
- <div class="col-header">
618
- <span style="width:52px">Level</span>
619
- <span style="flex:1">Message</span>
620
- <span style="width:130px;text-align:right">Time</span>
1426
+ `}renderTrends(t){let s=t.sessions;if(!s||s.length===0)return d;let r=s.slice(-10);return a`
1427
+ <div class="perf-trends">
1428
+ <div class="perf-section-title">Session Trend</div>
1429
+ <div class="perf-trends-list">
1430
+ ${r.map((o,n)=>{let l=n>0?r[n-1].p95DurationMs:null,c=l!==null?o.p95DurationMs>l*1.2?"slower":o.p95DurationMs<l*.8?"faster":"":"",p=this.formatTimeAgo(o.startedAt),h=n===r.length-1,m=this.healthGradeForDuration(o.p95DurationMs,t.baselineP95Ms);return a`
1431
+ <div class="perf-trend-row ${h?"perf-trend-current":""}">
1432
+ <span class="perf-trend-time">${h?"Current":p}</span>
1433
+ <span class="perf-trend-p95">
1434
+ <span class="perf-hm-p95" style="color:${m.color};background:${m.bg};border-color:${m.border}">
1435
+ p95: ${v(o.p95DurationMs)}
1436
+ </span>
1437
+ </span>
1438
+ <span class="perf-trend-reqs">${o.requestCount} req${o.requestCount!==1?"s":""}</span>
1439
+ <span class="perf-trend-errs" style="color:${o.errorCount>0?"var(--red)":"var(--text-dim)"}">${o.errorCount} err${o.errorCount!==1?"s":""}</span>
1440
+ ${c?a`<span class="perf-trend-arrow ${c==="slower"?"perf-trend-slower":"perf-trend-faster"}">${c==="slower"?"\u2191 slower":"\u2193 faster"}</span>`:d}
1441
+ </div>
1442
+ `})}
621
1443
  </div>
622
- <div id="log-list"></div>
623
1444
  </div>
624
- <div class="view-telemetry" id="security-container" style="display:none">
625
- <div class="sec-container" id="security-content"></div>
1445
+ `}formatTimeAgo(t){let s=Date.now()-t,r=Math.round(s/6e4);if(r<1)return "just now";if(r<60)return `${r}m ago`;let o=Math.round(r/60);return o<24?`${o}h ago`:`${Math.round(o/24)}d ago`}renderDetailChart(){return a`
1446
+ <div class="perf-chart-wrap">
1447
+ <div class="perf-section-title">Response Time</div>
1448
+ <canvas id="perf-detail-canvas" class="perf-canvas" style="width:100%;height:240px"></canvas>
1449
+ </div>
1450
+ `}renderDetailHistory(t){if(t.requests.length===0)return d;let s=[];for(let r=t.requests.length-1;r>=0&&s.length<50;r--)s.push({point:t.requests[r],originalIndex:r});return a`
1451
+ <div class="perf-history-wrap">
1452
+ <table class="perf-table">
1453
+ <thead>
1454
+ <tr>
1455
+ <th>Time</th>
1456
+ <th>Health</th>
1457
+ <th>Duration</th>
1458
+ <th>Breakdown</th>
1459
+ <th class="perf-th-center">Status</th>
1460
+ <th class="perf-th-right">Queries</th>
1461
+ </tr>
1462
+ </thead>
1463
+ <tbody>
1464
+ ${s.map(r=>this.renderHistoryRow(r.point,r.originalIndex,t.baselineP95Ms))}
1465
+ </tbody>
1466
+ </table>
626
1467
  </div>
627
- <div class="view-telemetry" id="performance-container" style="display:none">
628
- <div id="graph-content"></div>
1468
+ `}renderHistoryRow(t,s,r){let o=this.healthGradeForDuration(t.durationMs,r),n=new Date(t.timestamp).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),l=t.statusCode>=400,c=t.queryTimeMs||0,p=t.fetchTimeMs||0,h=Math.max(0,t.durationMs-c-p);return a`
1469
+ <tr class="perf-table-row ${l?"perf-row-err":""}" data-req-idx=${s}>
1470
+ <td class="perf-td-muted">${n}</td>
1471
+ <td>
1472
+ <span class="perf-badge perf-badge-sm" style="color:${o.color};background:${o.bg};border-color:${o.border}">${o.label}</span>
1473
+ </td>
1474
+ <td>${v(t.durationMs)}</td>
1475
+ <td>
1476
+ ${c>0?a`<span class="perf-bd-tag perf-bd-tag-db">DB ${v(c)}</span>`:d}
1477
+ ${p>0?a`<span class="perf-bd-tag perf-bd-tag-fetch">Fetch ${v(p)}</span>`:d}
1478
+ <span class="perf-bd-tag perf-bd-tag-app">App ${v(h)}</span>
1479
+ </td>
1480
+ <td class="perf-td-center" style="color:${l?"var(--red)":"var(--text-muted)"}">${t.statusCode}</td>
1481
+ <td class="perf-td-right perf-td-muted">${t.queryCount}</td>
1482
+ </tr>
1483
+ `}};u([R({context:x})],L.prototype,"store",2),u([_()],L.prototype,"selectedEndpoint",2),u([_()],L.prototype,"graphData",2),u([_()],L.prototype,"loadError",2),u([_()],L.prototype,"queryBreakdown",2),u([_()],L.prototype,"queryBreakdownLoading",2),L=u([g("bk-performance-view")],L);function _r(i){return i===0?"<1ms":v(i)}var w=class extends f{constructor(){super(...arguments);this.requestId="";this.requestStarted=0;this.data=null;this.loading=false;this.failed=false;this.expandedSqlIdx=-1;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate()),this.requestId&&this.loadTimeline();}async loadTimeline(){if(!this.requestId)return;let t=w.cache.get(this.requestId);if(t){this.data=t;return}this.loading=true;try{let s=await fetch(`${y.activity}?requestId=${this.requestId}`);if(!s.ok){this.failed=!0,this.loading=!1;return}let r=await s.json();if(w.cache.size>=pe){let o=w.cache.keys().next().value;o!==void 0&&w.cache.delete(o);}w.cache.set(this.requestId,r),this.data=r,this.loading=!1;}catch(s){console.debug("[brakit] timeline load failed:",s),this.failed=true,this.loading=false;}}toggleSql(t,s){s.stopPropagation(),this.expandedSqlIdx=this.expandedSqlIdx===t?-1:t;}copySql(t,s){s.stopPropagation(),navigator.clipboard.writeText(t).then(()=>C.show("SQL copied")).catch(()=>C.show("Copy failed"));}render(){if(this.loading)return a`<div class="tl-loading">Loading activity...</div>`;if(this.failed||!this.data||this.data.total===0)return d;let t=this.data,s=t.timeline[0]?.timestamp??0;return a`
1484
+ <div class="tl-header">
1485
+ <span class="tl-title">Activity Timeline</span>
1486
+ <span class="tl-counts">
1487
+ ${t.counts.queries>0?a`<span class="tl-count tl-count-query">${t.counts.queries} quer${t.counts.queries===1?"y":"ies"}</span>`:d}
1488
+ ${t.counts.fetches>0?a`<span class="tl-count tl-count-fetch">${t.counts.fetches} fetch${t.counts.fetches===1?"":"es"}</span>`:d}
1489
+ ${t.counts.logs>0?a`<span class="tl-count tl-count-log">${t.counts.logs} log${t.counts.logs===1?"":"s"}</span>`:d}
1490
+ ${t.counts.errors>0?a`<span class="tl-count tl-count-error">${t.counts.errors} error${t.counts.errors===1?"":"s"}</span>`:d}
1491
+ </span>
629
1492
  </div>
630
- </div>
631
- <div class="footer">
632
- <span id="stat-total">0 requests</span>
633
- <span id="stat-flows">0 actions</span>
634
- <span id="stat-errors" class="error-count">0 errors</span>
635
- <span id="stat-avg">Avg: 0ms</span>
636
- </div>
637
- </div>
638
- </div>
639
- <div class="toast" id="toast"></div>
640
-
641
- <script>
642
- (function(){
643
- var PORT = {{PORT}};
644
- var state = { flows: [], requests: [], fetches: [], errors: [], logs: [], queries: [], insights: [], findings: [], viewMode: 'simple', activeView: 'overview' };
645
-
646
- var appEl = document.getElementById('app');
647
- var flowListEl = document.getElementById('flow-list');
648
- var reqListEl = document.getElementById('request-list');
649
- var emptyFlows = document.getElementById('empty-flows');
650
- var toastEl = document.getElementById('toast');
651
-
652
-
653
- function formatDuration(ms) {
654
- if (ms < 1000) return ms + 'ms';
655
- return (ms / 1000).toFixed(1) + 's';
656
- }
657
- function formatSize(bytes) {
658
- if (!bytes || bytes === 0) return '';
659
- if (bytes < 1024) return bytes + 'b';
660
- return (bytes / 1024).toFixed(1) + 'kb';
661
- }
662
- function escHtml(s) {
663
- if (!s) return '';
664
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
665
- }
666
-
667
- function statusPillClass(code) {
668
- if (code >= 500) return 'status-pill-5xx';
669
- if (code >= 400) return 'status-pill-4xx';
670
- if (code >= 300) return 'status-pill-3xx';
671
- return 'status-pill-2xx';
672
- }
673
-
674
- function statusIcon(code) {
675
- if (code >= 500) return { icon: '\u2717', cls: 'status-error', tip: code + ' Server Error' };
676
- if (code >= 400) return { icon: '\u2717', cls: 'status-fail', tip: code + ' ' + httpStatus(code) };
677
- if (code >= 300) return { icon: '\u2713', cls: 'status-ok', tip: code + ' Redirect' };
678
- return { icon: '\u2713', cls: 'status-ok', tip: code + ' OK' };
679
- }
680
-
681
- function httpStatus(code) {
682
- var 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'};
683
- return map[code] || (code >= 500 ? 'Server Error' : code >= 400 ? 'Client Error' : 'OK');
684
- }
685
-
686
- var SENSITIVE = new Set(['cookie','set-cookie','authorization','proxy-authorization','x-api-key','x-auth-token']);
687
- function maskValue(k, v) {
688
- if (SENSITIVE.has(k.toLowerCase())) {
689
- var s = String(v);
690
- if (s.length <= 8) return '****';
691
- return s.slice(0, 4) + '...' + s.slice(-4) + ' (' + s.length + ' chars)';
692
- }
693
- return String(v);
694
- }
695
- function formatHeaders(headers) {
696
- if (!headers || Object.keys(headers).length === 0) return '<span style="color:var(--text-muted)">No headers</span>';
697
- return Object.entries(headers).map(function(e) { return '<span class="json-key">' + escHtml(e[0]) + '</span>: ' + escHtml(maskValue(e[0], e[1])); }).join('\n');
698
- }
699
-
700
- function buildBodyToggle(direction, label, body) {
701
- var block = document.createElement('div');
702
- block.className = 'traffic-body';
703
- var toggle = document.createElement('button');
704
- toggle.className = 'traffic-body-toggle';
705
- toggle.innerHTML = '<span class="chevron">\u25B8</span><span class="arrow-' + direction + '">' + (direction === 'out' ? '\u2192' : '\u2190') + '</span> ' + label;
706
- var pre = document.createElement('pre');
707
- pre.innerHTML = formatJsonBody(body);
708
- toggle.addEventListener('click', function(e) {
709
- e.stopPropagation();
710
- toggle.classList.toggle('open');
711
- pre.classList.toggle('open');
712
- });
713
- block.appendChild(toggle);
714
- block.appendChild(pre);
715
- return block;
716
- }
717
-
718
- function formatJsonBody(body) {
719
- if (!body) return '<span style="color:var(--text-muted)">No body</span>';
720
- try {
721
- var parsed = JSON.parse(body);
722
- return highlightJson(JSON.stringify(parsed, null, 2));
723
- } catch(e) { return escHtml(body); }
724
- }
725
- function highlightJson(json) {
726
- return escHtml(json).replace(
727
- /("(?:[^"\\\\]|\\\\.)*")(\\s*:)?|\\b(true|false)\\b|\\bnull\\b|(-?\\d+\\.?\\d*(?:[eE][+-]?\\d+)?)/g,
728
- function(m, str, colon, bool, num) {
729
- if (str) return colon ? '<span class="json-key">' + str + '</span>' + colon : '<span class="json-str">' + str + '</span>';
730
- if (bool) return '<span class="json-bool">' + m + '</span>';
731
- if (num) return '<span class="json-num">' + m + '</span>';
732
- if (m === 'null') return '<span class="json-null">null</span>';
733
- return m;
734
- }
735
- );
736
- }
737
-
738
- function showToast(msg) {
739
- toastEl.textContent = msg;
740
- toastEl.classList.add('show');
741
- setTimeout(function() { toastEl.classList.remove('show'); }, 2000);
742
- }
743
-
744
- function collapseAll(rowSelector, detailSelector) {
745
- document.querySelectorAll(rowSelector + '.expanded').forEach(function(r) { r.classList.remove('expanded'); });
746
- document.querySelectorAll(detailSelector + '.open').forEach(function(d) { d.classList.remove('open'); });
747
- }
748
-
749
-
750
- function createTelemetryView(listId, buildRowFn) {
751
- return {
752
- render: function(items) {
753
- var list = document.getElementById(listId);
754
- if (!list) return;
755
- list.innerHTML = '';
756
- items.forEach(function(item) { list.appendChild(buildRowFn(item)); });
757
- },
758
- prepend: function(item) {
759
- var list = document.getElementById(listId);
760
- if (!list) return;
761
- list.insertBefore(buildRowFn(item), list.firstChild);
762
- }
763
- };
764
- }
765
-
766
-
767
- var QUERY_OP_COLORS = { SELECT: 'var(--blue)', INSERT: 'var(--green)', UPDATE: 'var(--amber)', DELETE: 'var(--red)', COUNT: 'var(--text-muted)' };
768
-
769
- function truncateSQL(sql, max) {
770
- if (!sql) return '';
771
- var clean = sql.replace(/"public"\./g, '').replace(/"/g, '');
772
- if (clean.length <= max) return clean;
773
- return clean.substring(0, max) + '...';
774
- }
775
-
776
- function queryDuration(ms) {
777
- if (ms === 0) return '<1ms';
778
- return formatDuration(ms);
779
- }
780
-
781
-
782
- var flowColHeader = document.getElementById('flow-col-header');
783
- function renderFlows() {
784
- flowListEl.innerHTML = '';
785
- if (state.flows.length === 0) {
786
- flowListEl.appendChild(emptyFlows);
787
- emptyFlows.style.display = 'flex';
788
- if (flowColHeader) flowColHeader.style.display = 'none';
789
- return;
790
- }
791
- emptyFlows.style.display = 'none';
792
- if (flowColHeader) flowColHeader.style.display = 'flex';
793
- for (var i = 0; i < state.flows.length; i++) {
794
- var result = createFlowRow(state.flows[i]);
795
- flowListEl.appendChild(result.row);
796
- flowListEl.appendChild(result.expand);
797
- }
798
- }
799
-
800
- function flowDotClass(flow) {
801
- if (flow.hasErrors) return 'dot-error';
802
- if (flow.redundancyPct > 0) return 'dot-warn';
803
- return 'dot-clean';
804
- }
805
-
806
- function flowBadgeInfo(flow) {
807
- if (flow.hasErrors) {
808
- var errCount = flow.requests.filter(function(r){ return r.statusCode >= 400; }).length;
809
- return { text: errCount + ' error' + (errCount !== 1 ? 's' : ''), cls: 'badge-error' };
810
- }
811
- if (flow.redundancyPct > 0) {
812
- return { text: flow.redundancyPct + '% redundant', cls: 'badge-warn' };
813
- }
814
- return { text: 'clean', cls: 'badge-clean' };
815
- }
816
-
817
- function createFlowRow(flow) {
818
- var row = document.createElement('div');
819
- row.className = 'flow-row';
820
- var summary = document.createElement('div');
821
- summary.className = 'flow-summary-row';
822
- var dot = document.createElement('span');
823
- dot.className = 'flow-status-dot ' + flowDotClass(flow);
824
- var label = document.createElement('span');
825
- label.className = 'flow-label';
826
- label.textContent = flow.label;
827
- var count = document.createElement('span');
828
- count.className = 'flow-req-count';
829
- count.textContent = flow.requests.length + ' req' + (flow.requests.length !== 1 ? 's' : '');
830
- var badgeInfo = flowBadgeInfo(flow);
831
- var badge = document.createElement('span');
832
- badge.className = 'flow-badge-pill ' + badgeInfo.cls;
833
- badge.textContent = badgeInfo.text;
834
- var dur = document.createElement('span');
835
- dur.className = 'flow-duration';
836
- dur.textContent = formatDuration(flow.totalDurationMs);
837
- summary.appendChild(dot);
838
- summary.appendChild(label);
839
- summary.appendChild(count);
840
- summary.appendChild(badge);
841
- summary.appendChild(dur);
842
- row.appendChild(summary);
843
-
844
- var expand = document.createElement('div');
845
- expand.className = 'flow-expand';
846
-
847
- row.addEventListener('click', function() {
848
- var wasOpen = row.classList.contains('expanded');
849
- collapseAll('.flow-row', '.flow-expand');
850
- if (!wasOpen) {
851
- row.classList.add('expanded');
852
- expand.classList.add('open');
853
- expand.innerHTML = '';
854
- if (state.viewMode === 'simple') {
855
- expand.appendChild(createFlowInsights(flow));
856
- } else {
857
- expand.appendChild(createFlowSubReqs(flow));
858
- }
859
- var tlEls = expand.querySelectorAll('.request-timeline');
860
- for (var ti = 0; ti < tlEls.length; ti++) {
861
- var tlItem = tlEls[ti];
862
- var rid = tlItem.getAttribute('data-request-id');
863
- if (rid) loadTimeline(rid, tlItem, 0);
864
- }
865
- }
866
- });
867
-
868
- return { row: row, expand: expand };
869
- }
870
-
871
-
872
- var skipCats = { 'auth-handshake': 1, 'auth-check': 1, 'middleware': 1 };
873
-
874
- function createFlowInsights(flow) {
875
- var container = document.createElement('div');
876
- var traffic = document.createElement('div');
877
- traffic.className = 'flow-traffic';
878
-
879
- for (var i = 0; i < flow.requests.length; i++) {
880
- var req = flow.requests[i];
881
- if (skipCats[req.category]) continue;
882
- var sClass = statusPillClass(req.statusCode);
883
-
884
- var card = document.createElement('div');
885
- card.className = 'traffic-card';
886
-
887
- var header = document.createElement('div');
888
- header.className = 'traffic-card-header';
889
-
890
- var mEl = document.createElement('span');
891
- mEl.className = 'method-badge method-badge-' + escHtml(req.method);
892
- mEl.textContent = req.method;
893
-
894
- var pEl = document.createElement('span');
895
- pEl.className = 'traffic-card-path' + (req.isDuplicate ? ' is-dup' : '');
896
- pEl.textContent = req.label;
897
-
898
- var stEl = document.createElement('span');
899
- stEl.className = 'status-pill ' + sClass;
900
- stEl.textContent = String(req.statusCode);
901
-
902
- var dEl = document.createElement('span');
903
- dEl.className = 'traffic-card-dur';
904
- dEl.textContent = formatDuration(req.pollingDurationMs || req.durationMs);
905
-
906
- header.appendChild(mEl);
907
- header.appendChild(pEl);
908
- header.appendChild(stEl);
909
- header.appendChild(dEl);
910
-
911
- if (req.isDuplicate) {
912
- var dupEl = document.createElement('span');
913
- dupEl.className = 'traffic-card-dup';
914
- dupEl.textContent = 'duplicate';
915
- header.appendChild(dupEl);
916
- } else {
917
- var szEl = document.createElement('span');
918
- szEl.className = 'traffic-card-size';
919
- szEl.textContent = formatSize(req.responseSize);
920
- header.appendChild(szEl);
921
- }
922
-
923
- card.appendChild(header);
924
-
925
- if (req.isStrictModeDupe) {
926
- card.classList.add('strict-mode-dupe');
927
- var smBanner = document.createElement('div');
928
- smBanner.className = 'strict-mode-banner';
929
- smBanner.textContent = 'React Strict Mode duplicate \u2014 does not happen in production';
930
- card.appendChild(smBanner);
931
- }
932
-
933
- var hasDetails = false;
934
- if (!req.isDuplicate && req.category !== 'static' && req.category !== 'polling') {
935
- var tlEl = document.createElement('div');
936
- tlEl.className = 'request-timeline tl-hidden';
937
- tlEl.setAttribute('data-request-id', req.id);
938
- tlEl.setAttribute('data-request-started', String(req.startedAt));
939
- card.appendChild(tlEl);
940
- hasDetails = true;
941
- }
942
- if (req.requestBody && req.method !== 'GET') {
943
- card.appendChild(buildBodyToggle('out', 'Request Body', req.requestBody));
944
- hasDetails = true;
945
- }
946
- if (req.responseBody) {
947
- card.appendChild(buildBodyToggle('in', 'Response Body', req.responseBody));
948
- hasDetails = true;
949
- }
950
-
951
- if (hasDetails) header.classList.add('has-details');
952
-
953
- traffic.appendChild(card);
954
- }
955
-
956
- container.appendChild(traffic);
957
-
958
- var insights = analyzeFlow(flow);
959
- var hasIssues = insights.errors.length > 0 || insights.duplicates.length > 0 || insights.warnings.length > 0 || !!insights.tip;
960
- if (hasIssues) {
961
- var divider = document.createElement('div');
962
- divider.className = 'flow-divider';
963
- container.appendChild(divider);
964
- var insightsEl = document.createElement('div');
965
- insightsEl.className = 'flow-insights';
966
- for (var ei = 0; ei < insights.errors.length; ei++) {
967
- var errLine = document.createElement('div');
968
- errLine.className = 'insight-line insight-error';
969
- errLine.textContent = '\u2717 ' + insights.errors[ei];
970
- insightsEl.appendChild(errLine);
971
- }
972
- for (var di = 0; di < insights.duplicates.length; di++) {
973
- var dup = insights.duplicates[di];
974
- var dupLine = document.createElement('div');
975
- dupLine.className = 'insight-line insight-warn';
976
- dupLine.textContent = '\u26A0 ' + dup.name + ' \u2014 loaded ' + dup.count + 'x (wasting ~' + formatDuration(dup.wastedMs) + ')';
977
- insightsEl.appendChild(dupLine);
978
- }
979
- for (var wi = 0; wi < insights.warnings.length; wi++) {
980
- var warnLine = document.createElement('div');
981
- warnLine.className = 'insight-line insight-warn';
982
- warnLine.textContent = '\u26A0 ' + insights.warnings[wi];
983
- insightsEl.appendChild(warnLine);
984
- }
985
- if (insights.tip) {
986
- var tipLine = document.createElement('div');
987
- tipLine.className = 'insight-line insight-tip';
988
- tipLine.textContent = 'Tip: ' + insights.tip;
989
- insightsEl.appendChild(tipLine);
990
- }
991
- container.appendChild(insightsEl);
992
- }
993
-
994
- return container;
995
- }
996
-
997
- function analyzeFlow(flow) {
998
- var reqs = flow.requests;
999
- var successes = [];
1000
- var errors = [];
1001
- var warnings = [];
1002
- var duplicates = [];
1003
- var seen = new Map();
1004
- var totalMs = 0;
1005
- for (var i = 0; i < reqs.length; i++) {
1006
- var req = reqs[i];
1007
- var label = req.label;
1008
- var dur = req.pollingDurationMs || req.durationMs;
1009
- totalMs += dur;
1010
-
1011
- if (skipCats[req.category]) {
1012
- continue;
1013
- }
1014
-
1015
- if (req.isDuplicate) {
1016
- var ex = seen.get(label);
1017
- if (ex) { ex.count++; ex.wastedMs += dur; }
1018
- else seen.set(label, { name: label, count: 2, wastedMs: dur });
1019
- continue;
1020
- }
1021
- if (req.statusCode >= 400) {
1022
- errors.push(label + ' (' + httpStatus(req.statusCode) + ')');
1023
- continue;
1024
- }
1025
-
1026
- if (req.responseSize > 51200) {
1027
- warnings.push('Large response: ' + label + ' returned ' + formatSize(req.responseSize));
1028
- }
1029
-
1030
- successes.push(label);
1031
- }
1032
-
1033
- for (var d of seen.values()) duplicates.push(d);
1034
- var tip = '';
1035
- if (duplicates.length > 0) {
1036
- var names = duplicates.map(function(d) { return d.name; }).join(', ');
1037
- var totalWaste = duplicates.reduce(function(s, d) { return s + d.wastedMs; }, 0);
1038
- tip = 'Your app fetches ' + names + ' multiple times on this page. This wastes ~' + formatDuration(totalWaste) + '. Try caching these calls, deduplicating with React Query/SWR, or moving them to a shared layout.';
1039
- } else if (errors.length > 0) {
1040
- tip = 'Some requests are failing. Check your API routes and make sure the endpoints exist.';
1041
- }
1042
- var slow = reqs.filter(function(r) { return r.durationMs > 2000 && r.category !== 'polling'; });
1043
- if (slow.length > 0 && !tip) {
1044
- tip = slow.map(function(r) { return r.label; }).join(', ') + ' is taking over 2 seconds. Consider adding caching or optimizing the backend query.';
1045
- }
1046
- return { successes: successes, errors: errors, warnings: warnings, duplicates: duplicates, tip: tip };
1047
- }
1048
-
1049
-
1050
- function createFlowSubReqs(flow) {
1051
- var container = document.createElement('div');
1052
- container.className = 'flow-subreqs';
1053
- flow.requests.forEach(function(req) {
1054
- var isDup = req.isDuplicate;
1055
- var sClass = statusPillClass(req.statusCode);
1056
- var subRow = document.createElement('div');
1057
- subRow.className = 'flow-subreq';
1058
- var safeMethod = escHtml(req.method);
1059
- var methodEl = document.createElement('span');
1060
- methodEl.className = 'method-badge method-badge-' + safeMethod;
1061
- methodEl.textContent = req.method;
1062
- var labelEl = document.createElement('span');
1063
- labelEl.className = 'subreq-label' + (isDup ? ' is-dup' : '');
1064
- labelEl.textContent = req.path || req.url;
1065
- var statusEl = document.createElement('span');
1066
- statusEl.className = 'status-pill ' + sClass;
1067
- statusEl.textContent = String(req.statusCode);
1068
- var durEl = document.createElement('span');
1069
- durEl.className = 'subreq-dur';
1070
- durEl.textContent = req.pollingDurationMs ? formatDuration(req.pollingDurationMs) : formatDuration(req.durationMs);
1071
- subRow.appendChild(methodEl);
1072
- subRow.appendChild(labelEl);
1073
- if (isDup) {
1074
- var dupTag = document.createElement('span');
1075
- dupTag.className = 'subreq-dup-tag';
1076
- dupTag.textContent = 'duplicate';
1077
- subRow.appendChild(dupTag);
1078
- }
1079
- subRow.appendChild(statusEl);
1080
- subRow.appendChild(durEl);
1081
-
1082
- var detail = document.createElement('div');
1083
- detail.className = 'flow-subreq-detail';
1084
- subRow.addEventListener('click', function(e) {
1085
- e.stopPropagation();
1086
- var wasOpen = detail.classList.contains('open');
1087
- container.querySelectorAll('.flow-subreq-detail.open').forEach(function(d){ d.classList.remove('open'); });
1088
- container.querySelectorAll('.flow-subreq.expanded').forEach(function(r){ r.classList.remove('expanded'); });
1089
- if (!wasOpen) {
1090
- subRow.classList.add('expanded');
1091
- detail.classList.add('open');
1092
- detail.innerHTML = renderDetail(req);
1093
- var curlBtn = detail.querySelector('.btn-curl');
1094
- if (curlBtn) curlBtn.addEventListener('click', function(ev) { ev.stopPropagation(); copyAsCurl(req); });
1095
- var tlEl = detail.querySelector('.request-timeline');
1096
- if (tlEl) loadTimeline(tlEl.getAttribute('data-request-id'), tlEl, 0);
1097
- }
1098
- });
1099
- container.appendChild(subRow);
1100
- container.appendChild(detail);
1101
- });
1102
- return container;
1103
- }
1104
-
1105
- function renderDetail(req) {
1106
- var sClass = statusPillClass(req.statusCode);
1107
- var sm = escHtml(req.method);
1108
- var h = '<div class="detail-meta">';
1109
- h += '<span><span class="method-badge method-badge-' + sm + '">' + sm + '</span> ' + escHtml(req.url) + '</span>';
1110
- h += '<span><span class="status-pill ' + sClass + '">' + req.statusCode + '</span></span>';
1111
- h += '<span>' + req.durationMs + 'ms</span>';
1112
- if (req.responseSize) h += '<span>' + formatSize(req.responseSize) + '</span>';
1113
- h += '</div>';
1114
- h += '<div class="request-timeline tl-hidden" data-request-id="' + escHtml(req.id) + '" data-request-started="' + escHtml(String(req.startedAt)) + '"></div>';
1115
- h += '<div class="detail-grid">';
1116
- h += '<div class="detail-section"><h4>Request Headers</h4><pre>' + formatHeaders(req.headers) + '</pre></div>';
1117
- h += '<div class="detail-section"><h4>Response Headers</h4><pre>' + formatHeaders(req.responseHeaders) + '</pre></div>';
1118
- h += '<div class="detail-section"><h4>Request Body</h4><pre>' + formatJsonBody(req.requestBody) + '</pre></div>';
1119
- h += '<div class="detail-section"><h4>Response Body</h4><pre>' + formatJsonBody(req.responseBody) + '</pre></div>';
1120
- h += '</div>';
1121
- h += '<div class="detail-actions"><button class="btn btn-curl">Copy cURL</button></div>';
1122
- return h;
1123
- }
1124
-
1125
-
1126
-
1127
- function renderRequests() {
1128
- reqListEl.innerHTML = '';
1129
- for (var i = 0; i < state.requests.length; i++) {
1130
- var req = state.requests[i];
1131
- if (req.path && req.path.startsWith('/__brakit')) continue;
1132
- appendRequestRow(req);
1133
- }
1134
- }
1135
-
1136
- function prependRequestRow(req) {
1137
- var result = createReqRow(req);
1138
- reqListEl.prepend(result.detail);
1139
- reqListEl.prepend(result.row);
1140
- }
1141
-
1142
- function appendRequestRow(req) {
1143
- var result = createReqRow(req);
1144
- reqListEl.appendChild(result.row);
1145
- reqListEl.appendChild(result.detail);
1146
- }
1147
-
1148
- function createReqRow(req) {
1149
- var row = document.createElement('div');
1150
- row.className = 'req-row';
1151
- var sClass = statusPillClass(req.statusCode);
1152
- var safeMethod = escHtml(req.method);
1153
- row.innerHTML =
1154
- '<div class="req-summary">' +
1155
- '<span class="method-badge method-badge-' + safeMethod + '">' + safeMethod + '</span>' +
1156
- '<span class="req-url">' + escHtml(req.url) + '</span>' +
1157
- '<span class="status-pill ' + sClass + '">' + req.statusCode + '</span>' +
1158
- '<span class="req-duration">' + req.durationMs + 'ms</span>' +
1159
- '<span class="req-size">' + formatSize(req.responseSize) + '</span>' +
1160
- '</div>';
1161
- var detail = document.createElement('div');
1162
- detail.className = 'req-detail';
1163
- row.addEventListener('click', function() {
1164
- var wasOpen = row.classList.contains('expanded');
1165
- collapseAll('.req-row', '.req-detail');
1166
- if (!wasOpen) {
1167
- row.classList.add('expanded');
1168
- detail.classList.add('open');
1169
- detail.innerHTML = renderDetail(req);
1170
- var curlBtn = detail.querySelector('.btn-curl');
1171
- if (curlBtn) curlBtn.addEventListener('click', function(e) { e.stopPropagation(); copyAsCurl(req); });
1172
- var tlEl = detail.querySelector('.request-timeline');
1173
- if (tlEl) loadTimeline(tlEl.getAttribute('data-request-id'), tlEl, 0);
1174
- }
1175
- });
1176
- return { row: row, detail: detail };
1177
- }
1178
-
1179
-
1180
- function buildFetchAnalysis() {
1181
- var container = document.getElementById('fetch-analysis');
1182
- if (!container) return;
1183
- container.innerHTML = '';
1184
-
1185
- var fetches = state.fetches;
1186
- if (fetches.length === 0) {
1187
- container.style.display = 'none';
1188
- return;
1189
- }
1190
- container.style.display = 'block';
1191
-
1192
- var uniqueUrls = {};
1193
- var errCount = 0;
1194
- var totalDur = 0;
1195
- for (var i = 0; i < fetches.length; i++) {
1196
- uniqueUrls[fetches[i].url] = 1;
1197
- if (fetches[i].statusCode >= 400) errCount++;
1198
- totalDur += fetches[i].durationMs;
1199
- }
1200
- var uniqueCount = Object.keys(uniqueUrls).length;
1201
- var avgDur = Math.round(totalDur / fetches.length);
1202
-
1203
- var summary = document.createElement('div');
1204
- summary.className = 'fetch-summary';
1205
- summary.innerHTML =
1206
- '<div class="fetch-stat"><span class="fetch-stat-value">' + fetches.length + '</span><span class="fetch-stat-label">Total Fetches</span></div>' +
1207
- '<div class="fetch-stat"><span class="fetch-stat-value">' + uniqueCount + '</span><span class="fetch-stat-label">Unique URLs</span></div>' +
1208
- '<div class="fetch-stat"><span class="fetch-stat-value"' + (errCount > 0 ? ' style="color:var(--red)"' : '') + '>' + errCount + '</span><span class="fetch-stat-label">Errors</span></div>' +
1209
- '<div class="fetch-stat"><span class="fetch-stat-value">' + formatDuration(avgDur) + '</span><span class="fetch-stat-label">Avg Duration</span></div>';
1210
- container.appendChild(summary);
1211
-
1212
- var groups = {};
1213
- for (var gi = 0; gi < fetches.length; gi++) {
1214
- var f = fetches[gi];
1215
- var key = f.method + ' ' + f.url;
1216
- if (!groups[key]) groups[key] = { method: f.method, url: f.url, count: 0, totalDur: 0, maxDur: 0, errors: 0, callers: {} };
1217
- var g = groups[key];
1218
- g.count++;
1219
- g.totalDur += f.durationMs;
1220
- if (f.durationMs > g.maxDur) g.maxDur = f.durationMs;
1221
- if (f.statusCode >= 400) g.errors++;
1222
- if (f.parentRequestId) {
1223
- for (var ri = 0; ri < state.requests.length; ri++) {
1224
- if (state.requests[ri].id === f.parentRequestId) {
1225
- var callerLabel = state.requests[ri].method + ' ' + (state.requests[ri].path || state.requests[ri].url);
1226
- g.callers[callerLabel] = 1;
1227
- break;
1228
- }
1229
- }
1230
- }
1231
- }
1232
-
1233
- var groupEntries = [];
1234
- for (var gk in groups) groupEntries.push(groups[gk]);
1235
- groupEntries.sort(function(a, b) { return b.count - a.count; });
1236
-
1237
- if (groupEntries.length > 0) {
1238
- var title = document.createElement('div');
1239
- title.className = 'fetch-groups-title';
1240
- title.textContent = 'Grouped by URL (' + groupEntries.length + ')';
1241
- container.appendChild(title);
1242
-
1243
- var groupsDiv = document.createElement('div');
1244
- groupsDiv.className = 'fetch-groups';
1245
-
1246
- for (var gei = 0; gei < groupEntries.length; gei++) {
1247
- var ge = groupEntries[gei];
1248
- var card = document.createElement('div');
1249
- card.className = 'fetch-group';
1250
-
1251
- var avgMs = Math.round(ge.totalDur / ge.count);
1252
- var errRate = ge.count > 0 ? Math.round((ge.errors / ge.count) * 100) : 0;
1253
-
1254
- var headerHtml =
1255
- '<div class="fetch-group-header">' +
1256
- '<span class="method-badge method-badge-' + escHtml(ge.method) + '">' + escHtml(ge.method) + '</span>' +
1257
- '<span class="fetch-group-url" title="' + escHtml(ge.url) + '">' + escHtml(ge.url) + '</span>' +
1258
- '<span class="fetch-group-count">' + ge.count + 'x</span>' +
1259
- '</div>';
1260
-
1261
- var metaHtml = '<div class="fetch-group-meta">' +
1262
- '<span>Avg ' + formatDuration(avgMs) + '</span>' +
1263
- '<span>Max ' + formatDuration(ge.maxDur) + '</span>' +
1264
- (errRate > 0 ? '<span class="fetch-group-err">' + errRate + '% errors</span>' : '<span style="color:var(--green)">0% errors</span>') +
1265
- '</div>';
1266
-
1267
- var callerKeys = Object.keys(ge.callers);
1268
- var callerHtml = '';
1269
- if (callerKeys.length > 0) {
1270
- callerHtml = '<div class="fetch-group-callers">Called by: <strong>' + callerKeys.map(function(c) { return escHtml(c); }).join('</strong>, <strong>') + '</strong></div>';
1271
- }
1272
-
1273
- card.innerHTML = headerHtml + metaHtml + callerHtml;
1274
- groupsDiv.appendChild(card);
1275
- }
1276
-
1277
- container.appendChild(groupsDiv);
1278
- }
1279
- }
1280
-
1281
- function renderFetches() {
1282
- buildFetchAnalysis();
1283
- }
1284
-
1285
- function prependFetchRow(f) {
1286
- buildFetchAnalysis();
1287
- }
1288
-
1289
- async function loadFetches() {
1290
- try {
1291
- var res = await fetch('/__brakit/api/fetches');
1292
- var data = await res.json();
1293
- state.fetches = data.entries;
1294
- renderFetches();
1295
- } catch(e) { console.warn('[brakit]', e); }
1296
- }
1297
-
1298
-
1299
- function buildErrorRow(e) {
1300
- var row = document.createElement('div');
1301
- row.className = 'req-row tel-clickable';
1302
- var ts = new Date(e.timestamp).toLocaleTimeString();
1303
- row.innerHTML =
1304
- '<span class="tel-error-name" title="' + escHtml(e.name) + '">' + escHtml(e.name) + '</span>' +
1305
- '<span class="tel-message" title="' + escHtml(e.message) + '">' + escHtml(e.message) + '</span>' +
1306
- '<span class="tel-timestamp">' + ts + '</span>';
1307
- row.addEventListener('click', function() {
1308
- row.classList.toggle('expanded');
1309
- var existing = row.nextElementSibling;
1310
- if (existing && existing.classList.contains('error-stack')) {
1311
- existing.remove();
1312
- return;
1313
- }
1314
- if (e.stack) {
1315
- var stackEl = document.createElement('div');
1316
- stackEl.className = 'error-stack';
1317
- stackEl.textContent = e.stack;
1318
- row.parentNode.insertBefore(stackEl, row.nextSibling);
1319
- }
1320
- });
1321
- return row;
1322
- }
1323
-
1324
- var errorView = createTelemetryView('error-list', buildErrorRow);
1325
- function renderErrors() { errorView.render(state.errors); }
1326
- function prependErrorRow(e) { errorView.prepend(e); }
1327
-
1328
- async function loadErrors() {
1329
- try {
1330
- var res = await fetch('/__brakit/api/errors');
1331
- var data = await res.json();
1332
- state.errors = data.entries;
1333
- renderErrors();
1334
- } catch(e) { console.warn('[brakit]', e); }
1335
- }
1336
-
1337
-
1338
- function buildLogRow(l) {
1339
- var row = document.createElement('div');
1340
- row.className = 'req-row';
1341
- var ts = new Date(l.timestamp).toLocaleTimeString();
1342
- row.innerHTML =
1343
- '<span class="tel-level tel-level-' + l.level + '">' + l.level.toUpperCase() + '</span>' +
1344
- '<span class="tel-message tel-mono" title="' + escHtml(l.message) + '">' + escHtml(l.message) + '</span>' +
1345
- '<span class="tel-timestamp">' + ts + '</span>';
1346
- return row;
1347
- }
1348
-
1349
- function buildLogAnalysis() {
1350
- var container = document.getElementById('log-analysis');
1351
- if (!container) return;
1352
- container.innerHTML = '';
1353
-
1354
- var logs = state.logs;
1355
- if (logs.length === 0) {
1356
- container.style.display = 'none';
1357
- return;
1358
- }
1359
- container.style.display = 'block';
1360
-
1361
- var counts = { error: 0, warn: 0, info: 0, debug: 0, log: 0 };
1362
- for (var i = 0; i < logs.length; i++) {
1363
- var lvl = logs[i].level;
1364
- if (counts[lvl] !== undefined) counts[lvl]++;
1365
- }
1366
-
1367
- var summary = document.createElement('div');
1368
- summary.className = 'fetch-summary';
1369
- summary.innerHTML =
1370
- '<div class="fetch-stat"><span class="fetch-stat-value">' + logs.length + '</span><span class="fetch-stat-label">Total Logs</span></div>' +
1371
- (counts.error > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value" style="color:var(--red)">' + counts.error + '</span><span class="fetch-stat-label">Errors</span></div>' : '') +
1372
- (counts.warn > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value" style="color:var(--amber)">' + counts.warn + '</span><span class="fetch-stat-label">Warnings</span></div>' : '') +
1373
- '<div class="fetch-stat"><span class="fetch-stat-value">' + counts.info + '</span><span class="fetch-stat-label">Info</span></div>' +
1374
- (counts.debug > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value">' + counts.debug + '</span><span class="fetch-stat-label">Debug</span></div>' : '') +
1375
- (counts.log > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value">' + counts.log + '</span><span class="fetch-stat-label">Log</span></div>' : '');
1376
- container.appendChild(summary);
1377
- }
1378
-
1379
- var logView = createTelemetryView('log-list', buildLogRow);
1380
-
1381
- function renderLogs() {
1382
- buildLogAnalysis();
1383
- logView.render(state.logs);
1384
- }
1385
-
1386
- function prependLogRow(l) {
1387
- buildLogAnalysis();
1388
- logView.prepend(l);
1389
- }
1390
-
1391
- async function loadLogs() {
1392
- try {
1393
- var res = await fetch('/__brakit/api/logs');
1394
- var data = await res.json();
1395
- state.logs = data.entries;
1396
- renderLogs();
1397
- } catch(e) { console.warn('[brakit]', e); }
1398
- }
1399
-
1400
-
1401
- function buildQueryRow(q) {
1402
- var wrapper = document.createElement('div');
1403
- var row = document.createElement('div');
1404
- row.className = 'req-row query-row tel-clickable';
1405
-
1406
- var info = { op: (q.normalizedOp || q.operation || '?').toUpperCase(), table: q.table || q.model || '' };
1407
- var opColor = QUERY_OP_COLORS[info.op] || 'var(--text-dim)';
1408
- var slowCls = q.durationMs > 100 ? ' query-slow' : '';
1409
- var preview = q.sql || (info.op + ' ' + info.table);
1410
-
1411
- row.innerHTML =
1412
- '<span class="query-op" title="' + escHtml(info.op) + '" style="color:' + opColor + '">' + escHtml(info.op) + '</span>' +
1413
- '<span class="query-table" title="' + escHtml(info.table) + '">' + escHtml(info.table) + '</span>' +
1414
- '<span class="query-preview" title="' + escHtml(preview) + '">' + escHtml(preview) + '</span>' +
1415
- '<span class="query-dur' + slowCls + '">' + queryDuration(q.durationMs) + '</span>';
1416
-
1417
- var sqlText = q.sql || (info.op + ' ' + info.table);
1418
- var detail = document.createElement('div');
1419
- detail.className = 'query-detail';
1420
- detail.innerHTML = '<pre class="query-detail-sql">' + escHtml(sqlText) + '</pre><button class="query-detail-copy">Copy</button>';
1421
-
1422
- row.addEventListener('click', function() {
1423
- var wasOpen = detail.classList.contains('open');
1424
- if (wasOpen) {
1425
- detail.classList.remove('open');
1426
- row.classList.remove('expanded');
1427
- } else {
1428
- detail.classList.add('open');
1429
- row.classList.add('expanded');
1430
- }
1431
- });
1432
-
1433
- detail.querySelector('.query-detail-copy').addEventListener('click', function(e) {
1434
- e.stopPropagation();
1435
- navigator.clipboard.writeText(sqlText).then(function() { showToast('SQL copied'); });
1436
- });
1437
-
1438
- wrapper.appendChild(row);
1439
- wrapper.appendChild(detail);
1440
- return wrapper;
1441
- }
1442
-
1443
- var queryView = createTelemetryView('query-list', buildQueryRow);
1444
- function renderQueries() { queryView.render(state.queries); }
1445
- function prependQueryRow(q) { queryView.prepend(q); }
1446
-
1447
- async function loadQueries() {
1448
- try {
1449
- var res = await fetch('/__brakit/api/queries');
1450
- var data = await res.json();
1451
- state.queries = data.entries;
1452
- renderQueries();
1453
- } catch(e) { console.warn('[brakit]', e); }
1454
- }
1455
-
1456
-
1457
- var TL_TYPE_COLORS = { fetch: 'var(--blue)', log: 'var(--text-muted)', error: 'var(--red)', query: 'var(--accent)' };
1458
- var TL_TYPE_LABELS = { fetch: 'FETCH', log: 'LOG', error: 'ERROR', query: 'QUERY' };
1459
- var LOG_LEVEL_COLORS = { error: 'var(--red)', warn: 'var(--amber)', info: 'var(--blue)', debug: 'var(--text-muted)', log: 'var(--text-dim)' };
1460
-
1461
- var timelineCache = {};
1462
- var TIMELINE_CACHE_MAX = 50;
1463
-
1464
- async function loadTimeline(requestId, container, requestStartedAt) {
1465
- if (timelineCache[requestId]) {
1466
- renderTimelineContent(timelineCache[requestId], container, requestStartedAt);
1467
- return;
1468
- }
1469
-
1470
- container.classList.remove('tl-hidden');
1471
- container.innerHTML = '<div class="tl-loading">Loading activity...</div>';
1472
-
1473
- try {
1474
- var res = await fetch('/__brakit/api/activity?requestId=' + requestId);
1475
- var data = await res.json();
1476
-
1477
- var keys = Object.keys(timelineCache);
1478
- if (keys.length >= TIMELINE_CACHE_MAX) delete timelineCache[keys[0]];
1479
- timelineCache[requestId] = data;
1480
-
1481
- renderTimelineContent(data, container, requestStartedAt);
1482
- } catch(ex) {
1483
- container.innerHTML = '';
1484
- container.classList.add('tl-hidden');
1485
- }
1486
- }
1487
-
1488
- function renderTimelineContent(data, container, requestStartedAt) {
1489
- if (data.total === 0) {
1490
- container.innerHTML = '';
1491
- container.classList.add('tl-hidden');
1492
- return;
1493
- }
1494
- container.classList.remove('tl-hidden');
1495
-
1496
- var h = '<div class="tl-header">';
1497
- h += '<span class="tl-title">Activity Timeline</span>';
1498
- h += '<span class="tl-counts">';
1499
- if (data.counts.queries > 0) h += '<span class="tl-count tl-count-query">' + data.counts.queries + ' quer' + (data.counts.queries === 1 ? 'y' : 'ies') + '</span>';
1500
- if (data.counts.fetches > 0) h += '<span class="tl-count tl-count-fetch">' + data.counts.fetches + ' fetch' + (data.counts.fetches === 1 ? '' : 'es') + '</span>';
1501
- if (data.counts.logs > 0) h += '<span class="tl-count tl-count-log">' + data.counts.logs + ' log' + (data.counts.logs === 1 ? '' : 's') + '</span>';
1502
- if (data.counts.errors > 0) h += '<span class="tl-count tl-count-error">' + data.counts.errors + ' error' + (data.counts.errors === 1 ? '' : 's') + '</span>';
1503
- h += '</span></div>';
1504
- h += '<div class="tl-events">';
1505
-
1506
- var baseTs = data.timeline[0].timestamp;
1507
-
1508
- for (var i = 0; i < data.timeline.length; i++) {
1509
- var evt = data.timeline[i];
1510
- var color = TL_TYPE_COLORS[evt.type] || 'var(--text-dim)';
1511
- var label = TL_TYPE_LABELS[evt.type] || evt.type;
1512
- var relMs = Math.round(evt.timestamp - baseTs);
1513
- var relStr = '+' + formatDuration(relMs);
1514
- var isQuery = evt.type === 'query' && evt.data && evt.data.sql;
1515
-
1516
- h += '<div class="tl-event' + (isQuery ? ' tl-clickable' : '') + '"' + (isQuery ? '' : ' style="border-left-color:' + color + '"') + '>';
1517
- h += '<span class="tl-event-time">' + relStr + '</span>';
1518
- h += '<span class="tl-event-type" style="color:' + color + '">' + label + '</span>';
1519
- h += renderTimelineEvent(evt);
1520
- if (isQuery) {
1521
- h += '<div class="tl-event-sql" data-sql="' + escHtml(evt.data.sql) + '"><button class="tl-sql-copy">Copy</button>' + escHtml(evt.data.sql) + '</div>';
1522
- }
1523
- h += '</div>';
1524
- }
1525
-
1526
- h += '</div>';
1527
- container.innerHTML = h;
1528
-
1529
- container.querySelectorAll('.tl-clickable').forEach(function(el) {
1530
- el.addEventListener('click', function(e) {
1531
- e.stopPropagation();
1532
- var sqlEl = el.querySelector('.tl-event-sql');
1533
- if (sqlEl) sqlEl.classList.toggle('open');
1534
- });
1535
- });
1536
- container.querySelectorAll('.tl-sql-copy').forEach(function(btn) {
1537
- btn.addEventListener('click', function(e) {
1538
- e.stopPropagation();
1539
- var sqlEl = btn.closest('.tl-event-sql');
1540
- if (sqlEl) {
1541
- navigator.clipboard.writeText(sqlEl.getAttribute('data-sql')).then(function() { showToast('SQL copied'); });
1542
- }
1543
- });
1544
- });
1545
- }
1546
-
1547
- function renderTimelineEvent(evt) {
1548
- var d = evt.data;
1549
- if (evt.type === 'fetch') {
1550
- var sCls = d.statusCode >= 400 ? ' style="color:var(--red)"' : '';
1551
- return '<span class="tl-event-summary">' + escHtml(d.method) + ' ' + escHtml(d.url) + '</span>' +
1552
- '<span class="tl-event-status"' + sCls + '>' + d.statusCode + '</span>' +
1553
- '<span class="tl-event-dur">' + formatDuration(d.durationMs) + '</span>';
1554
- }
1555
- if (evt.type === 'query') {
1556
- var info = { op: (d.normalizedOp || d.operation || '?').toUpperCase(), table: d.table || d.model || '' };
1557
- var opColor = QUERY_OP_COLORS[info.op] || 'var(--text-dim)';
1558
- return '<span class="tl-event-summary"><span style="color:' + opColor + ';font-weight:600">' + escHtml(info.op) + '</span> ' + escHtml(info.table) + '</span>' +
1559
- '<span class="tl-event-dur">' + queryDuration(d.durationMs) + '</span>';
1560
- }
1561
- if (evt.type === 'log') {
1562
- var lColor = LOG_LEVEL_COLORS[d.level] || 'var(--text-dim)';
1563
- return '<span class="tl-event-summary"><span style="color:' + lColor + '">' + d.level.toUpperCase() + '</span> ' + escHtml(d.message) + '</span>';
1564
- }
1565
- if (evt.type === 'error') {
1566
- return '<span class="tl-event-summary" style="color:var(--red)">' + escHtml(d.name) + ': ' + escHtml(d.message) + '</span>';
1567
- }
1568
- return '';
1569
- }
1570
-
1571
- function invalidateTimelineCache(requestId) {
1572
- delete timelineCache[requestId];
1573
- }
1574
-
1575
- function refreshVisibleTimeline(requestId) {
1576
- var el = document.querySelector('.request-timeline[data-request-id="' + requestId + '"]');
1577
- if (el && el.closest('.flow-expand.open, .req-detail.open')) {
1578
- loadTimeline(requestId, el, 0);
1579
- }
1580
- }
1581
-
1582
- var timelineObserver = null;
1583
- if (window.IntersectionObserver) {
1584
- timelineObserver = new IntersectionObserver(function(entries) {
1585
- entries.forEach(function(entry) {
1586
- if (entry.isIntersecting) {
1587
- var el = entry.target;
1588
- var rid = el.getAttribute('data-request-id');
1589
- var started = parseFloat(el.getAttribute('data-request-started'));
1590
- if (rid && !el.hasAttribute('data-loaded')) {
1591
- el.setAttribute('data-loaded', '1');
1592
- loadTimeline(rid, el, started);
1593
- }
1594
- timelineObserver.unobserve(el);
1595
- }
1596
- });
1597
- }, { rootMargin: '200px' });
1598
- }
1599
-
1600
- function observeTimeline(el) {
1601
- if (timelineObserver) {
1602
- timelineObserver.observe(el);
1603
- } else {
1604
- var rid = el.getAttribute('data-request-id');
1605
- var started = parseFloat(el.getAttribute('data-request-started'));
1606
- loadTimeline(rid, el, started);
1607
- }
1608
- }
1609
-
1610
-
1611
- var graphData = null;
1612
- var selectedEndpoint = '__all__';
1613
-
1614
- var GRAPH_COLORS = ['#2563eb','#7c3aed','#16a34a','#d97706','#dc2626','#0891b2','#ea580c','#c026d3','#059669','#db2777'];
1615
-
1616
-
1617
- var HEALTH_GRADES = [
1618
- { max: 100, label: 'Fast', color: 'var(--green)', bg: 'rgba(22,163,74,0.08)', border: 'rgba(22,163,74,0.2)' },
1619
- { max: 300, label: 'Good', color: 'var(--green)', bg: 'rgba(22,163,74,0.06)', border: 'rgba(22,163,74,0.15)' },
1620
- { max: 800, label: 'OK', color: 'var(--amber)', bg: 'rgba(217,119,6,0.06)', border: 'rgba(217,119,6,0.15)' },
1621
- { max: 2000, label: 'Slow', color: 'var(--red)', bg: 'rgba(220,38,38,0.06)', border: 'rgba(220,38,38,0.15)' },
1622
- { max: Infinity, label: 'Critical', color: 'var(--red)', bg: 'rgba(220,38,38,0.08)', border: 'rgba(220,38,38,0.2)' }
1623
- ];
1624
- var DOT_COLORS = { green: '#4ade80', amber: '#fbbf24', red: '#f87171' };
1625
-
1626
- function healthGrade(ms) {
1627
- for (var i = 0; i < HEALTH_GRADES.length; i++) {
1628
- if (ms < HEALTH_GRADES[i].max) return HEALTH_GRADES[i];
1629
- }
1630
- return HEALTH_GRADES[HEALTH_GRADES.length - 1];
1631
- }
1632
-
1633
- function fmtMs(ms) {
1634
- if (ms < 1) return '<1ms';
1635
- if (ms < 1000) return Math.round(ms) + 'ms';
1636
- return (ms / 1000).toFixed(1) + 's';
1637
- }
1638
-
1639
- function dotColor(ms) {
1640
- if (ms < 300) return DOT_COLORS.green;
1641
- if (ms < 800) return DOT_COLORS.amber;
1642
- return DOT_COLORS.red;
1643
- }
1644
-
1645
- function buildMetricCard(label, value, color) {
1646
- return '<div class="perf-metric-card">' +
1647
- '<span class="perf-metric-label">' + label + '</span>' +
1648
- '<span class="perf-metric-value" style="color:' + color + '">' + value + '</span>' +
1649
- '</div>';
1650
- }
1651
-
1652
-
1653
- var HIGH_QUERY_THRESHOLD = 5;
1654
-
1655
- function renderPerfOverview(container) {
1656
- var list = document.createElement('div');
1657
- list.className = 'perf-endpoint-list';
1658
-
1659
- graphData.forEach(function(ep, idx) {
1660
- if (ep.requests.length === 0) return;
1661
- var s = ep.summary;
1662
- var g = healthGrade(s.p95Ms);
1663
- var errors = Math.round(s.errorRate * s.totalRequests);
1664
-
1665
- var card = document.createElement('div');
1666
- card.className = 'perf-endpoint-card';
1667
- card.addEventListener('click', function() { selectedEndpoint = ep.endpoint; renderGraph(); });
1668
-
1669
- var statsHtml =
1670
- '<span class="perf-ep-stat" style="color:' + g.color + '">p95: ' + fmtMs(s.p95Ms) + '</span>' +
1671
- '<span class="perf-ep-stat' + (errors > 0 ? ' perf-ep-stat-err' : '') + '">' + errors + ' err</span>' +
1672
- (s.avgQueryCount > 0 ? '<span class="perf-ep-stat' + (s.avgQueryCount > HIGH_QUERY_THRESHOLD ? ' perf-ep-stat-warn' : '') + '">' + s.avgQueryCount + ' q/req</span>' : '') +
1673
- '<span class="perf-ep-stat perf-ep-stat-muted">' + s.totalRequests + ' req' + (s.totalRequests !== 1 ? 's' : '') + '</span>';
1674
-
1675
- var ovTotal = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
1676
- var ovBarHtml = '';
1677
- if (ovTotal > 0) {
1678
- var ovDbPct = Math.round((s.avgQueryTimeMs || 0) / ovTotal * 100);
1679
- var ovFetchPct = Math.round((s.avgFetchTimeMs || 0) / ovTotal * 100);
1680
- var ovAppPct = Math.max(0, 100 - ovDbPct - ovFetchPct);
1681
- ovBarHtml =
1682
- '<div class="perf-breakdown-inline">' +
1683
- '<div class="perf-breakdown-bar perf-breakdown-bar-sm">' +
1684
- (ovDbPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + ovDbPct + '%"></div>' : '') +
1685
- (ovFetchPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + ovFetchPct + '%"></div>' : '') +
1686
- (ovAppPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + ovAppPct + '%"></div>' : '') +
1687
- '</div>' +
1688
- '<span class="perf-breakdown-labels">' +
1689
- (ovDbPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-db"></span>' + fmtMs(s.avgQueryTimeMs || 0) + '</span>' : '') +
1690
- (ovFetchPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>' + fmtMs(s.avgFetchTimeMs || 0) + '</span>' : '') +
1691
- '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-app"></span>' + fmtMs(s.avgAppTimeMs || 0) + '</span>' +
1692
- '</span>' +
1693
- '</div>';
1694
- }
1695
-
1696
- var chartId = 'inline-scatter-' + idx;
1697
-
1698
- card.innerHTML =
1699
- '<div class="perf-ep-header">' +
1700
- '<span class="perf-ep-name">' + escHtml(ep.endpoint) + '</span>' +
1701
- '<span class="perf-ep-stats">' + statsHtml + '</span>' +
1702
- '</div>' +
1703
- ovBarHtml +
1704
- '<canvas id="' + chartId + '" class="perf-inline-canvas"></canvas>';
1705
-
1706
- list.appendChild(card);
1707
-
1708
- setTimeout(function() {
1709
- var c = document.getElementById(chartId);
1710
- if (c) drawInlineScatter(c, ep.requests);
1711
- }, 0);
1712
- });
1713
-
1714
- container.appendChild(list);
1715
- }
1716
-
1717
-
1718
- function renderEndpointDetail(container) {
1719
- var ep = graphData.find(function(e) { return e.endpoint === selectedEndpoint; });
1720
- if (!ep || !ep.requests || ep.requests.length === 0) {
1721
- container.innerHTML += '<div class="empty"><span class="empty-sub">No data for this endpoint</span></div>';
1722
- return;
1723
- }
1724
-
1725
- var s = ep.summary;
1726
- var g = healthGrade(s.p95Ms);
1727
- var errors = Math.round(s.errorRate * s.totalRequests);
1728
-
1729
- var header = document.createElement('div');
1730
- header.className = 'perf-detail-header';
1731
- header.innerHTML =
1732
- '<div class="perf-detail-title">' +
1733
- '<span class="perf-badge perf-badge-lg" style="color:' + g.color + ';background:' + g.bg + ';border-color:' + g.border + '">' + g.label + '</span>' +
1734
- '<span>' + escHtml(ep.endpoint) + '</span>' +
1735
- '</div>';
1736
- container.appendChild(header);
1737
-
1738
- var metrics = document.createElement('div');
1739
- metrics.className = 'perf-metric-row';
1740
- metrics.innerHTML =
1741
- buildMetricCard('P95', fmtMs(s.p95Ms), g.color) +
1742
- buildMetricCard('Errors', errors > 0 ? errors + ' (' + Math.round(s.errorRate * 100) + '%)' : '0', errors > 0 ? 'var(--red)' : 'var(--green)') +
1743
- buildMetricCard('Queries/req', String(s.avgQueryCount), s.avgQueryCount > 5 ? 'var(--amber)' : 'var(--text)');
1744
- container.appendChild(metrics);
1745
-
1746
- var totalAvg = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
1747
- if (totalAvg > 0) {
1748
- var dbPct = Math.round((s.avgQueryTimeMs || 0) / totalAvg * 100);
1749
- var fetchPct = Math.round((s.avgFetchTimeMs || 0) / totalAvg * 100);
1750
- var appPct = Math.max(0, 100 - dbPct - fetchPct);
1751
-
1752
- var breakdown = document.createElement('div');
1753
- breakdown.className = 'perf-breakdown';
1754
-
1755
- var breakdownLabel = document.createElement('div');
1756
- breakdownLabel.className = 'perf-section-title';
1757
- breakdownLabel.textContent = 'Time Breakdown';
1758
- breakdown.appendChild(breakdownLabel);
1759
-
1760
- var bar = document.createElement('div');
1761
- bar.className = 'perf-breakdown-bar';
1762
- if (dbPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + dbPct + '%"></div>';
1763
- if (fetchPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + fetchPct + '%"></div>';
1764
- if (appPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + appPct + '%"></div>';
1765
- breakdown.appendChild(bar);
1766
-
1767
- var legend = document.createElement('div');
1768
- legend.className = 'perf-breakdown-legend';
1769
- legend.innerHTML =
1770
- '<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-db"></span>DB ' + fmtMs(s.avgQueryTimeMs || 0) + ' (' + dbPct + '%)</span>' +
1771
- '<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>Fetch ' + fmtMs(s.avgFetchTimeMs || 0) + ' (' + fetchPct + '%)</span>' +
1772
- '<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-app"></span>App ' + fmtMs(s.avgAppTimeMs || 0) + ' (' + appPct + '%)</span>';
1773
- breakdown.appendChild(legend);
1774
-
1775
- container.appendChild(breakdown);
1776
- }
1777
-
1778
- var chartWrap = document.createElement('div');
1779
- chartWrap.className = 'perf-chart-wrap';
1780
- var chartLabel = document.createElement('div');
1781
- chartLabel.className = 'perf-section-title';
1782
- chartLabel.textContent = 'Response Time';
1783
- chartWrap.appendChild(chartLabel);
1784
-
1785
- var canvas = document.createElement('canvas');
1786
- canvas.width = 800;
1787
- canvas.height = 240;
1788
- canvas.style.cssText = 'width:100%;height:240px';
1789
- canvas.className = 'perf-canvas';
1790
- chartWrap.appendChild(canvas);
1791
- container.appendChild(chartWrap);
1792
-
1793
- drawScatterChart(canvas, ep.requests);
1794
-
1795
- if (ep.requests.length > 0) {
1796
- var tableWrap = document.createElement('div');
1797
- tableWrap.className = 'perf-history-wrap';
1798
-
1799
- var colHeader = document.createElement('div');
1800
- colHeader.className = 'col-header';
1801
- colHeader.innerHTML =
1802
- '<span class="perf-col perf-col-date">Time</span>' +
1803
- '<span class="perf-col perf-col-health">Health</span>' +
1804
- '<span class="perf-col perf-col-avg">Duration</span>' +
1805
- '<span class="perf-col perf-col-breakdown">Breakdown</span>' +
1806
- '<span class="perf-col perf-col-status">Status</span>' +
1807
- '<span class="perf-col perf-col-qpr">Queries</span>';
1808
- tableWrap.appendChild(colHeader);
1809
-
1810
- var recentWithIdx = [];
1811
- for (var ri = ep.requests.length - 1; ri >= 0 && recentWithIdx.length < 50; ri--) {
1812
- recentWithIdx.push({ r: ep.requests[ri], origIdx: ri });
1813
- }
1814
- recentWithIdx.forEach(function(item) {
1815
- var r = item.r;
1816
- var rg = healthGrade(r.durationMs);
1817
- var date = new Date(r.timestamp);
1818
- var timeStr = date.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
1819
- var isError = r.statusCode >= 400;
1820
-
1821
- var row = document.createElement('div');
1822
- row.className = 'perf-hist-row' + (isError ? ' perf-hist-row-err' : '');
1823
- row.setAttribute('data-req-idx', item.origIdx);
1824
- var rDbMs = r.queryTimeMs || 0;
1825
- var rFetchMs = r.fetchTimeMs || 0;
1826
- var rAppMs = Math.max(0, r.durationMs - rDbMs - rFetchMs);
1827
- var breakdownParts = [];
1828
- if (rDbMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-db">DB ' + fmtMs(rDbMs) + '</span>');
1829
- if (rFetchMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-fetch">Fetch ' + fmtMs(rFetchMs) + '</span>');
1830
- breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-app">App ' + fmtMs(rAppMs) + '</span>');
1831
- var breakdownHtml = breakdownParts.join('');
1832
-
1833
- row.innerHTML =
1834
- '<span class="perf-col perf-col-date">' + timeStr + '</span>' +
1835
- '<span class="perf-col perf-col-health"><span class="perf-badge perf-badge-sm" style="color:' + rg.color + ';background:' + rg.bg + ';border-color:' + rg.border + '">' + rg.label + '</span></span>' +
1836
- '<span class="perf-col perf-col-avg">' + fmtMs(r.durationMs) + '</span>' +
1837
- '<span class="perf-col perf-col-breakdown">' + breakdownHtml + '</span>' +
1838
- '<span class="perf-col perf-col-status" style="color:' + (isError ? 'var(--red)' : 'var(--text-muted)') + '">' + r.statusCode + '</span>' +
1839
- '<span class="perf-col perf-col-qpr">' + r.queryCount + '</span>';
1840
- tableWrap.appendChild(row);
1841
- });
1842
- container.appendChild(tableWrap);
1843
- }
1844
- }
1845
-
1846
-
1847
- var THRESHOLD_GOOD = 300;
1848
- var THRESHOLD_OK = 800;
1849
- var CHART_PAD = { top: 16, right: 16, bottom: 28, left: 52 };
1850
-
1851
- var scatterDots = [];
1852
-
1853
- function setupCanvas(canvas) {
1854
- var ctx = canvas.getContext('2d');
1855
- if (!ctx) return null;
1856
- var dpr = window.devicePixelRatio || 1;
1857
- var w = canvas.clientWidth;
1858
- var h = canvas.clientHeight;
1859
- canvas.width = w * dpr;
1860
- canvas.height = h * dpr;
1861
- ctx.scale(dpr, dpr);
1862
- return { ctx: ctx, w: w, h: h };
1863
- }
1864
-
1865
- function reqDotColor(r) {
1866
- if (r.statusCode >= 400) return DOT_COLORS.red;
1867
- return dotColor(r.durationMs);
1868
- }
1869
-
1870
- function drawDot(ctx, x, y, radius, color) {
1871
- var r = parseInt(color.slice(1,3),16), g = parseInt(color.slice(3,5),16), b = parseInt(color.slice(5,7),16);
1872
- ctx.beginPath();
1873
- ctx.arc(x, y, radius + 2, 0, Math.PI * 2);
1874
- ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',0.25)';
1875
- ctx.fill();
1876
- ctx.beginPath();
1877
- ctx.arc(x, y, radius, 0, Math.PI * 2);
1878
- ctx.fillStyle = color;
1879
- ctx.fill();
1880
- }
1881
-
1882
- function drawErrorX(ctx, x, y, size, color, lineWidth) {
1883
- var r = parseInt(color.slice(1,3),16), g = parseInt(color.slice(3,5),16), b = parseInt(color.slice(5,7),16);
1884
- ctx.strokeStyle = 'rgba(' + r + ',' + g + ',' + b + ',0.3)';
1885
- ctx.lineWidth = lineWidth + 2;
1886
- ctx.beginPath();
1887
- ctx.moveTo(x - size, y - size); ctx.lineTo(x + size, y + size);
1888
- ctx.moveTo(x + size, y - size); ctx.lineTo(x - size, y + size);
1889
- ctx.stroke();
1890
- ctx.strokeStyle = color;
1891
- ctx.lineWidth = lineWidth;
1892
- ctx.beginPath();
1893
- ctx.moveTo(x - size, y - size); ctx.lineTo(x + size, y + size);
1894
- ctx.moveTo(x + size, y - size); ctx.lineTo(x - size, y + size);
1895
- ctx.stroke();
1896
- }
1897
-
1898
- // Maps time → x-axis, duration → y-axis as a scatter plot
1899
- function drawScatterChart(canvas, requests) {
1900
- scatterDots = [];
1901
- var setup = setupCanvas(canvas);
1902
- if (!setup) return;
1903
- var ctx = setup.ctx, w = setup.w, h = setup.h;
1904
- if (requests.length === 0) return;
1905
-
1906
- var pad = CHART_PAD;
1907
- var cw = w - pad.left - pad.right;
1908
- var ch = h - pad.top - pad.bottom;
1909
-
1910
- var maxVal = 0;
1911
- var minTime = requests[0].timestamp, maxTime = requests[0].timestamp;
1912
- requests.forEach(function(r) {
1913
- if (r.durationMs > maxVal) maxVal = r.durationMs;
1914
- if (r.timestamp < minTime) minTime = r.timestamp;
1915
- if (r.timestamp > maxTime) maxTime = r.timestamp;
1916
- });
1917
- maxVal = Math.max(maxVal, 10);
1918
- maxVal = Math.ceil(maxVal * 1.15 / 10) * 10;
1919
- var timeRange = maxTime - minTime || 1;
1920
-
1921
- ctx.strokeStyle = 'rgba(228,228,231,0.8)';
1922
- ctx.lineWidth = 1;
1923
- var gridLines = 4;
1924
- for (var gi = 0; gi <= gridLines; gi++) {
1925
- var gy = pad.top + ch - (gi / gridLines) * ch;
1926
- ctx.beginPath();
1927
- ctx.moveTo(pad.left, gy);
1928
- ctx.lineTo(pad.left + cw, gy);
1929
- ctx.stroke();
1930
- ctx.fillStyle = 'rgba(113,113,122,0.7)';
1931
- ctx.font = '10px monospace';
1932
- ctx.textAlign = 'right';
1933
- ctx.fillText(fmtMs(Math.round((gi / gridLines) * maxVal)), pad.left - 8, gy + 3);
1934
- }
1935
-
1936
- var thresholds = [
1937
- { ms: THRESHOLD_GOOD, label: fmtMs(THRESHOLD_GOOD) },
1938
- { ms: THRESHOLD_OK, label: fmtMs(THRESHOLD_OK) }
1939
- ];
1940
- thresholds.forEach(function(t) {
1941
- if (t.ms >= maxVal) return;
1942
- var ty = pad.top + ch - (t.ms / maxVal) * ch;
1943
- ctx.beginPath();
1944
- ctx.setLineDash([4, 4]);
1945
- ctx.strokeStyle = 'rgba(113,113,122,0.3)';
1946
- ctx.lineWidth = 1;
1947
- ctx.moveTo(pad.left, ty);
1948
- ctx.lineTo(pad.left + cw, ty);
1949
- ctx.stroke();
1950
- ctx.setLineDash([]);
1951
- ctx.fillStyle = 'rgba(113,113,122,0.5)';
1952
- ctx.font = '9px monospace';
1953
- ctx.textAlign = 'left';
1954
- ctx.fillText(t.label, pad.left + cw + 2, ty + 3);
1955
- });
1956
-
1957
- requests.forEach(function(r, idx) {
1958
- var x = requests.length === 1 ? pad.left + cw / 2 : pad.left + ((r.timestamp - minTime) / timeRange) * cw;
1959
- var y = pad.top + ch - (r.durationMs / maxVal) * ch;
1960
- var color = reqDotColor(r);
1961
-
1962
- scatterDots.push({ x: x, y: y, idx: idx, r: r });
1963
-
1964
- if (r.statusCode >= 400) {
1965
- drawErrorX(ctx, x, y, 4, color, 2);
1966
- } else {
1967
- drawDot(ctx, x, y, 4, color);
1968
- }
1969
- });
1970
-
1971
- ctx.fillStyle = 'rgba(113,113,122,0.7)';
1972
- ctx.font = '9px monospace';
1973
- ctx.textAlign = 'center';
1974
- var timePoints = [minTime, minTime + timeRange / 2, maxTime];
1975
- timePoints.forEach(function(t, i) {
1976
- var x = pad.left + (i / 2) * cw;
1977
- var d = new Date(t);
1978
- ctx.fillText(d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}), x, pad.top + ch + 14);
1979
- });
1980
-
1981
- canvas.style.cursor = 'pointer';
1982
- canvas.onclick = function(e) {
1983
- var rect = canvas.getBoundingClientRect();
1984
- var mx = e.clientX - rect.left;
1985
- var my = e.clientY - rect.top;
1986
- var closest = null, closestDist = Infinity;
1987
- scatterDots.forEach(function(d) {
1988
- var dist = Math.sqrt((d.x - mx) * (d.x - mx) + (d.y - my) * (d.y - my));
1989
- if (dist < closestDist) { closestDist = dist; closest = d; }
1990
- });
1991
- if (closest && closestDist < 16) {
1992
- highlightRow(closest.idx);
1993
- }
1994
- };
1995
- }
1996
-
1997
- function highlightRow(reqIdx) {
1998
- var prev = document.querySelector('.perf-hist-row-hl');
1999
- if (prev) prev.classList.remove('perf-hist-row-hl');
2000
- var row = document.querySelector('[data-req-idx="' + reqIdx + '"]');
2001
- if (row) {
2002
- row.classList.add('perf-hist-row-hl');
2003
- row.scrollIntoView({ behavior: 'smooth', block: 'center' });
2004
- }
2005
- }
2006
-
2007
- function drawInlineScatter(canvas, requests) {
2008
- var setup = setupCanvas(canvas);
2009
- if (!setup) return;
2010
- var ctx = setup.ctx, w = setup.w, h = setup.h;
2011
- if (requests.length === 0) return;
2012
-
2013
- var padX = 4, padY = 4;
2014
- var cw = w - padX * 2;
2015
- var ch = h - padY * 2;
2016
-
2017
- var maxVal = 0, minVal = Infinity;
2018
- var minTime = requests[0].timestamp, maxTime = requests[0].timestamp;
2019
- requests.forEach(function(r) {
2020
- if (r.durationMs > maxVal) maxVal = r.durationMs;
2021
- if (r.durationMs < minVal) minVal = r.durationMs;
2022
- if (r.timestamp < minTime) minTime = r.timestamp;
2023
- if (r.timestamp > maxTime) maxTime = r.timestamp;
2024
- });
2025
- maxVal = Math.max(maxVal, 10);
2026
- maxVal = Math.ceil(maxVal * 1.15 / 10) * 10;
2027
- var timeRange = maxTime - minTime || 1;
2028
-
2029
- [THRESHOLD_GOOD, THRESHOLD_OK].forEach(function(ms) {
2030
- if (ms >= maxVal) return;
2031
- var ty = padY + ch - (ms / maxVal) * ch;
2032
- ctx.beginPath();
2033
- ctx.setLineDash([2, 3]);
2034
- ctx.strokeStyle = 'rgba(113,113,122,0.15)';
2035
- ctx.lineWidth = 1;
2036
- ctx.moveTo(padX, ty);
2037
- ctx.lineTo(padX + cw, ty);
2038
- ctx.stroke();
2039
- ctx.setLineDash([]);
2040
- });
2041
-
2042
- requests.forEach(function(r) {
2043
- var x = requests.length === 1 ? padX + cw / 2 : padX + ((r.timestamp - minTime) / timeRange) * cw;
2044
- var y = padY + ch - (r.durationMs / maxVal) * ch;
2045
- var color = reqDotColor(r);
2046
-
2047
- if (r.statusCode >= 400) {
2048
- drawErrorX(ctx, x, y, 2.5, color, 1.5);
2049
- } else {
2050
- drawDot(ctx, x, y, 2.5, color);
2051
- }
2052
- });
2053
-
2054
- ctx.fillStyle = 'rgba(113,113,122,0.5)';
2055
- ctx.font = '8px monospace';
2056
- ctx.textAlign = 'right';
2057
- ctx.fillText(fmtMs(maxVal), w - 2, padY + 8);
2058
- ctx.fillText(fmtMs(0), w - 2, h - 2);
2059
- }
2060
-
2061
-
2062
- function renderGraph() {
2063
- var container = document.getElementById('graph-content');
2064
- if (!container) return;
2065
- container.innerHTML = '';
2066
-
2067
- if (!graphData || graphData.length === 0) {
2068
- container.innerHTML = '<div class="empty" style="height:300px"><span class="empty-title">No performance data yet</span><span class="empty-sub">Hit some endpoints and data will appear here</span></div>';
2069
- return;
2070
- }
2071
-
2072
- var selector = document.createElement('div');
2073
- selector.className = 'perf-selector';
2074
-
2075
- var allBtn = document.createElement('button');
2076
- allBtn.className = 'perf-selector-btn' + (selectedEndpoint === '__all__' ? ' active' : '');
2077
- allBtn.textContent = 'Overview';
2078
- allBtn.addEventListener('click', function() { selectedEndpoint = '__all__'; renderGraph(); });
2079
- selector.appendChild(allBtn);
2080
-
2081
- graphData.forEach(function(ep, idx) {
2082
- var btn = document.createElement('button');
2083
- var color = GRAPH_COLORS[idx % GRAPH_COLORS.length];
2084
- btn.className = 'perf-selector-btn' + (ep.endpoint === selectedEndpoint ? ' active' : '');
2085
- btn.innerHTML = '<span class="perf-dot" style="background:' + color + '"></span>' + escHtml(ep.endpoint);
2086
- btn.addEventListener('click', function() { selectedEndpoint = ep.endpoint; renderGraph(); });
2087
- selector.appendChild(btn);
2088
- });
2089
-
2090
- container.appendChild(selector);
2091
-
2092
- if (selectedEndpoint === '__all__') {
2093
- renderPerfOverview(container);
2094
- } else {
2095
- renderEndpointDetail(container);
2096
- }
2097
- }
2098
-
2099
- async function loadMetrics() {
2100
- try {
2101
- var res = await fetch('/__brakit/api/metrics/live');
2102
- var data = await res.json();
2103
- graphData = data.endpoints || [];
2104
- if (!selectedEndpoint || selectedEndpoint === '__all__') {
2105
- selectedEndpoint = '__all__';
2106
- }
2107
- renderGraph();
2108
- } catch(e) { console.warn('[brakit]', e); }
2109
- }
2110
-
2111
-
2112
- function renderOverview() {
2113
- var container = document.getElementById('overview-content');
2114
- if (!container) return;
2115
- container.innerHTML = '';
2116
-
2117
- var nonStatic = state.requests.filter(function(r) {
2118
- return !r.isStatic && (!r.path || r.path.indexOf('/__brakit') !== 0);
2119
- });
2120
-
2121
- var hasData = nonStatic.length > 0 || state.queries.length > 0 || state.errors.length > 0;
2122
-
2123
- if (!hasData) {
2124
- 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>';
2125
- return;
2126
- }
2127
-
2128
- var errCount = nonStatic.filter(function(r) { return r.statusCode >= 400; }).length;
2129
- var avgMs = nonStatic.length > 0 ? Math.round(nonStatic.reduce(function(s, r) { return s + r.durationMs; }, 0) / nonStatic.length) : 0;
2130
-
2131
- var summary = document.createElement('div');
2132
- summary.className = 'ov-summary';
2133
- summary.innerHTML =
2134
- '<div class="ov-stat"><span class="ov-stat-value">' + nonStatic.length + '</span><span class="ov-stat-label">Requests</span></div>' +
2135
- '<div class="ov-stat"><span class="ov-stat-value">' + state.flows.length + '</span><span class="ov-stat-label">Actions</span></div>' +
2136
- '<div class="ov-stat"><span class="ov-stat-value">' + formatDuration(avgMs) + '</span><span class="ov-stat-label">Avg Response</span></div>' +
2137
- '<div class="ov-stat"><span class="ov-stat-value">' + state.queries.length + '</span><span class="ov-stat-label">Queries</span></div>' +
2138
- (errCount > 0
2139
- ? '<div class="ov-stat"><span class="ov-stat-value" style="color:var(--red)">' + errCount + '</span><span class="ov-stat-label">Errors</span></div>'
2140
- : '<div class="ov-stat"><span class="ov-stat-value" style="color:var(--green)">' + errCount + '</span><span class="ov-stat-label">Errors</span></div>') +
2141
- '<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
2142
- container.appendChild(summary);
2143
-
2144
- var all = state.issues || [];
2145
- var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing' || si.state === 'regressed'; });
2146
- var resolved = all.filter(function(si) { return si.state === 'resolved'; });
2147
-
2148
- if (open.length === 0 && resolved.length === 0) {
2149
- var clear = document.createElement('div');
2150
- clear.className = 'ov-clear';
2151
- clear.innerHTML = '<span class="ov-clear-icon">\u2713</span>All clear — no issues detected';
2152
- container.appendChild(clear);
2153
- return;
2154
- }
2155
-
2156
- if (open.length === 0 && resolved.length > 0) {
2157
- var allFixed = document.createElement('div');
2158
- allFixed.className = 'ov-clear';
2159
- allFixed.innerHTML = '<span class="ov-clear-icon">\u2713</span>All issues resolved — ' + resolved.length + ' finding' + (resolved.length !== 1 ? 's were' : ' was') + ' detected and fixed';
2160
- container.appendChild(allFixed);
2161
- }
2162
-
2163
- var NAV_LABELS = { queries: 'Queries', requests: 'Requests', actions: 'Actions', errors: 'Errors', security: 'Security', fetches: 'Fetches', logs: 'Logs', performance: 'Performance' };
2164
- var SEV = { critical: { icon: '\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\u2139', cls: 'info', sort: 2 } };
2165
-
2166
- if (open.length > 0) {
2167
- var title = document.createElement('div');
2168
- title.className = 'ov-section-title';
2169
- title.innerHTML = 'Issues Found <span class="ov-issue-count">' + open.length + '</span>';
2170
- container.appendChild(title);
2171
-
2172
- var cards = document.createElement('div');
2173
- cards.className = 'ov-cards';
2174
-
2175
- for (var i = 0; i < open.length; i++) {
2176
- (function(si) {
2177
- var issue = si.issue;
2178
- var card = document.createElement('div');
2179
- card.className = 'ov-card';
2180
-
2181
- var sevCfg = SEV[issue.severity];
2182
- var iconCls = sevCfg.cls;
2183
- var iconChar = sevCfg.icon;
2184
-
2185
- var expandHtml = '';
2186
- if (issue.detail) expandHtml += issue.detail;
2187
- if (issue.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(issue.hint) + '</div>';
2188
- if (issue.nav) expandHtml += '<span class="ov-card-link" data-nav="' + issue.nav + '">View in ' + (NAV_LABELS[issue.nav] || issue.nav) + ' \u2192</span>';
2189
-
2190
- var aiBadge = '';
2191
- if (si.state === 'fixing' && si.aiStatus === 'fixed') {
2192
- aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>';
2193
- } else if (si.aiStatus === 'wont_fix') {
2194
- aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>';
2195
- } else if (si.state === 'regressed') {
2196
- aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
2197
- }
2198
-
2199
- var occBadge = si.occurrences > 1 ? ' <span class="sec-item-count">' + si.occurrences + 'x</span>' : '';
2200
-
2201
- card.innerHTML =
2202
- '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
2203
- '<div class="ov-card-body">' +
2204
- '<div class="ov-card-title">' + escHtml(issue.title) + occBadge + aiBadge + '</div>' +
2205
- '<div class="ov-card-desc">' + issue.desc + '</div>' +
2206
- '<div class="ov-card-expand">' + expandHtml + '</div>' +
2207
- '</div>' +
2208
- '<span class="ov-card-arrow">\u2192</span>';
2209
-
2210
- card.addEventListener('click', function(e) {
2211
- var target = e.target;
2212
- while (target && target !== card) {
2213
- if (target.classList && target.classList.contains('ov-card-link')) {
2214
- var navView = target.getAttribute('data-nav');
2215
- var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
2216
- if (sidebarItem) sidebarItem.click();
2217
- return;
2218
- }
2219
- target = target.parentElement;
2220
- }
2221
- var expand = card.querySelector('.ov-card-expand');
2222
- var arrow = card.querySelector('.ov-card-arrow');
2223
- if (card.classList.contains('expanded')) {
2224
- card.classList.remove('expanded');
2225
- expand.style.display = 'none';
2226
- arrow.textContent = '\u2192';
2227
- } else {
2228
- card.classList.add('expanded');
2229
- expand.style.display = 'block';
2230
- arrow.textContent = '\u2193';
2231
- }
2232
- });
2233
-
2234
- cards.appendChild(card);
2235
- })(open[i]);
2236
- }
2237
-
2238
- container.appendChild(cards);
2239
- }
2240
-
2241
- if (resolved.length > 0) {
2242
- var resolvedTitle = document.createElement('div');
2243
- resolvedTitle.className = 'ov-section-title ov-resolved-title';
2244
- resolvedTitle.innerHTML = '<span style="color:var(--green)">\u2713</span> Resolved <span class="ov-issue-count">' + resolved.length + '</span>';
2245
- container.appendChild(resolvedTitle);
2246
-
2247
- var resolvedCards = document.createElement('div');
2248
- resolvedCards.className = 'ov-cards';
2249
-
2250
- for (var ri = 0; ri < resolved.length; ri++) {
2251
- var rIssue = resolved[ri].issue;
2252
- var rCard = document.createElement('div');
2253
- rCard.className = 'ov-card ov-card-resolved';
2254
- rCard.innerHTML =
2255
- '<span class="ov-card-icon resolved">\u2713</span>' +
2256
- '<div class="ov-card-body">' +
2257
- '<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rIssue.title) + '</div>' +
2258
- '<div class="ov-card-desc">' + rIssue.desc + '</div>' +
2259
- '</div>';
2260
- resolvedCards.appendChild(rCard);
2261
- }
2262
-
2263
- container.appendChild(resolvedCards);
2264
- }
2265
- }
2266
-
2267
-
2268
- function renderSecurity() {
2269
- var container = document.getElementById('security-content');
2270
- if (!container) return;
2271
- container.innerHTML = '';
2272
- var SEV = { critical: { icon: '\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\u2139', cls: 'info', sort: 2 } };
2273
-
2274
- var all = (state.issues || []).slice();
2275
- var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing' || f.state === 'regressed'; });
2276
- var resolved = all.filter(function(f) { return f.state === 'resolved'; });
2277
- var stale = all.filter(function(f) { return f.state === 'stale'; });
2278
-
2279
- if (open.length === 0 && resolved.length === 0 && stale.length === 0) {
2280
- var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
2281
- if (!hasData) {
2282
- 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>';
2283
- } else {
2284
- 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>';
2285
- }
2286
- return;
2287
- }
2288
-
2289
- var critCount = 0, warnCount = 0, infoCount = 0;
2290
- for (var ci = 0; ci < open.length; ci++) {
2291
- var sev = open[ci].issue.severity;
2292
- if (sev === 'critical') critCount++;
2293
- else if (sev === 'info') infoCount++;
2294
- else warnCount++;
2295
- }
2296
-
2297
- var summaryEl = document.createElement('div');
2298
- summaryEl.className = 'sec-summary';
2299
- summaryEl.innerHTML =
2300
- '<div class="sec-summary-left">' +
2301
- '<span class="sec-summary-count">' + open.length + '</span>' +
2302
- '<span class="sec-summary-label">open issue' + (open.length !== 1 ? 's' : '') + '</span>' +
2303
- (resolved.length > 0 ? '<span class="sec-resolved-badge">' + resolved.length + ' resolved</span>' : '') +
2304
- '</div>' +
2305
- '<div class="sec-summary-right">' +
2306
- (critCount > 0 ? '<span class="sec-badge critical">' + critCount + ' critical</span>' : '') +
2307
- (warnCount > 0 ? '<span class="sec-badge warning">' + warnCount + ' warning</span>' : '') +
2308
- (infoCount > 0 ? '<span class="sec-badge info">' + infoCount + ' info</span>' : '') +
2309
- '</div>';
2310
- container.appendChild(summaryEl);
2311
-
2312
- if (open.length === 0 && resolved.length > 0) {
2313
- var allFixed = document.createElement('div');
2314
- allFixed.className = 'sec-clear';
2315
- 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>';
2316
- container.appendChild(allFixed);
2317
- }
2318
-
2319
- if (open.length > 0) {
2320
- var groups = {};
2321
- var groupOrder = [];
2322
- for (var gi = 0; gi < open.length; gi++) {
2323
- var sf = open[gi];
2324
- var f = sf.issue;
2325
- if (!groups[f.rule]) {
2326
- groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
2327
- groupOrder.push(f.rule);
2328
- }
2329
- groups[f.rule].items.push(sf);
2330
- }
2331
-
2332
- groupOrder.sort(function(a, b) {
2333
- var sa = SEV[groups[a].severity].sort;
2334
- var sb = SEV[groups[b].severity].sort;
2335
- if (sa !== sb) return sa - sb;
2336
- return groups[b].items.length - groups[a].items.length;
2337
- });
2338
-
2339
- for (var oi = 0; oi < groupOrder.length; oi++) {
2340
- var group = groups[groupOrder[oi]];
2341
- var section = document.createElement('div');
2342
- section.className = 'sec-group';
2343
-
2344
- var sevCfg = SEV[group.severity];
2345
- var iconCls = sevCfg.cls;
2346
- var iconChar = sevCfg.icon;
2347
-
2348
- var header = document.createElement('div');
2349
- header.className = 'sec-group-header';
2350
- header.innerHTML =
2351
- '<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
2352
- '<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
2353
- '<span class="sec-group-count">' + group.items.length + '</span>';
2354
- section.appendChild(header);
2355
-
2356
- if (group.hint) {
2357
- var hintEl = document.createElement('div');
2358
- hintEl.className = 'sec-hint';
2359
- hintEl.textContent = group.hint;
2360
- section.appendChild(hintEl);
2361
- }
2362
-
2363
- var list = document.createElement('div');
2364
- list.className = 'sec-items';
2365
- for (var ii = 0; ii < group.items.length; ii++) {
2366
- var sf2 = group.items[ii];
2367
- var item = sf2.issue;
2368
- var row = document.createElement('div');
2369
- row.className = 'sec-item';
2370
- var aiBadge = '';
2371
- if (sf2.state === 'fixing' && sf2.aiStatus === 'fixed') {
2372
- aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>';
2373
- } else if (sf2.aiStatus === 'wont_fix') {
2374
- aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>';
2375
- } else if (sf2.state === 'regressed') {
2376
- aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
2377
- }
2378
- var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
2379
- var occBadge = sf2.occurrences > 1 ? '<span class="sec-item-count">' + sf2.occurrences + 'x</span>' : '';
2380
- row.innerHTML =
2381
- '<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
2382
- occBadge +
2383
- aiBadge + aiNotes;
2384
- list.appendChild(row);
2385
- }
2386
- section.appendChild(list);
2387
- container.appendChild(section);
2388
- }
2389
- }
2390
-
2391
- if (resolved.length > 0) {
2392
- var resolvedTitle = document.createElement('div');
2393
- resolvedTitle.className = 'sec-resolved-title';
2394
- resolvedTitle.innerHTML = '<span class="sec-resolved-check">\u2713</span> Resolved <span class="sec-resolved-count">' + resolved.length + '</span>';
2395
- container.appendChild(resolvedTitle);
2396
-
2397
- var resolvedGroup = document.createElement('div');
2398
- resolvedGroup.className = 'sec-group sec-group-resolved';
2399
- var resolvedItems = document.createElement('div');
2400
- resolvedItems.className = 'sec-items';
2401
- for (var ri = 0; ri < resolved.length; ri++) {
2402
- var rsf = resolved[ri];
2403
- var rf = rsf.issue;
2404
- var rRow = document.createElement('div');
2405
- rRow.className = 'sec-item sec-item-resolved';
2406
- var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
2407
- var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
2408
- rRow.innerHTML =
2409
- '<span class="sec-resolved-item-icon">\u2713</span>' +
2410
- '<div class="sec-item-desc">' + escHtml(rf.title) + ' \u2014 ' + escHtml(rf.endpoint || 'global') + '</div>' +
2411
- verifiedBadge + rNotes;
2412
- resolvedItems.appendChild(rRow);
2413
- }
2414
- resolvedGroup.appendChild(resolvedItems);
2415
- container.appendChild(resolvedGroup);
2416
- }
2417
-
2418
- if (stale.length > 0) {
2419
- var staleTitle = document.createElement('div');
2420
- staleTitle.className = 'sec-resolved-title';
2421
- staleTitle.innerHTML = '<span style="color:var(--text-muted)">\u23F8</span> Stale <span class="sec-resolved-count">' + stale.length + '</span>';
2422
- container.appendChild(staleTitle);
2423
-
2424
- var staleGroup = document.createElement('div');
2425
- staleGroup.className = 'sec-group sec-group-resolved';
2426
- var staleItems = document.createElement('div');
2427
- staleItems.className = 'sec-items';
2428
- for (var sti = 0; sti < stale.length; sti++) {
2429
- var ssf = stale[sti];
2430
- var sf3 = ssf.issue;
2431
- var sRow = document.createElement('div');
2432
- sRow.className = 'sec-item sec-item-resolved';
2433
- sRow.innerHTML =
2434
- '<span style="color:var(--text-muted)">\u23F8</span>' +
2435
- '<div class="sec-item-desc" style="color:var(--text-muted)">' + escHtml(sf3.title) + ' \u2014 endpoint inactive</div>';
2436
- staleItems.appendChild(sRow);
2437
- }
2438
- staleGroup.appendChild(staleItems);
2439
- container.appendChild(staleGroup);
2440
- }
2441
- }
2442
-
2443
-
2444
- var VIEW_CONTAINERS = {
2445
- overview: 'overview-container',
2446
- actions: 'flow-container',
2447
- requests: 'request-container',
2448
- fetches: 'fetch-container',
2449
- queries: 'query-container',
2450
- errors: 'error-container',
2451
- logs: 'log-container',
2452
- performance: 'performance-container',
2453
- security: 'security-container'
2454
- };
2455
- var VIEW_TITLES = {
2456
- overview: 'Overview',
2457
- actions: 'Actions',
2458
- requests: 'Requests',
2459
- fetches: 'Server Fetches',
2460
- queries: 'Queries',
2461
- errors: 'Errors',
2462
- logs: 'Logs',
2463
- performance: 'Performance',
2464
- security: 'Security'
2465
- };
2466
- var VIEW_SUBTITLES = {
2467
- overview: 'Live summary of your application',
2468
- actions: 'User actions captured as sequences of HTTP requests',
2469
- requests: 'All HTTP requests proxied through brakit',
2470
- fetches: 'Outbound HTTP calls made by your server to external services',
2471
- queries: 'Database queries executed during request handling',
2472
- errors: 'Unhandled exceptions and errors thrown by your application',
2473
- logs: 'Console output from your application',
2474
- performance: 'Endpoint health and response time trends',
2475
- security: 'Security findings and recommendations'
2476
- };
2477
-
2478
- async function init() {
2479
- try {
2480
- var res = await fetch('/__brakit/api/flows');
2481
- var data = await res.json();
2482
- state.flows = data.flows;
2483
- renderFlows();
2484
- } catch(e) { console.error('Failed to load flows', e); }
2485
-
2486
- try {
2487
- var res2 = await fetch('/__brakit/api/requests');
2488
- var data2 = await res2.json();
2489
- state.requests = data2.requests;
2490
- renderRequests();
2491
- } catch(e) { console.warn('[brakit]', e); }
2492
-
2493
- await Promise.all([loadFetches(), loadErrors(), loadLogs(), loadQueries(), loadMetrics()]);
2494
-
2495
- try {
2496
- var res3 = await fetch('/__brakit/api/insights');
2497
- var data3 = await res3.json();
2498
- state.issues = data3.issues || [];
2499
- } catch(e) { console.warn('[brakit]', e); }
2500
-
2501
- updateStats();
2502
- renderOverview();
2503
-
2504
- var events = new EventSource('/__brakit/api/events');
2505
- var reloadTimer = null;
2506
- var perfReloadTimer = null;
2507
- events.onmessage = function(e) {
2508
- var req = JSON.parse(e.data);
2509
- if (req.path && req.path.startsWith('/__brakit')) return;
2510
- state.requests.unshift(req);
2511
- if (state.requests.length > 1000) state.requests.pop();
2512
- clearTimeout(reloadTimer);
2513
- reloadTimer = setTimeout(reloadFlows, 300);
2514
- prependRequestRow(req);
2515
- updateStats();
2516
- if (state.activeView === 'performance') {
2517
- clearTimeout(perfReloadTimer);
2518
- perfReloadTimer = setTimeout(loadMetrics, 500);
2519
- }
2520
- };
2521
-
2522
- function registerTelemetryListener(eventName, stateKey, prependFn) {
2523
- events.addEventListener(eventName, function(e) {
2524
- var item = JSON.parse(e.data);
2525
- state[stateKey].unshift(item);
2526
- if (state[stateKey].length > 1000) state[stateKey].pop();
2527
- prependFn(item);
2528
- updateStats();
2529
- if (item.parentRequestId) { invalidateTimelineCache(item.parentRequestId); refreshVisibleTimeline(item.parentRequestId); }
2530
- });
2531
- }
2532
- registerTelemetryListener('fetch', 'fetches', prependFetchRow);
2533
- registerTelemetryListener('log', 'logs', prependLogRow);
2534
- registerTelemetryListener('error_event', 'errors', prependErrorRow);
2535
- registerTelemetryListener('query', 'queries', prependQueryRow);
2536
-
2537
- events.addEventListener('issues', function(e) {
2538
- state.issues = JSON.parse(e.data);
2539
- if (state.activeView === 'overview') renderOverview();
2540
- if (state.activeView === 'security') renderSecurity();
2541
- updateStats();
2542
- });
2543
-
2544
- window.addEventListener('beforeunload', function() {
2545
- events.close();
2546
- clearTimeout(reloadTimer);
2547
- clearTimeout(perfReloadTimer);
2548
- });
2549
- }
2550
-
2551
- async function reloadFlows() {
2552
- try {
2553
- var res = await fetch('/__brakit/api/flows');
2554
- var data = await res.json();
2555
- state.flows = data.flows;
2556
- renderFlows();
2557
- updateStats();
2558
- } catch(e) { console.warn('[brakit]', e); }
2559
- }
2560
-
2561
- function switchView(view) {
2562
- Object.keys(VIEW_CONTAINERS).forEach(function(v) {
2563
- var el = document.getElementById(VIEW_CONTAINERS[v]);
2564
- if (el) el.style.display = v === view ? 'block' : 'none';
2565
- });
2566
- }
2567
-
2568
- var sidebarItems = document.querySelectorAll('.sidebar-item:not(.disabled)');
2569
- sidebarItems.forEach(function(item) {
2570
- item.addEventListener('click', function() {
2571
- var view = item.getAttribute('data-view');
2572
- if (!view || view === state.activeView) return;
2573
- sidebarItems.forEach(function(i) { i.classList.remove('active'); });
2574
- item.classList.add('active');
2575
- state.activeView = view;
2576
- fetch('/__brakit/api/tab?tab=' + encodeURIComponent(view)).catch(function(){});
2577
- document.getElementById('header-title').textContent = VIEW_TITLES[view] || view;
2578
- document.getElementById('header-sub').textContent = VIEW_SUBTITLES[view] || '';
2579
- document.getElementById('mode-toggle').style.display = view === 'actions' ? 'flex' : 'none';
2580
- if (view === 'overview') renderOverview();
2581
- if (view === 'security') renderSecurity();
2582
- if (view === 'performance') loadMetrics();
2583
- switchView(view);
2584
- });
2585
- });
2586
-
2587
- document.getElementById('mode-simple').addEventListener('click', function() {
2588
- state.viewMode = 'simple';
2589
- document.getElementById('mode-simple').classList.add('active');
2590
- document.getElementById('mode-detailed').classList.remove('active');
2591
- collapseAll('.flow-row', '.flow-expand');
2592
- });
2593
- document.getElementById('mode-detailed').addEventListener('click', function() {
2594
- state.viewMode = 'detailed';
2595
- document.getElementById('mode-detailed').classList.add('active');
2596
- document.getElementById('mode-simple').classList.remove('active');
2597
- collapseAll('.flow-row', '.flow-expand');
2598
- });
2599
-
2600
- function updateStats() {
2601
- var reqs = state.requests.filter(function(r) { return !r.path || !r.path.startsWith('/__brakit'); });
2602
- var errors = reqs.filter(function(r) { return r.statusCode >= 400; }).length;
2603
- var avg = reqs.length > 0 ? Math.round(reqs.reduce(function(s,r) { return s + r.durationMs; }, 0) / reqs.length) : 0;
2604
- document.getElementById('stat-total').textContent = reqs.length + ' request' + (reqs.length !== 1 ? 's' : '');
2605
- document.getElementById('stat-flows').textContent = state.flows.length + ' action' + (state.flows.length !== 1 ? 's' : '');
2606
- document.getElementById('stat-errors').textContent = errors + ' error' + (errors !== 1 ? 's' : '');
2607
- document.getElementById('stat-avg').textContent = 'Avg: ' + avg + 'ms';
2608
- var actionCount = document.getElementById('sidebar-count-actions');
2609
- var requestCount = document.getElementById('sidebar-count-requests');
2610
- var fetchCount = document.getElementById('sidebar-count-fetches');
2611
- var errorCount = document.getElementById('sidebar-count-errors');
2612
- var logCount = document.getElementById('sidebar-count-logs');
2613
- var queryCount = document.getElementById('sidebar-count-queries');
2614
- if (actionCount) actionCount.textContent = state.flows.length;
2615
- if (requestCount) requestCount.textContent = reqs.length;
2616
- if (fetchCount) fetchCount.textContent = state.fetches.length;
2617
- if (errorCount) errorCount.textContent = state.errors.length;
2618
- if (logCount) logCount.textContent = state.logs.length;
2619
- if (queryCount) queryCount.textContent = state.queries.length;
2620
- var secCount = document.getElementById('sidebar-count-security');
2621
- if (secCount) {
2622
- var numIssues = (state.issues || []).filter(function(f) { return f.state !== 'resolved' && f.state !== 'stale'; }).length;
2623
- secCount.textContent = numIssues;
2624
- secCount.style.display = numIssues > 0 ? '' : 'none';
2625
- }
2626
- }
2627
-
2628
- function copyAsCurl(req) {
2629
- var headers = Object.entries(req.headers || {})
2630
- .filter(function(e) { return ['host', 'connection', 'accept-encoding'].indexOf(e[0]) === -1; })
2631
- .map(function(e) { return "-H '" + e[0] + ": " + e[1] + "'"; })
2632
- .join(' ');
2633
- var body = req.requestBody ? " -d '" + req.requestBody.replace(/'/g, "'\\''") + "'" : '';
2634
- var curl = "curl -X " + req.method + " " + headers + body + " 'http://localhost:" + PORT + req.url + "'";
2635
- navigator.clipboard.writeText(curl).then(function() { showToast('Copied cURL command'); });
2636
- }
2637
-
2638
- document.getElementById('clear-btn').addEventListener('click', async function() {
2639
- if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
2640
- await fetch('/__brakit/api/clear', {method: 'POST'});
2641
- state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
2642
- state.issues = [];
2643
- graphData = []; selectedEndpoint = '__all__'; timelineCache = {};
2644
- renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
2645
- showToast('Cleared');
2646
- });
2647
-
2648
- init();
2649
-
2650
- })();
2651
- </script>
1493
+ <div class="tl-events">${this.renderTimeline(t.timeline,s)}</div>
1494
+ `}renderTimeline(t,s){let r=new Map,o=[];for(let l of t){let c=l.type==="query"?l.data.parentFetchId:void 0;if(l.type==="query"&&c){let p=r.get(c);p||(p=[],r.set(c,p)),p.push(l);}else o.push(l);}let n=0;return o.map(l=>{let c=n++,p=l.type==="fetch"?l.data.fetchId:void 0,h=p?r.get(p):void 0;if(h&&h.length>0){let m=h.length;return a`
1495
+ ${this.renderEvent(l,c,s)}
1496
+ <div class="tl-nested">
1497
+ <span class="tl-nested-label">${m} nested quer${m===1?"y":"ies"}</span>
1498
+ ${h.map(b=>{let $=n++;return this.renderEvent(b,$,s,true)})}
1499
+ </div>
1500
+ `}return this.renderEvent(l,c,s)})}renderEvent(t,s,r,o=false){let n=ts[t.type]||"var(--text-dim)",l=es[t.type]||t.type,c="+"+v(Math.round(t.timestamp-r)),p=t.type==="query"?t.data.sql:void 0,h=!!p,m=this.expandedSqlIdx===s;return a`
1501
+ <div class="tl-event ${h?"tl-clickable":""} ${o?"tl-nested-event":""}"
1502
+ style="${h?"":`border-left-color:${n}`}"
1503
+ @click=${h?b=>this.toggleSql(s,b):d}>
1504
+ <span class="tl-event-time">${c}</span>
1505
+ <span class="tl-event-type" style="color:${n}">${l}</span>
1506
+ ${this.renderEventContent(t)}
1507
+ ${p?a`
1508
+ <div class="tl-event-sql ${m?"open":""}">
1509
+ <button class="tl-sql-copy" @click=${b=>this.copySql(p,b)}>Copy</button>
1510
+ ${p}
1511
+ </div>`:d}
1512
+ </div>
1513
+ `}renderEventContent(t){switch(t.type){case "fetch":{let s=t.data,r=s.statusCode>=400;return a`
1514
+ <span class="tl-event-summary">${s.method} ${s.url}</span>
1515
+ <span class="tl-event-status" style="${r?"color:var(--red)":""}">${s.statusCode}</span>
1516
+ <span class="tl-event-dur">${v(s.durationMs)}</span>
1517
+ `}case "query":{let s=t.data,r=(s.normalizedOp||s.operation||"?").toUpperCase(),o=s.table||s.model||"",n=Ft[r]||"var(--text-dim)";return a`
1518
+ <span class="tl-event-summary"><span style="color:${n};font-weight:600">${r}</span> ${o}</span>
1519
+ <span class="tl-event-dur">${_r(s.durationMs)}</span>
1520
+ `}case "log":{let s=t.data,r=Ke[s.level]||"var(--text-dim)";return a`<span class="tl-event-summary"><span style="color:${r}">${s.level.toUpperCase()}</span> ${s.message}</span>`}case "error":{let s=t.data;return a`<span class="tl-event-summary" style="color:var(--red)">${s.name}: ${s.message}</span>`}default:return d}}};w.cache=new Map,u([R({context:x})],w.prototype,"store",2),u([T({attribute:"request-id"})],w.prototype,"requestId",2),u([T({attribute:"request-started",type:Number})],w.prototype,"requestStarted",2),u([_()],w.prototype,"data",2),u([_()],w.prototype,"loading",2),u([_()],w.prototype,"failed",2),u([_()],w.prototype,"expandedSqlIdx",2),w=u([g("bk-timeline-panel")],w);var Wt=class{constructor(e,t){this.host=e;this.store=t;this.retryCount=0;e.addController(this);}hostConnected(){this.connect();}hostDisconnected(){this.eventSource?.close(),clearTimeout(this.reloadTimer),clearTimeout(this.perfReloadTimer),clearTimeout(this.reconnectTimer);}connect(){this.eventSource?.close(),this.eventSource=new EventSource(y.events),this.eventSource.onopen=()=>{this.retryCount=0;},this.eventSource.onerror=()=>{this.eventSource?.close(),this.scheduleReconnect();},this.eventSource.onmessage=e=>{let t=JSON.parse(e.data);t.path?.startsWith(k)||(this.store.prependRequest(t),clearTimeout(this.reloadTimer),this.reloadTimer=setTimeout(()=>this.reloadFlows(),300),this.store.state.activeView==="performance"&&(clearTimeout(this.perfReloadTimer),this.perfReloadTimer=setTimeout(()=>this.reloadMetrics(),ue)));},this.eventSource.addEventListener(ee,e=>{this.store.prependFetch(JSON.parse(e.data));}),this.eventSource.addEventListener("log",e=>{this.store.prependLog(JSON.parse(e.data));}),this.eventSource.addEventListener(se,e=>{this.store.prependError(JSON.parse(e.data));}),this.eventSource.addEventListener(re,e=>{this.store.prependQuery(JSON.parse(e.data));}),this.eventSource.addEventListener(oe,e=>{this.store.setIssues(JSON.parse(e.data));});}scheduleReconnect(){if(this.retryCount>=10)return;let e=Math.min(1e3*2**this.retryCount,3e4);this.retryCount++,this.reconnectTimer=setTimeout(()=>this.connect(),e);}async reloadFlows(){try{let t=await(await fetch(y.flows)).json();this.store.setFlows(t.flows);}catch{}}async reloadMetrics(){try{let t=await(await fetch(y.metricsLive)).json();this.store.setMetrics(t.endpoints||[]);}catch{}}};function gs(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`}function bs(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`}function Es(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`}function _s(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>`}function Ss(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`}function $s(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`}function ys(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>`}function Ts(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`}function xs(){return a`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>`}var X=class extends f{constructor(){super(...arguments);this.store=new Gt;this.activeView="overview";this.viewMode="simple";this.sse=new Wt(this,this.store);}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.loadInitialData(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}async loadInitialData(){try{let[t,s]=await Promise.all([fetch(y.flows),fetch(y.requests)]),[r,o]=await Promise.all([t.json(),s.json()]);this.store.setFlows(r.flows),this.store.setRequests(o.requests);}catch(t){console.warn("[brakit]",t);}try{let[t,s,r,o,n]=await Promise.all([fetch(y.fetches),fetch(y.errors),fetch(y.logs),fetch(y.queries),fetch(y.metricsLive)]),[l,c,p,h,m]=await Promise.all([t.json(),s.json(),r.json(),o.json(),n.json()]);this.store.setFetches(l.entries),this.store.setErrors(c.entries),this.store.setLogs(p.entries),this.store.setQueries(h.entries),this.store.setMetrics(m.endpoints||[]);}catch(t){console.warn("[brakit]",t);}try{let s=await(await fetch(y.insights)).json();this.store.setIssues(s.issues||[]);}catch(t){console.warn("[brakit]",t);}}switchView(t){t!==this.activeView&&(this.activeView=t,this.store.setActiveView(t),fetch(`${y.tab}?tab=${encodeURIComponent(t)}`).catch(()=>{}),t==="performance"&&this.sse.reloadMetrics());}async handleClear(){confirm("This will clear all data including performance metrics history. Continue?")&&(await fetch(y.clear,{method:"POST"}),this.store.clearAll(),C.show("Cleared"));}handleCopyAsCurl(t){ct(t);}render(){let t=this.store.state,s=t.requests.filter(c=>!c.path?.startsWith(k)),r=s.filter(c=>c.statusCode>=400).length,o=s.length>0?Math.round(s.reduce((c,p)=>c+p.durationMs,0)/s.length):0,n=(t.issues||[]).filter(c=>c.state!=="resolved"&&c.state!=="stale").length,l=window.__BRAKIT_CONFIG__;return a`
1521
+ <div class="app" id="app">
1522
+ <aside class="sidebar">
1523
+ <div class="sidebar-logo">
1524
+ <span class="logo-text">brakit</span>
1525
+ <span class="logo-version">v${l?.version??""}</span>
1526
+ </div>
1527
+ <nav class="sidebar-nav">
1528
+ ${this.renderSidebarItem("overview","Overview",gs(),void 0)}
1529
+ <div class="sidebar-section">Monitor</div>
1530
+ ${this.renderSidebarItem("actions","Actions",bs(),t.flows.length)}
1531
+ ${this.renderSidebarItem("requests","Requests",Es(),s.length)}
1532
+ ${this.renderSidebarItem("fetches","Fetches",_s(),t.fetches.length)}
1533
+ <div class="sidebar-section">Insights</div>
1534
+ ${this.renderSidebarItem("queries","Queries",Ss(),t.queries.length)}
1535
+ ${this.renderSidebarItem("errors","Errors",$s(),t.errors.length)}
1536
+ ${this.renderSidebarItem("logs","Logs",ys(),t.logs.length)}
1537
+ ${this.renderSidebarItem("security","Security",Ts(),n,n===0)}
1538
+ ${this.renderSidebarItem("performance","Performance",xs(),void 0)}
1539
+ </nav>
1540
+ <div class="sidebar-footer">:${l?.port??""}</div>
1541
+ </aside>
1542
+ <div class="main-panel">
1543
+ <div class="header">
1544
+ <div class="header-left">
1545
+ <span class="header-title" id="header-title">${ie[this.activeView]||this.activeView}</span>
1546
+ <span class="header-sub" id="header-sub">${ne[this.activeView]||""}</span>
1547
+ </div>
1548
+ <div class="header-right">
1549
+ ${this.activeView==="actions"?a`
1550
+ <div class="segmented-control" id="mode-toggle">
1551
+ <button class="segmented-btn ${this.viewMode==="simple"?"active":""}" @click=${()=>{this.viewMode="simple",this.store.setViewMode("simple");}}>Quick</button>
1552
+ <button class="segmented-btn ${this.viewMode==="detailed"?"active":""}" @click=${()=>{this.viewMode="detailed",this.store.setViewMode("detailed");}}>Detailed</button>
1553
+ </div>
1554
+ `:d}
1555
+ <button class="btn btn-danger" @click=${this.handleClear}>Clear</button>
1556
+ </div>
1557
+ </div>
1558
+ <div class="main-content">
1559
+ <div id="overview-container" style="display:${this.activeView==="overview"?"block":"none"}">
1560
+ <bk-overview-view></bk-overview-view>
1561
+ </div>
1562
+ <div class="view-flows" id="flow-container" style="display:${this.activeView==="actions"?"block":"none"}">
1563
+ <bk-flows-view></bk-flows-view>
1564
+ </div>
1565
+ <div class="view-requests" id="request-container" style="display:${this.activeView==="requests"?"block":"none"}">
1566
+ <bk-requests-view></bk-requests-view>
1567
+ </div>
1568
+ <div class="view-telemetry" id="fetch-container" style="display:${this.activeView==="fetches"?"block":"none"}">
1569
+ <bk-fetches-view></bk-fetches-view>
1570
+ </div>
1571
+ <div class="view-telemetry" id="query-container" style="display:${this.activeView==="queries"?"block":"none"}">
1572
+ <bk-queries-view></bk-queries-view>
1573
+ </div>
1574
+ <div class="view-telemetry" id="error-container" style="display:${this.activeView==="errors"?"block":"none"}">
1575
+ <bk-errors-view></bk-errors-view>
1576
+ </div>
1577
+ <div class="view-telemetry" id="log-container" style="display:${this.activeView==="logs"?"block":"none"}">
1578
+ <bk-logs-view></bk-logs-view>
1579
+ </div>
1580
+ <div class="view-telemetry" id="security-container" style="display:${this.activeView==="security"?"block":"none"}">
1581
+ <bk-security-view></bk-security-view>
1582
+ </div>
1583
+ <div class="view-telemetry" id="performance-container" style="display:${this.activeView==="performance"?"block":"none"}">
1584
+ <bk-performance-view></bk-performance-view>
1585
+ </div>
1586
+ </div>
1587
+ <div class="footer">
1588
+ <span id="stat-total">${s.length} request${s.length!==1?"s":""}</span>
1589
+ <span id="stat-flows">${t.flows.length} action${t.flows.length!==1?"s":""}</span>
1590
+ <span id="stat-errors" class="error-count">${r} error${r!==1?"s":""}</span>
1591
+ <span id="stat-avg">Avg: ${o}ms</span>
1592
+ </div>
1593
+ </div>
1594
+ </div>
1595
+ <bk-toast></bk-toast>
1596
+ `}renderSidebarItem(t,s,r,o,n=false){return a`
1597
+ <button class="sidebar-item ${this.activeView===t?"active":""}" @click=${()=>this.switchView(t)}>
1598
+ <span class="item-icon">${r}</span>
1599
+ <span class="item-label">${s}</span>
1600
+ ${o!==void 0?a`<span class="item-count" style="display:${n?"none":""}">${o}</span>`:d}
1601
+ </button>
1602
+ `}};u([be({context:x})],X.prototype,"store",2),u([_()],X.prototype,"activeView",2),u([_()],X.prototype,"viewMode",2),X=u([g("bk-dashboard")],X);
1603
+ /*! Bundled license information:
1604
+
1605
+ @lit/reactive-element/css-tag.js:
1606
+ (**
1607
+ * @license
1608
+ * Copyright 2019 Google LLC
1609
+ * SPDX-License-Identifier: BSD-3-Clause
1610
+ *)
1611
+
1612
+ @lit/reactive-element/reactive-element.js:
1613
+ lit-html/lit-html.js:
1614
+ lit-element/lit-element.js:
1615
+ @lit/reactive-element/decorators/custom-element.js:
1616
+ @lit/reactive-element/decorators/property.js:
1617
+ @lit/reactive-element/decorators/state.js:
1618
+ @lit/reactive-element/decorators/event-options.js:
1619
+ @lit/reactive-element/decorators/base.js:
1620
+ @lit/reactive-element/decorators/query.js:
1621
+ @lit/reactive-element/decorators/query-all.js:
1622
+ @lit/reactive-element/decorators/query-async.js:
1623
+ @lit/reactive-element/decorators/query-assigned-nodes.js:
1624
+ @lit/context/lib/decorators/provide.js:
1625
+ (**
1626
+ * @license
1627
+ * Copyright 2017 Google LLC
1628
+ * SPDX-License-Identifier: BSD-3-Clause
1629
+ *)
1630
+
1631
+ lit-html/is-server.js:
1632
+ @lit/context/lib/decorators/consume.js:
1633
+ (**
1634
+ * @license
1635
+ * Copyright 2022 Google LLC
1636
+ * SPDX-License-Identifier: BSD-3-Clause
1637
+ *)
1638
+
1639
+ @lit/reactive-element/decorators/query-assigned-elements.js:
1640
+ @lit/context/lib/context-request-event.js:
1641
+ @lit/context/lib/create-context.js:
1642
+ @lit/context/lib/controllers/context-consumer.js:
1643
+ @lit/context/lib/value-notifier.js:
1644
+ @lit/context/lib/controllers/context-provider.js:
1645
+ @lit/context/lib/context-root.js:
1646
+ (**
1647
+ * @license
1648
+ * Copyright 2021 Google LLC
1649
+ * SPDX-License-Identifier: BSD-3-Clause
1650
+ *)
1651
+ */})();</script>
2652
1652
  </body>
2653
1653
  </html>