web_plsql 1.0.0 → 1.2.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 (51) hide show
  1. package/README.md +5 -5
  2. package/package.json +5 -5
  3. package/src/admin/client/charts.ts +303 -4
  4. package/src/admin/index.html +283 -69
  5. package/src/admin/js/api.ts +95 -20
  6. package/src/admin/js/app.ts +460 -152
  7. package/src/admin/js/schemas.ts +76 -14
  8. package/src/admin/js/templates/config.ts +10 -9
  9. package/src/admin/js/templates/errorRow.ts +8 -5
  10. package/src/admin/js/templates/index.ts +1 -0
  11. package/src/admin/js/templates/poolCard.ts +6 -6
  12. package/src/admin/js/templates/traceRow.ts +24 -0
  13. package/src/admin/js/types.ts +132 -19
  14. package/src/admin/js/ui/components.ts +44 -0
  15. package/src/admin/js/ui/table.ts +173 -0
  16. package/src/admin/js/ui/views.ts +311 -48
  17. package/src/admin/js/util/format.ts +22 -3
  18. package/src/admin/js/util/metrics.ts +24 -0
  19. package/src/admin/lib/chart.bundle.css +1 -0
  20. package/src/admin/lib/chart.bundle.js +54 -47
  21. package/src/admin/style.css +143 -27
  22. package/src/handler/handlerAdmin.js +121 -26
  23. package/src/handler/plsql/errorPage.js +0 -4
  24. package/src/handler/plsql/owaPageStream.js +93 -0
  25. package/src/handler/plsql/procedure.js +191 -121
  26. package/src/handler/plsql/procedureNamed.js +17 -3
  27. package/src/handler/plsql/procedureSanitize.js +24 -0
  28. package/src/handler/plsql/procedureVariable.js +2 -0
  29. package/src/handler/plsql/sendResponse.js +41 -3
  30. package/src/index.js +0 -1
  31. package/src/server/adminContext.js +23 -0
  32. package/src/server/server.js +83 -68
  33. package/src/types.js +1 -1
  34. package/src/util/statsManager.js +368 -0
  35. package/src/util/trace.js +2 -1
  36. package/src/util/traceManager.js +68 -0
  37. package/src/version.js +1 -1
  38. package/types/admin/js/schemas.d.ts +384 -0
  39. package/types/admin/js/types.d.ts +312 -0
  40. package/types/handler/handlerAdmin.d.ts +70 -0
  41. package/types/handler/plsql/owaPageStream.d.ts +28 -0
  42. package/types/handler/plsql/procedure.d.ts +1 -0
  43. package/types/index.d.ts +0 -1
  44. package/types/server/adminContext.d.ts +17 -0
  45. package/types/server/server.d.ts +2 -19
  46. package/types/types.d.ts +1 -1
  47. package/types/util/statsManager.d.ts +395 -0
  48. package/types/util/traceManager.d.ts +35 -0
  49. package/src/admin/lib/assets/main-zpdhQ1gD.css +0 -1
  50. package/src/handler/handlerMetrics.js +0 -68
  51. package/types/handler/handlerMetrics.d.ts +0 -25
@@ -6,6 +6,7 @@
6
6
  <title>web_plsql Admin Console</title>
7
7
  <link rel="icon" type="image/svg+xml" href="favicon.svg" />
8
8
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
9
+ <link rel="stylesheet" href="lib/chart.bundle.css" />
9
10
  <link rel="stylesheet" href="style.css" />
10
11
  </head>
11
12
  <body class="dark">
@@ -23,6 +24,10 @@
23
24
  <span class="material-symbols-rounded">dashboard</span>
24
25
  Overview
25
26
  </button>
27
+ <button data-view="trace">
28
+ <span class="material-symbols-rounded">bolt</span>
29
+ Requests
30
+ </button>
26
31
  <button data-view="errors">
27
32
  <span class="material-symbols-rounded">error</span>
28
33
  Errors
@@ -35,6 +40,10 @@
35
40
  <span class="material-symbols-rounded">database</span>
36
41
  Pools
37
42
  </button>
