mbkauthe 4.7.0 → 4.7.2

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.
@@ -10,18 +10,18 @@
10
10
  padding: 120px 1.7rem 40px;
11
11
  position: relative;
12
12
  overflow: hidden;
13
- background: radial-gradient(circle at top right, rgba(33, 150, 243, 0.25), transparent 55%),
14
- radial-gradient(circle at 20% 20%, rgba(0, 184, 148, 0.18), transparent 50%),
15
- linear-gradient(135deg, var(--darker), #0b1e25);
13
+ background: radial-gradient(circle at top right, color-mix(in srgb, var(--primary) 25%, transparent 75%), transparent 55%),
14
+ radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--accent) 18%, transparent 82%), transparent 50%),
15
+ linear-gradient(135deg, var(--hero-from), var(--hero-to));
16
16
  }
17
17
 
18
18
  .db-panel {
19
- background: rgba(10, 20, 20, 0.94);
19
+ background: color-mix(in srgb, var(--glass-bg) 96%, transparent 4%);
20
20
  border-radius: 18px;
21
21
  padding: 2.25rem;
22
22
  width: min(1200px, 100%);
23
23
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
24
- border: 1px solid rgba(0, 184, 148, 0.18);
24
+ border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent 80%);
25
25
  position: relative;
26
26
  z-index: 2;
27
27
  overflow: hidden;
@@ -31,7 +31,7 @@
31
31
  content: '';
32
32
  position: absolute;
33
33
  inset: 0;
34
- background: linear-gradient(120deg, rgba(33, 150, 243, 0.06), transparent 55%);
34
+ background: linear-gradient(120deg, color-mix(in srgb, var(--primary) 8%, transparent 92%), transparent 55%);
35
35
  pointer-events: none;
36
36
  }
37
37
 
@@ -65,15 +65,15 @@
65
65
  .db-link {
66
66
  padding: 0.45rem 0.85rem;
67
67
  border-radius: 0.6rem;
68
- border: 1px solid rgba(33, 150, 243, 0.4);
69
- background: rgba(33, 150, 243, 0.12);
68
+ border: 1px solid color-mix(in srgb, var(--primary) 45%, transparent 55%);
69
+ background: color-mix(in srgb, var(--primary) 14%, transparent 86%);
70
70
  color: var(--light);
71
71
  text-decoration: none;
72
72
  transition: var(--transition);
73
73
  }
74
74
 
75
75
  .db-link:hover {
76
- background: rgba(33, 150, 243, 0.25);
76
+ background: color-mix(in srgb, var(--primary) 24%, transparent 76%);
77
77
  }
78
78
 
79
79
  .db-card {
@@ -86,8 +86,8 @@
86
86
  .stat-box {
87
87
  padding: 1rem 1.2rem;
88
88
  border-radius: 14px;
89
- border: 1px solid rgba(0, 184, 148, 0.2);
90
- background: rgba(0, 0, 0, 0.25);
89
+ border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent 75%);
90
+ background: var(--surface-muted);
91
91
  }
92
92
 
93
93
  .stat-label {
@@ -119,69 +119,184 @@
119
119
  width: 5rem;
120
120
  padding: 0.35rem 0.5rem;
121
121
  border-radius: 0.5rem;
122
- border: 1px solid rgba(255, 255, 255, 0.2);
123
- background: rgba(0, 0, 0, 0.4);
122
+ border: 1px solid var(--muted-border);
123
+ background: var(--input-bg);
124
124
  color: var(--text);
125
125
  }
126
126
 
127
127
  .db-toolbar button {
128
128
  padding: 0.45rem 0.85rem;
129
129
  border-radius: 0.6rem;
130
- border: 1px solid rgba(0, 184, 148, 0.4);
131
- background: rgba(0, 184, 148, 0.12);
130
+ border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent 55%);
131
+ background: color-mix(in srgb, var(--accent) 14%, transparent 86%);
132
132
  color: var(--light);
133
133
  cursor: pointer;
