brakit 0.8.4 → 0.8.6
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 +3 -3
- package/dist/api.d.ts +133 -111
- package/dist/api.js +468 -327
- package/dist/bin/brakit.js +864 -448
- package/dist/dashboard.html +2653 -0
- package/dist/mcp/server.js +248 -158
- package/dist/runtime/index.js +1357 -783
- package/package.json +3 -2
|
@@ -0,0 +1,2653 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>brakit</title>
|
|
7
|
+
<style>
|
|
8
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
9
|
+
:root{
|
|
10
|
+
--bg:#ffffff;--bg-sidebar:#f8f8fa;--bg-card:#ffffff;--bg-hover:#f4f4f5;--bg-detail:#fafafa;
|
|
11
|
+
--bg-active:#ede9fe;--bg-muted:#f4f4f5;
|
|
12
|
+
--border:#e4e4e7;--border-light:#d4d4d8;--border-subtle:#f4f4f5;
|
|
13
|
+
--text:#18181b;--text-dim:#52525b;--text-muted:#a1a1aa;
|
|
14
|
+
--accent:#7c3aed;
|
|
15
|
+
--green:#16a34a;
|
|
16
|
+
--blue:#2563eb;
|
|
17
|
+
--amber:#d97706;
|
|
18
|
+
--red:#dc2626;
|
|
19
|
+
--cyan:#0891b2;
|
|
20
|
+
--green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
|
|
21
|
+
--sidebar-width:232px;--header-height:52px;
|
|
22
|
+
--radius:8px;--radius-sm:6px;
|
|
23
|
+
--shadow-sm:0 1px 2px rgba(0,0,0,0.05);
|
|
24
|
+
--shadow-md:0 1px 3px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.04);
|
|
25
|
+
--shadow-lg:0 4px 12px rgba(0,0,0,0.08),0 1px 4px rgba(0,0,0,0.04);
|
|
26
|
+
--breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
|
|
27
|
+
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
|
|
28
|
+
--sans:Inter,system-ui,-apple-system,sans-serif;
|
|
29
|
+
}
|
|
30
|
+
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:15px;overflow:hidden;-webkit-font-smoothing:antialiased}
|
|
31
|
+
|
|
32
|
+
/* Scrollbar */
|
|
33
|
+
::-webkit-scrollbar{width:8px}
|
|
34
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
35
|
+
::-webkit-scrollbar-thumb{background:#d4d4d8;border-radius:4px}
|
|
36
|
+
::-webkit-scrollbar-thumb:hover{background:#a1a1aa}
|
|
37
|
+
|
|
38
|
+
/* Tooltip */
|
|
39
|
+
.tooltip{position:relative}
|
|
40
|
+
.tooltip::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:#ffffff;border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-size:11px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;box-shadow:var(--shadow-lg)}
|
|
41
|
+
.tooltip:hover::after{opacity:1}
|
|
42
|
+
|
|
43
|
+
/* Toast */
|
|
44
|
+
.toast{position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-8px);background:#f0fdf4;border:1px solid #86efac;color:#15803d;padding:12px 24px;border-radius:10px;font-size:13px;font-weight:500;opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:100;box-shadow:var(--shadow-lg)}
|
|
45
|
+
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
46
|
+
|
|
47
|
+
/* Empty */
|
|
48
|
+
.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:400px;color:var(--text-muted);gap:12px}
|
|
49
|
+
.empty-title{font-size:19px;font-weight:600;color:var(--text-dim)}
|
|
50
|
+
.empty-sub{font-size:14px}
|
|
51
|
+
|
|
52
|
+
/* View toggle */
|
|
53
|
+
.view-flows{display:block}.view-requests{display:none}
|
|
54
|
+
|
|
55
|
+
/* Layout */
|
|
56
|
+
.app{display:grid;grid-template-columns:var(--sidebar-width) 1fr;height:100vh;overflow:hidden}
|
|
57
|
+
.main-panel{display:flex;flex-direction:column;overflow:hidden}
|
|
58
|
+
|
|
59
|
+
/* Sidebar */
|
|
60
|
+
.sidebar{background:var(--bg-sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}
|
|
61
|
+
.sidebar-logo{padding:20px 24px 24px;border-bottom:1px solid var(--border-subtle)}
|
|
62
|
+
.sidebar-logo .logo-text{font-weight:800;font-size:21px;color:var(--accent);letter-spacing:-.5px}
|
|
63
|
+
.sidebar-logo .logo-version{font-weight:400;font-size:11px;color:var(--text-muted);margin-left:8px;letter-spacing:0}
|
|
64
|
+
.sidebar-nav{padding:12px;flex:1}
|
|
65
|
+
.sidebar-section{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);padding:16px 12px 8px}
|
|
66
|
+
.sidebar-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius);color:var(--text-dim);font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
|
|
67
|
+
.sidebar-item:hover{background:var(--bg-hover);color:var(--text)}
|
|
68
|
+
.sidebar-item.active{background:var(--bg-active);color:var(--accent)}
|
|
69
|
+
.sidebar-item .item-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;opacity:.5}
|
|
70
|
+
.sidebar-item.active .item-icon{opacity:1}
|
|
71
|
+
.sidebar-item:hover .item-icon{opacity:.8}
|
|
72
|
+
.sidebar-item .item-label{flex:1}
|
|
73
|
+
.sidebar-item .item-count{font-size:12px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;min-width:24px;text-align:center}
|
|
74
|
+
.sidebar-item.disabled{opacity:.35;cursor:default;pointer-events:none}
|
|
75
|
+
.sidebar-item .coming-soon{font-size:10px;color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;font-weight:600;letter-spacing:.3px}
|
|
76
|
+
.sidebar-footer{padding:16px 24px;border-top:1px solid var(--border-subtle);font-size:12px;color:var(--text-muted);font-family:var(--mono)}
|
|
77
|
+
|
|
78
|
+
/* Header */
|
|
79
|
+
.header{display:flex;align-items:center;gap:16px;padding:0 28px;height:var(--header-height);border-bottom:1px solid var(--border);background:var(--bg);flex-shrink:0;box-shadow:0 1px 0 rgba(0,0,0,0.03)}
|
|
80
|
+
.header-left{display:flex;flex-direction:column;justify-content:center}
|
|
81
|
+
.header-title{font-weight:600;font-size:17px;color:var(--text);letter-spacing:-.2px;line-height:1.2}
|
|
82
|
+
.header-sub{font-size:11px;color:var(--text-muted);line-height:1.2}
|
|
83
|
+
.header-right{margin-left:auto;display:flex;gap:10px;align-items:center}
|
|
84
|
+
|
|
85
|
+
/* Segmented control */
|
|
86
|
+
.segmented-control{display:flex;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:3px;gap:2px}
|
|
87
|
+
.segmented-btn{background:transparent;border:none;color:var(--text-muted);padding:6px 14px;font-size:13px;cursor:pointer;transition:all .15s;font-family:var(--sans);font-weight:500;border-radius:var(--radius-sm)}
|
|
88
|
+
.segmented-btn:hover{color:var(--text)}
|
|
89
|
+
.segmented-btn.active{background:#ffffff;color:var(--text);box-shadow:var(--shadow-sm)}
|
|
90
|
+
|
|
91
|
+
.btn{background:#ffffff;border:1px solid var(--border);color:var(--text-dim);padding:7px 14px;border-radius:var(--radius);font-size:13px;cursor:pointer;transition:all .15s;font-family:var(--sans);font-weight:500;box-shadow:var(--shadow-sm)}
|
|
92
|
+
.btn:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-light)}
|
|
93
|
+
.btn-danger:hover{border-color:rgba(220,38,38,.3);color:var(--red);background:rgba(220,38,38,.05)}
|
|
94
|
+
|
|
95
|
+
/* Content */
|
|
96
|
+
.main-content{flex:1;overflow-y:auto}
|
|
97
|
+
|
|
98
|
+
/* Column headers */
|
|
99
|
+
.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)}
|
|
100
|
+
|
|
101
|
+
/* Footer */
|
|
102
|
+
.footer{padding:10px 28px;border-top:1px solid var(--border);font-size:13px;color:var(--text-muted);display:flex;gap:24px;font-family:var(--mono);flex-shrink:0;background:var(--bg-sidebar)}
|
|
103
|
+
.footer .error-count{color:var(--red)}
|
|
104
|
+
|
|
105
|
+
/* Flow rows */
|
|
106
|
+
.flow-row{padding:12px 28px;border-bottom:1px solid var(--border-subtle);cursor:pointer;transition:background .1s}
|
|
107
|
+
.flow-row:hover{background:var(--bg-hover)}
|
|
108
|
+
.flow-row.expanded{background:var(--bg-muted)}
|
|
109
|
+
.flow-summary-row{display:flex;align-items:center;gap:14px;font-size:14px}
|
|
110
|
+
.flow-status-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
|
|
111
|
+
.flow-status-dot.dot-clean{background:var(--green)}
|
|
112
|
+
.flow-status-dot.dot-warn{background:var(--amber)}
|
|
113
|
+
.flow-status-dot.dot-error{background:var(--red)}
|
|
114
|
+
.flow-label{font-weight:500;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
|
|
115
|
+
.flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
|
|
116
|
+
.flow-badge-pill{font-size:11px;flex-shrink:0;font-family:var(--mono);font-weight:600;padding:2px 10px;border-radius:10px;text-align:center}
|
|
117
|
+
.flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
|
|
118
|
+
.flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
|
|
119
|
+
.flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
|
|
120
|
+
.flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
|
|
121
|
+
|
|
122
|
+
/* Flow expand panel */
|
|
123
|
+
.flow-expand{display:none;padding:12px 28px 16px;border-bottom:1px solid var(--border);background:var(--bg-detail)}
|
|
124
|
+
.flow-expand.open{display:block}
|
|
125
|
+
|
|
126
|
+
/* Request cards in expanded flow */
|
|
127
|
+
.traffic-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;margin-bottom:10px;box-shadow:var(--shadow-sm)}
|
|
128
|
+
.traffic-card:last-child{margin-bottom:0}
|
|
129
|
+
|
|
130
|
+
/* Simple mode traffic */
|
|
131
|
+
.flow-traffic{padding:0;font-family:var(--mono);font-size:13px}
|
|
132
|
+
.traffic-card-header{display:flex;align-items:center;gap:10px;margin-bottom:0}
|
|
133
|
+
.traffic-card-header.has-details{margin-bottom:10px}
|
|
134
|
+
|
|
135
|
+
/* Method badges */
|
|
136
|
+
.method-badge{display:inline-flex;align-items:center;justify-content:center;padding:3px 8px;border-radius:5px;font-size:10px;font-weight:700;font-family:var(--mono);letter-spacing:.3px;flex-shrink:0}
|
|
137
|
+
.method-badge-GET{background:var(--green-bg);color:var(--green)}
|
|
138
|
+
.method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
|
|
139
|
+
.method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
|
|
140
|
+
.method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
|
|
141
|
+
.method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
|
|
142
|
+
|
|
143
|
+
/* Status pills */
|
|
144
|
+
.status-pill{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:600;font-family:var(--mono);flex-shrink:0}
|
|
145
|
+
.status-pill-2xx{background:var(--green-bg);color:var(--green)}
|
|
146
|
+
.status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
|
|
147
|
+
.status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
|
|
148
|
+
.status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
|
|
149
|
+
|
|
150
|
+
.traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
|
|
151
|
+
.traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
|
|
152
|
+
.traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
|
|
153
|
+
.traffic-card-size{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
154
|
+
.traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
|
|
155
|
+
|
|
156
|
+
/* Body toggles */
|
|
157
|
+
.traffic-body{padding:0;margin-top:8px}
|
|
158
|
+
.traffic-body-toggle{font-size:11px;color:var(--text-dim);display:inline-flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-muted);font-family:var(--mono);letter-spacing:.3px;transition:all .15s;margin-right:6px;margin-bottom:4px}
|
|
159
|
+
.traffic-body-toggle:hover{border-color:var(--border-light);color:var(--text);background:var(--bg-hover)}
|
|
160
|
+
.traffic-body-toggle .arrow-out{color:var(--blue)}
|
|
161
|
+
.traffic-body-toggle .arrow-in{color:var(--green)}
|
|
162
|
+
.traffic-body-toggle .chevron{font-size:9px;transition:transform .15s;display:inline-block}
|
|
163
|
+
.traffic-body-toggle.open .chevron{transform:rotate(90deg)}
|
|
164
|
+
.traffic-body pre{background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;font-family:var(--mono);font-size:12px;overflow-x:auto;max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;line-height:1.5;margin:6px 0 0;display:none}
|
|
165
|
+
.traffic-body pre.open{display:block}
|
|
166
|
+
.traffic-separator{height:0}
|
|
167
|
+
.flow-divider{border-top:1px solid var(--border);margin:14px 0 10px}
|
|
168
|
+
.flow-insights{padding:0;font-size:12px;line-height:1.8;color:var(--text-dim)}
|
|
169
|
+
.flow-insights .insight-line{padding:3px 0}
|
|
170
|
+
.flow-insights .insight-error{color:var(--red)}
|
|
171
|
+
.flow-insights .insight-warn{color:var(--amber)}
|
|
172
|
+
.flow-insights .insight-tip{margin-top:8px;color:var(--text-muted);font-size:11px;line-height:1.5}
|
|
173
|
+
|
|
174
|
+
/* Detailed mode sub-rows */
|
|
175
|
+
.flow-subreqs{padding:4px 0;display:flex;flex-direction:column;gap:6px}
|
|
176
|
+
.flow-subreq{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius);font-family:var(--mono);font-size:13px;cursor:pointer;transition:all .15s;background:var(--bg-card);box-shadow:var(--shadow-sm)}
|
|
177
|
+
.flow-subreq:hover{border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
178
|
+
.flow-subreq .subreq-method{font-weight:700;flex-shrink:0;font-size:12px}
|
|
179
|
+
.flow-subreq .subreq-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500}
|
|
180
|
+
.flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
|
|
181
|
+
.flow-subreq .subreq-status{flex-shrink:0}
|
|
182
|
+
.flow-subreq .subreq-dur{color:var(--text-muted);font-size:12px;text-align:right;flex-shrink:0}
|
|
183
|
+
.flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
|
|
184
|
+
.flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
|
|
185
|
+
.flow-subreq-detail.open{display:block}
|
|
186
|
+
|
|
187
|
+
/* Shared detail expand */
|
|
188
|
+
.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:14px}
|
|
189
|
+
.detail-section h4{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:8px;font-weight:600}
|
|
190
|
+
.detail-section pre{background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:14px;font-family:var(--mono);font-size:12px;overflow-x:auto;max-height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;line-height:1.6}
|
|
191
|
+
.detail-meta{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:14px;font-family:var(--mono);font-size:12px;color:var(--text-dim);padding:12px 16px;background:var(--bg-muted);border-radius:var(--radius);border:1px solid var(--border)}
|
|
192
|
+
.detail-meta span{display:flex;align-items:center;gap:6px}
|
|
193
|
+
.detail-actions{margin-top:14px;display:flex;gap:8px}
|
|
194
|
+
|
|
195
|
+
/* Server activity */
|
|
196
|
+
.server-activity{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}
|
|
197
|
+
.server-activity-header{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600;margin-bottom:10px}
|
|
198
|
+
.sa-section{margin-bottom:12px}
|
|
199
|
+
.sa-label{font-size:10px;font-weight:600;color:var(--text-dim);margin-bottom:4px}
|
|
200
|
+
.sa-row{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;padding:4px 0;color:var(--text)}
|
|
201
|
+
.sa-method{width:40px;font-weight:600;flex-shrink:0}
|
|
202
|
+
.sa-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim)}
|
|
203
|
+
.sa-status{width:36px;text-align:right;font-weight:600}
|
|
204
|
+
.sa-dur{width:60px;text-align:right;color:var(--text-muted)}
|
|
205
|
+
.sa-level{width:50px;font-weight:600;flex-shrink:0;font-size:10px}
|
|
206
|
+
.sa-msg{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:11px}
|
|
207
|
+
.sa-err-name{width:100px;color:var(--red);font-weight:600;flex-shrink:0}
|
|
208
|
+
|
|
209
|
+
/* Strict Mode duplicate banner */
|
|
210
|
+
.strict-mode-dupe{opacity:0.55}
|
|
211
|
+
.strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
|
|
212
|
+
|
|
213
|
+
/* Request rows */
|
|
214
|
+
.req-row{display:flex;align-items:center;gap:16px;padding:12px 28px;border-bottom:1px solid var(--border-subtle);cursor:pointer;transition:background .1s;font-family:var(--mono);font-size:14px}
|
|
215
|
+
.req-row:hover{background:var(--bg-hover)}
|
|
216
|
+
.req-row.expanded{background:var(--bg-muted)}
|
|
217
|
+
.req-summary{display:flex;align-items:center;gap:16px;font-family:var(--mono);font-size:14px}
|
|
218
|
+
.req-method{font-weight:700;width:60px;flex-shrink:0}
|
|
219
|
+
.method-GET{color:var(--green)}.method-POST{color:var(--blue)}.method-PUT,.method-PATCH{color:var(--amber)}.method-DELETE{color:var(--red)}
|
|
220
|
+
.req-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
|
|
221
|
+
.req-status{font-weight:600;width:36px;text-align:right}
|
|
222
|
+
.status-2xx{color:var(--green)}.status-3xx{color:var(--cyan)}.status-4xx{color:var(--amber)}.status-5xx{color:var(--red)}
|
|
223
|
+
.req-duration{color:var(--text-dim);width:70px;text-align:right}
|
|
224
|
+
.req-size{color:var(--text-muted);width:60px;text-align:right;font-size:13px}
|
|
225
|
+
.req-detail{padding:16px 28px 20px;border-bottom:1px solid var(--border);background:var(--bg-detail);display:none}
|
|
226
|
+
.req-detail.open{display:block}
|
|
227
|
+
|
|
228
|
+
/* JSON */
|
|
229
|
+
.json-key{color:var(--cyan)}.json-str{color:var(--green)}.json-num{color:var(--amber)}.json-bool{color:var(--accent)}.json-null{color:var(--red)}
|
|
230
|
+
|
|
231
|
+
/* Telemetry list views */
|
|
232
|
+
.tel-method{width:50px;font-weight:500;flex-shrink:0}
|
|
233
|
+
.tel-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
234
|
+
.tel-status{width:50px;text-align:right}
|
|
235
|
+
.tel-status-err{color:var(--red)}
|
|
236
|
+
.tel-duration{width:70px;text-align:right;color:var(--text-muted)}
|
|
237
|
+
.tel-timestamp{width:130px;text-align:right;color:var(--text-muted)}
|
|
238
|
+
.tel-level{width:52px;flex-shrink:0;font-size:10px;font-weight:600;text-align:center;padding:3px 0;border-radius:4px;letter-spacing:.5px}
|
|
239
|
+
.tel-level-error{color:var(--red);background:rgba(220,38,38,0.08)}
|
|
240
|
+
.tel-level-warn{color:var(--amber);background:rgba(217,119,6,0.08)}
|
|
241
|
+
.tel-level-info{color:var(--blue);background:rgba(37,99,235,0.08)}
|
|
242
|
+
.tel-level-debug{color:var(--text-muted);background:var(--bg-muted)}
|
|
243
|
+
.tel-level-log{color:var(--text-dim);background:var(--bg-muted)}
|
|
244
|
+
.tel-error-name{width:180px;color:var(--red);font-weight:500;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
245
|
+
.tel-message{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim)}
|
|
246
|
+
.tel-mono{font-family:var(--mono);font-size:12px}
|
|
247
|
+
.tel-clickable{cursor:pointer}
|
|
248
|
+
.error-stack{padding:8px 16px;font-size:10px;color:var(--text-dim);white-space:pre-wrap;font-family:var(--mono);background:var(--bg-muted);border-bottom:1px solid var(--border)}
|
|
249
|
+
|
|
250
|
+
/* Query rows */
|
|
251
|
+
.query-row{display:flex;align-items:center;gap:16px;font-family:var(--mono);font-size:12px}
|
|
252
|
+
.query-op{width:70px;flex-shrink:0;font-weight:600;border-right:1px solid var(--border-subtle);padding-right:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
253
|
+
.query-table{width:170px;flex-shrink:0;font-weight:500;color:var(--text);border-right:1px solid var(--border-subtle);padding-right:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
254
|
+
.query-preview{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:10px;border-right:1px solid var(--border-subtle);padding-right:16px}
|
|
255
|
+
.query-dur{width:60px;flex-shrink:0;text-align:right}
|
|
256
|
+
.query-slow{color:var(--red);font-weight:500}
|
|
257
|
+
.query-detail{display:none;padding:0 28px 12px;position:relative}
|
|
258
|
+
.query-detail.open{display:block}
|
|
259
|
+
.query-detail-sql{background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 14px;font-family:var(--mono);font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-word;color:var(--text-dim);max-height:200px;overflow-y:auto;margin:0}
|
|
260
|
+
.query-detail-copy{position:absolute;top:8px;right:36px;padding:2px 8px;font-size:9px;font-family:var(--mono);background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-muted);cursor:pointer;transition:all .15s}
|
|
261
|
+
.query-detail-copy:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-light)}
|
|
262
|
+
|
|
263
|
+
/* Fetch analysis */
|
|
264
|
+
.fetch-analysis,#log-analysis{padding:16px 28px 0}
|
|
265
|
+
.fetch-summary{display:flex;gap:24px;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:16px;box-shadow:var(--shadow-sm);flex-wrap:wrap}
|
|
266
|
+
.fetch-stat{display:flex;flex-direction:column;gap:2px}
|
|
267
|
+
.fetch-stat-value{font-size:17px;font-weight:700;font-family:var(--mono);color:var(--text)}
|
|
268
|
+
.fetch-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
|
|
269
|
+
.fetch-groups-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
|
|
270
|
+
.fetch-groups{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}
|
|
271
|
+
.fetch-group{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;box-shadow:var(--shadow-sm);transition:all .15s}
|
|
272
|
+
.fetch-group:hover{border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
273
|
+
.fetch-group-header{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:13px}
|
|
274
|
+
.fetch-group-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;color:var(--text)}
|
|
275
|
+
.fetch-group-count{font-size:12px;color:var(--text-muted);flex-shrink:0;background:var(--bg-muted);padding:2px 8px;border-radius:10px}
|
|
276
|
+
.fetch-group-meta{display:flex;gap:16px;margin-top:8px;font-size:11px;color:var(--text-dim);font-family:var(--mono)}
|
|
277
|
+
.fetch-group-meta span{display:flex;align-items:center;gap:4px}
|
|
278
|
+
.fetch-group-callers{margin-top:6px;font-size:11px;color:var(--text-muted)}
|
|
279
|
+
.fetch-group-callers strong{color:var(--text-dim);font-weight:500}
|
|
280
|
+
.fetch-group-err{color:var(--red)}
|
|
281
|
+
|
|
282
|
+
/* Performance tab */
|
|
283
|
+
.perf-selector{display:flex;gap:6px;flex-wrap:wrap;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
284
|
+
.perf-selector-btn{background:var(--bg-card);border:1px solid var(--border);color:var(--text-muted);padding:6px 12px;border-radius:var(--radius);font-size:12px;cursor:pointer;font-family:var(--mono);font-weight:500;transition:all .15s;display:flex;align-items:center;gap:8px;box-shadow:var(--shadow-sm)}
|
|
285
|
+
.perf-selector-btn:hover{background:var(--bg-hover);color:var(--text)}
|
|
286
|
+
.perf-selector-btn.active{background:var(--bg-active);color:var(--accent);border-color:var(--border-light)}
|
|
287
|
+
.perf-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
288
|
+
|
|
289
|
+
/* Health badges */
|
|
290
|
+
.perf-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:700;border:1px solid;letter-spacing:.3px;font-family:var(--sans);flex-shrink:0}
|
|
291
|
+
.perf-badge-lg{padding:4px 12px;font-size:13px;border-radius:var(--radius-sm)}
|
|
292
|
+
.perf-badge-sm{padding:1px 6px;font-size:9px}
|
|
293
|
+
|
|
294
|
+
/* Overview: endpoint list with inline scatter charts */
|
|
295
|
+
.perf-endpoint-list{padding:16px 28px;display:flex;flex-direction:column;gap:8px}
|
|
296
|
+
.perf-endpoint-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 20px 8px;cursor:pointer;transition:all .15s;box-shadow:var(--shadow-sm)}
|
|
297
|
+
.perf-endpoint-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
298
|
+
.perf-ep-header{display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap}
|
|
299
|
+
.perf-ep-name{flex:1;font-family:var(--mono);font-size:13px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:120px}
|
|
300
|
+
.perf-ep-stats{display:flex;align-items:center;gap:14px;flex-shrink:0}
|
|
301
|
+
.perf-ep-stat{font-size:11px;font-family:var(--mono);color:var(--text-muted)}
|
|
302
|
+
.perf-ep-stat-err{color:var(--red)}
|
|
303
|
+
.perf-ep-stat-warn{color:var(--amber)}
|
|
304
|
+
.perf-ep-stat-muted{color:var(--text-dim)}
|
|
305
|
+
.perf-inline-canvas{width:100%;height:88px;border-radius:var(--radius-sm);background:var(--bg-muted);border:1px solid var(--border);display:block}
|
|
306
|
+
|
|
307
|
+
/* Detail view */
|
|
308
|
+
.perf-detail-header{padding:20px 28px 16px;border-bottom:1px solid var(--border-subtle)}
|
|
309
|
+
.perf-detail-title{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:600;color:var(--text);font-family:var(--mono)}
|
|
310
|
+
.perf-metric-row{display:flex;gap:4px;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
311
|
+
.perf-metric-card{flex:1;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 16px;display:flex;flex-direction:column;gap:4px}
|
|
312
|
+
.perf-metric-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
|
|
313
|
+
.perf-metric-value{font-size:21px;font-weight:700;font-family:var(--mono)}
|
|
314
|
+
|
|
315
|
+
/* Time breakdown */
|
|
316
|
+
.perf-breakdown{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
317
|
+
.perf-breakdown-bar{display:flex;height:10px;border-radius:5px;overflow:hidden;background:var(--bg-muted);border:1px solid var(--border)}
|
|
318
|
+
.perf-breakdown-bar-sm{height:6px;border-radius:3px;flex:1}
|
|
319
|
+
.perf-breakdown-seg{min-width:2px;transition:width .3s}
|
|
320
|
+
.perf-breakdown-db{background:var(--breakdown-db)}
|
|
321
|
+
.perf-breakdown-fetch{background:var(--breakdown-fetch)}
|
|
322
|
+
.perf-breakdown-app{background:var(--breakdown-app)}
|
|
323
|
+
.perf-breakdown-legend{display:flex;gap:16px;margin-top:8px;font-size:11px;font-family:var(--mono);color:var(--text-muted)}
|
|
324
|
+
.perf-breakdown-item{display:flex;align-items:center;gap:5px}
|
|
325
|
+
.perf-breakdown-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
326
|
+
span.perf-breakdown-dot.perf-breakdown-db{background:var(--breakdown-db)}
|
|
327
|
+
span.perf-breakdown-dot.perf-breakdown-fetch{background:var(--breakdown-fetch)}
|
|
328
|
+
span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
329
|
+
.perf-breakdown-inline{margin:0 0 8px;display:flex;align-items:center;gap:10px}
|
|
330
|
+
.perf-breakdown-labels{display:flex;gap:8px;font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0}
|
|
331
|
+
.perf-breakdown-lbl{display:flex;align-items:center;gap:3px}
|
|
332
|
+
.perf-col-breakdown{flex:1;min-width:140px;display:flex;align-items:center;gap:4px;flex-wrap:wrap}
|
|
333
|
+
.perf-bd-tag{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:3px;font-size:10px;font-family:var(--mono);font-weight:500;white-space:nowrap}
|
|
334
|
+
.perf-bd-tag-db{color:#818cf8;background:rgba(99,102,241,0.1)}
|
|
335
|
+
.perf-bd-tag-fetch{color:#fbbf24;background:rgba(245,158,11,0.1)}
|
|
336
|
+
.perf-bd-tag-app{color:var(--breakdown-app);background:rgba(148,163,184,0.1)}
|
|
337
|
+
.perf-col-muted{color:var(--text-dim)}
|
|
338
|
+
|
|
339
|
+
/* Chart */
|
|
340
|
+
.perf-chart-wrap{padding:16px 28px}
|
|
341
|
+
.perf-canvas{border-radius:var(--radius);background:var(--bg-muted);border:1px solid var(--border)}
|
|
342
|
+
.perf-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
|
|
343
|
+
|
|
344
|
+
/* Request history table */
|
|
345
|
+
.perf-history-wrap{padding:0 28px 20px}
|
|
346
|
+
.perf-history-wrap .col-header{padding:8px 0;margin:0;position:static;background:var(--bg);gap:0}
|
|
347
|
+
.perf-hist-row{display:flex;align-items:center;padding:10px 0;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
|
|
348
|
+
.perf-hist-row:hover{background:var(--bg-hover);margin:0 -28px;padding-left:28px;padding-right:28px}
|
|
349
|
+
.perf-hist-row-err{background:rgba(220,38,38,0.04)}
|
|
350
|
+
.perf-hist-row-err:hover{background:rgba(220,38,38,0.08)}
|
|
351
|
+
.perf-hist-row-hl{background:rgba(37,99,235,0.1);margin:0 -28px;padding-left:28px;padding-right:28px;border-left:3px solid #4ade80}
|
|
352
|
+
.perf-hist-row-hl.perf-hist-row-err{background:rgba(220,38,38,0.12);border-left-color:#f87171}
|
|
353
|
+
.perf-col{flex-shrink:0;border-right:1px solid var(--border-subtle);padding-right:16px;margin-right:16px}
|
|
354
|
+
.perf-col:last-child{border-right:none;padding-right:0;margin-right:0}
|
|
355
|
+
.perf-col-date{width:100px;color:var(--text-dim)}
|
|
356
|
+
.perf-col-health{width:60px;display:flex;align-items:center}
|
|
357
|
+
.perf-col-avg{width:70px;color:var(--text)}
|
|
358
|
+
.perf-col-status{width:50px;text-align:center}
|
|
359
|
+
.perf-col-qpr{width:60px;text-align:right;color:var(--text-dim)}
|
|
360
|
+
|
|
361
|
+
/* Overview */
|
|
362
|
+
.ov-container{padding:24px 28px}
|
|
363
|
+
|
|
364
|
+
/* Summary banner */
|
|
365
|
+
.ov-summary{display:flex;gap:24px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:24px;flex-wrap:wrap;box-shadow:var(--shadow-sm)}
|
|
366
|
+
.ov-stat{display:flex;flex-direction:column;gap:2px}
|
|
367
|
+
.ov-stat-value{font-size:19px;font-weight:700;font-family:var(--mono);color:var(--text)}
|
|
368
|
+
.ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
|
|
369
|
+
|
|
370
|
+
/* Section header */
|
|
371
|
+
.ov-section-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
372
|
+
.ov-issue-count{font-size:11px;font-family:var(--mono);color:var(--text-dim);background:var(--bg-muted);border:1px solid var(--border);padding:1px 8px;border-radius:10px}
|
|
373
|
+
|
|
374
|
+
/* Insight cards */
|
|
375
|
+
.ov-cards{display:flex;flex-direction:column;gap:8px}
|
|
376
|
+
.ov-card{display:flex;align-items:flex-start;gap:14px;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all .15s;box-shadow:var(--shadow-sm)}
|
|
377
|
+
.ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
378
|
+
.ov-card-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;border-radius:50%;margin-top:2px}
|
|
379
|
+
.ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
|
|
380
|
+
.ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
|
|
381
|
+
.ov-card-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
|
|
382
|
+
.ov-card-icon.resolved{background:var(--green-bg);color:var(--green)}
|
|
383
|
+
.ov-card-body{flex:1;min-width:0}
|
|
384
|
+
.ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
|
|
385
|
+
.ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
|
|
386
|
+
.ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
|
|
387
|
+
.ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
|
|
388
|
+
|
|
389
|
+
/* Expanded card */
|
|
390
|
+
.ov-card.expanded{border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
391
|
+
.ov-card-expand{display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}
|
|
392
|
+
.ov-card-hint{font-size:12px;color:var(--text-dim);line-height:1.5;margin-bottom:10px}
|
|
393
|
+
.ov-card-link{font-size:12px;font-weight:600;color:var(--blue);cursor:pointer;display:inline-block;padding:4px 0}
|
|
394
|
+
.ov-card-link:hover{text-decoration:underline}
|
|
395
|
+
.ov-detail-label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
|
|
396
|
+
.ov-detail-item{font-size:12px;color:var(--text);font-family:var(--mono);padding:2px 0}
|
|
397
|
+
|
|
398
|
+
/* All-clear banner */
|
|
399
|
+
.ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--green-bg-subtle);border:1px solid var(--green-border);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
|
|
400
|
+
.ov-clear-icon{font-size:16px}
|
|
401
|
+
|
|
402
|
+
/* Resolved section */
|
|
403
|
+
.ov-resolved-title{margin-top:24px}
|
|
404
|
+
.ov-card-resolved{opacity:.7;border-color:var(--green-border);cursor:default}
|
|
405
|
+
.ov-card-resolved:hover{opacity:1;box-shadow:var(--shadow-sm)}
|
|
406
|
+
|
|
407
|
+
/* Security tab */
|
|
408
|
+
.sec-container{padding:24px 28px}
|
|
409
|
+
|
|
410
|
+
/* All-clear */
|
|
411
|
+
.sec-clear{display:flex;align-items:center;gap:16px;padding:20px 24px;background:var(--green-bg-subtle);border:1px solid var(--green-border-subtle);border-radius:var(--radius);margin-bottom:24px}
|
|
412
|
+
.sec-clear-icon{font-size:24px;color:var(--green);flex-shrink:0}
|
|
413
|
+
.sec-clear-title{font-size:15px;font-weight:600;color:var(--green);margin-bottom:2px}
|
|
414
|
+
.sec-clear-sub{font-size:12px;color:var(--text-dim)}
|
|
415
|
+
|
|
416
|
+
/* Summary bar */
|
|
417
|
+
.sec-summary{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:20px;box-shadow:var(--shadow-sm)}
|
|
418
|
+
.sec-summary-left{display:flex;align-items:baseline;gap:8px}
|
|
419
|
+
.sec-summary-count{font-size:23px;font-weight:700;font-family:var(--mono);color:var(--text)}
|
|
420
|
+
.sec-summary-label{font-size:12px;color:var(--text-dim)}
|
|
421
|
+
.sec-summary-right{display:flex;gap:8px}
|
|
422
|
+
.sec-badge{font-size:11px;font-weight:600;padding:3px 10px;border-radius:10px}
|
|
423
|
+
.sec-badge.critical{background:rgba(220,38,38,.08);color:var(--red)}
|
|
424
|
+
.sec-badge.warning{background:rgba(217,119,6,.08);color:var(--amber)}
|
|
425
|
+
.sec-badge.info{background:rgba(37,99,235,.08);color:var(--blue)}
|
|
426
|
+
|
|
427
|
+
/* Rule group */
|
|
428
|
+
.sec-group{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden;box-shadow:var(--shadow-sm)}
|
|
429
|
+
.sec-group-header{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border)}
|
|
430
|
+
.sec-group-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;border-radius:50%}
|
|
431
|
+
.sec-group-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
|
|
432
|
+
.sec-group-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
|
|
433
|
+
.sec-group-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
|
|
434
|
+
.sec-group-title{font-size:13px;font-weight:600;color:var(--text);flex:1}
|
|
435
|
+
.sec-group-count{font-size:11px;font-family:var(--mono);color:var(--text-dim);background:var(--bg-muted);padding:1px 8px;border-radius:10px;border:1px solid var(--border)}
|
|
436
|
+
|
|
437
|
+
/* Hint */
|
|
438
|
+
.sec-hint{padding:8px 16px;font-size:11px;color:var(--text-muted);background:var(--bg-muted);border-bottom:1px solid var(--border)}
|
|
439
|
+
|
|
440
|
+
/* Items */
|
|
441
|
+
.sec-items{padding:4px 0}
|
|
442
|
+
.sec-item{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;font-size:12px;transition:background .1s}
|
|
443
|
+
.sec-item:hover{background:var(--bg-hover)}
|
|
444
|
+
.sec-item-desc{color:var(--text-dim);line-height:1.5;flex:1;min-width:0}
|
|
445
|
+
.sec-item-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
|
|
446
|
+
.sec-item-count{font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0;margin-left:12px}
|
|
447
|
+
|
|
448
|
+
/* Resolved badge in summary */
|
|
449
|
+
.sec-resolved-badge{font-size:11px;font-weight:600;padding:3px 10px;border-radius:10px;background:var(--green-bg);color:var(--green);margin-left:12px}
|
|
450
|
+
|
|
451
|
+
/* Resolved section */
|
|
452
|
+
.sec-resolved-title{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--text-dim);margin:20px 0 8px 0}
|
|
453
|
+
.sec-resolved-check{color:var(--green);font-size:14px}
|
|
454
|
+
.sec-resolved-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 8px;border-radius:10px;border:1px solid var(--border)}
|
|
455
|
+
.sec-group-resolved{opacity:.7;border-color:var(--green-border)}
|
|
456
|
+
.sec-group-resolved:hover{opacity:1}
|
|
457
|
+
.sec-item-resolved{color:var(--text-muted)}
|
|
458
|
+
.sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
|
|
459
|
+
.sec-resolved-item-icon{color:var(--green);font-size:12px;flex-shrink:0;margin-right:8px}
|
|
460
|
+
|
|
461
|
+
/* AI status badges */
|
|
462
|
+
.sec-ai-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:8px;margin-left:8px;white-space:nowrap}
|
|
463
|
+
.sec-ai-fixing{background:rgba(217,119,6,.1);color:var(--amber)}
|
|
464
|
+
.sec-ai-wontfix{background:rgba(107,114,128,.1);color:var(--text-muted)}
|
|
465
|
+
.sec-ai-verified{background:rgba(22,163,74,.1);color:var(--green)}
|
|
466
|
+
.sec-ai-notes{font-size:11px;color:var(--text-muted);font-style:italic;margin-top:2px;padding-left:0}
|
|
467
|
+
|
|
468
|
+
/* Timeline */
|
|
469
|
+
.request-timeline{margin-top:8px;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;overflow:hidden}
|
|
470
|
+
.request-timeline.tl-hidden{display:none}
|
|
471
|
+
.tl-header{display:flex;align-items:center;justify-content:space-between;padding:0 0 8px}
|
|
472
|
+
.tl-title{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
|
|
473
|
+
.tl-counts{display:flex;gap:10px;font-family:var(--mono);font-size:10px}
|
|
474
|
+
.tl-count{color:var(--text-dim)}
|
|
475
|
+
.tl-count-query{color:var(--accent)}
|
|
476
|
+
.tl-count-fetch{color:var(--blue)}
|
|
477
|
+
.tl-count-error{color:var(--red)}
|
|
478
|
+
.tl-count-log{color:var(--text-muted)}
|
|
479
|
+
.tl-loading{color:var(--text-muted);padding:4px 0;font-size:10px;font-family:var(--mono)}
|
|
480
|
+
.tl-events{position:relative;padding-left:4px}
|
|
481
|
+
.tl-event{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;padding:4px 0 4px 12px;border-left:2px solid var(--border);position:relative;flex-wrap:wrap}
|
|
482
|
+
.tl-event-time{width:48px;color:var(--text-muted);font-size:10px;flex-shrink:0;text-align:right}
|
|
483
|
+
.tl-event-type{width:44px;font-weight:700;font-size:9px;letter-spacing:.5px;flex-shrink:0}
|
|
484
|
+
.tl-event-summary{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
|
|
485
|
+
.tl-event-status{width:32px;text-align:right;font-weight:600;flex-shrink:0}
|
|
486
|
+
.tl-event-dur{width:48px;text-align:right;color:var(--text-muted);flex-shrink:0;font-size:10px}
|
|
487
|
+
.tl-event.tl-clickable{cursor:pointer;border-radius:6px;margin-left:-4px;padding:6px 12px;border-left:none;border:1px solid var(--border);background:var(--bg-card)}
|
|
488
|
+
.tl-event.tl-clickable:hover{background:var(--bg-hover);border-color:var(--border-light)}
|
|
489
|
+
.tl-event-sql{width:100%;display:none;margin:4px 0 2px 0;padding:8px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);font-size:10px;line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--text-dim);overflow-x:auto;max-height:150px;overflow-y:auto;position:relative}
|
|
490
|
+
.tl-event-sql.open{display:block}
|
|
491
|
+
.tl-sql-copy{position:absolute;top:6px;right:6px;padding:2px 8px;font-size:9px;font-family:var(--mono);background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-muted);cursor:pointer;transition:all .15s}
|
|
492
|
+
.tl-sql-copy:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-light)}
|
|
493
|
+
</style>
|
|
494
|
+
</head>
|
|
495
|
+
<body>
|
|
496
|
+
|
|
497
|
+
<div class="app" id="app">
|
|
498
|
+
<aside class="sidebar">
|
|
499
|
+
<div class="sidebar-logo">
|
|
500
|
+
<span class="logo-text">brakit</span>
|
|
501
|
+
<span class="logo-version">v0.0.0</span>
|
|
502
|
+
</div>
|
|
503
|
+
<nav class="sidebar-nav">
|
|
504
|
+
<button class="sidebar-item active" data-view="overview">
|
|
505
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
|
|
506
|
+
<span class="item-label">Overview</span>
|
|
507
|
+
</button>
|
|
508
|
+
<div class="sidebar-section">Monitor</div>
|
|
509
|
+
<button class="sidebar-item" data-view="actions">
|
|
510
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span>
|
|
511
|
+
<span class="item-label">Actions</span>
|
|
512
|
+
<span class="item-count" id="sidebar-count-actions">0</span>
|
|
513
|
+
</button>
|
|
514
|
+
<button class="sidebar-item" data-view="requests">
|
|
515
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
|
|
516
|
+
<span class="item-label">Requests</span>
|
|
517
|
+
<span class="item-count" id="sidebar-count-requests">0</span>
|
|
518
|
+
</button>
|
|
519
|
+
<button class="sidebar-item" data-view="fetches">
|
|
520
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span>
|
|
521
|
+
<span class="item-label">Fetches</span>
|
|
522
|
+
<span class="item-count" id="sidebar-count-fetches">0</span>
|
|
523
|
+
</button>
|
|
524
|
+
<div class="sidebar-section">Insights</div>
|
|
525
|
+
<button class="sidebar-item" data-view="queries">
|
|
526
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg></span>
|
|
527
|
+
<span class="item-label">Queries</span>
|
|
528
|
+
<span class="item-count" id="sidebar-count-queries">0</span>
|
|
529
|
+
</button>
|
|
530
|
+
<button class="sidebar-item" data-view="errors">
|
|
531
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg></span>
|
|
532
|
+
<span class="item-label">Errors</span>
|
|
533
|
+
<span class="item-count" id="sidebar-count-errors">0</span>
|
|
534
|
+
</button>
|
|
535
|
+
<button class="sidebar-item" data-view="logs">
|
|
536
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>
|
|
537
|
+
<span class="item-label">Logs</span>
|
|
538
|
+
<span class="item-count" id="sidebar-count-logs">0</span>
|
|
539
|
+
</button>
|
|
540
|
+
<button class="sidebar-item" data-view="security">
|
|
541
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span>
|
|
542
|
+
<span class="item-label">Security</span>
|
|
543
|
+
<span class="item-count" id="sidebar-count-security" style="display:none">0</span>
|
|
544
|
+
</button>
|
|
545
|
+
<button class="sidebar-item" data-view="performance">
|
|
546
|
+
<span class="item-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></span>
|
|
547
|
+
<span class="item-label">Performance</span>
|
|
548
|
+
</button>
|
|
549
|
+
</nav>
|
|
550
|
+
<div class="sidebar-footer">:{{PORT}}</div>
|
|
551
|
+
</aside>
|
|
552
|
+
<div class="main-panel">
|
|
553
|
+
<div class="header">
|
|
554
|
+
<div class="header-left">
|
|
555
|
+
<span class="header-title" id="header-title">Overview</span>
|
|
556
|
+
<span class="header-sub" id="header-sub">Live summary of your application</span>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="header-right">
|
|
559
|
+
<div class="segmented-control" id="mode-toggle" style="display:none">
|
|
560
|
+
<button class="segmented-btn active" id="mode-simple">Quick</button>
|
|
561
|
+
<button class="segmented-btn" id="mode-detailed">Detailed</button>
|
|
562
|
+
</div>
|
|
563
|
+
<button class="btn btn-danger" id="clear-btn">Clear</button>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
<div class="main-content">
|
|
567
|
+
<div id="overview-container">
|
|
568
|
+
<div class="ov-container" id="overview-content"></div>
|
|
569
|
+
</div>
|
|
570
|
+
<div class="view-flows" id="flow-container" style="display:none">
|
|
571
|
+
<div class="col-header" id="flow-col-header">
|
|
572
|
+
<span style="width:8px"></span>
|
|
573
|
+
<span style="flex:1">Action</span>
|
|
574
|
+
<span style="width:60px;text-align:right">Reqs</span>
|
|
575
|
+
<span style="width:120px;text-align:right">Status</span>
|
|
576
|
+
<span style="width:70px;text-align:right">Time</span>
|
|
577
|
+
</div>
|
|
578
|
+
<div id="flow-list">
|
|
579
|
+
<div class="empty" id="empty-flows">
|
|
580
|
+
<span class="empty-title">Waiting for requests...</span>
|
|
581
|
+
<span class="empty-sub">Use your app and actions will appear here</span>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="view-requests" id="request-container">
|
|
586
|
+
<div class="col-header">
|
|
587
|
+
<span style="width:60px">Method</span>
|
|
588
|
+
<span style="flex:1">URL</span>
|
|
589
|
+
<span style="width:36px;text-align:right">Status</span>
|
|
590
|
+
<span style="width:70px;text-align:right">Time</span>
|
|
591
|
+
<span style="width:60px;text-align:right">Size</span>
|
|
592
|
+
</div>
|
|
593
|
+
<div id="request-list"></div>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="view-telemetry" id="fetch-container" style="display:none">
|
|
596
|
+
<div class="fetch-analysis" id="fetch-analysis"></div>
|
|
597
|
+
</div>
|
|
598
|
+
<div class="view-telemetry" id="query-container" style="display:none">
|
|
599
|
+
<div class="col-header">
|
|
600
|
+
<span style="width:70px;border-right:1px solid var(--border);padding-right:16px">Operation</span>
|
|
601
|
+
<span style="width:170px;border-right:1px solid var(--border);padding-right:16px">Table</span>
|
|
602
|
+
<span style="flex:1;border-right:1px solid var(--border);padding-right:16px">Query</span>
|
|
603
|
+
<span style="width:60px;text-align:right">Time</span>
|
|
604
|
+
</div>
|
|
605
|
+
<div id="query-list"></div>
|
|
606
|
+
</div>
|
|
607
|
+
<div class="view-telemetry" id="error-container" style="display:none">
|
|
608
|
+
<div class="col-header">
|
|
609
|
+
<span style="width:180px">Type</span>
|
|
610
|
+
<span style="flex:1">Message</span>
|
|
611
|
+
<span style="width:130px;text-align:right">Time</span>
|
|
612
|
+
</div>
|
|
613
|
+
<div id="error-list"></div>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="view-telemetry" id="log-container" style="display:none">
|
|
616
|
+
<div id="log-analysis"></div>
|
|
617
|
+
<div class="col-header">
|
|
618
|
+
<span style="width:52px">Level</span>
|
|
619
|
+
<span style="flex:1">Message</span>
|
|
620
|
+
<span style="width:130px;text-align:right">Time</span>
|
|
621
|
+
</div>
|
|
622
|
+
<div id="log-list"></div>
|
|
623
|
+
</div>
|
|
624
|
+
<div class="view-telemetry" id="security-container" style="display:none">
|
|
625
|
+
<div class="sec-container" id="security-content"></div>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="view-telemetry" id="performance-container" style="display:none">
|
|
628
|
+
<div id="graph-content"></div>
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="footer">
|
|
632
|
+
<span id="stat-total">0 requests</span>
|
|
633
|
+
<span id="stat-flows">0 actions</span>
|
|
634
|
+
<span id="stat-errors" class="error-count">0 errors</span>
|
|
635
|
+
<span id="stat-avg">Avg: 0ms</span>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="toast" id="toast"></div>
|
|
640
|
+
|
|
641
|
+
<script>
|
|
642
|
+
(function(){
|
|
643
|
+
var PORT = {{PORT}};
|
|
644
|
+
var state = { flows: [], requests: [], fetches: [], errors: [], logs: [], queries: [], insights: [], findings: [], viewMode: 'simple', activeView: 'overview' };
|
|
645
|
+
|
|
646
|
+
var appEl = document.getElementById('app');
|
|
647
|
+
var flowListEl = document.getElementById('flow-list');
|
|
648
|
+
var reqListEl = document.getElementById('request-list');
|
|
649
|
+
var emptyFlows = document.getElementById('empty-flows');
|
|
650
|
+
var toastEl = document.getElementById('toast');
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
function formatDuration(ms) {
|
|
654
|
+
if (ms < 1000) return ms + 'ms';
|
|
655
|
+
return (ms / 1000).toFixed(1) + 's';
|
|
656
|
+
}
|
|
657
|
+
function formatSize(bytes) {
|
|
658
|
+
if (!bytes || bytes === 0) return '';
|
|
659
|
+
if (bytes < 1024) return bytes + 'b';
|
|
660
|
+
return (bytes / 1024).toFixed(1) + 'kb';
|
|
661
|
+
}
|
|
662
|
+
function escHtml(s) {
|
|
663
|
+
if (!s) return '';
|
|
664
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function statusPillClass(code) {
|
|
668
|
+
if (code >= 500) return 'status-pill-5xx';
|
|
669
|
+
if (code >= 400) return 'status-pill-4xx';
|
|
670
|
+
if (code >= 300) return 'status-pill-3xx';
|
|
671
|
+
return 'status-pill-2xx';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function statusIcon(code) {
|
|
675
|
+
if (code >= 500) return { icon: '\u2717', cls: 'status-error', tip: code + ' Server Error' };
|
|
676
|
+
if (code >= 400) return { icon: '\u2717', cls: 'status-fail', tip: code + ' ' + httpStatus(code) };
|
|
677
|
+
if (code >= 300) return { icon: '\u2713', cls: 'status-ok', tip: code + ' Redirect' };
|
|
678
|
+
return { icon: '\u2713', cls: 'status-ok', tip: code + ' OK' };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function httpStatus(code) {
|
|
682
|
+
var map = {400:'Bad Request',401:'Unauthorized',403:'Forbidden',404:'Not Found',405:'Method Not Allowed',408:'Timeout',409:'Conflict',422:'Unprocessable',429:'Too Many Requests',500:'Internal Server Error',502:'Bad Gateway',503:'Service Unavailable',504:'Gateway Timeout'};
|
|
683
|
+
return map[code] || (code >= 500 ? 'Server Error' : code >= 400 ? 'Client Error' : 'OK');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
var SENSITIVE = new Set(['cookie','set-cookie','authorization','proxy-authorization','x-api-key','x-auth-token']);
|
|
687
|
+
function maskValue(k, v) {
|
|
688
|
+
if (SENSITIVE.has(k.toLowerCase())) {
|
|
689
|
+
var s = String(v);
|
|
690
|
+
if (s.length <= 8) return '****';
|
|
691
|
+
return s.slice(0, 4) + '...' + s.slice(-4) + ' (' + s.length + ' chars)';
|
|
692
|
+
}
|
|
693
|
+
return String(v);
|
|
694
|
+
}
|
|
695
|
+
function formatHeaders(headers) {
|
|
696
|
+
if (!headers || Object.keys(headers).length === 0) return '<span style="color:var(--text-muted)">No headers</span>';
|
|
697
|
+
return Object.entries(headers).map(function(e) { return '<span class="json-key">' + escHtml(e[0]) + '</span>: ' + escHtml(maskValue(e[0], e[1])); }).join('\n');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function buildBodyToggle(direction, label, body) {
|
|
701
|
+
var block = document.createElement('div');
|
|
702
|
+
block.className = 'traffic-body';
|
|
703
|
+
var toggle = document.createElement('button');
|
|
704
|
+
toggle.className = 'traffic-body-toggle';
|
|
705
|
+
toggle.innerHTML = '<span class="chevron">\u25B8</span><span class="arrow-' + direction + '">' + (direction === 'out' ? '\u2192' : '\u2190') + '</span> ' + label;
|
|
706
|
+
var pre = document.createElement('pre');
|
|
707
|
+
pre.innerHTML = formatJsonBody(body);
|
|
708
|
+
toggle.addEventListener('click', function(e) {
|
|
709
|
+
e.stopPropagation();
|
|
710
|
+
toggle.classList.toggle('open');
|
|
711
|
+
pre.classList.toggle('open');
|
|
712
|
+
});
|
|
713
|
+
block.appendChild(toggle);
|
|
714
|
+
block.appendChild(pre);
|
|
715
|
+
return block;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function formatJsonBody(body) {
|
|
719
|
+
if (!body) return '<span style="color:var(--text-muted)">No body</span>';
|
|
720
|
+
try {
|
|
721
|
+
var parsed = JSON.parse(body);
|
|
722
|
+
return highlightJson(JSON.stringify(parsed, null, 2));
|
|
723
|
+
} catch(e) { return escHtml(body); }
|
|
724
|
+
}
|
|
725
|
+
function highlightJson(json) {
|
|
726
|
+
return escHtml(json).replace(
|
|
727
|
+
/("(?:[^"\\\\]|\\\\.)*")(\\s*:)?|\\b(true|false)\\b|\\bnull\\b|(-?\\d+\\.?\\d*(?:[eE][+-]?\\d+)?)/g,
|
|
728
|
+
function(m, str, colon, bool, num) {
|
|
729
|
+
if (str) return colon ? '<span class="json-key">' + str + '</span>' + colon : '<span class="json-str">' + str + '</span>';
|
|
730
|
+
if (bool) return '<span class="json-bool">' + m + '</span>';
|
|
731
|
+
if (num) return '<span class="json-num">' + m + '</span>';
|
|
732
|
+
if (m === 'null') return '<span class="json-null">null</span>';
|
|
733
|
+
return m;
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function showToast(msg) {
|
|
739
|
+
toastEl.textContent = msg;
|
|
740
|
+
toastEl.classList.add('show');
|
|
741
|
+
setTimeout(function() { toastEl.classList.remove('show'); }, 2000);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function collapseAll(rowSelector, detailSelector) {
|
|
745
|
+
document.querySelectorAll(rowSelector + '.expanded').forEach(function(r) { r.classList.remove('expanded'); });
|
|
746
|
+
document.querySelectorAll(detailSelector + '.open').forEach(function(d) { d.classList.remove('open'); });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
function createTelemetryView(listId, buildRowFn) {
|
|
751
|
+
return {
|
|
752
|
+
render: function(items) {
|
|
753
|
+
var list = document.getElementById(listId);
|
|
754
|
+
if (!list) return;
|
|
755
|
+
list.innerHTML = '';
|
|
756
|
+
items.forEach(function(item) { list.appendChild(buildRowFn(item)); });
|
|
757
|
+
},
|
|
758
|
+
prepend: function(item) {
|
|
759
|
+
var list = document.getElementById(listId);
|
|
760
|
+
if (!list) return;
|
|
761
|
+
list.insertBefore(buildRowFn(item), list.firstChild);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
var QUERY_OP_COLORS = { SELECT: 'var(--blue)', INSERT: 'var(--green)', UPDATE: 'var(--amber)', DELETE: 'var(--red)', COUNT: 'var(--text-muted)' };
|
|
768
|
+
|
|
769
|
+
function truncateSQL(sql, max) {
|
|
770
|
+
if (!sql) return '';
|
|
771
|
+
var clean = sql.replace(/"public"\./g, '').replace(/"/g, '');
|
|
772
|
+
if (clean.length <= max) return clean;
|
|
773
|
+
return clean.substring(0, max) + '...';
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function queryDuration(ms) {
|
|
777
|
+
if (ms === 0) return '<1ms';
|
|
778
|
+
return formatDuration(ms);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
var flowColHeader = document.getElementById('flow-col-header');
|
|
783
|
+
function renderFlows() {
|
|
784
|
+
flowListEl.innerHTML = '';
|
|
785
|
+
if (state.flows.length === 0) {
|
|
786
|
+
flowListEl.appendChild(emptyFlows);
|
|
787
|
+
emptyFlows.style.display = 'flex';
|
|
788
|
+
if (flowColHeader) flowColHeader.style.display = 'none';
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
emptyFlows.style.display = 'none';
|
|
792
|
+
if (flowColHeader) flowColHeader.style.display = 'flex';
|
|
793
|
+
for (var i = 0; i < state.flows.length; i++) {
|
|
794
|
+
var result = createFlowRow(state.flows[i]);
|
|
795
|
+
flowListEl.appendChild(result.row);
|
|
796
|
+
flowListEl.appendChild(result.expand);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function flowDotClass(flow) {
|
|
801
|
+
if (flow.hasErrors) return 'dot-error';
|
|
802
|
+
if (flow.redundancyPct > 0) return 'dot-warn';
|
|
803
|
+
return 'dot-clean';
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function flowBadgeInfo(flow) {
|
|
807
|
+
if (flow.hasErrors) {
|
|
808
|
+
var errCount = flow.requests.filter(function(r){ return r.statusCode >= 400; }).length;
|
|
809
|
+
return { text: errCount + ' error' + (errCount !== 1 ? 's' : ''), cls: 'badge-error' };
|
|
810
|
+
}
|
|
811
|
+
if (flow.redundancyPct > 0) {
|
|
812
|
+
return { text: flow.redundancyPct + '% redundant', cls: 'badge-warn' };
|
|
813
|
+
}
|
|
814
|
+
return { text: 'clean', cls: 'badge-clean' };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function createFlowRow(flow) {
|
|
818
|
+
var row = document.createElement('div');
|
|
819
|
+
row.className = 'flow-row';
|
|
820
|
+
var summary = document.createElement('div');
|
|
821
|
+
summary.className = 'flow-summary-row';
|
|
822
|
+
var dot = document.createElement('span');
|
|
823
|
+
dot.className = 'flow-status-dot ' + flowDotClass(flow);
|
|
824
|
+
var label = document.createElement('span');
|
|
825
|
+
label.className = 'flow-label';
|
|
826
|
+
label.textContent = flow.label;
|
|
827
|
+
var count = document.createElement('span');
|
|
828
|
+
count.className = 'flow-req-count';
|
|
829
|
+
count.textContent = flow.requests.length + ' req' + (flow.requests.length !== 1 ? 's' : '');
|
|
830
|
+
var badgeInfo = flowBadgeInfo(flow);
|
|
831
|
+
var badge = document.createElement('span');
|
|
832
|
+
badge.className = 'flow-badge-pill ' + badgeInfo.cls;
|
|
833
|
+
badge.textContent = badgeInfo.text;
|
|
834
|
+
var dur = document.createElement('span');
|
|
835
|
+
dur.className = 'flow-duration';
|
|
836
|
+
dur.textContent = formatDuration(flow.totalDurationMs);
|
|
837
|
+
summary.appendChild(dot);
|
|
838
|
+
summary.appendChild(label);
|
|
839
|
+
summary.appendChild(count);
|
|
840
|
+
summary.appendChild(badge);
|
|
841
|
+
summary.appendChild(dur);
|
|
842
|
+
row.appendChild(summary);
|
|
843
|
+
|
|
844
|
+
var expand = document.createElement('div');
|
|
845
|
+
expand.className = 'flow-expand';
|
|
846
|
+
|
|
847
|
+
row.addEventListener('click', function() {
|
|
848
|
+
var wasOpen = row.classList.contains('expanded');
|
|
849
|
+
collapseAll('.flow-row', '.flow-expand');
|
|
850
|
+
if (!wasOpen) {
|
|
851
|
+
row.classList.add('expanded');
|
|
852
|
+
expand.classList.add('open');
|
|
853
|
+
expand.innerHTML = '';
|
|
854
|
+
if (state.viewMode === 'simple') {
|
|
855
|
+
expand.appendChild(createFlowInsights(flow));
|
|
856
|
+
} else {
|
|
857
|
+
expand.appendChild(createFlowSubReqs(flow));
|
|
858
|
+
}
|
|
859
|
+
var tlEls = expand.querySelectorAll('.request-timeline');
|
|
860
|
+
for (var ti = 0; ti < tlEls.length; ti++) {
|
|
861
|
+
var tlItem = tlEls[ti];
|
|
862
|
+
var rid = tlItem.getAttribute('data-request-id');
|
|
863
|
+
if (rid) loadTimeline(rid, tlItem, 0);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
return { row: row, expand: expand };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
var skipCats = { 'auth-handshake': 1, 'auth-check': 1, 'middleware': 1 };
|
|
873
|
+
|
|
874
|
+
function createFlowInsights(flow) {
|
|
875
|
+
var container = document.createElement('div');
|
|
876
|
+
var traffic = document.createElement('div');
|
|
877
|
+
traffic.className = 'flow-traffic';
|
|
878
|
+
|
|
879
|
+
for (var i = 0; i < flow.requests.length; i++) {
|
|
880
|
+
var req = flow.requests[i];
|
|
881
|
+
if (skipCats[req.category]) continue;
|
|
882
|
+
var sClass = statusPillClass(req.statusCode);
|
|
883
|
+
|
|
884
|
+
var card = document.createElement('div');
|
|
885
|
+
card.className = 'traffic-card';
|
|
886
|
+
|
|
887
|
+
var header = document.createElement('div');
|
|
888
|
+
header.className = 'traffic-card-header';
|
|
889
|
+
|
|
890
|
+
var mEl = document.createElement('span');
|
|
891
|
+
mEl.className = 'method-badge method-badge-' + escHtml(req.method);
|
|
892
|
+
mEl.textContent = req.method;
|
|
893
|
+
|
|
894
|
+
var pEl = document.createElement('span');
|
|
895
|
+
pEl.className = 'traffic-card-path' + (req.isDuplicate ? ' is-dup' : '');
|
|
896
|
+
pEl.textContent = req.label;
|
|
897
|
+
|
|
898
|
+
var stEl = document.createElement('span');
|
|
899
|
+
stEl.className = 'status-pill ' + sClass;
|
|
900
|
+
stEl.textContent = String(req.statusCode);
|
|
901
|
+
|
|
902
|
+
var dEl = document.createElement('span');
|
|
903
|
+
dEl.className = 'traffic-card-dur';
|
|
904
|
+
dEl.textContent = formatDuration(req.pollingDurationMs || req.durationMs);
|
|
905
|
+
|
|
906
|
+
header.appendChild(mEl);
|
|
907
|
+
header.appendChild(pEl);
|
|
908
|
+
header.appendChild(stEl);
|
|
909
|
+
header.appendChild(dEl);
|
|
910
|
+
|
|
911
|
+
if (req.isDuplicate) {
|
|
912
|
+
var dupEl = document.createElement('span');
|
|
913
|
+
dupEl.className = 'traffic-card-dup';
|
|
914
|
+
dupEl.textContent = 'duplicate';
|
|
915
|
+
header.appendChild(dupEl);
|
|
916
|
+
} else {
|
|
917
|
+
var szEl = document.createElement('span');
|
|
918
|
+
szEl.className = 'traffic-card-size';
|
|
919
|
+
szEl.textContent = formatSize(req.responseSize);
|
|
920
|
+
header.appendChild(szEl);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
card.appendChild(header);
|
|
924
|
+
|
|
925
|
+
if (req.isStrictModeDupe) {
|
|
926
|
+
card.classList.add('strict-mode-dupe');
|
|
927
|
+
var smBanner = document.createElement('div');
|
|
928
|
+
smBanner.className = 'strict-mode-banner';
|
|
929
|
+
smBanner.textContent = 'React Strict Mode duplicate \u2014 does not happen in production';
|
|
930
|
+
card.appendChild(smBanner);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
var hasDetails = false;
|
|
934
|
+
if (!req.isDuplicate && req.category !== 'static' && req.category !== 'polling') {
|
|
935
|
+
var tlEl = document.createElement('div');
|
|
936
|
+
tlEl.className = 'request-timeline tl-hidden';
|
|
937
|
+
tlEl.setAttribute('data-request-id', req.id);
|
|
938
|
+
tlEl.setAttribute('data-request-started', String(req.startedAt));
|
|
939
|
+
card.appendChild(tlEl);
|
|
940
|
+
hasDetails = true;
|
|
941
|
+
}
|
|
942
|
+
if (req.requestBody && req.method !== 'GET') {
|
|
943
|
+
card.appendChild(buildBodyToggle('out', 'Request Body', req.requestBody));
|
|
944
|
+
hasDetails = true;
|
|
945
|
+
}
|
|
946
|
+
if (req.responseBody) {
|
|
947
|
+
card.appendChild(buildBodyToggle('in', 'Response Body', req.responseBody));
|
|
948
|
+
hasDetails = true;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (hasDetails) header.classList.add('has-details');
|
|
952
|
+
|
|
953
|
+
traffic.appendChild(card);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
container.appendChild(traffic);
|
|
957
|
+
|
|
958
|
+
var insights = analyzeFlow(flow);
|
|
959
|
+
var hasIssues = insights.errors.length > 0 || insights.duplicates.length > 0 || insights.warnings.length > 0 || !!insights.tip;
|
|
960
|
+
if (hasIssues) {
|
|
961
|
+
var divider = document.createElement('div');
|
|
962
|
+
divider.className = 'flow-divider';
|
|
963
|
+
container.appendChild(divider);
|
|
964
|
+
var insightsEl = document.createElement('div');
|
|
965
|
+
insightsEl.className = 'flow-insights';
|
|
966
|
+
for (var ei = 0; ei < insights.errors.length; ei++) {
|
|
967
|
+
var errLine = document.createElement('div');
|
|
968
|
+
errLine.className = 'insight-line insight-error';
|
|
969
|
+
errLine.textContent = '\u2717 ' + insights.errors[ei];
|
|
970
|
+
insightsEl.appendChild(errLine);
|
|
971
|
+
}
|
|
972
|
+
for (var di = 0; di < insights.duplicates.length; di++) {
|
|
973
|
+
var dup = insights.duplicates[di];
|
|
974
|
+
var dupLine = document.createElement('div');
|
|
975
|
+
dupLine.className = 'insight-line insight-warn';
|
|
976
|
+
dupLine.textContent = '\u26A0 ' + dup.name + ' \u2014 loaded ' + dup.count + 'x (wasting ~' + formatDuration(dup.wastedMs) + ')';
|
|
977
|
+
insightsEl.appendChild(dupLine);
|
|
978
|
+
}
|
|
979
|
+
for (var wi = 0; wi < insights.warnings.length; wi++) {
|
|
980
|
+
var warnLine = document.createElement('div');
|
|
981
|
+
warnLine.className = 'insight-line insight-warn';
|
|
982
|
+
warnLine.textContent = '\u26A0 ' + insights.warnings[wi];
|
|
983
|
+
insightsEl.appendChild(warnLine);
|
|
984
|
+
}
|
|
985
|
+
if (insights.tip) {
|
|
986
|
+
var tipLine = document.createElement('div');
|
|
987
|
+
tipLine.className = 'insight-line insight-tip';
|
|
988
|
+
tipLine.textContent = 'Tip: ' + insights.tip;
|
|
989
|
+
insightsEl.appendChild(tipLine);
|
|
990
|
+
}
|
|
991
|
+
container.appendChild(insightsEl);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return container;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function analyzeFlow(flow) {
|
|
998
|
+
var reqs = flow.requests;
|
|
999
|
+
var successes = [];
|
|
1000
|
+
var errors = [];
|
|
1001
|
+
var warnings = [];
|
|
1002
|
+
var duplicates = [];
|
|
1003
|
+
var seen = new Map();
|
|
1004
|
+
var totalMs = 0;
|
|
1005
|
+
for (var i = 0; i < reqs.length; i++) {
|
|
1006
|
+
var req = reqs[i];
|
|
1007
|
+
var label = req.label;
|
|
1008
|
+
var dur = req.pollingDurationMs || req.durationMs;
|
|
1009
|
+
totalMs += dur;
|
|
1010
|
+
|
|
1011
|
+
if (skipCats[req.category]) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (req.isDuplicate) {
|
|
1016
|
+
var ex = seen.get(label);
|
|
1017
|
+
if (ex) { ex.count++; ex.wastedMs += dur; }
|
|
1018
|
+
else seen.set(label, { name: label, count: 2, wastedMs: dur });
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (req.statusCode >= 400) {
|
|
1022
|
+
errors.push(label + ' (' + httpStatus(req.statusCode) + ')');
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (req.responseSize > 51200) {
|
|
1027
|
+
warnings.push('Large response: ' + label + ' returned ' + formatSize(req.responseSize));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
successes.push(label);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
for (var d of seen.values()) duplicates.push(d);
|
|
1034
|
+
var tip = '';
|
|
1035
|
+
if (duplicates.length > 0) {
|
|
1036
|
+
var names = duplicates.map(function(d) { return d.name; }).join(', ');
|
|
1037
|
+
var totalWaste = duplicates.reduce(function(s, d) { return s + d.wastedMs; }, 0);
|
|
1038
|
+
tip = 'Your app fetches ' + names + ' multiple times on this page. This wastes ~' + formatDuration(totalWaste) + '. Try caching these calls, deduplicating with React Query/SWR, or moving them to a shared layout.';
|
|
1039
|
+
} else if (errors.length > 0) {
|
|
1040
|
+
tip = 'Some requests are failing. Check your API routes and make sure the endpoints exist.';
|
|
1041
|
+
}
|
|
1042
|
+
var slow = reqs.filter(function(r) { return r.durationMs > 2000 && r.category !== 'polling'; });
|
|
1043
|
+
if (slow.length > 0 && !tip) {
|
|
1044
|
+
tip = slow.map(function(r) { return r.label; }).join(', ') + ' is taking over 2 seconds. Consider adding caching or optimizing the backend query.';
|
|
1045
|
+
}
|
|
1046
|
+
return { successes: successes, errors: errors, warnings: warnings, duplicates: duplicates, tip: tip };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
function createFlowSubReqs(flow) {
|
|
1051
|
+
var container = document.createElement('div');
|
|
1052
|
+
container.className = 'flow-subreqs';
|
|
1053
|
+
flow.requests.forEach(function(req) {
|
|
1054
|
+
var isDup = req.isDuplicate;
|
|
1055
|
+
var sClass = statusPillClass(req.statusCode);
|
|
1056
|
+
var subRow = document.createElement('div');
|
|
1057
|
+
subRow.className = 'flow-subreq';
|
|
1058
|
+
var safeMethod = escHtml(req.method);
|
|
1059
|
+
var methodEl = document.createElement('span');
|
|
1060
|
+
methodEl.className = 'method-badge method-badge-' + safeMethod;
|
|
1061
|
+
methodEl.textContent = req.method;
|
|
1062
|
+
var labelEl = document.createElement('span');
|
|
1063
|
+
labelEl.className = 'subreq-label' + (isDup ? ' is-dup' : '');
|
|
1064
|
+
labelEl.textContent = req.path || req.url;
|
|
1065
|
+
var statusEl = document.createElement('span');
|
|
1066
|
+
statusEl.className = 'status-pill ' + sClass;
|
|
1067
|
+
statusEl.textContent = String(req.statusCode);
|
|
1068
|
+
var durEl = document.createElement('span');
|
|
1069
|
+
durEl.className = 'subreq-dur';
|
|
1070
|
+
durEl.textContent = req.pollingDurationMs ? formatDuration(req.pollingDurationMs) : formatDuration(req.durationMs);
|
|
1071
|
+
subRow.appendChild(methodEl);
|
|
1072
|
+
subRow.appendChild(labelEl);
|
|
1073
|
+
if (isDup) {
|
|
1074
|
+
var dupTag = document.createElement('span');
|
|
1075
|
+
dupTag.className = 'subreq-dup-tag';
|
|
1076
|
+
dupTag.textContent = 'duplicate';
|
|
1077
|
+
subRow.appendChild(dupTag);
|
|
1078
|
+
}
|
|
1079
|
+
subRow.appendChild(statusEl);
|
|
1080
|
+
subRow.appendChild(durEl);
|
|
1081
|
+
|
|
1082
|
+
var detail = document.createElement('div');
|
|
1083
|
+
detail.className = 'flow-subreq-detail';
|
|
1084
|
+
subRow.addEventListener('click', function(e) {
|
|
1085
|
+
e.stopPropagation();
|
|
1086
|
+
var wasOpen = detail.classList.contains('open');
|
|
1087
|
+
container.querySelectorAll('.flow-subreq-detail.open').forEach(function(d){ d.classList.remove('open'); });
|
|
1088
|
+
container.querySelectorAll('.flow-subreq.expanded').forEach(function(r){ r.classList.remove('expanded'); });
|
|
1089
|
+
if (!wasOpen) {
|
|
1090
|
+
subRow.classList.add('expanded');
|
|
1091
|
+
detail.classList.add('open');
|
|
1092
|
+
detail.innerHTML = renderDetail(req);
|
|
1093
|
+
var curlBtn = detail.querySelector('.btn-curl');
|
|
1094
|
+
if (curlBtn) curlBtn.addEventListener('click', function(ev) { ev.stopPropagation(); copyAsCurl(req); });
|
|
1095
|
+
var tlEl = detail.querySelector('.request-timeline');
|
|
1096
|
+
if (tlEl) loadTimeline(tlEl.getAttribute('data-request-id'), tlEl, 0);
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
container.appendChild(subRow);
|
|
1100
|
+
container.appendChild(detail);
|
|
1101
|
+
});
|
|
1102
|
+
return container;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function renderDetail(req) {
|
|
1106
|
+
var sClass = statusPillClass(req.statusCode);
|
|
1107
|
+
var sm = escHtml(req.method);
|
|
1108
|
+
var h = '<div class="detail-meta">';
|
|
1109
|
+
h += '<span><span class="method-badge method-badge-' + sm + '">' + sm + '</span> ' + escHtml(req.url) + '</span>';
|
|
1110
|
+
h += '<span><span class="status-pill ' + sClass + '">' + req.statusCode + '</span></span>';
|
|
1111
|
+
h += '<span>' + req.durationMs + 'ms</span>';
|
|
1112
|
+
if (req.responseSize) h += '<span>' + formatSize(req.responseSize) + '</span>';
|
|
1113
|
+
h += '</div>';
|
|
1114
|
+
h += '<div class="request-timeline tl-hidden" data-request-id="' + escHtml(req.id) + '" data-request-started="' + escHtml(String(req.startedAt)) + '"></div>';
|
|
1115
|
+
h += '<div class="detail-grid">';
|
|
1116
|
+
h += '<div class="detail-section"><h4>Request Headers</h4><pre>' + formatHeaders(req.headers) + '</pre></div>';
|
|
1117
|
+
h += '<div class="detail-section"><h4>Response Headers</h4><pre>' + formatHeaders(req.responseHeaders) + '</pre></div>';
|
|
1118
|
+
h += '<div class="detail-section"><h4>Request Body</h4><pre>' + formatJsonBody(req.requestBody) + '</pre></div>';
|
|
1119
|
+
h += '<div class="detail-section"><h4>Response Body</h4><pre>' + formatJsonBody(req.responseBody) + '</pre></div>';
|
|
1120
|
+
h += '</div>';
|
|
1121
|
+
h += '<div class="detail-actions"><button class="btn btn-curl">Copy cURL</button></div>';
|
|
1122
|
+
return h;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
function renderRequests() {
|
|
1128
|
+
reqListEl.innerHTML = '';
|
|
1129
|
+
for (var i = 0; i < state.requests.length; i++) {
|
|
1130
|
+
var req = state.requests[i];
|
|
1131
|
+
if (req.path && req.path.startsWith('/__brakit')) continue;
|
|
1132
|
+
appendRequestRow(req);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function prependRequestRow(req) {
|
|
1137
|
+
var result = createReqRow(req);
|
|
1138
|
+
reqListEl.prepend(result.detail);
|
|
1139
|
+
reqListEl.prepend(result.row);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function appendRequestRow(req) {
|
|
1143
|
+
var result = createReqRow(req);
|
|
1144
|
+
reqListEl.appendChild(result.row);
|
|
1145
|
+
reqListEl.appendChild(result.detail);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function createReqRow(req) {
|
|
1149
|
+
var row = document.createElement('div');
|
|
1150
|
+
row.className = 'req-row';
|
|
1151
|
+
var sClass = statusPillClass(req.statusCode);
|
|
1152
|
+
var safeMethod = escHtml(req.method);
|
|
1153
|
+
row.innerHTML =
|
|
1154
|
+
'<div class="req-summary">' +
|
|
1155
|
+
'<span class="method-badge method-badge-' + safeMethod + '">' + safeMethod + '</span>' +
|
|
1156
|
+
'<span class="req-url">' + escHtml(req.url) + '</span>' +
|
|
1157
|
+
'<span class="status-pill ' + sClass + '">' + req.statusCode + '</span>' +
|
|
1158
|
+
'<span class="req-duration">' + req.durationMs + 'ms</span>' +
|
|
1159
|
+
'<span class="req-size">' + formatSize(req.responseSize) + '</span>' +
|
|
1160
|
+
'</div>';
|
|
1161
|
+
var detail = document.createElement('div');
|
|
1162
|
+
detail.className = 'req-detail';
|
|
1163
|
+
row.addEventListener('click', function() {
|
|
1164
|
+
var wasOpen = row.classList.contains('expanded');
|
|
1165
|
+
collapseAll('.req-row', '.req-detail');
|
|
1166
|
+
if (!wasOpen) {
|
|
1167
|
+
row.classList.add('expanded');
|
|
1168
|
+
detail.classList.add('open');
|
|
1169
|
+
detail.innerHTML = renderDetail(req);
|
|
1170
|
+
var curlBtn = detail.querySelector('.btn-curl');
|
|
1171
|
+
if (curlBtn) curlBtn.addEventListener('click', function(e) { e.stopPropagation(); copyAsCurl(req); });
|
|
1172
|
+
var tlEl = detail.querySelector('.request-timeline');
|
|
1173
|
+
if (tlEl) loadTimeline(tlEl.getAttribute('data-request-id'), tlEl, 0);
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
return { row: row, detail: detail };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
function buildFetchAnalysis() {
|
|
1181
|
+
var container = document.getElementById('fetch-analysis');
|
|
1182
|
+
if (!container) return;
|
|
1183
|
+
container.innerHTML = '';
|
|
1184
|
+
|
|
1185
|
+
var fetches = state.fetches;
|
|
1186
|
+
if (fetches.length === 0) {
|
|
1187
|
+
container.style.display = 'none';
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
container.style.display = 'block';
|
|
1191
|
+
|
|
1192
|
+
var uniqueUrls = {};
|
|
1193
|
+
var errCount = 0;
|
|
1194
|
+
var totalDur = 0;
|
|
1195
|
+
for (var i = 0; i < fetches.length; i++) {
|
|
1196
|
+
uniqueUrls[fetches[i].url] = 1;
|
|
1197
|
+
if (fetches[i].statusCode >= 400) errCount++;
|
|
1198
|
+
totalDur += fetches[i].durationMs;
|
|
1199
|
+
}
|
|
1200
|
+
var uniqueCount = Object.keys(uniqueUrls).length;
|
|
1201
|
+
var avgDur = Math.round(totalDur / fetches.length);
|
|
1202
|
+
|
|
1203
|
+
var summary = document.createElement('div');
|
|
1204
|
+
summary.className = 'fetch-summary';
|
|
1205
|
+
summary.innerHTML =
|
|
1206
|
+
'<div class="fetch-stat"><span class="fetch-stat-value">' + fetches.length + '</span><span class="fetch-stat-label">Total Fetches</span></div>' +
|
|
1207
|
+
'<div class="fetch-stat"><span class="fetch-stat-value">' + uniqueCount + '</span><span class="fetch-stat-label">Unique URLs</span></div>' +
|
|
1208
|
+
'<div class="fetch-stat"><span class="fetch-stat-value"' + (errCount > 0 ? ' style="color:var(--red)"' : '') + '>' + errCount + '</span><span class="fetch-stat-label">Errors</span></div>' +
|
|
1209
|
+
'<div class="fetch-stat"><span class="fetch-stat-value">' + formatDuration(avgDur) + '</span><span class="fetch-stat-label">Avg Duration</span></div>';
|
|
1210
|
+
container.appendChild(summary);
|
|
1211
|
+
|
|
1212
|
+
var groups = {};
|
|
1213
|
+
for (var gi = 0; gi < fetches.length; gi++) {
|
|
1214
|
+
var f = fetches[gi];
|
|
1215
|
+
var key = f.method + ' ' + f.url;
|
|
1216
|
+
if (!groups[key]) groups[key] = { method: f.method, url: f.url, count: 0, totalDur: 0, maxDur: 0, errors: 0, callers: {} };
|
|
1217
|
+
var g = groups[key];
|
|
1218
|
+
g.count++;
|
|
1219
|
+
g.totalDur += f.durationMs;
|
|
1220
|
+
if (f.durationMs > g.maxDur) g.maxDur = f.durationMs;
|
|
1221
|
+
if (f.statusCode >= 400) g.errors++;
|
|
1222
|
+
if (f.parentRequestId) {
|
|
1223
|
+
for (var ri = 0; ri < state.requests.length; ri++) {
|
|
1224
|
+
if (state.requests[ri].id === f.parentRequestId) {
|
|
1225
|
+
var callerLabel = state.requests[ri].method + ' ' + (state.requests[ri].path || state.requests[ri].url);
|
|
1226
|
+
g.callers[callerLabel] = 1;
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
var groupEntries = [];
|
|
1234
|
+
for (var gk in groups) groupEntries.push(groups[gk]);
|
|
1235
|
+
groupEntries.sort(function(a, b) { return b.count - a.count; });
|
|
1236
|
+
|
|
1237
|
+
if (groupEntries.length > 0) {
|
|
1238
|
+
var title = document.createElement('div');
|
|
1239
|
+
title.className = 'fetch-groups-title';
|
|
1240
|
+
title.textContent = 'Grouped by URL (' + groupEntries.length + ')';
|
|
1241
|
+
container.appendChild(title);
|
|
1242
|
+
|
|
1243
|
+
var groupsDiv = document.createElement('div');
|
|
1244
|
+
groupsDiv.className = 'fetch-groups';
|
|
1245
|
+
|
|
1246
|
+
for (var gei = 0; gei < groupEntries.length; gei++) {
|
|
1247
|
+
var ge = groupEntries[gei];
|
|
1248
|
+
var card = document.createElement('div');
|
|
1249
|
+
card.className = 'fetch-group';
|
|
1250
|
+
|
|
1251
|
+
var avgMs = Math.round(ge.totalDur / ge.count);
|
|
1252
|
+
var errRate = ge.count > 0 ? Math.round((ge.errors / ge.count) * 100) : 0;
|
|
1253
|
+
|
|
1254
|
+
var headerHtml =
|
|
1255
|
+
'<div class="fetch-group-header">' +
|
|
1256
|
+
'<span class="method-badge method-badge-' + escHtml(ge.method) + '">' + escHtml(ge.method) + '</span>' +
|
|
1257
|
+
'<span class="fetch-group-url" title="' + escHtml(ge.url) + '">' + escHtml(ge.url) + '</span>' +
|
|
1258
|
+
'<span class="fetch-group-count">' + ge.count + 'x</span>' +
|
|
1259
|
+
'</div>';
|
|
1260
|
+
|
|
1261
|
+
var metaHtml = '<div class="fetch-group-meta">' +
|
|
1262
|
+
'<span>Avg ' + formatDuration(avgMs) + '</span>' +
|
|
1263
|
+
'<span>Max ' + formatDuration(ge.maxDur) + '</span>' +
|
|
1264
|
+
(errRate > 0 ? '<span class="fetch-group-err">' + errRate + '% errors</span>' : '<span style="color:var(--green)">0% errors</span>') +
|
|
1265
|
+
'</div>';
|
|
1266
|
+
|
|
1267
|
+
var callerKeys = Object.keys(ge.callers);
|
|
1268
|
+
var callerHtml = '';
|
|
1269
|
+
if (callerKeys.length > 0) {
|
|
1270
|
+
callerHtml = '<div class="fetch-group-callers">Called by: <strong>' + callerKeys.map(function(c) { return escHtml(c); }).join('</strong>, <strong>') + '</strong></div>';
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
card.innerHTML = headerHtml + metaHtml + callerHtml;
|
|
1274
|
+
groupsDiv.appendChild(card);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
container.appendChild(groupsDiv);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function renderFetches() {
|
|
1282
|
+
buildFetchAnalysis();
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function prependFetchRow(f) {
|
|
1286
|
+
buildFetchAnalysis();
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function loadFetches() {
|
|
1290
|
+
try {
|
|
1291
|
+
var res = await fetch('/__brakit/api/fetches');
|
|
1292
|
+
var data = await res.json();
|
|
1293
|
+
state.fetches = data.entries;
|
|
1294
|
+
renderFetches();
|
|
1295
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
function buildErrorRow(e) {
|
|
1300
|
+
var row = document.createElement('div');
|
|
1301
|
+
row.className = 'req-row tel-clickable';
|
|
1302
|
+
var ts = new Date(e.timestamp).toLocaleTimeString();
|
|
1303
|
+
row.innerHTML =
|
|
1304
|
+
'<span class="tel-error-name" title="' + escHtml(e.name) + '">' + escHtml(e.name) + '</span>' +
|
|
1305
|
+
'<span class="tel-message" title="' + escHtml(e.message) + '">' + escHtml(e.message) + '</span>' +
|
|
1306
|
+
'<span class="tel-timestamp">' + ts + '</span>';
|
|
1307
|
+
row.addEventListener('click', function() {
|
|
1308
|
+
row.classList.toggle('expanded');
|
|
1309
|
+
var existing = row.nextElementSibling;
|
|
1310
|
+
if (existing && existing.classList.contains('error-stack')) {
|
|
1311
|
+
existing.remove();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (e.stack) {
|
|
1315
|
+
var stackEl = document.createElement('div');
|
|
1316
|
+
stackEl.className = 'error-stack';
|
|
1317
|
+
stackEl.textContent = e.stack;
|
|
1318
|
+
row.parentNode.insertBefore(stackEl, row.nextSibling);
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
return row;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
var errorView = createTelemetryView('error-list', buildErrorRow);
|
|
1325
|
+
function renderErrors() { errorView.render(state.errors); }
|
|
1326
|
+
function prependErrorRow(e) { errorView.prepend(e); }
|
|
1327
|
+
|
|
1328
|
+
async function loadErrors() {
|
|
1329
|
+
try {
|
|
1330
|
+
var res = await fetch('/__brakit/api/errors');
|
|
1331
|
+
var data = await res.json();
|
|
1332
|
+
state.errors = data.entries;
|
|
1333
|
+
renderErrors();
|
|
1334
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
function buildLogRow(l) {
|
|
1339
|
+
var row = document.createElement('div');
|
|
1340
|
+
row.className = 'req-row';
|
|
1341
|
+
var ts = new Date(l.timestamp).toLocaleTimeString();
|
|
1342
|
+
row.innerHTML =
|
|
1343
|
+
'<span class="tel-level tel-level-' + l.level + '">' + l.level.toUpperCase() + '</span>' +
|
|
1344
|
+
'<span class="tel-message tel-mono" title="' + escHtml(l.message) + '">' + escHtml(l.message) + '</span>' +
|
|
1345
|
+
'<span class="tel-timestamp">' + ts + '</span>';
|
|
1346
|
+
return row;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function buildLogAnalysis() {
|
|
1350
|
+
var container = document.getElementById('log-analysis');
|
|
1351
|
+
if (!container) return;
|
|
1352
|
+
container.innerHTML = '';
|
|
1353
|
+
|
|
1354
|
+
var logs = state.logs;
|
|
1355
|
+
if (logs.length === 0) {
|
|
1356
|
+
container.style.display = 'none';
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
container.style.display = 'block';
|
|
1360
|
+
|
|
1361
|
+
var counts = { error: 0, warn: 0, info: 0, debug: 0, log: 0 };
|
|
1362
|
+
for (var i = 0; i < logs.length; i++) {
|
|
1363
|
+
var lvl = logs[i].level;
|
|
1364
|
+
if (counts[lvl] !== undefined) counts[lvl]++;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
var summary = document.createElement('div');
|
|
1368
|
+
summary.className = 'fetch-summary';
|
|
1369
|
+
summary.innerHTML =
|
|
1370
|
+
'<div class="fetch-stat"><span class="fetch-stat-value">' + logs.length + '</span><span class="fetch-stat-label">Total Logs</span></div>' +
|
|
1371
|
+
(counts.error > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value" style="color:var(--red)">' + counts.error + '</span><span class="fetch-stat-label">Errors</span></div>' : '') +
|
|
1372
|
+
(counts.warn > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value" style="color:var(--amber)">' + counts.warn + '</span><span class="fetch-stat-label">Warnings</span></div>' : '') +
|
|
1373
|
+
'<div class="fetch-stat"><span class="fetch-stat-value">' + counts.info + '</span><span class="fetch-stat-label">Info</span></div>' +
|
|
1374
|
+
(counts.debug > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value">' + counts.debug + '</span><span class="fetch-stat-label">Debug</span></div>' : '') +
|
|
1375
|
+
(counts.log > 0 ? '<div class="fetch-stat"><span class="fetch-stat-value">' + counts.log + '</span><span class="fetch-stat-label">Log</span></div>' : '');
|
|
1376
|
+
container.appendChild(summary);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
var logView = createTelemetryView('log-list', buildLogRow);
|
|
1380
|
+
|
|
1381
|
+
function renderLogs() {
|
|
1382
|
+
buildLogAnalysis();
|
|
1383
|
+
logView.render(state.logs);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function prependLogRow(l) {
|
|
1387
|
+
buildLogAnalysis();
|
|
1388
|
+
logView.prepend(l);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
async function loadLogs() {
|
|
1392
|
+
try {
|
|
1393
|
+
var res = await fetch('/__brakit/api/logs');
|
|
1394
|
+
var data = await res.json();
|
|
1395
|
+
state.logs = data.entries;
|
|
1396
|
+
renderLogs();
|
|
1397
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
function buildQueryRow(q) {
|
|
1402
|
+
var wrapper = document.createElement('div');
|
|
1403
|
+
var row = document.createElement('div');
|
|
1404
|
+
row.className = 'req-row query-row tel-clickable';
|
|
1405
|
+
|
|
1406
|
+
var info = { op: (q.normalizedOp || q.operation || '?').toUpperCase(), table: q.table || q.model || '' };
|
|
1407
|
+
var opColor = QUERY_OP_COLORS[info.op] || 'var(--text-dim)';
|
|
1408
|
+
var slowCls = q.durationMs > 100 ? ' query-slow' : '';
|
|
1409
|
+
var preview = q.sql || (info.op + ' ' + info.table);
|
|
1410
|
+
|
|
1411
|
+
row.innerHTML =
|
|
1412
|
+
'<span class="query-op" title="' + escHtml(info.op) + '" style="color:' + opColor + '">' + escHtml(info.op) + '</span>' +
|
|
1413
|
+
'<span class="query-table" title="' + escHtml(info.table) + '">' + escHtml(info.table) + '</span>' +
|
|
1414
|
+
'<span class="query-preview" title="' + escHtml(preview) + '">' + escHtml(preview) + '</span>' +
|
|
1415
|
+
'<span class="query-dur' + slowCls + '">' + queryDuration(q.durationMs) + '</span>';
|
|
1416
|
+
|
|
1417
|
+
var sqlText = q.sql || (info.op + ' ' + info.table);
|
|
1418
|
+
var detail = document.createElement('div');
|
|
1419
|
+
detail.className = 'query-detail';
|
|
1420
|
+
detail.innerHTML = '<pre class="query-detail-sql">' + escHtml(sqlText) + '</pre><button class="query-detail-copy">Copy</button>';
|
|
1421
|
+
|
|
1422
|
+
row.addEventListener('click', function() {
|
|
1423
|
+
var wasOpen = detail.classList.contains('open');
|
|
1424
|
+
if (wasOpen) {
|
|
1425
|
+
detail.classList.remove('open');
|
|
1426
|
+
row.classList.remove('expanded');
|
|
1427
|
+
} else {
|
|
1428
|
+
detail.classList.add('open');
|
|
1429
|
+
row.classList.add('expanded');
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
detail.querySelector('.query-detail-copy').addEventListener('click', function(e) {
|
|
1434
|
+
e.stopPropagation();
|
|
1435
|
+
navigator.clipboard.writeText(sqlText).then(function() { showToast('SQL copied'); });
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
wrapper.appendChild(row);
|
|
1439
|
+
wrapper.appendChild(detail);
|
|
1440
|
+
return wrapper;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
var queryView = createTelemetryView('query-list', buildQueryRow);
|
|
1444
|
+
function renderQueries() { queryView.render(state.queries); }
|
|
1445
|
+
function prependQueryRow(q) { queryView.prepend(q); }
|
|
1446
|
+
|
|
1447
|
+
async function loadQueries() {
|
|
1448
|
+
try {
|
|
1449
|
+
var res = await fetch('/__brakit/api/queries');
|
|
1450
|
+
var data = await res.json();
|
|
1451
|
+
state.queries = data.entries;
|
|
1452
|
+
renderQueries();
|
|
1453
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
var TL_TYPE_COLORS = { fetch: 'var(--blue)', log: 'var(--text-muted)', error: 'var(--red)', query: 'var(--accent)' };
|
|
1458
|
+
var TL_TYPE_LABELS = { fetch: 'FETCH', log: 'LOG', error: 'ERROR', query: 'QUERY' };
|
|
1459
|
+
var LOG_LEVEL_COLORS = { error: 'var(--red)', warn: 'var(--amber)', info: 'var(--blue)', debug: 'var(--text-muted)', log: 'var(--text-dim)' };
|
|
1460
|
+
|
|
1461
|
+
var timelineCache = {};
|
|
1462
|
+
var TIMELINE_CACHE_MAX = 50;
|
|
1463
|
+
|
|
1464
|
+
async function loadTimeline(requestId, container, requestStartedAt) {
|
|
1465
|
+
if (timelineCache[requestId]) {
|
|
1466
|
+
renderTimelineContent(timelineCache[requestId], container, requestStartedAt);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
container.classList.remove('tl-hidden');
|
|
1471
|
+
container.innerHTML = '<div class="tl-loading">Loading activity...</div>';
|
|
1472
|
+
|
|
1473
|
+
try {
|
|
1474
|
+
var res = await fetch('/__brakit/api/activity?requestId=' + requestId);
|
|
1475
|
+
var data = await res.json();
|
|
1476
|
+
|
|
1477
|
+
var keys = Object.keys(timelineCache);
|
|
1478
|
+
if (keys.length >= TIMELINE_CACHE_MAX) delete timelineCache[keys[0]];
|
|
1479
|
+
timelineCache[requestId] = data;
|
|
1480
|
+
|
|
1481
|
+
renderTimelineContent(data, container, requestStartedAt);
|
|
1482
|
+
} catch(ex) {
|
|
1483
|
+
container.innerHTML = '';
|
|
1484
|
+
container.classList.add('tl-hidden');
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function renderTimelineContent(data, container, requestStartedAt) {
|
|
1489
|
+
if (data.total === 0) {
|
|
1490
|
+
container.innerHTML = '';
|
|
1491
|
+
container.classList.add('tl-hidden');
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
container.classList.remove('tl-hidden');
|
|
1495
|
+
|
|
1496
|
+
var h = '<div class="tl-header">';
|
|
1497
|
+
h += '<span class="tl-title">Activity Timeline</span>';
|
|
1498
|
+
h += '<span class="tl-counts">';
|
|
1499
|
+
if (data.counts.queries > 0) h += '<span class="tl-count tl-count-query">' + data.counts.queries + ' quer' + (data.counts.queries === 1 ? 'y' : 'ies') + '</span>';
|
|
1500
|
+
if (data.counts.fetches > 0) h += '<span class="tl-count tl-count-fetch">' + data.counts.fetches + ' fetch' + (data.counts.fetches === 1 ? '' : 'es') + '</span>';
|
|
1501
|
+
if (data.counts.logs > 0) h += '<span class="tl-count tl-count-log">' + data.counts.logs + ' log' + (data.counts.logs === 1 ? '' : 's') + '</span>';
|
|
1502
|
+
if (data.counts.errors > 0) h += '<span class="tl-count tl-count-error">' + data.counts.errors + ' error' + (data.counts.errors === 1 ? '' : 's') + '</span>';
|
|
1503
|
+
h += '</span></div>';
|
|
1504
|
+
h += '<div class="tl-events">';
|
|
1505
|
+
|
|
1506
|
+
var baseTs = data.timeline[0].timestamp;
|
|
1507
|
+
|
|
1508
|
+
for (var i = 0; i < data.timeline.length; i++) {
|
|
1509
|
+
var evt = data.timeline[i];
|
|
1510
|
+
var color = TL_TYPE_COLORS[evt.type] || 'var(--text-dim)';
|
|
1511
|
+
var label = TL_TYPE_LABELS[evt.type] || evt.type;
|
|
1512
|
+
var relMs = Math.round(evt.timestamp - baseTs);
|
|
1513
|
+
var relStr = '+' + formatDuration(relMs);
|
|
1514
|
+
var isQuery = evt.type === 'query' && evt.data && evt.data.sql;
|
|
1515
|
+
|
|
1516
|
+
h += '<div class="tl-event' + (isQuery ? ' tl-clickable' : '') + '"' + (isQuery ? '' : ' style="border-left-color:' + color + '"') + '>';
|
|
1517
|
+
h += '<span class="tl-event-time">' + relStr + '</span>';
|
|
1518
|
+
h += '<span class="tl-event-type" style="color:' + color + '">' + label + '</span>';
|
|
1519
|
+
h += renderTimelineEvent(evt);
|
|
1520
|
+
if (isQuery) {
|
|
1521
|
+
h += '<div class="tl-event-sql" data-sql="' + escHtml(evt.data.sql) + '"><button class="tl-sql-copy">Copy</button>' + escHtml(evt.data.sql) + '</div>';
|
|
1522
|
+
}
|
|
1523
|
+
h += '</div>';
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
h += '</div>';
|
|
1527
|
+
container.innerHTML = h;
|
|
1528
|
+
|
|
1529
|
+
container.querySelectorAll('.tl-clickable').forEach(function(el) {
|
|
1530
|
+
el.addEventListener('click', function(e) {
|
|
1531
|
+
e.stopPropagation();
|
|
1532
|
+
var sqlEl = el.querySelector('.tl-event-sql');
|
|
1533
|
+
if (sqlEl) sqlEl.classList.toggle('open');
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
1536
|
+
container.querySelectorAll('.tl-sql-copy').forEach(function(btn) {
|
|
1537
|
+
btn.addEventListener('click', function(e) {
|
|
1538
|
+
e.stopPropagation();
|
|
1539
|
+
var sqlEl = btn.closest('.tl-event-sql');
|
|
1540
|
+
if (sqlEl) {
|
|
1541
|
+
navigator.clipboard.writeText(sqlEl.getAttribute('data-sql')).then(function() { showToast('SQL copied'); });
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function renderTimelineEvent(evt) {
|
|
1548
|
+
var d = evt.data;
|
|
1549
|
+
if (evt.type === 'fetch') {
|
|
1550
|
+
var sCls = d.statusCode >= 400 ? ' style="color:var(--red)"' : '';
|
|
1551
|
+
return '<span class="tl-event-summary">' + escHtml(d.method) + ' ' + escHtml(d.url) + '</span>' +
|
|
1552
|
+
'<span class="tl-event-status"' + sCls + '>' + d.statusCode + '</span>' +
|
|
1553
|
+
'<span class="tl-event-dur">' + formatDuration(d.durationMs) + '</span>';
|
|
1554
|
+
}
|
|
1555
|
+
if (evt.type === 'query') {
|
|
1556
|
+
var info = { op: (d.normalizedOp || d.operation || '?').toUpperCase(), table: d.table || d.model || '' };
|
|
1557
|
+
var opColor = QUERY_OP_COLORS[info.op] || 'var(--text-dim)';
|
|
1558
|
+
return '<span class="tl-event-summary"><span style="color:' + opColor + ';font-weight:600">' + escHtml(info.op) + '</span> ' + escHtml(info.table) + '</span>' +
|
|
1559
|
+
'<span class="tl-event-dur">' + queryDuration(d.durationMs) + '</span>';
|
|
1560
|
+
}
|
|
1561
|
+
if (evt.type === 'log') {
|
|
1562
|
+
var lColor = LOG_LEVEL_COLORS[d.level] || 'var(--text-dim)';
|
|
1563
|
+
return '<span class="tl-event-summary"><span style="color:' + lColor + '">' + d.level.toUpperCase() + '</span> ' + escHtml(d.message) + '</span>';
|
|
1564
|
+
}
|
|
1565
|
+
if (evt.type === 'error') {
|
|
1566
|
+
return '<span class="tl-event-summary" style="color:var(--red)">' + escHtml(d.name) + ': ' + escHtml(d.message) + '</span>';
|
|
1567
|
+
}
|
|
1568
|
+
return '';
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function invalidateTimelineCache(requestId) {
|
|
1572
|
+
delete timelineCache[requestId];
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function refreshVisibleTimeline(requestId) {
|
|
1576
|
+
var el = document.querySelector('.request-timeline[data-request-id="' + requestId + '"]');
|
|
1577
|
+
if (el && el.closest('.flow-expand.open, .req-detail.open')) {
|
|
1578
|
+
loadTimeline(requestId, el, 0);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
var timelineObserver = null;
|
|
1583
|
+
if (window.IntersectionObserver) {
|
|
1584
|
+
timelineObserver = new IntersectionObserver(function(entries) {
|
|
1585
|
+
entries.forEach(function(entry) {
|
|
1586
|
+
if (entry.isIntersecting) {
|
|
1587
|
+
var el = entry.target;
|
|
1588
|
+
var rid = el.getAttribute('data-request-id');
|
|
1589
|
+
var started = parseFloat(el.getAttribute('data-request-started'));
|
|
1590
|
+
if (rid && !el.hasAttribute('data-loaded')) {
|
|
1591
|
+
el.setAttribute('data-loaded', '1');
|
|
1592
|
+
loadTimeline(rid, el, started);
|
|
1593
|
+
}
|
|
1594
|
+
timelineObserver.unobserve(el);
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
}, { rootMargin: '200px' });
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function observeTimeline(el) {
|
|
1601
|
+
if (timelineObserver) {
|
|
1602
|
+
timelineObserver.observe(el);
|
|
1603
|
+
} else {
|
|
1604
|
+
var rid = el.getAttribute('data-request-id');
|
|
1605
|
+
var started = parseFloat(el.getAttribute('data-request-started'));
|
|
1606
|
+
loadTimeline(rid, el, started);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
var graphData = null;
|
|
1612
|
+
var selectedEndpoint = '__all__';
|
|
1613
|
+
|
|
1614
|
+
var GRAPH_COLORS = ['#2563eb','#7c3aed','#16a34a','#d97706','#dc2626','#0891b2','#ea580c','#c026d3','#059669','#db2777'];
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
var HEALTH_GRADES = [
|
|
1618
|
+
{ max: 100, label: 'Fast', color: 'var(--green)', bg: 'rgba(22,163,74,0.08)', border: 'rgba(22,163,74,0.2)' },
|
|
1619
|
+
{ max: 300, label: 'Good', color: 'var(--green)', bg: 'rgba(22,163,74,0.06)', border: 'rgba(22,163,74,0.15)' },
|
|
1620
|
+
{ max: 800, label: 'OK', color: 'var(--amber)', bg: 'rgba(217,119,6,0.06)', border: 'rgba(217,119,6,0.15)' },
|
|
1621
|
+
{ max: 2000, label: 'Slow', color: 'var(--red)', bg: 'rgba(220,38,38,0.06)', border: 'rgba(220,38,38,0.15)' },
|
|
1622
|
+
{ max: Infinity, label: 'Critical', color: 'var(--red)', bg: 'rgba(220,38,38,0.08)', border: 'rgba(220,38,38,0.2)' }
|
|
1623
|
+
];
|
|
1624
|
+
var DOT_COLORS = { green: '#4ade80', amber: '#fbbf24', red: '#f87171' };
|
|
1625
|
+
|
|
1626
|
+
function healthGrade(ms) {
|
|
1627
|
+
for (var i = 0; i < HEALTH_GRADES.length; i++) {
|
|
1628
|
+
if (ms < HEALTH_GRADES[i].max) return HEALTH_GRADES[i];
|
|
1629
|
+
}
|
|
1630
|
+
return HEALTH_GRADES[HEALTH_GRADES.length - 1];
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function fmtMs(ms) {
|
|
1634
|
+
if (ms < 1) return '<1ms';
|
|
1635
|
+
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
1636
|
+
return (ms / 1000).toFixed(1) + 's';
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function dotColor(ms) {
|
|
1640
|
+
if (ms < 300) return DOT_COLORS.green;
|
|
1641
|
+
if (ms < 800) return DOT_COLORS.amber;
|
|
1642
|
+
return DOT_COLORS.red;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function buildMetricCard(label, value, color) {
|
|
1646
|
+
return '<div class="perf-metric-card">' +
|
|
1647
|
+
'<span class="perf-metric-label">' + label + '</span>' +
|
|
1648
|
+
'<span class="perf-metric-value" style="color:' + color + '">' + value + '</span>' +
|
|
1649
|
+
'</div>';
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
var HIGH_QUERY_THRESHOLD = 5;
|
|
1654
|
+
|
|
1655
|
+
function renderPerfOverview(container) {
|
|
1656
|
+
var list = document.createElement('div');
|
|
1657
|
+
list.className = 'perf-endpoint-list';
|
|
1658
|
+
|
|
1659
|
+
graphData.forEach(function(ep, idx) {
|
|
1660
|
+
if (ep.requests.length === 0) return;
|
|
1661
|
+
var s = ep.summary;
|
|
1662
|
+
var g = healthGrade(s.p95Ms);
|
|
1663
|
+
var errors = Math.round(s.errorRate * s.totalRequests);
|
|
1664
|
+
|
|
1665
|
+
var card = document.createElement('div');
|
|
1666
|
+
card.className = 'perf-endpoint-card';
|
|
1667
|
+
card.addEventListener('click', function() { selectedEndpoint = ep.endpoint; renderGraph(); });
|
|
1668
|
+
|
|
1669
|
+
var statsHtml =
|
|
1670
|
+
'<span class="perf-ep-stat" style="color:' + g.color + '">p95: ' + fmtMs(s.p95Ms) + '</span>' +
|
|
1671
|
+
'<span class="perf-ep-stat' + (errors > 0 ? ' perf-ep-stat-err' : '') + '">' + errors + ' err</span>' +
|
|
1672
|
+
(s.avgQueryCount > 0 ? '<span class="perf-ep-stat' + (s.avgQueryCount > HIGH_QUERY_THRESHOLD ? ' perf-ep-stat-warn' : '') + '">' + s.avgQueryCount + ' q/req</span>' : '') +
|
|
1673
|
+
'<span class="perf-ep-stat perf-ep-stat-muted">' + s.totalRequests + ' req' + (s.totalRequests !== 1 ? 's' : '') + '</span>';
|
|
1674
|
+
|
|
1675
|
+
var ovTotal = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
|
|
1676
|
+
var ovBarHtml = '';
|
|
1677
|
+
if (ovTotal > 0) {
|
|
1678
|
+
var ovDbPct = Math.round((s.avgQueryTimeMs || 0) / ovTotal * 100);
|
|
1679
|
+
var ovFetchPct = Math.round((s.avgFetchTimeMs || 0) / ovTotal * 100);
|
|
1680
|
+
var ovAppPct = Math.max(0, 100 - ovDbPct - ovFetchPct);
|
|
1681
|
+
ovBarHtml =
|
|
1682
|
+
'<div class="perf-breakdown-inline">' +
|
|
1683
|
+
'<div class="perf-breakdown-bar perf-breakdown-bar-sm">' +
|
|
1684
|
+
(ovDbPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + ovDbPct + '%"></div>' : '') +
|
|
1685
|
+
(ovFetchPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + ovFetchPct + '%"></div>' : '') +
|
|
1686
|
+
(ovAppPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + ovAppPct + '%"></div>' : '') +
|
|
1687
|
+
'</div>' +
|
|
1688
|
+
'<span class="perf-breakdown-labels">' +
|
|
1689
|
+
(ovDbPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-db"></span>' + fmtMs(s.avgQueryTimeMs || 0) + '</span>' : '') +
|
|
1690
|
+
(ovFetchPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>' + fmtMs(s.avgFetchTimeMs || 0) + '</span>' : '') +
|
|
1691
|
+
'<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-app"></span>' + fmtMs(s.avgAppTimeMs || 0) + '</span>' +
|
|
1692
|
+
'</span>' +
|
|
1693
|
+
'</div>';
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
var chartId = 'inline-scatter-' + idx;
|
|
1697
|
+
|
|
1698
|
+
card.innerHTML =
|
|
1699
|
+
'<div class="perf-ep-header">' +
|
|
1700
|
+
'<span class="perf-ep-name">' + escHtml(ep.endpoint) + '</span>' +
|
|
1701
|
+
'<span class="perf-ep-stats">' + statsHtml + '</span>' +
|
|
1702
|
+
'</div>' +
|
|
1703
|
+
ovBarHtml +
|
|
1704
|
+
'<canvas id="' + chartId + '" class="perf-inline-canvas"></canvas>';
|
|
1705
|
+
|
|
1706
|
+
list.appendChild(card);
|
|
1707
|
+
|
|
1708
|
+
setTimeout(function() {
|
|
1709
|
+
var c = document.getElementById(chartId);
|
|
1710
|
+
if (c) drawInlineScatter(c, ep.requests);
|
|
1711
|
+
}, 0);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
container.appendChild(list);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
function renderEndpointDetail(container) {
|
|
1719
|
+
var ep = graphData.find(function(e) { return e.endpoint === selectedEndpoint; });
|
|
1720
|
+
if (!ep || !ep.requests || ep.requests.length === 0) {
|
|
1721
|
+
container.innerHTML += '<div class="empty"><span class="empty-sub">No data for this endpoint</span></div>';
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
var s = ep.summary;
|
|
1726
|
+
var g = healthGrade(s.p95Ms);
|
|
1727
|
+
var errors = Math.round(s.errorRate * s.totalRequests);
|
|
1728
|
+
|
|
1729
|
+
var header = document.createElement('div');
|
|
1730
|
+
header.className = 'perf-detail-header';
|
|
1731
|
+
header.innerHTML =
|
|
1732
|
+
'<div class="perf-detail-title">' +
|
|
1733
|
+
'<span class="perf-badge perf-badge-lg" style="color:' + g.color + ';background:' + g.bg + ';border-color:' + g.border + '">' + g.label + '</span>' +
|
|
1734
|
+
'<span>' + escHtml(ep.endpoint) + '</span>' +
|
|
1735
|
+
'</div>';
|
|
1736
|
+
container.appendChild(header);
|
|
1737
|
+
|
|
1738
|
+
var metrics = document.createElement('div');
|
|
1739
|
+
metrics.className = 'perf-metric-row';
|
|
1740
|
+
metrics.innerHTML =
|
|
1741
|
+
buildMetricCard('P95', fmtMs(s.p95Ms), g.color) +
|
|
1742
|
+
buildMetricCard('Errors', errors > 0 ? errors + ' (' + Math.round(s.errorRate * 100) + '%)' : '0', errors > 0 ? 'var(--red)' : 'var(--green)') +
|
|
1743
|
+
buildMetricCard('Queries/req', String(s.avgQueryCount), s.avgQueryCount > 5 ? 'var(--amber)' : 'var(--text)');
|
|
1744
|
+
container.appendChild(metrics);
|
|
1745
|
+
|
|
1746
|
+
var totalAvg = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
|
|
1747
|
+
if (totalAvg > 0) {
|
|
1748
|
+
var dbPct = Math.round((s.avgQueryTimeMs || 0) / totalAvg * 100);
|
|
1749
|
+
var fetchPct = Math.round((s.avgFetchTimeMs || 0) / totalAvg * 100);
|
|
1750
|
+
var appPct = Math.max(0, 100 - dbPct - fetchPct);
|
|
1751
|
+
|
|
1752
|
+
var breakdown = document.createElement('div');
|
|
1753
|
+
breakdown.className = 'perf-breakdown';
|
|
1754
|
+
|
|
1755
|
+
var breakdownLabel = document.createElement('div');
|
|
1756
|
+
breakdownLabel.className = 'perf-section-title';
|
|
1757
|
+
breakdownLabel.textContent = 'Time Breakdown';
|
|
1758
|
+
breakdown.appendChild(breakdownLabel);
|
|
1759
|
+
|
|
1760
|
+
var bar = document.createElement('div');
|
|
1761
|
+
bar.className = 'perf-breakdown-bar';
|
|
1762
|
+
if (dbPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + dbPct + '%"></div>';
|
|
1763
|
+
if (fetchPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + fetchPct + '%"></div>';
|
|
1764
|
+
if (appPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + appPct + '%"></div>';
|
|
1765
|
+
breakdown.appendChild(bar);
|
|
1766
|
+
|
|
1767
|
+
var legend = document.createElement('div');
|
|
1768
|
+
legend.className = 'perf-breakdown-legend';
|
|
1769
|
+
legend.innerHTML =
|
|
1770
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-db"></span>DB ' + fmtMs(s.avgQueryTimeMs || 0) + ' (' + dbPct + '%)</span>' +
|
|
1771
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>Fetch ' + fmtMs(s.avgFetchTimeMs || 0) + ' (' + fetchPct + '%)</span>' +
|
|
1772
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-app"></span>App ' + fmtMs(s.avgAppTimeMs || 0) + ' (' + appPct + '%)</span>';
|
|
1773
|
+
breakdown.appendChild(legend);
|
|
1774
|
+
|
|
1775
|
+
container.appendChild(breakdown);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
var chartWrap = document.createElement('div');
|
|
1779
|
+
chartWrap.className = 'perf-chart-wrap';
|
|
1780
|
+
var chartLabel = document.createElement('div');
|
|
1781
|
+
chartLabel.className = 'perf-section-title';
|
|
1782
|
+
chartLabel.textContent = 'Response Time';
|
|
1783
|
+
chartWrap.appendChild(chartLabel);
|
|
1784
|
+
|
|
1785
|
+
var canvas = document.createElement('canvas');
|
|
1786
|
+
canvas.width = 800;
|
|
1787
|
+
canvas.height = 240;
|
|
1788
|
+
canvas.style.cssText = 'width:100%;height:240px';
|
|
1789
|
+
canvas.className = 'perf-canvas';
|
|
1790
|
+
chartWrap.appendChild(canvas);
|
|
1791
|
+
container.appendChild(chartWrap);
|
|
1792
|
+
|
|
1793
|
+
drawScatterChart(canvas, ep.requests);
|
|
1794
|
+
|
|
1795
|
+
if (ep.requests.length > 0) {
|
|
1796
|
+
var tableWrap = document.createElement('div');
|
|
1797
|
+
tableWrap.className = 'perf-history-wrap';
|
|
1798
|
+
|
|
1799
|
+
var colHeader = document.createElement('div');
|
|
1800
|
+
colHeader.className = 'col-header';
|
|
1801
|
+
colHeader.innerHTML =
|
|
1802
|
+
'<span class="perf-col perf-col-date">Time</span>' +
|
|
1803
|
+
'<span class="perf-col perf-col-health">Health</span>' +
|
|
1804
|
+
'<span class="perf-col perf-col-avg">Duration</span>' +
|
|
1805
|
+
'<span class="perf-col perf-col-breakdown">Breakdown</span>' +
|
|
1806
|
+
'<span class="perf-col perf-col-status">Status</span>' +
|
|
1807
|
+
'<span class="perf-col perf-col-qpr">Queries</span>';
|
|
1808
|
+
tableWrap.appendChild(colHeader);
|
|
1809
|
+
|
|
1810
|
+
var recentWithIdx = [];
|
|
1811
|
+
for (var ri = ep.requests.length - 1; ri >= 0 && recentWithIdx.length < 50; ri--) {
|
|
1812
|
+
recentWithIdx.push({ r: ep.requests[ri], origIdx: ri });
|
|
1813
|
+
}
|
|
1814
|
+
recentWithIdx.forEach(function(item) {
|
|
1815
|
+
var r = item.r;
|
|
1816
|
+
var rg = healthGrade(r.durationMs);
|
|
1817
|
+
var date = new Date(r.timestamp);
|
|
1818
|
+
var timeStr = date.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
1819
|
+
var isError = r.statusCode >= 400;
|
|
1820
|
+
|
|
1821
|
+
var row = document.createElement('div');
|
|
1822
|
+
row.className = 'perf-hist-row' + (isError ? ' perf-hist-row-err' : '');
|
|
1823
|
+
row.setAttribute('data-req-idx', item.origIdx);
|
|
1824
|
+
var rDbMs = r.queryTimeMs || 0;
|
|
1825
|
+
var rFetchMs = r.fetchTimeMs || 0;
|
|
1826
|
+
var rAppMs = Math.max(0, r.durationMs - rDbMs - rFetchMs);
|
|
1827
|
+
var breakdownParts = [];
|
|
1828
|
+
if (rDbMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-db">DB ' + fmtMs(rDbMs) + '</span>');
|
|
1829
|
+
if (rFetchMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-fetch">Fetch ' + fmtMs(rFetchMs) + '</span>');
|
|
1830
|
+
breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-app">App ' + fmtMs(rAppMs) + '</span>');
|
|
1831
|
+
var breakdownHtml = breakdownParts.join('');
|
|
1832
|
+
|
|
1833
|
+
row.innerHTML =
|
|
1834
|
+
'<span class="perf-col perf-col-date">' + timeStr + '</span>' +
|
|
1835
|
+
'<span class="perf-col perf-col-health"><span class="perf-badge perf-badge-sm" style="color:' + rg.color + ';background:' + rg.bg + ';border-color:' + rg.border + '">' + rg.label + '</span></span>' +
|
|
1836
|
+
'<span class="perf-col perf-col-avg">' + fmtMs(r.durationMs) + '</span>' +
|
|
1837
|
+
'<span class="perf-col perf-col-breakdown">' + breakdownHtml + '</span>' +
|
|
1838
|
+
'<span class="perf-col perf-col-status" style="color:' + (isError ? 'var(--red)' : 'var(--text-muted)') + '">' + r.statusCode + '</span>' +
|
|
1839
|
+
'<span class="perf-col perf-col-qpr">' + r.queryCount + '</span>';
|
|
1840
|
+
tableWrap.appendChild(row);
|
|
1841
|
+
});
|
|
1842
|
+
container.appendChild(tableWrap);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
var THRESHOLD_GOOD = 300;
|
|
1848
|
+
var THRESHOLD_OK = 800;
|
|
1849
|
+
var CHART_PAD = { top: 16, right: 16, bottom: 28, left: 52 };
|
|
1850
|
+
|
|
1851
|
+
var scatterDots = [];
|
|
1852
|
+
|
|
1853
|
+
function setupCanvas(canvas) {
|
|
1854
|
+
var ctx = canvas.getContext('2d');
|
|
1855
|
+
if (!ctx) return null;
|
|
1856
|
+
var dpr = window.devicePixelRatio || 1;
|
|
1857
|
+
var w = canvas.clientWidth;
|
|
1858
|
+
var h = canvas.clientHeight;
|
|
1859
|
+
canvas.width = w * dpr;
|
|
1860
|
+
canvas.height = h * dpr;
|
|
1861
|
+
ctx.scale(dpr, dpr);
|
|
1862
|
+
return { ctx: ctx, w: w, h: h };
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function reqDotColor(r) {
|
|
1866
|
+
if (r.statusCode >= 400) return DOT_COLORS.red;
|
|
1867
|
+
return dotColor(r.durationMs);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function drawDot(ctx, x, y, radius, color) {
|
|
1871
|
+
var r = parseInt(color.slice(1,3),16), g = parseInt(color.slice(3,5),16), b = parseInt(color.slice(5,7),16);
|
|
1872
|
+
ctx.beginPath();
|
|
1873
|
+
ctx.arc(x, y, radius + 2, 0, Math.PI * 2);
|
|
1874
|
+
ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',0.25)';
|
|
1875
|
+
ctx.fill();
|
|
1876
|
+
ctx.beginPath();
|
|
1877
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
1878
|
+
ctx.fillStyle = color;
|
|
1879
|
+
ctx.fill();
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function drawErrorX(ctx, x, y, size, color, lineWidth) {
|
|
1883
|
+
var r = parseInt(color.slice(1,3),16), g = parseInt(color.slice(3,5),16), b = parseInt(color.slice(5,7),16);
|
|
1884
|
+
ctx.strokeStyle = 'rgba(' + r + ',' + g + ',' + b + ',0.3)';
|
|
1885
|
+
ctx.lineWidth = lineWidth + 2;
|
|
1886
|
+
ctx.beginPath();
|
|
1887
|
+
ctx.moveTo(x - size, y - size); ctx.lineTo(x + size, y + size);
|
|
1888
|
+
ctx.moveTo(x + size, y - size); ctx.lineTo(x - size, y + size);
|
|
1889
|
+
ctx.stroke();
|
|
1890
|
+
ctx.strokeStyle = color;
|
|
1891
|
+
ctx.lineWidth = lineWidth;
|
|
1892
|
+
ctx.beginPath();
|
|
1893
|
+
ctx.moveTo(x - size, y - size); ctx.lineTo(x + size, y + size);
|
|
1894
|
+
ctx.moveTo(x + size, y - size); ctx.lineTo(x - size, y + size);
|
|
1895
|
+
ctx.stroke();
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Maps time → x-axis, duration → y-axis as a scatter plot
|
|
1899
|
+
function drawScatterChart(canvas, requests) {
|
|
1900
|
+
scatterDots = [];
|
|
1901
|
+
var setup = setupCanvas(canvas);
|
|
1902
|
+
if (!setup) return;
|
|
1903
|
+
var ctx = setup.ctx, w = setup.w, h = setup.h;
|
|
1904
|
+
if (requests.length === 0) return;
|
|
1905
|
+
|
|
1906
|
+
var pad = CHART_PAD;
|
|
1907
|
+
var cw = w - pad.left - pad.right;
|
|
1908
|
+
var ch = h - pad.top - pad.bottom;
|
|
1909
|
+
|
|
1910
|
+
var maxVal = 0;
|
|
1911
|
+
var minTime = requests[0].timestamp, maxTime = requests[0].timestamp;
|
|
1912
|
+
requests.forEach(function(r) {
|
|
1913
|
+
if (r.durationMs > maxVal) maxVal = r.durationMs;
|
|
1914
|
+
if (r.timestamp < minTime) minTime = r.timestamp;
|
|
1915
|
+
if (r.timestamp > maxTime) maxTime = r.timestamp;
|
|
1916
|
+
});
|
|
1917
|
+
maxVal = Math.max(maxVal, 10);
|
|
1918
|
+
maxVal = Math.ceil(maxVal * 1.15 / 10) * 10;
|
|
1919
|
+
var timeRange = maxTime - minTime || 1;
|
|
1920
|
+
|
|
1921
|
+
ctx.strokeStyle = 'rgba(228,228,231,0.8)';
|
|
1922
|
+
ctx.lineWidth = 1;
|
|
1923
|
+
var gridLines = 4;
|
|
1924
|
+
for (var gi = 0; gi <= gridLines; gi++) {
|
|
1925
|
+
var gy = pad.top + ch - (gi / gridLines) * ch;
|
|
1926
|
+
ctx.beginPath();
|
|
1927
|
+
ctx.moveTo(pad.left, gy);
|
|
1928
|
+
ctx.lineTo(pad.left + cw, gy);
|
|
1929
|
+
ctx.stroke();
|
|
1930
|
+
ctx.fillStyle = 'rgba(113,113,122,0.7)';
|
|
1931
|
+
ctx.font = '10px monospace';
|
|
1932
|
+
ctx.textAlign = 'right';
|
|
1933
|
+
ctx.fillText(fmtMs(Math.round((gi / gridLines) * maxVal)), pad.left - 8, gy + 3);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
var thresholds = [
|
|
1937
|
+
{ ms: THRESHOLD_GOOD, label: fmtMs(THRESHOLD_GOOD) },
|
|
1938
|
+
{ ms: THRESHOLD_OK, label: fmtMs(THRESHOLD_OK) }
|
|
1939
|
+
];
|
|
1940
|
+
thresholds.forEach(function(t) {
|
|
1941
|
+
if (t.ms >= maxVal) return;
|
|
1942
|
+
var ty = pad.top + ch - (t.ms / maxVal) * ch;
|
|
1943
|
+
ctx.beginPath();
|
|
1944
|
+
ctx.setLineDash([4, 4]);
|
|
1945
|
+
ctx.strokeStyle = 'rgba(113,113,122,0.3)';
|
|
1946
|
+
ctx.lineWidth = 1;
|
|
1947
|
+
ctx.moveTo(pad.left, ty);
|
|
1948
|
+
ctx.lineTo(pad.left + cw, ty);
|
|
1949
|
+
ctx.stroke();
|
|
1950
|
+
ctx.setLineDash([]);
|
|
1951
|
+
ctx.fillStyle = 'rgba(113,113,122,0.5)';
|
|
1952
|
+
ctx.font = '9px monospace';
|
|
1953
|
+
ctx.textAlign = 'left';
|
|
1954
|
+
ctx.fillText(t.label, pad.left + cw + 2, ty + 3);
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
requests.forEach(function(r, idx) {
|
|
1958
|
+
var x = requests.length === 1 ? pad.left + cw / 2 : pad.left + ((r.timestamp - minTime) / timeRange) * cw;
|
|
1959
|
+
var y = pad.top + ch - (r.durationMs / maxVal) * ch;
|
|
1960
|
+
var color = reqDotColor(r);
|
|
1961
|
+
|
|
1962
|
+
scatterDots.push({ x: x, y: y, idx: idx, r: r });
|
|
1963
|
+
|
|
1964
|
+
if (r.statusCode >= 400) {
|
|
1965
|
+
drawErrorX(ctx, x, y, 4, color, 2);
|
|
1966
|
+
} else {
|
|
1967
|
+
drawDot(ctx, x, y, 4, color);
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
ctx.fillStyle = 'rgba(113,113,122,0.7)';
|
|
1972
|
+
ctx.font = '9px monospace';
|
|
1973
|
+
ctx.textAlign = 'center';
|
|
1974
|
+
var timePoints = [minTime, minTime + timeRange / 2, maxTime];
|
|
1975
|
+
timePoints.forEach(function(t, i) {
|
|
1976
|
+
var x = pad.left + (i / 2) * cw;
|
|
1977
|
+
var d = new Date(t);
|
|
1978
|
+
ctx.fillText(d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}), x, pad.top + ch + 14);
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
canvas.style.cursor = 'pointer';
|
|
1982
|
+
canvas.onclick = function(e) {
|
|
1983
|
+
var rect = canvas.getBoundingClientRect();
|
|
1984
|
+
var mx = e.clientX - rect.left;
|
|
1985
|
+
var my = e.clientY - rect.top;
|
|
1986
|
+
var closest = null, closestDist = Infinity;
|
|
1987
|
+
scatterDots.forEach(function(d) {
|
|
1988
|
+
var dist = Math.sqrt((d.x - mx) * (d.x - mx) + (d.y - my) * (d.y - my));
|
|
1989
|
+
if (dist < closestDist) { closestDist = dist; closest = d; }
|
|
1990
|
+
});
|
|
1991
|
+
if (closest && closestDist < 16) {
|
|
1992
|
+
highlightRow(closest.idx);
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function highlightRow(reqIdx) {
|
|
1998
|
+
var prev = document.querySelector('.perf-hist-row-hl');
|
|
1999
|
+
if (prev) prev.classList.remove('perf-hist-row-hl');
|
|
2000
|
+
var row = document.querySelector('[data-req-idx="' + reqIdx + '"]');
|
|
2001
|
+
if (row) {
|
|
2002
|
+
row.classList.add('perf-hist-row-hl');
|
|
2003
|
+
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function drawInlineScatter(canvas, requests) {
|
|
2008
|
+
var setup = setupCanvas(canvas);
|
|
2009
|
+
if (!setup) return;
|
|
2010
|
+
var ctx = setup.ctx, w = setup.w, h = setup.h;
|
|
2011
|
+
if (requests.length === 0) return;
|
|
2012
|
+
|
|
2013
|
+
var padX = 4, padY = 4;
|
|
2014
|
+
var cw = w - padX * 2;
|
|
2015
|
+
var ch = h - padY * 2;
|
|
2016
|
+
|
|
2017
|
+
var maxVal = 0, minVal = Infinity;
|
|
2018
|
+
var minTime = requests[0].timestamp, maxTime = requests[0].timestamp;
|
|
2019
|
+
requests.forEach(function(r) {
|
|
2020
|
+
if (r.durationMs > maxVal) maxVal = r.durationMs;
|
|
2021
|
+
if (r.durationMs < minVal) minVal = r.durationMs;
|
|
2022
|
+
if (r.timestamp < minTime) minTime = r.timestamp;
|
|
2023
|
+
if (r.timestamp > maxTime) maxTime = r.timestamp;
|
|
2024
|
+
});
|
|
2025
|
+
maxVal = Math.max(maxVal, 10);
|
|
2026
|
+
maxVal = Math.ceil(maxVal * 1.15 / 10) * 10;
|
|
2027
|
+
var timeRange = maxTime - minTime || 1;
|
|
2028
|
+
|
|
2029
|
+
[THRESHOLD_GOOD, THRESHOLD_OK].forEach(function(ms) {
|
|
2030
|
+
if (ms >= maxVal) return;
|
|
2031
|
+
var ty = padY + ch - (ms / maxVal) * ch;
|
|
2032
|
+
ctx.beginPath();
|
|
2033
|
+
ctx.setLineDash([2, 3]);
|
|
2034
|
+
ctx.strokeStyle = 'rgba(113,113,122,0.15)';
|
|
2035
|
+
ctx.lineWidth = 1;
|
|
2036
|
+
ctx.moveTo(padX, ty);
|
|
2037
|
+
ctx.lineTo(padX + cw, ty);
|
|
2038
|
+
ctx.stroke();
|
|
2039
|
+
ctx.setLineDash([]);
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
requests.forEach(function(r) {
|
|
2043
|
+
var x = requests.length === 1 ? padX + cw / 2 : padX + ((r.timestamp - minTime) / timeRange) * cw;
|
|
2044
|
+
var y = padY + ch - (r.durationMs / maxVal) * ch;
|
|
2045
|
+
var color = reqDotColor(r);
|
|
2046
|
+
|
|
2047
|
+
if (r.statusCode >= 400) {
|
|
2048
|
+
drawErrorX(ctx, x, y, 2.5, color, 1.5);
|
|
2049
|
+
} else {
|
|
2050
|
+
drawDot(ctx, x, y, 2.5, color);
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
ctx.fillStyle = 'rgba(113,113,122,0.5)';
|
|
2055
|
+
ctx.font = '8px monospace';
|
|
2056
|
+
ctx.textAlign = 'right';
|
|
2057
|
+
ctx.fillText(fmtMs(maxVal), w - 2, padY + 8);
|
|
2058
|
+
ctx.fillText(fmtMs(0), w - 2, h - 2);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
function renderGraph() {
|
|
2063
|
+
var container = document.getElementById('graph-content');
|
|
2064
|
+
if (!container) return;
|
|
2065
|
+
container.innerHTML = '';
|
|
2066
|
+
|
|
2067
|
+
if (!graphData || graphData.length === 0) {
|
|
2068
|
+
container.innerHTML = '<div class="empty" style="height:300px"><span class="empty-title">No performance data yet</span><span class="empty-sub">Hit some endpoints and data will appear here</span></div>';
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
var selector = document.createElement('div');
|
|
2073
|
+
selector.className = 'perf-selector';
|
|
2074
|
+
|
|
2075
|
+
var allBtn = document.createElement('button');
|
|
2076
|
+
allBtn.className = 'perf-selector-btn' + (selectedEndpoint === '__all__' ? ' active' : '');
|
|
2077
|
+
allBtn.textContent = 'Overview';
|
|
2078
|
+
allBtn.addEventListener('click', function() { selectedEndpoint = '__all__'; renderGraph(); });
|
|
2079
|
+
selector.appendChild(allBtn);
|
|
2080
|
+
|
|
2081
|
+
graphData.forEach(function(ep, idx) {
|
|
2082
|
+
var btn = document.createElement('button');
|
|
2083
|
+
var color = GRAPH_COLORS[idx % GRAPH_COLORS.length];
|
|
2084
|
+
btn.className = 'perf-selector-btn' + (ep.endpoint === selectedEndpoint ? ' active' : '');
|
|
2085
|
+
btn.innerHTML = '<span class="perf-dot" style="background:' + color + '"></span>' + escHtml(ep.endpoint);
|
|
2086
|
+
btn.addEventListener('click', function() { selectedEndpoint = ep.endpoint; renderGraph(); });
|
|
2087
|
+
selector.appendChild(btn);
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
container.appendChild(selector);
|
|
2091
|
+
|
|
2092
|
+
if (selectedEndpoint === '__all__') {
|
|
2093
|
+
renderPerfOverview(container);
|
|
2094
|
+
} else {
|
|
2095
|
+
renderEndpointDetail(container);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
async function loadMetrics() {
|
|
2100
|
+
try {
|
|
2101
|
+
var res = await fetch('/__brakit/api/metrics/live');
|
|
2102
|
+
var data = await res.json();
|
|
2103
|
+
graphData = data.endpoints || [];
|
|
2104
|
+
if (!selectedEndpoint || selectedEndpoint === '__all__') {
|
|
2105
|
+
selectedEndpoint = '__all__';
|
|
2106
|
+
}
|
|
2107
|
+
renderGraph();
|
|
2108
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
function renderOverview() {
|
|
2113
|
+
var container = document.getElementById('overview-content');
|
|
2114
|
+
if (!container) return;
|
|
2115
|
+
container.innerHTML = '';
|
|
2116
|
+
|
|
2117
|
+
var nonStatic = state.requests.filter(function(r) {
|
|
2118
|
+
return !r.isStatic && (!r.path || r.path.indexOf('/__brakit') !== 0);
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
var hasData = nonStatic.length > 0 || state.queries.length > 0 || state.errors.length > 0;
|
|
2122
|
+
|
|
2123
|
+
if (!hasData) {
|
|
2124
|
+
container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see insights here</span></div>';
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
var errCount = nonStatic.filter(function(r) { return r.statusCode >= 400; }).length;
|
|
2129
|
+
var avgMs = nonStatic.length > 0 ? Math.round(nonStatic.reduce(function(s, r) { return s + r.durationMs; }, 0) / nonStatic.length) : 0;
|
|
2130
|
+
|
|
2131
|
+
var summary = document.createElement('div');
|
|
2132
|
+
summary.className = 'ov-summary';
|
|
2133
|
+
summary.innerHTML =
|
|
2134
|
+
'<div class="ov-stat"><span class="ov-stat-value">' + nonStatic.length + '</span><span class="ov-stat-label">Requests</span></div>' +
|
|
2135
|
+
'<div class="ov-stat"><span class="ov-stat-value">' + state.flows.length + '</span><span class="ov-stat-label">Actions</span></div>' +
|
|
2136
|
+
'<div class="ov-stat"><span class="ov-stat-value">' + formatDuration(avgMs) + '</span><span class="ov-stat-label">Avg Response</span></div>' +
|
|
2137
|
+
'<div class="ov-stat"><span class="ov-stat-value">' + state.queries.length + '</span><span class="ov-stat-label">Queries</span></div>' +
|
|
2138
|
+
(errCount > 0
|
|
2139
|
+
? '<div class="ov-stat"><span class="ov-stat-value" style="color:var(--red)">' + errCount + '</span><span class="ov-stat-label">Errors</span></div>'
|
|
2140
|
+
: '<div class="ov-stat"><span class="ov-stat-value" style="color:var(--green)">' + errCount + '</span><span class="ov-stat-label">Errors</span></div>') +
|
|
2141
|
+
'<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
|
|
2142
|
+
container.appendChild(summary);
|
|
2143
|
+
|
|
2144
|
+
var all = state.issues || [];
|
|
2145
|
+
var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing' || si.state === 'regressed'; });
|
|
2146
|
+
var resolved = all.filter(function(si) { return si.state === 'resolved'; });
|
|
2147
|
+
|
|
2148
|
+
if (open.length === 0 && resolved.length === 0) {
|
|
2149
|
+
var clear = document.createElement('div');
|
|
2150
|
+
clear.className = 'ov-clear';
|
|
2151
|
+
clear.innerHTML = '<span class="ov-clear-icon">\u2713</span>All clear — no issues detected';
|
|
2152
|
+
container.appendChild(clear);
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
if (open.length === 0 && resolved.length > 0) {
|
|
2157
|
+
var allFixed = document.createElement('div');
|
|
2158
|
+
allFixed.className = 'ov-clear';
|
|
2159
|
+
allFixed.innerHTML = '<span class="ov-clear-icon">\u2713</span>All issues resolved — ' + resolved.length + ' finding' + (resolved.length !== 1 ? 's were' : ' was') + ' detected and fixed';
|
|
2160
|
+
container.appendChild(allFixed);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
var NAV_LABELS = { queries: 'Queries', requests: 'Requests', actions: 'Actions', errors: 'Errors', security: 'Security', fetches: 'Fetches', logs: 'Logs', performance: 'Performance' };
|
|
2164
|
+
var SEV = { critical: { icon: '\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\u2139', cls: 'info', sort: 2 } };
|
|
2165
|
+
|
|
2166
|
+
if (open.length > 0) {
|
|
2167
|
+
var title = document.createElement('div');
|
|
2168
|
+
title.className = 'ov-section-title';
|
|
2169
|
+
title.innerHTML = 'Issues Found <span class="ov-issue-count">' + open.length + '</span>';
|
|
2170
|
+
container.appendChild(title);
|
|
2171
|
+
|
|
2172
|
+
var cards = document.createElement('div');
|
|
2173
|
+
cards.className = 'ov-cards';
|
|
2174
|
+
|
|
2175
|
+
for (var i = 0; i < open.length; i++) {
|
|
2176
|
+
(function(si) {
|
|
2177
|
+
var issue = si.issue;
|
|
2178
|
+
var card = document.createElement('div');
|
|
2179
|
+
card.className = 'ov-card';
|
|
2180
|
+
|
|
2181
|
+
var sevCfg = SEV[issue.severity];
|
|
2182
|
+
var iconCls = sevCfg.cls;
|
|
2183
|
+
var iconChar = sevCfg.icon;
|
|
2184
|
+
|
|
2185
|
+
var expandHtml = '';
|
|
2186
|
+
if (issue.detail) expandHtml += issue.detail;
|
|
2187
|
+
if (issue.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(issue.hint) + '</div>';
|
|
2188
|
+
if (issue.nav) expandHtml += '<span class="ov-card-link" data-nav="' + issue.nav + '">View in ' + (NAV_LABELS[issue.nav] || issue.nav) + ' \u2192</span>';
|
|
2189
|
+
|
|
2190
|
+
var aiBadge = '';
|
|
2191
|
+
if (si.state === 'fixing' && si.aiStatus === 'fixed') {
|
|
2192
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>';
|
|
2193
|
+
} else if (si.aiStatus === 'wont_fix') {
|
|
2194
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>';
|
|
2195
|
+
} else if (si.state === 'regressed') {
|
|
2196
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
var occBadge = si.occurrences > 1 ? ' <span class="sec-item-count">' + si.occurrences + 'x</span>' : '';
|
|
2200
|
+
|
|
2201
|
+
card.innerHTML =
|
|
2202
|
+
'<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
2203
|
+
'<div class="ov-card-body">' +
|
|
2204
|
+
'<div class="ov-card-title">' + escHtml(issue.title) + occBadge + aiBadge + '</div>' +
|
|
2205
|
+
'<div class="ov-card-desc">' + issue.desc + '</div>' +
|
|
2206
|
+
'<div class="ov-card-expand">' + expandHtml + '</div>' +
|
|
2207
|
+
'</div>' +
|
|
2208
|
+
'<span class="ov-card-arrow">\u2192</span>';
|
|
2209
|
+
|
|
2210
|
+
card.addEventListener('click', function(e) {
|
|
2211
|
+
var target = e.target;
|
|
2212
|
+
while (target && target !== card) {
|
|
2213
|
+
if (target.classList && target.classList.contains('ov-card-link')) {
|
|
2214
|
+
var navView = target.getAttribute('data-nav');
|
|
2215
|
+
var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
|
|
2216
|
+
if (sidebarItem) sidebarItem.click();
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
target = target.parentElement;
|
|
2220
|
+
}
|
|
2221
|
+
var expand = card.querySelector('.ov-card-expand');
|
|
2222
|
+
var arrow = card.querySelector('.ov-card-arrow');
|
|
2223
|
+
if (card.classList.contains('expanded')) {
|
|
2224
|
+
card.classList.remove('expanded');
|
|
2225
|
+
expand.style.display = 'none';
|
|
2226
|
+
arrow.textContent = '\u2192';
|
|
2227
|
+
} else {
|
|
2228
|
+
card.classList.add('expanded');
|
|
2229
|
+
expand.style.display = 'block';
|
|
2230
|
+
arrow.textContent = '\u2193';
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
cards.appendChild(card);
|
|
2235
|
+
})(open[i]);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
container.appendChild(cards);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (resolved.length > 0) {
|
|
2242
|
+
var resolvedTitle = document.createElement('div');
|
|
2243
|
+
resolvedTitle.className = 'ov-section-title ov-resolved-title';
|
|
2244
|
+
resolvedTitle.innerHTML = '<span style="color:var(--green)">\u2713</span> Resolved <span class="ov-issue-count">' + resolved.length + '</span>';
|
|
2245
|
+
container.appendChild(resolvedTitle);
|
|
2246
|
+
|
|
2247
|
+
var resolvedCards = document.createElement('div');
|
|
2248
|
+
resolvedCards.className = 'ov-cards';
|
|
2249
|
+
|
|
2250
|
+
for (var ri = 0; ri < resolved.length; ri++) {
|
|
2251
|
+
var rIssue = resolved[ri].issue;
|
|
2252
|
+
var rCard = document.createElement('div');
|
|
2253
|
+
rCard.className = 'ov-card ov-card-resolved';
|
|
2254
|
+
rCard.innerHTML =
|
|
2255
|
+
'<span class="ov-card-icon resolved">\u2713</span>' +
|
|
2256
|
+
'<div class="ov-card-body">' +
|
|
2257
|
+
'<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rIssue.title) + '</div>' +
|
|
2258
|
+
'<div class="ov-card-desc">' + rIssue.desc + '</div>' +
|
|
2259
|
+
'</div>';
|
|
2260
|
+
resolvedCards.appendChild(rCard);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
container.appendChild(resolvedCards);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
function renderSecurity() {
|
|
2269
|
+
var container = document.getElementById('security-content');
|
|
2270
|
+
if (!container) return;
|
|
2271
|
+
container.innerHTML = '';
|
|
2272
|
+
var SEV = { critical: { icon: '\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\u2139', cls: 'info', sort: 2 } };
|
|
2273
|
+
|
|
2274
|
+
var all = (state.issues || []).slice();
|
|
2275
|
+
var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing' || f.state === 'regressed'; });
|
|
2276
|
+
var resolved = all.filter(function(f) { return f.state === 'resolved'; });
|
|
2277
|
+
var stale = all.filter(function(f) { return f.state === 'stale'; });
|
|
2278
|
+
|
|
2279
|
+
if (open.length === 0 && resolved.length === 0 && stale.length === 0) {
|
|
2280
|
+
var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
|
|
2281
|
+
if (!hasData) {
|
|
2282
|
+
container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
|
|
2283
|
+
} else {
|
|
2284
|
+
container.innerHTML = '<div class="sec-clear"><span class="sec-clear-icon">\u2713</span><div class="sec-clear-text"><div class="sec-clear-title">All clear</div><div class="sec-clear-sub">No security or quality issues detected this session</div></div></div>';
|
|
2285
|
+
}
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
var critCount = 0, warnCount = 0, infoCount = 0;
|
|
2290
|
+
for (var ci = 0; ci < open.length; ci++) {
|
|
2291
|
+
var sev = open[ci].issue.severity;
|
|
2292
|
+
if (sev === 'critical') critCount++;
|
|
2293
|
+
else if (sev === 'info') infoCount++;
|
|
2294
|
+
else warnCount++;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
var summaryEl = document.createElement('div');
|
|
2298
|
+
summaryEl.className = 'sec-summary';
|
|
2299
|
+
summaryEl.innerHTML =
|
|
2300
|
+
'<div class="sec-summary-left">' +
|
|
2301
|
+
'<span class="sec-summary-count">' + open.length + '</span>' +
|
|
2302
|
+
'<span class="sec-summary-label">open issue' + (open.length !== 1 ? 's' : '') + '</span>' +
|
|
2303
|
+
(resolved.length > 0 ? '<span class="sec-resolved-badge">' + resolved.length + ' resolved</span>' : '') +
|
|
2304
|
+
'</div>' +
|
|
2305
|
+
'<div class="sec-summary-right">' +
|
|
2306
|
+
(critCount > 0 ? '<span class="sec-badge critical">' + critCount + ' critical</span>' : '') +
|
|
2307
|
+
(warnCount > 0 ? '<span class="sec-badge warning">' + warnCount + ' warning</span>' : '') +
|
|
2308
|
+
(infoCount > 0 ? '<span class="sec-badge info">' + infoCount + ' info</span>' : '') +
|
|
2309
|
+
'</div>';
|
|
2310
|
+
container.appendChild(summaryEl);
|
|
2311
|
+
|
|
2312
|
+
if (open.length === 0 && resolved.length > 0) {
|
|
2313
|
+
var allFixed = document.createElement('div');
|
|
2314
|
+
allFixed.className = 'sec-clear';
|
|
2315
|
+
allFixed.innerHTML = '<span class="sec-clear-icon">\u2713</span><div class="sec-clear-text"><div class="sec-clear-title">All issues resolved</div><div class="sec-clear-sub">' + resolved.length + ' finding' + (resolved.length !== 1 ? 's were' : ' was') + ' detected and fixed</div></div>';
|
|
2316
|
+
container.appendChild(allFixed);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (open.length > 0) {
|
|
2320
|
+
var groups = {};
|
|
2321
|
+
var groupOrder = [];
|
|
2322
|
+
for (var gi = 0; gi < open.length; gi++) {
|
|
2323
|
+
var sf = open[gi];
|
|
2324
|
+
var f = sf.issue;
|
|
2325
|
+
if (!groups[f.rule]) {
|
|
2326
|
+
groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
|
|
2327
|
+
groupOrder.push(f.rule);
|
|
2328
|
+
}
|
|
2329
|
+
groups[f.rule].items.push(sf);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
groupOrder.sort(function(a, b) {
|
|
2333
|
+
var sa = SEV[groups[a].severity].sort;
|
|
2334
|
+
var sb = SEV[groups[b].severity].sort;
|
|
2335
|
+
if (sa !== sb) return sa - sb;
|
|
2336
|
+
return groups[b].items.length - groups[a].items.length;
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
for (var oi = 0; oi < groupOrder.length; oi++) {
|
|
2340
|
+
var group = groups[groupOrder[oi]];
|
|
2341
|
+
var section = document.createElement('div');
|
|
2342
|
+
section.className = 'sec-group';
|
|
2343
|
+
|
|
2344
|
+
var sevCfg = SEV[group.severity];
|
|
2345
|
+
var iconCls = sevCfg.cls;
|
|
2346
|
+
var iconChar = sevCfg.icon;
|
|
2347
|
+
|
|
2348
|
+
var header = document.createElement('div');
|
|
2349
|
+
header.className = 'sec-group-header';
|
|
2350
|
+
header.innerHTML =
|
|
2351
|
+
'<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
2352
|
+
'<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
|
|
2353
|
+
'<span class="sec-group-count">' + group.items.length + '</span>';
|
|
2354
|
+
section.appendChild(header);
|
|
2355
|
+
|
|
2356
|
+
if (group.hint) {
|
|
2357
|
+
var hintEl = document.createElement('div');
|
|
2358
|
+
hintEl.className = 'sec-hint';
|
|
2359
|
+
hintEl.textContent = group.hint;
|
|
2360
|
+
section.appendChild(hintEl);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
var list = document.createElement('div');
|
|
2364
|
+
list.className = 'sec-items';
|
|
2365
|
+
for (var ii = 0; ii < group.items.length; ii++) {
|
|
2366
|
+
var sf2 = group.items[ii];
|
|
2367
|
+
var item = sf2.issue;
|
|
2368
|
+
var row = document.createElement('div');
|
|
2369
|
+
row.className = 'sec-item';
|
|
2370
|
+
var aiBadge = '';
|
|
2371
|
+
if (sf2.state === 'fixing' && sf2.aiStatus === 'fixed') {
|
|
2372
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \u2014 awaiting verification</span>';
|
|
2373
|
+
} else if (sf2.aiStatus === 'wont_fix') {
|
|
2374
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\u2019t fix</span>';
|
|
2375
|
+
} else if (sf2.state === 'regressed') {
|
|
2376
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
|
|
2377
|
+
}
|
|
2378
|
+
var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
|
|
2379
|
+
var occBadge = sf2.occurrences > 1 ? '<span class="sec-item-count">' + sf2.occurrences + 'x</span>' : '';
|
|
2380
|
+
row.innerHTML =
|
|
2381
|
+
'<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
|
|
2382
|
+
occBadge +
|
|
2383
|
+
aiBadge + aiNotes;
|
|
2384
|
+
list.appendChild(row);
|
|
2385
|
+
}
|
|
2386
|
+
section.appendChild(list);
|
|
2387
|
+
container.appendChild(section);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
if (resolved.length > 0) {
|
|
2392
|
+
var resolvedTitle = document.createElement('div');
|
|
2393
|
+
resolvedTitle.className = 'sec-resolved-title';
|
|
2394
|
+
resolvedTitle.innerHTML = '<span class="sec-resolved-check">\u2713</span> Resolved <span class="sec-resolved-count">' + resolved.length + '</span>';
|
|
2395
|
+
container.appendChild(resolvedTitle);
|
|
2396
|
+
|
|
2397
|
+
var resolvedGroup = document.createElement('div');
|
|
2398
|
+
resolvedGroup.className = 'sec-group sec-group-resolved';
|
|
2399
|
+
var resolvedItems = document.createElement('div');
|
|
2400
|
+
resolvedItems.className = 'sec-items';
|
|
2401
|
+
for (var ri = 0; ri < resolved.length; ri++) {
|
|
2402
|
+
var rsf = resolved[ri];
|
|
2403
|
+
var rf = rsf.issue;
|
|
2404
|
+
var rRow = document.createElement('div');
|
|
2405
|
+
rRow.className = 'sec-item sec-item-resolved';
|
|
2406
|
+
var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
|
|
2407
|
+
var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
|
|
2408
|
+
rRow.innerHTML =
|
|
2409
|
+
'<span class="sec-resolved-item-icon">\u2713</span>' +
|
|
2410
|
+
'<div class="sec-item-desc">' + escHtml(rf.title) + ' \u2014 ' + escHtml(rf.endpoint || 'global') + '</div>' +
|
|
2411
|
+
verifiedBadge + rNotes;
|
|
2412
|
+
resolvedItems.appendChild(rRow);
|
|
2413
|
+
}
|
|
2414
|
+
resolvedGroup.appendChild(resolvedItems);
|
|
2415
|
+
container.appendChild(resolvedGroup);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
if (stale.length > 0) {
|
|
2419
|
+
var staleTitle = document.createElement('div');
|
|
2420
|
+
staleTitle.className = 'sec-resolved-title';
|
|
2421
|
+
staleTitle.innerHTML = '<span style="color:var(--text-muted)">\u23F8</span> Stale <span class="sec-resolved-count">' + stale.length + '</span>';
|
|
2422
|
+
container.appendChild(staleTitle);
|
|
2423
|
+
|
|
2424
|
+
var staleGroup = document.createElement('div');
|
|
2425
|
+
staleGroup.className = 'sec-group sec-group-resolved';
|
|
2426
|
+
var staleItems = document.createElement('div');
|
|
2427
|
+
staleItems.className = 'sec-items';
|
|
2428
|
+
for (var sti = 0; sti < stale.length; sti++) {
|
|
2429
|
+
var ssf = stale[sti];
|
|
2430
|
+
var sf3 = ssf.issue;
|
|
2431
|
+
var sRow = document.createElement('div');
|
|
2432
|
+
sRow.className = 'sec-item sec-item-resolved';
|
|
2433
|
+
sRow.innerHTML =
|
|
2434
|
+
'<span style="color:var(--text-muted)">\u23F8</span>' +
|
|
2435
|
+
'<div class="sec-item-desc" style="color:var(--text-muted)">' + escHtml(sf3.title) + ' \u2014 endpoint inactive</div>';
|
|
2436
|
+
staleItems.appendChild(sRow);
|
|
2437
|
+
}
|
|
2438
|
+
staleGroup.appendChild(staleItems);
|
|
2439
|
+
container.appendChild(staleGroup);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
|
|
2444
|
+
var VIEW_CONTAINERS = {
|
|
2445
|
+
overview: 'overview-container',
|
|
2446
|
+
actions: 'flow-container',
|
|
2447
|
+
requests: 'request-container',
|
|
2448
|
+
fetches: 'fetch-container',
|
|
2449
|
+
queries: 'query-container',
|
|
2450
|
+
errors: 'error-container',
|
|
2451
|
+
logs: 'log-container',
|
|
2452
|
+
performance: 'performance-container',
|
|
2453
|
+
security: 'security-container'
|
|
2454
|
+
};
|
|
2455
|
+
var VIEW_TITLES = {
|
|
2456
|
+
overview: 'Overview',
|
|
2457
|
+
actions: 'Actions',
|
|
2458
|
+
requests: 'Requests',
|
|
2459
|
+
fetches: 'Server Fetches',
|
|
2460
|
+
queries: 'Queries',
|
|
2461
|
+
errors: 'Errors',
|
|
2462
|
+
logs: 'Logs',
|
|
2463
|
+
performance: 'Performance',
|
|
2464
|
+
security: 'Security'
|
|
2465
|
+
};
|
|
2466
|
+
var VIEW_SUBTITLES = {
|
|
2467
|
+
overview: 'Live summary of your application',
|
|
2468
|
+
actions: 'User actions captured as sequences of HTTP requests',
|
|
2469
|
+
requests: 'All HTTP requests proxied through brakit',
|
|
2470
|
+
fetches: 'Outbound HTTP calls made by your server to external services',
|
|
2471
|
+
queries: 'Database queries executed during request handling',
|
|
2472
|
+
errors: 'Unhandled exceptions and errors thrown by your application',
|
|
2473
|
+
logs: 'Console output from your application',
|
|
2474
|
+
performance: 'Endpoint health and response time trends',
|
|
2475
|
+
security: 'Security findings and recommendations'
|
|
2476
|
+
};
|
|
2477
|
+
|
|
2478
|
+
async function init() {
|
|
2479
|
+
try {
|
|
2480
|
+
var res = await fetch('/__brakit/api/flows');
|
|
2481
|
+
var data = await res.json();
|
|
2482
|
+
state.flows = data.flows;
|
|
2483
|
+
renderFlows();
|
|
2484
|
+
} catch(e) { console.error('Failed to load flows', e); }
|
|
2485
|
+
|
|
2486
|
+
try {
|
|
2487
|
+
var res2 = await fetch('/__brakit/api/requests');
|
|
2488
|
+
var data2 = await res2.json();
|
|
2489
|
+
state.requests = data2.requests;
|
|
2490
|
+
renderRequests();
|
|
2491
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
2492
|
+
|
|
2493
|
+
await Promise.all([loadFetches(), loadErrors(), loadLogs(), loadQueries(), loadMetrics()]);
|
|
2494
|
+
|
|
2495
|
+
try {
|
|
2496
|
+
var res3 = await fetch('/__brakit/api/insights');
|
|
2497
|
+
var data3 = await res3.json();
|
|
2498
|
+
state.issues = data3.issues || [];
|
|
2499
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
2500
|
+
|
|
2501
|
+
updateStats();
|
|
2502
|
+
renderOverview();
|
|
2503
|
+
|
|
2504
|
+
var events = new EventSource('/__brakit/api/events');
|
|
2505
|
+
var reloadTimer = null;
|
|
2506
|
+
var perfReloadTimer = null;
|
|
2507
|
+
events.onmessage = function(e) {
|
|
2508
|
+
var req = JSON.parse(e.data);
|
|
2509
|
+
if (req.path && req.path.startsWith('/__brakit')) return;
|
|
2510
|
+
state.requests.unshift(req);
|
|
2511
|
+
if (state.requests.length > 1000) state.requests.pop();
|
|
2512
|
+
clearTimeout(reloadTimer);
|
|
2513
|
+
reloadTimer = setTimeout(reloadFlows, 300);
|
|
2514
|
+
prependRequestRow(req);
|
|
2515
|
+
updateStats();
|
|
2516
|
+
if (state.activeView === 'performance') {
|
|
2517
|
+
clearTimeout(perfReloadTimer);
|
|
2518
|
+
perfReloadTimer = setTimeout(loadMetrics, 500);
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2521
|
+
|
|
2522
|
+
function registerTelemetryListener(eventName, stateKey, prependFn) {
|
|
2523
|
+
events.addEventListener(eventName, function(e) {
|
|
2524
|
+
var item = JSON.parse(e.data);
|
|
2525
|
+
state[stateKey].unshift(item);
|
|
2526
|
+
if (state[stateKey].length > 1000) state[stateKey].pop();
|
|
2527
|
+
prependFn(item);
|
|
2528
|
+
updateStats();
|
|
2529
|
+
if (item.parentRequestId) { invalidateTimelineCache(item.parentRequestId); refreshVisibleTimeline(item.parentRequestId); }
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
registerTelemetryListener('fetch', 'fetches', prependFetchRow);
|
|
2533
|
+
registerTelemetryListener('log', 'logs', prependLogRow);
|
|
2534
|
+
registerTelemetryListener('error_event', 'errors', prependErrorRow);
|
|
2535
|
+
registerTelemetryListener('query', 'queries', prependQueryRow);
|
|
2536
|
+
|
|
2537
|
+
events.addEventListener('issues', function(e) {
|
|
2538
|
+
state.issues = JSON.parse(e.data);
|
|
2539
|
+
if (state.activeView === 'overview') renderOverview();
|
|
2540
|
+
if (state.activeView === 'security') renderSecurity();
|
|
2541
|
+
updateStats();
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
window.addEventListener('beforeunload', function() {
|
|
2545
|
+
events.close();
|
|
2546
|
+
clearTimeout(reloadTimer);
|
|
2547
|
+
clearTimeout(perfReloadTimer);
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
async function reloadFlows() {
|
|
2552
|
+
try {
|
|
2553
|
+
var res = await fetch('/__brakit/api/flows');
|
|
2554
|
+
var data = await res.json();
|
|
2555
|
+
state.flows = data.flows;
|
|
2556
|
+
renderFlows();
|
|
2557
|
+
updateStats();
|
|
2558
|
+
} catch(e) { console.warn('[brakit]', e); }
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
function switchView(view) {
|
|
2562
|
+
Object.keys(VIEW_CONTAINERS).forEach(function(v) {
|
|
2563
|
+
var el = document.getElementById(VIEW_CONTAINERS[v]);
|
|
2564
|
+
if (el) el.style.display = v === view ? 'block' : 'none';
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
var sidebarItems = document.querySelectorAll('.sidebar-item:not(.disabled)');
|
|
2569
|
+
sidebarItems.forEach(function(item) {
|
|
2570
|
+
item.addEventListener('click', function() {
|
|
2571
|
+
var view = item.getAttribute('data-view');
|
|
2572
|
+
if (!view || view === state.activeView) return;
|
|
2573
|
+
sidebarItems.forEach(function(i) { i.classList.remove('active'); });
|
|
2574
|
+
item.classList.add('active');
|
|
2575
|
+
state.activeView = view;
|
|
2576
|
+
fetch('/__brakit/api/tab?tab=' + encodeURIComponent(view)).catch(function(){});
|
|
2577
|
+
document.getElementById('header-title').textContent = VIEW_TITLES[view] || view;
|
|
2578
|
+
document.getElementById('header-sub').textContent = VIEW_SUBTITLES[view] || '';
|
|
2579
|
+
document.getElementById('mode-toggle').style.display = view === 'actions' ? 'flex' : 'none';
|
|
2580
|
+
if (view === 'overview') renderOverview();
|
|
2581
|
+
if (view === 'security') renderSecurity();
|
|
2582
|
+
if (view === 'performance') loadMetrics();
|
|
2583
|
+
switchView(view);
|
|
2584
|
+
});
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
document.getElementById('mode-simple').addEventListener('click', function() {
|
|
2588
|
+
state.viewMode = 'simple';
|
|
2589
|
+
document.getElementById('mode-simple').classList.add('active');
|
|
2590
|
+
document.getElementById('mode-detailed').classList.remove('active');
|
|
2591
|
+
collapseAll('.flow-row', '.flow-expand');
|
|
2592
|
+
});
|
|
2593
|
+
document.getElementById('mode-detailed').addEventListener('click', function() {
|
|
2594
|
+
state.viewMode = 'detailed';
|
|
2595
|
+
document.getElementById('mode-detailed').classList.add('active');
|
|
2596
|
+
document.getElementById('mode-simple').classList.remove('active');
|
|
2597
|
+
collapseAll('.flow-row', '.flow-expand');
|
|
2598
|
+
});
|
|
2599
|
+
|
|
2600
|
+
function updateStats() {
|
|
2601
|
+
var reqs = state.requests.filter(function(r) { return !r.path || !r.path.startsWith('/__brakit'); });
|
|
2602
|
+
var errors = reqs.filter(function(r) { return r.statusCode >= 400; }).length;
|
|
2603
|
+
var avg = reqs.length > 0 ? Math.round(reqs.reduce(function(s,r) { return s + r.durationMs; }, 0) / reqs.length) : 0;
|
|
2604
|
+
document.getElementById('stat-total').textContent = reqs.length + ' request' + (reqs.length !== 1 ? 's' : '');
|
|
2605
|
+
document.getElementById('stat-flows').textContent = state.flows.length + ' action' + (state.flows.length !== 1 ? 's' : '');
|
|
2606
|
+
document.getElementById('stat-errors').textContent = errors + ' error' + (errors !== 1 ? 's' : '');
|
|
2607
|
+
document.getElementById('stat-avg').textContent = 'Avg: ' + avg + 'ms';
|
|
2608
|
+
var actionCount = document.getElementById('sidebar-count-actions');
|
|
2609
|
+
var requestCount = document.getElementById('sidebar-count-requests');
|
|
2610
|
+
var fetchCount = document.getElementById('sidebar-count-fetches');
|
|
2611
|
+
var errorCount = document.getElementById('sidebar-count-errors');
|
|
2612
|
+
var logCount = document.getElementById('sidebar-count-logs');
|
|
2613
|
+
var queryCount = document.getElementById('sidebar-count-queries');
|
|
2614
|
+
if (actionCount) actionCount.textContent = state.flows.length;
|
|
2615
|
+
if (requestCount) requestCount.textContent = reqs.length;
|
|
2616
|
+
if (fetchCount) fetchCount.textContent = state.fetches.length;
|
|
2617
|
+
if (errorCount) errorCount.textContent = state.errors.length;
|
|
2618
|
+
if (logCount) logCount.textContent = state.logs.length;
|
|
2619
|
+
if (queryCount) queryCount.textContent = state.queries.length;
|
|
2620
|
+
var secCount = document.getElementById('sidebar-count-security');
|
|
2621
|
+
if (secCount) {
|
|
2622
|
+
var numIssues = (state.issues || []).filter(function(f) { return f.state !== 'resolved' && f.state !== 'stale'; }).length;
|
|
2623
|
+
secCount.textContent = numIssues;
|
|
2624
|
+
secCount.style.display = numIssues > 0 ? '' : 'none';
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function copyAsCurl(req) {
|
|
2629
|
+
var headers = Object.entries(req.headers || {})
|
|
2630
|
+
.filter(function(e) { return ['host', 'connection', 'accept-encoding'].indexOf(e[0]) === -1; })
|
|
2631
|
+
.map(function(e) { return "-H '" + e[0] + ": " + e[1] + "'"; })
|
|
2632
|
+
.join(' ');
|
|
2633
|
+
var body = req.requestBody ? " -d '" + req.requestBody.replace(/'/g, "'\\''") + "'" : '';
|
|
2634
|
+
var curl = "curl -X " + req.method + " " + headers + body + " 'http://localhost:" + PORT + req.url + "'";
|
|
2635
|
+
navigator.clipboard.writeText(curl).then(function() { showToast('Copied cURL command'); });
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
document.getElementById('clear-btn').addEventListener('click', async function() {
|
|
2639
|
+
if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
|
|
2640
|
+
await fetch('/__brakit/api/clear', {method: 'POST'});
|
|
2641
|
+
state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
|
|
2642
|
+
state.issues = [];
|
|
2643
|
+
graphData = []; selectedEndpoint = '__all__'; timelineCache = {};
|
|
2644
|
+
renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
|
|
2645
|
+
showToast('Cleared');
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
init();
|
|
2649
|
+
|
|
2650
|
+
})();
|
|
2651
|
+
</script>
|
|
2652
|
+
</body>
|
|
2653
|
+
</html>
|