brakit 0.8.6 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,7 @@
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
24
  --shadow-sm:0 1px 2px rgba(0,0,0,0.05);
@@ -94,6 +95,11 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
94
95
 
95
96
  /* Content */
96
97
  .main-content{flex:1;overflow-y:auto}
98
+ bk-dashboard{display:contents}
99
+ 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}
100
+ bk-method-badge,bk-status-pill,bk-duration-label,bk-copy-button{display:inline-flex;flex-shrink:0}
101
+ bk-stat-card{display:inline-flex}
102
+ bk-toast{display:block;position:fixed;top:0;left:0;right:0;z-index:100;pointer-events:none}
97
103
 
98
104
  /* Column headers */
99
105
  .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 +121,8 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
115
121
  .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
116
122
  .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
123
  .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)}
124
+ .flow-badge-pill.badge-warn{background:var(--amber-bg);color:var(--amber)}
125
+ .flow-badge-pill.badge-error{background:var(--red-bg);color:var(--red)}
120
126
  .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
121
127
 
122
128
  /* Flow expand panel */
@@ -135,23 +141,23 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
135
141
  /* Method badges */
136
142
  .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
143
  .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)}
144
+ .method-badge-POST{background:var(--blue-bg);color:var(--blue)}
145
+ .method-badge-PUT,.method-badge-PATCH{background:var(--amber-bg);color:var(--amber)}
146
+ .method-badge-DELETE{background:var(--red-bg);color:var(--red)}
141
147
  .method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
142
148
 
143
149
  /* Status pills */
144
150
  .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
151
  .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)}
152
+ .status-pill-3xx{background:var(--cyan-bg);color:var(--cyan)}
153
+ .status-pill-4xx{background:var(--amber-bg);color:var(--amber)}
154
+ .status-pill-5xx{background:var(--red-bg);color:var(--red)}
149
155
 
150
156
  .traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
151
157
  .traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
152
158
  .traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
153
159
  .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}
160
+ .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
161
 
156
162
  /* Body toggles */
157
163
  .traffic-body{padding:0;margin-top:8px}
@@ -180,7 +186,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
180
186
  .flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
181
187
  .flow-subreq .subreq-status{flex-shrink:0}
182
188
  .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}
189
+ .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
190
  .flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
185
191
  .flow-subreq-detail.open{display:block}
186
192
 
@@ -210,6 +216,41 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
210
216
  .strict-mode-dupe{opacity:0.55}
211
217
  .strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
212
218
 
219
+ /* Flow detail tabs */
220
+ .flow-detail-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
221
+ .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}
222
+ .flow-tab:hover{color:var(--text)}
223
+ .flow-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
224
+
225
+ /* Waterfall chart — request bars on time axis, sub-events as text rows */
226
+ .flow-waterfall{padding:0;font-family:var(--mono);font-size:11px}
227
+ .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}
228
+ .wf-rows{display:flex;flex-direction:column;gap:0}
229
+
230
+ /* Request group — request bar + its sub-events */
231
+ .wf-request-group{border-bottom:1px solid var(--border-subtle);padding:2px 0}
232
+ .wf-request-group:last-child{border-bottom:none}
233
+
234
+ /* Request row — label | bar on time axis | duration */
235
+ .wf-req-row{display:flex;align-items:center;gap:0;height:24px;transition:background .1s}
236
+ .wf-req-row:hover{background:var(--bg-hover)}
237
+ .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}
238
+ .wf-bar-track{flex:1;position:relative;height:14px;min-width:0;overflow:hidden}
239
+ .wf-bar{position:absolute;top:1px;height:12px;border-radius:3px;opacity:0.8;min-width:3px}
240
+ .wf-req-row:hover .wf-bar{opacity:1}
241
+ .wf-req-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-muted);font-size:10px;padding-left:8px}
242
+
243
+ /* Sub-event rows — same layout as request rows: label | bar track | duration */
244
+ .wf-sub-row{display:flex;align-items:center;gap:0;height:20px;transition:background .1s}
245
+ .wf-sub-row:hover{background:var(--bg-hover)}
246
+ .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}
247
+ .wf-sub-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
248
+ .wf-sub-bar-sized{height:8px !important;top:3px !important;opacity:0.65}
249
+ .wf-sub-row:hover .wf-sub-bar-sized{opacity:0.9}
250
+ .wf-sub-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-dim);font-size:9px;padding-left:8px}
251
+
252
+ .wf-loading{color:var(--text-muted);padding:12px 0;font-size:11px;font-family:var(--mono)}
253
+
213
254
  /* Request rows */
214
255
  .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
256
  .req-row:hover{background:var(--bg-hover)}
@@ -267,17 +308,23 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
267
308
  .fetch-stat-value{font-size:17px;font-weight:700;font-family:var(--mono);color:var(--text)}
268
309
  .fetch-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
269
310
  .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}
311
+ .fetch-groups{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}
312
+ .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
313
  .fetch-group:hover{border-color:var(--border-light);box-shadow:var(--shadow-md)}
273
314
  .fetch-group-header{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:13px}
274
315
  .fetch-group-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;color:var(--text)}
275
316
  .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)}
317
+ .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
318
  .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}
319
+ .fetch-group-sep{color:var(--text-muted);font-size:9px}
320
+ .fetch-group-ok{color:var(--green)}
280
321
  .fetch-group-err{color:var(--red)}
322
+ .fetch-group-timeline{display:flex;align-items:center;gap:6px;margin-top:4px;font-size:10px;color:var(--text-muted);font-family:var(--mono)}
323
+ .fetch-group-timeline-dot{width:6px;height:6px;border-radius:50%;background:var(--blue);opacity:.5;flex-shrink:0}
324
+ .fetch-group-timeline-range{letter-spacing:.3px}
325
+ .fetch-group-callers{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:10px;flex-wrap:wrap}
326
+ .fetch-group-callers-label{color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}
327
+ .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
328
 
282
329
  /* Performance tab */
