@zerotal/devtools 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +233 -1
  2. package/api-surface.md +296 -0
  3. package/package.json +5 -4
  4. package/src/DevtoolsInjectionMiddleware.ts +41 -4
  5. package/src/RequestTrace.ts +96 -1
  6. package/src/TraceStore.ts +12 -0
  7. package/src/activity.ts +116 -0
  8. package/src/callsite.ts +146 -0
  9. package/src/client/filter.ts +108 -0
  10. package/src/client/index.ts +122 -0
  11. package/src/client/metrics.ts +98 -0
  12. package/src/client/registry.ts +65 -0
  13. package/src/client/state.ts +311 -0
  14. package/src/client/tabs/all.ts +276 -0
  15. package/src/client/tabs/app.ts +292 -0
  16. package/src/client/tabs/cache.ts +49 -0
  17. package/src/client/tabs/channel.ts +263 -0
  18. package/src/client/tabs/exceptions.ts +68 -0
  19. package/src/client/tabs/jobs.ts +50 -0
  20. package/src/client/tabs/logs.ts +44 -0
  21. package/src/client/tabs/mail.ts +59 -0
  22. package/src/client/tabs/queries.ts +124 -0
  23. package/src/client/tabs/request.ts +76 -0
  24. package/src/client/tabs/timeline.ts +132 -0
  25. package/src/client/tabs/types.ts +51 -0
  26. package/src/client/transport.ts +81 -0
  27. package/src/client/tree.ts +138 -0
  28. package/src/client/ui/format.ts +118 -0
  29. package/src/client/ui/render.ts +87 -0
  30. package/src/client/ui/shell.ts +511 -0
  31. package/src/client/ui/theme.ts +389 -0
  32. package/src/client-auto.ts +1 -1
  33. package/src/config.ts +77 -2
  34. package/src/dashboard-auto.ts +1 -1
  35. package/src/editor.ts +107 -0
  36. package/src/enabled.ts +59 -0
  37. package/src/index.ts +19 -3
  38. package/src/map.ts +213 -0
  39. package/src/provider/DevtoolsProvider.ts +32 -7
  40. package/src/redaction.ts +161 -20
  41. package/src/tracing.ts +213 -24
  42. package/src/client.ts +0 -1048
  43. package/src/panel-app.js +0 -519
