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.
- package/README.md +5 -5
- package/package.json +5 -5
- package/src/admin/client/charts.ts +303 -4
- package/src/admin/index.html +283 -69
- package/src/admin/js/api.ts +95 -20
- package/src/admin/js/app.ts +460 -152
- package/src/admin/js/schemas.ts +76 -14
- package/src/admin/js/templates/config.ts +10 -9
- package/src/admin/js/templates/errorRow.ts +8 -5
- package/src/admin/js/templates/index.ts +1 -0
- package/src/admin/js/templates/poolCard.ts +6 -6
- package/src/admin/js/templates/traceRow.ts +24 -0
- package/src/admin/js/types.ts +132 -19
- package/src/admin/js/ui/components.ts +44 -0
- package/src/admin/js/ui/table.ts +173 -0
- package/src/admin/js/ui/views.ts +311 -48
- package/src/admin/js/util/format.ts +22 -3
- 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 +54 -47
- package/src/admin/style.css +143 -27
- package/src/handler/handlerAdmin.js +121 -26
- package/src/handler/plsql/errorPage.js +0 -4
- package/src/handler/plsql/owaPageStream.js +93 -0
- package/src/handler/plsql/procedure.js +191 -121
- package/src/handler/plsql/procedureNamed.js +17 -3
- package/src/handler/plsql/procedureSanitize.js +24 -0
- package/src/handler/plsql/procedureVariable.js +2 -0
- package/src/handler/plsql/sendResponse.js +41 -3
- package/src/index.js +0 -1
- package/src/server/adminContext.js +23 -0
- package/src/server/server.js +83 -68
- package/src/types.js +1 -1
- package/src/util/statsManager.js +368 -0
- package/src/util/trace.js +2 -1
- 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 +70 -0
- package/types/handler/plsql/owaPageStream.d.ts +28 -0
- package/types/handler/plsql/procedure.d.ts +1 -0
- package/types/index.d.ts +0 -1
- package/types/server/adminContext.d.ts +17 -0
- package/types/server/server.d.ts +2 -19
- package/types/types.d.ts +1 -1
- package/types/util/statsManager.d.ts +395 -0
- package/types/util/traceManager.d.ts +35 -0
- package/src/admin/lib/assets/main-zpdhQ1gD.css +0 -1
- package/src/handler/handlerMetrics.js +0 -68
- 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
|
+
}
|
package/src/admin/js/ui/views.ts
CHANGED
|
@@ -1,34 +1,159 @@
|
|
|
1
1
|
import {typedApi} from '../api.js';
|
|
2
|
-
import {formatDuration, formatDateTime} from '../util/format.js';
|
|
3
|
-
import {
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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']
|
|
322
|
+
export function refreshSystem(status: Partial<State['status']>, _state?: State): void {
|
|
98
323
|
const middlewareVersion = document.getElementById('middleware-version');
|
|
99
|
-
if (middlewareVersion && 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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
127
|
-
const
|
|
128
|
-
|
|
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
|
|
131
|
-
|
|
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
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
146
|
-
if (
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
4
|
-
* @returns
|
|
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
|
+
}
|