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
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Column configuration for the DataTable.
3
+ */
4
+ export type TableColumn<T> = {
5
+ /** Unique identifier for the column */
6
+ id: string;
7
+ /** Header text to display */
8
+ header: string;
9
+ /** Tooltip for the column header */
10
+ headerTitle?: string;
11
+ /** Optional fixed width (e.g., '80px') */
12
+ width?: string;
13
+ /** Text alignment */
14
+ align?: 'left' | 'center' | 'right';
15
+ /** Custom CSS classes for the cell */
16
+ className?: string | ((row: T) => string);
17
+ /** Function to get the cell content (HTML string) */
18
+ render?: (row: T) => string;
19
+ /** Function to get the raw value (text content) */
20
+ accessor?: (row: T) => string | number | undefined | null;
21
+ /** Tooltip for the data cell */
22
+ cellTitle?: (row: T) => string;
23
+ };
24
+
25
+ /**
26
+ * Options for the DataTable.
27
+ */
28
+ export type TableOptions<T> = {
29
+ /** Array of column definitions */
30
+ columns: TableColumn<T>[];
31
+ /** Enable compact/dense layout */
32
+ dense?: boolean;
33
+ /** Callback when a row is clicked */
34
+ onRowClick?: (row: T) => void;
35
+ };
36
+
37
+ /**
38
+ * A reusable, configuration-driven data table component.
39
+ */
40
+ export class DataTable<T> {
41
+ private element: HTMLTableElement | null = null;
42
+
43
+ /**
44
+ * Creates a new DataTable instance.
45
+ *
46
+ * @param elementId - The ID of the table element in the DOM.
47
+ * @param options - Configuration options for the table.
48
+ */
49
+ constructor(
50
+ private elementId: string,
51
+ private options: TableOptions<T>,
52
+ ) {}
53
+
54
+ /**
55
+ * Mounts the table to the DOM and renders the provided data.
56
+ *
57
+ * @param data - The array of data to display.
58
+ */
59
+ render(data: T[]): void {
60
+ this.element = document.getElementById(this.elementId) as HTMLTableElement;
61
+ if (!this.element) return;
62
+
63
+ // Apply dense class if enabled
64
+ if (this.options.dense) {
65
+ this.element.classList.add('dense-table');
66
+ } else {
67
+ this.element.classList.remove('dense-table');
68
+ }
69
+
70
+ this.renderHeader();
71
+ this.renderBody(data);
72
+ }
73
+
74
+ /**
75
+ * Renders the table header.
76
+ */
77
+ private renderHeader(): void {
78
+ if (!this.element) return;
79
+
80
+ let thead = this.element.querySelector('thead');
81
+ if (!thead) {
82
+ thead = document.createElement('thead');
83
+ this.element.appendChild(thead);
84
+ }
85
+
86
+ thead.innerHTML = '';
87
+ const tr = document.createElement('tr');
88
+
89
+ this.options.columns.forEach((col) => {
90
+ const th = document.createElement('th');
91
+ th.textContent = col.header;
92
+ if (col.headerTitle) {
93
+ th.title = col.headerTitle;
94
+ }
95
+ if (col.width) {
96
+ th.style.width = col.width;
97
+ }
98
+ if (col.align) {
99
+ th.style.textAlign = col.align;
100
+ }
101
+ tr.appendChild(th);
102
+ });
103
+
104
+ thead.appendChild(tr);
105
+ }
106
+
107
+ /**
108
+ * Renders the table body.
109
+ *
110
+ * @param data - The data to render.
111
+ */
112
+ private renderBody(data: T[]): void {
113
+ if (!this.element) return;
114
+
115
+ let tbody = this.element.querySelector('tbody');
116
+ if (!tbody) {
117
+ tbody = document.createElement('tbody');
118
+ this.element.appendChild(tbody);
119
+ }
120
+
121
+ tbody.innerHTML = '';
122
+
123
+ data.forEach((row) => {
124
+ const tr = document.createElement('tr');
125
+
126
+ // Add row click handler if provided
127
+ if (this.options.onRowClick) {
128
+ tr.style.cursor = 'pointer';
129
+ tr.addEventListener('click', () => {
130
+ if (this.options.onRowClick) {
131
+ this.options.onRowClick(row);
132
+ }
133
+ });
134
+ // Add hover effect class usually handled by CSS, but good to ensure
135
+ tr.classList.add('table-row-hover');
136
+ }
137
+
138
+ this.options.columns.forEach((col) => {
139
+ const td = document.createElement('td');
140
+
141
+ // Content
142
+ if (col.render) {
143
+ td.innerHTML = col.render(row);
144
+ } else if (col.accessor) {
145
+ const val = col.accessor(row);
146
+ td.textContent = val !== undefined && val !== null ? String(val) : '';
147
+ }
148
+
149
+ // Styling
150
+ if (col.className) {
151
+ const className = typeof col.className === 'function' ? col.className(row) : col.className;
152
+ if (className) {
153
+ td.className = className;
154
+ }
155
+ }
156
+
157
+ // Alignment
158
+ if (col.align) {
159
+ td.style.textAlign = col.align;
160
+ }
161
+
162
+ // Tooltip
163
+ if (col.cellTitle) {
164
+ td.title = col.cellTitle(row);
165
+ }
166
+
167
+ tr.appendChild(td);
168
+ });
169
+
170
+ tbody.appendChild(tr);
171
+ });
172
+ }
173
+ }
@@ -1,34 +1,159 @@
1
1
  import {typedApi} from '../api.js';