43
+ <button data-view="stats">
44
+ <span class="material-symbols-rounded">query_stats</span>
45
+ Stats
46
+ </button>
38
47
  <button data-view="config">
39
48
  <span class="material-symbols-rounded">settings</span>
40
49
  Configuration
@@ -57,10 +66,11 @@
57
66
  <label for="auto-refresh-toggle">Auto-refresh</label>
58
67
  <input type="checkbox" id="auto-refresh-toggle" checked />
59
68
  <select id="refresh-interval">
69
+ <option value="1000">1s</option>
60
70
  <option value="5000">5s</option>
61
- <option value="10000">10s</option>
62
- <option value="30000" selected>30s</option>
63
- <option value="60000">60s</option>
71
+ <option value="10000" selected>10s</option>
72
+ <option value="30000">30s</option>
73
+ <option value="60000">1m</option>
64
74
  </select>
65
75
  <button
66
76
  id="manual-refresh"
@@ -79,46 +89,55 @@
79
89
 
80
90
  <section id="view-overview" class="view">
81
91
  <div class="metrics-grid">
82
- <div class="card">
92
+ <div class="card" title="Time since the server started">
83
93
  <div class="card-header">
84
94
  <span class="material-symbols-rounded">schedule</span>
85
- <h3>Uptime</h3>
95
+ <h3 title="Time since the server started">Uptime</h3>
86
96
  </div>
87
- <p id="uptime-val">0s</p>
88
- <small id="start-time-val">-</small>
97
+ <p id="uptime-val" title="Total uptime duration">0s</p>
98
+ <small id="start-time-val" title="Exact server start time">-</small>
89
99
  </div>
90
- <div class="card">
100
+ <div class="card" title="Request throughput and response times">
91
101
  <div class="card-header">
92
102
  <span class="material-symbols-rounded">swap_vert</span>
93
- <h3>Requests</h3>
103
+ <h3 title="Request throughput and response times">Requests</h3>
104
+ </div>
105
+ <div class="flex items-baseline gap-1 mb-2" title="Current and maximum requests per second">
106
+ <span id="req-per-sec" class="text-2xl font-bold text-bright">0.00</span>
107
+ <span class="text-xs text-main font-medium uppercase mr-2">cur /s</span>
108
+ <span class="text-main opacity-30 mr-2">/</span>
109
+ <span id="max-req-per-sec" class="text-2xl font-bold text-bright">0.00</span>
110
+ <span class="text-xs text-main font-medium uppercase">max /s</span>
94
111
  </div>
95
- <p id="req-count">0</p>
96
- <div style="display: flex; gap: 8px">
97
- <small id="req-per-sec">0 req/s</small>
98
- <small id="avg-resp-time">Avg: 0ms</small>
112
+ <div class="flex gap-4">
113
+ <small class="text-main" title="Average response time">Avg: <span id="avg-resp-time" class="font-bold">0ms</span></small>
114
+ <small class="text-main" title="Maximum response time">Max: <span id="max-resp-time" class="font-bold">0ms</span></small>
99
115
  </div>
100
116
  </div>
101
- <div class="card">
117
+ <div class="card" title="Total number of requests that resulted in an error">
102
118
  <div class="card-header">
103
119
  <span class="material-symbols-rounded">report</span>
104
- <h3>Errors</h3>
120
+ <h3 title="Total number of requests that resulted in an error">Errors</h3>
105
121
  </div>
106
- <p id="err-count">0</p>
122
+ <p id="err-count" title="Total error count">0</p>
107
123
  </div>
108
- <div class="card">
124
+ <div class="card" title="Efficiency of the internal cache">
109
125
  <div class="card-header">
110
126
  <span class="material-symbols-rounded">cached</span>
111
- <h3>Cache Hit Rate</h3>
127
+ <h3 title="Efficiency of the internal cache">Cache Hit Rate</h3>
112
128
  </div>
113
- <p id="cache-hit-rate-val">0%</p>
114
- <small><span id="cache-hits-val">0</span> hits, <span id="cache-misses-val">0</span> misses</small>
129
+ <p id="cache-hit-rate-val" title="Percentage of requests served from cache">0%</p>
130
+ <small
131
+ ><span id="cache-hits-val" title="Total cache hits">0</span> hits,
132
+ <span id="cache-misses-val" title="Total cache misses">0</span> misses</small
133
+ >
115
134
  </div>
