brakit 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/api.d.ts +124 -117
- package/dist/api.js +417 -363
- package/dist/bin/brakit.js +499 -339
- package/dist/dashboard-client.global.js +703 -0
- package/dist/dashboard.html +895 -2168
- package/dist/mcp/server.js +75 -90
- package/dist/runtime/index.js +2934 -5028
- package/package.json +4 -2
package/dist/dashboard.html
CHANGED
|
@@ -94,6 +94,11 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
|
|
|
94
94
|
|
|
95
95
|
/* Content */
|
|
96
96
|
.main-content{flex:1;overflow-y:auto}
|
|
97
|
+
bk-dashboard{display:contents}
|
|
98
|
+
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}
|
|
99
|
+
bk-method-badge,bk-status-pill,bk-duration-label,bk-copy-button{display:inline-flex;flex-shrink:0}
|
|
100
|
+
bk-stat-card{display:inline-flex}
|
|
101
|
+
bk-toast{display:block;position:fixed;top:0;left:0;right:0;z-index:100;pointer-events:none}
|
|
97
102
|
|
|
98
103
|
/* Column headers */
|
|
99
104
|
.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)}
|
|
@@ -267,17 +272,23 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
|
|
|
267
272
|
.fetch-stat-value{font-size:17px;font-weight:700;font-family:var(--mono);color:var(--text)}
|
|
268
273
|
.fetch-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
|
|
269
274
|
.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:
|
|
271
|
-
.fetch-group{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:
|
|
275
|
+
.fetch-groups{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}
|
|
276
|
+
.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
277
|
.fetch-group:hover{border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
273
278
|
.fetch-group-header{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:13px}
|
|
274
279
|
.fetch-group-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;color:var(--text)}
|
|
275
280
|
.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:
|
|
281
|
+
.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
282
|
.fetch-group-meta span{display:flex;align-items:center;gap:4px}
|
|
278
|
-
.fetch-group-
|
|
279
|
-
.fetch-group-
|
|
283
|
+
.fetch-group-sep{color:var(--text-muted);font-size:9px}
|
|
284
|
+
.fetch-group-ok{color:var(--green)}
|
|
280
285
|
.fetch-group-err{color:var(--red)}
|
|
286
|
+
.fetch-group-timeline{display:flex;align-items:center;gap:6px;margin-top:4px;font-size:10px;color:var(--text-muted);font-family:var(--mono)}
|
|
287
|
+
.fetch-group-timeline-dot{width:6px;height:6px;border-radius:50%;background:var(--blue);opacity:.5;flex-shrink:0}
|
|
288
|
+
.fetch-group-timeline-range{letter-spacing:.3px}
|
|
289
|
+
.fetch-group-callers{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:10px;flex-wrap:wrap}
|
|
290
|
+
.fetch-group-callers-label{color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}
|
|
291
|
+
.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
292
|
|
|
282
293
|
/* Performance tab */
|
|
283
294
|
.perf-selector{display:flex;gap:6px;flex-wrap:wrap;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
@@ -438,9 +449,8 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
|
438
449
|
.sec-hint{padding:8px 16px;font-size:11px;color:var(--text-muted);background:var(--bg-muted);border-bottom:1px solid var(--border)}
|
|
439
450
|
|
|
440
451
|
/* Items */
|
|
441
|
-
.sec-items{padding:
|
|
442
|
-
.sec-item{display:flex;align-items:center;
|
|
443
|
-
.sec-item:hover{background:var(--bg-hover)}
|
|
452
|
+
.sec-items{padding:2px 0}
|
|
453
|
+
.sec-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:12px;flex-wrap:wrap}
|
|
444
454
|
.sec-item-desc{color:var(--text-dim);line-height:1.5;flex:1;min-width:0}
|
|
445
455
|
.sec-item-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
|
|
446
456
|
.sec-item-count{font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0;margin-left:12px}
|
|
@@ -465,2188 +475,905 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
|
465
475
|
.sec-ai-verified{background:rgba(22,163,74,.1);color:var(--green)}
|
|
466
476
|
.sec-ai-notes{font-size:11px;color:var(--text-muted);font-style:italic;margin-top:2px;padding-left:0}
|
|
467
477
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
.tl-
|
|
478
|
-
|
|
479
|
-
.tl-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
.tl-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
478
|
+
.request-timeline {
|
|
479
|
+
margin-top: 8px;
|
|
480
|
+
background: var(--bg-muted);
|
|
481
|
+
border: 1px solid var(--border);
|
|
482
|
+
border-radius: var(--radius);
|
|
483
|
+
padding: 10px 14px;
|
|
484
|
+
overflow: hidden;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.request-timeline.tl-hidden { display: none; }
|
|
488
|
+
|
|
489
|
+
.tl-header {
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
justify-content: space-between;
|
|
493
|
+
padding: 0 0 8px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.tl-title {
|
|
497
|
+
font-size: 10px;
|
|
498
|
+
text-transform: uppercase;
|
|
499
|
+
letter-spacing: .8px;
|
|
500
|
+
color: var(--text-muted);
|
|
501
|
+
font-weight: 600;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.tl-counts {
|
|
505
|
+
display: flex;
|
|
506
|
+
gap: 10px;
|
|
507
|
+
font-family: var(--mono);
|
|
508
|
+
font-size: 10px;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.tl-count { color: var(--text-dim); }
|
|
512
|
+
.tl-count-query { color: var(--accent); }
|
|
513
|
+
.tl-count-fetch { color: var(--blue); }
|
|
514
|
+
.tl-count-error { color: var(--red); }
|
|
515
|
+
.tl-count-log { color: var(--text-muted); }
|
|
516
|
+
|
|
517
|
+
.tl-loading {
|
|
518
|
+
color: var(--text-muted);
|
|
519
|
+
padding: 4px 0;
|
|
520
|
+
font-size: 10px;
|
|
521
|
+
font-family: var(--mono);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.tl-events {
|
|
525
|
+
position: relative;
|
|
526
|
+
padding-left: 4px;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.tl-event {
|
|
530
|
+
display: flex;
|
|
531
|
+
align-items: center;
|
|
532
|
+
gap: 10px;
|
|
533
|
+
font-family: var(--mono);
|
|
534
|
+
font-size: 11px;
|
|
535
|
+
padding: 4px 0 4px 12px;
|
|
536
|
+
border-left: 2px solid var(--border);
|
|
537
|
+
position: relative;
|
|
538
|
+
flex-wrap: wrap;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.tl-event-time {
|
|
542
|
+
width: 48px;
|
|
543
|
+
color: var(--text-muted);
|
|
544
|
+
font-size: 10px;
|
|
545
|
+
flex-shrink: 0;
|
|
546
|
+
text-align: right;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.tl-event-type {
|
|
550
|
+
width: 44px;
|
|
551
|
+
font-weight: 700;
|
|
552
|
+
font-size: 9px;
|
|
553
|
+
letter-spacing: .5px;
|
|
554
|
+
flex-shrink: 0;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.tl-event-summary {
|
|
558
|
+
flex: 1;
|
|
559
|
+
overflow: hidden;
|
|
560
|
+
text-overflow: ellipsis;
|
|
561
|
+
white-space: nowrap;
|
|
562
|
+
color: var(--text);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.tl-event-status {
|
|
566
|
+
width: 32px;
|
|
567
|
+
text-align: right;
|
|
568
|
+
font-weight: 600;
|
|
569
|
+
flex-shrink: 0;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.tl-event-dur {
|
|
573
|
+
width: 48px;
|
|
574
|
+
text-align: right;
|
|
575
|
+
color: var(--text-muted);
|
|
576
|
+
flex-shrink: 0;
|
|
577
|
+
font-size: 10px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.tl-event.tl-clickable {
|
|
581
|
+
cursor: pointer;
|
|
582
|
+
border-radius: 6px;
|
|
583
|
+
margin-left: -4px;
|
|
584
|
+
padding: 6px 12px;
|
|
585
|
+
border-left: none;
|
|
586
|
+
border: 1px solid var(--border);
|
|
587
|
+
background: var(--bg-card);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.tl-event.tl-clickable:hover {
|
|
591
|
+
background: var(--bg-hover);
|
|
592
|
+
border-color: var(--border-light);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.tl-event-sql {
|
|
596
|
+
width: 100%;
|
|
597
|
+
display: none;
|
|
598
|
+
margin: 4px 0 2px 0;
|
|
599
|
+
padding: 8px 10px;
|
|
600
|
+
background: var(--bg-card);
|
|
601
|
+
border: 1px solid var(--border);
|
|
602
|
+
border-radius: var(--radius-sm);
|
|
603
|
+
font-size: 10px;
|
|
604
|
+
line-height: 1.5;
|
|
605
|
+
white-space: pre-wrap;
|
|
606
|
+
word-break: break-word;
|
|
607
|
+
color: var(--text-dim);
|
|
608
|
+
overflow-x: auto;
|
|
609
|
+
max-height: 150px;
|
|
610
|
+
overflow-y: auto;
|
|
611
|
+
position: relative;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.tl-event-sql.open { display: block; }
|
|
615
|
+
|
|
616
|
+
.tl-sql-copy {
|
|
617
|
+
position: absolute;
|
|
618
|
+
top: 6px;
|
|
619
|
+
right: 6px;
|
|
620
|
+
padding: 2px 8px;
|
|
621
|
+
font-size: 9px;
|
|
622
|
+
font-family: var(--mono);
|
|
623
|
+
background: var(--bg-muted);
|
|
624
|
+
border: 1px solid var(--border);
|
|
625
|
+
border-radius: var(--radius-sm);
|
|
626
|
+
color: var(--text-muted);
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
transition: all .15s;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.tl-sql-copy:hover {
|
|
632
|
+
background: var(--bg-hover);
|
|
633
|
+
color: var(--text);
|
|
634
|
+
border-color: var(--border-light);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.tl-nested {
|
|
638
|
+
margin-left: 24px;
|
|
639
|
+
padding-left: 10px;
|
|
640
|
+
border-left: 2px dashed var(--blue);
|
|
641
|
+
margin-top: 2px;
|
|
642
|
+
margin-bottom: 2px;
|
|
643
|
+
position: relative;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.tl-nested::before {
|
|
647
|
+
content: "";
|
|
648
|
+
position: absolute;
|
|
649
|
+
top: 12px;
|
|
650
|
+
left: -2px;
|
|
651
|
+
width: 8px;
|
|
652
|
+
height: 0;
|
|
653
|
+
border-top: 2px dashed var(--blue);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.tl-nested-label {
|
|
657
|
+
display: block;
|
|
658
|
+
font-family: var(--mono);
|
|
659
|
+
font-size: 9px;
|
|
660
|
+
color: var(--blue);
|
|
661
|
+
letter-spacing: .3px;
|
|
662
|
+
padding: 2px 0 4px;
|
|
663
|
+
opacity: .7;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.tl-nested-event {
|
|
667
|
+
opacity: .9;
|
|
668
|
+
font-size: 10px;
|
|
669
|
+
}
|
|
493
670
|
</style>
|
|
494
671
|
</head>
|
|
495
672
|
<body>
|
|
496
|
-
|
|
497
|
-
<
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
<span class="logo-text">brakit</span>
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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>
|
|
673
|
+
<bk-dashboard></bk-dashboard>
|
|
674
|
+
<script>window.__BRAKIT_CONFIG__={port:{{PORT}},version:"{{VERSION}}"};</script>
|
|
675
|
+
<script>(function(){'use strict';var $s=Object.defineProperty;var ys=Object.getOwnPropertyDescriptor;var u=(o,e,t,s)=>{for(var r=s>1?void 0:s?ys(e,t):e,i=o.length-1,a;i>=0;i--)(a=o[i])&&(r=(s?a(e,t,r):a(r))||r);return s&&r&&$s(e,t,r),r};var Mt=globalThis,Dt=Mt.ShadowRoot&&(Mt.ShadyCSS===void 0||Mt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Re=Symbol(),Te=new WeakMap,Ot=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(Dt&&e===void 0){let s=t!==void 0&&t.length===1;s&&(e=Te.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),s&&Te.set(t,e));}return e}toString(){return this.cssText}},we=o=>new Ot(typeof o=="string"?o:o+"",void 0,Re);var Ce=(o,e)=>{if(Dt)o.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let s=document.createElement("style"),r=Mt.litNonce;r!==void 0&&s.setAttribute("nonce",r),s.textContent=t.cssText,o.appendChild(s);}},zt=Dt?o=>o:o=>o instanceof CSSStyleSheet?(e=>{let t="";for(let s of e.cssRules)t+=s.cssText;return we(t)})(o):o;var{is:Ss,defineProperty:_s,getOwnPropertyDescriptor:xs,getOwnPropertyNames:Ts,getOwnPropertySymbols:Rs,getPrototypeOf:ws}=Object,D=globalThis,Ae=D.trustedTypes,Cs=Ae?Ae.emptyScript:"",As=D.reactiveElementPolyfillSupport,vt=(o,e)=>o,ft={toAttribute(o,e){switch(e){case Boolean:o=o?Cs:null;break;case Object:case Array:o=o==null?o:JSON.stringify(o);}return o},fromAttribute(o,e){let t=o;switch(e){case Boolean:t=o!==null;break;case Number:t=o===null?null:Number(o);break;case Object:case Array:try{t=JSON.parse(o);}catch{t=null;}}return t}},Nt=(o,e)=>!Ss(o,e),ke={attribute:true,type:String,converter:ft,reflect:false,useDefault:false,hasChanged:Nt};Symbol.metadata??(Symbol.metadata=Symbol("metadata")),D.litPropertyMetadata??(D.litPropertyMetadata=new WeakMap);var M=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=ke){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&&_s(this.prototype,e,r);}}static getPropertyDescriptor(e,t,s){let{get:r,set:i}=xs(this.prototype,e)??{get(){return this[t]},set(a){this[t]=a;}};return {get:r,set(a){let c=r?.call(this);i?.call(this,a),this.requestUpdate(e,c,s);},configurable:true,enumerable:true}}static getPropertyOptions(e){return this.elementProperties.get(e)??ke}static _$Ei(){if(this.hasOwnProperty(vt("elementProperties")))return;let e=ws(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties);}static finalize(){if(this.hasOwnProperty(vt("finalized")))return;if(this.finalized=true,this._$Ei(),this.hasOwnProperty(vt("properties"))){let t=this.properties,s=[...Ts(t),...Rs(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(zt(r));}else e!==void 0&&t.push(zt(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 Ce(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 i=(s.converter?.toAttribute!==void 0?s.converter:ft).toAttribute(t,s.type);this._$Em=e,i==null?this.removeAttribute(r):this.setAttribute(r,i),this._$Em=null;}}_$AK(e,t){let s=this.constructor,r=s._$Eh.get(e);if(r!==void 0&&this._$Em!==r){let i=s.getPropertyOptions(r),a=typeof i.converter=="function"?{fromAttribute:i.converter}:i.converter?.fromAttribute!==void 0?i.converter:ft;this._$Em=r;let c=a.fromAttribute(t,i.type);this[r]=c??this._$Ej?.get(r)??c,this._$Em=null;}}requestUpdate(e,t,s,r=false,i){if(e!==void 0){let a=this.constructor;if(r===false&&(i=this[e]),s??(s=a.getPropertyOptions(e)),!((s.hasChanged??Nt)(i,t)||s.useDefault&&s.reflect&&i===this._$Ej?.get(e)&&!this.hasAttribute(a._$Eu(e,s))))return;this.C(e,t,s);}this.isUpdatePending===false&&(this._$ES=this._$EP());}C(e,t,{useDefault:s,reflect:r,wrapped:i},a){s&&!(this._$Ej??(this._$Ej=new Map)).has(e)&&(this._$Ej.set(e,a??t??this[e]),i!==true||a!==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,i]of this._$Ep)this[r]=i;this._$Ep=void 0;}let s=this.constructor.elementProperties;if(s.size>0)for(let[r,i]of s){let{wrapped:a}=i,c=this[r];a!==true||this._$AL.has(r)||c===void 0||this.C(r,void 0,i,c);}}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){}};M.elementStyles=[],M.shadowRootOptions={mode:"open"},M[vt("elementProperties")]=new Map,M[vt("finalized")]=new Map,As?.({ReactiveElement:M}),(D.reactiveElementVersions??(D.reactiveElementVersions=[])).push("2.1.2");var bt=globalThis,Le=o=>o,qt=bt.trustedTypes,Ie=qt?qt.createPolicy("lit-html",{createHTML:o=>o}):void 0,He="$lit$",N=`lit$${Math.random().toFixed(9).slice(2)}$`,Pe="?"+N,ks=`<${Pe}>`,j=document,Et=()=>j.createComment(""),$t=o=>o===null||typeof o!="object"&&typeof o!="function",ie=Array.isArray,Ls=o=>ie(o)||typeof o?.[Symbol.iterator]=="function",Jt=`[
|
|
676
|
+
\f\r]`,gt=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Me=/-->/g,Oe=/>/g,B=RegExp(`>|${Jt}(?:([^\\s"'>=/]+)(${Jt}*=${Jt}*(?:[^
|
|
677
|
+
\f\r"'\`<>=]|("|')|))|$)`,"g"),De=/'/g,Ne=/"/g,Ue=/^(?:script|style|textarea|title)$/i,oe=o=>(e,...t)=>({_$litType$:o,strings:e,values:t}),n=oe(1),Q=Symbol.for("lit-noChange"),d=Symbol.for("lit-nothing"),qe=new WeakMap,G=j.createTreeWalker(j,129);function Fe(o,e){if(!ie(o)||!o.hasOwnProperty("raw"))throw Error("invalid template strings array");return Ie!==void 0?Ie.createHTML(e):e}var Is=(o,e)=>{let t=o.length-1,s=[],r,i=e===2?"<svg>":e===3?"<math>":"",a=gt;for(let c=0;c<t;c++){let l=o[c],h,m,p=-1,v=0;for(;v<l.length&&(a.lastIndex=v,m=a.exec(l),m!==null);)v=a.lastIndex,a===gt?m[1]==="!--"?a=Me:m[1]!==void 0?a=Oe:m[2]!==void 0?(Ue.test(m[2])&&(r=RegExp("</"+m[2],"g")),a=B):m[3]!==void 0&&(a=B):a===B?m[0]===">"?(a=r??gt,p=-1):m[1]===void 0?p=-2:(p=a.lastIndex-m[2].length,h=m[1],a=m[3]===void 0?B:m[3]==='"'?Ne:De):a===Ne||a===De?a=B:a===Me||a===Oe?a=gt:(a=B,r=void 0);let E=a===B&&o[c+1].startsWith("/>")?" ":"";i+=a===gt?l+ks:p>=0?(s.push(h),l.slice(0,p)+He+l.slice(p)+N+E):l+N+(p===-2?c:E);}return [Fe(o,i+(o[t]||"<?>")+(e===2?"</svg>":e===3?"</math>":"")),s]},yt=class o{constructor({strings:e,_$litType$:t},s){let r;this.parts=[];let i=0,a=0,c=e.length-1,l=this.parts,[h,m]=Is(e,t);if(this.el=o.createElement(h,s),G.currentNode=this.el.content,t===2||t===3){let p=this.el.content.firstChild;p.replaceWith(...p.childNodes);}for(;(r=G.nextNode())!==null&&l.length<c;){if(r.nodeType===1){if(r.hasAttributes())for(let p of r.getAttributeNames())if(p.endsWith(He)){let v=m[a++],E=r.getAttribute(p).split(N),C=/([.?@])?(.*)/.exec(v);l.push({type:1,index:i,name:C[2],strings:E,ctor:C[1]==="."?te:C[1]==="?"?ee:C[1]==="@"?se:st}),r.removeAttribute(p);}else p.startsWith(N)&&(l.push({type:6,index:i}),r.removeAttribute(p));if(Ue.test(r.tagName)){let p=r.textContent.split(N),v=p.length-1;if(v>0){r.textContent=qt?qt.emptyScript:"";for(let E=0;E<v;E++)r.append(p[E],Et()),G.nextNode(),l.push({type:2,index:++i});r.append(p[v],Et());}}}else if(r.nodeType===8)if(r.data===Pe)l.push({type:2,index:i});else {let p=-1;for(;(p=r.data.indexOf(N,p+1))!==-1;)l.push({type:7,index:i}),p+=N.length-1;}i++;}}static createElement(e,t){let s=j.createElement("template");return s.innerHTML=e,s}};function et(o,e,t=o,s){if(e===Q)return e;let r=s!==void 0?t._$Co?.[s]:t._$Cl,i=$t(e)?void 0:e._$litDirective$;return r?.constructor!==i&&(r?._$AO?.(false),i===void 0?r=void 0:(r=new i(o),r._$AT(o,t,s)),s!==void 0?(t._$Co??(t._$Co=[]))[s]=r:t._$Cl=r),r!==void 0&&(e=et(o,r._$AS(o,e.values),r,s)),e}var Zt=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);G.currentNode=r;let i=G.nextNode(),a=0,c=0,l=s[0];for(;l!==void 0;){if(a===l.index){let h;l.type===2?h=new St(i,i.nextSibling,this,e):l.type===1?h=new l.ctor(i,l.name,l.strings,this,e):l.type===6&&(h=new re(i,this,e)),this._$AV.push(h),l=s[++c];}a!==l?.index&&(i=G.nextNode(),a++);}return G.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++;}},St=class o{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=et(this,e,t),$t(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):Ls(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&&$t(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=yt.createElement(Fe(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===r)this._$AH.p(t);else {let i=new Zt(r,this),a=i.u(this.options);i.p(t),this.T(a),this._$AH=i;}}_$AC(e){let t=qe.get(e.strings);return t===void 0&&qe.set(e.strings,t=new yt(e)),t}k(e){ie(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,s,r=0;for(let i of e)r===t.length?t.push(s=new o(this.O(Et()),this.O(Et()),this,this.options)):s=t[r],s._$AI(i),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));}},st=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,s,r,i){this.type=1,this._$AH=d,this._$AN=void 0,this.element=e,this.name=t,this._$AM=r,this.options=i,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 i=this.strings,a=false;if(i===void 0)e=et(this,e,t,0),a=!$t(e)||e!==this._$AH&&e!==Q,a&&(this._$AH=e);else {let c=e,l,h;for(e=i[0],l=0;l<i.length-1;l++)h=et(this,c[s+l],t,l),h===Q&&(h=this._$AH[l]),a||(a=!$t(h)||h!==this._$AH[l]),h===d?e=d:e!==d&&(e+=(h??"")+i[l+1]),this._$AH[l]=h;}a&&!r&&this.j(e);}j(e){e===d?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??"");}},te=class extends st{constructor(){super(...arguments),this.type=3;}j(e){this.element[this.name]=e===d?void 0:e;}},ee=class extends st{constructor(){super(...arguments),this.type=4;}j(e){this.element.toggleAttribute(this.name,!!e&&e!==d);}},se=class extends st{constructor(e,t,s,r,i){super(e,t,s,r,i),this.type=5;}_$AI(e,t=this){if((e=et(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,i=e!==d&&(s===d||r);r&&this.element.removeEventListener(this.name,this,s),i&&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);}},re=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){et(this,e);}};var Ms=bt.litHtmlPolyfillSupport;Ms?.(yt,St),(bt.litHtmlVersions??(bt.litHtmlVersions=[])).push("3.3.2");var Be=(o,e,t)=>{let s=t?.renderBefore??e,r=s._$litPart$;if(r===void 0){let i=t?.renderBefore??null;s._$litPart$=r=new St(e.insertBefore(Et(),i),i,void 0,t??{});}return r._$AI(o),r};var _t=globalThis,f=class extends M{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,_t.litElementHydrateSupport?.({LitElement:f});var Os=_t.litElementPolyfillSupport;Os?.({LitElement:f});(_t.litElementVersions??(_t.litElementVersions=[])).push("4.2.2");var g=o=>(e,t)=>{t!==void 0?t.addInitializer(()=>{customElements.define(o,e);}):customElements.define(o,e);};var Ds={attribute:true,type:String,converter:ft,reflect:false,hasChanged:Nt},Ns=(o=Ds,e,t)=>{let{kind:s,metadata:r}=t,i=globalThis.litPropertyMetadata.get(r);if(i===void 0&&globalThis.litPropertyMetadata.set(r,i=new Map),s==="setter"&&((o=Object.create(o)).wrapped=true),i.set(t.name,o),s==="accessor"){let{name:a}=t;return {set(c){let l=e.get.call(this);e.set.call(this,c),this.requestUpdate(a,l,o,true,c);},init(c){return c!==void 0&&this.C(a,void 0,o,c),c}}}if(s==="setter"){let{name:a}=t;return function(c){let l=this[a];e.call(this,c),this.requestUpdate(a,l,o,true,c);}}throw Error("Unsupported decorator location: "+s)};function y(o){return (e,t)=>typeof t=="object"?Ns(o,e,t):((s,r,i)=>{let a=r.hasOwnProperty(i);return r.constructor.createProperty(i,s),a?Object.getOwnPropertyDescriptor(r,i):void 0})(o,e,t)}function $(o){return y({...o,state:true,attribute:false})}var xt=class extends f{constructor(){super(...arguments);this.method="";}createRenderRoot(){return this}render(){let t=this.method.toUpperCase();return n`<span class="method-badge method-badge-${t}">${t}</span>`}};u([y()],xt.prototype,"method",2),xt=u([g("bk-method-badge")],xt);var L="/__brakit/api",O="/__brakit",R={flows:`${L}/flows`,requests:`${L}/requests`,events:`${L}/events`,clear:`${L}/clear`,fetches:`${L}/fetches`,errors:`${L}/errors`,logs:`${L}/logs`,queries:`${L}/queries`,metricsLive:`${L}/metrics/live`,insights:`${L}/insights`,tab:`${L}/tab`,activity:`${L}/activity`};var Tt="polling",Pt="static",qs="auth-handshake",Hs="auth-check",Ps="middleware",Ut={[qs]:1,[Hs]:1,[Ps]:1};var ne="fetch";var ae="error_event",ce="query",le="issues";var de={overview:"Overview",queries:"Queries",requests:"Requests",actions:"Actions",errors:"Errors",security:"Security",fetches:"Fetches",logs:"Logs",performance:"Performance"};var Rt={overview:"Overview",actions:"Actions",requests:"Requests",fetches:"Server Fetches",queries:"Queries",errors:"Errors",logs:"Logs",performance:"Performance",security:"Security"},pe={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 he=100,V=300,W=800,me=2e3,ve=100,fe=50,ge=500;var q="__all__",jt={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)"},be=["#2563eb","#7c3aed","#16a34a","#d97706","#dc2626","#0891b2","#ea580c","#c026d3","#059669","#db2777"],wt={green:"#4ade80",amber:"#fbbf24",red:"#f87171"},Qt=[{max:he,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:W,label:"OK",color:"var(--amber)",bg:"rgba(217,119,6,0.06)",border:"rgba(217,119,6,0.15)"},{max:me,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)",Ee="rgba(113,113,122,0.7)",Ze="10px monospace",$e="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"]),H={critical:{icon:"\u2717",cls:"critical",sort:0},warning:{icon:"\u26A0",cls:"warning",sort:1},info:{icon:"\u2139",cls:"info",sort:2}};function S(o){return o<1e3?o+"ms":(o/1e3).toFixed(1)+"s"}function Y(o){return !o||o===0?"":o<1024?o+"b":(o/1024).toFixed(1)+"kb"}function P(o){return o?o.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}function rt(o){return o>=500?"status-pill-5xx":o>=400?"status-pill-4xx":o>=300?"status-pill-3xx":"status-pill-2xx"}function as(o){return os[o]||(o>=500?"Server Error":o>=400?"Client Error":"OK")}function Us(o,e){if(is.has(o.toLowerCase())){let t=String(e);return t.length<=8?"****":t.slice(0,4)+"..."+t.slice(-4)+" ("+t.length+" chars)"}return String(e)}function it(o){return !o||Object.keys(o).length===0?'<span style="color:var(--text-muted)">No headers</span>':Object.entries(o).map(([e,t])=>'<span class="json-key">'+P(e)+"</span>: "+P(Us(e,t))).join(`
|
|
678
|
+
`)}function X(o){if(!o)return '<span style="color:var(--text-muted)">No body</span>';try{let e=JSON.parse(o);return Fs(JSON.stringify(e,null,2))}catch{return P(o)}}function Fs(o){return P(o).replace(/("(?:[^"\\]|\\.)*")(\s*:)?|\b(true|false)\b|\bnull\b|(-?\d+\.?\d*(?:[eE][+-]?\d+)?)/g,(e,t,s,r,i)=>t?s?'<span class="json-key">'+t+"</span>"+s:'<span class="json-str">'+t+"</span>":r?'<span class="json-bool">'+e+"</span>":i?'<span class="json-num">'+e+"</span>":e==="null"?'<span class="json-null">null</span>':e)}var Ct=class extends f{constructor(){super(...arguments);this.code=0;}createRenderRoot(){return this}render(){let t=rt(this.code);return n`<span class="status-pill ${t}">${this.code}</span>`}};u([y({type:Number})],Ct.prototype,"code",2),Ct=u([g("bk-status-pill")],Ct);var At=class extends f{constructor(){super(...arguments);this.ms=0;}createRenderRoot(){return this}render(){return n`<span class="req-duration">${S(this.ms)}</span>`}};u([y({type:Number})],At.prototype,"ms",2),At=u([g("bk-duration-label")],At);var ot=class extends f{constructor(){super(...arguments);this.title="";this.subtitle="";}createRenderRoot(){return this}render(){return n`
|
|
679
|
+
<div class="empty">
|
|
680
|
+
<span class="empty-title">${this.title}</span>
|
|
681
|
+
<span class="empty-sub">${this.subtitle}</span>
|
|
682
|
+
</div>
|
|
683
|
+
`}};u([y()],ot.prototype,"title",2),u([y()],ot.prototype,"subtitle",2),ot=u([g("bk-empty-state")],ot);var k=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 n`<div class="toast ${this.visible?"show":""}">${this.message}</div>`}};u([$()],k.prototype,"message",2),u([$()],k.prototype,"visible",2),k=u([g("bk-toast")],k);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),k.show(this.toastMessage);}catch{}}render(){return n`<button class="query-detail-copy" @click=${this.copy}>${this.label}</button>`}};u([y()],K.prototype,"text",2),u([y()],K.prototype,"label",2),u([y({attribute:"toast-message"})],K.prototype,"toastMessage",2),K=u([g("bk-copy-button")],K);var z=class extends f{constructor(){super(...arguments);this.value="";this.label="";this.color="";}createRenderRoot(){return this}render(){return n`
|
|
684
|
+
<div class="fetch-stat">
|
|
685
|
+
<span class="fetch-stat-value" style="color:${this.color}">${this.value}</span>
|
|
686
|
+
<span class="fetch-stat-label">${this.label}</span>
|
|
557
687
|
</div>
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
688
|
+
`}};u([y()],z.prototype,"value",2),u([y()],z.prototype,"label",2),u([y()],z.prototype,"color",2),z=u([g("bk-stat-card")],z);var U=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 nt=class{constructor(e,t,s,r){if(this.subscribe=false,this.provided=false,this.value=void 0,this.t=(i,a)=>{this.unsubscribe&&(this.unsubscribe!==a&&(this.provided=false,this.unsubscribe()),this.subscribe||this.unsubscribe()),this.value=i,this.host.requestUpdate(),this.provided&&!this.subscribe||(this.provided=true,this.callback&&this.callback(i,a)),this.unsubscribe=a;},this.host=e,t.context!==void 0){let i=t;this.context=i.context,this.callback=i.callback,this.subscribe=i.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 U(this.context,this.host,this.t,this.subscribe));}};var Vt=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 ye=class extends Event{constructor(e,t){super("context-provider",{bubbles:true,composed:true}),this.context=e,this.contextTarget=t;}},at=class extends Vt{constructor(e,t,s){super(t.context!==void 0?t.initialValue:s),this.onContextRequest=r=>{if(r.context!==this.context)return;let i=r.contextTarget??r.composedPath()[0];i!==this.host&&(r.stopPropagation(),this.addCallback(r.callback,i,r.subscribe));},this.onProviderRequest=r=>{if(r.context!==this.context||(r.contextTarget??r.composedPath()[0])===this.host)return;let i=new Set;for(let[a,{consumerHost:c}]of this.subscriptions)i.has(a)||(i.add(a),c.dispatchEvent(new U(this.context,c,a,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 ye(this.context,this.host));}};function Se({context:o}){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 at(this,{context:o,initialValue:r})),r}};{e.constructor.addInitializer((a=>{s.set(a,new at(a,{context:o}));}));let r=Object.getOwnPropertyDescriptor(e,t),i;if(r===void 0){let a=new WeakMap;i={get(){return a.get(this)},set(c){s.get(this).setValue(c),a.set(this,c);},configurable:true,enumerable:true};}else {let a=r.set;i={...r,set(c){s.get(this).setValue(c),a?.call(this,c);}};}return void Object.defineProperty(e,t,i)}}}function T({context:o,subscribe:e}){return (t,s)=>{typeof s=="object"?s.addInitializer((function(){new nt(this,{context:o,callback:r=>{t.set.call(this,r);},subscribe:e});})):t.constructor.addInitializer((r=>{new nt(r,{context:o,callback:i=>{r[s]=i;},subscribe:e});}));}}var _="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 ct=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(),i=this.expandedIdx===s;return n`
|
|
689
|
+
<div
|
|
690
|
+
class="req-row tel-clickable ${i?"expanded":""}"
|
|
691
|
+
@click=${()=>this.toggleError(s)}
|
|
692
|
+
>
|
|
693
|
+
<span class="tel-error-name" title=${t.name}>${t.name}</span>
|
|
694
|
+
<span class="tel-message" title=${t.message}>${t.message}</span>
|
|
695
|
+
<span class="tel-timestamp">${r}</span>
|
|
696
|
+
</div>
|
|
697
|
+
${i&&t.stack?n`<div class="error-stack">${t.stack}</div>`:d}
|
|
698
|
+
`}render(){let t=this.store.state.errors;return t.length===0?n`<bk-empty-state
|
|
699
|
+
title="No errors"
|
|
700
|
+
subtitle="No errors have been captured yet"
|
|
701
|
+
></bk-empty-state>`:n`
|
|
702
|
+
<div class="col-header">
|
|
703
|
+
<span style="width:180px">Type</span>
|
|
704
|
+
<span style="flex:1">Message</span>
|
|
705
|
+
<span style="width:130px;text-align:right">Time</span>
|
|
706
|
+
</div>
|
|
707
|
+
<div id="error-list">
|
|
708
|
+
${t.map((s,r)=>this.renderErrorRow(s,r))}
|
|
709
|
+
</div>
|
|
710
|
+
`}};u([T({context:_})],ct.prototype,"store",2),u([$()],ct.prototype,"expandedIdx",2),ct=u([g("bk-errors-view")],ct);var kt=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 n`
|
|
711
|
+
<div id="log-analysis">
|
|
712
|
+
<div class="fetch-summary">
|
|
713
|
+
<bk-stat-card value=${String(e.length)} label="Total Logs"></bk-stat-card>
|
|
714
|
+
${t.error>0?n`<bk-stat-card value=${String(t.error)} label="Errors" color="var(--red)"></bk-stat-card>`:d}
|
|
715
|
+
${t.warn>0?n`<bk-stat-card value=${String(t.warn)} label="Warnings" color="var(--amber)"></bk-stat-card>`:d}
|
|
716
|
+
<bk-stat-card value=${String(t.info)} label="Info"></bk-stat-card>
|
|
717
|
+
${t.debug>0?n`<bk-stat-card value=${String(t.debug)} label="Debug"></bk-stat-card>`:d}
|
|
718
|
+
${t.log>0?n`<bk-stat-card value=${String(t.log)} label="Log"></bk-stat-card>`:d}
|
|
562
719
|
</div>
|
|
563
|
-
<button class="btn btn-danger" id="clear-btn">Clear</button>
|
|
564
720
|
</div>
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
<
|
|
721
|
+
`}renderLogRow(e){let t=new Date(e.timestamp).toLocaleTimeString();return n`
|
|
722
|
+
<div class="req-row">
|
|
723
|
+
<span class="tel-level tel-level-${e.level}">${e.level.toUpperCase()}</span>
|
|
724
|
+
<span class="tel-message tel-mono" title=${e.message}>${e.message}</span>
|
|
725
|
+
<span class="tel-timestamp">${t}</span>
|
|
726
|
+
</div>
|
|
727
|
+
`}render(){let e=this.store.state.logs;return e.length===0?n`<bk-empty-state
|
|
728
|
+
title="No logs"
|
|
729
|
+
subtitle="No console output has been captured yet"
|
|
730
|
+
></bk-empty-state>`:n`
|
|
731
|
+
${this.renderAnalysis(e)}
|
|
732
|
+
<div class="col-header">
|
|
733
|
+
<span style="width:52px">Level</span>
|
|
734
|
+
<span style="flex:1">Message</span>
|
|
735
|
+
<span style="width:130px;text-align:right">Time</span>
|
|
569
736
|
</div>
|
|
570
|
-
<div
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
737
|
+
<div id="log-list">
|
|
738
|
+
${e.map(t=>this.renderLogRow(t))}
|
|
739
|
+
</div>
|
|
740
|
+
`}};u([T({context:_})],kt.prototype,"store",2),kt=u([g("bk-logs-view")],kt);var Gs=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 cs(o){let e=o.trim().match(/^(\w+)/);return e?e[1].toUpperCase():"?"}function ls(o){let e=o.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(o){return P(o).replace(/\b\w+\b/g,e=>Gs.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?cs(t.sql):"?")).toUpperCase(),r=t.table||t.model||(t.sql?ls(t.sql):""),i=t.sql||s+" "+r;return {op:s,table:r,sqlText:i}}renderQueryRow(t,s){let{op:r,table:i,sqlText:a}=this.getQueryInfo(t),c=jt[r]||"var(--text-dim)",l=t.durationMs>ve,h=t.sql||r+" "+i,m=this.expandedIdx===s;return n`
|
|
741
|
+
<div>
|
|
742
|
+
<div
|
|
743
|
+
class="req-row query-row tel-clickable ${m?"expanded":""}"
|
|
744
|
+
@click=${()=>this.toggleQuery(s)}
|
|
745
|
+
>
|
|
746
|
+
<span class="query-op" title=${r} style="color:${c}">${r}</span>
|
|
747
|
+
<span class="query-table" title=${i}>${i}</span>
|
|
748
|
+
<span class="query-preview" title=${h}>${h}</span>
|
|
749
|
+
<span class="query-dur${l?" query-slow":""}">${this.queryDuration(t.durationMs)}</span>
|
|
750
|
+
</div>
|
|
751
|
+
<div class="query-detail ${m?"open":""}">
|
|
752
|
+
${m?n`
|
|
753
|
+
<pre class="query-detail-sql" .innerHTML=${ds(a)}></pre>
|
|
754
|
+
<bk-copy-button .text=${a} label="Copy"></bk-copy-button>
|
|
755
|
+
`:d}
|
|
577
756
|
</div>
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
757
|
+
</div>
|
|
758
|
+
`}render(){let t=this.store.state.queries;return t.length===0?n`<bk-empty-state
|
|
759
|
+
title="No queries"
|
|
760
|
+
subtitle="No database queries have been captured yet"
|
|
761
|
+
></bk-empty-state>`:n`
|
|
762
|
+
<div class="col-header">
|
|
763
|
+
<span style="width:70px;border-right:1px solid var(--border);padding-right:16px">Operation</span>
|
|
764
|
+
<span style="width:170px;border-right:1px solid var(--border);padding-right:16px">Table</span>
|
|
765
|
+
<span style="flex:1;border-right:1px solid var(--border);padding-right:16px">Query</span>
|
|
766
|
+
<span style="width:60px;text-align:right">Time</span>
|
|
767
|
+
</div>
|
|
768
|
+
<div id="query-list">
|
|
769
|
+
${t.map((s,r)=>this.renderQueryRow(s,r))}
|
|
770
|
+
</div>
|
|
771
|
+
`}};u([T({context:_})],lt.prototype,"store",2),u([$()],lt.prototype,"expandedIdx",2),lt=u([g("bk-queries-view")],lt);function _e(o){return o.replace(/'/g,"'\\''")}function js(o,e){let t=Object.entries(o.headers||{}).filter(([i])=>!ns.has(i)).map(([i,a])=>`-H '${_e(i)}: ${_e(a)}'`).join(" "),s=o.requestBody?` -d '${_e(o.requestBody)}'`:"",r=e?`http://localhost:${e}`:"";return `curl -X ${o.method} ${t}${s} '${r}${o.url}'`}function dt(o){let e=window.__BRAKIT_CONFIG__?.port??"",t=js(o,e);navigator.clipboard.writeText(t).then(()=>k.show("Copied cURL command"));}var pt=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(),dt(t);}renderDetail(t){return n`
|
|
772
|
+
<div class="detail-meta">
|
|
773
|
+
<span><bk-method-badge .method=${t.method}></bk-method-badge> ${t.url}</span>
|
|
774
|
+
<span><bk-status-pill .code=${t.statusCode}></bk-status-pill></span>
|
|
775
|
+
<span>${t.durationMs}ms</span>
|
|
776
|
+
${t.responseSize?n`<span>${Y(t.responseSize)}</span>`:d}
|
|
777
|
+
</div>
|
|
778
|
+
<div class="request-timeline tl-hidden" data-request-id=${t.id} data-request-started=${String(t.startedAt)}></div>
|
|
779
|
+
<div class="detail-grid">
|
|
780
|
+
<div class="detail-section"><h4>Request Headers</h4><pre .innerHTML=${it(t.headers)}></pre></div>
|
|
781
|
+
<div class="detail-section"><h4>Response Headers</h4><pre .innerHTML=${it(t.responseHeaders)}></pre></div>
|
|
782
|
+
<div class="detail-section"><h4>Request Body</h4><pre .innerHTML=${X(t.requestBody)}></pre></div>
|
|
783
|
+
<div class="detail-section"><h4>Response Body</h4><pre .innerHTML=${X(t.responseBody)}></pre></div>
|
|
784
|
+
</div>
|
|
785
|
+
<div class="detail-actions">
|
|
786
|
+
<button class="btn btn-curl" @click=${s=>this.handleCopyAsCurl(t,s)}>Copy cURL</button>
|
|
787
|
+
</div>
|
|
788
|
+
`}renderRequestRow(t){let s=this.expandedId===t.id;return n`
|
|
789
|
+
<div class="req-row ${s?"expanded":""}" @click=${()=>this.toggleRequest(t.id)}>
|
|
790
|
+
<div class="req-summary">
|
|
791
|
+
<bk-method-badge .method=${t.method}></bk-method-badge>
|
|
792
|
+
<span class="req-url">${t.url}</span>
|
|
793
|
+
<bk-status-pill .code=${t.statusCode}></bk-status-pill>
|
|
794
|
+
<bk-duration-label .ms=${t.durationMs}></bk-duration-label>
|
|
795
|
+
<span class="req-size">${Y(t.responseSize)}</span>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
<div class="req-detail ${s?"open":""}">${s?this.renderDetail(t):d}</div>
|
|
799
|
+
`}render(){let t=this.store.state.requests.filter(s=>!s.path?.startsWith(O));return t.length===0?n`<bk-empty-state title="No requests" subtitle="No HTTP requests have been captured yet"></bk-empty-state>`:n`
|
|
800
|
+
<div class="col-header">
|
|
801
|
+
<span style="width:60px">Method</span>
|
|
802
|
+
<span style="flex:1">URL</span>
|
|
803
|
+
<span style="width:36px;text-align:right">Status</span>
|
|
804
|
+
<span style="width:70px;text-align:right">Time</span>
|
|
805
|
+
<span style="width:60px;text-align:right">Size</span>
|
|
806
|
+
</div>
|
|
807
|
+
<div id="request-list">${t.map(s=>this.renderRequestRow(s))}</div>
|
|
808
|
+
`}};u([T({context:_})],pt.prototype,"store",2),u([$()],pt.prototype,"expandedId",2),pt=u([g("bk-requests-view")],pt);var Lt=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 i of t)s.set(i.id,i);let r={};for(let i of e){let a=i.method+" "+i.url;r[a]||(r[a]={method:i.method,url:i.url,count:0,totalDur:0,maxDur:0,errors:0,callers:{},statusCodes:{},firstTs:i.timestamp,lastTs:i.timestamp});let c=r[a];if(c.count++,c.totalDur+=i.durationMs,i.durationMs>c.maxDur&&(c.maxDur=i.durationMs),i.statusCode>=400&&c.errors++,c.statusCodes[i.statusCode]=(c.statusCodes[i.statusCode]||0)+1,i.timestamp<c.firstTs&&(c.firstTs=i.timestamp),i.timestamp>c.lastTs&&(c.lastTs=i.timestamp),i.parentRequestId){let l=s.get(i.parentRequestId);l&&(c.callers[l.method+" "+(l.path||l.url)]=1);}}return Object.values(r).sort((i,a)=>a.count-i.count)}renderSummary(e){let t=new Set,s=0,r=0;for(let a of e)t.add(a.url),a.statusCode>=400&&s++,r+=a.durationMs;let i=Math.round(r/e.length);return n`
|
|
809
|
+
<div class="fetch-summary">
|
|
810
|
+
<bk-stat-card value=${String(e.length)} label="Total Fetches"></bk-stat-card>
|
|
811
|
+
<bk-stat-card value=${String(t.size)} label="Unique URLs"></bk-stat-card>
|
|
812
|
+
<bk-stat-card value=${String(s)} label="Errors" color=${s>0?"var(--red)":""}></bk-stat-card>
|
|
813
|
+
<bk-stat-card value=${S(i)} label="Avg Duration"></bk-stat-card>
|
|
814
|
+
</div>
|
|
815
|
+
`}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),i=Object.entries(e.statusCodes),a=i.length>0?Number(i.sort((c,l)=>l[1]-c[1])[0][0]):0;return n`
|
|
816
|
+
<div class="fetch-group">
|
|
817
|
+
<div class="fetch-group-header">
|
|
818
|
+
<bk-method-badge .method=${e.method}></bk-method-badge>
|
|
819
|
+
<span class="fetch-group-url" title=${e.url}>${e.url}</span>
|
|
820
|
+
${a>0?n`<bk-status-pill .code=${a}></bk-status-pill>`:d}
|
|
821
|
+
<span class="fetch-group-count">${e.count}x</span>
|
|
822
|
+
</div>
|
|
823
|
+
<div class="fetch-group-meta">
|
|
824
|
+
<span>avg ${S(t)}</span>
|
|
825
|
+
<span class="fetch-group-sep">\u00b7</span>
|
|
826
|
+
<span>max ${S(e.maxDur)}</span>
|
|
827
|
+
<span class="fetch-group-sep">\u00b7</span>
|
|
828
|
+
${s>0?n`<span class="fetch-group-err">${s}% errors</span>`:n`<span class="fetch-group-ok">0% errors</span>`}
|
|
829
|
+
</div>
|
|
830
|
+
${e.firstTs>0?n`
|
|
831
|
+
<div class="fetch-group-timeline">
|
|
832
|
+
<span class="fetch-group-timeline-dot"></span>
|
|
833
|
+
<span class="fetch-group-timeline-range">
|
|
834
|
+
${this.formatTime(e.firstTs)}${e.firstTs!==e.lastTs?n` \u2192 ${this.formatTime(e.lastTs)}`:d}
|
|
835
|
+
</span>
|
|
836
|
+
</div>`:d}
|
|
837
|
+
${r.length>0?n`
|
|
838
|
+
<div class="fetch-group-callers">
|
|
839
|
+
<span class="fetch-group-callers-label">Called by</span>
|
|
840
|
+
${r.map(c=>n`<span class="fetch-group-caller-pill">${c}</span>`)}
|
|
841
|
+
</div>`:d}
|
|
842
|
+
</div>
|
|
843
|
+
`}render(){let e=this.store.state.fetches,t=this.store.state.requests;if(e.length===0)return n`<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 n`
|
|
844
|
+
<div class="fetch-analysis" id="fetch-analysis">
|
|
845
|
+
${this.renderSummary(e)}
|
|
846
|
+
${s.length>0?n`
|
|
847
|
+
<div class="fetch-groups-title">Grouped by URL (${s.length})</div>
|
|
848
|
+
<div class="fetch-groups">${s.map(r=>this.renderGroup(r))}</div>
|
|
849
|
+
`:d}
|
|
850
|
+
</div>
|
|
851
|
+
`}};u([T({context:_})],Lt.prototype,"store",2),Lt=u([g("bk-fetches-view")],Lt);var J=class extends f{constructor(){super(...arguments);this.expandedFlowIdx=-1;this.expandedSubReqIdx=-1;}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"}}analyzeFlow(t){let s=t.requests,r=[],i=[],a=[],c=[],l=new Map;for(let p of s){let v=p.label,E=p.pollingDurationMs||p.durationMs;if(!Ut[p.category||""]){if(p.isDuplicate){let C=l.get(v);C?(C.count++,C.wastedMs+=E):l.set(v,{name:v,count:2,wastedMs:E});continue}if(p.statusCode>=400){i.push(v+" ("+as(p.statusCode)+")");continue}p.responseSize>51200&&a.push("Large response: "+v+" returned "+Y(p.responseSize)),r.push(v);}}for(let p of l.values())c.push(p);let h="";if(c.length>0){let p=c.map(E=>E.name).join(", "),v=c.reduce((E,C)=>E+C.wastedMs,0);h="Your app fetches "+p+" multiple times on this page. This wastes ~"+S(v)+". Try caching these calls, deduplicating with React Query/SWR, or moving them to a shared layout.";}else i.length>0&&(h="Some requests are failing. Check your API routes and make sure the endpoints exist.");let m=s.filter(p=>p.durationMs>2e3&&p.category!==Tt);return m.length>0&&!h&&(h=m.map(p=>p.label).join(", ")+` is taking over ${S(2e3)}. Consider adding caching or optimizing the backend query.`),{successes:r,errors:i,warnings:a,duplicates:c,tip:h}}toggleFlow(t){this.expandedFlowIdx===t?this.expandedFlowIdx=-1:(this.expandedFlowIdx=t,this.expandedSubReqIdx=-1);}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 i=r.querySelector("pre");i&&i.classList.toggle("open");}loadTimelineForContainer(t){let s=t.querySelectorAll(".request-timeline");for(let r of s){let i=r.getAttribute("data-request-id");if(i&&!r.hasAttribute("data-loaded")){r.setAttribute("data-loaded","1");let a=document.createElement("bk-timeline-panel");a.setAttribute("request-id",i),a.setAttribute("request-started",r.getAttribute("data-request-started")||"0"),r.appendChild(a),r.classList.remove("tl-hidden");}}}updated(){if(this.expandedFlowIdx>=0){let t=this.querySelector(".flow-expand.open");t&&this.loadTimelineForContainer(t);}}render(){let t=this.flows;return t.length===0?n`<bk-empty-state title="No actions yet" subtitle="Start using your app to see user action flows here"></bk-empty-state>`:n`
|
|
852
|
+
<div id="flow-col-header" class="col-header">
|
|
853
|
+
<span style="width:8px"></span>
|
|
854
|
+
<span style="flex:1">Action</span>
|
|
855
|
+
<span style="width:60px;text-align:right">Reqs</span>
|
|
856
|
+
<span style="width:120px;text-align:right">Status</span>
|
|
857
|
+
<span style="width:70px;text-align:right">Time</span>
|
|
858
|
+
</div>
|
|
859
|
+
<div id="flow-list">${t.map((s,r)=>this.renderFlowRow(s,r))}</div>
|
|
860
|
+
`}renderFlowRow(t,s){let r=this.expandedFlowIdx===s,i=this.flowDotClass(t),a=this.flowBadgeInfo(t);return n`
|
|
861
|
+
<div class="flow-row ${r?"expanded":""}" @click=${()=>this.toggleFlow(s)}>
|
|
862
|
+
<div class="flow-summary-row">
|
|
863
|
+
<span class="flow-status-dot ${i}"></span>
|
|
864
|
+
<span class="flow-label">${t.label}</span>
|
|
865
|
+
<span class="flow-req-count">${t.requests.length} req${t.requests.length!==1?"s":""}</span>
|
|
866
|
+
<span class="flow-badge-pill ${a.cls}">${a.text}</span>
|
|
867
|
+
<span class="flow-duration">${S(t.totalDurationMs)}</span>
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
<div class="flow-expand ${r?"open":""}">
|
|
871
|
+
${r?this.viewMode==="simple"?this.renderFlowInsights(t):this.renderFlowSubReqs(t):d}
|
|
872
|
+
</div>
|
|
873
|
+
`}renderFlowInsights(t){let s=this.analyzeFlow(t),r=s.errors.length>0||s.duplicates.length>0||s.warnings.length>0||!!s.tip;return n`
|
|
874
|
+
<div>
|
|
875
|
+
<div class="flow-traffic">${t.requests.map(i=>this.renderTrafficCard(i))}</div>
|
|
876
|
+
${r?n`
|
|
877
|
+
<div class="flow-divider"></div>
|
|
878
|
+
<div class="flow-insights">
|
|
879
|
+
${s.errors.map(i=>n`<div class="insight-line insight-error">\u2717 ${i}</div>`)}
|
|
880
|
+
${s.duplicates.map(i=>n`<div class="insight-line insight-warn">\u26A0 ${i.name} \u2014 loaded ${i.count}x (wasting ~${S(i.wastedMs)})</div>`)}
|
|
881
|
+
${s.warnings.map(i=>n`<div class="insight-line insight-warn">\u26A0 ${i}</div>`)}
|
|
882
|
+
${s.tip?n`<div class="insight-line insight-tip">Tip: ${s.tip}</div>`:d}
|
|
582
883
|
</div>
|
|
884
|
+
`:d}
|
|
885
|
+
</div>
|
|
886
|
+
`}renderTrafficCard(t){if(Ut[t.category||""])return d;let s=rt(t.statusCode),r=S(t.pollingDurationMs||t.durationMs),i=!t.isDuplicate&&t.category!==Pt&&t.category!==Tt||t.requestBody&&t.method!=="GET"||!!t.responseBody;return n`
|
|
887
|
+
<div class="traffic-card ${t.isStrictModeDupe?"strict-mode-dupe":""}">
|
|
888
|
+
<div class="traffic-card-header ${i?"has-details":""}">
|
|
889
|
+
<bk-method-badge .method=${t.method}></bk-method-badge>
|
|
890
|
+
<span class="traffic-card-path ${t.isDuplicate?"is-dup":""}">${t.label}</span>
|
|
891
|
+
<span class="status-pill ${s}">${t.statusCode}</span>
|
|
892
|
+
<span class="traffic-card-dur">${r}</span>
|
|
893
|
+
${t.isDuplicate?n`<span class="traffic-card-dup">duplicate</span>`:n`<span class="traffic-card-size">${Y(t.responseSize)}</span>`}
|
|
583
894
|
</div>
|
|
895
|
+
${t.isStrictModeDupe?n`<div class="strict-mode-banner">React Strict Mode duplicate \u2014 does not happen in production</div>`:d}
|
|
896
|
+
${!t.isDuplicate&&t.category!==Pt&&t.category!==Tt?n`<div class="request-timeline tl-hidden" data-request-id=${t.id} data-request-started=${String(t.startedAt)}></div>`:d}
|
|
897
|
+
${t.requestBody&&t.method!=="GET"?this.renderBodyToggle("out","Request Body",t.requestBody):d}
|
|
898
|
+
${t.responseBody?this.renderBodyToggle("in","Response Body",t.responseBody):d}
|
|
584
899
|
</div>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
<span
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
900
|
+
`}renderBodyToggle(t,s,r){let i=t==="out"?"\u2192":"\u2190";return n`
|
|
901
|
+
<div class="traffic-body">
|
|
902
|
+
<button class="traffic-body-toggle" @click=${this.toggleBodyBlock}>
|
|
903
|
+
<span class="chevron">\u25B8</span><span class="arrow-${t}">${i}</span> ${s}
|
|
904
|
+
</button>
|
|
905
|
+
<pre .innerHTML=${X(r)}></pre>
|
|
906
|
+
</div>
|
|
907
|
+
`}renderFlowSubReqs(t){return n`<div class="flow-subreqs">${t.requests.map((s,r)=>this.renderSubReqRow(s,r))}</div>`}renderSubReqRow(t,s){let r=this.expandedSubReqIdx===s,i=rt(t.statusCode),a=t.pollingDurationMs?S(t.pollingDurationMs):S(t.durationMs);return n`
|
|
908
|
+
<div class="flow-subreq ${r?"expanded":""}" @click=${c=>this.toggleSubReq(s,c)}>
|
|
909
|
+
<bk-method-badge .method=${t.method}></bk-method-badge>
|
|
910
|
+
<span class="subreq-label ${t.isDuplicate?"is-dup":""}">${t.path||t.url}</span>
|
|
911
|
+
${t.isDuplicate?n`<span class="subreq-dup-tag">duplicate</span>`:d}
|
|
912
|
+
<span class="status-pill ${i}">${t.statusCode}</span>
|
|
913
|
+
<span class="subreq-dur">${a}</span>
|
|
914
|
+
</div>
|
|
915
|
+
<div class="flow-subreq-detail ${r?"open":""}">
|
|
916
|
+
${r?this.renderSubReqDetail(t):d}
|
|
917
|
+
</div>
|
|
918
|
+
`}renderSubReqDetail(t){let s=rt(t.statusCode);return n`
|
|
919
|
+
<div class="detail-meta">
|
|
920
|
+
<span><bk-method-badge .method=${t.method}></bk-method-badge> ${P(t.url)}</span>
|
|
921
|
+
<span><span class="status-pill ${s}">${t.statusCode}</span></span>
|
|
922
|
+
<span>${t.durationMs}ms</span>
|
|
923
|
+
${t.responseSize?n`<span>${Y(t.responseSize)}</span>`:d}
|
|
924
|
+
</div>
|
|
925
|
+
<div class="request-timeline tl-hidden" data-request-id=${t.id} data-request-started=${String(t.startedAt)}></div>
|
|
926
|
+
<div class="detail-grid">
|
|
927
|
+
<div class="detail-section"><h4>Request Headers</h4><pre .innerHTML=${it(t.headers)}></pre></div>
|
|
928
|
+
<div class="detail-section"><h4>Response Headers</h4><pre .innerHTML=${it(t.responseHeaders)}></pre></div>
|
|
929
|
+
<div class="detail-section"><h4>Request Body</h4><pre .innerHTML=${X(t.requestBody)}></pre></div>
|
|
930
|
+
<div class="detail-section"><h4>Response Body</h4><pre .innerHTML=${X(t.responseBody)}></pre></div>
|
|
931
|
+
</div>
|
|
932
|
+
<div class="detail-actions">
|
|
933
|
+
<button class="btn btn-curl" @click=${r=>{r.stopPropagation(),dt(t);}}>Copy cURL</button>
|
|
934
|
+
</div>
|
|
935
|
+
`}};u([T({context:_})],J.prototype,"store",2),u([$()],J.prototype,"expandedFlowIdx",2),u([$()],J.prototype,"expandedSubReqIdx",2),J=u([g("bk-flows-view")],J);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(c=>c.state==="open"||c.state==="fixing"||c.state==="regressed"),s=e.filter(c=>c.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?n`
|
|
936
|
+
<div class="sec-clear">
|
|
937
|
+
<span class="sec-clear-icon">\u2713</span>
|
|
938
|
+
<div class="sec-clear-text">
|
|
939
|
+
<div class="sec-clear-title">All clear</div>
|
|
940
|
+
<div class="sec-clear-sub">No security or quality issues detected this session</div>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
`:n`<bk-empty-state title="Waiting for requests..." subtitle="Start using your app to see security findings here"></bk-empty-state>`;let r=0,i=0,a=0;for(let c of t){let l=c.issue.severity;l==="critical"?r++:l==="info"?a++:i++;}return n`
|
|
944
|
+
<div id="security-content">
|
|
945
|
+
${this.renderSummary(t.length,s.length,r,i,a)}
|
|
946
|
+
${t.length===0&&s.length>0?n`
|
|
947
|
+
<div class="sec-clear">
|
|
948
|
+
<span class="sec-clear-icon">\u2713</span>
|
|
949
|
+
<div class="sec-clear-text">
|
|
950
|
+
<div class="sec-clear-title">All issues resolved</div>
|
|
951
|
+
<div class="sec-clear-sub">${s.length} finding${s.length!==1?"s were":" was"} detected and fixed</div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
`:d}
|
|
955
|
+
${t.length>0?this.renderOpenGroups(t):d}
|
|
956
|
+
${s.length>0?this.renderResolved(s):d}
|
|
957
|
+
</div>
|
|
958
|
+
`}renderSummary(e,t,s,r,i){return n`
|
|
959
|
+
<div class="sec-summary">
|
|
960
|
+
<div class="sec-summary-left">
|
|
961
|
+
<span class="sec-summary-count">${e}</span>
|
|
962
|
+
<span class="sec-summary-label">open issue${e!==1?"s":""}</span>
|
|
963
|
+
${t>0?n`<span class="sec-resolved-badge">${t} resolved</span>`:d}
|
|
592
964
|
</div>
|
|
593
|
-
<div
|
|
965
|
+
<div class="sec-summary-right">
|
|
966
|
+
${s>0?n`<span class="sec-badge critical">${s} critical</span>`:d}
|
|
967
|
+
${r>0?n`<span class="sec-badge warning">${r} warning</span>`:d}
|
|
968
|
+
${i>0?n`<span class="sec-badge info">${i} info</span>`:d}
|
|
969
|
+
</div>
|
|
970
|
+
</div>
|
|
971
|
+
`}renderOpenGroups(e){let t={},s=[];for(let r of e){let i=r.issue,a=i.rule||i.type;t[a]||(t[a]={rule:a,title:i.title,severity:i.severity,hint:i.hint,items:[]},s.push(a)),t[a].items.push(r);}return s.sort((r,i)=>{let a=H[t[r].severity]?.sort??2,c=H[t[i].severity]?.sort??2;return a!==c?a-c:t[i].items.length-t[r].items.length}),n`${s.map(r=>this.renderGroup(t[r]))}`}renderGroup(e){let t=H[e.severity]||H.info;return n`
|
|
972
|
+
<div class="sec-group">
|
|
973
|
+
<div class="sec-group-header">
|
|
974
|
+
<span class="sec-group-icon ${t.cls}">${t.icon}</span>
|
|
975
|
+
<span class="sec-group-title">${e.title}</span>
|
|
976
|
+
<span class="sec-group-count">${e.items.length}</span>
|
|
977
|
+
</div>
|
|
978
|
+
${e.hint?n`<div class="sec-hint">${e.hint}</div>`:d}
|
|
979
|
+
<div class="sec-items">${e.items.map(s=>this.renderIssueItem(s))}</div>
|
|
594
980
|
</div>
|
|
595
|
-
|
|
596
|
-
|
|
981
|
+
`}renderIssueItem(e){let t=e.issue;return n`
|
|
982
|
+
<div class="sec-item">
|
|
983
|
+
<div class="sec-item-desc">${t.desc}</div>
|
|
984
|
+
${e.occurrences>1?n`<span class="sec-item-count">${e.occurrences}x</span>`:d}
|
|
985
|
+
${e.state==="fixing"&&e.aiStatus==="fixed"?n`<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>`:e.aiStatus==="wont_fix"?n`<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>`:e.state==="regressed"?n`<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>`:d}
|
|
986
|
+
${e.aiNotes?n`<div class="sec-ai-notes">${e.aiNotes}</div>`:d}
|
|
597
987
|
</div>
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
988
|
+
`}renderResolved(e){return n`
|
|
989
|
+
<div class="sec-resolved-title">
|
|
990
|
+
<span class="sec-resolved-check">\u2713</span> Resolved
|
|
991
|
+
<span class="sec-resolved-count">${e.length}</span>
|
|
992
|
+
</div>
|
|
993
|
+
<div class="sec-group sec-group-resolved">
|
|
994
|
+
<div class="sec-items">
|
|
995
|
+
${e.map(t=>n`
|
|
996
|
+
<div class="sec-item sec-item-resolved">
|
|
997
|
+
<span class="sec-resolved-item-icon">\u2713</span>
|
|
998
|
+
<div class="sec-item-desc">${t.issue.title} \u2014 ${t.issue.endpoint||"global"}</div>
|
|
999
|
+
${t.aiStatus==="fixed"?n`<span class="sec-ai-badge sec-ai-verified">Verified fix</span>`:d}
|
|
1000
|
+
${t.aiNotes?n`<div class="sec-ai-notes">${t.aiNotes}</div>`:d}
|
|
1001
|
+
</div>
|
|
1002
|
+
`)}
|
|
604
1003
|
</div>
|
|
605
|
-
<div id="query-list"></div>
|
|
606
1004
|
</div>
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1005
|
+
`}};u([T({context:_})],It.prototype,"store",2),It=u([g("bk-security-view")],It);var ut=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 i=r.querySelector(".item-label");if(i&&i.textContent?.trim()===(Rt[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 i=r.getAttribute("data-nav");i&&this.navigateToView(i);return}r=r.parentElement;}this.expandedCardIdx=this.expandedCardIdx===t?-1:t;}render(){let t=this.store.state,s=t.requests.filter(p=>!p.isStatic&&(!p.path||p.path.indexOf(O)!==0));if(!(s.length>0||t.queries.length>0||t.errors.length>0))return n`<bk-empty-state
|
|
1006
|
+
title="Waiting for requests..."
|
|
1007
|
+
subtitle="Start using your app to see insights here"
|
|
1008
|
+
></bk-empty-state>`;let i=s.filter(p=>p.statusCode>=400).length,a=s.length>0?Math.round(s.reduce((p,v)=>p+v.durationMs,0)/s.length):0,c=t.issues||[],l=c.filter(p=>p.state==="open"||p.state==="regressed"),h=c.filter(p=>p.state==="fixing"),m=c.filter(p=>p.state==="resolved");return n`
|
|
1009
|
+
<div class="ov-container" id="overview-content">
|
|
1010
|
+
${this.renderSummary(s.length,t.flows.length,a,t.queries.length,i,t.fetches.length)}
|
|
1011
|
+
${l.length===0&&h.length===0&&m.length===0?n`<div class="ov-clear">
|
|
1012
|
+
<span class="ov-clear-icon">\u2713</span>All clear \u2014 no issues detected
|
|
1013
|
+
</div>`:d}
|
|
1014
|
+
${l.length===0&&m.length>0?n`<div class="ov-clear">
|
|
1015
|
+
<span class="ov-clear-icon">\u2713</span>All issues resolved \u2014
|
|
1016
|
+
${m.length} finding${m.length!==1?"s were":" was"} detected and
|
|
1017
|
+
fixed
|
|
1018
|
+
</div>`:d}
|
|
1019
|
+
${l.length>0?this.renderOpenIssues(l):d}
|
|
1020
|
+
${h.length>0?this.renderVerifying(h):d}
|
|
1021
|
+
${m.length>0?this.renderResolvedIssues(m):d}
|
|
1022
|
+
</div>
|
|
1023
|
+
`}renderSummary(t,s,r,i,a,c){return n`
|
|
1024
|
+
<div class="ov-summary">
|
|
1025
|
+
<div class="ov-stat"><span class="ov-stat-value">${t}</span><span class="ov-stat-label">Requests</span></div>
|
|
1026
|
+
<div class="ov-stat"><span class="ov-stat-value">${s}</span><span class="ov-stat-label">Actions</span></div>
|
|
1027
|
+
<div class="ov-stat"><span class="ov-stat-value">${S(r)}</span><span class="ov-stat-label">Avg Response</span></div>
|
|
1028
|
+
<div class="ov-stat"><span class="ov-stat-value">${i}</span><span class="ov-stat-label">Queries</span></div>
|
|
1029
|
+
<div class="ov-stat"><span class="ov-stat-value" style="color:${a>0?"var(--red)":"var(--green)"}">${a}</span><span class="ov-stat-label">Errors</span></div>
|
|
1030
|
+
<div class="ov-stat"><span class="ov-stat-value">${c}</span><span class="ov-stat-label">Fetches</span></div>
|
|
1031
|
+
</div>
|
|
1032
|
+
`}renderOpenIssues(t){return n`
|
|
1033
|
+
<div class="ov-section-title">Issues Found <span class="ov-issue-count">${t.length}</span></div>
|
|
1034
|
+
<div class="ov-cards">${t.map((s,r)=>this.renderIssueCard(s,r))}</div>
|
|
1035
|
+
`}renderIssueCard(t,s){let r=t.issue,i=H[r.severity]||H.info,a=this.expandedCardIdx===s,c=t.aiStatus==="wont_fix"?n`<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>`:t.state==="regressed"?n`<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>`:d,l=t.cleanHitsSinceLastSeen>0?n`<div class="ov-card-resolving">Resolving\u2026 ${t.cleanHitsSinceLastSeen}/${5} clean requests</div>`:d;return n`
|
|
1036
|
+
<div class="ov-card ${a?"expanded":""}" @click=${h=>this.toggleCard(s,h)}>
|
|
1037
|
+
<span class="ov-card-icon ${i.cls}">${i.icon}</span>
|
|
1038
|
+
<div class="ov-card-body">
|
|
1039
|
+
<div class="ov-card-title">${r.title}${c}</div>
|
|
1040
|
+
<div class="ov-card-desc">${r.desc}</div>
|
|
1041
|
+
${l}
|
|
1042
|
+
<div class="ov-card-expand" style="display:${a?"block":"none"}">
|
|
1043
|
+
${r.detail?n`<div .innerHTML=${r.detail}></div>`:d}
|
|
1044
|
+
${r.hint?n`<div class="ov-card-hint">${r.hint}</div>`:d}
|
|
1045
|
+
${r.nav?n`<span class="ov-card-link" data-nav=${r.nav}>View in ${de[r.nav]||r.nav} \u2192</span>`:d}
|
|
1046
|
+
</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
<span class="ov-card-arrow">${a?"\u2193":"\u2192"}</span>
|
|
1049
|
+
</div>
|
|
1050
|
+
`}renderVerifying(t){return n`
|
|
1051
|
+
<div class="ov-section-title ov-resolved-title">
|
|
1052
|
+
<span style="color:var(--yellow,#f5a623)">\u29d7</span> Awaiting Verification
|
|
1053
|
+
<span class="ov-issue-count">${t.length}</span>
|
|
1054
|
+
</div>
|
|
1055
|
+
<div class="ov-cards">
|
|
1056
|
+
${t.map(s=>{let r=s.issue,i=s.cleanHitsSinceLastSeen>0?n`<div class="ov-card-resolving">Verifying\u2026 ${s.cleanHitsSinceLastSeen}/${5} clean requests</div>`:d;return n`
|
|
1057
|
+
<div class="ov-card ov-card-resolved">
|
|
1058
|
+
<span class="ov-card-icon resolved">\u29d7</span>
|
|
1059
|
+
<div class="ov-card-body">
|
|
1060
|
+
<div class="ov-card-title" style="color:var(--text-muted)">
|
|
1061
|
+
${r.title}
|
|
1062
|
+
<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>
|
|
1063
|
+
</div>
|
|
1064
|
+
<div class="ov-card-desc">${r.desc}</div>
|
|
1065
|
+
${i}
|
|
1066
|
+
</div>
|
|
1067
|
+
</div>
|
|
1068
|
+
`})}
|
|
1069
|
+
</div>
|
|
1070
|
+
`}renderResolvedIssues(t){return n`
|
|
1071
|
+
<div class="ov-section-title ov-resolved-title">
|
|
1072
|
+
<span style="color:var(--green)">\u2713</span> Resolved
|
|
1073
|
+
<span class="ov-issue-count">${t.length}</span>
|
|
1074
|
+
</div>
|
|
1075
|
+
<div class="ov-cards">
|
|
1076
|
+
${t.map(s=>n`
|
|
1077
|
+
<div class="ov-card ov-card-resolved">
|
|
1078
|
+
<span class="ov-card-icon resolved">\u2713</span>
|
|
1079
|
+
<div class="ov-card-body">
|
|
1080
|
+
<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">${s.issue.title}</div>
|
|
1081
|
+
<div class="ov-card-desc">${s.issue.desc}</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
`)}
|
|
1085
|
+
</div>
|
|
1086
|
+
`}};u([T({context:_})],ut.prototype,"store",2),u([$()],ut.prototype,"expandedCardIdx",2),ut=u([g("bk-overview-view")],ut);var F=class extends f{constructor(){super(...arguments);this.selectedEndpoint=q;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(R.metricsLive)).json();this.graphData=s.endpoints||[],this.loadError=!1,(!this.selectedEndpoint||this.selectedEndpoint===q)&&(this.selectedEndpoint=q);}catch{this.loadError=true;}}healthGrade(t){for(let s of Qt)if(t<s.max)return s;return Qt[Qt.length-1]}fmtMs(t){return t<1?"<1ms":t<1e3?Math.round(t)+"ms":(t/1e3).toFixed(1)+"s"}dotColor(t){return t<V?wt.green:t<W?wt.amber:wt.red}reqDotColor(t){return t.statusCode>=400?wt.red:this.dotColor(t.durationMs)}parseHex(t){return [parseInt(t.slice(1,3),16),parseInt(t.slice(3,5),16),parseInt(t.slice(5,7),16)]}setupCanvas(t){let s=t.getContext("2d");if(!s)return null;let r=window.devicePixelRatio||1,i=t.clientWidth,a=t.clientHeight;return t.width=i*r,t.height=a*r,s.scale(r,r),{ctx:s,w:i,h:a}}drawDot(t,s,r,i,a){let[c,l,h]=this.parseHex(a);t.beginPath(),t.arc(s,r,i+2,0,Math.PI*2),t.fillStyle=`rgba(${c},${l},${h},0.25)`,t.fill(),t.beginPath(),t.arc(s,r,i,0,Math.PI*2),t.fillStyle=a,t.fill();}drawErrorX(t,s,r,i,a,c){let[l,h,m]=this.parseHex(a);t.strokeStyle=`rgba(${l},${h},${m},0.3)`,t.lineWidth=c+2,t.beginPath(),t.moveTo(s-i,r-i),t.lineTo(s+i,r+i),t.moveTo(s+i,r-i),t.lineTo(s-i,r+i),t.stroke(),t.strokeStyle=a,t.lineWidth=c,t.beginPath(),t.moveTo(s-i,r-i),t.lineTo(s+i,r+i),t.moveTo(s+i,r-i),t.lineTo(s-i,r+i),t.stroke();}drawScatterChart(t,s){this.scatterDots=[];let r=this.setupCanvas(t);if(!r||s.length===0)return;let{ctx:i,w:a,h:c}=r,l=es,h=a-l.left-l.right,m=c-l.top-l.bottom,p=0,v=s[0].timestamp,E=s[0].timestamp;for(let b of s)b.durationMs>p&&(p=b.durationMs),b.timestamp<v&&(v=b.timestamp),b.timestamp>E&&(E=b.timestamp);p=Math.max(p,10),p=Math.ceil(p*1.15/10)*10;let C=E-v||1;i.strokeStyle=Je,i.lineWidth=1;let ht=4;for(let b=0;b<=ht;b++){let x=l.top+m-b/ht*m;i.beginPath(),i.moveTo(l.left,x),i.lineTo(l.left+h,x),i.stroke(),i.fillStyle=Ee,i.font=Ze,i.textAlign="right",i.fillText(this.fmtMs(Math.round(b/ht*p)),l.left-8,x+3);}for(let b of [{ms:V},{ms:W}]){if(b.ms>=p)continue;let x=l.top+m-b.ms/p*m;i.beginPath(),i.setLineDash([4,4]),i.strokeStyle="rgba(113,113,122,0.3)",i.lineWidth=1,i.moveTo(l.left,x),i.lineTo(l.left+h,x),i.stroke(),i.setLineDash([]),i.fillStyle="rgba(113,113,122,0.5)",i.font=$e,i.textAlign="left",i.fillText(this.fmtMs(b.ms),l.left+h+2,x+3);}for(let b=0;b<s.length;b++){let x=s[b],I=s.length===1?l.left+h/2:l.left+(x.timestamp-v)/C*h,mt=l.top+m-x.durationMs/p*m,tt=this.reqDotColor(x);this.scatterDots.push({x:I,y:mt,idx:b,r:x}),x.statusCode>=400?this.drawErrorX(i,I,mt,4,tt,2):this.drawDot(i,I,mt,4,tt);}i.fillStyle=Ee,i.font=$e,i.textAlign="center";let A=[v,v+C/2,E];for(let b=0;b<A.length;b++){let x=l.left+b/2*h,I=new Date(A[b]);i.fillText(I.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),x,l.top+m+14);}t.style.cursor="pointer",t.onclick=b=>{let x=t.getBoundingClientRect(),I=b.clientX-x.left,mt=b.clientY-x.top,tt=null,Xt=1/0;for(let Kt of this.scatterDots){let xe=Math.sqrt((Kt.x-I)**2+(Kt.y-mt)**2);xe<Xt&&(Xt=xe,tt=Kt);}tt&&Xt<16&&this.highlightRow(tt.idx);};}drawInlineScatter(t,s){let r=this.setupCanvas(t);if(!r||s.length===0)return;let{ctx:i,w:a,h:c}=r,l=4,h=4,m=a-l*2,p=c-h*2,v=0,E=s[0].timestamp,C=s[0].timestamp;for(let A of s)A.durationMs>v&&(v=A.durationMs),A.timestamp<E&&(E=A.timestamp),A.timestamp>C&&(C=A.timestamp);v=Math.max(v,10),v=Math.ceil(v*1.15/10)*10;let ht=C-E||1;for(let A of [V,W]){if(A>=v)continue;let b=h+p-A/v*p;i.beginPath(),i.setLineDash([2,3]),i.strokeStyle="rgba(113,113,122,0.15)",i.lineWidth=1,i.moveTo(l,b),i.lineTo(l+m,b),i.stroke(),i.setLineDash([]);}for(let A of s){let b=s.length===1?l+m/2:l+(A.timestamp-E)/ht*m,x=h+p-A.durationMs/v*p,I=this.reqDotColor(A);A.statusCode>=400?this.drawErrorX(i,b,x,2.5,I,1.5):this.drawDot(i,b,x,2.5,I);}i.fillStyle="rgba(113,113,122,0.5)",i.font=ts,i.textAlign="right",i.fillText(this.fmtMs(v),a-2,h+8),i.fillText(this.fmtMs(0),a-2,c-2);}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===q)this.graphData.forEach((t,s)=>{if(t.requests.length===0)return;let r=this.querySelector(`#inline-scatter-${s}`);r&&this.drawInlineScatter(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.drawScatterChart(t,s.requests);}}}render(){return !this.graphData||this.graphData.length===0?n`<bk-empty-state title="No performance data yet" subtitle="Hit some endpoints and data will appear here"></bk-empty-state>`:n`
|
|
1087
|
+
<div id="graph-content">
|
|
1088
|
+
${this.renderSelector()}
|
|
1089
|
+
${this.selectedEndpoint===q?this.renderOverview():this.renderDetail()}
|
|
1090
|
+
</div>
|
|
1091
|
+
`}renderSelector(){return n`
|
|
1092
|
+
<div class="perf-selector">
|
|
1093
|
+
<button class="perf-selector-btn ${this.selectedEndpoint===q?"active":""}"
|
|
1094
|
+
@click=${()=>{this.selectedEndpoint=q;}}>Overview</button>
|
|
1095
|
+
${this.graphData.map((t,s)=>n`
|
|
1096
|
+
<button class="perf-selector-btn ${t.endpoint===this.selectedEndpoint?"active":""}"
|
|
1097
|
+
@click=${()=>{this.selectedEndpoint=t.endpoint;}}>
|
|
1098
|
+
<span class="perf-dot" style="background:${be[s%be.length]}"></span>${t.endpoint}
|
|
1099
|
+
</button>
|
|
1100
|
+
`)}
|
|
1101
|
+
</div>
|
|
1102
|
+
`}renderOverview(){return n`
|
|
1103
|
+
<div class="perf-endpoint-list">
|
|
1104
|
+
${this.graphData.map((t,s)=>t.requests.length===0?d:this.renderEndpointCard(t,s))}
|
|
1105
|
+
</div>
|
|
1106
|
+
`}renderEndpointCard(t,s){let r=t.summary,i=this.healthGrade(r.p95Ms),a=Math.round(r.errorRate*r.totalRequests),c=(r.avgQueryTimeMs||0)+(r.avgFetchTimeMs||0)+(r.avgAppTimeMs||0),l=d;if(c>0){let h=Math.round((r.avgQueryTimeMs||0)/c*100),m=Math.round((r.avgFetchTimeMs||0)/c*100),p=Math.max(0,100-h-m);l=n`
|
|
1107
|
+
<div class="perf-breakdown-inline">
|
|
1108
|
+
<div class="perf-breakdown-bar perf-breakdown-bar-sm">
|
|
1109
|
+
${h>0?n`<div class="perf-breakdown-seg perf-breakdown-db" style="width:${h}%"></div>`:d}
|
|
1110
|
+
${m>0?n`<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:${m}%"></div>`:d}
|
|
1111
|
+
${p>0?n`<div class="perf-breakdown-seg perf-breakdown-app" style="width:${p}%"></div>`:d}
|
|
1112
|
+
</div>
|
|
1113
|
+
<span class="perf-breakdown-labels">
|
|
1114
|
+
${h>0?n`<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-db"></span>${this.fmtMs(r.avgQueryTimeMs||0)}</span>`:d}
|
|
1115
|
+
${m>0?n`<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>${this.fmtMs(r.avgFetchTimeMs||0)}</span>`:d}
|
|
1116
|
+
<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-app"></span>${this.fmtMs(r.avgAppTimeMs||0)}</span>
|
|
1117
|
+
</span>
|
|
1118
|
+
</div>
|
|
1119
|
+
`;}return n`
|
|
1120
|
+
<div class="perf-endpoint-card" @click=${()=>{this.selectedEndpoint=t.endpoint;}}>
|
|
1121
|
+
<div class="perf-ep-header">
|
|
1122
|
+
<span class="perf-ep-name">${t.endpoint}</span>
|
|
1123
|
+
<span class="perf-ep-stats">
|
|
1124
|
+
<span class="perf-ep-stat" style="color:${i.color}">p95: ${this.fmtMs(r.p95Ms)}</span>
|
|
1125
|
+
<span class="perf-ep-stat ${a>0?"perf-ep-stat-err":""}">${a} err</span>
|
|
1126
|
+
${r.avgQueryCount>0?n`<span class="perf-ep-stat ${r.avgQueryCount>5?"perf-ep-stat-warn":""}">${r.avgQueryCount} q/req</span>`:d}
|
|
1127
|
+
<span class="perf-ep-stat perf-ep-stat-muted">${r.totalRequests} req${r.totalRequests!==1?"s":""}</span>
|
|
1128
|
+
</span>
|
|
1129
|
+
</div>
|
|
1130
|
+
${l}
|
|
1131
|
+
<canvas id="inline-scatter-${s}" class="perf-inline-canvas"></canvas>
|
|
1132
|
+
</div>
|
|
1133
|
+
`}renderDetail(){let t=this.graphData.find(a=>a.endpoint===this.selectedEndpoint);if(!t?.requests?.length)return n`<bk-empty-state subtitle="No data for this endpoint"></bk-empty-state>`;let s=t.summary,r=this.healthGrade(s.p95Ms),i=Math.round(s.errorRate*s.totalRequests);return n`
|
|
1134
|
+
${this.renderDetailHeader(t,r)}
|
|
1135
|
+
${this.renderDetailMetrics(s,r,i)}
|
|
1136
|
+
${this.renderDetailBreakdown(s)}
|
|
1137
|
+
${this.renderDetailChart()}
|
|
1138
|
+
${this.renderDetailHistory(t)}
|
|
1139
|
+
`}renderDetailHeader(t,s){return n`
|
|
1140
|
+
<div class="perf-detail-header">
|
|
1141
|
+
<div class="perf-detail-title">
|
|
1142
|
+
<span class="perf-badge perf-badge-lg" style="color:${s.color};background:${s.bg};border-color:${s.border}">${s.label}</span>
|
|
1143
|
+
<span>${t.endpoint}</span>
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
`}renderDetailMetrics(t,s,r){return n`
|
|
1147
|
+
<div class="perf-metric-row">
|
|
1148
|
+
<div class="perf-metric-card">
|
|
1149
|
+
<span class="perf-metric-label">P95</span>
|
|
1150
|
+
<span class="perf-metric-value" style="color:${s.color}">${this.fmtMs(t.p95Ms)}</span>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="perf-metric-card">
|
|
1153
|
+
<span class="perf-metric-label">Errors</span>
|
|
1154
|
+
<span class="perf-metric-value" style="color:${r>0?"var(--red)":"var(--green)"}">
|
|
1155
|
+
${r>0?r+" ("+Math.round(t.errorRate*100)+"%)":"0"}
|
|
1156
|
+
</span>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div class="perf-metric-card">
|
|
1159
|
+
<span class="perf-metric-label">Queries/req</span>
|
|
1160
|
+
<span class="perf-metric-value" style="color:${t.avgQueryCount>5?"var(--amber)":"var(--text)"}">${t.avgQueryCount}</span>
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
`}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),i=Math.round((t.avgFetchTimeMs||0)/s*100),a=Math.max(0,100-r-i);return n`
|
|
1164
|
+
<div class="perf-breakdown">
|
|
1165
|
+
<div class="perf-section-title">Time Breakdown</div>
|
|
1166
|
+
<div class="perf-breakdown-bar">
|
|
1167
|
+
${r>0?n`<div class="perf-breakdown-seg perf-breakdown-db" style="width:${r}%"></div>`:d}
|
|
1168
|
+
${i>0?n`<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:${i}%"></div>`:d}
|
|
1169
|
+
${a>0?n`<div class="perf-breakdown-seg perf-breakdown-app" style="width:${a}%"></div>`:d}
|
|
1170
|
+
</div>
|
|
1171
|
+
<div class="perf-breakdown-legend">
|
|
1172
|
+
<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-db"></span>DB ${this.fmtMs(t.avgQueryTimeMs||0)} (${r}%)</span>
|
|
1173
|
+
<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>Fetch ${this.fmtMs(t.avgFetchTimeMs||0)} (${i}%)</span>
|
|
1174
|
+
<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-app"></span>App ${this.fmtMs(t.avgAppTimeMs||0)} (${a}%)</span>
|
|
612
1175
|
</div>
|
|
613
|
-
<div id="error-list"></div>
|
|
614
1176
|
</div>
|
|
615
|
-
|
|
616
|
-
|
|
1177
|
+
`}renderDetailChart(){return n`
|
|
1178
|
+
<div class="perf-chart-wrap">
|
|
1179
|
+
<div class="perf-section-title">Response Time</div>
|
|
1180
|
+
<canvas id="perf-detail-canvas" class="perf-canvas" style="width:100%;height:240px"></canvas>
|
|
1181
|
+
</div>
|
|
1182
|
+
`}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 n`
|
|
1183
|
+
<div class="perf-history-wrap">
|
|
617
1184
|
<div class="col-header">
|
|
618
|
-
<span
|
|
619
|
-
<span
|
|
620
|
-
<span
|
|
1185
|
+
<span class="perf-col perf-col-date">Time</span>
|
|
1186
|
+
<span class="perf-col perf-col-health">Health</span>
|
|
1187
|
+
<span class="perf-col perf-col-avg">Duration</span>
|
|
1188
|
+
<span class="perf-col perf-col-breakdown">Breakdown</span>
|
|
1189
|
+
<span class="perf-col perf-col-status">Status</span>
|
|
1190
|
+
<span class="perf-col perf-col-qpr">Queries</span>
|
|
621
1191
|
</div>
|
|
622
|
-
|
|
1192
|
+
${s.map(r=>this.renderHistoryRow(r.r,r.origIdx))}
|
|
1193
|
+
</div>
|
|
1194
|
+
`}renderHistoryRow(t,s){let r=this.healthGrade(t.durationMs),i=new Date(t.timestamp).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),a=t.statusCode>=400,c=t.queryTimeMs||0,l=t.fetchTimeMs||0,h=Math.max(0,t.durationMs-c-l);return n`
|
|
1195
|
+
<div class="perf-hist-row ${a?"perf-hist-row-err":""}" data-req-idx=${s}>
|
|
1196
|
+
<span class="perf-col perf-col-date">${i}</span>
|
|
1197
|
+
<span class="perf-col perf-col-health">
|
|
1198
|
+
<span class="perf-badge perf-badge-sm" style="color:${r.color};background:${r.bg};border-color:${r.border}">${r.label}</span>
|
|
1199
|
+
</span>
|
|
1200
|
+
<span class="perf-col perf-col-avg">${this.fmtMs(t.durationMs)}</span>
|
|
1201
|
+
<span class="perf-col perf-col-breakdown">
|
|
1202
|
+
${c>0?n`<span class="perf-bd-tag perf-bd-tag-db">DB ${this.fmtMs(c)}</span>`:d}
|
|
1203
|
+
${l>0?n`<span class="perf-bd-tag perf-bd-tag-fetch">Fetch ${this.fmtMs(l)}</span>`:d}
|
|
1204
|
+
<span class="perf-bd-tag perf-bd-tag-app">App ${this.fmtMs(h)}</span>
|
|
1205
|
+
</span>
|
|
1206
|
+
<span class="perf-col perf-col-status" style="color:${a?"var(--red)":"var(--text-muted)"}">${t.statusCode}</span>
|
|
1207
|
+
<span class="perf-col perf-col-qpr">${t.queryCount}</span>
|
|
623
1208
|
</div>
|
|
624
|
-
|
|
625
|
-
|
|
1209
|
+
`}};u([T({context:_})],F.prototype,"store",2),u([$()],F.prototype,"selectedEndpoint",2),u([$()],F.prototype,"graphData",2),u([$()],F.prototype,"loadError",2),F=u([g("bk-performance-view")],F);function Qs(o){return o===0?"<1ms":S(o)}var w=class extends f{constructor(){super(...arguments);this.requestId="";this.requestStarted=0;this.data=null;this.loading=false;this.failed=false;this.expandedSqlIdx=-1;}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.store.addEventListener("state-changed",()=>this.requestUpdate()),this.requestId&&this.loadTimeline();}async loadTimeline(){if(!this.requestId)return;let t=w.cache.get(this.requestId);if(t){this.data=t;return}this.loading=true;try{let s=await fetch(`${R.activity}?requestId=${this.requestId}`);if(!s.ok){this.failed=!0,this.loading=!1;return}let r=await s.json();if(w.cache.size>=fe){let i=w.cache.keys().next().value;i!==void 0&&w.cache.delete(i);}w.cache.set(this.requestId,r),this.data=r,this.loading=!1;}catch(s){console.debug("[brakit] timeline load failed:",s),this.failed=true,this.loading=false;}}toggleSql(t,s){s.stopPropagation(),this.expandedSqlIdx=this.expandedSqlIdx===t?-1:t;}copySql(t,s){s.stopPropagation(),navigator.clipboard.writeText(t).then(()=>k.show("SQL copied")).catch(()=>k.show("Copy failed"));}render(){if(this.loading)return n`<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 n`
|
|
1210
|
+
<div class="tl-header">
|
|
1211
|
+
<span class="tl-title">Activity Timeline</span>
|
|
1212
|
+
<span class="tl-counts">
|
|
1213
|
+
${t.counts.queries>0?n`<span class="tl-count tl-count-query">${t.counts.queries} quer${t.counts.queries===1?"y":"ies"}</span>`:d}
|
|
1214
|
+
${t.counts.fetches>0?n`<span class="tl-count tl-count-fetch">${t.counts.fetches} fetch${t.counts.fetches===1?"":"es"}</span>`:d}
|
|
1215
|
+
${t.counts.logs>0?n`<span class="tl-count tl-count-log">${t.counts.logs} log${t.counts.logs===1?"":"s"}</span>`:d}
|
|
1216
|
+
${t.counts.errors>0?n`<span class="tl-count tl-count-error">${t.counts.errors} error${t.counts.errors===1?"":"s"}</span>`:d}
|
|
1217
|
+
</span>
|
|
626
1218
|
</div>
|
|
627
|
-
<div class="
|
|
628
|
-
|
|
1219
|
+
<div class="tl-events">${this.renderTimeline(t.timeline,s)}</div>
|
|
1220
|
+
`}renderTimeline(t,s){let r=new Map,i=[];for(let c of t){let l=c.type==="query"?c.data.parentFetchId:void 0;if(c.type==="query"&&l){let h=r.get(l);h||(h=[],r.set(l,h)),h.push(c);}else i.push(c);}let a=0;return i.map(c=>{let l=a++,h=c.type==="fetch"?c.data.fetchId:void 0,m=h?r.get(h):void 0;if(m&&m.length>0){let p=m.length;return n`
|
|
1221
|
+
${this.renderEvent(c,l,s)}
|
|
1222
|
+
<div class="tl-nested">
|
|
1223
|
+
<span class="tl-nested-label">${p} nested quer${p===1?"y":"ies"}</span>
|
|
1224
|
+
${m.map(v=>{let E=a++;return this.renderEvent(v,E,s,true)})}
|
|
1225
|
+
</div>
|
|
1226
|
+
`}return this.renderEvent(c,l,s)})}renderEvent(t,s,r,i=false){let a=ss[t.type]||"var(--text-dim)",c=rs[t.type]||t.type,l="+"+S(Math.round(t.timestamp-r)),h=t.type==="query"?t.data.sql:void 0,m=!!h,p=this.expandedSqlIdx===s;return n`
|
|
1227
|
+
<div class="tl-event ${m?"tl-clickable":""} ${i?"tl-nested-event":""}"
|
|
1228
|
+
style="${m?"":`border-left-color:${a}`}"
|
|
1229
|
+
@click=${m?v=>this.toggleSql(s,v):d}>
|
|
1230
|
+
<span class="tl-event-time">${l}</span>
|
|
1231
|
+
<span class="tl-event-type" style="color:${a}">${c}</span>
|
|
1232
|
+
${this.renderEventContent(t)}
|
|
1233
|
+
${h?n`
|
|
1234
|
+
<div class="tl-event-sql ${p?"open":""}">
|
|
1235
|
+
<button class="tl-sql-copy" @click=${v=>this.copySql(h,v)}>Copy</button>
|
|
1236
|
+
${h}
|
|
1237
|
+
</div>`:d}
|
|
1238
|
+
</div>
|
|
1239
|
+
`}renderEventContent(t){switch(t.type){case "fetch":{let s=t.data,r=s.statusCode>=400;return n`
|
|
1240
|
+
<span class="tl-event-summary">${s.method} ${s.url}</span>
|
|
1241
|
+
<span class="tl-event-status" style="${r?"color:var(--red)":""}">${s.statusCode}</span>
|
|
1242
|
+
<span class="tl-event-dur">${S(s.durationMs)}</span>
|
|
1243
|
+
`}case "query":{let s=t.data,r=(s.normalizedOp||s.operation||"?").toUpperCase(),i=s.table||s.model||"",a=jt[r]||"var(--text-dim)";return n`
|
|
1244
|
+
<span class="tl-event-summary"><span style="color:${a};font-weight:600">${r}</span> ${i}</span>
|
|
1245
|
+
<span class="tl-event-dur">${Qs(s.durationMs)}</span>
|
|
1246
|
+
`}case "log":{let s=t.data,r=ze[s.level]||"var(--text-dim)";return n`<span class="tl-event-summary"><span style="color:${r}">${s.level.toUpperCase()}</span> ${s.message}</span>`}case "error":{let s=t.data;return n`<span class="tl-event-summary" style="color:var(--red)">${s.name}: ${s.message}</span>`}default:return d}}};w.cache=new Map,u([T({context:_})],w.prototype,"store",2),u([y({attribute:"request-id"})],w.prototype,"requestId",2),u([y({attribute:"request-started",type:Number})],w.prototype,"requestStarted",2),u([$()],w.prototype,"data",2),u([$()],w.prototype,"loading",2),u([$()],w.prototype,"failed",2),u([$()],w.prototype,"expandedSqlIdx",2),w=u([g("bk-timeline-panel")],w);var Yt=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(R.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(),ge)));},this.eventSource.addEventListener(ne,e=>{this.store.prependFetch(JSON.parse(e.data));}),this.eventSource.addEventListener("log",e=>{this.store.prependLog(JSON.parse(e.data));}),this.eventSource.addEventListener(ae,e=>{this.store.prependError(JSON.parse(e.data));}),this.eventSource.addEventListener(ce,e=>{this.store.prependQuery(JSON.parse(e.data));}),this.eventSource.addEventListener(le,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(R.flows)).json();this.store.setFlows(t.flows);}catch{}}async reloadMetrics(){try{let t=await(await fetch(R.metricsLive)).json();this.store.setMetrics(t.endpoints||[]);}catch{}}};function ps(){return n`<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 us(){return n`<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 hs(){return n`<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 ms(){return n`<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 vs(){return n`<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 fs(){return n`<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 gs(){return n`<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 bs(){return n`<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 Es(){return n`<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 Z=class extends f{constructor(){super(...arguments);this.store=new Wt;this.activeView="overview";this.viewMode="simple";this.sse=new Yt(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(R.flows),fetch(R.requests)]),[r,i]=await Promise.all([t.json(),s.json()]);this.store.setFlows(r.flows),this.store.setRequests(i.requests);}catch(t){console.warn("[brakit]",t);}try{let[t,s,r,i,a]=await Promise.all([fetch(R.fetches),fetch(R.errors),fetch(R.logs),fetch(R.queries),fetch(R.metricsLive)]),[c,l,h,m,p]=await Promise.all([t.json(),s.json(),r.json(),i.json(),a.json()]);this.store.setFetches(c.entries),this.store.setErrors(l.entries),this.store.setLogs(h.entries),this.store.setQueries(m.entries),this.store.setMetrics(p.endpoints||[]);}catch(t){console.warn("[brakit]",t);}try{let s=await(await fetch(R.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(`${R.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(R.clear,{method:"POST"}),this.store.clearAll(),k.show("Cleared"));}handleCopyAsCurl(t){dt(t);}render(){let t=this.store.state,s=t.requests.filter(l=>!l.path?.startsWith(O)),r=s.filter(l=>l.statusCode>=400).length,i=s.length>0?Math.round(s.reduce((l,h)=>l+h.durationMs,0)/s.length):0,a=(t.issues||[]).filter(l=>l.state!=="resolved"&&l.state!=="stale").length,c=window.__BRAKIT_CONFIG__;return n`
|
|
1247
|
+
<div class="app" id="app">
|
|
1248
|
+
<aside class="sidebar">
|
|
1249
|
+
<div class="sidebar-logo">
|
|
1250
|
+
<span class="logo-text">brakit</span>
|
|
1251
|
+
<span class="logo-version">v${c?.version??""}</span>
|
|
1252
|
+
</div>
|
|
1253
|
+
<nav class="sidebar-nav">
|
|
1254
|
+
${this.renderSidebarItem("overview","Overview",ps(),void 0)}
|
|
1255
|
+
<div class="sidebar-section">Monitor</div>
|
|
1256
|
+
${this.renderSidebarItem("actions","Actions",us(),t.flows.length)}
|
|
1257
|
+
${this.renderSidebarItem("requests","Requests",hs(),s.length)}
|
|
1258
|
+
${this.renderSidebarItem("fetches","Fetches",ms(),t.fetches.length)}
|
|
1259
|
+
<div class="sidebar-section">Insights</div>
|
|
1260
|
+
${this.renderSidebarItem("queries","Queries",vs(),t.queries.length)}
|
|
1261
|
+
${this.renderSidebarItem("errors","Errors",fs(),t.errors.length)}
|
|
1262
|
+
${this.renderSidebarItem("logs","Logs",gs(),t.logs.length)}
|
|
1263
|
+
${this.renderSidebarItem("security","Security",bs(),a,a===0)}
|
|
1264
|
+
${this.renderSidebarItem("performance","Performance",Es(),void 0)}
|
|
1265
|
+
</nav>
|
|
1266
|
+
<div class="sidebar-footer">:${c?.port??""}</div>
|
|
1267
|
+
</aside>
|
|
1268
|
+
<div class="main-panel">
|
|
1269
|
+
<div class="header">
|
|
1270
|
+
<div class="header-left">
|
|
1271
|
+
<span class="header-title" id="header-title">${Rt[this.activeView]||this.activeView}</span>
|
|
1272
|
+
<span class="header-sub" id="header-sub">${pe[this.activeView]||""}</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
<div class="header-right">
|
|
1275
|
+
${this.activeView==="actions"?n`
|
|
1276
|
+
<div class="segmented-control" id="mode-toggle">
|
|
1277
|
+
<button class="segmented-btn ${this.viewMode==="simple"?"active":""}" @click=${()=>{this.viewMode="simple",this.store.setViewMode("simple");}}>Quick</button>
|
|
1278
|
+
<button class="segmented-btn ${this.viewMode==="detailed"?"active":""}" @click=${()=>{this.viewMode="detailed",this.store.setViewMode("detailed");}}>Detailed</button>
|
|
1279
|
+
</div>
|
|
1280
|
+
`:d}
|
|
1281
|
+
<button class="btn btn-danger" @click=${this.handleClear}>Clear</button>
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
<div class="main-content">
|
|
1285
|
+
<div id="overview-container" style="display:${this.activeView==="overview"?"block":"none"}">
|
|
1286
|
+
<bk-overview-view></bk-overview-view>
|
|
1287
|
+
</div>
|
|
1288
|
+
<div class="view-flows" id="flow-container" style="display:${this.activeView==="actions"?"block":"none"}">
|
|
1289
|
+
<bk-flows-view></bk-flows-view>
|
|
1290
|
+
</div>
|
|
1291
|
+
<div class="view-requests" id="request-container" style="display:${this.activeView==="requests"?"block":"none"}">
|
|
1292
|
+
<bk-requests-view></bk-requests-view>
|
|
1293
|
+
</div>
|
|
1294
|
+
<div class="view-telemetry" id="fetch-container" style="display:${this.activeView==="fetches"?"block":"none"}">
|
|
1295
|
+
<bk-fetches-view></bk-fetches-view>
|
|
1296
|
+
</div>
|
|
1297
|
+
<div class="view-telemetry" id="query-container" style="display:${this.activeView==="queries"?"block":"none"}">
|
|
1298
|
+
<bk-queries-view></bk-queries-view>
|
|
1299
|
+
</div>
|
|
1300
|
+
<div class="view-telemetry" id="error-container" style="display:${this.activeView==="errors"?"block":"none"}">
|
|
1301
|
+
<bk-errors-view></bk-errors-view>
|
|
1302
|
+
</div>
|
|
1303
|
+
<div class="view-telemetry" id="log-container" style="display:${this.activeView==="logs"?"block":"none"}">
|
|
1304
|
+
<bk-logs-view></bk-logs-view>
|
|
1305
|
+
</div>
|
|
1306
|
+
<div class="view-telemetry" id="security-container" style="display:${this.activeView==="security"?"block":"none"}">
|
|
1307
|
+
<bk-security-view></bk-security-view>
|
|
1308
|
+
</div>
|
|
1309
|
+
<div class="view-telemetry" id="performance-container" style="display:${this.activeView==="performance"?"block":"none"}">
|
|
1310
|
+
<bk-performance-view></bk-performance-view>
|
|
1311
|
+
</div>
|
|
1312
|
+
</div>
|
|
1313
|
+
<div class="footer">
|
|
1314
|
+
<span id="stat-total">${s.length} request${s.length!==1?"s":""}</span>
|
|
1315
|
+
<span id="stat-flows">${t.flows.length} action${t.flows.length!==1?"s":""}</span>
|
|
1316
|
+
<span id="stat-errors" class="error-count">${r} error${r!==1?"s":""}</span>
|
|
1317
|
+
<span id="stat-avg">Avg: ${i}ms</span>
|
|
1318
|
+
</div>
|
|
1319
|
+
</div>
|
|
629
1320
|
</div>
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
<
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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.insights || [];
|
|
2145
|
-
var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing'; });
|
|
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 insight = si.insight;
|
|
2178
|
-
var card = document.createElement('div');
|
|
2179
|
-
card.className = 'ov-card';
|
|
2180
|
-
|
|
2181
|
-
var sevCfg = SEV[insight.severity];
|
|
2182
|
-
var iconCls = sevCfg.cls;
|
|
2183
|
-
var iconChar = sevCfg.icon;
|
|
2184
|
-
|
|
2185
|
-
var expandHtml = '';
|
|
2186
|
-
if (insight.detail) expandHtml += insight.detail;
|
|
2187
|
-
if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
|
|
2188
|
-
expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.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
|
-
}
|
|
2196
|
-
|
|
2197
|
-
card.innerHTML =
|
|
2198
|
-
'<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
2199
|
-
'<div class="ov-card-body">' +
|
|
2200
|
-
'<div class="ov-card-title">' + escHtml(insight.title) + aiBadge + '</div>' +
|
|
2201
|
-
'<div class="ov-card-desc">' + insight.desc + '</div>' +
|
|
2202
|
-
'<div class="ov-card-expand">' + expandHtml + '</div>' +
|
|
2203
|
-
'</div>' +
|
|
2204
|
-
'<span class="ov-card-arrow">\u2192</span>';
|
|
2205
|
-
|
|
2206
|
-
card.addEventListener('click', function(e) {
|
|
2207
|
-
var target = e.target;
|
|
2208
|
-
while (target && target !== card) {
|
|
2209
|
-
if (target.classList && target.classList.contains('ov-card-link')) {
|
|
2210
|
-
var navView = target.getAttribute('data-nav');
|
|
2211
|
-
var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
|
|
2212
|
-
if (sidebarItem) sidebarItem.click();
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
target = target.parentElement;
|
|
2216
|
-
}
|
|
2217
|
-
var expand = card.querySelector('.ov-card-expand');
|
|
2218
|
-
var arrow = card.querySelector('.ov-card-arrow');
|
|
2219
|
-
if (card.classList.contains('expanded')) {
|
|
2220
|
-
card.classList.remove('expanded');
|
|
2221
|
-
expand.style.display = 'none';
|
|
2222
|
-
arrow.textContent = '\u2192';
|
|
2223
|
-
} else {
|
|
2224
|
-
card.classList.add('expanded');
|
|
2225
|
-
expand.style.display = 'block';
|
|
2226
|
-
arrow.textContent = '\u2193';
|
|
2227
|
-
}
|
|
2228
|
-
});
|
|
2229
|
-
|
|
2230
|
-
cards.appendChild(card);
|
|
2231
|
-
})(open[i]);
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
container.appendChild(cards);
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
if (resolved.length > 0) {
|
|
2238
|
-
var resolvedTitle = document.createElement('div');
|
|
2239
|
-
resolvedTitle.className = 'ov-section-title ov-resolved-title';
|
|
2240
|
-
resolvedTitle.innerHTML = '<span style="color:var(--green)">\u2713</span> Resolved <span class="ov-issue-count">' + resolved.length + '</span>';
|
|
2241
|
-
container.appendChild(resolvedTitle);
|
|
2242
|
-
|
|
2243
|
-
var resolvedCards = document.createElement('div');
|
|
2244
|
-
resolvedCards.className = 'ov-cards';
|
|
2245
|
-
|
|
2246
|
-
for (var ri = 0; ri < resolved.length; ri++) {
|
|
2247
|
-
var rInsight = resolved[ri].insight;
|
|
2248
|
-
var rCard = document.createElement('div');
|
|
2249
|
-
rCard.className = 'ov-card ov-card-resolved';
|
|
2250
|
-
rCard.innerHTML =
|
|
2251
|
-
'<span class="ov-card-icon resolved">\u2713</span>' +
|
|
2252
|
-
'<div class="ov-card-body">' +
|
|
2253
|
-
'<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rInsight.title) + '</div>' +
|
|
2254
|
-
'<div class="ov-card-desc">' + rInsight.desc + '</div>' +
|
|
2255
|
-
'</div>';
|
|
2256
|
-
resolvedCards.appendChild(rCard);
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
container.appendChild(resolvedCards);
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
function renderSecurity() {
|
|
2265
|
-
var container = document.getElementById('security-content');
|
|
2266
|
-
if (!container) return;
|
|
2267
|
-
container.innerHTML = '';
|
|
2268
|
-
var SEV = { critical: { icon: '\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\u2139', cls: 'info', sort: 2 } };
|
|
2269
|
-
|
|
2270
|
-
var all = (state.findings || []).slice();
|
|
2271
|
-
var insightsList = state.insights || [];
|
|
2272
|
-
for (var ix = 0; ix < insightsList.length; ix++) {
|
|
2273
|
-
var si = insightsList[ix];
|
|
2274
|
-
all.push({
|
|
2275
|
-
findingId: si.key,
|
|
2276
|
-
state: si.state,
|
|
2277
|
-
aiStatus: si.aiStatus,
|
|
2278
|
-
aiNotes: si.aiNotes,
|
|
2279
|
-
finding: {
|
|
2280
|
-
severity: si.insight.severity,
|
|
2281
|
-
rule: 'insight-' + si.insight.type,
|
|
2282
|
-
title: si.insight.title,
|
|
2283
|
-
desc: si.insight.desc,
|
|
2284
|
-
hint: si.insight.hint,
|
|
2285
|
-
endpoint: si.insight.nav || 'global',
|
|
2286
|
-
count: 1
|
|
2287
|
-
}
|
|
2288
|
-
});
|
|
2289
|
-
}
|
|
2290
|
-
var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
|
|
2291
|
-
var resolved = all.filter(function(f) { return f.state === 'resolved'; });
|
|
2292
|
-
|
|
2293
|
-
if (open.length === 0 && resolved.length === 0) {
|
|
2294
|
-
var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
|
|
2295
|
-
if (!hasData) {
|
|
2296
|
-
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>';
|
|
2297
|
-
} else {
|
|
2298
|
-
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>';
|
|
2299
|
-
}
|
|
2300
|
-
return;
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
var critCount = 0, warnCount = 0, infoCount = 0;
|
|
2304
|
-
for (var ci = 0; ci < open.length; ci++) {
|
|
2305
|
-
var sev = open[ci].finding.severity;
|
|
2306
|
-
if (sev === 'critical') critCount++;
|
|
2307
|
-
else if (sev === 'info') infoCount++;
|
|
2308
|
-
else warnCount++;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
var summaryEl = document.createElement('div');
|
|
2312
|
-
summaryEl.className = 'sec-summary';
|
|
2313
|
-
summaryEl.innerHTML =
|
|
2314
|
-
'<div class="sec-summary-left">' +
|
|
2315
|
-
'<span class="sec-summary-count">' + open.length + '</span>' +
|
|
2316
|
-
'<span class="sec-summary-label">open issue' + (open.length !== 1 ? 's' : '') + '</span>' +
|
|
2317
|
-
(resolved.length > 0 ? '<span class="sec-resolved-badge">' + resolved.length + ' resolved</span>' : '') +
|
|
2318
|
-
'</div>' +
|
|
2319
|
-
'<div class="sec-summary-right">' +
|
|
2320
|
-
(critCount > 0 ? '<span class="sec-badge critical">' + critCount + ' critical</span>' : '') +
|
|
2321
|
-
(warnCount > 0 ? '<span class="sec-badge warning">' + warnCount + ' warning</span>' : '') +
|
|
2322
|
-
(infoCount > 0 ? '<span class="sec-badge info">' + infoCount + ' info</span>' : '') +
|
|
2323
|
-
'</div>';
|
|
2324
|
-
container.appendChild(summaryEl);
|
|
2325
|
-
|
|
2326
|
-
if (open.length === 0 && resolved.length > 0) {
|
|
2327
|
-
var allFixed = document.createElement('div');
|
|
2328
|
-
allFixed.className = 'sec-clear';
|
|
2329
|
-
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>';
|
|
2330
|
-
container.appendChild(allFixed);
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
if (open.length > 0) {
|
|
2334
|
-
var groups = {};
|
|
2335
|
-
var groupOrder = [];
|
|
2336
|
-
for (var gi = 0; gi < open.length; gi++) {
|
|
2337
|
-
var sf = open[gi];
|
|
2338
|
-
var f = sf.finding;
|
|
2339
|
-
if (!groups[f.rule]) {
|
|
2340
|
-
groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
|
|
2341
|
-
groupOrder.push(f.rule);
|
|
2342
|
-
}
|
|
2343
|
-
groups[f.rule].items.push(sf);
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
groupOrder.sort(function(a, b) {
|
|
2347
|
-
var sa = SEV[groups[a].severity].sort;
|
|
2348
|
-
var sb = SEV[groups[b].severity].sort;
|
|
2349
|
-
if (sa !== sb) return sa - sb;
|
|
2350
|
-
return groups[b].items.length - groups[a].items.length;
|
|
2351
|
-
});
|
|
2352
|
-
|
|
2353
|
-
for (var oi = 0; oi < groupOrder.length; oi++) {
|
|
2354
|
-
var group = groups[groupOrder[oi]];
|
|
2355
|
-
var section = document.createElement('div');
|
|
2356
|
-
section.className = 'sec-group';
|
|
2357
|
-
|
|
2358
|
-
var sevCfg = SEV[group.severity];
|
|
2359
|
-
var iconCls = sevCfg.cls;
|
|
2360
|
-
var iconChar = sevCfg.icon;
|
|
2361
|
-
|
|
2362
|
-
var header = document.createElement('div');
|
|
2363
|
-
header.className = 'sec-group-header';
|
|
2364
|
-
header.innerHTML =
|
|
2365
|
-
'<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
2366
|
-
'<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
|
|
2367
|
-
'<span class="sec-group-count">' + group.items.length + '</span>';
|
|
2368
|
-
section.appendChild(header);
|
|
2369
|
-
|
|
2370
|
-
if (group.hint) {
|
|
2371
|
-
var hintEl = document.createElement('div');
|
|
2372
|
-
hintEl.className = 'sec-hint';
|
|
2373
|
-
hintEl.textContent = group.hint;
|
|
2374
|
-
section.appendChild(hintEl);
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
var list = document.createElement('div');
|
|
2378
|
-
list.className = 'sec-items';
|
|
2379
|
-
for (var ii = 0; ii < group.items.length; ii++) {
|
|
2380
|
-
var sf2 = group.items[ii];
|
|
2381
|
-
var item = sf2.finding;
|
|
2382
|
-
var row = document.createElement('div');
|
|
2383
|
-
row.className = 'sec-item';
|
|
2384
|
-
var aiBadge = '';
|
|
2385
|
-
if (sf2.state === 'fixing' && sf2.aiStatus === 'fixed') {
|
|
2386
|
-
aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>';
|
|
2387
|
-
} else if (sf2.aiStatus === 'wont_fix') {
|
|
2388
|
-
aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>';
|
|
2389
|
-
}
|
|
2390
|
-
var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
|
|
2391
|
-
row.innerHTML =
|
|
2392
|
-
'<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
|
|
2393
|
-
(item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '') +
|
|
2394
|
-
aiBadge + aiNotes;
|
|
2395
|
-
list.appendChild(row);
|
|
2396
|
-
}
|
|
2397
|
-
section.appendChild(list);
|
|
2398
|
-
container.appendChild(section);
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
if (resolved.length > 0) {
|
|
2403
|
-
var resolvedTitle = document.createElement('div');
|
|
2404
|
-
resolvedTitle.className = 'sec-resolved-title';
|
|
2405
|
-
resolvedTitle.innerHTML = '<span class="sec-resolved-check">\u2713</span> Resolved <span class="sec-resolved-count">' + resolved.length + '</span>';
|
|
2406
|
-
container.appendChild(resolvedTitle);
|
|
2407
|
-
|
|
2408
|
-
var resolvedGroup = document.createElement('div');
|
|
2409
|
-
resolvedGroup.className = 'sec-group sec-group-resolved';
|
|
2410
|
-
var resolvedItems = document.createElement('div');
|
|
2411
|
-
resolvedItems.className = 'sec-items';
|
|
2412
|
-
for (var ri = 0; ri < resolved.length; ri++) {
|
|
2413
|
-
var rsf = resolved[ri];
|
|
2414
|
-
var rf = rsf.finding;
|
|
2415
|
-
var rRow = document.createElement('div');
|
|
2416
|
-
rRow.className = 'sec-item sec-item-resolved';
|
|
2417
|
-
var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
|
|
2418
|
-
var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
|
|
2419
|
-
rRow.innerHTML =
|
|
2420
|
-
'<span class="sec-resolved-item-icon">\u2713</span>' +
|
|
2421
|
-
'<div class="sec-item-desc">' + escHtml(rf.title) + ' \u2014 ' + escHtml(rf.endpoint) + '</div>' +
|
|
2422
|
-
verifiedBadge + rNotes;
|
|
2423
|
-
resolvedItems.appendChild(rRow);
|
|
2424
|
-
}
|
|
2425
|
-
resolvedGroup.appendChild(resolvedItems);
|
|
2426
|
-
container.appendChild(resolvedGroup);
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
var VIEW_CONTAINERS = {
|
|
2432
|
-
overview: 'overview-container',
|
|
2433
|
-
actions: 'flow-container',
|
|
2434
|
-
requests: 'request-container',
|
|
2435
|
-
fetches: 'fetch-container',
|
|
2436
|
-
queries: 'query-container',
|
|
2437
|
-
errors: 'error-container',
|
|
2438
|
-
logs: 'log-container',
|
|
2439
|
-
performance: 'performance-container',
|
|
2440
|
-
security: 'security-container'
|
|
2441
|
-
};
|
|
2442
|
-
var VIEW_TITLES = {
|
|
2443
|
-
overview: 'Overview',
|
|
2444
|
-
actions: 'Actions',
|
|
2445
|
-
requests: 'Requests',
|
|
2446
|
-
fetches: 'Server Fetches',
|
|
2447
|
-
queries: 'Queries',
|
|
2448
|
-
errors: 'Errors',
|
|
2449
|
-
logs: 'Logs',
|
|
2450
|
-
performance: 'Performance',
|
|
2451
|
-
security: 'Security'
|
|
2452
|
-
};
|
|
2453
|
-
var VIEW_SUBTITLES = {
|
|
2454
|
-
overview: 'Live summary of your application',
|
|
2455
|
-
actions: 'User actions captured as sequences of HTTP requests',
|
|
2456
|
-
requests: 'All HTTP requests proxied through brakit',
|
|
2457
|
-
fetches: 'Outbound HTTP calls made by your server to external services',
|
|
2458
|
-
queries: 'Database queries executed during request handling',
|
|
2459
|
-
errors: 'Unhandled exceptions and errors thrown by your application',
|
|
2460
|
-
logs: 'Console output from your application',
|
|
2461
|
-
performance: 'Endpoint health and response time trends',
|
|
2462
|
-
security: 'Security findings and recommendations'
|
|
2463
|
-
};
|
|
2464
|
-
|
|
2465
|
-
async function init() {
|
|
2466
|
-
try {
|
|
2467
|
-
var res = await fetch('/__brakit/api/flows');
|
|
2468
|
-
var data = await res.json();
|
|
2469
|
-
state.flows = data.flows;
|
|
2470
|
-
renderFlows();
|
|
2471
|
-
} catch(e) { console.error('Failed to load flows', e); }
|
|
2472
|
-
|
|
2473
|
-
try {
|
|
2474
|
-
var res2 = await fetch('/__brakit/api/requests');
|
|
2475
|
-
var data2 = await res2.json();
|
|
2476
|
-
state.requests = data2.requests;
|
|
2477
|
-
renderRequests();
|
|
2478
|
-
} catch(e) { console.warn('[brakit]', e); }
|
|
2479
|
-
|
|
2480
|
-
await Promise.all([loadFetches(), loadErrors(), loadLogs(), loadQueries(), loadMetrics()]);
|
|
2481
|
-
|
|
2482
|
-
try {
|
|
2483
|
-
var res3 = await fetch('/__brakit/api/insights');
|
|
2484
|
-
var data3 = await res3.json();
|
|
2485
|
-
state.insights = data3.insights || [];
|
|
2486
|
-
} catch(e) { console.warn('[brakit]', e); }
|
|
2487
|
-
|
|
2488
|
-
try {
|
|
2489
|
-
var res4 = await fetch('/__brakit/api/security');
|
|
2490
|
-
var data4 = await res4.json();
|
|
2491
|
-
state.findings = data4.findings || [];
|
|
2492
|
-
} catch(e) { console.warn('[brakit]', e); }
|
|
2493
|
-
|
|
2494
|
-
updateStats();
|
|
2495
|
-
renderOverview();
|
|
2496
|
-
|
|
2497
|
-
var events = new EventSource('/__brakit/api/events');
|
|
2498
|
-
var reloadTimer = null;
|
|
2499
|
-
var perfReloadTimer = null;
|
|
2500
|
-
events.onmessage = function(e) {
|
|
2501
|
-
var req = JSON.parse(e.data);
|
|
2502
|
-
if (req.path && req.path.startsWith('/__brakit')) return;
|
|
2503
|
-
state.requests.unshift(req);
|
|
2504
|
-
if (state.requests.length > 1000) state.requests.pop();
|
|
2505
|
-
clearTimeout(reloadTimer);
|
|
2506
|
-
reloadTimer = setTimeout(reloadFlows, 300);
|
|
2507
|
-
prependRequestRow(req);
|
|
2508
|
-
updateStats();
|
|
2509
|
-
if (state.activeView === 'performance') {
|
|
2510
|
-
clearTimeout(perfReloadTimer);
|
|
2511
|
-
perfReloadTimer = setTimeout(loadMetrics, 500);
|
|
2512
|
-
}
|
|
2513
|
-
};
|
|
2514
|
-
|
|
2515
|
-
function registerTelemetryListener(eventName, stateKey, prependFn) {
|
|
2516
|
-
events.addEventListener(eventName, function(e) {
|
|
2517
|
-
var item = JSON.parse(e.data);
|
|
2518
|
-
state[stateKey].unshift(item);
|
|
2519
|
-
if (state[stateKey].length > 1000) state[stateKey].pop();
|
|
2520
|
-
prependFn(item);
|
|
2521
|
-
updateStats();
|
|
2522
|
-
if (item.parentRequestId) { invalidateTimelineCache(item.parentRequestId); refreshVisibleTimeline(item.parentRequestId); }
|
|
2523
|
-
});
|
|
2524
|
-
}
|
|
2525
|
-
registerTelemetryListener('fetch', 'fetches', prependFetchRow);
|
|
2526
|
-
registerTelemetryListener('log', 'logs', prependLogRow);
|
|
2527
|
-
registerTelemetryListener('error_event', 'errors', prependErrorRow);
|
|
2528
|
-
registerTelemetryListener('query', 'queries', prependQueryRow);
|
|
2529
|
-
|
|
2530
|
-
events.addEventListener('insights', function(e) {
|
|
2531
|
-
state.insights = JSON.parse(e.data);
|
|
2532
|
-
if (state.activeView === 'overview') renderOverview();
|
|
2533
|
-
if (state.activeView === 'security') renderSecurity();
|
|
2534
|
-
updateStats();
|
|
2535
|
-
});
|
|
2536
|
-
|
|
2537
|
-
events.addEventListener('security', function(e) {
|
|
2538
|
-
state.findings = JSON.parse(e.data);
|
|
2539
|
-
if (state.activeView === 'security') renderSecurity();
|
|
2540
|
-
updateStats();
|
|
2541
|
-
});
|
|
2542
|
-
|
|
2543
|
-
window.addEventListener('beforeunload', function() {
|
|
2544
|
-
events.close();
|
|
2545
|
-
clearTimeout(reloadTimer);
|
|
2546
|
-
clearTimeout(perfReloadTimer);
|
|
2547
|
-
});
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
async function reloadFlows() {
|
|
2551
|
-
try {
|
|
2552
|
-
var res = await fetch('/__brakit/api/flows');
|
|
2553
|
-
var data = await res.json();
|
|
2554
|
-
state.flows = data.flows;
|
|
2555
|
-
renderFlows();
|
|
2556
|
-
updateStats();
|
|
2557
|
-
} catch(e) { console.warn('[brakit]', e); }
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
function switchView(view) {
|
|
2561
|
-
Object.keys(VIEW_CONTAINERS).forEach(function(v) {
|
|
2562
|
-
var el = document.getElementById(VIEW_CONTAINERS[v]);
|
|
2563
|
-
if (el) el.style.display = v === view ? 'block' : 'none';
|
|
2564
|
-
});
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
var sidebarItems = document.querySelectorAll('.sidebar-item:not(.disabled)');
|
|
2568
|
-
sidebarItems.forEach(function(item) {
|
|
2569
|
-
item.addEventListener('click', function() {
|
|
2570
|
-
var view = item.getAttribute('data-view');
|
|
2571
|
-
if (!view || view === state.activeView) return;
|
|
2572
|
-
sidebarItems.forEach(function(i) { i.classList.remove('active'); });
|
|
2573
|
-
item.classList.add('active');
|
|
2574
|
-
state.activeView = view;
|
|
2575
|
-
fetch('/__brakit/api/tab?tab=' + encodeURIComponent(view)).catch(function(){});
|
|
2576
|
-
document.getElementById('header-title').textContent = VIEW_TITLES[view] || view;
|
|
2577
|
-
document.getElementById('header-sub').textContent = VIEW_SUBTITLES[view] || '';
|
|
2578
|
-
document.getElementById('mode-toggle').style.display = view === 'actions' ? 'flex' : 'none';
|
|
2579
|
-
if (view === 'overview') renderOverview();
|
|
2580
|
-
if (view === 'security') renderSecurity();
|
|
2581
|
-
if (view === 'performance') loadMetrics();
|
|
2582
|
-
switchView(view);
|
|
2583
|
-
});
|
|
2584
|
-
});
|
|
2585
|
-
|
|
2586
|
-
document.getElementById('mode-simple').addEventListener('click', function() {
|
|
2587
|
-
state.viewMode = 'simple';
|
|
2588
|
-
document.getElementById('mode-simple').classList.add('active');
|
|
2589
|
-
document.getElementById('mode-detailed').classList.remove('active');
|
|
2590
|
-
collapseAll('.flow-row', '.flow-expand');
|
|
2591
|
-
});
|
|
2592
|
-
document.getElementById('mode-detailed').addEventListener('click', function() {
|
|
2593
|
-
state.viewMode = 'detailed';
|
|
2594
|
-
document.getElementById('mode-detailed').classList.add('active');
|
|
2595
|
-
document.getElementById('mode-simple').classList.remove('active');
|
|
2596
|
-
collapseAll('.flow-row', '.flow-expand');
|
|
2597
|
-
});
|
|
2598
|
-
|
|
2599
|
-
function updateStats() {
|
|
2600
|
-
var reqs = state.requests.filter(function(r) { return !r.path || !r.path.startsWith('/__brakit'); });
|
|
2601
|
-
var errors = reqs.filter(function(r) { return r.statusCode >= 400; }).length;
|
|
2602
|
-
var avg = reqs.length > 0 ? Math.round(reqs.reduce(function(s,r) { return s + r.durationMs; }, 0) / reqs.length) : 0;
|
|
2603
|
-
document.getElementById('stat-total').textContent = reqs.length + ' request' + (reqs.length !== 1 ? 's' : '');
|
|
2604
|
-
document.getElementById('stat-flows').textContent = state.flows.length + ' action' + (state.flows.length !== 1 ? 's' : '');
|
|
2605
|
-
document.getElementById('stat-errors').textContent = errors + ' error' + (errors !== 1 ? 's' : '');
|
|
2606
|
-
document.getElementById('stat-avg').textContent = 'Avg: ' + avg + 'ms';
|
|
2607
|
-
var actionCount = document.getElementById('sidebar-count-actions');
|
|
2608
|
-
var requestCount = document.getElementById('sidebar-count-requests');
|
|
2609
|
-
var fetchCount = document.getElementById('sidebar-count-fetches');
|
|
2610
|
-
var errorCount = document.getElementById('sidebar-count-errors');
|
|
2611
|
-
var logCount = document.getElementById('sidebar-count-logs');
|
|
2612
|
-
var queryCount = document.getElementById('sidebar-count-queries');
|
|
2613
|
-
if (actionCount) actionCount.textContent = state.flows.length;
|
|
2614
|
-
if (requestCount) requestCount.textContent = reqs.length;
|
|
2615
|
-
if (fetchCount) fetchCount.textContent = state.fetches.length;
|
|
2616
|
-
if (errorCount) errorCount.textContent = state.errors.length;
|
|
2617
|
-
if (logCount) logCount.textContent = state.logs.length;
|
|
2618
|
-
if (queryCount) queryCount.textContent = state.queries.length;
|
|
2619
|
-
var secCount = document.getElementById('sidebar-count-security');
|
|
2620
|
-
if (secCount) {
|
|
2621
|
-
var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
|
|
2622
|
-
secCount.textContent = numFindings;
|
|
2623
|
-
secCount.style.display = numFindings > 0 ? '' : 'none';
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
function copyAsCurl(req) {
|
|
2628
|
-
var headers = Object.entries(req.headers || {})
|
|
2629
|
-
.filter(function(e) { return ['host', 'connection', 'accept-encoding'].indexOf(e[0]) === -1; })
|
|
2630
|
-
.map(function(e) { return "-H '" + e[0] + ": " + e[1] + "'"; })
|
|
2631
|
-
.join(' ');
|
|
2632
|
-
var body = req.requestBody ? " -d '" + req.requestBody.replace(/'/g, "'\\''") + "'" : '';
|
|
2633
|
-
var curl = "curl -X " + req.method + " " + headers + body + " 'http://localhost:" + PORT + req.url + "'";
|
|
2634
|
-
navigator.clipboard.writeText(curl).then(function() { showToast('Copied cURL command'); });
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
document.getElementById('clear-btn').addEventListener('click', async function() {
|
|
2638
|
-
if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
|
|
2639
|
-
await fetch('/__brakit/api/clear', {method: 'POST'});
|
|
2640
|
-
state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
|
|
2641
|
-
state.insights = []; state.findings = [];
|
|
2642
|
-
graphData = []; selectedEndpoint = '__all__'; timelineCache = {};
|
|
2643
|
-
renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
|
|
2644
|
-
showToast('Cleared');
|
|
2645
|
-
});
|
|
2646
|
-
|
|
2647
|
-
init();
|
|
2648
|
-
|
|
2649
|
-
})();
|
|
2650
|
-
</script>
|
|
1321
|
+
<bk-toast></bk-toast>
|
|
1322
|
+
`}renderSidebarItem(t,s,r,i,a=false){return n`
|
|
1323
|
+
<button class="sidebar-item ${this.activeView===t?"active":""}" @click=${()=>this.switchView(t)}>
|
|
1324
|
+
<span class="item-icon">${r}</span>
|
|
1325
|
+
<span class="item-label">${s}</span>
|
|
1326
|
+
${i!==void 0?n`<span class="item-count" style="display:${a?"none":""}">${i}</span>`:d}
|
|
1327
|
+
</button>
|
|
1328
|
+
`}};u([Se({context:_})],Z.prototype,"store",2),u([$()],Z.prototype,"activeView",2),u([$()],Z.prototype,"viewMode",2),Z=u([g("bk-dashboard")],Z);
|
|
1329
|
+
/*! Bundled license information:
|
|
1330
|
+
|
|
1331
|
+
@lit/reactive-element/css-tag.js:
|
|
1332
|
+
(**
|
|
1333
|
+
* @license
|
|
1334
|
+
* Copyright 2019 Google LLC
|
|
1335
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1336
|
+
*)
|
|
1337
|
+
|
|
1338
|
+
@lit/reactive-element/reactive-element.js:
|
|
1339
|
+
lit-html/lit-html.js:
|
|
1340
|
+
lit-element/lit-element.js:
|
|
1341
|
+
@lit/reactive-element/decorators/custom-element.js:
|
|
1342
|
+
@lit/reactive-element/decorators/property.js:
|
|
1343
|
+
@lit/reactive-element/decorators/state.js:
|
|
1344
|
+
@lit/reactive-element/decorators/event-options.js:
|
|
1345
|
+
@lit/reactive-element/decorators/base.js:
|
|
1346
|
+
@lit/reactive-element/decorators/query.js:
|
|
1347
|
+
@lit/reactive-element/decorators/query-all.js:
|
|
1348
|
+
@lit/reactive-element/decorators/query-async.js:
|
|
1349
|
+
@lit/reactive-element/decorators/query-assigned-nodes.js:
|
|
1350
|
+
@lit/context/lib/decorators/provide.js:
|
|
1351
|
+
(**
|
|
1352
|
+
* @license
|
|
1353
|
+
* Copyright 2017 Google LLC
|
|
1354
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1355
|
+
*)
|
|
1356
|
+
|
|
1357
|
+
lit-html/is-server.js:
|
|
1358
|
+
@lit/context/lib/decorators/consume.js:
|
|
1359
|
+
(**
|
|
1360
|
+
* @license
|
|
1361
|
+
* Copyright 2022 Google LLC
|
|
1362
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1363
|
+
*)
|
|
1364
|
+
|
|
1365
|
+
@lit/reactive-element/decorators/query-assigned-elements.js:
|
|
1366
|
+
@lit/context/lib/context-request-event.js:
|
|
1367
|
+
@lit/context/lib/create-context.js:
|
|
1368
|
+
@lit/context/lib/controllers/context-consumer.js:
|
|
1369
|
+
@lit/context/lib/value-notifier.js:
|
|
1370
|
+
@lit/context/lib/controllers/context-provider.js:
|
|
1371
|
+
@lit/context/lib/context-root.js:
|
|
1372
|
+
(**
|
|
1373
|
+
* @license
|
|
1374
|
+
* Copyright 2021 Google LLC
|
|
1375
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1376
|
+
*)
|
|
1377
|
+
*/})();</script>
|
|
2651
1378
|
</body>
|
|
2652
1379
|
</html>
|