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
package/src/admin/js/schemas.ts
CHANGED
|
@@ -4,25 +4,17 @@ import {z} from 'zod';
|
|
|
4
4
|
* Cache statistics schema.
|
|
5
5
|
*/
|
|
6
6
|
export const cacheStatsSchema = z.object({
|
|
7
|
+
size: z.number(),
|
|
7
8
|
hits: z.number(),
|
|
8
9
|
misses: z.number(),
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*/
|
|
14
|
-
export const cacheInfoSchema = z.object({
|
|
15
|
-
size: z.number(),
|
|
16
|
-
stats: cacheStatsSchema,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Cache data schema for a connection pool.
|
|
13
|
+
* Pool cache snapshot schema.
|
|
21
14
|
*/
|
|
22
|
-
export const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
argumentCache: cacheInfoSchema,
|
|
15
|
+
export const poolCacheSnapshotSchema = z.object({
|
|
16
|
+
procedureName: cacheStatsSchema,
|
|
17
|
+
argument: cacheStatsSchema,
|
|
26
18
|
});
|
|
27
19
|
|
|
28
20
|
/**
|
|
@@ -44,6 +36,7 @@ export const poolInfoSchema = z.object({
|
|
|
44
36
|
connectionsInUse: z.number(),
|
|
45
37
|
connectionsOpen: z.number(),
|
|
46
38
|
stats: poolStatsSchema.nullable(),
|
|
39
|
+
cache: poolCacheSnapshotSchema.optional(),
|
|
47
40
|
});
|
|
48
41
|
|
|
49
42
|
/**
|
|
@@ -53,6 +46,38 @@ export const metricsSchema = z.object({
|
|
|
53
46
|
requestCount: z.number(),
|
|
54
47
|
errorCount: z.number(),
|
|
55
48
|
avgResponseTime: z.number(),
|
|
49
|
+
minResponseTime: z.number(),
|
|
50
|
+
maxResponseTime: z.number(),
|
|
51
|
+
maxRequestsPerSecond: z.number(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Historical bucket schema.
|
|
56
|
+
*/
|
|
57
|
+
export const bucketSchema = z.object({
|
|
58
|
+
timestamp: z.number(),
|
|
59
|
+
requests: z.number(),
|
|
60
|
+
errors: z.number(),
|
|
61
|
+
durationMin: z.number(),
|
|
62
|
+
durationMax: z.number(),
|
|
63
|
+
durationAvg: z.number(),
|
|
64
|
+
durationP95: z.number(),
|
|
65
|
+
durationP99: z.number(),
|
|
66
|
+
system: z.object({
|
|
67
|
+
cpu: z.number(),
|
|
68
|
+
heapUsed: z.number(),
|
|
69
|
+
heapTotal: z.number(),
|
|
70
|
+
rss: z.number(),
|
|
71
|
+
external: z.number(),
|
|
72
|
+
}),
|
|
73
|
+
pools: z.array(
|
|
74
|
+
z.object({
|
|
75
|
+
name: z.string(),
|
|
76
|
+
connectionsInUse: z.number(),
|
|
77
|
+
connectionsOpen: z.number(),
|
|
78
|
+
cache: poolCacheSnapshotSchema.optional(),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
56
81
|
});
|
|
57
82
|
|
|
58
83
|
/**
|
|
@@ -97,15 +122,24 @@ export const systemInfoSchema = z.object({
|
|
|
97
122
|
nodeVersion: z.string(),
|
|
98
123
|
platform: z.string(),
|
|
99
124
|
arch: z.string(),
|
|
125
|
+
cpuCores: z.number().optional(),
|
|
100
126
|
memory: z.object({
|
|
101
127
|
rss: z.number(),
|
|
102
128
|
heapTotal: z.number(),
|
|
103
129
|
heapUsed: z.number(),
|
|
104
130
|
external: z.number(),
|
|
131
|
+
totalMemory: z.number().optional(),
|
|
132
|
+
rssMax: z.number().optional(),
|
|
133
|
+
heapTotalMax: z.number().optional(),
|
|
134
|
+
heapUsedMax: z.number().optional(),
|
|
135
|
+
externalMax: z.number().optional(),
|
|
105
136
|
}),
|
|
106
137
|
cpu: z.object({
|
|
107
138
|
user: z.number(),
|
|
108
139
|
system: z.number(),
|
|
140
|
+
max: z.number().optional(),
|
|
141
|
+
userMax: z.number().optional(),
|
|
142
|
+
systemMax: z.number().optional(),
|
|
109
143
|
}),
|
|
110
144
|
});
|
|
111
145
|
|
|
@@ -117,7 +151,9 @@ export const statusSchema = z.object({
|
|
|
117
151
|
status: z.enum(['running', 'paused', 'stopped']),
|
|
118
152
|
uptime: z.number(),
|
|
119
153
|
startTime: z.string(),
|
|
154
|
+
intervalMs: z.number().optional(),
|
|
120
155
|
metrics: metricsSchema,
|
|
156
|
+
history: z.array(bucketSchema).optional(),
|
|
121
157
|
pools: z.array(poolInfoSchema),
|
|
122
158
|
system: systemInfoSchema,
|
|
123
159
|
config: serverConfigSchema.partial(),
|
|
@@ -144,10 +180,36 @@ export const errorLogSchema = z.object({
|
|
|
144
180
|
|
|
145
181
|
export const accessLogResponseSchema = z.union([z.array(z.string()), z.object({message: z.string()})]);
|
|
146
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Trace entry schema.
|
|
185
|
+
*/
|
|
186
|
+
export const traceEntrySchema = z.object({
|
|
187
|
+
id: z.string(),
|
|
188
|
+
timestamp: z.string(),
|
|
189
|
+
source: z.string(),
|
|
190
|
+
url: z.string(),
|
|
191
|
+
method: z.string(),
|
|
192
|
+
status: z.string(),
|
|
193
|
+
duration: z.number(),
|
|
194
|
+
procedure: z.string().optional(),
|
|
195
|
+
parameters: z.record(z.string(), z.any()).optional(),
|
|
196
|
+
uploads: z.array(z.any()).optional(),
|
|
197
|
+
downloads: z
|
|
198
|
+
.object({
|
|
199
|
+
fileType: z.string(),
|
|
200
|
+
fileSize: z.number(),
|
|
201
|
+
})
|
|
202
|
+
.optional(),
|
|
203
|
+
html: z.string().optional(),
|
|
204
|
+
cookies: z.record(z.string(), z.string()).optional(),
|
|
205
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
206
|
+
cgi: z.record(z.string(), z.string()).optional(),
|
|
207
|
+
error: z.string().optional(),
|
|
208
|
+
});
|
|
209
|
+
|
|
147
210
|
/**
|
|
148
211
|
* API response type helpers.
|
|
149
212
|
*/
|
|
150
213
|
export type StatusResponse = z.infer<typeof statusSchema>;
|
|
151
|
-
export type CacheDataResponse = z.infer<typeof cacheDataSchema>;
|
|
152
214
|
export type ErrorLogResponse = z.infer<typeof errorLogSchema>;
|
|
153
215
|
export type AccessLogResponse = z.infer<typeof accessLogResponseSchema>;
|
|
@@ -6,12 +6,13 @@ import type {ServerConfig, RouteConfig} from '../types.js';
|
|
|
6
6
|
* @param value - The value to display.
|
|
7
7
|
* @param isMono - Whether to use monospaced font.
|
|
8
8
|
* @param colorClass - The CSS class for coloring the value.
|
|
9
|
+
* @param title - Optional tooltip explanation.
|
|
9
10
|
* @returns HTML string.
|
|
10
11
|
*/
|
|
11
|
-
function renderStatRow(label: string, value: string | number, isMono = true, colorClass = 'text-accent'): string {
|
|
12
|
+
function renderStatRow(label: string, value: string | number, isMono = true, colorClass = 'text-accent', title = ''): string {
|
|
12
13
|
const valClass = `${isMono ? 'font-mono' : ''} ${colorClass} font-bold break-all`.trim();
|
|
13
14
|
return `
|
|
14
|
-
<div class="stat-row text-sm">
|
|
15
|
+
<div class="stat-row text-sm" ${title ? `title="${title}"` : ''}>
|
|
15
16
|
<span>${label}</span>
|
|
16
17
|
<span class="${valClass}">${value}</span>
|
|
17
18
|
</div>
|
|
@@ -109,19 +110,19 @@ export function renderConfig(config: Partial<ServerConfig>): string {
|
|
|
109
110
|
html += '<div class="card mb-8">';
|
|
110
111
|
html += '<div class="card-header"><span class="material-symbols-rounded">dns</span><h3>Server Settings</h3></div>';
|
|
111
112
|
if (typeof config.port === 'number') {
|
|
112
|
-
html += renderStatRow('Port', config.port);
|
|
113
|
+
html += renderStatRow('Port', config.port, true, 'text-accent', 'The TCP port the server is listening on');
|
|
113
114
|
}
|
|
114
|
-
html += renderStatRow('Admin Route', config.adminRoute ?? '/admin');
|
|
115
|
-
html += renderStatRow('Admin User', config.adminUser ?? '(Not authenticated)');
|
|
116
|
-
html += renderStatRow('Admin Password', config.adminPassword ?? '(None)');
|
|
117
|
-
html += renderStatRow('Logger Filename', config.loggerFilename ?? '(Logging disabled)');
|
|
115
|
+
html += renderStatRow('Admin Route', config.adminRoute ?? '/admin', true, 'text-accent', 'The URL path for the administration console');
|
|
116
|
+
html += renderStatRow('Admin User', config.adminUser ?? '(Not authenticated)', true, 'text-accent', 'The username required for admin access');
|
|
117
|
+
html += renderStatRow('Admin Password', config.adminPassword ?? '(None)', true, 'text-accent', 'The password required for admin access');
|
|
118
|
+
html += renderStatRow('Logger Filename', config.loggerFilename ?? '(Logging disabled)', true, 'text-accent', 'The path to the server log file');
|
|
118
119
|
|
|
119
120
|
if (typeof config.uploadFileSizeLimit === 'number') {
|
|
120
121
|
const limit = config.uploadFileSizeLimit;
|
|
121
122
|
const mb = (limit / (1024 * 1024)).toFixed(2);
|
|
122
|
-
html += renderStatRow('Upload Size Limit', `${mb} MB (${limit.toLocaleString()} bytes)
|
|
123
|
+
html += renderStatRow('Upload Size Limit', `${mb} MB (${limit.toLocaleString()} bytes)`, true, 'text-accent', 'Maximum allowed size for file uploads');
|
|
123
124
|
} else {
|
|
124
|
-
html += renderStatRow('Upload Size Limit', 'No limit');
|
|
125
|
+
html += renderStatRow('Upload Size Limit', 'No limit', false, 'text-accent', 'Maximum allowed size for file uploads');
|
|
125
126
|
}
|
|
126
127
|
html += '</div>';
|
|
127
128
|
|
|
@@ -8,11 +8,14 @@ import type {ErrorLogResponse} from '../schemas.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export function errorRow(l: ErrorLogResponse): string {
|
|
10
10
|
return `
|
|
11
|
-
<tr>
|
|
12
|
-
<td>${new Date(l.timestamp).toLocaleString()}</td>
|
|
13
|
-
<td><code>${l.req?.method ?? '-'}</code></td>
|
|
14
|
-
<td>${l.req?.url ?? '-'}</td>
|
|
15
|
-
<td title="${l.details?.fullMessage ?? ''}">${l.message}</td>
|
|
11
|
+
<tr title="Click to see full error details">
|
|
12
|
+
<td title="Timestamp: ${new Date(l.timestamp).toLocaleString()}">${new Date(l.timestamp).toLocaleString()}</td>
|
|
13
|
+
<td title="Method: ${l.req?.method ?? '-'}"><code>${l.req?.method ?? '-'}</code></td>
|
|
14
|
+
<td title="URL: ${l.req?.url ?? '-'}">${l.req?.url ?? '-'}</td>
|
|
15
|
+
<td title="Click for details: ${l.details?.fullMessage ?? ''}" class="break-all">${l.message}</td>
|
|
16
|
+
<td style="width: 40px; text-align: center; color: var(--accent); opacity: 0.7">
|
|
17
|
+
<span class="material-symbols-rounded">chevron_right</span>
|
|
18
|
+
</td>
|
|
16
19
|
</tr>
|
|
17
20
|
`;
|
|
18
21
|
}
|
|
@@ -15,12 +15,12 @@ export function poolCard(p: PoolInfo): string {
|
|
|
15
15
|
const stats = p.stats ?? undefined;
|
|
16
16
|
|
|
17
17
|
return `
|
|
18
|
-
<div class="card card-clickable transition-all duration-200">
|
|
18
|
+
<div class="card card-clickable transition-all duration-200" title="Connection pool status for ${p.name}">
|
|
19
19
|
<div class="card-header">
|
|
20
20
|
<span class="material-symbols-rounded">database</span>
|
|
21
21
|
<h3 class="text-lg font-bold text-bright">${p.name}</h3>
|
|
22
22
|
</div>
|
|
23
|
-
<div class="mb-6">
|
|
23
|
+
<div class="mb-6" title="Utilization: ${utilization}% of open connections are currently in use">
|
|
24
24
|
<div class="flex justify-between items-baseline mb-2">
|
|
25
25
|
<span class="text-3xl font-bold text-bright">${utilization}%</span>
|
|
26
26
|
<span class="text-main font-mono">${p.connectionsInUse} / ${p.connectionsOpen}</span>
|
|
@@ -31,11 +31,11 @@ export function poolCard(p: PoolInfo): string {
|
|
|
31
31
|
<small class="description mt-2 block opacity-75">Pool Utilization (Active / Open)</small>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
<div class="stat-row text-sm">
|
|
34
|
+
<div class="stat-row text-sm" title="Total number of connections currently open in the pool">
|
|
35
35
|
<span class="text-xs uppercase font-bold opacity-75">Connections Open</span>
|
|
36
36
|
<span class="font-mono text-accent font-bold">${p.connectionsOpen}</span>
|
|
37
37
|
</div>
|
|
38
|
-
<div class="stat-row text-sm">
|
|
38
|
+
<div class="stat-row text-sm" title="Number of connections currently active and handling requests">
|
|
39
39
|
<span class="text-xs uppercase font-bold opacity-75">Connections In Use</span>
|
|
40
40
|
<span class="font-mono text-accent font-bold">${p.connectionsInUse}</span>
|
|
41
41
|
</div>
|
|
@@ -44,11 +44,11 @@ export function poolCard(p: PoolInfo): string {
|
|
|
44
44
|
stats
|
|
45
45
|
? `
|
|
46
46
|
<div class="mt-4 pt-4 border-t">
|
|
47
|
-
<div class="stat-row text-sm">
|
|
47
|
+
<div class="stat-row text-sm" title="Cumulative number of requests handled by this pool since server start">
|
|
48
48
|
<span>Total Requests</span>
|
|
49
49
|
<span class="font-mono text-accent font-bold">${stats.totalRequests.toLocaleString()}</span>
|
|
50
50
|
</div>
|
|
51
|
-
<div class="stat-row text-sm">
|
|
51
|
+
<div class="stat-row text-sm" title="Number of requests that timed out waiting for a connection">
|
|
52
52
|
<span>Total Timeouts</span>
|
|
53
53
|
<span class="font-mono ${stats.totalTimeouts > 0 ? 'text-danger' : 'text-accent'} font-bold">${stats.totalTimeouts.toLocaleString()}</span>
|
|
54
54
|
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {TraceEntry} from '../types.js';
|
|
2
|
+
import {formatMs} from '../util/format.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render a single trace row for the trace table.
|
|
6
|
+
*
|
|
7
|
+
* @param trace - The trace log entry.
|
|
8
|
+
* @returns HTML string.
|
|
9
|
+
*/
|
|
10
|
+
export function traceRow(trace: TraceEntry): string {
|
|
11
|
+
const time = new Date(trace.timestamp).toLocaleTimeString();
|
|
12
|
+
const statusClass = trace.status === 'success' ? 'text-success' : 'text-danger';
|
|
13
|
+
|
|
14
|
+
return `
|
|
15
|
+
<tr class="trace-table-row cursor-pointer hover:bg-white/5 transition-colors border-b border-white/5" data-id="${trace.id}">
|
|
16
|
+
<td class="px-4 py-2 font-mono text-xs text-dim">${time}</td>
|
|
17
|
+
<td class="px-4 py-2 text-dim text-xs truncate max-w-[150px]" title="${trace.source}">${trace.source}</td>
|
|
18
|
+
<td class="px-4 py-2 text-bright font-mono text-xs truncate max-w-[300px]" title="${trace.url}">${trace.url}</td>
|
|
19
|
+
<td class="px-4 py-2 font-bold ${statusClass} text-xs uppercase">${trace.status}</td>
|
|
20
|
+
<td class="px-4 py-2 text-right font-mono text-xs text-dim">${formatMs(trace.duration)}</td>
|
|
21
|
+
<td class="px-4 py-2 text-dim text-xs truncate max-w-[200px]" title="${trace.procedure ?? ''}">${trace.procedure ?? '-'}</td>
|
|
22
|
+
</tr>
|
|
23
|
+
`;
|
|
24
|
+
}
|
package/src/admin/js/types.ts
CHANGED
|
@@ -51,6 +51,9 @@ export type Metrics = {
|
|
|
51
51
|
requestCount: number;
|
|
52
52
|
errorCount: number;
|
|
53
53
|
avgResponseTime: number;
|
|
54
|
+
minResponseTime: number;
|
|
55
|
+
maxResponseTime: number;
|
|
56
|
+
maxRequestsPerSecond: number;
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
/**
|
|
@@ -96,11 +99,38 @@ export type Status = {
|
|
|
96
99
|
status: 'running' | 'paused' | 'stopped';
|
|
97
100
|
uptime: number;
|
|
98
101
|
startTime: string;
|
|
102
|
+
intervalMs?: number;
|
|
99
103
|
metrics: Metrics;
|
|
100
104
|
pools: PoolInfo[];
|
|
101
105
|
config: Partial<ServerConfig>;
|
|
102
106
|
};
|
|
103
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Resident Set Size - total memory used by the process
|
|
110
|
+
*/
|
|
111
|
+
export type HistoryBucket = {
|
|
112
|
+
timestamp: number;
|
|
113
|
+
requests: number;
|
|
114
|
+
errors: number;
|
|
115
|
+
durationMin: number;
|
|
116
|
+
durationMax: number;
|
|
117
|
+
durationAvg: number;
|
|
118
|
+
durationP95: number;
|
|
119
|
+
durationP99: number;
|
|
120
|
+
system: {
|
|
121
|
+
cpu: number;
|
|
122
|
+
heapUsed: number;
|
|
123
|
+
heapTotal: number;
|
|
124
|
+
rss: number;
|
|
125
|
+
external: number;
|
|
126
|
+
};
|
|
127
|
+
pools: {
|
|
128
|
+
name: string;
|
|
129
|
+
connectionsInUse: number;
|
|
130
|
+
connectionsOpen: number;
|
|
131
|
+
}[];
|
|
132
|
+
};
|
|
133
|
+
|
|
104
134
|
/**
|
|
105
135
|
* Historical data for charts.
|
|
106
136
|
*/
|
|
@@ -108,6 +138,8 @@ export type HistoryData = {
|
|
|
108
138
|
labels: string[];
|
|
109
139
|
requests: number[];
|
|
110
140
|
avgResponseTimes: number[];
|
|
141
|
+
p95ResponseTimes?: number[];
|
|
142
|
+
p99ResponseTimes?: number[];
|
|
111
143
|
poolUsage: Record<string, number[]>;
|
|
112
144
|
};
|
|
113
145
|
|
|
@@ -115,45 +147,85 @@ export type HistoryData = {
|
|
|
115
147
|
* Chart grid options.
|
|
116
148
|
*/
|
|
117
149
|
export type ChartGridOptions = {
|
|
118
|
-
color
|
|
150
|
+
color?: string;
|
|
151
|
+
display?: boolean;
|
|
152
|
+
drawBorder?: boolean;
|
|
153
|
+
drawOnChartArea?: boolean;
|
|
119
154
|
};
|
|
120
155
|
|
|
121
156
|
/**
|
|
122
157
|
* Chart ticks options.
|
|
123
158
|
*/
|
|
124
159
|
export type ChartTicksOptions = {
|
|
125
|
-
color
|
|
160
|
+
color?: string;
|
|
126
161
|
display?: boolean;
|
|
162
|
+
stepSize?: number;
|
|
163
|
+
maxRotation?: number;
|
|
164
|
+
autoSkip?: boolean;
|
|
165
|
+
maxTicksLimit?: number;
|
|
166
|
+
callback?: (value: number | string) => string;
|
|
127
167
|
};
|
|
128
168
|
|
|
129
169
|
/**
|
|
130
170
|
* Chart scale options.
|
|
131
171
|
*/
|
|
132
172
|
export type ChartScaleOptions = {
|
|
173
|
+
display?: boolean;
|
|
133
174
|
grid?: ChartGridOptions;
|
|
134
175
|
ticks?: ChartTicksOptions;
|
|
176
|
+
border?: {
|
|
177
|
+
display?: boolean;
|
|
178
|
+
color?: string;
|
|
179
|
+
};
|
|
135
180
|
title?: {
|
|
136
181
|
display: boolean;
|
|
137
182
|
text: string;
|
|
138
183
|
color: string;
|
|
139
184
|
};
|
|
185
|
+
type?: string;
|
|
186
|
+
position?: string;
|
|
187
|
+
beginAtZero?: boolean;
|
|
188
|
+
max?: number;
|
|
140
189
|
};
|
|
141
190
|
|
|
142
191
|
/**
|
|
143
192
|
* Chart options.
|
|
144
193
|
*/
|
|
145
194
|
export type ChartOptions = {
|
|
195
|
+
responsive?: boolean;
|
|
196
|
+
maintainAspectRatio?: boolean;
|
|
146
197
|
scales?: {
|
|
147
198
|
x?: ChartScaleOptions;
|
|
148
199
|
y?: ChartScaleOptions;
|
|
149
200
|
y1?: ChartScaleOptions;
|
|
150
201
|
};
|
|
202
|
+
layout?: {
|
|
203
|
+
padding?: number;
|
|
204
|
+
};
|
|
151
205
|
plugins?: {
|
|
152
206
|
legend?: {
|
|
207
|
+
display?: boolean;
|
|
153
208
|
labels?: {
|
|
154
|
-
color
|
|
209
|
+
color?: string;
|
|
155
210
|
};
|
|
156
211
|
};
|
|
212
|
+
tooltip?: {
|
|
213
|
+
enabled?: boolean;
|
|
214
|
+
position?: string;
|
|
215
|
+
intersect?: boolean;
|
|
216
|
+
callbacks?: Record<string, unknown>;
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
elements?: {
|
|
220
|
+
point?: {
|
|
221
|
+
radius?: number;
|
|
222
|
+
};
|
|
223
|
+
line?: {
|
|
224
|
+
borderWidth?: number;
|
|
225
|
+
};
|
|
226
|
+
bar?: {
|
|
227
|
+
borderWidth?: number;
|
|
228
|
+
};
|
|
157
229
|
};
|
|
158
230
|
};
|
|
159
231
|
|
|
@@ -165,8 +237,13 @@ export type ChartDataset = {
|
|
|
165
237
|
data: number[];
|
|
166
238
|
borderColor: string;
|
|
167
239
|
backgroundColor: string | undefined;
|
|
168
|
-
fill
|
|
169
|
-
tension
|
|
240
|
+
fill?: boolean | undefined;
|
|
241
|
+
tension?: number | undefined;
|
|
242
|
+
barThickness?: number;
|
|
243
|
+
categoryPercentage?: number;
|
|
244
|
+
barPercentage?: number;
|
|
245
|
+
borderWidth?: number;
|
|
246
|
+
yAxisID?: string;
|
|
170
247
|
};
|
|
171
248
|
|
|
172
249
|
/**
|
|
@@ -187,25 +264,48 @@ export type ChartInstance = {
|
|
|
187
264
|
};
|
|
188
265
|
|
|
189
266
|
/**
|
|
190
|
-
*
|
|
267
|
+
* Trace entry.
|
|
191
268
|
*/
|
|
192
|
-
export type
|
|
269
|
+
export type TraceEntry = {
|
|
270
|
+
id: string;
|
|
193
271
|
timestamp: string;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
272
|
+
source: string;
|
|
273
|
+
url: string;
|
|
274
|
+
method: string;
|
|
275
|
+
status: string;
|
|
276
|
+
duration: number;
|
|
277
|
+
procedure?: string | undefined;
|
|
278
|
+
parameters?: Record<string, unknown> | undefined;
|
|
279
|
+
uploads?: {originalname: string; mimetype: string; size: number}[] | undefined;
|
|
280
|
+
downloads?:
|
|
202
281
|
| {
|
|
203
|
-
|
|
282
|
+
fileType: string;
|
|
283
|
+
fileSize: number;
|
|
204
284
|
}
|
|
205
285
|
| undefined;
|
|
286
|
+
html?: string | undefined;
|
|
287
|
+
cookies?: Record<string, string> | undefined;
|
|
288
|
+
headers?: Record<string, string> | undefined;
|
|
289
|
+
cgi?: Record<string, string> | undefined;
|
|
290
|
+
error?: string | undefined;
|
|
206
291
|
};
|
|
207
292
|
|
|
208
|
-
import type {StatusResponse} from './schemas.js';
|
|
293
|
+
import type {StatusResponse, ErrorLogResponse, AccessLogResponse} from './schemas.js';
|
|
294
|
+
|
|
295
|
+
// Re-export types from schemas
|
|
296
|
+
export type {StatusResponse, ErrorLogResponse, AccessLogResponse};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* System metrics for tracking min/max.
|
|
300
|
+
*/
|
|
301
|
+
export type SystemMetrics = {
|
|
302
|
+
heapUsed: number;
|
|
303
|
+
heapTotal: number;
|
|
304
|
+
rss: number;
|
|
305
|
+
external: number;
|
|
306
|
+
cpuUser: number;
|
|
307
|
+
cpuSystem: number;
|
|
308
|
+
};
|
|
209
309
|
|
|
210
310
|
/**
|
|
211
311
|
* Application state.
|
|
@@ -217,7 +317,20 @@ export type State = {
|
|
|
217
317
|
lastRequestCount: number;
|
|
218
318
|
lastErrorCount: number;
|
|
219
319
|
lastUpdateTime: number;
|
|
220
|
-
|
|
221
|
-
|
|
320
|
+
lastBucketTimestamp: number;
|
|
321
|
+
nextRefreshTimeout: ReturnType<typeof setTimeout> | null;
|
|
322
|
+
nextRefreshTime: number;
|
|
323
|
+
countdownInterval: ReturnType<typeof setInterval> | null;
|
|
324
|
+
history: HistoryData & {
|
|
325
|
+
cpuUsage: number[];
|
|
326
|
+
memoryUsage: number[];
|
|
327
|
+
};
|
|
222
328
|
charts: Record<string, ChartInstance>;
|
|
329
|
+
metricsMin: Partial<SystemMetrics>;
|
|
330
|
+
metricsMax: Partial<SystemMetrics>;
|
|
223
331
|
};
|
|
332
|
+
|
|
333
|
+
declare global {
|
|
334
|
+
/** Injected by Vite during build */
|
|
335
|
+
const __BUILD_TIME__: string;
|
|
336
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhances a button with loading state logic.
|
|
3
|
+
*
|
|
4
|
+
* @param btn - The button element to enhance.
|
|
5
|
+
* @param action - The async function to execute when the button is clicked.
|
|
6
|
+
* @returns The result of the action.
|
|
7
|
+
*/
|
|
8
|
+
export async function withLoading<T>(btn: HTMLButtonElement, action: () => Promise<T>): Promise<T> {
|
|
9
|
+
const iconSpan = btn.querySelector('.material-symbols-rounded');
|
|
10
|
+
const originalIcon = iconSpan?.textContent;
|
|
11
|
+
|
|
12
|
+
btn.disabled = true;
|
|
13
|
+
btn.classList.add('loading');
|
|
14
|
+
if (iconSpan) {
|
|
15
|
+
iconSpan.textContent = 'sync';
|
|
16
|
+
iconSpan.classList.add('animate-spin');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return await action();
|
|
21
|
+
} finally {
|
|
22
|
+
btn.disabled = false;
|
|
23
|
+
btn.classList.remove('loading');
|
|
24
|
+
if (iconSpan) {
|
|
25
|
+
iconSpan.textContent = originalIcon ?? '';
|
|
26
|
+
iconSpan.classList.remove('animate-spin');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Binds an async action to a button with standardized loading feedback.
|
|
33
|
+
*
|
|
34
|
+
* @param id - The ID of the button element.
|
|
35
|
+
* @param action - The async action to perform.
|
|
36
|
+
* @returns The button element or null if not found.
|
|
37
|
+
*/
|
|
38
|
+
export function bindLoadingButton(id: string, action: () => Promise<void>): HTMLButtonElement | null {
|
|
39
|
+
const btn = document.getElementById(id) as HTMLButtonElement | null;
|
|
40
|
+
if (btn) {
|
|
41
|
+
btn.onclick = () => void withLoading(btn, action);
|
|
42
|
+
}
|
|
43
|
+
return btn;
|
|
44
|
+
}
|