134
134
  transition: var(--transition);
135
135
  }
136
136
 
137
137
  .db-toolbar button:hover {
138
- background: rgba(0, 184, 148, 0.25);
138
+ background: color-mix(in srgb, var(--accent) 24%, transparent 76%);
139
139
  }
140
140
 
141
141
  .db-table-wrapper {
142
142
  border-radius: 16px;
143
- border: 1px solid rgba(255, 255, 255, 0.08);
144
- background: rgba(6, 12, 14, 0.7);
145
- padding: 0.4rem;
146
- overflow-x: auto;
143
+ border: 1px solid var(--muted-border);
144
+ background: color-mix(in srgb, var(--surface-1) 82%, transparent 18%);
145
+ padding: 0.75rem;
146
+ overflow: hidden;
147
147
  }
148
148
 
149
- .copy-row-btn {
150
- padding: 0.25rem 0.6rem;
151
- border-radius: 0.5rem;
152
- border: 1px solid rgba(33, 150, 243, 0.5);
153
- background: rgba(33, 150, 243, 0.12);
154
- color: var(--light);
155
- cursor: pointer;
149
+ .log-list {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 0.85rem;
153
+ }
154
+
155
+ .log-card {
156
+ border: 1px solid var(--muted-border);
157
+ border-radius: 12px;
158
+ background: var(--surface-muted);
159
+ padding: 0.85rem;
160
+ }
161
+
162
+ .log-head {
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: space-between;
166
+ gap: 0.8rem;
167
+ margin-bottom: 0.75rem;
168
+ flex-wrap: wrap;
169
+ }
170
+
171
+ .log-mainline {
172
+ display: inline-flex;
173
+ align-items: center;
174
+ gap: 0.45rem;
175
+ flex-wrap: wrap;
176
+ }
177
+
178
+ .log-index {
156
179
  font-size: 0.75rem;
157
- transition: var(--transition);
180
+ color: var(--text-light);
181
+ border: 1px solid var(--muted-border);
182
+ border-radius: 999px;
183
+ padding: 0.1rem 0.45rem;
158
184
  }
159
185
 
160
- .copy-row-btn:hover {
161
- background: rgba(33, 150, 243, 0.25);
186
+ .log-time {
187
+ color: var(--text-light);
188
+ font-size: 0.8rem;
162
189
  }
163
190
 
164
- table {
165
- width: 100%;
166
- border-collapse: collapse;
167
- color: var(--text);
168
- font-size: 0.9rem;
169
- min-width: 900px;
191
+ .log-query-block {
192
+ border: 1px solid var(--muted-border);
193
+ border-radius: 10px;
194
+ padding: 0.55rem;
195
+ margin-bottom: 0.65rem;
196
+ background: color-mix(in srgb, var(--surface-1) 50%, transparent 50%);
170
197
  }
171
198
 
172
- th, td {
173
- padding: 0.6rem 0.8rem;
174
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
175
- vertical-align: top;
199
+ .log-return-block {
200
+ border: 1px solid color-mix(in srgb, var(--primary) 35%, transparent 65%);
201
+ border-radius: 10px;
202
+ padding: 0.55rem;
203
+ margin-bottom: 0.65rem;
204
+ background: color-mix(in srgb, var(--primary) 10%, transparent 90%);
176
205
  }
177
206
 
178
- th {
179
- text-align: left;
207
+ .log-return-block summary {
208
+ cursor: pointer;
209
+ color: var(--primary);
210
+ font-size: 0.82rem;
211
+ user-select: none;
180
212
  font-weight: 600;
213
+ }
214
+
215
+ .log-return-block[open] summary {
216
+ margin-bottom: 0.45rem;
217
+ }
218
+
219
+ .log-label {
181
220
  color: var(--text-light);
182
221
  text-transform: uppercase;
183
- font-size: 0.75rem;
184
222
  letter-spacing: 0.08em;
223
+ font-size: 0.65rem;
224
+ margin-bottom: 0.35rem;
225
+ }
226
+
227
+ .log-query-block pre,
228
+ .log-return-block pre,
229
+ .log-detail-item pre {
230
+ margin: 0;
231
+ white-space: pre-wrap;
232
+ word-break: break-word;
233
+ overflow-wrap: anywhere;
234
+ }
235
+
236
+ .log-meta-grid {
237
+ display: grid;
238
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
239
+ gap: 0.45rem;
240
+ margin-bottom: 0.65rem;
241
+ }
242
+
243
+ .log-meta-item {
244
+ border: 1px solid var(--muted-border);
245
+ border-radius: 8px;
246
+ padding: 0.4rem 0.55rem;
247
+ background: color-mix(in srgb, var(--surface-1) 35%, transparent 65%);
248
+ }
249
+
250
+ .log-meta-item span {
251
+ color: var(--text-light);
252
+ font-size: 0.72rem;
253
+ display: block;
254
+ margin-bottom: 0.2rem;
255
+ }
256
+
257
+ .log-meta-item code {
258
+ font-size: 0.78rem;
259
+ }
260
+
261
+ .log-details {
262
+ border-top: 1px dashed var(--muted-border);
263
+ padding-top: 0.55rem;
264
+ }
265
+
266
+ .log-details summary {
267
+ cursor: pointer;
268
+ color: var(--accent);
269
+ font-size: 0.82rem;
270
+ user-select: none;
271
+ }
272
+
273
+ .log-detail-grid {
274
+ display: grid;
275
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
276
+ gap: 0.45rem;
277
+ margin-top: 0.55rem;
278
+ }
279
+
280
+ .log-detail-item {
281
+ border: 1px solid var(--muted-border);
282
+ border-radius: 8px;
283
+ padding: 0.45rem 0.55rem;
284
+ background: color-mix(in srgb, var(--surface-1) 35%, transparent 65%);
285
+ }
286
+
287
+ .copy-row-btn {
288
+ padding: 0.25rem 0.6rem;
289
+ border-radius: 0.5rem;
290
+ border: 1px solid color-mix(in srgb, var(--primary) 55%, transparent 45%);
291
+ background: color-mix(in srgb, var(--primary) 14%, transparent 86%);
292
+ color: var(--light);
293
+ cursor: pointer;
294
+ font-size: 0.75rem;
295
+ transition: var(--transition);
296
+ }
297
+
298
+ .copy-row-btn:hover {
299
+ background: color-mix(in srgb, var(--primary) 24%, transparent 76%);
185
300
  }
186
301
 
187
302
  code {
@@ -201,15 +316,15 @@
201
316
  }
202
317
 
203
318
  .badge.ok {
204
- background: rgba(67, 233, 123, 0.15);
319
+ background: color-mix(in srgb, var(--success) 15%, transparent 85%);
205
320
  color: var(--success);
206
- border: 1px solid rgba(67, 233, 123, 0.4);
321
+ border: 1px solid color-mix(in srgb, var(--success) 45%, transparent 55%);
207
322
  }
208
323
 
209
324
  .badge.err {
210
- background: rgba(255, 118, 117, 0.15);
325
+ background: color-mix(in srgb, var(--danger) 15%, transparent 85%);
211
326
  color: var(--danger);
212
- border: 1px solid rgba(255, 118, 117, 0.35);
327
+ border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent 60%);
213
328
  }
214
329
 
215
330
  .empty {
@@ -223,9 +338,15 @@
223
338
  margin-bottom: 1rem;
224
339
  padding: 0.75rem 1rem;
225
340
  border-radius: 0.7rem;
226
- background: rgba(67, 233, 123, 0.12);
341
+ background: color-mix(in srgb, var(--success) 14%, transparent 86%);
227
342
  color: var(--success);
228
- border: 1px solid rgba(67, 233, 123, 0.4);
343
+ border: 1px solid color-mix(in srgb, var(--success) 45%, transparent 55%);
344
+ }
345
+
346
+ .notification.warn {
347
+ background: color-mix(in srgb, var(--warning) 14%, transparent 86%);
348
+ color: var(--warning);
349
+ border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent 55%);
229
350
  }
230
351
 
231
352
  @media (max-width: 900px) {
@@ -237,43 +358,12 @@
237
358
  width: 100%;
238
359
  }
239
360
 
240
- table {
241
- min-width: 100%;
242
- display: block;
243
- }
244
-
245
- thead {
246
- display: none;
247
- }
248
-
249
- tbody,
250
- tr,
251
- td {
252
- display: block;
253
- width: 100%;
254
- }
255
-
256
- tr {
257
- border: 1px solid rgba(255, 255, 255, 0.08);
258
- border-radius: 12px;
259
- margin-bottom: 0.9rem;
260
- padding: 0.6rem 0.8rem;
261
- background: rgba(0, 0, 0, 0.28);
262
- }
263
-
264
- td {
265
- border: none;
266
- padding: 0.5rem 0;
361
+ .db-table-wrapper {
362
+ padding: 0.6rem;
267
363
  }
268
364
 
269
- td::before {
270
- content: attr(data-label);
271
- display: block;
272
- font-size: 0.7rem;
273
- text-transform: uppercase;
274
- letter-spacing: 0.08em;
275
- color: var(--text-light);
276
- margin-bottom: 0.25rem;
365
+ .log-detail-grid {
366
+ grid-template-columns: 1fr;
277
367
  }
278
368
  }
279
369
  </style>"}}
@@ -291,7 +381,7 @@
291
381
  <div class="db-subtitle">Live view of recent database queries and diagnostics</div>
292
382
  </div>
293
383
  <div class="db-actions">
294
- <a class="db-link" href="/mbkauthe/info">Back to info</a>
384
+ <a class="db-link" href="/mbkauthe/">Back</a>
295
385
  </div>
296
386
  </div>
297
387
 
@@ -309,19 +399,21 @@
309
399
  <div class="db-toolbar">
310
400
  <form id="limit-form" action="/mbkauthe/db" method="get">
311
401
  <label for="limit">Limit</label>
312
- <input id="limit" name="limit" type="number" min="1" max="500" value="{{queryLimit}}" />
313
- <button type="submit">Update</button>
402
+ <input id="limit" name="limit" type="number" min="1" max="500" value="{{queryLimit}}" {{#unless isDev}}disabled{{/unless}} />
403
+ <button type="submit" {{#unless isDev}}disabled{{/unless}}>Update</button>
314
404
  </form>
315
- <button id="reset-btn" type="button">Reset</button>
316
- <button id="copy-btn" type="button">Copy Log</button>
405
+ <button id="reset-btn" type="button" {{#unless isDev}}disabled{{/unless}}>Reset</button>
406
+ <button id="copy-btn" type="button" {{#unless isDev}}disabled{{/unless}}>Copy Logs</button>
317
407
  </div>
318
408
 
319
- {{#if resetDone}}
320
- <div class="notification">Query log and count have been reset.</div>
321
- {{/if}}
409
+ {{#unless isDev}}
410
+ <div class="notification warn" id="disabled-notice">{{disabledMessage}}</div>
411
+ {{/unless}}
412
+
413
+ <div class="notification" id="reset-notice" {{#unless resetDone}}hidden{{/unless}}>Query log and count have been reset.</div>
322
414
 
323
415
  <div id="query-log" class="db-table-wrapper">
324
- <div class="empty">Loading query log...</div>
416
+ <div class="empty">{{#if isDev}}Loading query log...{{else}}DB logs are disabled.{{/if}}</div>
325
417
  </div>
326
418
  </div>
327
419
  </section>
@@ -333,31 +425,118 @@
333
425
  const queryLogEl = document.getElementById('query-log');
334
426
  const resetBtn = document.getElementById('reset-btn');
335
427
  const copyBtn = document.getElementById('copy-btn');
428
+ const resetNoticeEl = document.getElementById('reset-notice');
429
+ const isDev = {{#if isDev}}true{{else}}false{{/if}};
430
+ let latestQueryLog = [];
431
+
432
+ const showDisabledState = (message = 'DB logs are disabled.') => {
433
+ queryCountEl.textContent = '-';
434
+ latestQueryLog = [];
435
+ queryLogEl.innerHTML = `<div class="empty">${escapeHtml(message)}</div>`;
436
+ };
336
437
 
337
438
  const escapeHtml = (str) => String(str || '')
338
439
  .replace(/[&<>"'`]/g, (s) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '`': '&#96;' })[s]);
339
440
 
441
+ const escapeMarkdownCell = (value) => String(value || '')
442
+ .replace(/\\/g, '\\\\')
443
+ .replace(/\|/g, '\\|')
444
+ .replace(/\r?\n/g, '<br>');
445
+
446
+ const formatParamValues = (values, pretty = false) => {
447
+ if (!Array.isArray(values) || values.length === 0) return '';
448
+
449
+ if (pretty) {
450
+ return values
451
+ .map((v, i) => {
452
+ const printable = typeof v === 'string' ? v : JSON.stringify(v);
453
+ return `$${i + 1}: ${printable}`;
454
+ })
455
+ .join('\n');
456
+ }
457
+
458
+ return values
459
+ .map((v, i) => {
460
+ const printable = typeof v === 'string' ? v : JSON.stringify(v);
461
+ return `$${i + 1}: ${printable}`;
462
+ })
463
+ .join(' | ');
464
+ };
465
+
466
+ const toMarkdownReport = (queryLog, limit) => {
467
+ if (!Array.isArray(queryLog) || queryLog.length === 0) {
468
+ return '# DB Query Monitor\n\n_No queries recorded yet._';
469
+ }
470
+
471
+ const now = new Date().toISOString();
472
+ const header = [
473
+ '# DB Query Monitor',
474
+ '',
475
+ `- Exported: ${now}`,
476
+ `- Query Count: ${queryLog.length}`,
477
+ `- Limit: ${limit}`,
478
+ '',
479
+ '| # | Time | Name | Duration | Status | Query | Values | Return Value | Error | Request | Pool | Callsite |',
480
+ '|---:|---|---|---|---|---|---|---|---|---|---|---|'
481
+ ];
482
+
483
+ const rows = queryLog.map((entry, idx) => {
484
+ const durationText = typeof entry.durationMs === 'number'
485
+ ? `${entry.durationMs.toFixed(2)} ms`
486
+ : '';
487
+ const statusText = entry.success === true ? 'OK' : entry.success === false ? 'ERR' : '';
488
+ const values = formatParamValues(entry.values, false);
489
+ const returnValueText = entry.returnValue ? JSON.stringify(entry.returnValue) : '';
490
+ const errorText = entry.error
491
+ ? `${entry.error.code ? `${entry.error.code}: ` : ''}${entry.error.message || ''}`
492
+ : '';
493
+ const requestText = entry.request
494
+ ? `${entry.request.method || ''} ${entry.request.url || ''}`.trim() +
495
+ `\nuser: ${entry.request.username || entry.request.userId || 'anonymous'}` +
496
+ `\nip: ${entry.request.ip || ''}`
497
+ : '';
498
+ const poolText = entry.pool
499
+ ? `total:${entry.pool.total} idle:${entry.pool.idle} waiting:${entry.pool.waiting}`
500
+ : '';
501
+ const callsiteText = entry.callsite
502
+ ? `${entry.callsite.function || '(anonymous)'}\n${entry.callsite.file}:${entry.callsite.line}:${entry.callsite.column}`
503
+ : '';
504
+
505
+ return `| ${idx + 1} | ${escapeMarkdownCell(entry.time)} | ${escapeMarkdownCell(entry.name || '')} | ${escapeMarkdownCell(durationText)} | ${escapeMarkdownCell(statusText)} | ${escapeMarkdownCell(entry.query || '')} | ${escapeMarkdownCell(values)} | ${escapeMarkdownCell(returnValueText)} | ${escapeMarkdownCell(errorText)} | ${escapeMarkdownCell(requestText)} | ${escapeMarkdownCell(poolText)} | ${escapeMarkdownCell(callsiteText)} |`;
506
+ });
507
+
508
+ return [...header, ...rows].join('\n');
509
+ };
510
+
340
511
  const buildTable = (queryLog) => {
341
512
  if (!Array.isArray(queryLog) || queryLog.length === 0) {
342
513
  queryLogEl.innerHTML = '<div class="empty">No queries recorded yet.</div>';
343
514
  return;
344
515
  }
345
516
 
346
- const rows = queryLog.map((entry, idx) => {
347
- const values = Array.isArray(entry.values) ? escapeHtml(JSON.stringify(entry.values)) : '';
517
+ const cards = queryLog.map((entry, idx) => {
518
+ const values = formatParamValues(entry.values, true);
519
+ const returnValueText = entry.returnValue ? JSON.stringify(entry.returnValue, null, 2) : '';
520
+ const returnValueSummary = entry.returnValue
521
+ ? [
522
+ entry.returnValue.command ? `command: ${entry.returnValue.command}` : null,
523
+ typeof entry.returnValue.rowCount === 'number' ? `rowCount: ${entry.returnValue.rowCount}` : null,
524
+ typeof entry.returnValue.returnedRows === 'number' ? `rows: ${entry.returnValue.returnedRows}` : null
525
+ ].filter(Boolean).join(' | ')
526
+ : '';
348
527
  const callsite = entry.callsite || null;
349
528
  const callsiteText = callsite
350
529
  ? `${callsite.function || '(anonymous)'}\n${callsite.file}:${callsite.line}:${callsite.column}`
351
530
  : '';
352
531
  const durationText = typeof entry.durationMs === 'number'
353
532
  ? `${entry.durationMs.toFixed(2)} ms`
354
- : '';
355
- const statusText = entry.success === true ? 'OK' : entry.success === false ? 'ERR' : '';
533
+ : 'n/a';
534
+ const statusText = entry.success === true ? 'OK' : entry.success === false ? 'ERR' : 'n/a';
356
535
  const statusBadge = statusText === 'OK'
357
536
  ? '<span class="badge ok">OK</span>'
358
537
  : statusText === 'ERR'
359
538
  ? '<span class="badge err">ERR</span>'
360
- : '';
539
+ : '<span class="badge">N/A</span>';
361
540
  const errorText = entry.error
362
541
  ? `${entry.error.code ? `${entry.error.code}: ` : ''}${entry.error.message || ''}`
363
542
  : '';
@@ -370,83 +549,127 @@
370
549
  ? `total:${entry.pool.total} idle:${entry.pool.idle} waiting:${entry.pool.waiting}`
371
550
  : '';
372
551
 
552
+ const detailBlocks = [
553
+ values ? `<div class="log-detail-item"><div class="log-label">Values</div><pre><code>${escapeHtml(values)}</code></pre></div>` : '',
554
+ errorText ? `<div class="log-detail-item"><div class="log-label">Error</div><pre><code>${escapeHtml(errorText)}</code></pre></div>` : '',
555
+ requestText ? `<div class="log-detail-item"><div class="log-label">Request</div><pre><code>${escapeHtml(requestText)}</code></pre></div>` : '',
556
+ poolText ? `<div class="log-detail-item"><div class="log-label">Pool</div><pre><code>${escapeHtml(poolText)}</code></pre></div>` : '',
557
+ callsiteText ? `<div class="log-detail-item"><div class="log-label">Callsite</div><pre><code>${escapeHtml(callsiteText)}</code></pre></div>` : ''
558
+ ].filter(Boolean).join('');
559
+
373
560
  return `
374
- <tr>
375
- <td data-label="#">${idx + 1}</td>
376
- <td data-label="Time">${escapeHtml(entry.time)}</td>
377
- <td data-label="Name">${entry.name ? `<code>${escapeHtml(entry.name)}</code>` : ''}</td>
378
- <td data-label="Duration">${durationText ? `<code>${escapeHtml(durationText)}</code>` : ''}</td>
379
- <td data-label="Status">${statusBadge}</td>
380
- <td data-label="Query"><code>${escapeHtml(entry.query)}</code></td>
381
- <td data-label="Values">${values ? `<code>${values}</code>` : ''}</td>
382
- <td data-label="Error">${errorText ? `<code>${escapeHtml(errorText)}</code>` : ''}</td>
383
- <td data-label="Request">${requestText ? `<code>${escapeHtml(requestText)}</code>` : ''}</td>
384
- <td data-label="Pool">${poolText ? `<code>${escapeHtml(poolText)}</code>` : ''}</td>
385
- <td data-label="Callsite">${callsiteText ? `<code>${escapeHtml(callsiteText)}</code>` : ''}</td>
386
- <td data-label="Copy"><button class="copy-row-btn" data-copy='${escapeHtml(JSON.stringify(entry))}'>Copy</button></td>
387
- </tr>
561
+ <article class="log-card">
562
+ <div class="log-head">
563
+ <div class="log-mainline">
564
+ <span class="log-index">#${idx + 1}</span>
565
+ ${statusBadge}
566
+ <span class="log-time">${escapeHtml(entry.time)}</span>
567
+ </div>
568
+ <button class="copy-row-btn" data-copy='${escapeHtml(JSON.stringify(entry))}'>Copy</button>
569
+ </div>
570
+
571
+ <div class="log-query-block">
572
+ <div class="log-label">Query</div>
573
+ <pre><code>${escapeHtml(entry.query || '')}</code></pre>
574
+ </div>
575
+
576
+ <div class="log-meta-grid">
577
+ <div class="log-meta-item"><span>Name</span><code>${escapeHtml(entry.name || 'n/a')}</code></div>
578
+ <div class="log-meta-item"><span>Duration</span><code>${escapeHtml(durationText)}</code></div>
579
+ </div>
580
+
581
+ ${returnValueText ? `<details class="log-return-block"><summary>Return Value${returnValueSummary ? ` (${escapeHtml(returnValueSummary)})` : ''}</summary><pre><code>${escapeHtml(returnValueText)}</code></pre></details>` : ''}
582
+
583
+ ${detailBlocks ? `<details class="log-details"><summary>More details</summary><div class="log-detail-grid">${detailBlocks}</div></details>` : ''}
584
+ </article>
388
585
  `;
389
586
  }).join('');
390
587
 
391
- queryLogEl.innerHTML = `
392
- <table>
393
- <thead>
394
- <tr>
395
- <th>#</th>
396
- <th>Time</th>
397
- <th>Name</th>
398
- <th>Duration</th>
399
- <th>Status</th>
400
- <th>Query</th>
401
- <th>Values</th>
402
- <th>Error</th>
403
- <th>Request</th>
404
- <th>Pool</th>
405
- <th>Callsite</th>
406
- <th>Copy</th>
407
- </tr>
408
- </thead>
409
- <tbody>
410
- ${rows}
411
- </tbody>
412
- </table>
413
- `;
588
+ queryLogEl.innerHTML = `<div class="log-list">${cards}</div>`;
414
589
  };
415
590
 
416
- const loadLog = async (options = {}) => {
591
+ const loadLog = async () => {
592
+ if (!isDev) {
593
+ showDisabledState();
594
+ return;
595
+ }
596
+
417
597
  const limit = Number(limitInput.value) || 50;
418
598
  queryLimitEl.textContent = String(limit);
419
599
 
420
600
  const params = new URLSearchParams({ limit: String(limit) });
421
- if (options.reset) params.set('reset', '1');
422
601
 
423
602
  const response = await fetch(`/mbkauthe/db.json?${params.toString()}`);
424
603
  if (!response.ok) {
425
- queryLogEl.innerHTML = '<div class="empty">Failed to load query log.</div>';
604
+ let message = 'Failed to load query log.';
605
+ try {
606
+ const errorData = await response.json();
607
+ if (response.status === 403 || errorData?.isDev === false) {
608
+ showDisabledState(errorData?.message || 'DB logs are disabled.');
609
+ return;
610
+ }
611
+ if (errorData?.message) message = errorData.message;
612
+ } catch {
613
+ // Ignore parse errors and keep fallback message.
614
+ }
615
+
616
+ queryLogEl.innerHTML = `<div class="empty">${escapeHtml(message)}</div>`;
426
617
  return;
427
618
  }
428
619
 
429
620
  const data = await response.json();
621
+ if (data?.isDev === false) {
622
+ showDisabledState(data.message || 'DB logs are disabled.');
623
+ return;
624
+ }
625
+
430
626
  queryCountEl.textContent = data.queryCount ?? '-';
627
+ latestQueryLog = Array.isArray(data.queryLog) ? data.queryLog : [];
431
628
  buildTable(data.queryLog);
432
-
433
- if (options.reset) {
434
- const next = new URLSearchParams({ limit: String(limit), resetDone: '1' });
435
- window.location.assign(`/mbkauthe/db?${next.toString()}`);
436
- }
437
629
  };
438
630
 
439
631
  resetBtn.addEventListener('click', () => {
440
- loadLog({ reset: true }).catch(() => {
441
- queryLogEl.innerHTML = '<div class="empty">Failed to reset query log.</div>';
442
- });
632
+ if (!isDev) {
633
+ showDisabledState();
634
+ return;
635
+ }
636
+
637
+ fetch('/mbkauthe/db/reset', { method: 'POST' })
638
+ .then(async (response) => {
639
+ if (!response.ok) {
640
+ const errorData = await response.json().catch(() => ({}));
641
+ if (response.status === 403 || errorData?.isDev === false) {
642
+ showDisabledState(errorData?.message || 'DB logs are disabled.');
643
+ return;
644
+ }
645
+ throw new Error('reset failed');
646
+ }
647
+ const result = await response.json().catch(() => ({}));
648
+
649
+ if (resetNoticeEl) {
650
+ resetNoticeEl.textContent = result.message || 'Query log and count have been reset.';
651
+ resetNoticeEl.hidden = false;
652
+ }
653
+
654
+ return loadLog();
655
+ })
656
+ .catch(() => {
657
+ queryLogEl.innerHTML = '<div class="empty">Failed to reset query log.</div>';
658
+ });
443
659
  });
444
660
 
445
661
  copyBtn.addEventListener('click', () => {
446
- const text = queryLogEl.innerText;
447
- navigator.clipboard.writeText(text).then(() => {
662
+ if (!isDev) {
663
+ showDisabledState();
664
+ return;
665
+ }
666
+
667
+ const limit = Number(limitInput.value) || 50;
668
+ const markdown = toMarkdownReport(latestQueryLog, limit);
669
+
670
+ navigator.clipboard.writeText(markdown).then(() => {
448
671
  copyBtn.textContent = 'Copied!';
449
- setTimeout(() => (copyBtn.textContent = 'Copy log'), 1200);
672
+ setTimeout(() => (copyBtn.textContent = 'Copy Logs'), 1200);
450
673
  });
451
674
  });
452
675
 
@@ -467,9 +690,13 @@
467
690
  });
468
691
  });
469
692
 
470
- loadLog().catch(() => {
471
- queryLogEl.innerHTML = '<div class="empty">Failed to load query log.</div>';
472
- });
693
+ if (!isDev) {
694
+ showDisabledState();
695
+ } else {
696
+ loadLog().catch(() => {
697
+ queryLogEl.innerHTML = '<div class="empty">Failed to load query log.</div>';
698
+ });
699
+ }
473
700
  </script>
474
701
  {{> versionInfo}}
475
702
  </body>