116
135
  </div>
117
136
 
118
137
  <div class="card" style="grid-column: span 2">
119
138
  <div class="card-header">
120
139
  <span class="material-symbols-rounded">monitoring</span>
121
- <h3>Traffic & Errors</h3>
140
+ <h3>TRAFFIC</h3>
122
141
  <div class="header-toolbar ml-auto">
123
142
  <select
124
143
  id="chart-history-points"
@@ -132,9 +151,14 @@
132
151
  padding: 2px 4px;
133
152
  "
134
153
  >
135
- <option value="30">Last 30 pts</option>
136
- <option value="60">Last 60 pts</option>
137
- <option value="120">Last 120 pts</option>
154
+ <option value="60">Last 1 min</option>
155
+ <option value="300">Last 5 min</option>
156
+ <option value="600" selected>Last 10 min</option>
157
+ <option value="1800">Last 30 min</option>
158
+ <option value="3600">Last 1 hour</option>
159
+ <option value="7200">Last 2 hours</option>
160
+ <option value="43200">Last 12 hours</option>
161
+ <option value="86400">Last 24 hours</option>
138
162
  </select>
139
163
  </div>
140
164
  </div>
@@ -158,24 +182,143 @@
158
182
  </section>
159
183
 
160
184
  <section id="view-errors" class="view" style="display: none">
185
+ <div class="header-toolbar mb-4" style="justify-content: flex-start; gap: 12px">
186
+ <div class="search-group" style="flex: 1; max-width: 400px">
187
+ <span
188
+ class="material-symbols-rounded"
189
+ style="position: absolute; left: 8px; top: 50%; transform: translateY(-50%); font-size: 18px; opacity: 0.5"
190
+ >search</span
191
+ >
192
+ <input
193
+ type="text"
194
+ id="error-filter"
195
+ placeholder="Filter errors (URL, message, method)..."
196
+ class="input-sm"
197
+ style="width: 100%; padding-left: 32px"
198
+ />
199
+ </div>
200
+ <div class="flex items-center gap-2">
201
+ <label for="error-limit" class="text-xs text-main">Max entries:</label>
202
+ <input type="number" id="error-limit" value="100" min="1" max="5000" class="input-sm" style="width: 100px" />
203
+ </div>
204
+ <button id="error-load-btn" class="btn btn-sm btn-primary">
205
+ <span class="material-symbols-rounded" style="font-size: 18px">refresh</span>
206
+ Load Errors
207
+ </button>
208
+ </div>
161
209
  <div class="table-container">
162
210
  <table id="errors-table">
163
- <thead>
164
- <tr>
165
- <th>Timestamp</th>
166
- <th>Method</th>
167
- <th>URL</th>
168
- <th>Message</th>
169
- </tr>
170
- </thead>
211
+ <thead></thead>
212
+ <tbody></tbody>
213
+ </table>
214
+ </div>
215
+ </section>
216
+
217
+ <section id="view-trace" class="view" style="display: none">
218
+ <div class="header-toolbar mb-4" style="justify-content: flex-start; gap: 12px">
219
+ <div class="search-group" style="flex: 1; max-width: 300px">
220
+ <span
221
+ class="material-symbols-rounded"
222
+ style="position: absolute; left: 8px; top: 50%; transform: translateY(-50%); font-size: 18px; opacity: 0.5"
223
+ >search</span
224
+ >
225
+ <input type="text" id="trace-filter" placeholder="Filter traces..." class="input-sm" style="width: 100%; padding-left: 32px" />
226
+ </div>
227
+ <div class="flex items-center gap-2">
228
+ <label for="trace-limit" class="text-xs text-main">Max entries:</label>
229
+ <input type="number" id="trace-limit" value="100" min="1" max="5000" class="input-sm" style="width: 80px" />
230
+ </div>
231
+ <button id="trace-load-btn" class="btn btn-sm btn-primary">
232
+ <span class="material-symbols-rounded" style="font-size: 18px">refresh</span>
233
+ Refresh
234
+ </button>
235
+ <button id="trace-clear-btn" class="btn btn-sm btn-danger">
236
+ <span class="material-symbols-rounded" style="font-size: 18px">delete</span>
237
+ Clear
238
+ </button>
239
+ <button
240
+ id="trace-status-toggle"
241
+ class="btn btn-sm px-4 py-2 rounded-lg text-sm font-bold transition-colors bg-white/10 text-white hover:bg-white/20"
242
+ >
243
+ Tracing: OFF
244
+ </button>
245
+ </div>
246
+ <div class="table-container">
247
+ <table id="trace-table">
248
+ <thead></thead>
171
249
  <tbody></tbody>