package/src/panel-app.js DELETED
@@ -1,519 +0,0 @@
1
- /* @zerotal/devtools — browser panel injected by DevtoolsInjectionMiddleware */
2
- (function () {
3
- "use strict";
4
-
5
- const T = window.__ZT_DT__;
6
- if (!T) return;
7
-
8
- // ── Styles (shadow DOM — fully isolated from host page) ──────────────────
9
- const STYLES = `<style>
10
- :host {
11
- --bg: #1a1b26;
12
- --surf: #24283b;
13
- --card: #2f3452;
14
- --border: #3b4261;
15
- --text: #c0caf5;
16
- --muted: #565f89;
17
- --purple: #7aa2f7;
18
- --green: #9ece6a;
19
- --yellow: #e0af68;
20
- --red: #f7768e;
21
- --orange: #ff9e64;
22
- --cyan: #7dcfff;
23
- }
24
- * { box-sizing: border-box; margin: 0; padding: 0; }
25
- #wrap {
26
- display: flex; flex-direction: column;
27
- font: 12px/1.5 'JetBrains Mono','Fira Code','SF Mono',ui-monospace,monospace;
28
- color: var(--text);
29
- }
30
- #bar {
31
- display: flex; align-items: center; gap: 8px;
32
- height: 32px; padding: 0 12px;
33
- background: var(--bg); border-top: 1px solid var(--border);
34
- cursor: pointer; user-select: none; flex-shrink: 0;
35
- }
36
- #bar:hover { background: var(--surf); }
37
- .logo { color: var(--purple); font-weight: 700; }
38
- .divider { color: var(--border); }
39
- .flex1 { flex: 1; }
40
- .dim { color: var(--muted); }
41
- .path { color: var(--text); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
42
- .icon-btn {
43
- background: none; border: none; color: var(--muted);
44
- cursor: pointer; font: inherit; font-size: 14px; padding: 0 4px; line-height: 1;
45
- }
46
- .icon-btn:hover { color: var(--text); }
47
- .tog { font-size: 10px; }
48
- .meth {
49
- font-size: 10px; font-weight: 700; padding: 2px 6px;
50
- border-radius: 3px; letter-spacing: 0.3px;
51
- }
52
- .get { background: #1d2d50; color: var(--purple); }
53
- .post { background: #1e3328; color: var(--green); }
54
- .put, .patch { background: #362b18; color: var(--yellow); }
55
- .delete { background: #361e22; color: var(--red); }
56
- .head, .options { background: var(--card); color: var(--muted); }
57
- .sc { font-weight: 700; font-size: 12px; }
58
- .green { color: var(--green); }
59
- .yellow { color: var(--yellow); }
60
- .red { color: var(--red); }
61
- .orange { color: var(--orange); }
62
- .cyan { color: var(--cyan); }
63
- .chip-n1 {
64
- background: #362314; color: var(--orange);
65
- font-size: 10px; font-weight: 700;
66
- padding: 1px 6px; border-radius: 10px;
67
- }
68
- .chip-user {
69
- background: var(--card); color: var(--cyan);
70
- font-size: 10px; padding: 1px 7px; border-radius: 10px;
71
- max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
72
- }
73
- #panel {
74
- flex-direction: column; height: 380px;
75
- background: var(--bg); border-top: 1px solid var(--border);
76
- }
77
- #tabs {
78
- display: flex; flex-shrink: 0;
79
- background: var(--surf); border-bottom: 1px solid var(--border);
80
- }
81
- .tab {
82
- background: none; border: none; border-bottom: 2px solid transparent;
83
- color: var(--muted); cursor: pointer; font: inherit; font-size: 12px;
84
- padding: 7px 14px;
85
- }
86
- .tab:hover { color: var(--text); }
87
- .tab.active { color: var(--text); border-bottom-color: var(--purple); }
88
- .tab-badge {
89
- display: inline-block; background: var(--card); color: var(--muted);
90
- font-size: 10px; padding: 0 5px; border-radius: 8px; margin-left: 4px;
91
- }
92
- .tab-badge.warn { background: #362314; color: var(--orange); }
93
- #content {
94
- flex: 1; overflow-y: auto; padding: 12px 16px;
95
- display: flex; flex-direction: column; gap: 12px;
96
- }
97
- .stats { display: flex; flex-wrap: wrap; gap: 16px; }
98
- .stat { display: flex; flex-direction: column; gap: 2px; }
99
- .slbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.6px; }
100
- .sval { font-size: 15px; font-weight: 700; }
101
- .section { display: flex; flex-direction: column; gap: 6px; }
102
- .sec-title { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.8px; }
103
- .route-card {
104
- background: var(--surf); border: 1px solid var(--border);
105
- border-radius: 5px; padding: 8px 10px;
106
- display: flex; flex-direction: column; gap: 3px;
107
- }
108
- .route-pattern { color: var(--cyan); font-size: 12px; }
109
- .route-action { color: var(--muted); font-size: 11px; }
110
- .qrow {
111
- background: var(--surf); border: 1px solid var(--border);
112
- border-radius: 5px; padding: 8px 10px;
113
- }
114
- .qmeta { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
115
- .qdur { font-size: 11px; font-weight: 700; min-width: 44px; }
116
- .qbar { flex: 1; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
117
- .qfill { height: 100%; background: var(--purple); border-radius: 2px; }
118
- .qrc { font-size: 11px; color: var(--muted); flex-shrink: 0; }
119
- .qsql { font-size: 11px; color: var(--text); word-break: break-all; line-height: 1.6; }
120
- .qbind { font-size: 11px; margin-top: 4px; }
121
- .bind { color: var(--orange); }
122
- .warn-row {
123
- background: #261b10; border: 1px solid var(--orange);
124
- border-radius: 5px; padding: 8px 10px;
125
- }
126
- .warn-head { font-size: 11px; font-weight: 700; color: var(--orange); margin-bottom: 5px; }
127
- .warn-fix { font-size: 11px; color: var(--muted); margin-top: 5px; }
128
- .warn-fix code { color: var(--cyan); }
129
- .log-row {
130
- display: flex; gap: 8px; padding: 4px 0;
131
- border-bottom: 1px solid var(--border); align-items: flex-start;
132
- }
133
- .log-row:last-child { border-bottom: none; }
134
- .log-time { color: var(--muted); font-size: 11px; flex-shrink: 0; min-width: 42px; }
135
- .log-lvl { font-size: 10px; font-weight: 700; flex-shrink: 0; min-width: 36px; text-transform: uppercase; }
136
- .log-msg { font-size: 11px; word-break: break-all; flex: 1; line-height: 1.5; }
137
- .lvl-log { color: var(--muted); }
138
- .lvl-debug { color: var(--muted); }
139
- .lvl-info { color: var(--cyan); }
140
- .lvl-warn { color: var(--yellow); }
141
- .lvl-error { color: var(--red); }
142
- .kv-table { width: 100%; border-collapse: collapse; }
143
- .kv-table tr:nth-child(odd) td { background: var(--surf); }
144
- .kv-table td { padding: 3px 8px; vertical-align: top; word-break: break-all; }
145
- .kv-key { color: var(--purple); white-space: nowrap; padding-right: 12px; min-width: 140px; }
146
- .kv-val { color: var(--text); }
147
- .hrow {
148
- display: flex; align-items: center; gap: 8px;
149
- padding: 5px 8px; border-radius: 4px;
150
- }
151
- .hrow:nth-child(odd) { background: var(--surf); }
152
- .hrow.cur { background: #1d2d50; }
153
- .hpath { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
154
- .empty { color: var(--muted); font-size: 12px; padding: 16px 0; }
155
- ::-webkit-scrollbar { width: 5px; }
156
- ::-webkit-scrollbar-track { background: transparent; }
157
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
158
- ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
159
- </style>`;
160
-
161
- // ── Mount ────────────────────────────────────────────────────────────────
162
- const host = document.createElement("div");
163
- host.id = "__zerotal_dt__";
164
- host.style.cssText =
165
- "position:fixed;bottom:0;left:0;right:0;z-index:2147483647;pointer-events:none";
166
- document.body.appendChild(host);
167
-
168
- const shadow = host.attachShadow({ mode: "open" });
169
- shadow.innerHTML =
170
- STYLES +
171
- `
172
- <div id="wrap" style="pointer-events:auto">
173
- <div id="panel" style="display:none">
174
- <div id="tabs">
175
- <button id="tab-cur" class="tab active">Queries<span id="badge-q" class="tab-badge"></span></button>
176
- <button id="tab-logs" class="tab">Logs<span id="badge-l" class="tab-badge"></span></button>
177
- <button id="tab-req" class="tab">Request</button>
178
- <button id="tab-hist" class="tab">History</button>
179
- </div>
180
- <div id="content"></div>
181
- </div>
182
- <div id="bar"></div>
183
- </div>`;
184
-
185
- // ── State ────────────────────────────────────────────────────────────────
186
- let open = false;
187
- let tab = "cur";
188
- let hist = null;
189
- let histErr = false;
190
-
191
- // Populate tab badges once
192
- q("#badge-q").textContent = T.queries.length;
193
- if (T.warnings.length) q("#badge-q").classList.add("warn");
194
- if ((T.logs || []).length) {
195
- q("#badge-l").textContent = T.logs.length;
196
- const hasErr = T.logs.some(function (l) {
197
- return l.level === "error" || l.level === "warn";
198
- });
199
- if (hasErr) q("#badge-l").classList.add("warn");
200
- }
201
-
202
- // ── Wire events ──────────────────────────────────────────────────────────
203
- q("#bar").addEventListener("click", function (e) {
204
- if (e.target.closest && e.target.closest("#btn-close")) return;
205
- toggle();
206
- });
207
- q("#tab-cur").addEventListener("click", function () {
208
- setTab("cur");
209
- });
210
- q("#tab-logs").addEventListener("click", function () {
211
- setTab("logs");
212
- });
213
- q("#tab-req").addEventListener("click", function () {
214
- setTab("req");
215
- });
216
- q("#tab-hist").addEventListener("click", function () {
217
- setTab("hist");
218
- });
219
- document.addEventListener("keydown", function (e) {
220
- if ((e.altKey || e.metaKey) && e.key === "d") {
221
- e.preventDefault();
222
- toggle();
223
- }
224
- });
225
-
226
- // ── Boot ─────────────────────────────────────────────────────────────────
227
- renderBar();
228
-
229
- // ── Core ─────────────────────────────────────────────────────────────────
230
- function toggle() {
231
- open = !open;
232
- q("#panel").style.display = open ? "flex" : "none";
233
- if (open) renderPanel();
234
- renderBar();
235
- }
236
-
237
- function setTab(t) {
238
- tab = t;
239
- q("#tab-cur").classList.toggle("active", t === "cur");
240
- q("#tab-logs").classList.toggle("active", t === "logs");
241
- q("#tab-req").classList.toggle("active", t === "req");
242
- q("#tab-hist").classList.toggle("active", t === "hist");
243
- if (t === "hist" && hist === null && !histErr) fetchHistory();
244
- renderPanel();
245
- }
246
-
247
- // ── Toolbar ───────────────────────────────────────────────────────────────
248
- function renderBar() {
249
- const sc = scls(T.statusCode);
250
- const dc = dcls(T.durationMs);
251
- const n1 = T.warnings.length ? `<span class="chip-n1">⚠ N+1 ×${T.warnings.length}</span>` : "";
252
- const mem = T.memory
253
- ? `<span class="divider">·</span><span class="dim">${fmtMem(T.memory)}</span>`
254
- : "";
255
- const user = T.auth
256
- ? `<span class="chip-user">👤 ${esc(String(T.auth.name || T.auth.email || T.auth.id))}</span>`
257
- : `<span class="dim" style="font-size:11px">guest</span>`;
258
- const arr = open ? "▼" : "▲";
259
- q("#bar").innerHTML = `<span class="logo">⬡ <b>Zerotal</b></span>
260
- <span class="divider">|</span>
261
- <span class="meth ${T.method.toLowerCase()}">${esc(T.method)}</span>
262
- <span class="path">${esc(T.path)}</span>
263
- <span class="divider">|</span>
264
- <span class="sc ${sc}">${T.statusCode || "—"}</span>
265
- <span class="divider">·</span>
266
- <span class="${dc}">${fmt(T.durationMs)}</span>
267
- <span class="divider">·</span>
268
- <span class="dim">${T.queries.length}&thinsp;quer${T.queries.length === 1 ? "y" : "ies"}</span>
269
- ${mem}
270
- <span class="divider">·</span>
271
- ${user}
272
- ${n1}
273
- <span class="flex1"></span>
274
- <button id="btn-close" class="icon-btn" title="Hide (Alt+D)">×</button>
275
- <button class="icon-btn tog" title="Toggle panel">${arr}</button>`;
276
-
277
- q("#btn-close").addEventListener("click", function (e) {
278
- e.stopPropagation();
279
- host.remove();
280
- });
281
- }
282
-
283
- // ── Panel ─────────────────────────────────────────────────────────────────
284
- function renderPanel() {
285
- if (tab === "cur") renderQueries();
286
- else if (tab === "logs") renderLogs();
287
- else if (tab === "req") renderRequest();
288
- else renderHistory();
289
- }
290
-
291
- function renderQueries() {
292
- const total = T.queries.reduce(function (s, q) {
293
- return s + q.durationMs;
294
- }, 0);
295
- const peak = T.queries.reduce(function (m, q) {
296
- return Math.max(m, q.durationMs);
297
- }, 1);
298
-
299
- const routeCard = T.route
300
- ? `<div class="route-card">
301
- <div class="route-pattern">${esc(T.method)} <strong>${esc(T.route.pattern)}</strong></div>
302
- <div class="route-action dim">${esc(T.route.controller)}@${esc(T.route.action)}</div>
303
- </div>`
304
- : "";
305
-
306
- const authCard = T.auth
307
- ? `<div class="stat">
308
- <div class="slbl">User</div>
309
- <div class="sval cyan" style="font-size:13px">${esc(String(T.auth.name || T.auth.email || T.auth.id))}</div>
310
- ${T.auth.email ? `<div class="dim" style="font-size:10px">${esc(String(T.auth.email))}</div>` : ""}
311
- </div>`
312
- : `<div class="stat"><div class="slbl">User</div><div class="sval dim" style="font-size:13px">Guest</div></div>`;
313
-
314
- const qRows =
315
- T.queries.length === 0
316
- ? '<p class="empty">No queries recorded</p>'
317
- : T.queries
318
- .map(function (qr) {
319
- const pct = Math.round((qr.durationMs / peak) * 100);
320
- const dc = qr.durationMs < 10 ? "green" : qr.durationMs < 100 ? "yellow" : "red";
321
- const bindings =
322
- qr.bindings && qr.bindings.length
323
- ? `<div class="qbind"><span class="dim">bindings:</span> ${qr.bindings
324
- .map(function (v) {
325
- return v === null || v === undefined
326
- ? '<span class="bind">null</span>'
327
- : typeof v === "string"
328
- ? `<span class="bind">'${esc(v)}'</span>`
329
- : `<span class="bind">${esc(String(v))}</span>`;
330
- })
331
- .join('<span class="dim">, </span>')}</div>`
332
- : "";
333
- return `<div class="qrow">
334
- <div class="qmeta">
335
- <span class="qdur ${dc}">${fmt(qr.durationMs)}</span>
336
- <div class="qbar"><div class="qfill" style="width:${pct}%"></div></div>
337
- <span class="qrc">${qr.rowCount}&thinsp;row${qr.rowCount !== 1 ? "s" : ""}</span>
338
- </div>
339
- <div class="qsql">${esc(qr.sql)}</div>
340
- ${bindings}
341
- </div>`;
342
- })
343
- .join("");
344
-
345
- const wRows = T.warnings
346
- .map(function (w) {
347
- return `<div class="warn-row">
348
- <div class="warn-head">⚠ Executed <strong>${w.count}×</strong> in this request — N+1 detected</div>
349
- <div class="qsql">${esc(w.sql)}</div>
350
- <div class="warn-fix">Fix: use <code>.with('relation')</code>
351
- &nbsp;·&nbsp; suppress: <code>DB.allowNPlusOne('${esc(tableFrom(w.sql))}')</code></div>
352
- </div>`;
353
- })
354
- .join("");
355
-
356
- q("#content").innerHTML = `${routeCard ? `<div class="section">${routeCard}</div>` : ""}
357
- <div class="stats">
358
- <div class="stat"><div class="slbl">Duration</div>
359
- <div class="sval ${dcls(T.durationMs)}">${fmt(T.durationMs)}</div></div>
360
- <div class="stat"><div class="slbl">Queries</div>
361
- <div class="sval">${T.queries.length}</div></div>
362
- <div class="stat"><div class="slbl">Query time</div>
363
- <div class="sval">${fmt(total)}</div></div>
364
- ${
365
- T.memory
366
- ? `<div class="stat"><div class="slbl">Memory</div>
367
- <div class="sval">${fmtMem(T.memory)}</div></div>`
368
- : ""
369
- }
370
- ${
371
- T.warnings.length
372
- ? `<div class="stat"><div class="slbl">N+1</div>
373
- <div class="sval orange">${T.warnings.length}</div></div>`
374
- : ""
375
- }
376
- ${authCard}
377
- </div>
378
- <div class="section">
379
- <div class="sec-title">Queries (${T.queries.length})</div>${qRows}
380
- </div>
381
- ${
382
- T.warnings.length
383
- ? `<div class="section"><div class="sec-title">⚠ N+1 Warnings</div>${wRows}</div>`
384
- : ""
385
- }`;
386
- }
387
-
388
- function renderLogs() {
389
- const logs = T.logs || [];
390
- if (logs.length === 0) {
391
- q("#content").innerHTML = '<p class="empty">No console output captured for this request</p>';
392
- return;
393
- }
394
- q("#content").innerHTML = `<div class="section">
395
- <div class="sec-title">Console (${logs.length})</div>
396
- <div>${logs
397
- .map(function (l) {
398
- return `<div class="log-row">
399
- <span class="log-time dim">+${l.offsetMs}ms</span>
400
- <span class="log-lvl lvl-${esc(l.level)}">${esc(l.level)}</span>
401
- <span class="log-msg">${l.args
402
- .map(function (a) {
403
- return esc(a);
404
- })
405
- .join(" ")}</span>
406
- </div>`;
407
- })
408
- .join("")}</div>
409
- </div>`;
410
- }
411
-
412
- function renderRequest() {
413
- const qp = T.queryParams || {};
414
- const hdrs = T.headers || {};
415
- const qpKeys = Object.keys(qp);
416
- const hdrKeys = Object.keys(hdrs);
417
-
418
- const qpRows =
419
- qpKeys.length === 0
420
- ? '<tr><td colspan="2" class="dim" style="padding:6px 8px">No query parameters</td></tr>'
421
- : qpKeys
422
- .map(function (k) {
423
- return `<tr><td class="kv-key">${esc(k)}</td><td class="kv-val">${esc(qp[k])}</td></tr>`;
424
- })
425
- .join("");
426
-
427
- const hdrRows =
428
- hdrKeys.length === 0
429
- ? '<tr><td colspan="2" class="dim" style="padding:6px 8px">—</td></tr>'
430
- : hdrKeys
431
- .map(function (k) {
432
- return `<tr><td class="kv-key">${esc(k)}</td><td class="kv-val">${esc(hdrs[k])}</td></tr>`;
433
- })
434
- .join("");
435
-
436
- q("#content").innerHTML = `<div class="section">
437
- <div class="sec-title">Query Parameters (${qpKeys.length})</div>
438
- <table class="kv-table">${qpRows}</table>
439
- </div>
440
- <div class="section">
441
- <div class="sec-title">Request Headers</div>
442
- <table class="kv-table">${hdrRows}</table>
443
- </div>`;
444
- }
445
-
446
- function renderHistory() {
447
- if (hist === null) {
448
- q("#content").innerHTML = '<p class="empty">Loading…</p>';
449
- return;
450
- }
451
- const rows =
452
- histErr || !hist.length
453
- ? `<p class="empty">${histErr ? "Failed to load history" : "No requests recorded yet"}</p>`
454
- : hist
455
- .map(function (t) {
456
- const cur = t.id === T.id ? " cur" : "";
457
- return `<div class="hrow${cur}">
458
- <span class="meth ${t.method.toLowerCase()}">${esc(t.method)}</span>
459
- <span class="hpath">${esc(t.path)}</span>
460
- <span class="sc ${scls(t.statusCode)}">${t.statusCode || "—"}</span>
461
- <span class="${dcls(t.durationMs)}">${fmt(t.durationMs)}</span>
462
- <span class="dim">${t.queries.length}q</span>
463
- ${t.logs && t.logs.length ? `<span class="dim">${t.logs.length}log</span>` : ""}
464
- ${t.warnings.length ? '<span class="chip-n1">N+1</span>' : ""}
465
- </div>`;
466
- })
467
- .join("");
468
- q("#content").innerHTML = rows;
469
- }
470
-
471
- function fetchHistory() {
472
- fetch("/__zerotal/devtools/api/traces")
473
- .then(function (r) {
474
- return r.json();
475
- })
476
- .then(function (data) {
477
- hist = data;
478
- if (tab === "hist") renderPanel();
479
- })
480
- .catch(function () {
481
- histErr = true;
482
- if (tab === "hist") renderPanel();
483
- });
484
- }
485
-
486
- // ── Helpers ───────────────────────────────────────────────────────────────
487
- function q(sel) {
488
- return shadow.querySelector(sel);
489
- }
490
- function esc(s) {
491
- return String(s).replace(/[&<>"']/g, function (c) {
492
- return {
493
- "&": "&amp;",
494
- "<": "&lt;",
495
- ">": "&gt;",
496
- '"': "&quot;",
497
- "'": "&#39;",
498
- }[c];
499
- });
500
- }
501
- function fmt(ms) {
502
- return ms >= 1000 ? (ms / 1000).toFixed(1) + "s" : ms + "ms";
503
- }
504
- function fmtMem(bytes) {
505
- return bytes >= 1048576
506
- ? (bytes / 1048576).toFixed(1) + " MB"
507
- : (bytes / 1024).toFixed(0) + " KB";
508
- }
509
- function scls(s) {
510
- return s >= 500 ? "red" : s >= 400 ? "yellow" : s >= 300 ? "cyan" : s ? "green" : "dim";
511
- }
512
- function dcls(ms) {
513
- return ms > 1000 ? "red" : ms > 300 ? "yellow" : "";
514
- }
515
- function tableFrom(sql) {
516
- const m = /from\s+[`'"]?(\w+)[`'"]?/i.exec(sql);
517
- return m ? m[1] : "table_name";
518
- }
519
- })();