web_plsql 0.18.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.
- package/README.md +29 -9
- package/package.json +17 -7
- package/src/admin/client/charts.ts +598 -0
- package/src/admin/client/main.js +3 -0
- package/src/admin/client/tailwind.css +41 -0
- package/src/admin/favicon.svg +3 -0
- package/src/admin/index.html +529 -0
- package/src/admin/js/api.ts +170 -0
- package/src/admin/js/app.ts +614 -0
- package/src/admin/js/eslint.config.js +74 -0
- package/src/admin/js/schemas.ts +215 -0
- package/src/admin/js/templates/config.ts +147 -0
- package/src/admin/js/templates/errorRow.ts +21 -0
- package/src/admin/js/templates/index.ts +4 -0
- package/src/admin/js/templates/poolCard.ts +61 -0
- package/src/admin/js/templates/traceRow.ts +24 -0
- package/src/admin/js/tsconfig.json +24 -0
- package/src/admin/js/types.ts +336 -0
- package/src/admin/js/ui/components.ts +44 -0
- package/src/admin/js/ui/table.ts +173 -0
- package/src/admin/js/ui/theme.ts +93 -0
- package/src/admin/js/ui/views.ts +427 -0
- package/src/admin/js/util/format.ts +46 -0
- package/src/admin/js/util/metrics.ts +24 -0
- package/src/admin/lib/chart.bundle.css +1 -0
- package/src/admin/lib/chart.bundle.js +146 -0
- package/src/admin/style.css +1437 -0
- package/src/bin/load-test.js +202 -0
- package/src/handler/handlerAdmin.js +293 -0
- package/src/handler/plsql/cgi.js +1 -1
- package/src/handler/plsql/errorPage.js +31 -6
- package/src/handler/plsql/handlerPlSql.js +32 -6
- package/src/handler/plsql/owaPageStream.js +93 -0
- package/src/handler/plsql/parsePage.js +7 -3
- package/src/handler/plsql/procedure.js +221 -105
- package/src/handler/plsql/procedureNamed.js +34 -49
- package/src/handler/plsql/procedureSanitize.js +120 -45
- package/src/handler/plsql/procedureVariable.js +4 -2
- package/src/handler/plsql/request.js +8 -3
- package/src/handler/plsql/sendResponse.js +43 -5
- package/src/index.js +0 -1
- package/src/server/adminContext.js +23 -0
- package/src/server/config.js +1 -0
- package/src/server/server.js +130 -9
- package/src/types.js +7 -1
- package/src/util/cache.js +123 -0
- package/src/util/jsonLogger.js +47 -0
- package/src/util/shutdown.js +6 -2
- package/src/util/statsManager.js +368 -0
- package/src/util/trace.js +7 -6
- package/src/util/traceManager.js +68 -0
- package/src/version.js +1 -1
- package/types/admin/js/schemas.d.ts +384 -0
- package/types/admin/js/types.d.ts +312 -0
- package/types/handler/handlerAdmin.d.ts +79 -0
- package/types/handler/plsql/errorPage.d.ts +1 -0
- package/types/handler/plsql/handlerPlSql.d.ts +6 -1
- package/types/handler/plsql/owaPageStream.d.ts +28 -0
- package/types/handler/plsql/procedure.d.ts +4 -1
- package/types/handler/plsql/procedureNamed.d.ts +2 -5
- package/types/handler/plsql/procedureSanitize.d.ts +2 -5
- package/types/handler/plsql/procedureVariable.d.ts +1 -1
- package/types/handler/plsql/request.d.ts +3 -1
- package/types/handler/plsql/sendResponse.d.ts +1 -1
- package/types/index.d.ts +0 -1
- package/types/server/adminContext.d.ts +17 -0
- package/types/server/server.d.ts +5 -0
- package/types/types.d.ts +19 -1
- package/types/util/cache.d.ts +69 -0
- package/types/util/jsonLogger.d.ts +45 -0
- package/types/util/statsManager.d.ts +395 -0
- package/types/util/traceManager.d.ts +35 -0
- package/src/handler/handlerMetrics.js +0 -68
- package/types/handler/handlerMetrics.d.ts +0 -25
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import {typedApi} from '../api.js';
|
|
2
|
+
import {formatDuration, formatDateTime, formatMs} from '../util/format.js';
|
|
3
|
+
import {poolCard} from '../templates/index.js';
|
|
4
|
+
import {renderConfig} from '../templates/config.js';
|
|
5
|
+
import type {State, ServerConfig, SystemMetrics, ErrorLogResponse, TraceEntry} from '../types.js';
|
|
6
|
+
import {DataTable, type TableColumn} from './table.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Refresh the error logs view.
|
|
10
|
+
*/
|
|
11
|
+
export async function refreshErrors(): Promise<void> {
|
|
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)
|
|
76
|
+
const tbody = document.querySelector('#errors-table tbody');
|
|
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);
|
|
108
|
+
|
|
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('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Refresh the access logs view.
|
|
130
|
+
*/
|
|
131
|
+
export async function refreshAccess(): Promise<void> {
|
|
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);
|
|
139
|
+
const el = document.getElementById('access-log-view');
|
|
140
|
+
const rangeEl = document.getElementById('access-log-range');
|
|
141
|
+
if (!el) return;
|
|
142
|
+
|
|
143
|
+
let logCount = 0;
|
|
144
|
+
if (Array.isArray(result)) {
|
|
145
|
+
el.textContent = result.join('\n');
|
|
146
|
+
logCount = result.length;
|
|
147
|
+
} else if (result && typeof result === 'object' && 'message' in result) {
|
|
148
|
+
el.textContent = result.message;
|
|
149
|
+
logCount = 0;
|
|
150
|
+
} else {
|
|
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';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (el.parentElement) {
|
|
160
|
+
el.parentElement.scrollTop = el.parentElement.scrollHeight;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
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
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Refresh the pools view.
|
|
266
|
+
*
|
|
267
|
+
* @param status - The status data.
|
|
268
|
+
*/
|
|
269
|
+
export function refreshPools(status: Partial<State['status']>): void {
|
|
270
|
+
const poolsCont = document.getElementById('pools-container');
|
|
271
|
+
if (!poolsCont || !status.pools) return;
|
|
272
|
+
|
|
273
|
+
poolsCont.innerHTML = status.pools.map((p) => poolCard(p)).join('');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Refresh the config view.
|
|
278
|
+
*
|
|
279
|
+
* @param state - The application state.
|
|
280
|
+
*/
|
|
281
|
+
export function refreshConfig(state: State): void {
|
|
282
|
+
const config = state.status.config;
|
|
283
|
+
if (!config) return;
|
|
284
|
+
|
|
285
|
+
const cont = document.getElementById('config-view');
|
|
286
|
+
if (!cont) return;
|
|
287
|
+
|
|
288
|
+
// Use type assertion to bypass exactOptionalPropertyTypes check if needed
|
|
289
|
+
cont.innerHTML = renderConfig(config as Partial<ServerConfig>);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Format bytes to human readable format.
|
|
294
|
+
*
|
|
295
|
+
* @param bytes - The number of bytes.
|
|
296
|
+
* @returns Formatted string.
|
|
297
|
+
*/
|
|
298
|
+
function formatBytes(bytes: number): string {
|
|
299
|
+
if (bytes === 0) return '0 B';
|
|
300
|
+
const k = 1024;
|
|
301
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
302
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
303
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Format microseconds to a human-readable duration.
|
|
308
|
+
*
|
|
309
|
+
* @param microseconds - CPU time in microseconds.
|
|
310
|
+
* @returns Formatted string.
|
|
311
|
+
*/
|
|
312
|
+
function formatCpuTime(microseconds: number): string {
|
|
313
|
+
return formatMs(microseconds / 1000);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Refresh the system info view.
|
|
318
|
+
*
|
|
319
|
+
* @param status - The status data.
|
|
320
|
+
* @param _state - The application state.
|
|
321
|
+
*/
|
|
322
|
+
export function refreshSystem(status: Partial<State['status']>, _state?: State): void {
|
|
323
|
+
const middlewareVersion = document.getElementById('middleware-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
|
+
}
|
|
328
|
+
|
|
329
|
+
const nodeVersion = document.getElementById('node-version');
|
|
330
|
+
if (nodeVersion && status.system) {
|
|
331
|
+
nodeVersion.textContent = status.system.nodeVersion;
|
|
332
|
+
nodeVersion.title = 'The version of the Node.js runtime';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const platform = document.getElementById('platform-info');
|
|
336
|
+
if (platform && status.system) {
|
|
337
|
+
platform.textContent = `${status.system.platform} (${status.system.arch})`;
|
|
338
|
+
platform.title = 'The operating system and architecture';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const systemUptime = document.getElementById('system-uptime');
|
|
342
|
+
if (systemUptime && status.uptime) systemUptime.textContent = formatDuration(status.uptime);
|
|
343
|
+
|
|
344
|
+
const systemStartTime = document.getElementById('system-start-time');
|
|
345
|
+
if (systemStartTime && status.startTime) systemStartTime.textContent = formatDateTime(status.startTime);
|
|
346
|
+
|
|
347
|
+
const systemStatusText = document.getElementById('system-status-text');
|
|
348
|
+
if (systemStatusText) systemStatusText.textContent = status.status?.toUpperCase() ?? '-';
|
|
349
|
+
|
|
350
|
+
const totalRequests = document.getElementById('total-requests');
|
|
351
|
+
if (totalRequests && status.metrics) {
|
|
352
|
+
totalRequests.textContent = status.metrics.requestCount.toLocaleString();
|
|
353
|
+
totalRequests.title = 'Cumulative number of requests handled by the server';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const totalErrors = document.getElementById('total-errors');
|
|
357
|
+
if (totalErrors && status.metrics) {
|
|
358
|
+
totalErrors.textContent = status.metrics.errorCount.toLocaleString();
|
|
359
|
+
totalErrors.title = 'Cumulative number of failed requests';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const activePools = document.getElementById('active-pools');
|
|
363
|
+
if (activePools && status.pools) {
|
|
364
|
+
activePools.textContent = status.pools.length.toString();
|
|
365
|
+
activePools.title = 'Number of database connection pools currently active';
|
|
366
|
+
}
|
|
367
|
+
|
|
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
|
+
};
|
|
379
|
+
|
|
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
|
+
};
|
|
388
|
+
|
|
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);
|
|
393
|
+
|
|
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
|
+
};
|
|
400
|
+
|
|
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');
|
|
407
|
+
|
|
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
|
+
}
|
|
421
|
+
|
|
422
|
+
const cpuCoresInfo = document.getElementById('cpu-cores-info');
|
|
423
|
+
if (cpuCoresInfo && system.cpuCores) {
|
|
424
|
+
cpuCoresInfo.textContent = `${system.cpuCores} Cores`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a duration in seconds to a human-readable string.
|
|
3
|
+
* @param seconds - Duration in seconds.
|
|
4
|
+
* @returns Formatted duration string (e.g., "2d 3h 45m 30s").
|
|
5
|
+
*/
|
|
6
|
+
export function formatDuration(seconds: number): string {
|
|
7
|
+
if (seconds < 1) return formatMs(seconds * 1000);
|
|
8
|
+
|
|
9
|
+
const d = Math.floor(seconds / (3600 * 24));
|
|
10
|
+
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
|
11
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
12
|
+
const s = Math.floor(seconds % 60);
|
|
13
|
+
const parts: string[] = [];
|
|
14
|
+
if (d > 0) parts.push(`${d}d`);
|
|
15
|
+
if (h > 0) parts.push(`${h}h`);
|
|
16
|
+
if (m > 0) parts.push(`${m}m`);
|
|
17
|
+
if (s > 0 || parts.length === 0) parts.push(`${s}s`);
|
|
18
|
+
return parts.join(' ');
|
|
19
|
+
}
|
|
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
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format an ISO date string to a localized date time string.
|
|
40
|
+
* @param isoDate - ISO date string.
|
|
41
|
+
* @returns Formatted date time string or "-" if invalid.
|
|
42
|
+
*/
|
|
43
|
+
export function formatDateTime(isoDate: string): string {
|
|
44
|
+
if (!isoDate) return '-';
|
|
45
|
+
return new Date(isoDate).toLocaleString();
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-bold:700;--tracking-wider:.05em;--radius-lg:.5rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-success:#10b981;--color-warning:#f59e0b;--color-danger:#ef4444}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.w-full{width:100%}.max-w-\[150px\]{max-width:150px}.max-w-\[200px\]{max-width:200px}.max-w-\[300px\]{max-width:300px}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/5{border-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.bg-success{background-color:var(--color-success)}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.px-4{padding-inline:calc(var(--spacing)*4)}.py-2{padding-block:calc(var(--spacing)*2)}.pt-4{padding-top:calc(var(--spacing)*4)}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.text-black{color:var(--color-black)}.text-danger{color:var(--color-danger)}.text-success{color:var(--color-success)}.text-warning{color:var(--color-warning)}.text-white{color:var(--color-white)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.opacity-30{opacity:.3}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media(hover:hover){.hover\:bg-success\/80:hover{background-color:#10b981cc}@supports (color:color-mix(in lab,red,red)){.hover\:bg-success\/80:hover{background-color:color-mix(in oklab,var(--color-success)80%,transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.hover\:bg-white\/20:hover{background-color:#fff3}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/20:hover{background-color:color-mix(in oklab,var(--color-white)20%,transparent)}}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}body{-webkit-font-smoothing:antialiased;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}body.dark{--bg-main:#0f172a;--bg-side:#1e293b;--bg-card:#1e293b;--bg-hover:#334155;--text-main:#94a3b8;--text-bright:#f8fafc;--border:#334155}body:not(.dark){--bg-main:#f8fafc;--bg-side:#fff;--bg-card:#fff;--bg-hover:#f1f5f9;--text-main:#475569;--text-bright:#0f172a;--border:#e2e8f0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|