172
250
  </table>
173
251
  </div>
174
252
  </section>
175
253
 
176
254
  <section id="view-access" class="view" style="display: none">
255
+ <div class="header-toolbar mb-4" style="justify-content: flex-start; gap: 12px">
256
+ <div class="search-group" style="flex: 1; max-width: 400px">
257
+ <span
258
+ class="material-symbols-rounded"
259
+ style="position: absolute; left: 8px; top: 50%; transform: translateY(-50%); font-size: 18px; opacity: 0.5"
260
+ >search</span
261
+ >
262
+ <input type="text" id="access-filter" placeholder="Filter logs..." class="input-sm" style="width: 100%; padding-left: 32px" />
263
+ </div>
264
+ <div class="flex items-center gap-2">
265
+ <label for="access-limit" class="text-xs text-main">Max entries:</label>
266
+ <input type="number" id="access-limit" value="100" min="1" max="5000" class="input-sm" style="width: 100px" />
267
+ </div>
268
+ <button id="access-load-btn" class="btn btn-sm btn-primary">
269
+ <span class="material-symbols-rounded" style="font-size: 18px">refresh</span>
270
+ Load Logs
271
+ </button>
272
+ <small id="access-log-range" class="text-main" style="margin-left: auto">No logs loaded</small>
273
+ </div>
177
274
  <div class="terminal-container">
178
- <pre id="access-log-view" class="terminal"></pre>
275
+ <pre id="access-log-view" class="terminal" title="Real-time access logs from the server"></pre>
276
+ </div>
277
+ </section>
278
+
279
+ <section id="view-stats" class="view" style="display: none">
280
+ <div class="header-toolbar mb-4" style="justify-content: space-between">
281
+ <div class="flex items-center gap-4">
282
+ <small id="stats-history-info" class="text-main">Showing latest historical data points</small>
283
+ <select
284
+ id="stats-row-limit"
285
+ class="btn-sm"
286
+ style="
287
+ background: var(--bg-hover);
288
+ border: 1px solid var(--border);
289
+ border-radius: 4px;
290
+ color: var(--text-main);
291
+ font-size: 0.75rem;
292
+ padding: 2px 8px;
293
+ "
294
+ >
295
+ <option value="20">Latest 20</option>
296
+ <option value="50" selected>Latest 50</option>
297
+ <option value="100">Latest 100</option>
298
+ <option value="200">Latest 200</option>
299
+ <option value="0">All Points</option>
300
+ </select>
301
+ </div>
302
+ </div>
303
+ <div class="table-container">
304
+ <table id="stats-table">
305
+ <thead>
306
+ <tr>
307
+ <th title="Timestamp of the interval">Timestamp</th>
308
+ <th title="Requests in this interval" class="text-center">Req</th>
309
+ <th title="Errors in this interval" class="text-center">Err</th>
310
+ <th title="Min Latency (ms)" class="text-right">Min</th>
311
+ <th title="Avg Latency (ms)" class="text-right">Avg</th>
312
+ <th title="95th Percentile Latency (ms)" class="text-right">P95</th>
313
+ <th title="99th Percentile Latency (ms)" class="text-right">P99</th>
314
+ <th title="Max Latency (ms)" class="text-right">Max</th>
315
+ <th title="CPU Usage (%)" class="text-right">CPU %</th>
316
+ <th title="Resident Set Size (MB)" class="text-right">RSS</th>
317
+ <th title="Heap Used (MB)" class="text-right">Heap</th>
318
+ </tr>
319
+ </thead>
320
+ <tbody></tbody>
321
+ </table>
179
322
  </div>