283
330
  .perf-selector{display:flex;gap:6px;flex-wrap:wrap;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
@@ -438,9 +485,8 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
438
485
  .sec-hint{padding:8px 16px;font-size:11px;color:var(--text-muted);background:var(--bg-muted);border-bottom:1px solid var(--border)}
439
486
 
440
487
  /* 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)}
488
+ .sec-items{padding:2px 0}
489
+ .sec-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:12px;flex-wrap:wrap}
444
490
  .sec-item-desc{color:var(--text-dim);line-height:1.5;flex:1;min-width:0}
445
491
  .sec-item-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
446
492
  .sec-item-count{font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0;margin-left:12px}
@@ -465,2189 +511,1028 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
465
511
  .sec-ai-verified{background:rgba(22,163,74,.1);color:var(--green)}
466
512
  .sec-ai-notes{font-size:11px;color:var(--text-muted);font-style:italic;margin-top:2px;padding-left:0}
467
513
 
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)}
514
+ .request-timeline {
515
+ margin-top: 8px;
516
+ background: var(--bg-muted);
517
+ border: 1px solid var(--border);
518
+ border-radius: var(--radius);
519
+ padding: 10px 14px;
520
+ overflow: hidden;
521
+ }
522
+
523
+ .request-timeline.tl-hidden { display: none; }
524
+
525
+ .tl-header {
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: space-between;
529
+ padding: 0 0 8px;
530
+ }
531
+
532
+ .tl-title {
533
+ font-size: 10px;
534
+ text-transform: uppercase;
535
+ letter-spacing: .8px;
536
+ color: var(--text-muted);
537
+ font-weight: 600;
538
+ }
539
+
540
+ .tl-counts {
541
+ display: flex;
542
+ gap: 10px;
543
+ font-family: var(--mono);
544
+ font-size: 10px;
545
+ }
546
+
547
+ .tl-count { color: var(--text-dim); }
548
+ .tl-count-query { color: var(--accent); }
549
+ .tl-count-fetch { color: var(--blue); }
550
+ .tl-count-error { color: var(--red); }
551
+ .tl-count-log { color: var(--text-muted); }
552
+
553
+ .tl-loading {
554
+ color: var(--text-muted);
555
+ padding: 4px 0;
556
+ font-size: 10px;
557
+ font-family: var(--mono);
558
+ }
559
+
560
+ .tl-events {
561
+ position: relative;
562
+ padding-left: 4px;
563
+ }
564
+
565
+ .tl-event {
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 10px;
569
+ font-family: var(--mono);
570
+ font-size: 11px;
571
+ padding: 4px 0 4px 12px;
572
+ border-left: 2px solid var(--border);
573
+ position: relative;
574
+ flex-wrap: wrap;
575
+ }
576
+
577
+ .tl-event-time {
578
+ width: 48px;
579
+ color: var(--text-muted);
580
+ font-size: 10px;
581
+ flex-shrink: 0;
582
+ text-align: right;
583
+ }
584
+
585
+ .tl-event-type {
586
+ width: 44px;
587
+ font-weight: 700;
588
+ font-size: 9px;
589
+ letter-spacing: .5px;
590
+ flex-shrink: 0;
591
+ }
592
+
593
+ .tl-event-summary {
594
+ flex: 1;
595
+ overflow: hidden;
596
+ text-overflow: ellipsis;
597
+ white-space: nowrap;
598
+ color: var(--text);
599
+ }
600
+
601
+ .tl-event-status {
602
+ width: 32px;
603
+ text-align: right;
604
+ font-weight: 600;
605
+ flex-shrink: 0;
606
+ }
607
+
608
+ .tl-event-dur {
609
+ width: 48px;
610
+ text-align: right;
611
+ color: var(--text-muted);
612
+ flex-shrink: 0;
613
+ font-size: 10px;
614
+ }
615
+
616
+ .tl-event.tl-clickable {
617
+ cursor: pointer;
618
+ border-radius: 6px;
619
+ margin-left: -4px;
620
+ padding: 6px 12px;
621
+ border-left: none;
622
+ border: 1px solid var(--border);
623
+ background: var(--bg-card);
624
+ }
625
+
626
+ .tl-event.tl-clickable:hover {
627
+ background: var(--bg-hover);
628
+ border-color: var(--border-light);
629
+ }
630
+
631
+ .tl-event-sql {
632
+ width: 100%;
633
+ display: none;
634
+ margin: 4px 0 2px 0;
635
+ padding: 8px 10px;
636
+ background: var(--bg-card);
637
+ border: 1px solid var(--border);
638
+ border-radius: var(--radius-sm);
639
+ font-size: 10px;
640
+ line-height: 1.5;
641
+ white-space: pre-wrap;
642
+ word-break: break-word;
643
+ color: var(--text-dim);
644
+ overflow-x: auto;
645
+ max-height: 150px;
646
+ overflow-y: auto;
647
+ position: relative;
648
+ }
649
+
650
+ .tl-event-sql.open { display: block; }
651
+
652
+ .tl-sql-copy {
653
+ position: absolute;
654
+ top: 6px;
655
+ right: 6px;
656
+ padding: 2px 8px;
657
+ font-size: 9px;
658
+ font-family: var(--mono);
659
+ background: var(--bg-muted);
660
+ border: 1px solid var(--border);
661
+ border-radius: var(--radius-sm);
662
+ color: var(--text-muted);
663
+ cursor: pointer;
664
+ transition: all .15s;
665
+ }
666
+
667
+ .tl-sql-copy:hover {
668
+ background: var(--bg-hover);
669
+ color: var(--text);
670
+ border-color: var(--border-light);
671
+ }
672
+
673
+ .tl-nested {
674
+ margin-left: 24px;
675
+ padding-left: 10px;
676
+ border-left: 2px dashed var(--blue);
677
+ margin-top: 2px;
678
+ margin-bottom: 2px;
679
+ position: relative;
680
+ }
681
+
682
+ .tl-nested::before {
683
+ content: "";
684
+ position: absolute;
685
+ top: 12px;
686
+ left: -2px;
687
+ width: 8px;
688
+ height: 0;
689
+ border-top: 2px dashed var(--blue);
690
+ }
691
+
692
+ .tl-nested-label {
693
+ display: block;
694
+ font-family: var(--mono);
695
+ font-size: 9px;
696
+ color: var(--blue);
697
+ letter-spacing: .3px;
698
+ padding: 2px 0 4px;
699
+ opacity: .7;
700
+ }
701
+
702
+ .tl-nested-event {
703
+ opacity: .9;
704
+ font-size: 10px;
705
+ }
493
706
  </style>
494
707
  </head>
495
708
  <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>
709
+ <bk-dashboard></bk-dashboard>
710
+ <script>window.__BRAKIT_CONFIG__={port:{{PORT}},version:"{{VERSION}}"};</script>
711
+ <script>(function(){'use strict';var Ls=Object.defineProperty;var Ms=Object.getOwnPropertyDescriptor;var h=(i,e,t,s)=>{for(var r=s>1?void 0:s?Ms(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&&Ls(e,t,r),r};var Lt=globalThis,kt=Lt.ShadowRoot&&(Lt.ShadyCSS===void 0||Lt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Re=Symbol(),xe=new WeakMap,Mt=class{constructor(e,t,s){if(this._$cssResult$=true,s!==Re)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(kt&&e===void 0){let s=t!==void 0&&t.length===1;s&&(e=xe.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),s&&xe.set(t,e));}return e}toString(){return this.cssText}},we=i=>new Mt(typeof i=="string"?i:i+"",void 0,Re);var Ae=(i,e)=>{if(kt)i.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let s=document.createElement("style"),r=Lt.litNonce;r!==void 0&&s.setAttribute("nonce",r),s.textContent=t.cssText,i.appendChild(s);}},Yt=kt?i=>i:i=>i instanceof CSSStyleSheet?(e=>{let t="";for(let s of e.cssRules)t+=s.cssText;return we(t)})(i):i;var{is:ks,defineProperty:Os,getOwnPropertyDescriptor:Ds,getOwnPropertyNames:Ns,getOwnPropertySymbols:Hs,getPrototypeOf:qs}=Object,D=globalThis,Ce=D.trustedTypes,Ps=Ce?Ce.emptyScript:"",Us=D.reactiveElementPolyfillSupport,ht=(i,e)=>i,mt={toAttribute(i,e){switch(e){case Boolean:i=i?Ps: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}},Ot=(i,e)=>!ks(i,e),Ie={attribute:true,type:String,converter:mt,reflect:false,useDefault:false,hasChanged:Ot};Symbol.metadata??(Symbol.metadata=Symbol("metadata")),D.litPropertyMetadata??(D.litPropertyMetadata=new WeakMap);var k=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=Ie){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&&Os(this.prototype,e,r);}}static getPropertyDescriptor(e,t,s){let{get:r,set:o}=Ds(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)??Ie}static _$Ei(){if(this.hasOwnProperty(ht("elementProperties")))return;let e=qs(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=[...Ns(t),...Hs(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(Yt(r));}else e!==void 0&&t.push(Yt(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 Ae(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??Ot)(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){}};k.elementStyles=[],k.shadowRootOptions={mode:"open"},k[ht("elementProperties")]=new Map,k[ht("finalized")]=new Map,Us?.({ReactiveElement:k}),(D.reactiveElementVersions??(D.reactiveElementVersions=[])).push("2.1.2");var ft=globalThis,Le=i=>i,Dt=ft.trustedTypes,Me=Dt?Dt.createPolicy("lit-html",{createHTML:i=>i}):void 0,qe="$lit$",N=`lit$${Math.random().toFixed(9).slice(2)}$`,Pe="?"+N,Fs=`<${Pe}>`,j=document,gt=()=>j.createComment(""),bt=i=>i===null||typeof i!="object"&&typeof i!="function",ee=Array.isArray,Bs=i=>ee(i)||typeof i?.[Symbol.iterator]=="function",Xt=`[
712
+ \f\r]`,vt=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,ke=/-->/g,Oe=/>/g,G=RegExp(`>|${Xt}(?:([^\\s"'>=/]+)(${Xt}*=${Xt}*(?:[^
713
+ \f\r"'\`<>=]|("|')|))|$)`,"g"),De=/'/g,Ne=/"/g,Ue=/^(?:script|style|textarea|title)$/i,se=i=>(e,...t)=>({_$litType$:i,strings:e,values:t}),a=se(1),Q=Symbol.for("lit-noChange"),d=Symbol.for("lit-nothing"),He=new WeakMap,W=j.createTreeWalker(j,129);function Fe(i,e){if(!ee(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return Me!==void 0?Me.createHTML(e):e}var Gs=(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,u,m=-1,b=0;for(;b<c.length&&(n.lastIndex=b,u=n.exec(c),u!==null);)b=n.lastIndex,n===vt?u[1]==="!--"?n=ke:u[1]!==void 0?n=Oe:u[2]!==void 0?(Ue.test(u[2])&&(r=RegExp("</"+u[2],"g")),n=G):u[3]!==void 0&&(n=G):n===G?u[0]===">"?(n=r??vt,m=-1):u[1]===void 0?m=-2:(m=n.lastIndex-u[2].length,p=u[1],n=u[3]===void 0?G:u[3]==='"'?Ne:De):n===Ne||n===De?n=G:n===ke||n===Oe?n=vt:(n=G,r=void 0);let $=n===G&&i[l+1].startsWith("/>")?" ":"";o+=n===vt?c+Fs:m>=0?(s.push(p),c.slice(0,m)+qe+c.slice(m)+N+$):c+N+(m===-2?l:$);}return [Fe(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,u]=Gs(e,t);if(this.el=i.createElement(p,s),W.currentNode=this.el.content,t===2||t===3){let m=this.el.content.firstChild;m.replaceWith(...m.childNodes);}for(;(r=W.nextNode())!==null&&c.length<l;){if(r.nodeType===1){if(r.hasAttributes())for(let m of r.getAttributeNames())if(m.endsWith(qe)){let b=u[n++],$=r.getAttribute(m).split(N),v=/([.?@])?(.*)/.exec(b);c.push({type:1,index:o,name:v[2],strings:$,ctor:v[1]==="."?zt:v[1]==="?"?Jt:v[1]==="@"?Zt:tt}),r.removeAttribute(m);}else m.startsWith(N)&&(c.push({type:6,index:o}),r.removeAttribute(m));if(Ue.test(r.tagName)){let m=r.textContent.split(N),b=m.length-1;if(b>0){r.textContent=Dt?Dt.emptyScript:"";for(let $=0;$<b;$++)r.append(m[$],gt()),W.nextNode(),c.push({type:2,index:++o});r.append(m[b],gt());}}}else if(r.nodeType===8)if(r.data===Pe)c.push({type:2,index:o});else {let m=-1;for(;(m=r.data.indexOf(N,m+1))!==-1;)c.push({type:7,index:o}),m+=N.length-1;}o++;}}static createElement(e,t){let s=j.createElement("template");return s.innerHTML=e,s}};function Z(i,e,t=i,s){if(e===Q)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=Z(i,r._$AS(i,e.values),r,s)),e}var Kt=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??j).importNode(t,true);W.currentNode=r;let o=W.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 te(o,this,e)),this._$AV.push(p),c=s[++l];}n!==c?.index&&(o=W.nextNode(),n++);}return W.currentNode=j,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=Z(this,e,t),bt(e)?e===d||e==null||e===""?(this._$AH!==d&&this._$AR(),this._$AH=d):e!==this._$AH&&e!==Q&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):Bs(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(j.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(Fe(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===r)this._$AH.p(t);else {let o=new Kt(r,this),n=o.u(this.options);o.p(t),this.T(n),this._$AH=o;}}_$AC(e){let t=He.get(e.strings);return t===void 0&&He.set(e.strings,t=new Et(e)),t}k(e){ee(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=Le(e).nextSibling;Le(e).remove(),e=s;}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e));}},tt=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=Z(this,e,t,0),n=!bt(e)||e!==this._$AH&&e!==Q,n&&(this._$AH=e);else {let l=e,c,p;for(e=o[0],c=0;c<o.length-1;c++)p=Z(this,l[s+c],t,c),p===Q&&(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??"");}},zt=class extends tt{constructor(){super(...arguments),this.type=3;}j(e){this.element[this.name]=e===d?void 0:e;}},Jt=class extends tt{constructor(){super(...arguments),this.type=4;}j(e){this.element.toggleAttribute(this.name,!!e&&e!==d);}},Zt=class extends tt{constructor(e,t,s,r,o){super(e,t,s,r,o),this.type=5;}_$AI(e,t=this){if((e=Z(this,e,t,0)??d)===Q)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);}},te=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){Z(this,e);}};var Ws=ft.litHtmlPolyfillSupport;Ws?.(Et,_t),(ft.litHtmlVersions??(ft.litHtmlVersions=[])).push("3.3.2");var Be=(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 k{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=Be(t,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return Q}};f._$litElement$=true,f.finalized=true,St.litElementHydrateSupport?.({LitElement:f});var js=St.litElementPolyfillSupport;js?.({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 Qs={attribute:true,type:String,converter:mt,reflect:false,hasChanged:Ot},Vs=(i=Qs,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"?Vs(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 E(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>`}};h([T()],$t.prototype,"method",2),$t=h([g("bk-method-badge")],$t);var I="/__brakit/api",O="/__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 et="polling",Ht="static",Ys="auth-handshake",Xs="auth-check",Ks="middleware",Tt={[Ys]:1,[Xs]:1,[Ks]:1};var re="fetch";var ie="error_event",oe="query",ne="issues";var ae={overview:"Overview",queries:"Queries",requests:"Requests",actions:"Actions",errors:"Errors",security:"Security",fetches:"Fetches",logs:"Logs",performance:"Performance"};var yt={overview:"Overview",actions:"Actions",requests:"Requests",fetches:"Server Fetches",queries:"Queries",errors:"Errors",logs:"Logs",performance:"Performance",security:"Security"},le={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 de=100,V=300,Y=800,pe=2e3,ue=100,he=50,me=500;var H="__all__",Ft={SELECT:"var(--blue)",INSERT:"var(--green)",UPDATE:"var(--amber)",DELETE:"var(--red)",COUNT:"var(--text-muted)"},ze={error:"var(--red)",warn:"var(--amber)",info:"var(--blue)",debug:"var(--text-muted)",log:"var(--text-dim)"},fe=["#2563eb","#7c3aed","#16a34a","#d97706","#dc2626","#0891b2","#ea580c","#c026d3","#059669","#db2777"],xt={green:"#4ade80",amber:"#fbbf24",red:"#f87171"},Bt=[{max:de,label:"Fast",color:"var(--green)",bg:"rgba(22,163,74,0.08)",border:"rgba(22,163,74,0.2)"},{max:V,label:"Good",color:"var(--green)",bg:"rgba(22,163,74,0.06)",border:"rgba(22,163,74,0.15)"},{max:Y,label:"OK",color:"var(--amber)",bg:"rgba(217,119,6,0.06)",border:"rgba(217,119,6,0.15)"},{max:pe,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)"}],Je="rgba(228,228,231,0.8)",ge="rgba(113,113,122,0.7)",Ze="10px monospace",be="9px monospace",ts="8px monospace",es={top:16,right:16,bottom:28,left:52},ss={fetch:"var(--blue)",log:"var(--text-muted)",error:"var(--red)",query:"var(--accent)"},rs={fetch:"FETCH",log:"LOG",error:"ERROR",query:"QUERY"},is=new Set(["cookie","set-cookie","authorization","proxy-authorization","x-api-key","x-auth-token"]),os={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"},ns=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 S(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 as(i){return os[i]||(i>=500?"Server Error":i>=400?"Client Error":"OK")}function zs(i,e){if(is.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(zs(e,t))).join(`
714
+ `)}function X(i){if(!i)return '<span style="color:var(--text-muted)">No body</span>';try{let e=JSON.parse(i);return Js(JSON.stringify(e,null,2))}catch{return P(i)}}function Js(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 Rt=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>`}};h([T({type:Number})],Rt.prototype,"code",2),Rt=h([g("bk-status-pill")],Rt);var wt=class extends f{constructor(){super(...arguments);this.ms=0;}createRenderRoot(){return this}render(){return a`<span class="req-duration">${S(this.ms)}</span>`}};h([T({type:Number})],wt.prototype,"ms",2),wt=h([g("bk-duration-label")],wt);var it=class extends f{constructor(){super(...arguments);this.title="";this.subtitle="";}createRenderRoot(){return this}render(){return a`
715
+ <div class="empty">
716
+ <span class="empty-title">${this.title}</span>
717
+ <span class="empty-sub">${this.subtitle}</span>
718
+ </div>
719
+ `}};h([T()],it.prototype,"title",2),h([T()],it.prototype,"subtitle",2),it=h([g("bk-empty-state")],it);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>`}};h([E()],C.prototype,"message",2),h([E()],C.prototype,"visible",2),C=h([g("bk-toast")],C);var K=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>`}};h([T()],K.prototype,"text",2),h([T()],K.prototype,"label",2),h([T({attribute:"toast-message"})],K.prototype,"toastMessage",2),K=h([g("bk-copy-button")],K);var z=class extends f{constructor(){super(...arguments);this.value="";this.label="";this.color="";}createRenderRoot(){return this}render(){return a`
720
+ <div class="fetch-stat">
721
+ <span class="fetch-stat-value" style="color:${this.color}">${this.value}</span>
722
+ <span class="fetch-stat-label">${this.label}</span>
723
+ </div>
724
+ `}};h([T()],z.prototype,"value",2),h([T()],z.prototype,"label",2),h([T()],z.prototype,"color",2),z=h([g("bk-stat-card")],z);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 ot=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 Gt=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 Ee=class extends Event{constructor(e,t){super("context-provider",{bubbles:true,composed:true}),this.context=e,this.contextTarget=t;}},nt=class extends Gt{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 Ee(this.context,this.host));}};function _e({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 ot(this,{context:i,callback:r=>{t.set.call(this,r);},subscribe:e});})):t.constructor.addInitializer((r=>{new ot(r,{context:i,callback:o=>{r[s]=o;},subscribe:e});}));}}var x="dashboard-store",Wt=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`
725
+ <div
726
+ class="req-row tel-clickable ${o?"expanded":""}"
727
+ @click=${()=>this.toggleError(s)}
728
+ >
729
+ <span class="tel-error-name" title=${t.name}>${t.name}</span>
730
+ <span class="tel-message" title=${t.message}>${t.message}</span>
731
+ <span class="tel-timestamp">${r}</span>
732
+ </div>
733
+ ${o&&t.stack?a`<div class="error-stack">${t.stack}</div>`:d}
734
+ `}render(){let t=this.store.state.errors;return t.length===0?a`<bk-empty-state
735
+ title="No errors"
736
+ subtitle="No errors have been captured yet"
737
+ ></bk-empty-state>`:a`
738
+ <div class="col-header">
739
+ <span style="width:180px">Type</span>
740
+ <span style="flex:1">Message</span>
741
+ <span style="width:130px;text-align:right">Time</span>
742
+ </div>
743
+ <div id="error-list">
744
+ ${t.map((s,r)=>this.renderErrorRow(s,r))}
745
+ </div>
746
+ `}};h([R({context:x})],at.prototype,"store",2),h([E()],at.prototype,"expandedIdx",2),at=h([g("bk-errors-view")],at);var At=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`
747
+ <div id="log-analysis">
748
+ <div class="fetch-summary">
749
+ <bk-stat-card value=${String(e.length)} label="Total Logs"></bk-stat-card>
750
+ ${t.error>0?a`<bk-stat-card value=${String(t.error)} label="Errors" color="var(--red)"></bk-stat-card>`:d}
751
+ ${t.warn>0?a`<bk-stat-card value=${String(t.warn)} label="Warnings" color="var(--amber)"></bk-stat-card>`:d}
752
+ <bk-stat-card value=${String(t.info)} label="Info"></bk-stat-card>
753
+ ${t.debug>0?a`<bk-stat-card value=${String(t.debug)} label="Debug"></bk-stat-card>`:d}
754
+ ${t.log>0?a`<bk-stat-card value=${String(t.log)} label="Log"></bk-stat-card>`:d}
755
+ </div>
756
+ </div>
757
+ `}renderLogRow(e){let t=new Date(e.timestamp).toLocaleTimeString();return a`
758
+ <div class="req-row">
759
+ <span class="tel-level tel-level-${e.level}">${e.level.toUpperCase()}</span>
760
+ <span class="tel-message tel-mono" title=${e.message}>${e.message}</span>
761
+ <span class="tel-timestamp">${t}</span>
762
+ </div>
763
+ `}render(){let e=this.store.state.logs;return e.length===0?a`<bk-empty-state
764
+ title="No logs"
765
+ subtitle="No console output has been captured yet"
766
+ ></bk-empty-state>`:a`
767
+ ${this.renderAnalysis(e)}
768
+ <div class="col-header">
769
+ <span style="width:52px">Level</span>
770
+ <span style="flex:1">Message</span>
771
+ <span style="width:130px;text-align:right">Time</span>
772
+ </div>
773
+ <div id="log-list">
774
+ ${e.map(t=>this.renderLogRow(t))}
775
+ </div>
776
+ `}};h([R({context:x})],At.prototype,"store",2),At=h([g("bk-logs-view")],At);var tr=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 ls(i){let e=i.trim().match(/^(\w+)/);return e?e[1].toUpperCase():"?"}function cs(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 ds(i){return P(i).replace(/\b\w+\b/g,e=>tr.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":S(t)}getQueryInfo(t){let s=(t.normalizedOp||t.operation||(t.sql?ls(t.sql):"?")).toUpperCase(),r=t.table||t.model||(t.sql?cs(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>ue,p=t.sql||r+" "+o,u=this.expandedIdx===s;return a`
777
+ <div>
778
+ <div
779
+ class="req-row query-row tel-clickable ${u?"expanded":""}"
780
+ @click=${()=>this.toggleQuery(s)}
781
+ >
782
+ <span class="query-op" title=${r} style="color:${l}">${r}</span>
783
+ <span class="query-table" title=${o}>${o}</span>
784
+ <span class="query-preview" title=${p}>${p}</span>
785
+ <span class="query-dur${c?" query-slow":""}">${this.queryDuration(t.durationMs)}</span>
786
+ </div>
787
+ <div class="query-detail ${u?"open":""}">
788
+ ${u?a`
789
+ <pre class="query-detail-sql" .innerHTML=${ds(n)}></pre>
790
+ <bk-copy-button .text=${n} label="Copy"></bk-copy-button>
791
+ `:d}
792
+ </div>
793
+ </div>
794
+ `}render(){let t=this.store.state.queries;return t.length===0?a`<bk-empty-state
795
+ title="No queries"
796
+ subtitle="No database queries have been captured yet"
797
+ ></bk-empty-state>`:a`
798
+ <div class="col-header">
799
+ <span style="width:70px;border-right:1px solid var(--border);padding-right:16px">Operation</span>
800
+ <span style="width:170px;border-right:1px solid var(--border);padding-right:16px">Table</span>
801
+ <span style="flex:1;border-right:1px solid var(--border);padding-right:16px">Query</span>
802
+ <span style="width:60px;text-align:right">Time</span>
803
+ </div>
804
+ <div id="query-list">
805
+ ${t.map((s,r)=>this.renderQueryRow(s,r))}
806
+ </div>
807
+ `}};h([R({context:x})],lt.prototype,"store",2),h([E()],lt.prototype,"expandedIdx",2),lt=h([g("bk-queries-view")],lt);function Se(i){return i.replace(/'/g,"'\\''")}function er(i,e){let t=Object.entries(i.headers||{}).filter(([o])=>!ns.has(o)).map(([o,n])=>`-H '${Se(o)}: ${Se(n)}'`).join(" "),s=i.requestBody?` -d '${Se(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=er(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`
808
+ <div class="detail-meta">
809
+ <span><bk-method-badge .method=${t.method}></bk-method-badge> ${t.url}</span>
810
+ <span><bk-status-pill .code=${t.statusCode}></bk-status-pill></span>
811
+ <span>${t.durationMs}ms</span>
812
+ ${t.responseSize?a`<span>${U(t.responseSize)}</span>`:d}
813
+ </div>
814
+ <div class="request-timeline tl-hidden" data-request-id=${t.id} data-request-started=${String(t.startedAt)}></div>
815
+ <div class="detail-grid">
816
+ <div class="detail-section"><h4>Request Headers</h4><pre .innerHTML=${rt(t.headers)}></pre></div>
817
+ <div class="detail-section"><h4>Response Headers</h4><pre .innerHTML=${rt(t.responseHeaders)}></pre></div>
818
+ <div class="detail-section"><h4>Request Body</h4><pre .innerHTML=${X(t.requestBody)}></pre></div>
819
+ <div class="detail-section"><h4>Response Body</h4><pre .innerHTML=${X(t.responseBody)}></pre></div>
820
+ </div>
821
+ <div class="detail-actions">
822
+ <button class="btn btn-curl" @click=${s=>this.handleCopyAsCurl(t,s)}>Copy cURL</button>
823
+ </div>
824
+ `}renderRequestRow(t){let s=this.expandedId===t.id;return a`
825
+ <div class="req-row ${s?"expanded":""}" @click=${()=>this.toggleRequest(t.id)}>
826
+ <div class="req-summary">
827
+ <bk-method-badge .method=${t.method}></bk-method-badge>
828
+ <span class="req-url">${t.url}</span>
829
+ <bk-status-pill .code=${t.statusCode}></bk-status-pill>
830
+ <bk-duration-label .ms=${t.durationMs}></bk-duration-label>
831
+ <span class="req-size">${U(t.responseSize)}</span>
832
+ </div>
833
+ </div>
834
+ <div class="req-detail ${s?"open":""}">${s?this.renderDetail(t):d}</div>
835
+ `}render(){let t=this.store.state.requests.filter(s=>!s.path?.startsWith(O));return t.length===0?a`<bk-empty-state title="No requests" subtitle="No HTTP requests have been captured yet"></bk-empty-state>`:a`
836
+ <div class="col-header">
837
+ <span style="width:60px">Method</span>
838
+ <span style="flex:1">URL</span>
839
+ <span style="width:36px;text-align:right">Status</span>
840
+ <span style="width:70px;text-align:right">Time</span>
841
+ <span style="width:60px;text-align:right">Size</span>
842
+ </div>
843
+ <div id="request-list">${t.map(s=>this.renderRequestRow(s))}</div>
844
+ `}};h([R({context:x})],dt.prototype,"store",2),h([E()],dt.prototype,"expandedId",2),dt=h([g("bk-requests-view")],dt);var Ct=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`
845
+ <div class="fetch-summary">
846
+ <bk-stat-card value=${String(e.length)} label="Total Fetches"></bk-stat-card>
847
+ <bk-stat-card value=${String(t.size)} label="Unique URLs"></bk-stat-card>
848
+ <bk-stat-card value=${String(s)} label="Errors" color=${s>0?"var(--red)":""}></bk-stat-card>
849
+ <bk-stat-card value=${S(o)} label="Avg Duration"></bk-stat-card>
850
+ </div>
851
+ `}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`
852
+ <div class="fetch-group">
853
+ <div class="fetch-group-header">
854
+ <bk-method-badge .method=${e.method}></bk-method-badge>
855
+ <span class="fetch-group-url" title=${e.url}>${e.url}</span>
856
+ ${n>0?a`<bk-status-pill .code=${n}></bk-status-pill>`:d}
857
+ <span class="fetch-group-count">${e.count}x</span>
858
+ </div>
859
+ <div class="fetch-group-meta">
860
+ <span>avg ${S(t)}</span>
861
+ <span class="fetch-group-sep">\u00b7</span>
862
+ <span>max ${S(e.maxDur)}</span>
863
+ <span class="fetch-group-sep">\u00b7</span>
864
+ ${s>0?a`<span class="fetch-group-err">${s}% errors</span>`:a`<span class="fetch-group-ok">0% errors</span>`}
865
+ </div>
866
+ ${e.firstTs>0?a`
867
+ <div class="fetch-group-timeline">
868
+ <span class="fetch-group-timeline-dot"></span>
869
+ <span class="fetch-group-timeline-range">
870
+ ${this.formatTime(e.firstTs)}${e.firstTs!==e.lastTs?a` \u2192 ${this.formatTime(e.lastTs)}`:d}
871
+ </span>
872
+ </div>`:d}
873
+ ${r.length>0?a`
874
+ <div class="fetch-group-callers">
875
+ <span class="fetch-group-callers-label">Called by</span>
876
+ ${r.map(l=>a`<span class="fetch-group-caller-pill">${l}</span>`)}
877
+ </div>`:d}
878
+ </div>
879
+ `}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`
880
+ <div class="fetch-analysis" id="fetch-analysis">
881
+ ${this.renderSummary(e)}
882
+ ${s.length>0?a`
883
+ <div class="fetch-groups-title">Grouped by URL (${s.length})</div>
884
+ <div class="fetch-groups">${s.map(r=>this.renderGroup(r))}</div>
885
+ `:d}
886
+ </div>
887
+ `}};h([R({context:x})],Ct.prototype,"store",2),Ct=h([g("bk-fetches-view")],Ct);function ps(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 or(i){return i.type==="query"||i.type==="fetch"}function nr(i){let e=(i.normalizedOp||i.operation||"QUERY").toUpperCase(),t=i.table||i.model||"";return {label:`${e} ${t}`,tooltip:i.sql||`${e} ${t}`}}function ar(i){return {label:`${i.method} ${i.url}`,tooltip:`${i.method} ${i.url}`}}function lr(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 cr(i,e){let t=i.timeline.filter(or);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:u}=lr(n,l,t,s,r,o),m=n.type==="query"?nr(n.data):ar(n.data);return {type:n.type,label:m.label,durMs:c,durLabel:S(c),tooltip:m.tooltip,leftPct:p,widthPct:u}})}function hs(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),u=e?.activities?.[l.id],m=u?cr(u,l):[];return {label:`${l.method} ${l.label}`,leftPct:c,widthPct:p,color:ps(l.method,l.statusCode),durMs:l.durationMs,durLabel:S(l.durationMs),tooltip:`${l.method} ${l.label} (${S(l.durationMs)})`,subEvents:m}}),totalMs:o}}function ms(i){let e=i.requests,t=[],s=[],r=[],o=[],n=new Map;for(let p of e){let u=p.label,m=p.pollingDurationMs||p.durationMs;if(!Tt[p.category||""]){if(p.isDuplicate){let b=n.get(u);b?(b.count++,b.wastedMs+=m):n.set(u,{name:u,count:2,wastedMs:m});continue}if(p.statusCode>=400){s.push(u+" ("+as(p.statusCode)+")");continue}p.responseSize>51200&&r.push("Large response: "+u+" returned "+U(p.responseSize)),t.push(u);}}for(let p of n.values())o.push(p);let l="";if(o.length>0){let p=o.map(m=>m.name).join(", "),u=o.reduce((m,b)=>m+b.wastedMs,0);l="Your app fetches "+p+" multiple times on this page. This wastes ~"+S(u)+". 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!==et);return c.length>0&&!l&&(l=c.map(p=>p.label).join(", ")+` is taking over ${S(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
888
+ title="No actions yet"
889
+ subtitle="Start using your app to see user action flows here"
890
+ ></bk-empty-state>`:a`
891
+ <div id="flow-col-header" class="col-header">
892
+ <span style="width:8px"></span>
893
+ <span style="flex:1">Action</span>
894
+ <span style="width:60px;text-align:right">Reqs</span>
895
+ <span style="width:120px;text-align:right">Status</span>
896
+ <span style="width:70px;text-align:right">Time</span>
897
+ </div>
898
+ <div id="flow-list">
899
+ ${t.map((s,r)=>this.renderFlowRow(s,r))}
557
900
  </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>
901
+ `}renderFlowRow(t,s){let r=this.expandedFlowIdx===s,o=this.flowDotClass(t),n=this.flowBadgeInfo(t);return a`
902
+ <div
903
+ class="flow-row ${r?"expanded":""}"
904
+ @click=${()=>this.toggleFlow(s)}
905
+ >
906
+ <div class="flow-summary-row">
907
+ <span class="flow-status-dot ${o}"></span>
908
+ <span class="flow-label">${t.label}</span>
909
+ <span class="flow-req-count"
910
+ >${t.requests.length}
911
+ req${t.requests.length!==1?"s":""}</span
912
+ >
913
+ <span class="flow-badge-pill ${n.cls}">${n.text}</span>
914
+ <span class="flow-duration"
915
+ >${S(t.totalDurationMs)}</span
916
+ >
562
917
  </div>
563
- <button class="btn btn-danger" id="clear-btn">Clear</button>
564
918
  </div>
565
- </div>
566
- <div class="main-content">
567
- <div id="overview-container">
568
- <div class="ov-container" id="overview-content"></div>
919
+ <div class="flow-expand ${r?"open":""}">
920
+ ${r?this.renderFlowDetail(t):d}
569
921
  </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>
922
+ `}renderFlowDetail(t){let s=this.viewMode==="simple"?"Insights":"Details";return a`
923
+ <div class="flow-detail-tabs">
924
+ <button
925
+ class="flow-tab ${this.flowDetailTab==="insights"?"active":""}"
926
+ @click=${r=>this.switchTab("insights",t,r)}
927
+ >
928
+ ${s}
929
+ </button>
930
+ <button
931
+ class="flow-tab ${this.flowDetailTab==="timeline"?"active":""}"
932
+ @click=${r=>this.switchTab("timeline",t,r)}
933
+ >
934
+ Timeline
935
+ </button>
936
+ </div>
937
+ ${this.flowDetailTab==="insights"?this.viewMode==="simple"?this.renderFlowInsights(t):this.renderFlowSubReqs(t):this.renderFlowWaterfall(t)}
938
+ `}renderFlowWaterfall(t){if(this.flowTimelineLoading)return a`<div class="wf-loading">Loading timeline...</div>`;let{rows:s,totalMs:r}=hs(t,this.flowTimeline);if(s.length===0)return d;let o=[];for(let n=0;n<=5;n++)o.push(S(r/5*n));return a`
939
+ <div class="flow-waterfall">
940
+ <div class="wf-time-axis">
941
+ ${o.map(n=>a`<span>${n}</span>`)}
942
+ </div>
943
+ <div class="wf-rows">
944
+ ${s.map(n=>this.renderWaterfallGroup(n))}
577
945
  </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>
946
+ </div>
947
+ `}renderWaterfallGroup(t){return a`
948
+ <div class="wf-request-group">
949
+ <div class="wf-req-row" title="${t.tooltip}">
950
+ <div class="wf-req-label">${t.label}</div>
951
+ <div class="wf-bar-track">
952
+ <div
953
+ class="wf-bar"
954
+ style="left:${t.leftPct}%;width:${t.widthPct}%;background:${t.color}"
955
+ ></div>
582
956
  </div>
957
+ <div class="wf-req-dur">${t.durLabel}</div>
583
958
  </div>
959
+ ${t.subEvents.length>0?t.subEvents.map(s=>a`
960
+ <div class="wf-sub-row" title="${s.tooltip}">
961
+ <div class="wf-sub-label">
962
+ <span
963
+ class="wf-sub-dot"
964
+ style="background:${$e(s.type)}"
965
+ ></span>
966
+ ${s.label}
967
+ </div>
968
+ <div class="wf-bar-track">
969
+ <div
970
+ class="wf-bar wf-sub-bar-sized"
971
+ style="left:${t.leftPct+s.leftPct/100*t.widthPct}%;width:${s.widthPct/100*t.widthPct}%;background:${$e(s.type)}"
972
+ ></div>
973
+ </div>
974
+ <div class="wf-sub-dur">${s.durLabel}</div>
975
+ </div>
976
+ `):d}
584
977
  </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>
978
+ `}renderFlowInsights(t){let s=ms(t),r=s.errors.length>0||s.duplicates.length>0||s.warnings.length>0||!!s.tip;return a`
979
+ <div>
980
+ <div class="flow-traffic">
981
+ ${t.requests.map(o=>this.renderTrafficCard(o))}
592
982
  </div>
593
- <div id="request-list"></div>
983
+ ${r?a`
984
+ <div class="flow-divider"></div>
985
+ <div class="flow-insights">
986
+ ${s.errors.map(o=>a`<div class="insight-line insight-error">
987
+ ✗ ${o}
988
+ </div>`)}
989
+ ${s.duplicates.map(o=>a`<div class="insight-line insight-warn">
990
+ ⚠ ${o.name} — loaded ${o.count}x (wasting
991
+ ~${S(o.wastedMs)})
992
+ </div>`)}
993
+ ${s.warnings.map(o=>a`<div class="insight-line insight-warn">⚠ ${o}</div>`)}
994
+ ${s.tip?a`<div class="insight-line insight-tip">
995
+ Tip: ${s.tip}
996
+ </div>`:d}
997
+ </div>
998
+ `:d}
594
999
  </div>
595
- <div class="view-telemetry" id="fetch-container" style="display:none">
596
- <div class="fetch-analysis" id="fetch-analysis"></div>
1000
+ `}renderTrafficCard(t){if(Tt[t.category||""])return d;let s=st(t.statusCode),r=S(t.pollingDurationMs||t.durationMs),o=!t.isDuplicate&&t.category!==Ht&&t.category!==et||t.requestBody&&t.method!=="GET"||!!t.responseBody;return a`
1001
+ <div
1002
+ class="traffic-card ${t.isStrictModeDupe?"strict-mode-dupe":""}"
1003
+ >
1004
+ <div class="traffic-card-header ${o?"has-details":""}">
1005
+ <bk-method-badge .method=${t.method}></bk-method-badge>
1006
+ <span class="traffic-card-path ${t.isDuplicate?"is-dup":""}"
1007
+ >${t.label}</span
1008
+ >
1009
+ <span class="status-pill ${s}">${t.statusCode}</span>
1010
+ <span class="traffic-card-dur">${r}</span>
1011
+ ${t.isDuplicate?a`<span class="traffic-card-dup">duplicate</span>`:a`<span class="traffic-card-size"
1012
+ >${U(t.responseSize)}</span
1013
+ >`}
1014
+ </div>
1015
+ ${t.isStrictModeDupe?a`<div class="strict-mode-banner">
1016
+ React Strict Mode duplicate — does not happen in production
1017
+ </div>`:d}
1018
+ ${!t.isDuplicate&&t.category!==Ht&&t.category!==et?a`<div
1019
+ class="request-timeline tl-hidden"
1020
+ data-request-id=${t.id}
1021
+ data-request-started=${String(t.startedAt)}
1022
+ ></div>`:d}
1023
+ ${t.requestBody&&t.method!=="GET"?this.renderBodyToggle("out","Request Body",t.requestBody):d}
1024
+ ${t.responseBody?this.renderBodyToggle("in","Response Body",t.responseBody):d}
597
1025
  </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>
1026
+ `}renderBodyToggle(t,s,r){let o=t==="out"?"\u2192":"\u2190";return a`
1027
+ <div class="traffic-body">
1028
+ <button class="traffic-body-toggle" @click=${this.toggleBodyBlock}>
1029
+ <span class="chevron">▸</span
1030
+ ><span class="arrow-${t}">${o}</span> ${s}
1031
+ </button>
1032
+ <pre .innerHTML=${X(r)}></pre>
1033
+ </div>
1034
+ `}renderFlowSubReqs(t){return a`<div class="flow-subreqs">
1035
+ ${t.requests.map((s,r)=>this.renderSubReqRow(s,r))}
1036
+ </div>`}renderSubReqRow(t,s){let r=this.expandedSubReqIdx===s,o=st(t.statusCode),n=t.pollingDurationMs?S(t.pollingDurationMs):S(t.durationMs);return a`
1037
+ <div
1038
+ class="flow-subreq ${r?"expanded":""}"
1039
+ @click=${l=>this.toggleSubReq(s,l)}
1040
+ >
1041
+ <bk-method-badge .method=${t.method}></bk-method-badge>
1042
+ <span class="subreq-label ${t.isDuplicate?"is-dup":""}"
1043
+ >${t.path||t.url}</span
1044
+ >
1045
+ ${t.isDuplicate?a`<span class="subreq-dup-tag">duplicate</span>`:d}
1046
+ <span class="status-pill ${o}">${t.statusCode}</span>
1047
+ <span class="subreq-dur">${n}</span>
1048
+ </div>
1049
+ <div class="flow-subreq-detail ${r?"open":""}">
1050
+ ${r?this.renderSubReqDetail(t):d}
1051
+ </div>
1052
+ `}renderSubReqDetail(t){let s=st(t.statusCode);return a`
1053
+ <div class="detail-meta">
1054
+ <span
1055
+ ><bk-method-badge .method=${t.method}></bk-method-badge> ${P(t.url)}</span
1056
+ >
1057
+ <span
1058
+ ><span class="status-pill ${s}">${t.statusCode}</span></span
1059
+ >
1060
+ <span>${t.durationMs}ms</span>
1061
+ ${t.responseSize?a`<span>${U(t.responseSize)}</span>`:d}
1062
+ </div>
1063
+ <div
1064
+ class="request-timeline tl-hidden"
1065
+ data-request-id=${t.id}
1066
+ data-request-started=${String(t.startedAt)}
1067
+ ></div>
1068
+ <div class="detail-grid">
1069
+ <div class="detail-section">
1070
+ <h4>Request Headers</h4>
1071
+ <pre .innerHTML=${rt(t.headers)}></pre>
1072
+ </div>
1073
+ <div class="detail-section">
1074
+ <h4>Response Headers</h4>
1075
+ <pre .innerHTML=${rt(t.responseHeaders)}></pre>
1076
+ </div>
1077
+ <div class="detail-section">
1078
+ <h4>Request Body</h4>
1079
+ <pre .innerHTML=${X(t.requestBody)}></pre>
1080
+ </div>
1081
+ <div class="detail-section">
1082
+ <h4>Response Body</h4>
1083
+ <pre .innerHTML=${X(t.responseBody)}></pre>
604
1084
  </div>
605
- <div id="query-list"></div>
606
1085
  </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>
1086
+ <div class="detail-actions">
1087
+ <button
1088
+ class="btn btn-curl"
1089
+ @click=${r=>{r.stopPropagation(),ct(t);}}
1090
+ >
1091
+ Copy cURL
1092
+ </button>
1093
+ </div>
1094
+ `}};h([R({context:x})],M.prototype,"store",2),h([E()],M.prototype,"expandedFlowIdx",2),h([E()],M.prototype,"expandedSubReqIdx",2),h([E()],M.prototype,"flowDetailTab",2),h([E()],M.prototype,"flowTimeline",2),h([E()],M.prototype,"flowTimelineLoading",2),M=h([g("bk-flows-view")],M);var It=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`
1095
+ <div class="sec-clear">
1096
+ <span class="sec-clear-icon">\u2713</span>
1097
+ <div class="sec-clear-text">
1098
+ <div class="sec-clear-title">All clear</div>
1099
+ <div class="sec-clear-sub">No security or quality issues detected this session</div>
1100
+ </div>
1101
+ </div>
1102
+ `: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`
1103
+ <div id="security-content">
1104
+ ${this.renderSummary(t.length,s.length,r,o,n)}
1105
+ ${t.length===0&&s.length>0?a`
1106
+ <div class="sec-clear">
1107
+ <span class="sec-clear-icon">\u2713</span>
1108
+ <div class="sec-clear-text">
1109
+ <div class="sec-clear-title">All issues resolved</div>
1110
+ <div class="sec-clear-sub">${s.length} finding${s.length!==1?"s were":" was"} detected and fixed</div>
1111
+ </div>
1112
+ </div>
1113
+ `:d}
1114
+ ${t.length>0?this.renderOpenGroups(t):d}
1115
+ ${s.length>0?this.renderResolved(s):d}
1116
+ </div>
1117
+ `}renderSummary(e,t,s,r,o){return a`
1118
+ <div class="sec-summary">
1119
+ <div class="sec-summary-left">
1120
+ <span class="sec-summary-count">${e}</span>
1121
+ <span class="sec-summary-label">open issue${e!==1?"s":""}</span>
1122
+ ${t>0?a`<span class="sec-resolved-badge">${t} resolved</span>`:d}
1123
+ </div>
1124
+ <div class="sec-summary-right">
1125
+ ${s>0?a`<span class="sec-badge critical">${s} critical</span>`:d}
1126
+ ${r>0?a`<span class="sec-badge warning">${r} warning</span>`:d}
1127
+ ${o>0?a`<span class="sec-badge info">${o} info</span>`:d}
612
1128
  </div>
613
- <div id="error-list"></div>
614
1129
  </div>
615
- <div class="view-telemetry" id="log-container" style="display:none">
616
- <div id="log-analysis"></div>
1130
+ `}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`
1131
+ <div class="sec-group">
1132
+ <div class="sec-group-header">
1133
+ <span class="sec-group-icon ${t.cls}">${t.icon}</span>
1134
+ <span class="sec-group-title">${e.title}</span>
1135
+ <span class="sec-group-count">${e.items.length}</span>
1136
+ </div>
1137
+ ${e.hint?a`<div class="sec-hint">${e.hint}</div>`:d}
1138
+ <div class="sec-items">${e.items.map(s=>this.renderIssueItem(s))}</div>
1139
+ </div>
1140
+ `}renderIssueItem(e){let t=e.issue;return a`
1141
+ <div class="sec-item">
1142
+ <div class="sec-item-desc">${t.desc}</div>
1143
+ ${e.occurrences>1?a`<span class="sec-item-count">${e.occurrences}x</span>`:d}
1144
+ ${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}
1145
+ ${e.aiNotes?a`<div class="sec-ai-notes">${e.aiNotes}</div>`:d}
1146
+ </div>
1147
+ `}renderResolved(e){return a`
1148
+ <div class="sec-resolved-title">
1149
+ <span class="sec-resolved-check">\u2713</span> Resolved
1150
+ <span class="sec-resolved-count">${e.length}</span>
1151
+ </div>
1152
+ <div class="sec-group sec-group-resolved">
1153
+ <div class="sec-items">
1154
+ ${e.map(t=>a`
1155
+ <div class="sec-item sec-item-resolved">
1156
+ <span class="sec-resolved-item-icon">\u2713</span>
1157
+ <div class="sec-item-desc">${t.issue.title} \u2014 ${t.issue.endpoint||"global"}</div>
1158
+ ${t.aiStatus==="fixed"?a`<span class="sec-ai-badge sec-ai-verified">Verified fix</span>`:d}
1159
+ ${t.aiNotes?a`<div class="sec-ai-notes">${t.aiNotes}</div>`:d}
1160
+ </div>
1161
+ `)}
1162
+ </div>
1163
+ </div>
1164
+ `}};h([R({context:x})],It.prototype,"store",2),It=h([g("bk-security-view")],It);var pt=class extends f{constructor(){super(...arguments);this.expandedCardIdx=-1;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate());}navigateToView(t){let s=document.querySelectorAll(".sidebar-item");for(let r of s){let o=r.querySelector(".item-label");if(o&&o.textContent?.trim()===(yt[t]||t)){r.click();return}}}toggleCard(t,s){let r=s.target;for(;r&&r!==s.currentTarget;){if(r.classList?.contains("ov-card-link")){let o=r.getAttribute("data-nav");o&&this.navigateToView(o);return}r=r.parentElement;}this.expandedCardIdx=this.expandedCardIdx===t?-1:t;}render(){let t=this.store.state,s=t.requests.filter(v=>!v.isStatic&&!v.isHealthCheck&&(!v.path||v.path.indexOf(O)!==0));if(!(s.length>0||t.queries.length>0||t.errors.length>0))return a`<bk-empty-state
1165
+ title="Waiting for requests..."
1166
+ subtitle="Start using your app to see insights here"
1167
+ ></bk-empty-state>`;let o=s.filter(v=>v.statusCode>=400).length,n=new Set(["data-fetch","api-call","server-action","page-load"]),l=s.filter(v=>v.category&&n.has(v.category)),c=l.length>0?l:s,p=c.length>0?Math.round(c.reduce((v,L)=>v+L.durationMs,0)/c.length):0,u=t.issues||[],m=u.filter(v=>v.state==="open"||v.state==="regressed"),b=u.filter(v=>v.state==="fixing"),$=u.filter(v=>v.state==="resolved");return a`
1168
+ <div class="ov-container" id="overview-content">
1169
+ ${this.renderSummary(s.length,t.flows.length,p,t.queries.length,o,t.fetches.length)}
1170
+ ${m.length===0&&b.length===0&&$.length===0?a`<div class="ov-clear">
1171
+ <span class="ov-clear-icon">\u2713</span>All clear \u2014 no issues detected
1172
+ </div>`:d}
1173
+ ${m.length===0&&$.length>0?a`<div class="ov-clear">
1174
+ <span class="ov-clear-icon">\u2713</span>All issues resolved \u2014
1175
+ ${$.length} finding${$.length!==1?"s were":" was"} detected and
1176
+ fixed
1177
+ </div>`:d}
1178
+ ${m.length>0?this.renderOpenIssues(m):d}
1179
+ ${b.length>0?this.renderVerifying(b):d}
1180
+ ${$.length>0?this.renderResolvedIssues($):d}
1181
+ </div>
1182
+ `}renderSummary(t,s,r,o,n,l){return a`
1183
+ <div class="ov-summary">
1184
+ <div class="ov-stat"><span class="ov-stat-value">${t}</span><span class="ov-stat-label">Requests</span></div>
1185
+ <div class="ov-stat"><span class="ov-stat-value">${s}</span><span class="ov-stat-label">Actions</span></div>
1186
+ <div class="ov-stat"><span class="ov-stat-value">${S(r)}</span><span class="ov-stat-label">Avg Response</span></div>
1187
+ <div class="ov-stat"><span class="ov-stat-value">${o}</span><span class="ov-stat-label">Queries</span></div>
1188
+ <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>
1189
+ <div class="ov-stat"><span class="ov-stat-value">${l}</span><span class="ov-stat-label">Fetches</span></div>
1190
+ </div>
1191
+ `}renderOpenIssues(t){return a`
1192
+ <div class="ov-section-title">Issues Found <span class="ov-issue-count">${t.length}</span></div>
1193
+ <div class="ov-cards">${t.map((s,r)=>this.renderIssueCard(s,r))}</div>
1194
+ `}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`
1195
+ <div class="ov-card ${n?"expanded":""}" @click=${p=>this.toggleCard(s,p)}>
1196
+ <span class="ov-card-icon ${o.cls}">${o.icon}</span>
1197
+ <div class="ov-card-body">
1198
+ <div class="ov-card-title">${r.title}${l}</div>
1199
+ <div class="ov-card-desc">${r.desc}</div>
1200
+ ${c}
1201
+ <div class="ov-card-expand" style="display:${n?"block":"none"}">
1202
+ ${r.detail?a`<div .innerHTML=${r.detail}></div>`:d}
1203
+ ${r.hint?a`<div class="ov-card-hint">${r.hint}</div>`:d}
1204
+ ${r.nav?a`<span class="ov-card-link" data-nav=${r.nav}>View in ${ae[r.nav]||r.nav} \u2192</span>`:d}
1205
+ </div>
1206
+ </div>
1207
+ <span class="ov-card-arrow">${n?"\u2193":"\u2192"}</span>
1208
+ </div>
1209
+ `}renderVerifying(t){return a`
1210
+ <div class="ov-section-title ov-resolved-title">
1211
+ <span style="color:var(--yellow,#f5a623)">\u29d7</span> Awaiting Verification
1212
+ <span class="ov-issue-count">${t.length}</span>
1213
+ </div>
1214
+ <div class="ov-cards">
1215
+ ${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`
1216
+ <div class="ov-card ov-card-resolved">
1217
+ <span class="ov-card-icon resolved">\u29d7</span>
1218
+ <div class="ov-card-body">
1219
+ <div class="ov-card-title" style="color:var(--text-muted)">
1220
+ ${r.title}
1221
+ <span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>
1222
+ </div>
1223
+ <div class="ov-card-desc">${r.desc}</div>
1224
+ ${o}
1225
+ </div>
1226
+ </div>
1227
+ `})}
1228
+ </div>
1229
+ `}renderResolvedIssues(t){return a`
1230
+ <div class="ov-section-title ov-resolved-title">
1231
+ <span style="color:var(--green)">\u2713</span> Resolved
1232
+ <span class="ov-issue-count">${t.length}</span>
1233
+ </div>
1234
+ <div class="ov-cards">
1235
+ ${t.map(s=>a`
1236
+ <div class="ov-card ov-card-resolved">
1237
+ <span class="ov-card-icon resolved">\u2713</span>
1238
+ <div class="ov-card-body">
1239
+ <div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">${s.issue.title}</div>
1240
+ <div class="ov-card-desc">${s.issue.desc}</div>
1241
+ </div>
1242
+ </div>
1243
+ `)}
1244
+ </div>
1245
+ `}};h([R({context:x})],pt.prototype,"store",2),h([E()],pt.prototype,"expandedCardIdx",2),pt=h([g("bk-overview-view")],pt);function jt(i){return i<1?"<1ms":i<1e3?Math.round(i)+"ms":(i/1e3).toFixed(1)+"s"}function dr(i){return i<V?xt.green:i<Y?xt.amber:xt.red}function vs(i){return i.statusCode>=400?xt.red:dr(i.durationMs)}function fs(i){return [parseInt(i.slice(1,3),16),parseInt(i.slice(3,5),16),parseInt(i.slice(5,7),16)]}function gs(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 bs(i,e,t,s,r){let[o,n,l]=fs(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 Es(i,e,t,s,r,o){let[n,l,c]=fs(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 _s(i,e){let t=[],s=gs(i);if(!s||e.length===0)return t;let{ctx:r,w:o,h:n}=s,l=es,c=o-l.left-l.right,p=n-l.top-l.bottom,u=0,m=e[0].timestamp,b=e[0].timestamp;for(let _ of e)_.durationMs>u&&(u=_.durationMs),_.timestamp<m&&(m=_.timestamp),_.timestamp>b&&(b=_.timestamp);u=Math.max(u,10),u=Math.ceil(u*1.15/10)*10;let $=b-m||1;r.strokeStyle=Je,r.lineWidth=1;let v=4;for(let _=0;_<=v;_++){let w=l.top+p-_/v*p;r.beginPath(),r.moveTo(l.left,w),r.lineTo(l.left+c,w),r.stroke(),r.fillStyle=ge,r.font=Ze,r.textAlign="right",r.fillText(jt(Math.round(_/v*u)),l.left-8,w+3);}for(let _ of [{ms:V},{ms:Y}]){if(_.ms>=u)continue;let w=l.top+p-_.ms/u*p;r.beginPath(),r.setLineDash([4,4]),r.strokeStyle="rgba(113,113,122,0.3)",r.lineWidth=1,r.moveTo(l.left,w),r.lineTo(l.left+c,w),r.stroke(),r.setLineDash([]),r.fillStyle="rgba(113,113,122,0.5)",r.font=be,r.textAlign="left",r.fillText(jt(_.ms),l.left+c+2,w+3);}for(let _=0;_<e.length;_++){let w=e[_],ut=e.length===1?l.left+c/2:l.left+(w.timestamp-m)/$*c,Vt=l.top+p-w.durationMs/u*p,ye=vs(w);t.push({x:ut,y:Vt,idx:_,r:w}),w.statusCode>=400?Es(r,ut,Vt,4,ye,2):bs(r,ut,Vt,4,ye);}r.fillStyle=ge,r.font=be,r.textAlign="center";let L=[m,m+$/2,b];for(let _=0;_<L.length;_++){let w=l.left+_/2*c,ut=new Date(L[_]);r.fillText(ut.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),w,l.top+p+14);}return t}function Ss(i,e){let t=gs(i);if(!t||e.length===0)return;let{ctx:s,w:r,h:o}=t,n=4,l=4,c=r-n*2,p=o-l*2,u=0,m=e[0].timestamp,b=e[0].timestamp;for(let v of e)v.durationMs>u&&(u=v.durationMs),v.timestamp<m&&(m=v.timestamp),v.timestamp>b&&(b=v.timestamp);u=Math.max(u,10),u=Math.ceil(u*1.15/10)*10;let $=b-m||1;for(let v of [V,Y]){if(v>=u)continue;let L=l+p-v/u*p;s.beginPath(),s.setLineDash([2,3]),s.strokeStyle="rgba(113,113,122,0.15)",s.lineWidth=1,s.moveTo(n,L),s.lineTo(n+c,L),s.stroke(),s.setLineDash([]);}for(let v of e){let L=e.length===1?n+c/2:n+(v.timestamp-m)/$*c,_=l+p-v.durationMs/u*p,w=vs(v);v.statusCode>=400?Es(s,L,_,2.5,w,1.5):bs(s,L,_,2.5,w);}s.fillStyle="rgba(113,113,122,0.5)",s.font=ts,s.textAlign="right",s.fillText(jt(u),r-2,l+8),s.fillText(jt(0),r-2,o-2);}var B=class extends f{constructor(){super(...arguments);this.selectedEndpoint=H;this.graphData=[];this.loadError=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;}}healthGrade(t){for(let s of Bt)if(t<s.max)return s;return Bt[Bt.length-1]}fmtMs(t){return t<1?"<1ms":t<1e3?Math.round(t)+"ms":(t/1e3).toFixed(1)+"s"}renderScatterChart(t,s){this.scatterDots=_s(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 u of this.scatterDots){let m=Math.sqrt((u.x-n)**2+(u.y-l)**2);m<p&&(p=m,c=u);}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)this.graphData.forEach((t,s)=>{if(t.requests.length===0)return;let r=this.querySelector(`#inline-scatter-${s}`);r&&Ss(r,t.requests);});else {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`
1246
+ <div id="graph-content">
1247
+ ${this.renderSelector()}
1248
+ ${this.selectedEndpoint===H?this.renderOverview():this.renderDetail()}
1249
+ </div>
1250
+ `}renderSelector(){return a`
1251
+ <div class="perf-selector">
1252
+ <button class="perf-selector-btn ${this.selectedEndpoint===H?"active":""}"
1253
+ @click=${()=>{this.selectedEndpoint=H;}}>Overview</button>
1254
+ ${this.graphData.map((t,s)=>a`
1255
+ <button class="perf-selector-btn ${t.endpoint===this.selectedEndpoint?"active":""}"
1256
+ @click=${()=>{this.selectedEndpoint=t.endpoint;}}>
1257
+ <span class="perf-dot" style="background:${fe[s%fe.length]}"></span>${t.endpoint}
1258
+ </button>
1259
+ `)}
1260
+ </div>
1261
+ `}renderOverview(){return a`
1262
+ <div class="perf-endpoint-list">
1263
+ ${this.graphData.map((t,s)=>t.requests.length===0?d:this.renderEndpointCard(t,s))}
1264
+ </div>
1265
+ `}renderEndpointCard(t,s){let r=t.summary,o=this.healthGrade(r.p95Ms),n=Math.round(r.errorRate*r.totalRequests),l=(r.avgQueryTimeMs||0)+(r.avgFetchTimeMs||0)+(r.avgAppTimeMs||0),c=d;if(l>0){let p=Math.round((r.avgQueryTimeMs||0)/l*100),u=Math.round((r.avgFetchTimeMs||0)/l*100),m=Math.max(0,100-p-u);c=a`
1266
+ <div class="perf-breakdown-inline">
1267
+ <div class="perf-breakdown-bar perf-breakdown-bar-sm">
1268
+ ${p>0?a`<div class="perf-breakdown-seg perf-breakdown-db" style="width:${p}%"></div>`:d}
1269
+ ${u>0?a`<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:${u}%"></div>`:d}
1270
+ ${m>0?a`<div class="perf-breakdown-seg perf-breakdown-app" style="width:${m}%"></div>`:d}
1271
+ </div>
1272
+ <span class="perf-breakdown-labels">
1273
+ ${p>0?a`<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-db"></span>${this.fmtMs(r.avgQueryTimeMs||0)}</span>`:d}
1274
+ ${u>0?a`<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>${this.fmtMs(r.avgFetchTimeMs||0)}</span>`:d}
1275
+ <span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-app"></span>${this.fmtMs(r.avgAppTimeMs||0)}</span>
1276
+ </span>
1277
+ </div>
1278
+ `;}return a`
1279
+ <div class="perf-endpoint-card" @click=${()=>{this.selectedEndpoint=t.endpoint;}}>
1280
+ <div class="perf-ep-header">
1281
+ <span class="perf-ep-name">${t.endpoint}</span>
1282
+ <span class="perf-ep-stats">
1283
+ <span class="perf-ep-stat" style="color:${o.color}">p95: ${this.fmtMs(r.p95Ms)}</span>
1284
+ <span class="perf-ep-stat ${n>0?"perf-ep-stat-err":""}">${n} err</span>
1285
+ ${r.avgQueryCount>0?a`<span class="perf-ep-stat ${r.avgQueryCount>5?"perf-ep-stat-warn":""}">${r.avgQueryCount} q/req</span>`:d}
1286
+ <span class="perf-ep-stat perf-ep-stat-muted">${r.totalRequests} req${r.totalRequests!==1?"s":""}</span>
1287
+ </span>
1288
+ </div>
1289
+ ${c}
1290
+ <canvas id="inline-scatter-${s}" class="perf-inline-canvas"></canvas>
1291
+ </div>
1292
+ `}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.healthGrade(s.p95Ms),o=Math.round(s.errorRate*s.totalRequests);return a`
1293
+ ${this.renderDetailHeader(t,r)}
1294
+ ${this.renderDetailMetrics(s,r,o)}
1295
+ ${this.renderDetailBreakdown(s)}
1296
+ ${this.renderDetailChart()}
1297
+ ${this.renderDetailHistory(t)}
1298
+ `}renderDetailHeader(t,s){return a`
1299
+ <div class="perf-detail-header">
1300
+ <div class="perf-detail-title">
1301
+ <span class="perf-badge perf-badge-lg" style="color:${s.color};background:${s.bg};border-color:${s.border}">${s.label}</span>
1302
+ <span>${t.endpoint}</span>
1303
+ </div>
1304
+ </div>
1305
+ `}renderDetailMetrics(t,s,r){return a`
1306
+ <div class="perf-metric-row">
1307
+ <div class="perf-metric-card">
1308
+ <span class="perf-metric-label">P95</span>
1309
+ <span class="perf-metric-value" style="color:${s.color}">${this.fmtMs(t.p95Ms)}</span>
1310
+ </div>
1311
+ <div class="perf-metric-card">
1312
+ <span class="perf-metric-label">Errors</span>
1313
+ <span class="perf-metric-value" style="color:${r>0?"var(--red)":"var(--green)"}">
1314
+ ${r>0?r+" ("+Math.round(t.errorRate*100)+"%)":"0"}
1315
+ </span>
1316
+ </div>
1317
+ <div class="perf-metric-card">
1318
+ <span class="perf-metric-label">Queries/req</span>
1319
+ <span class="perf-metric-value" style="color:${t.avgQueryCount>5?"var(--amber)":"var(--text)"}">${t.avgQueryCount}</span>
1320
+ </div>
1321
+ </div>
1322
+ `}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`
1323
+ <div class="perf-breakdown">
1324
+ <div class="perf-section-title">Time Breakdown</div>
1325
+ <div class="perf-breakdown-bar">
1326
+ ${r>0?a`<div class="perf-breakdown-seg perf-breakdown-db" style="width:${r}%"></div>`:d}
1327
+ ${o>0?a`<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:${o}%"></div>`:d}
1328
+ ${n>0?a`<div class="perf-breakdown-seg perf-breakdown-app" style="width:${n}%"></div>`:d}
1329
+ </div>
1330
+ <div class="perf-breakdown-legend">
1331
+ <span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-db"></span>DB ${this.fmtMs(t.avgQueryTimeMs||0)} (${r}%)</span>
1332
+ <span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>Fetch ${this.fmtMs(t.avgFetchTimeMs||0)} (${o}%)</span>
1333
+ <span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-app"></span>App ${this.fmtMs(t.avgAppTimeMs||0)} (${n}%)</span>
1334
+ </div>
1335
+ </div>
1336
+ `}renderDetailChart(){return a`
1337
+ <div class="perf-chart-wrap">
1338
+ <div class="perf-section-title">Response Time</div>
1339
+ <canvas id="perf-detail-canvas" class="perf-canvas" style="width:100%;height:240px"></canvas>
1340
+ </div>
1341
+ `}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({r:t.requests[r],origIdx:r});return a`
1342
+ <div class="perf-history-wrap">
617
1343
  <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>
1344
+ <span class="perf-col perf-col-date">Time</span>
1345
+ <span class="perf-col perf-col-health">Health</span>
1346
+ <span class="perf-col perf-col-avg">Duration</span>
1347
+ <span class="perf-col perf-col-breakdown">Breakdown</span>
1348
+ <span class="perf-col perf-col-status">Status</span>
1349
+ <span class="perf-col perf-col-qpr">Queries</span>
621
1350
  </div>
622
- <div id="log-list"></div>
1351
+ ${s.map(r=>this.renderHistoryRow(r.r,r.origIdx))}
623
1352
  </div>
624
- <div class="view-telemetry" id="security-container" style="display:none">
625
- <div class="sec-container" id="security-content"></div>
1353
+ `}renderHistoryRow(t,s){let r=this.healthGrade(t.durationMs),o=new Date(t.timestamp).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),n=t.statusCode>=400,l=t.queryTimeMs||0,c=t.fetchTimeMs||0,p=Math.max(0,t.durationMs-l-c);return a`
1354
+ <div class="perf-hist-row ${n?"perf-hist-row-err":""}" data-req-idx=${s}>
1355
+ <span class="perf-col perf-col-date">${o}</span>
1356
+ <span class="perf-col perf-col-health">
1357
+ <span class="perf-badge perf-badge-sm" style="color:${r.color};background:${r.bg};border-color:${r.border}">${r.label}</span>
1358
+ </span>
1359
+ <span class="perf-col perf-col-avg">${this.fmtMs(t.durationMs)}</span>
1360
+ <span class="perf-col perf-col-breakdown">
1361
+ ${l>0?a`<span class="perf-bd-tag perf-bd-tag-db">DB ${this.fmtMs(l)}</span>`:d}
1362
+ ${c>0?a`<span class="perf-bd-tag perf-bd-tag-fetch">Fetch ${this.fmtMs(c)}</span>`:d}
1363
+ <span class="perf-bd-tag perf-bd-tag-app">App ${this.fmtMs(p)}</span>
1364
+ </span>
1365
+ <span class="perf-col perf-col-status" style="color:${n?"var(--red)":"var(--text-muted)"}">${t.statusCode}</span>
1366
+ <span class="perf-col perf-col-qpr">${t.queryCount}</span>
626
1367
  </div>
627
- <div class="view-telemetry" id="performance-container" style="display:none">
628
- <div id="graph-content"></div>
1368
+ `}};h([R({context:x})],B.prototype,"store",2),h([E()],B.prototype,"selectedEndpoint",2),h([E()],B.prototype,"graphData",2),h([E()],B.prototype,"loadError",2),B=h([g("bk-performance-view")],B);function pr(i){return i===0?"<1ms":S(i)}var A=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=A.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(A.cache.size>=he){let o=A.cache.keys().next().value;o!==void 0&&A.cache.delete(o);}A.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`
1369
+ <div class="tl-header">
1370
+ <span class="tl-title">Activity Timeline</span>
1371
+ <span class="tl-counts">
1372
+ ${t.counts.queries>0?a`<span class="tl-count tl-count-query">${t.counts.queries} quer${t.counts.queries===1?"y":"ies"}</span>`:d}
1373
+ ${t.counts.fetches>0?a`<span class="tl-count tl-count-fetch">${t.counts.fetches} fetch${t.counts.fetches===1?"":"es"}</span>`:d}
1374
+ ${t.counts.logs>0?a`<span class="tl-count tl-count-log">${t.counts.logs} log${t.counts.logs===1?"":"s"}</span>`:d}
1375
+ ${t.counts.errors>0?a`<span class="tl-count tl-count-error">${t.counts.errors} error${t.counts.errors===1?"":"s"}</span>`:d}
1376
+ </span>
629
1377
  </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>
1378
+ <div class="tl-events">${this.renderTimeline(t.timeline,s)}</div>
1379
+ `}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,u=p?r.get(p):void 0;if(u&&u.length>0){let m=u.length;return a`
1380
+ ${this.renderEvent(l,c,s)}
1381
+ <div class="tl-nested">
1382
+ <span class="tl-nested-label">${m} nested quer${m===1?"y":"ies"}</span>
1383
+ ${u.map(b=>{let $=n++;return this.renderEvent(b,$,s,true)})}
1384
+ </div>
1385
+ `}return this.renderEvent(l,c,s)})}renderEvent(t,s,r,o=false){let n=ss[t.type]||"var(--text-dim)",l=rs[t.type]||t.type,c="+"+S(Math.round(t.timestamp-r)),p=t.type==="query"?t.data.sql:void 0,u=!!p,m=this.expandedSqlIdx===s;return a`
1386
+ <div class="tl-event ${u?"tl-clickable":""} ${o?"tl-nested-event":""}"
1387
+ style="${u?"":`border-left-color:${n}`}"
1388
+ @click=${u?b=>this.toggleSql(s,b):d}>
1389
+ <span class="tl-event-time">${c}</span>
1390
+ <span class="tl-event-type" style="color:${n}">${l}</span>
1391
+ ${this.renderEventContent(t)}
1392
+ ${p?a`
1393
+ <div class="tl-event-sql ${m?"open":""}">
1394
+ <button class="tl-sql-copy" @click=${b=>this.copySql(p,b)}>Copy</button>
1395
+ ${p}
1396
+ </div>`:d}
1397
+ </div>
1398
+ `}renderEventContent(t){switch(t.type){case "fetch":{let s=t.data,r=s.statusCode>=400;return a`
1399
+ <span class="tl-event-summary">${s.method} ${s.url}</span>
1400
+ <span class="tl-event-status" style="${r?"color:var(--red)":""}">${s.statusCode}</span>
1401
+ <span class="tl-event-dur">${S(s.durationMs)}</span>
1402
+ `}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`
1403
+ <span class="tl-event-summary"><span style="color:${n};font-weight:600">${r}</span> ${o}</span>
1404
+ <span class="tl-event-dur">${pr(s.durationMs)}</span>
1405
+ `}case "log":{let s=t.data,r=ze[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}}};A.cache=new Map,h([R({context:x})],A.prototype,"store",2),h([T({attribute:"request-id"})],A.prototype,"requestId",2),h([T({attribute:"request-started",type:Number})],A.prototype,"requestStarted",2),h([E()],A.prototype,"data",2),h([E()],A.prototype,"loading",2),h([E()],A.prototype,"failed",2),h([E()],A.prototype,"expandedSqlIdx",2),A=h([g("bk-timeline-panel")],A);var Qt=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(O)||(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(),me)));},this.eventSource.addEventListener(re,e=>{this.store.prependFetch(JSON.parse(e.data));}),this.eventSource.addEventListener("log",e=>{this.store.prependLog(JSON.parse(e.data));}),this.eventSource.addEventListener(ie,e=>{this.store.prependError(JSON.parse(e.data));}),this.eventSource.addEventListener(oe,e=>{this.store.prependQuery(JSON.parse(e.data));}),this.eventSource.addEventListener(ne,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 $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"><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 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"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></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"><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 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"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>`}function Rs(){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 ws(){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 As(){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 Cs(){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 Is(){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 J=class extends f{constructor(){super(...arguments);this.store=new Wt;this.activeView="overview";this.viewMode="simple";this.sse=new Qt(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,u,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(u.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(O)),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`
1406
+ <div class="app" id="app">
1407
+ <aside class="sidebar">
1408
+ <div class="sidebar-logo">
1409
+ <span class="logo-text">brakit</span>
1410
+ <span class="logo-version">v${l?.version??""}</span>
1411
+ </div>
1412
+ <nav class="sidebar-nav">
1413
+ ${this.renderSidebarItem("overview","Overview",$s(),void 0)}
1414
+ <div class="sidebar-section">Monitor</div>
1415
+ ${this.renderSidebarItem("actions","Actions",Ts(),t.flows.length)}
1416
+ ${this.renderSidebarItem("requests","Requests",ys(),s.length)}
1417
+ ${this.renderSidebarItem("fetches","Fetches",xs(),t.fetches.length)}
1418
+ <div class="sidebar-section">Insights</div>
1419
+ ${this.renderSidebarItem("queries","Queries",Rs(),t.queries.length)}
1420
+ ${this.renderSidebarItem("errors","Errors",ws(),t.errors.length)}
1421
+ ${this.renderSidebarItem("logs","Logs",As(),t.logs.length)}
1422
+ ${this.renderSidebarItem("security","Security",Cs(),n,n===0)}
1423
+ ${this.renderSidebarItem("performance","Performance",Is(),void 0)}
1424
+ </nav>
1425
+ <div class="sidebar-footer">:${l?.port??""}</div>
1426
+ </aside>
1427
+ <div class="main-panel">
1428
+ <div class="header">
1429
+ <div class="header-left">
1430
+ <span class="header-title" id="header-title">${yt[this.activeView]||this.activeView}</span>
1431
+ <span class="header-sub" id="header-sub">${le[this.activeView]||""}</span>
1432
+ </div>
1433
+ <div class="header-right">
1434
+ ${this.activeView==="actions"?a`
1435
+ <div class="segmented-control" id="mode-toggle">
1436
+ <button class="segmented-btn ${this.viewMode==="simple"?"active":""}" @click=${()=>{this.viewMode="simple",this.store.setViewMode("simple");}}>Quick</button>
1437
+ <button class="segmented-btn ${this.viewMode==="detailed"?"active":""}" @click=${()=>{this.viewMode="detailed",this.store.setViewMode("detailed");}}>Detailed</button>
1438
+ </div>
1439
+ `:d}
1440
+ <button class="btn btn-danger" @click=${this.handleClear}>Clear</button>
1441
+ </div>
1442
+ </div>
1443
+ <div class="main-content">
1444
+ <div id="overview-container" style="display:${this.activeView==="overview"?"block":"none"}">
1445
+ <bk-overview-view></bk-overview-view>
1446
+ </div>
1447
+ <div class="view-flows" id="flow-container" style="display:${this.activeView==="actions"?"block":"none"}">
1448
+ <bk-flows-view></bk-flows-view>
1449
+ </div>
1450
+ <div class="view-requests" id="request-container" style="display:${this.activeView==="requests"?"block":"none"}">
1451
+ <bk-requests-view></bk-requests-view>
1452
+ </div>
1453
+ <div class="view-telemetry" id="fetch-container" style="display:${this.activeView==="fetches"?"block":"none"}">
1454
+ <bk-fetches-view></bk-fetches-view>
1455
+ </div>
1456
+ <div class="view-telemetry" id="query-container" style="display:${this.activeView==="queries"?"block":"none"}">
1457
+ <bk-queries-view></bk-queries-view>
1458
+ </div>
1459
+ <div class="view-telemetry" id="error-container" style="display:${this.activeView==="errors"?"block":"none"}">
1460
+ <bk-errors-view></bk-errors-view>
1461
+ </div>
1462
+ <div class="view-telemetry" id="log-container" style="display:${this.activeView==="logs"?"block":"none"}">
1463
+ <bk-logs-view></bk-logs-view>
1464
+ </div>
1465
+ <div class="view-telemetry" id="security-container" style="display:${this.activeView==="security"?"block":"none"}">
1466
+ <bk-security-view></bk-security-view>
1467
+ </div>
1468
+ <div class="view-telemetry" id="performance-container" style="display:${this.activeView==="performance"?"block":"none"}">
1469
+ <bk-performance-view></bk-performance-view>
1470
+ </div>
1471
+ </div>
1472
+ <div class="footer">
1473
+ <span id="stat-total">${s.length} request${s.length!==1?"s":""}</span>
1474
+ <span id="stat-flows">${t.flows.length} action${t.flows.length!==1?"s":""}</span>
1475
+ <span id="stat-errors" class="error-count">${r} error${r!==1?"s":""}</span>
1476
+ <span id="stat-avg">Avg: ${o}ms</span>
1477
+ </div>
1478
+ </div>
1479
+ </div>
1480
+ <bk-toast></bk-toast>
1481
+ `}renderSidebarItem(t,s,r,o,n=false){return a`
1482
+ <button class="sidebar-item ${this.activeView===t?"active":""}" @click=${()=>this.switchView(t)}>
1483
+ <span class="item-icon">${r}</span>
1484
+ <span class="item-label">${s}</span>
1485
+ ${o!==void 0?a`<span class="item-count" style="display:${n?"none":""}">${o}</span>`:d}
1486
+ </button>
1487
+ `}};h([_e({context:x})],J.prototype,"store",2),h([E()],J.prototype,"activeView",2),h([E()],J.prototype,"viewMode",2),J=h([g("bk-dashboard")],J);
1488
+ /*! Bundled license information:
1489
+
1490
+ @lit/reactive-element/css-tag.js:
1491
+ (**
1492
+ * @license
1493
+ * Copyright 2019 Google LLC
1494
+ * SPDX-License-Identifier: BSD-3-Clause
1495
+ *)
1496
+
1497
+ @lit/reactive-element/reactive-element.js:
1498
+ lit-html/lit-html.js:
1499
+ lit-element/lit-element.js:
1500
+ @lit/reactive-element/decorators/custom-element.js:
1501
+ @lit/reactive-element/decorators/property.js:
1502
+ @lit/reactive-element/decorators/state.js:
1503
+ @lit/reactive-element/decorators/event-options.js:
1504
+ @lit/reactive-element/decorators/base.js:
1505
+ @lit/reactive-element/decorators/query.js:
1506
+ @lit/reactive-element/decorators/query-all.js:
1507
+ @lit/reactive-element/decorators/query-async.js:
1508
+ @lit/reactive-element/decorators/query-assigned-nodes.js:
1509
+ @lit/context/lib/decorators/provide.js:
1510
+ (**
1511
+ * @license
1512
+ * Copyright 2017 Google LLC
1513
+ * SPDX-License-Identifier: BSD-3-Clause
1514
+ *)
1515
+
1516
+ lit-html/is-server.js:
1517
+ @lit/context/lib/decorators/consume.js:
1518
+ (**
1519
+ * @license
1520
+ * Copyright 2022 Google LLC
1521
+ * SPDX-License-Identifier: BSD-3-Clause
1522
+ *)
1523
+
1524
+ @lit/reactive-element/decorators/query-assigned-elements.js:
1525
+ @lit/context/lib/context-request-event.js:
1526
+ @lit/context/lib/create-context.js:
1527
+ @lit/context/lib/controllers/context-consumer.js:
1528
+ @lit/context/lib/value-notifier.js:
1529
+ @lit/context/lib/controllers/context-provider.js:
1530
+ @lit/context/lib/context-root.js:
1531
+ (**
1532
+ * @license
1533
+ * Copyright 2021 Google LLC
1534
+ * SPDX-License-Identifier: BSD-3-Clause
1535
+ *)
1536
+ */})();</script>
2652
1537
  </body>
2653
1538
  </html>