binja 0.6.0 → 0.6.1
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/dist/debug/index.js +427 -384
- package/dist/debug/panel.d.ts +10 -8
- package/dist/debug/panel.d.ts.map +1 -1
- package/dist/index.js +427 -384
- package/package.json +1 -1
package/dist/debug/index.js
CHANGED
|
@@ -220,473 +220,516 @@ function endDebugCollection() {
|
|
|
220
220
|
|
|
221
221
|
// src/debug/panel.ts
|
|
222
222
|
var DEFAULT_OPTIONS = {
|
|
223
|
-
position: "bottom
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
position: "bottom",
|
|
224
|
+
height: 300,
|
|
225
|
+
width: 400,
|
|
226
|
+
open: false,
|
|
227
|
+
dark: true
|
|
227
228
|
};
|
|
228
229
|
function generateDebugPanel(data, options = {}) {
|
|
229
230
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
230
|
-
const id = `binja-
|
|
231
|
-
const
|
|
231
|
+
const id = `binja-dbg-${Date.now()}`;
|
|
232
|
+
const c = opts.dark ? darkTheme : lightTheme;
|
|
232
233
|
return `
|
|
233
234
|
<!-- Binja Debug Panel -->
|
|
234
|
-
<div id="${id}" class="binja-
|
|
235
|
-
<style>${generateStyles(id,
|
|
236
|
-
${
|
|
237
|
-
|
|
238
|
-
<script>${generateScript(id)}</script>
|
|
235
|
+
<div id="${id}" class="binja-devtools" data-position="${opts.position}" data-open="${opts.open}">
|
|
236
|
+
<style>${generateStyles(id, c, opts)}</style>
|
|
237
|
+
${generateHTML(id, data, c, opts)}
|
|
238
|
+
<script>${generateScript(id, data, opts)}</script>
|
|
239
239
|
</div>
|
|
240
240
|
<!-- /Binja Debug Panel -->
|
|
241
241
|
`;
|
|
242
242
|
}
|
|
243
243
|
var darkTheme = {
|
|
244
|
-
bg: "#
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
text: "#
|
|
250
|
-
textSecondary: "#
|
|
251
|
-
textMuted: "#
|
|
252
|
-
accent: "#
|
|
253
|
-
accentHover: "#
|
|
254
|
-
success: "#
|
|
255
|
-
warning: "#
|
|
256
|
-
error: "#
|
|
257
|
-
info: "#
|
|
244
|
+
bg: "#1e1e1e",
|
|
245
|
+
bgPanel: "#252526",
|
|
246
|
+
bgHover: "#2a2d2e",
|
|
247
|
+
bgActive: "#37373d",
|
|
248
|
+
border: "#3c3c3c",
|
|
249
|
+
text: "#cccccc",
|
|
250
|
+
textSecondary: "#969696",
|
|
251
|
+
textMuted: "#6e6e6e",
|
|
252
|
+
accent: "#0078d4",
|
|
253
|
+
accentHover: "#1c86d8",
|
|
254
|
+
success: "#4ec9b0",
|
|
255
|
+
warning: "#dcdcaa",
|
|
256
|
+
error: "#f14c4c",
|
|
257
|
+
info: "#75beff",
|
|
258
|
+
string: "#ce9178",
|
|
259
|
+
number: "#b5cea8",
|
|
260
|
+
keyword: "#569cd6"
|
|
258
261
|
};
|
|
259
262
|
var lightTheme = {
|
|
260
|
-
bg: "#
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
text: "#
|
|
266
|
-
textSecondary: "#
|
|
267
|
-
textMuted: "#
|
|
268
|
-
accent: "#
|
|
269
|
-
accentHover: "#
|
|
270
|
-
success: "#
|
|
271
|
-
warning: "#
|
|
272
|
-
error: "#
|
|
273
|
-
info: "#
|
|
263
|
+
bg: "#f3f3f3",
|
|
264
|
+
bgPanel: "#ffffff",
|
|
265
|
+
bgHover: "#e8e8e8",
|
|
266
|
+
bgActive: "#d4d4d4",
|
|
267
|
+
border: "#d4d4d4",
|
|
268
|
+
text: "#1e1e1e",
|
|
269
|
+
textSecondary: "#616161",
|
|
270
|
+
textMuted: "#a0a0a0",
|
|
271
|
+
accent: "#0078d4",
|
|
272
|
+
accentHover: "#106ebe",
|
|
273
|
+
success: "#16825d",
|
|
274
|
+
warning: "#bf8803",
|
|
275
|
+
error: "#cd3131",
|
|
276
|
+
info: "#0078d4",
|
|
277
|
+
string: "#a31515",
|
|
278
|
+
number: "#098658",
|
|
279
|
+
keyword: "#0000ff"
|
|
274
280
|
};
|
|
275
281
|
function generateStyles(id, c, opts) {
|
|
276
|
-
const pos = getPosition(opts.position);
|
|
277
282
|
return `
|
|
278
|
-
#${id} { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
283
|
+
#${id} { --bg: ${c.bg}; --bg-panel: ${c.bgPanel}; --bg-hover: ${c.bgHover}; --bg-active: ${c.bgActive}; --border: ${c.border}; --text: ${c.text}; --text-secondary: ${c.textSecondary}; --text-muted: ${c.textMuted}; --accent: ${c.accent}; --success: ${c.success}; --warning: ${c.warning}; --error: ${c.error}; --string: ${c.string}; --number: ${c.number}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-size: 12px; line-height: 1.4; color: var(--text); }
|
|
279
284
|
#${id} * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
#${id} .
|
|
283
|
-
#${id} .
|
|
284
|
-
#${id} .
|
|
285
|
-
#${id} .
|
|
286
|
-
#${id} .
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
#${id} .
|
|
290
|
-
#${id} .
|
|
291
|
-
#${id} .
|
|
292
|
-
#${id} .
|
|
293
|
-
#${id} .
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
#${id} .
|
|
297
|
-
#${id} .
|
|
298
|
-
#${id} .
|
|
299
|
-
#${id} .
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
#${id} .
|
|
303
|
-
#${id} .
|
|
304
|
-
#${id} .
|
|
305
|
-
#${id} .
|
|
306
|
-
#${id} .
|
|
307
|
-
#${id} .
|
|
308
|
-
#${id} .
|
|
309
|
-
#${id} .
|
|
310
|
-
#${id} .
|
|
311
|
-
#${id} .
|
|
312
|
-
#${id} .
|
|
313
|
-
#${id} .
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
#${id} .
|
|
317
|
-
#${id} .
|
|
318
|
-
#${id} .
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
#${id} .
|
|
322
|
-
#${id} .
|
|
323
|
-
#${id} .
|
|
324
|
-
#${id} .
|
|
325
|
-
#${id} .
|
|
326
|
-
#${id} .
|
|
327
|
-
#${id} .
|
|
328
|
-
#${id} .
|
|
329
|
-
#${id} .
|
|
330
|
-
#${id} .
|
|
331
|
-
#${id} .
|
|
332
|
-
#${id} .
|
|
333
|
-
#${id} .
|
|
334
|
-
#${id} .
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
#${id} .
|
|
338
|
-
#${id} .
|
|
339
|
-
#${id} .
|
|
340
|
-
#${id} .
|
|
341
|
-
#${id} .
|
|
342
|
-
#${id} .
|
|
343
|
-
#${id} .
|
|
344
|
-
#${id} .
|
|
345
|
-
#${id} .
|
|
346
|
-
#${id} .
|
|
347
|
-
#${id} .
|
|
348
|
-
#${id} .
|
|
349
|
-
#${id} .
|
|
350
|
-
#${id} .
|
|
351
|
-
#${id} .
|
|
352
|
-
#${id} .
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
#${id} .
|
|
356
|
-
#${id} .
|
|
357
|
-
#${id} .
|
|
358
|
-
#${id} .
|
|
359
|
-
#${id} .
|
|
360
|
-
#${id} .
|
|
361
|
-
#${id} .
|
|
362
|
-
#${id} .
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
#${id} .
|
|
366
|
-
#${id} .
|
|
367
|
-
#${id} .
|
|
368
|
-
#${id} .
|
|
369
|
-
#${id} .
|
|
370
|
-
#${id} .
|
|
371
|
-
#${id} .
|
|
285
|
+
|
|
286
|
+
/* Toggle Button */
|
|
287
|
+
#${id} .devtools-toggle { position: fixed; bottom: 10px; right: 10px; z-index: 2147483646; display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text); cursor: pointer; font-size: 11px; font-weight: 500; box-shadow: 0 2px 8px rgba(0,0,0,0.2); transition: all 0.15s; }
|
|
288
|
+
#${id} .devtools-toggle:hover { background: var(--bg-hover); border-color: var(--accent); }
|
|
289
|
+
#${id} .devtools-toggle svg { width: 14px; height: 14px; color: var(--accent); }
|
|
290
|
+
#${id} .devtools-toggle .badge { padding: 2px 6px; background: var(--accent); color: #fff; border-radius: 3px; font-size: 10px; }
|
|
291
|
+
#${id}[data-open="true"] .devtools-toggle { display: none; }
|
|
292
|
+
|
|
293
|
+
/* Panel Container */
|
|
294
|
+
#${id} .devtools-panel { display: none; position: fixed; z-index: 2147483647; background: var(--bg); border: 1px solid var(--border); box-shadow: 0 -2px 12px rgba(0,0,0,0.3); }
|
|
295
|
+
#${id}[data-open="true"] .devtools-panel { display: flex; flex-direction: column; }
|
|
296
|
+
#${id}[data-position="bottom"] .devtools-panel { left: 0; right: 0; bottom: 0; height: ${opts.height}px; border-left: none; border-right: none; border-bottom: none; }
|
|
297
|
+
#${id}[data-position="right"] .devtools-panel { top: 0; right: 0; bottom: 0; width: ${opts.width}px; border-top: none; border-right: none; border-bottom: none; }
|
|
298
|
+
#${id}[data-position="popup"] .devtools-panel { bottom: 50px; right: 20px; width: 700px; height: 500px; border-radius: 8px; }
|
|
299
|
+
|
|
300
|
+
/* Resize Handle */
|
|
301
|
+
#${id} .devtools-resize { position: absolute; background: transparent; }
|
|
302
|
+
#${id}[data-position="bottom"] .devtools-resize { top: 0; left: 0; right: 0; height: 4px; cursor: ns-resize; }
|
|
303
|
+
#${id}[data-position="right"] .devtools-resize { top: 0; left: 0; bottom: 0; width: 4px; cursor: ew-resize; }
|
|
304
|
+
#${id} .devtools-resize:hover { background: var(--accent); }
|
|
305
|
+
|
|
306
|
+
/* Toolbar */
|
|
307
|
+
#${id} .devtools-toolbar { display: flex; align-items: center; justify-content: space-between; height: 30px; padding: 0 8px; background: var(--bg-panel); border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
308
|
+
#${id} .devtools-tabs { display: flex; height: 100%; }
|
|
309
|
+
#${id} .devtools-tab { display: flex; align-items: center; gap: 4px; padding: 0 12px; border: none; background: none; color: var(--text-secondary); font-size: 11px; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.1s; }
|
|
310
|
+
#${id} .devtools-tab:hover { color: var(--text); background: var(--bg-hover); }
|
|
311
|
+
#${id} .devtools-tab.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
312
|
+
#${id} .devtools-tab svg { width: 12px; height: 12px; opacity: 0.7; }
|
|
313
|
+
#${id} .devtools-tab .count { margin-left: 4px; padding: 1px 5px; background: var(--bg-active); border-radius: 8px; font-size: 10px; }
|
|
314
|
+
#${id} .devtools-tab .count.warn { background: rgba(241,76,76,0.2); color: var(--error); }
|
|
315
|
+
#${id} .devtools-actions { display: flex; align-items: center; gap: 4px; }
|
|
316
|
+
#${id} .devtools-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; background: none; color: var(--text-secondary); cursor: pointer; border-radius: 3px; }
|
|
317
|
+
#${id} .devtools-btn:hover { background: var(--bg-hover); color: var(--text); }
|
|
318
|
+
#${id} .devtools-btn svg { width: 14px; height: 14px; }
|
|
319
|
+
|
|
320
|
+
/* Content Area */
|
|
321
|
+
#${id} .devtools-content { flex: 1; overflow: hidden; }
|
|
322
|
+
#${id} .devtools-pane { display: none; height: 100%; overflow: auto; padding: 8px; }
|
|
323
|
+
#${id} .devtools-pane.active { display: block; }
|
|
324
|
+
|
|
325
|
+
/* Performance Tab */
|
|
326
|
+
#${id} .perf-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 12px; }
|
|
327
|
+
#${id} .perf-card { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 10px; text-align: center; }
|
|
328
|
+
#${id} .perf-card-value { font-size: 20px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; color: var(--success); }
|
|
329
|
+
#${id} .perf-card-label { font-size: 10px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; }
|
|
330
|
+
#${id} .perf-breakdown { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
331
|
+
#${id} .perf-row { display: flex; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
|
332
|
+
#${id} .perf-row:last-child { border-bottom: none; }
|
|
333
|
+
#${id} .perf-row-label { flex: 1; font-size: 11px; color: var(--text-secondary); }
|
|
334
|
+
#${id} .perf-row-value { font-family: 'SF Mono', Monaco, monospace; font-size: 11px; min-width: 60px; text-align: right; }
|
|
335
|
+
#${id} .perf-row-bar { flex: 2; height: 4px; background: var(--bg-active); border-radius: 2px; margin: 0 12px; overflow: hidden; }
|
|
336
|
+
#${id} .perf-row-fill { height: 100%; border-radius: 2px; }
|
|
337
|
+
#${id} .perf-row-fill.lexer { background: ${c.info}; }
|
|
338
|
+
#${id} .perf-row-fill.parser { background: ${c.warning}; }
|
|
339
|
+
#${id} .perf-row-fill.render { background: ${c.success}; }
|
|
340
|
+
|
|
341
|
+
/* Context Tab - Tree View */
|
|
342
|
+
#${id} .tree { font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 11px; }
|
|
343
|
+
#${id} .tree-item { }
|
|
344
|
+
#${id} .tree-row { display: flex; align-items: center; padding: 2px 4px; cursor: default; border-radius: 2px; }
|
|
345
|
+
#${id} .tree-row:hover { background: var(--bg-hover); }
|
|
346
|
+
#${id} .tree-row.expandable { cursor: pointer; }
|
|
347
|
+
#${id} .tree-arrow { width: 12px; height: 12px; color: var(--text-muted); transition: transform 0.1s; flex-shrink: 0; }
|
|
348
|
+
#${id} .tree-item.open > .tree-row .tree-arrow { transform: rotate(90deg); }
|
|
349
|
+
#${id} .tree-key { color: var(--keyword); margin-right: 4px; }
|
|
350
|
+
#${id} .tree-colon { color: var(--text-muted); margin-right: 6px; }
|
|
351
|
+
#${id} .tree-value { color: var(--text); }
|
|
352
|
+
#${id} .tree-value.string { color: var(--string); }
|
|
353
|
+
#${id} .tree-value.number { color: var(--number); }
|
|
354
|
+
#${id} .tree-value.null { color: var(--text-muted); font-style: italic; }
|
|
355
|
+
#${id} .tree-type { color: var(--text-muted); margin-left: 6px; font-size: 10px; }
|
|
356
|
+
#${id} .tree-children { display: none; padding-left: 16px; }
|
|
357
|
+
#${id} .tree-item.open > .tree-children { display: block; }
|
|
358
|
+
|
|
359
|
+
/* Templates Tab */
|
|
360
|
+
#${id} .template-list { display: flex; flex-direction: column; gap: 4px; }
|
|
361
|
+
#${id} .template-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; }
|
|
362
|
+
#${id} .template-icon { width: 14px; height: 14px; color: var(--text-muted); }
|
|
363
|
+
#${id} .template-name { font-family: 'SF Mono', Monaco, monospace; font-size: 11px; }
|
|
364
|
+
#${id} .template-badge { font-size: 9px; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; font-weight: 500; }
|
|
365
|
+
#${id} .template-badge.root { background: rgba(0,120,212,0.15); color: var(--accent); }
|
|
366
|
+
#${id} .template-badge.extends { background: rgba(156,86,246,0.15); color: #9c56f6; }
|
|
367
|
+
#${id} .template-badge.include { background: rgba(78,201,176,0.15); color: var(--success); }
|
|
368
|
+
|
|
369
|
+
/* Queries Tab */
|
|
370
|
+
#${id} .queries-stats { display: flex; gap: 16px; padding: 8px 12px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; margin-bottom: 8px; }
|
|
371
|
+
#${id} .queries-stat { text-align: center; }
|
|
372
|
+
#${id} .queries-stat-value { font-size: 16px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; }
|
|
373
|
+
#${id} .queries-stat-value.warn { color: var(--warning); }
|
|
374
|
+
#${id} .queries-stat-value.error { color: var(--error); }
|
|
375
|
+
#${id} .queries-stat-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; }
|
|
376
|
+
#${id} .query-list { display: flex; flex-direction: column; gap: 4px; }
|
|
377
|
+
#${id} .query-item { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
378
|
+
#${id} .query-item.n1 { border-left: 3px solid var(--error); }
|
|
379
|
+
#${id} .query-item.slow { border-left: 3px solid var(--warning); }
|
|
380
|
+
#${id} .query-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; }
|
|
381
|
+
#${id} .query-sql { flex: 1; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
|
|
382
|
+
#${id} .query-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
383
|
+
#${id} .query-badge { font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600; }
|
|
384
|
+
#${id} .query-badge.n1 { background: rgba(241,76,76,0.15); color: var(--error); }
|
|
385
|
+
#${id} .query-source { font-size: 9px; padding: 2px 5px; border-radius: 3px; background: var(--bg-active); color: var(--text-muted); text-transform: uppercase; }
|
|
386
|
+
#${id} .query-time { font-family: 'SF Mono', Monaco, monospace; font-size: 10px; color: var(--text-secondary); }
|
|
387
|
+
#${id} .query-time.slow { color: var(--warning); }
|
|
388
|
+
#${id} .query-rows { font-size: 10px; color: var(--text-muted); }
|
|
389
|
+
|
|
390
|
+
/* Filters Tab */
|
|
391
|
+
#${id} .filter-grid { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
392
|
+
#${id} .filter-chip { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; }
|
|
393
|
+
#${id} .filter-count { color: var(--accent); font-weight: 600; font-size: 10px; }
|
|
394
|
+
|
|
395
|
+
/* Cache Tab */
|
|
396
|
+
#${id} .cache-stats { display: flex; gap: 20px; }
|
|
397
|
+
#${id} .cache-stat { flex: 1; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 16px; text-align: center; }
|
|
398
|
+
#${id} .cache-value { font-size: 32px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; }
|
|
399
|
+
#${id} .cache-value.hit { color: var(--success); }
|
|
400
|
+
#${id} .cache-value.miss { color: var(--error); }
|
|
401
|
+
#${id} .cache-label { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
402
|
+
|
|
403
|
+
/* Warnings Tab */
|
|
404
|
+
#${id} .warning-list { display: flex; flex-direction: column; gap: 6px; }
|
|
405
|
+
#${id} .warning-item { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(241,76,76,0.08); border: 1px solid rgba(241,76,76,0.2); border-radius: 4px; }
|
|
406
|
+
#${id} .warning-icon { color: var(--error); flex-shrink: 0; width: 14px; height: 14px; }
|
|
407
|
+
#${id} .warning-text { font-size: 11px; color: var(--text); }
|
|
408
|
+
|
|
409
|
+
/* Empty State */
|
|
410
|
+
#${id} .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-muted); font-size: 12px; }
|
|
411
|
+
#${id} .empty-state svg { width: 32px; height: 32px; margin-bottom: 8px; opacity: 0.5; }
|
|
372
412
|
`;
|
|
373
413
|
}
|
|
374
|
-
function getPosition(pos) {
|
|
375
|
-
switch (pos) {
|
|
376
|
-
case "bottom-left":
|
|
377
|
-
return "bottom: 16px; left: 16px;";
|
|
378
|
-
case "top-right":
|
|
379
|
-
return "top: 16px; right: 16px;";
|
|
380
|
-
case "top-left":
|
|
381
|
-
return "top: 16px; left: 16px;";
|
|
382
|
-
default:
|
|
383
|
-
return "bottom: 16px; right: 16px;";
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
414
|
var icons = {
|
|
387
415
|
logo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`,
|
|
388
416
|
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`,
|
|
389
|
-
|
|
417
|
+
minimize: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`,
|
|
418
|
+
dock: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 15h18"/></svg>`,
|
|
419
|
+
dockRight: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></svg>`,
|
|
420
|
+
popup: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v6h6"/></svg>`,
|
|
421
|
+
arrow: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>`,
|
|
390
422
|
perf: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>`,
|
|
391
|
-
template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>`,
|
|
392
423
|
context: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/></svg>`,
|
|
424
|
+
template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>`,
|
|
393
425
|
filter: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`,
|
|
426
|
+
database: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>`,
|
|
394
427
|
cache: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1010 10H12V2z"/><path d="M12 2a10 10 0 00-8.66 15"/></svg>`,
|
|
395
|
-
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg
|
|
396
|
-
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg>`,
|
|
397
|
-
database: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>`
|
|
428
|
+
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`
|
|
398
429
|
};
|
|
399
|
-
function
|
|
430
|
+
function generateHTML(id, data, c, opts) {
|
|
400
431
|
const time = (data.totalTime || 0).toFixed(1);
|
|
432
|
+
const queryCount = data.queries?.length || 0;
|
|
433
|
+
const hasWarnings = data.warnings.length > 0 || data.queryStats?.n1Count > 0;
|
|
434
|
+
const tabs = [
|
|
435
|
+
{ id: "perf", icon: icons.perf, label: "Performance", count: `${time}ms` },
|
|
436
|
+
{ id: "context", icon: icons.context, label: "Context", count: Object.keys(data.contextSnapshot).length || null },
|
|
437
|
+
{ id: "templates", icon: icons.template, label: "Templates", count: data.templateChain.length || null },
|
|
438
|
+
{ id: "filters", icon: icons.filter, label: "Filters", count: data.filtersUsed.size || null },
|
|
439
|
+
{ id: "queries", icon: icons.database, label: "Queries", count: queryCount || null, warn: data.queryStats?.n1Count > 0 },
|
|
440
|
+
{ id: "cache", icon: icons.cache, label: "Cache", count: data.cacheHits + data.cacheMisses || null },
|
|
441
|
+
{ id: "warnings", icon: icons.warning, label: "Warnings", count: data.warnings.length || null, warn: hasWarnings }
|
|
442
|
+
];
|
|
443
|
+
const tabsHtml = tabs.map((t, i) => {
|
|
444
|
+
const countHtml = t.count !== null ? `<span class="count${t.warn ? " warn" : ""}">${t.count}</span>` : "";
|
|
445
|
+
return `<button class="devtools-tab${i === 0 ? " active" : ""}" data-tab="${t.id}">${t.icon}${t.label}${countHtml}</button>`;
|
|
446
|
+
}).join("");
|
|
401
447
|
return `
|
|
402
|
-
<button class="
|
|
448
|
+
<button class="devtools-toggle" onclick="document.getElementById('${id}').dataset.open='true'">
|
|
403
449
|
${icons.logo}
|
|
404
450
|
<span>Binja</span>
|
|
405
|
-
<span class="
|
|
406
|
-
</button
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
<div class="
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<
|
|
417
|
-
<span class="dbg-badge time">${time}ms</span>
|
|
418
|
-
<button class="dbg-close" onclick="document.querySelector('#${id} .dbg-panel').classList.remove('open');document.querySelector('#${id} .dbg-toggle').style.display='inline-flex'">${icons.close}</button>
|
|
451
|
+
<span class="badge">${time}ms</span>
|
|
452
|
+
</button>
|
|
453
|
+
|
|
454
|
+
<div class="devtools-panel">
|
|
455
|
+
<div class="devtools-resize"></div>
|
|
456
|
+
<div class="devtools-toolbar">
|
|
457
|
+
<div class="devtools-tabs">${tabsHtml}</div>
|
|
458
|
+
<div class="devtools-actions">
|
|
459
|
+
<button class="devtools-btn" title="Dock to bottom" data-dock="bottom">${icons.dock}</button>
|
|
460
|
+
<button class="devtools-btn" title="Dock to right" data-dock="right">${icons.dockRight}</button>
|
|
461
|
+
<button class="devtools-btn" title="Popup" data-dock="popup">${icons.popup}</button>
|
|
462
|
+
<button class="devtools-btn" title="Close" onclick="document.getElementById('${id}').dataset.open='false'">${icons.close}</button>
|
|
419
463
|
</div>
|
|
420
464
|
</div>
|
|
421
|
-
<div class="
|
|
422
|
-
${
|
|
423
|
-
${
|
|
424
|
-
${
|
|
425
|
-
${
|
|
426
|
-
${
|
|
427
|
-
${
|
|
428
|
-
${
|
|
465
|
+
<div class="devtools-content">
|
|
466
|
+
${generatePerfPane(data)}
|
|
467
|
+
${generateContextPane(data)}
|
|
468
|
+
${generateTemplatesPane(data)}
|
|
469
|
+
${generateFiltersPane(data)}
|
|
470
|
+
${generateQueriesPane(data)}
|
|
471
|
+
${generateCachePane(data)}
|
|
472
|
+
${generateWarningsPane(data)}
|
|
429
473
|
</div>
|
|
430
474
|
</div>`;
|
|
431
475
|
}
|
|
432
|
-
function
|
|
476
|
+
function generatePerfPane(data) {
|
|
433
477
|
const total = data.totalTime || 0.01;
|
|
434
478
|
const lexer = data.lexerTime || 0;
|
|
435
479
|
const parser = data.parserTime || 0;
|
|
436
480
|
const render = data.renderTime || 0;
|
|
481
|
+
const mode = data.mode === "aot" ? "AOT" : "Runtime";
|
|
437
482
|
return `
|
|
438
|
-
<div class="
|
|
439
|
-
<div class="
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
<div class="dbg-section-content">
|
|
444
|
-
<div class="dbg-row">
|
|
445
|
-
<span class="dbg-label">Lexer</span>
|
|
446
|
-
<span class="dbg-value">${lexer.toFixed(2)}ms</span>
|
|
483
|
+
<div class="devtools-pane active" data-pane="perf">
|
|
484
|
+
<div class="perf-grid">
|
|
485
|
+
<div class="perf-card">
|
|
486
|
+
<div class="perf-card-value">${total.toFixed(2)}ms</div>
|
|
487
|
+
<div class="perf-card-label">Total Time</div>
|
|
447
488
|
</div>
|
|
448
|
-
<div class="
|
|
449
|
-
|
|
450
|
-
<
|
|
451
|
-
<span class="dbg-value">${parser.toFixed(2)}ms</span>
|
|
489
|
+
<div class="perf-card">
|
|
490
|
+
<div class="perf-card-value" style="color:var(--accent)">${mode}</div>
|
|
491
|
+
<div class="perf-card-label">Mode</div>
|
|
452
492
|
</div>
|
|
453
|
-
<div class="
|
|
454
|
-
|
|
455
|
-
<
|
|
456
|
-
<span class="dbg-value">${render.toFixed(2)}ms</span>
|
|
493
|
+
<div class="perf-card">
|
|
494
|
+
<div class="perf-card-value">${data.templateChain.length}</div>
|
|
495
|
+
<div class="perf-card-label">Templates</div>
|
|
457
496
|
</div>
|
|
458
|
-
<div class="
|
|
459
|
-
|
|
460
|
-
<
|
|
461
|
-
<span class="dbg-value" style="font-weight:600">${total.toFixed(2)}ms</span>
|
|
497
|
+
<div class="perf-card">
|
|
498
|
+
<div class="perf-card-value">${data.queries?.length || 0}</div>
|
|
499
|
+
<div class="perf-card-label">Queries</div>
|
|
462
500
|
</div>
|
|
463
501
|
</div>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
<div class="
|
|
471
|
-
|
|
472
|
-
<
|
|
473
|
-
<span class="
|
|
502
|
+
<div class="perf-breakdown">
|
|
503
|
+
<div class="perf-row">
|
|
504
|
+
<span class="perf-row-label">Lexer</span>
|
|
505
|
+
<div class="perf-row-bar"><div class="perf-row-fill lexer" style="width:${lexer / total * 100}%"></div></div>
|
|
506
|
+
<span class="perf-row-value">${lexer.toFixed(2)}ms</span>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="perf-row">
|
|
509
|
+
<span class="perf-row-label">Parser</span>
|
|
510
|
+
<div class="perf-row-bar"><div class="perf-row-fill parser" style="width:${parser / total * 100}%"></div></div>
|
|
511
|
+
<span class="perf-row-value">${parser.toFixed(2)}ms</span>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="perf-row">
|
|
514
|
+
<span class="perf-row-label">Render</span>
|
|
515
|
+
<div class="perf-row-bar"><div class="perf-row-fill render" style="width:${render / total * 100}%"></div></div>
|
|
516
|
+
<span class="perf-row-value">${render.toFixed(2)}ms</span>
|
|
474
517
|
</div>
|
|
475
|
-
`).join("");
|
|
476
|
-
return `
|
|
477
|
-
<div class="dbg-section">
|
|
478
|
-
<div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
|
|
479
|
-
<span class="dbg-section-title">${icons.template} Templates</span>
|
|
480
|
-
<span class="dbg-section-meta">${data.templateChain.length}</span>
|
|
481
|
-
<span class="dbg-chevron">${icons.chevron}</span>
|
|
482
|
-
</div>
|
|
483
|
-
<div class="dbg-section-content">
|
|
484
|
-
<div class="dbg-templates">${templates}</div>
|
|
485
518
|
</div>
|
|
486
519
|
</div>`;
|
|
487
520
|
}
|
|
488
|
-
function
|
|
521
|
+
function generateContextPane(data) {
|
|
489
522
|
const keys = Object.keys(data.contextSnapshot);
|
|
490
|
-
if (keys.length === 0)
|
|
491
|
-
return ""
|
|
492
|
-
const items = keys.map((key) => renderContextValue(key, data.contextSnapshot[key])).join("");
|
|
493
|
-
return `
|
|
494
|
-
<div class="dbg-section">
|
|
495
|
-
<div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
|
|
496
|
-
<span class="dbg-section-title">${icons.context} Context</span>
|
|
497
|
-
<span class="dbg-section-meta">${keys.length} vars</span>
|
|
498
|
-
<span class="dbg-chevron">${icons.chevron}</span>
|
|
499
|
-
</div>
|
|
500
|
-
<div class="dbg-section-content">
|
|
501
|
-
<div class="dbg-ctx-grid">${items}</div>
|
|
502
|
-
</div>
|
|
503
|
-
</div>`;
|
|
504
|
-
}
|
|
505
|
-
function renderContextValue(key, ctx) {
|
|
506
|
-
const arrow = ctx.expandable ? `<svg class="dbg-ctx-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>` : "";
|
|
507
|
-
const expandableClass = ctx.expandable ? "expandable" : "";
|
|
508
|
-
const onClick = ctx.expandable ? `onclick="this.parentElement.classList.toggle('open')"` : "";
|
|
509
|
-
let children = "";
|
|
510
|
-
if (ctx.expandable && ctx.children) {
|
|
511
|
-
const childItems = Object.entries(ctx.children).map(([k, v]) => renderContextValue(k, v)).join("");
|
|
512
|
-
children = `<div class="dbg-ctx-children">${childItems}</div>`;
|
|
523
|
+
if (keys.length === 0) {
|
|
524
|
+
return `<div class="devtools-pane" data-pane="context"><div class="empty-state">${icons.context}<span>No context variables</span></div></div>`;
|
|
513
525
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
<div class="dbg-ctx-row ${expandableClass}" ${onClick}>
|
|
517
|
-
<div class="dbg-ctx-key">
|
|
518
|
-
${arrow}
|
|
519
|
-
<span class="dbg-ctx-name">${escapeHtml(key)}</span>
|
|
520
|
-
<span class="dbg-ctx-type">${ctx.type}</span>
|
|
521
|
-
</div>
|
|
522
|
-
<span class="dbg-ctx-preview">${escapeHtml(ctx.preview)}</span>
|
|
523
|
-
</div>
|
|
524
|
-
${children}
|
|
525
|
-
</div>`;
|
|
526
|
+
const items = keys.map((key) => renderTreeItem(key, data.contextSnapshot[key])).join("");
|
|
527
|
+
return `<div class="devtools-pane" data-pane="context"><div class="tree">${items}</div></div>`;
|
|
526
528
|
}
|
|
527
|
-
function
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const
|
|
529
|
+
function renderTreeItem(key, ctx) {
|
|
530
|
+
const hasChildren = ctx.expandable && ctx.children && Object.keys(ctx.children).length > 0;
|
|
531
|
+
const arrowHtml = hasChildren ? `<span class="tree-arrow">${icons.arrow}</span>` : '<span class="tree-arrow" style="visibility:hidden">${icons.arrow}</span>';
|
|
532
|
+
const expandableClass = hasChildren ? "expandable" : "";
|
|
533
|
+
const valueClass = getValueClass(ctx.type);
|
|
534
|
+
let childrenHtml = "";
|
|
535
|
+
if (hasChildren && ctx.children) {
|
|
536
|
+
childrenHtml = `<div class="tree-children">${Object.entries(ctx.children).map(([k, v]) => renderTreeItem(k, v)).join("")}</div>`;
|
|
537
|
+
}
|
|
532
538
|
return `
|
|
533
|
-
<div class="
|
|
534
|
-
<div class="
|
|
535
|
-
|
|
536
|
-
<span class="
|
|
537
|
-
<span class="
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
<div class="dbg-filters">${items}</div>
|
|
539
|
+
<div class="tree-item">
|
|
540
|
+
<div class="tree-row ${expandableClass}">
|
|
541
|
+
${arrowHtml}
|
|
542
|
+
<span class="tree-key">${escapeHtml(key)}</span>
|
|
543
|
+
<span class="tree-colon">:</span>
|
|
544
|
+
<span class="tree-value ${valueClass}">${escapeHtml(ctx.preview)}</span>
|
|
545
|
+
<span class="tree-type">${ctx.type}</span>
|
|
541
546
|
</div>
|
|
547
|
+
${childrenHtml}
|
|
542
548
|
</div>`;
|
|
543
549
|
}
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
</
|
|
561
|
-
<
|
|
562
|
-
<div class="dbg-cache-num miss">${data.cacheMisses}</div>
|
|
563
|
-
<div class="dbg-cache-label">Cache Misses</div>
|
|
564
|
-
</div>
|
|
550
|
+
function getValueClass(type) {
|
|
551
|
+
if (type === "string")
|
|
552
|
+
return "string";
|
|
553
|
+
if (type === "number" || type === "integer" || type === "float")
|
|
554
|
+
return "number";
|
|
555
|
+
if (type === "null" || type === "undefined")
|
|
556
|
+
return "null";
|
|
557
|
+
return "";
|
|
558
|
+
}
|
|
559
|
+
function generateTemplatesPane(data) {
|
|
560
|
+
if (data.templateChain.length === 0) {
|
|
561
|
+
return `<div class="devtools-pane" data-pane="templates"><div class="empty-state">${icons.template}<span>No templates loaded</span></div></div>`;
|
|
562
|
+
}
|
|
563
|
+
const items = data.templateChain.map((t) => `
|
|
564
|
+
<div class="template-item">
|
|
565
|
+
<span class="template-icon">${icons.template}</span>
|
|
566
|
+
<span class="template-name">${escapeHtml(t.name)}</span>
|
|
567
|
+
<span class="template-badge ${t.type}">${t.type}</span>
|
|
565
568
|
</div>
|
|
566
|
-
|
|
567
|
-
</div>`;
|
|
569
|
+
`).join("");
|
|
570
|
+
return `<div class="devtools-pane" data-pane="templates"><div class="template-list">${items}</div></div>`;
|
|
568
571
|
}
|
|
569
|
-
function
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
</div>
|
|
572
|
+
function generateFiltersPane(data) {
|
|
573
|
+
const filters = Array.from(data.filtersUsed.entries());
|
|
574
|
+
if (filters.length === 0) {
|
|
575
|
+
return `<div class="devtools-pane" data-pane="filters"><div class="empty-state">${icons.filter}<span>No filters used</span></div></div>`;
|
|
576
|
+
}
|
|
577
|
+
const items = filters.map(([name, count]) => `
|
|
578
|
+
<span class="filter-chip">${escapeHtml(name)}<span class="filter-count">\xD7${count}</span></span>
|
|
577
579
|
`).join("");
|
|
578
|
-
return
|
|
579
|
-
<div class="dbg-section open">
|
|
580
|
-
<div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
|
|
581
|
-
<span class="dbg-section-title">${icons.warning} Warnings</span>
|
|
582
|
-
<span class="dbg-section-meta" style="color:#eab308">${data.warnings.length}</span>
|
|
583
|
-
<span class="dbg-chevron">${icons.chevron}</span>
|
|
584
|
-
</div>
|
|
585
|
-
<div class="dbg-section-content">
|
|
586
|
-
<div class="dbg-warnings">${items}</div>
|
|
587
|
-
</div>
|
|
588
|
-
</div>`;
|
|
580
|
+
return `<div class="devtools-pane" data-pane="filters"><div class="filter-grid">${items}</div></div>`;
|
|
589
581
|
}
|
|
590
|
-
function
|
|
591
|
-
if (data.queries.length === 0)
|
|
592
|
-
return ""
|
|
582
|
+
function generateQueriesPane(data) {
|
|
583
|
+
if (!data.queries || data.queries.length === 0) {
|
|
584
|
+
return `<div class="devtools-pane" data-pane="queries"><div class="empty-state">${icons.database}<span>No queries recorded</span></div></div>`;
|
|
585
|
+
}
|
|
593
586
|
const stats = data.queryStats;
|
|
594
|
-
const hasIssues = stats.slowCount > 0 || stats.n1Count > 0;
|
|
595
587
|
const statsHtml = `
|
|
596
|
-
<div class="
|
|
597
|
-
<div class="
|
|
598
|
-
<div class="
|
|
599
|
-
<div class="
|
|
588
|
+
<div class="queries-stats">
|
|
589
|
+
<div class="queries-stat">
|
|
590
|
+
<div class="queries-stat-value">${stats.count}</div>
|
|
591
|
+
<div class="queries-stat-label">Queries</div>
|
|
600
592
|
</div>
|
|
601
|
-
<div class="
|
|
602
|
-
<div class="
|
|
603
|
-
<div class="
|
|
593
|
+
<div class="queries-stat">
|
|
594
|
+
<div class="queries-stat-value">${stats.totalDuration.toFixed(1)}ms</div>
|
|
595
|
+
<div class="queries-stat-label">Total</div>
|
|
604
596
|
</div>
|
|
605
|
-
<div class="
|
|
606
|
-
<div class="
|
|
607
|
-
<div class="
|
|
597
|
+
<div class="queries-stat">
|
|
598
|
+
<div class="queries-stat-value ${stats.slowCount > 0 ? "warn" : ""}">${stats.slowCount}</div>
|
|
599
|
+
<div class="queries-stat-label">Slow</div>
|
|
608
600
|
</div>
|
|
609
|
-
<div class="
|
|
610
|
-
<div class="
|
|
611
|
-
<div class="
|
|
601
|
+
<div class="queries-stat">
|
|
602
|
+
<div class="queries-stat-value ${stats.n1Count > 0 ? "error" : ""}">${stats.n1Count}</div>
|
|
603
|
+
<div class="queries-stat-label">N+1</div>
|
|
612
604
|
</div>
|
|
613
605
|
</div>`;
|
|
614
606
|
const queries = data.queries.map((q) => {
|
|
615
607
|
const isSlow = q.duration > 100;
|
|
616
|
-
const classes = [
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
].filter(Boolean).join(" ");
|
|
621
|
-
const badges = [];
|
|
622
|
-
if (q.isN1) {
|
|
623
|
-
badges.push('<span class="dbg-query-badge n1">N+1</span>');
|
|
624
|
-
}
|
|
625
|
-
const rowsText = q.rows !== undefined ? `<span class="dbg-query-rows">${q.rows} rows</span>` : "";
|
|
626
|
-
const sourceText = q.source ? `<span class="dbg-query-source">${escapeHtml(q.source)}</span>` : "";
|
|
608
|
+
const classes = ["query-item", q.isN1 ? "n1" : "", isSlow ? "slow" : ""].filter(Boolean).join(" ");
|
|
609
|
+
const badge = q.isN1 ? '<span class="query-badge n1">N+1</span>' : "";
|
|
610
|
+
const source = q.source ? `<span class="query-source">${escapeHtml(q.source)}</span>` : "";
|
|
611
|
+
const rows = q.rows !== undefined ? `<span class="query-rows">${q.rows} rows</span>` : "";
|
|
627
612
|
return `
|
|
628
613
|
<div class="${classes}">
|
|
629
|
-
<div class="
|
|
630
|
-
<span class="
|
|
631
|
-
<div class="
|
|
632
|
-
${
|
|
633
|
-
${
|
|
634
|
-
${rowsText}
|
|
635
|
-
<span class="dbg-query-time ${isSlow ? "slow" : ""}">${q.duration.toFixed(1)}ms</span>
|
|
614
|
+
<div class="query-header">
|
|
615
|
+
<span class="query-sql" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>
|
|
616
|
+
<div class="query-meta">
|
|
617
|
+
${badge}${source}${rows}
|
|
618
|
+
<span class="query-time ${isSlow ? "slow" : ""}">${q.duration.toFixed(1)}ms</span>
|
|
636
619
|
</div>
|
|
637
620
|
</div>
|
|
638
621
|
</div>`;
|
|
639
622
|
}).join("");
|
|
640
|
-
|
|
623
|
+
return `<div class="devtools-pane" data-pane="queries">${statsHtml}<div class="query-list">${queries}</div></div>`;
|
|
624
|
+
}
|
|
625
|
+
function generateCachePane(data) {
|
|
626
|
+
const total = data.cacheHits + data.cacheMisses;
|
|
627
|
+
if (total === 0) {
|
|
628
|
+
return `<div class="devtools-pane" data-pane="cache"><div class="empty-state">${icons.cache}<span>No cache activity</span></div></div>`;
|
|
629
|
+
}
|
|
630
|
+
const hitRate = (data.cacheHits / total * 100).toFixed(0);
|
|
641
631
|
return `
|
|
642
|
-
<div class="
|
|
643
|
-
<div class="
|
|
644
|
-
<
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
632
|
+
<div class="devtools-pane" data-pane="cache">
|
|
633
|
+
<div class="cache-stats">
|
|
634
|
+
<div class="cache-stat">
|
|
635
|
+
<div class="cache-value hit">${data.cacheHits}</div>
|
|
636
|
+
<div class="cache-label">Cache Hits</div>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="cache-stat">
|
|
639
|
+
<div class="cache-value miss">${data.cacheMisses}</div>
|
|
640
|
+
<div class="cache-label">Cache Misses</div>
|
|
641
|
+
</div>
|
|
642
|
+
<div class="cache-stat">
|
|
643
|
+
<div class="cache-value">${hitRate}%</div>
|
|
644
|
+
<div class="cache-label">Hit Rate</div>
|
|
645
|
+
</div>
|
|
651
646
|
</div>
|
|
652
647
|
</div>`;
|
|
653
648
|
}
|
|
654
|
-
function
|
|
649
|
+
function generateWarningsPane(data) {
|
|
650
|
+
if (data.warnings.length === 0) {
|
|
651
|
+
return `<div class="devtools-pane" data-pane="warnings"><div class="empty-state">${icons.warning}<span>No warnings</span></div></div>`;
|
|
652
|
+
}
|
|
653
|
+
const items = data.warnings.map((w) => `
|
|
654
|
+
<div class="warning-item">
|
|
655
|
+
<span class="warning-icon">${icons.warning}</span>
|
|
656
|
+
<span class="warning-text">${escapeHtml(w)}</span>
|
|
657
|
+
</div>
|
|
658
|
+
`).join("");
|
|
659
|
+
return `<div class="devtools-pane" data-pane="warnings"><div class="warning-list">${items}</div></div>`;
|
|
660
|
+
}
|
|
661
|
+
function generateScript(id, data, opts) {
|
|
655
662
|
return `
|
|
656
|
-
(function(){
|
|
657
|
-
var
|
|
658
|
-
if (!
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
663
|
+
(function() {
|
|
664
|
+
var root = document.getElementById('${id}');
|
|
665
|
+
if (!root) return;
|
|
666
|
+
|
|
667
|
+
// Tab switching
|
|
668
|
+
root.querySelectorAll('.devtools-tab').forEach(function(tab) {
|
|
669
|
+
tab.addEventListener('click', function() {
|
|
670
|
+
root.querySelectorAll('.devtools-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
671
|
+
root.querySelectorAll('.devtools-pane').forEach(function(p) { p.classList.remove('active'); });
|
|
672
|
+
tab.classList.add('active');
|
|
673
|
+
var pane = root.querySelector('[data-pane="' + tab.dataset.tab + '"]');
|
|
674
|
+
if (pane) pane.classList.add('active');
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Tree expand/collapse
|
|
679
|
+
root.querySelectorAll('.tree-row.expandable').forEach(function(row) {
|
|
680
|
+
row.addEventListener('click', function() {
|
|
681
|
+
row.parentElement.classList.toggle('open');
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Dock position switching
|
|
686
|
+
root.querySelectorAll('[data-dock]').forEach(function(btn) {
|
|
687
|
+
btn.addEventListener('click', function() {
|
|
688
|
+
root.dataset.position = btn.dataset.dock;
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Resize functionality
|
|
693
|
+
var resize = root.querySelector('.devtools-resize');
|
|
694
|
+
var panel = root.querySelector('.devtools-panel');
|
|
695
|
+
if (resize && panel) {
|
|
696
|
+
var isResizing = false;
|
|
697
|
+
var startY, startX, startHeight, startWidth;
|
|
698
|
+
|
|
699
|
+
resize.addEventListener('mousedown', function(e) {
|
|
700
|
+
isResizing = true;
|
|
701
|
+
startY = e.clientY;
|
|
702
|
+
startX = e.clientX;
|
|
703
|
+
startHeight = panel.offsetHeight;
|
|
704
|
+
startWidth = panel.offsetWidth;
|
|
705
|
+
document.body.style.cursor = root.dataset.position === 'right' ? 'ew-resize' : 'ns-resize';
|
|
706
|
+
e.preventDefault();
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
document.addEventListener('mousemove', function(e) {
|
|
710
|
+
if (!isResizing) return;
|
|
711
|
+
if (root.dataset.position === 'bottom') {
|
|
712
|
+
var newHeight = startHeight - (e.clientY - startY);
|
|
713
|
+
if (newHeight > 100 && newHeight < window.innerHeight * 0.8) {
|
|
714
|
+
panel.style.height = newHeight + 'px';
|
|
715
|
+
}
|
|
716
|
+
} else if (root.dataset.position === 'right') {
|
|
717
|
+
var newWidth = startWidth - (e.clientX - startX);
|
|
718
|
+
if (newWidth > 200 && newWidth < window.innerWidth * 0.6) {
|
|
719
|
+
panel.style.width = newWidth + 'px';
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
document.addEventListener('mouseup', function() {
|
|
725
|
+
isResizing = false;
|
|
726
|
+
document.body.style.cursor = '';
|
|
727
|
+
});
|
|
728
|
+
}
|
|
686
729
|
})();`;
|
|
687
730
|
}
|
|
688
731
|
function escapeHtml(str) {
|
|
689
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
732
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
690
733
|
}
|
|
691
734
|
// src/debug/integrations.ts
|
|
692
735
|
function recordQuery(query) {
|