180
323
  </section>
181
324
 
@@ -183,8 +326,8 @@
183
326
  <div id="config-view"></div>
184
327
  </section>
185
328
 
186
- <section id="view-system" class="view" style="display: none">
187
- <div class="metrics-grid mb-6">
329
+ <section id="view-system" class="view view-system-compact" style="display: none">
330
+ <div class="metrics-grid mb-4">
188
331
  <div class="card">
189
332
  <div class="card-header">
190
333
  <span class="material-symbols-rounded">info</span>
@@ -226,10 +369,6 @@
226
369
  <span id="system-status-text" class="text-accent font-bold">-</span>
227
370
  </span>
228
371
  </div>
229
- <div class="stat-row">
230
- <span>Auto-refresh</span>
231
- <span id="system-refresh-status" class="text-accent font-bold">Active (30s)</span>
232
- </div>
233
372
  </div>
234
373
  </div>
235
374
  <div class="card">
@@ -254,52 +393,84 @@
254
393
  </div>
255
394
  </div>
256
395
 
257
- <div class="metrics-grid mb-6">
258
- <div class="card">
396
+ <div class="metrics-grid">
397
+ <div class="card resource-card" title="Memory usage statistics for the Node.js process">
259
398
  <div class="card-header">
260
399
  <span class="material-symbols-rounded">memory</span>
261
- <h3>Memory Usage</h3>
400
+ <h3 title="Memory usage statistics for the Node.js process">Memory Usage</h3>
262
401
  </div>
263
- <div class="text-sm text-main">
264
- <div class="stat-row">
265
- <span>Heap Used</span>
266
- <span id="memory-heap-used" class="font-mono text-accent font-bold">-</span>
267
- </div>
268
- <div class="stat-row">
269
- <span>Heap Total</span>
270
- <span id="memory-heap-total" class="font-mono text-accent font-bold">-</span>
271
- </div>
272
- <div class="stat-row">
273
- <span>RSS</span>
274
- <span id="memory-rss" class="font-mono text-accent font-bold">-</span>
275
- </div>
276
- <div class="stat-row">
277
- <span>External</span>
278
- <span id="memory-external" class="font-mono text-accent font-bold">-</span>
402
+ <div class="stat-container-wrapper text-sm text-main">
403
+ <div class="stat-container">
404
+ <div class="stat-row">
405
+ <span>Memory Usage (%)</span>
406
+ <span id="memory-percent" class="font-mono text-accent font-bold">-</span>
407
+ </div>
408
+ <div class="stat-row" title="Memory currently used by Node.js objects">
409
+ <span>Heap Used</span>
410
+ <div class="stat-values">
411
+ <span id="memory-heap-used" class="font-mono text-accent font-bold">-</span>
412
+ <small class="min-max">max: <span id="memory-heap-used-max">-</span></small>
413
+ </div>
414
+ </div>
415
+ <div class="stat-row" title="Total memory allocated for the heap">
416
+ <span>Heap Total</span>
417
+ <div class="stat-values">
418
+ <span id="memory-heap-total" class="font-mono text-accent font-bold">-</span>
419
+ <small class="min-max">max: <span id="memory-heap-total-max">-</span></small>
420
+ </div>
421
+ </div>
422
+ <div class="stat-row" title="Resident Set Size - total memory used by the process">
423
+ <span>RSS</span>
424
+ <div class="stat-values">
425
+ <span id="memory-rss" class="font-mono text-accent font-bold">-</span>
426
+ <small class="min-max">max: <span id="memory-rss-max">-</span></small>
427
+ </div>
428
+ </div>
279
429
  </div>
280
430
  </div>
431
+ <div class="chart-container-mini">
432
+ <canvas id="memory-chart"></canvas>
433
+ </div>
281
434
  </div>
282
- <div class="card">
435
+ <div class="card resource-card" title="CPU time consumed by the process">
283
436
  <div class="card-header">