2
- import {formatDuration, formatDateTime} from '../util/format.js';
3
- import {errorRow, poolCard} from '../templates/index.js';
2
+ import {formatDuration, formatDateTime, formatMs} from '../util/format.js';
3
+ import {poolCard} from '../templates/index.js';
4
4
  import {renderConfig} from '../templates/config.js';
5
- import type {State, ServerConfig} from '../types.js';
5
+ import type {State, ServerConfig, SystemMetrics, ErrorLogResponse, TraceEntry} from '../types.js';
6
+ import {DataTable, type TableColumn} from './table.js';
6
7
 
7
8
  /**
8
9
  * Refresh the error logs view.
9
10
  */
10
11
  export async function refreshErrors(): Promise<void> {
11
- const logs = await typedApi.getErrorLogs();
12
+ const filterInput = document.getElementById('error-filter') as HTMLInputElement | null;
13
+ const limitInput = document.getElementById('error-limit') as HTMLInputElement | null;
14
+
15
+ const filter = filterInput?.value ?? '';
16
+ const limit = limitInput ? parseInt(limitInput.value) : 100;
17
+
18
+ const logs = await typedApi.getErrorLogs(limit, filter);
19
+
20
+ const columns: TableColumn<ErrorLogResponse>[] = [
21
+ {
22
+ id: 'timestamp',
23
+ header: 'Timestamp',
24
+ headerTitle: 'When the error occurred',
25
+ accessor: (l) => new Date(l.timestamp).toLocaleString(),
26
+ cellTitle: (l) => new Date(l.timestamp).toLocaleString(),
27
+ },
28
+ {
29
+ id: 'method',
30
+ header: 'Method',
31
+ headerTitle: 'HTTP method used',
32
+ render: (l) => `<code>${l.req?.method ?? '-'}</code>`,
33
+ cellTitle: (l) => `Method: ${l.req?.method ?? '-'}`,
34
+ },
35
+ {
36
+ id: 'url',
37
+ header: 'URL',
38
+ headerTitle: 'Requested URL',
39
+ accessor: (l) => l.req?.url ?? '-',
40
+ cellTitle: (l) => `URL: ${l.req?.url ?? '-'}`,
41
+ },
42
+ {
43
+ id: 'message',
44
+ header: 'Message',
45
+ headerTitle: 'Error message summary',
46
+ accessor: (l) => l.message,
47
+ className: 'break-all',
48
+ cellTitle: (l) => `Click for details: ${l.details?.fullMessage ?? ''}`,
49
+ },
50
+ {
51
+ id: 'action',
52
+ header: '',
53
+ width: '40px',
54
+ align: 'center',
55
+ render: () => '<span class="material-symbols-rounded">chevron_right</span>',
56
+ className: 'text-center text-accent opacity-70',
57
+ },
58
+ ];
59
+
60
+ const table = new DataTable<ErrorLogResponse>('errors-table', {
61
+ columns,
62
+ dense: true,
63
+ onRowClick: (log) => {
64
+ const modal = document.getElementById('error-modal');
65
+ const content = document.getElementById('error-detail-content');
66
+ if (modal && content) {
67
+ content.textContent = JSON.stringify(log, null, 2);
68
+ modal.style.display = 'flex';
69
+ }
70
+ },
71
+ });
72
+
73
+ table.render(logs);
74
+
75
+ // Add class to rows for styling (legacy support if needed, or handled by table component)
12
76
  const tbody = document.querySelector('#errors-table tbody');
13
- if (!tbody) return;
77
+ if (tbody) {
78
+ tbody.querySelectorAll('tr').forEach((row) => {
79
+ row.classList.add('errors-table-row');
80
+ });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Refresh the statistical history view.
86
+ *
87
+ * @param status - The status data.
88
+ */
89
+ export function refreshStats(status: Partial<State['status']>): void {
90
+ const tbody = document.querySelector('#stats-table tbody');
91
+ const limitSelect = document.getElementById('stats-row-limit') as HTMLSelectElement | null;
92
+ const infoEl = document.getElementById('stats-history-info');
93
+ if (!tbody || !status.history) return;
94
+
95
+ const limit = limitSelect ? parseInt(limitSelect.value) : 50;
96
+ const history = [...status.history].reverse();
97
+ const displayed = limit > 0 ? history.slice(0, limit) : history;
98
+
99
+ if (infoEl) {
100
+ infoEl.textContent = `Showing latest ${displayed.length} of ${history.length} data points`;
101
+ }
102
+
103
+ tbody.innerHTML = displayed
104
+ .map((b) => {
105
+ const time = new Date(b.timestamp).toLocaleTimeString();
106
+ const rss = (b.system.rss / 1024 / 1024).toFixed(1);
107
+ const heap = (b.system.heapUsed / 1024 / 1024).toFixed(1);
14
108
 
15
- tbody.innerHTML = logs.map((l) => errorRow(l)).join('');
109
+ return `
110
+ <tr>
111
+ <td class="font-mono text-xs">${time}</td>
112
+ <td class="text-center font-bold text-bright">${b.requests}</td>
113
+ <td class="text-center font-bold ${b.errors > 0 ? 'text-danger' : 'text-success'}">${b.errors}</td>
114
+ <td class="text-right font-mono">${b.durationMin.toFixed(1)}</td>
115
+ <td class="text-right font-mono text-accent font-bold">${b.durationAvg.toFixed(1)}</td>
116
+ <td class="text-right font-mono text-warning">${b.durationP95.toFixed(1)}</td>
117
+ <td class="text-right font-mono text-danger">${b.durationP99.toFixed(1)}</td>
118
+ <td class="text-right font-mono">${b.durationMax.toFixed(1)}</td>
119
+ <td class="text-right font-mono">${b.system.cpu.toFixed(1)}%</td>
120
+ <td class="text-right font-mono">${rss}</td>
121
+ <td class="text-right font-mono">${heap}</td>
122
+ </tr>
123
+ `;
124
+ })
125
+ .join('');
16
126
  }
17
127
 
18
128
  /**
19
129
  * Refresh the access logs view.
20
130
  */
21
131
  export async function refreshAccess(): Promise<void> {
22
- const result = await typedApi.getAccessLogs();
132
+ const filterInput = document.getElementById('access-filter') as HTMLInputElement | null;
133
+ const limitInput = document.getElementById('access-limit') as HTMLInputElement | null;
134
+
135
+ const filter = filterInput?.value ?? '';
136
+ const limit = limitInput ? parseInt(limitInput.value) : 100;
137
+
138
+ const result = await typedApi.getAccessLogs(limit, filter);
23
139
  const el = document.getElementById('access-log-view');
140
+ const rangeEl = document.getElementById('access-log-range');
24
141
  if (!el) return;
25
142
 
143
+ let logCount = 0;
26
144
  if (Array.isArray(result)) {
27
145
  el.textContent = result.join('\n');
146
+ logCount = result.length;
28
147
  } else if (result && typeof result === 'object' && 'message' in result) {
29
148
  el.textContent = result.message;
149
+ logCount = 0;
30
150
  } else {
31
151
  el.textContent = 'No logs available';
152
+ logCount = 0;
153
+ }
154
+
155
+ if (rangeEl) {
156
+ rangeEl.textContent = logCount > 0 ? `Showing last ${logCount} log entries` : 'No logs available';
32
157
  }
33
158
 
34
159
  if (el.parentElement) {
@@ -36,6 +161,106 @@ export async function refreshAccess(): Promise<void> {
36
161
  }
37
162
  }
38
163
 
164
+ /**
165
+ * Refresh the trace logs view.
166
+ */
167
+ export async function refreshTrace(): Promise<void> {
168
+ const filterInput = document.getElementById('trace-filter') as HTMLInputElement | null;
169
+ const limitInput = document.getElementById('trace-limit') as HTMLInputElement | null;
170
+ const statusToggle = document.getElementById('trace-status-toggle') as HTMLButtonElement | null;
171
+
172
+ const filter = filterInput?.value ?? '';
173
+ const limit = limitInput ? parseInt(limitInput.value) : 100;
174
+
175
+ // Update status toggle
176
+ try {
177
+ const {enabled} = await typedApi.getTraceStatus();
178
+ if (statusToggle) {
179
+ statusToggle.textContent = enabled ? 'Tracing: ON' : 'Tracing: OFF';
180
+ statusToggle.dataset.enabled = String(enabled);
181
+ statusToggle.className = `px-4 py-2 rounded-lg text-sm font-bold transition-colors ${
182
+ enabled ? 'bg-success text-black hover:bg-success/80' : 'bg-white/10 text-white hover:bg-white/20'
183
+ }`;
184
+ }
185
+ } catch (err) {
186
+ console.error('Failed to get trace status', err);
187
+ }
188
+
189
+ const logs = await typedApi.getTraceLogs(limit, filter);
190
+
191
+ const columns: TableColumn<TraceEntry>[] = [
192
+ {
193
+ id: 'timestamp',
194
+ header: 'Timestamp',
195
+ headerTitle: 'Time of request',
196
+ accessor: (l) => new Date(l.timestamp).toLocaleString(),
197
+ className: 'font-mono text-xs text-dim',
198
+ },
199
+ {
200
+ id: 'source',
201
+ header: 'Source',
202
+ headerTitle: 'Request source (IP/User)',
203
+ accessor: (l) => l.source,
204
+ className: 'text-dim text-xs truncate max-w-[150px]',
205
+ cellTitle: (l) => l.source,
206
+ },
207
+ {
208
+ id: 'method',
209
+ header: 'Method',
210
+ headerTitle: 'HTTP Method',
211
+ accessor: (l) => l.method,
212
+ className: 'font-mono text-xs',
213
+ },
214
+ {
215
+ id: 'url',
216
+ header: 'Request URL',
217
+ headerTitle: 'Request URL',
218
+ accessor: (l) => l.url,
219
+ className: 'text-bright font-mono text-xs truncate max-w-[300px]',
220
+ cellTitle: (l) => l.url,
221
+ },
222
+ {
223
+ id: 'status',
224
+ header: 'Status',
225
+ headerTitle: 'HTTP Status Code',
226
+ width: '80px',
227
+ accessor: (l) => l.status,
228
+ className: (l) => `font-bold text-xs uppercase ${l.status === 'success' ? 'text-success' : 'text-danger'}`,
229
+ },
230
+ {
231
+ id: 'duration',
232
+ header: 'Duration',
233
+ headerTitle: 'Execution time in ms',
234
+ align: 'right',
235
+ accessor: (l) => formatMs(l.duration),
236
+ className: 'font-mono text-xs text-dim',
237
+ },
238
+ {
239
+ id: 'procedure',
240
+ header: 'Procedure',
241
+ headerTitle: 'Executed PL/SQL procedure',
242
+ accessor: (l) => l.procedure ?? '-',
243
+ className: 'text-dim text-xs truncate max-w-[200px]',
244
+ cellTitle: (l) => l.procedure ?? '',
245
+ },
246
+ ];
247
+
248
+ const table = new DataTable<TraceEntry>('trace-table', {
249
+ columns,
250
+ dense: true,
251
+ onRowClick: (log) => {
252
+ const modal = document.getElementById('trace-modal');
253
+ const content = document.getElementById('trace-detail-content');
254
+ if (modal && content) {
255
+ content.textContent = JSON.stringify(log, null, 2);
256
+ modal.style.display = 'flex';
257
+ }
258
+ },
259
+ });
260
+
261
+ table.render(logs);
262
+ }
263
+
39
264
  /**
40
265
  * Refresh the pools view.
41
266
  *
@@ -79,30 +304,39 @@ function formatBytes(bytes: number): string {
79
304
  }
80
305
 
81
306
  /**
82
- * Format microseconds to milliseconds.
307
+ * Format microseconds to a human-readable duration.
83
308
  *
84
309
  * @param microseconds - CPU time in microseconds.
85
310
  * @returns Formatted string.
86
311
  */
87
312
  function formatCpuTime(microseconds: number): string {
88
- const milliseconds = microseconds / 1000;
89
- return `${milliseconds.toFixed(2)} ms`;
313
+ return formatMs(microseconds / 1000);
90
314
  }
91
315
 
92
316
  /**
93
317
  * Refresh the system info view.
94
318
  *
95
319
  * @param status - The status data.
320
+ * @param _state - The application state.
96
321
  */
97
- export function refreshSystem(status: Partial<State['status']>): void {
322
+ export function refreshSystem(status: Partial<State['status']>, _state?: State): void {
98
323
  const middlewareVersion = document.getElementById('middleware-version');
99
- if (middlewareVersion && status.version) middlewareVersion.textContent = status.version;
324
+ if (middlewareVersion && status.version) {
325
+ middlewareVersion.textContent = `${status.version} (${__BUILD_TIME__})`;
326
+ middlewareVersion.title = 'The version of the web_plsql gateway and admin console build time';
327
+ }
100
328
 
101
329
  const nodeVersion = document.getElementById('node-version');
102
- if (nodeVersion && status.system) nodeVersion.textContent = status.system.nodeVersion;
330
+ if (nodeVersion && status.system) {
331
+ nodeVersion.textContent = status.system.nodeVersion;
332
+ nodeVersion.title = 'The version of the Node.js runtime';
333
+ }
103
334
 
104
335
  const platform = document.getElementById('platform-info');
105
- if (platform && status.system) platform.textContent = `${status.system.platform} (${status.system.arch})`;
336
+ if (platform && status.system) {
337
+ platform.textContent = `${status.system.platform} (${status.system.arch})`;
338
+ platform.title = 'The operating system and architecture';
339
+ }
106
340
 
107
341
  const systemUptime = document.getElementById('system-uptime');
108
342
  if (systemUptime && status.uptime) systemUptime.textContent = formatDuration(status.uptime);
@@ -114,51 +348,80 @@ export function refreshSystem(status: Partial<State['status']>): void {
114
348
  if (systemStatusText) systemStatusText.textContent = status.status?.toUpperCase() ?? '-';
115
349
 
116
350
  const totalRequests = document.getElementById('total-requests');
117
- if (totalRequests && status.metrics) totalRequests.textContent = status.metrics.requestCount.toLocaleString();
351
+ if (totalRequests && status.metrics) {
352
+ totalRequests.textContent = status.metrics.requestCount.toLocaleString();
353
+ totalRequests.title = 'Cumulative number of requests handled by the server';
354
+ }
118
355
 
119
356
  const totalErrors = document.getElementById('total-errors');
120
- if (totalErrors && status.metrics) totalErrors.textContent = status.metrics.errorCount.toLocaleString();
357
+ if (totalErrors && status.metrics) {
358
+ totalErrors.textContent = status.metrics.errorCount.toLocaleString();
359
+ totalErrors.title = 'Cumulative number of failed requests';
360
+ }
121
361
 
122
362
  const activePools = document.getElementById('active-pools');
123
- if (activePools && status.pools) activePools.textContent = status.pools.length.toString();
363
+ if (activePools && status.pools) {
364
+ activePools.textContent = status.pools.length.toString();
365
+ activePools.title = 'Number of database connection pools currently active';
366
+ }
124
367
 
125
- // Memory usage
126
- if (status.system?.memory) {
127
- const memHeapUsed = document.getElementById('memory-heap-used');
128
- if (memHeapUsed) memHeapUsed.textContent = formatBytes(status.system.memory.heapUsed);
368
+ // Memory and CPU usage with Server-provided Min/Max
369
+ if (status.system) {
370
+ const system = status.system;
371
+ const metrics = {
372
+ heapUsed: system.memory.heapUsed,
373
+ heapTotal: system.memory.heapTotal,
374
+ rss: system.memory.rss,
375
+ external: system.memory.external,
376
+ cpuUser: system.cpu.user,
377
+ cpuSystem: system.cpu.system,
378
+ };
129
379
 
130
- const memHeapTotal = document.getElementById('memory-heap-total');
131
- if (memHeapTotal) memHeapTotal.textContent = formatBytes(status.system.memory.heapTotal);
380
+ const updateMem = (id: string, key: keyof SystemMetrics, maxKey: keyof typeof system.memory) => {
381
+ const val = metrics[key];
382
+ const max = system.memory[maxKey];
383
+ const el = document.getElementById(id);
384
+ const maxEl = document.getElementById(`${id}-max`);
385
+ if (el) el.textContent = formatBytes(val);
386
+ if (maxEl && typeof max === 'number') maxEl.textContent = formatBytes(max);
387
+ };
132
388
 
133
- const memRss = document.getElementById('memory-rss');
134
- if (memRss) memRss.textContent = formatBytes(status.system.memory.rss);
389
+ const updateCpu = (id: string, key: keyof SystemMetrics, maxKey?: 'userMax' | 'systemMax') => {
390
+ const val = metrics[key];
391
+ const el = document.getElementById(id);
392
+ if (el) el.textContent = formatCpuTime(val);
135
393
 
136
- const memExternal = document.getElementById('memory-external');
137
- if (memExternal) memExternal.textContent = formatBytes(status.system.memory.external);
138
- }
394
+ if (maxKey) {
395
+ const max = system.cpu[maxKey];
396
+ const maxEl = document.getElementById(`${id}-max`);
397
+ if (maxEl && typeof max === 'number') maxEl.textContent = formatCpuTime(max);
398
+ }
399
+ };
139
400
 
140
- // CPU usage
141
- if (status.system?.cpu) {
142
- const cpuUser = document.getElementById('cpu-user');
143
- if (cpuUser) cpuUser.textContent = formatCpuTime(status.system.cpu.user);
401
+ updateMem('memory-heap-used', 'heapUsed', 'heapUsedMax');
402
+ updateMem('memory-heap-total', 'heapTotal', 'heapTotalMax');
403
+ updateMem('memory-rss', 'rss', 'rssMax');
404
+ updateMem('memory-external', 'external', 'externalMax');
405
+ updateCpu('cpu-user', 'cpuUser', 'userMax');
406
+ updateCpu('cpu-system', 'cpuSystem', 'systemMax');
144
407
 
145
- const cpuSystem = document.getElementById('cpu-system');
146
- if (cpuSystem) cpuSystem.textContent = formatCpuTime(status.system.cpu.system);
147
- }
408
+ const cpuPercentEl = document.getElementById('cpu-percent');
409
+ if (cpuPercentEl && system.cpu.max !== undefined) {
410
+ const totalPercent = system.cpu.max;
411
+ cpuPercentEl.textContent = `${totalPercent.toFixed(1)}%`;
412
+ cpuPercentEl.title = `Max System CPU usage`;
413
+ }
414
+
415
+ const memoryPercentEl = document.getElementById('memory-percent');
416
+ if (memoryPercentEl && system.memory.totalMemory) {
417
+ const pct = (system.memory.rss / system.memory.totalMemory) * 100;
418
+ memoryPercentEl.textContent = `${pct.toFixed(1)}%`;
419
+ memoryPercentEl.title = `System Memory Used relative to Total (${formatBytes(system.memory.totalMemory)})`;
420
+ }
148
421
 
149
- const systemRefreshStatus = document.getElementById('system-refresh-status');
150
- const autoRefreshToggle = document.getElementById('auto-refresh-toggle') as HTMLInputElement | null;
151
- const refreshInterval = document.getElementById('refresh-interval') as HTMLSelectElement | null;
152
- if (systemRefreshStatus && autoRefreshToggle && refreshInterval) {
153
- if (autoRefreshToggle.checked) {
154
- const intervalSec = parseInt(refreshInterval.value) / 1000;
155
- systemRefreshStatus.textContent = `Active (${intervalSec}s)`;
156
- systemRefreshStatus.classList.add('text-success');
157
- systemRefreshStatus.classList.remove('text-accent');
158
- } else {
159
- systemRefreshStatus.textContent = 'Paused';
160
- systemRefreshStatus.classList.remove('text-success');
161
- systemRefreshStatus.classList.add('text-accent');
422
+ const cpuCoresInfo = document.getElementById('cpu-cores-info');
423
+ if (cpuCoresInfo && system.cpuCores) {
424
+ cpuCoresInfo.textContent = `${system.cpuCores} Cores`;
162
425
  }
163
426
  }
164
427
  }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Format a duration in seconds to a human-readable string.
3
- * @param {number} seconds - Duration in seconds.
4
- * @returns {string} Formatted duration string (e.g., "2d 3h 45m 30s").
3
+ * @param seconds - Duration in seconds.
4
+ * @returns Formatted duration string (e.g., "2d 3h 45m 30s").
5
5
  */
6
6
  export function formatDuration(seconds: number): string {
7
+ if (seconds < 1) return formatMs(seconds * 1000);
8
+
7
9
  const d = Math.floor(seconds / (3600 * 24));
8
10
  const h = Math.floor((seconds % (3600 * 24)) / 3600);
9
11
  const m = Math.floor((seconds % 3600) / 60);
@@ -12,10 +14,27 @@ export function formatDuration(seconds: number): string {
12
14
  if (d > 0) parts.push(`${d}d`);
13
15
  if (h > 0) parts.push(`${h}h`);
14
16
  if (m > 0) parts.push(`${m}m`);
15
- parts.push(`${s}s`);
17
+ if (s > 0 || parts.length === 0) parts.push(`${s}s`);
16
18
  return parts.join(' ');
17
19
  }
18
20
 
21
+ /**
22
+ * Format milliseconds to a human-readable string.
23
+ * @param ms - Duration in milliseconds.
24
+ * @returns Formatted string (e.g., "1.23 ms" or "450 μs").
25
+ */
26
+ export function formatMs(ms: number): string {
27
+ if (ms === 0) return '0 ms';
28
+ if (ms < 1) {
29
+ const us = ms * 1000;
30
+ return `${us.toFixed(0)} μs`;
31
+ }
32
+ if (ms < 1000) {
33
+ return `${ms.toFixed(2)} ms`;
34
+ }
35
+ return formatDuration(ms / 1000);
36
+ }
37
+
19
38
  /**
20
39
  * Format an ISO date string to a localized date time string.
21
40
  * @param isoDate - ISO date string.
@@ -0,0 +1,24 @@
1
+ import type {SystemMetrics} from '../types.js';
2
+
3
+ /**
4
+ * Update the minimum and maximum metrics based on the current metrics.
5
+ *
6
+ * @param current - The current system metrics.
7
+ * @param min - The object storing minimum values.
8
+ * @param max - The object storing maximum values.
9
+ */
10
+ export function updateMinMaxMetrics(current: SystemMetrics, min: Partial<SystemMetrics>, max: Partial<SystemMetrics>): void {
11
+ const keys = Object.keys(current) as (keyof SystemMetrics)[];
12
+ keys.forEach((key) => {
13
+ const val = current[key];
14
+ const currentMin = min[key];
15
+ const currentMax = max[key];
16
+
17
+ if (currentMin === undefined || val < currentMin) {
18
+ min[key] = val;
19
+ }
20
+ if (currentMax === undefined || val > currentMax) {
21
+ max[key] = val;
22
+ }
23
+ });
24
+ }