284
437
  <span class="material-symbols-rounded">speed</span>
285
- <h3>CPU Usage</h3>
438
+ <h3 title="CPU time consumed by the process">CPU Usage</h3>
286
439
  </div>
287
- <div class="text-sm text-main">
288
- <div class="stat-row">
289
- <span>User</span>
290
- <span id="cpu-user" class="font-mono text-accent font-bold">-</span>
291
- </div>
292
- <div class="stat-row">
293
- <span>System</span>
294
- <span id="cpu-system" class="font-mono text-accent font-bold">-</span>
440
+ <div class="stat-container-wrapper text-sm text-main">
441
+ <div class="stat-container">
442
+ <div class="stat-row">
443
+ <span>CPU Usage (%)</span>
444
+ <span id="cpu-percent" class="font-mono text-accent font-bold">-</span>
445
+ </div>
446
+ <div class="stat-row" title="Time spent in user code">
447
+ <span>User</span>
448
+ <div class="stat-values">
449
+ <span id="cpu-user" class="font-mono text-accent font-bold">-</span>
450
+ <small class="min-max">max: <span id="cpu-user-max">-</span></small>
451
+ </div>
452
+ </div>
453
+ <div class="stat-row" title="Time spent in system (kernel) code">
454
+ <span>System</span>
455
+ <div class="stat-values">
456
+ <span id="cpu-system" class="font-mono text-accent font-bold">-</span>
457
+ <small class="min-max">max: <span id="cpu-system-max">-</span></small>
458
+ </div>
459
+ </div>
460
+ <div class="stat-row">
461
+ <span>Hardware</span>
462
+ <span id="cpu-cores-info" class="text-accent font-bold">-</span>
463
+ </div>
295
464
  </div>
296
465
  </div>
466
+ <div class="chart-container-mini">
467
+ <canvas id="cpu-chart"></canvas>
468
+ </div>
297
469
  </div>
298
470
  </div>
299
471
 
300
472
  <div class="card danger-zone">
301
473
  <h3 class="danger-zone-title">Danger Zone</h3>
302
- <p class="danger-zone-text">Careful! These actions affect the production server state.</p>
303
474
  <div style="display: flex; gap: 12px; margin-top: 16px">
304
475
  <button id="btn-clear-all-cache" class="btn btn-sm btn-warning">Clear All Caches</button>
305
476
  <button id="btn-pause" class="btn btn-sm btn-warning">Pause Server</button>
@@ -310,6 +481,49 @@
310
481
  </section>
311
482
  </main>
312
483
  </div>
484
+ <div id="error-modal" class="modal" style="display: none">
485
+ <div class="modal-content">
486
+ <div class="modal-header">
487
+ <h3>Error Details</h3>
488
+ <button class="icon-btn" onclick="document.getElementById('error-modal').style.display = 'none'">
489
+ <span class="material-symbols-rounded">close</span>
490
+ </button>
491
+ </div>
492
+ <div class="modal-body">
493
+ <pre id="error-detail-content" class="terminal" style="max-height: 70vh; overflow: auto"></pre>
494
+ </div>
495
+ </div>
496
+ </div>
497
+
498
+ <div id="trace-modal" class="modal" style="display: none">
499
+ <div class="modal-content">
500
+ <div class="modal-header">
501
+ <h3>Trace Details</h3>
502
+ <button class="icon-btn" onclick="document.getElementById('trace-modal').style.display = 'none'">
503
+ <span class="material-symbols-rounded">close</span>
504
+ </button>
505
+ </div>
506
+ <div class="modal-body">
507
+ <pre id="trace-detail-content" class="terminal" style="max-height: 70vh; overflow: auto"></pre>
508
+ </div>
509
+ </div>
510
+ </div>
511
+
512
+ <div id="offline-modal" class="modal" style="display: none">
513
+ <div class="modal-content" style="max-width: 400px; text-align: center">
514
+ <div class="modal-body" style="display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 32px">
515
+ <span class="material-symbols-rounded" style="font-size: 48px; color: var(--danger)">wifi_off</span>
516
+ <h3 style="margin: 0; font-size: 1.25rem; color: var(--text-bright)">Connection Lost</h3>
517
+ <p style="margin: 0; color: var(--text-main)">The server is unreachable. Please check your connection or server status.</p>
518
+ <p id="offline-countdown" style="margin: 0; color: var(--text-main); font-size: 0.875rem"></p>
519
+ <button id="btn-reconnect" class="btn btn-primary btn-lg w-full justify-center">
520
+ <span id="reconnect-icon" class="material-symbols-rounded">refresh</span>
521
+ <span id="reconnect-spinner" class="material-symbols-rounded animate-spin" style="display: none">sync</span>
522
+ <span id="reconnect-text">Try Reconnect</span>
523
+ </button>
524
+ </div>
525
+ </div>
526
+ </div>
313
527
  <script type="module" src="lib/chart.bundle.js"></script>
314
528
  </body>
315
529
  </html>
@@ -1,14 +1,14 @@
1
1
  import {z} from 'zod';
2
2
  import {
3
3
  statusSchema,
4
- cacheDataSchema,
5
4
  errorLogSchema,
6
5
  accessLogResponseSchema,
6
+ traceEntrySchema,
7
7
  type StatusResponse,
8
- type CacheDataResponse,
9
8
  type ErrorLogResponse,
10
9
  type AccessLogResponse,
11
10
  } from './schemas.js';
11
+ import type {TraceEntry} from './types.js';
12
12
 
13
13
  /**
14
14
  * Validates response data against a Zod schema.
@@ -26,6 +26,28 @@ function validate<T>(data: unknown, schema: z.ZodType<T>, path: string): T {
26
26
  return result.data;
27
27
  }
28
28
 
29
+ /**
30
+ * Fetch with retry logic.
31
+ *
32
+ * @param url - The URL to fetch.
33
+ * @param options - Fetch options.
34
+ * @param retries - Number of retries (default 2).
35
+ * @param delay - Delay between retries in ms (default 500).
36
+ * @returns The response.
37
+ */
38
+ async function fetchWithRetry(url: string, options?: RequestInit, retries = 2, delay = 500): Promise<Response> {
39
+ for (let i = 0; i <= retries; i++) {
40
+ try {
41
+ const res = await fetch(url, options);
42
+ return res;
43
+ } catch (err) {
44
+ if (i === retries) throw err;
45
+ await new Promise((resolve) => setTimeout(resolve, delay));
46
+ }
47
+ }
48
+ throw new Error('Unreachable');
49
+ }
50
+
29
51
  /**
30
52
  * Typed API client with Zod validation.
31
53
  */
@@ -36,31 +58,25 @@ export const typedApi = {
36
58
  * @returns The validated status response.
37
59
  */
38
60
  async getStatus(): Promise<StatusResponse> {
39
- const res = await fetch('api/status');
61
+ const res = await fetchWithRetry('api/status');
40
62
  if (!res.ok) throw new Error(`GET api/status failed: ${res.statusText}`);
41
63
  const data: unknown = await res.json();
42
64
  return validate(data, statusSchema, 'api/status');
43
65
  },
44
66
 
45
- /**
46
- * Get cache data with validation.
47
- *
48
- * @returns The validated cache data response.
49
- */
50
- async getCache(): Promise<CacheDataResponse[]> {
51
- const res = await fetch('api/cache');
52
- if (!res.ok) throw new Error(`GET api/cache failed: ${res.statusText}`);
53
- const data: unknown = await res.json();
54
- return validate(data, z.array(cacheDataSchema), 'api/cache');
55
- },
56
-
57
67
  /**
58
68
  * Get error logs with validation.
59
69
  *
70
+ * @param limit - Max number of entries.
71
+ * @param filter - Optional filter string.
60
72
  * @returns The validated error log response.
61
73
  */
62
- async getErrorLogs(): Promise<ErrorLogResponse[]> {
63
- const res = await fetch('api/logs/error');
74
+ async getErrorLogs(limit = 100, filter = ''): Promise<ErrorLogResponse[]> {
75
+ const query = new URLSearchParams({
76
+ limit: limit.toString(),
77
+ filter,
78
+ });
79
+ const res = await fetchWithRetry(`api/logs/error?${query.toString()}`);
64
80
  if (!res.ok) throw new Error(`GET api/logs/error failed: ${res.statusText}`);
65
81
  const data: unknown = await res.json();
66
82
  return validate(data, z.array(errorLogSchema), 'api/logs/error');
@@ -69,15 +85,74 @@ export const typedApi = {
69
85
  /**
70
86
  * Get access logs with validation.
71
87
  *
88
+ * @param limit - Max number of entries.
89
+ * @param filter - Optional filter string.
72
90
  * @returns The validated access log response.
73
91
  */
74
- async getAccessLogs(): Promise<AccessLogResponse> {
75
- const res = await fetch('api/logs/access');
92
+ async getAccessLogs(limit = 100, filter = ''): Promise<AccessLogResponse> {
93
+ const query = new URLSearchParams({
94
+ limit: limit.toString(),
95
+ filter,
96
+ });
97
+ const res = await fetchWithRetry(`api/logs/access?${query.toString()}`);
76
98
  if (!res.ok) throw new Error(`GET api/logs/access failed: ${res.statusText}`);
77
99
  const data: unknown = await res.json();
78
100
  return validate(data, accessLogResponseSchema, 'api/logs/access');
79
101
  },
80
102
 
103
+ /**
104
+ * Get trace logs with validation.
105
+ *
106
+ * @param limit - Max number of entries.
107
+ * @param filter - Optional filter string.
108
+ * @returns The validated trace log response.
109
+ */
110
+ async getTraceLogs(limit = 100, filter = ''): Promise<TraceEntry[]> {
111
+ const query = new URLSearchParams({
112
+ limit: limit.toString(),
113
+ filter,
114
+ });
115
+ const res = await fetchWithRetry(`api/trace/logs?${query.toString()}`);
116
+ if (!res.ok) throw new Error(`GET api/trace/logs failed: ${res.statusText}`);
117
+ const data: unknown = await res.json();
118
+ return validate(data, z.array(traceEntrySchema), 'api/trace/logs');
119
+ },
120
+
121
+ /**
122
+ * Get trace status.
123
+ *
124
+ * @returns Trace status.
125
+ */
126
+ async getTraceStatus(): Promise<{enabled: boolean}> {
127
+ const res = await fetchWithRetry('api/trace/status');
128
+ if (!res.ok) throw new Error(`GET api/trace/status failed: ${res.statusText}`);
129
+ return (await res.json()) as {enabled: boolean};
130
+ },
131
+
132
+ /**
133
+ * Toggle trace status.
134
+ *
135
+ * @param enabled - Enable or disable.
136
+ * @returns New status.
137
+ */
138
+ async toggleTrace(enabled: boolean): Promise<{enabled: boolean}> {
139
+ const res = await fetchWithRetry('api/trace/toggle', {
140
+ method: 'POST',
141
+ headers: {'Content-Type': 'application/json'},
142
+ body: JSON.stringify({enabled}),
143
+ });
144
+ if (!res.ok) throw new Error(`POST api/trace/toggle failed: ${res.statusText}`);
145
+ return (await res.json()) as {enabled: boolean};
146
+ },
147
+
148
+ /**
149
+ * Clear trace logs.
150
+ */
151
+ async clearTraces(): Promise<void> {
152
+ const res = await fetchWithRetry('api/trace/clear', {method: 'POST'});
153
+ if (!res.ok) throw new Error(`POST api/trace/clear failed: ${res.statusText}`);
154
+ },
155
+
81
156
  /**
82
157
  * POST to an endpoint without response validation.
83
158
  *
@@ -85,7 +160,7 @@ export const typedApi = {
85
160
  * @param body - The request body.
86
161
  */
87
162
  async post(path: string, body: object = {}): Promise<void> {
88
- const res = await fetch(path, {
163
+ const res = await fetchWithRetry(path, {
89
164
  method: 'POST',
90
165
  headers: {'Content-Type': 'application/json'},
91
166
  body: JSON.stringify(body),