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/app.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import {typedApi} from './api.js';
|
|
2
|
-
import {initCharts, updateCharts} from '../client/charts.js';
|
|
2
|
+
import {initCharts, updateCharts, hydrateHistory} from '../client/charts.js';
|
|
3
3
|
import {formatDuration, formatDateTime} from './util/format.js';
|
|
4
|
+
import {updateMinMaxMetrics} from './util/metrics.js';
|
|
4
5
|
import {initTheme} from './ui/theme.js';
|
|
5
|
-
import {refreshErrors, refreshAccess, refreshConfig, refreshPools, refreshSystem} from './ui/views.js';
|
|
6
|
-
import
|
|
6
|
+
import {refreshErrors, refreshTrace, refreshAccess, refreshConfig, refreshPools, refreshSystem, refreshStats} from './ui/views.js';
|
|
7
|
+
import {bindLoadingButton, withLoading} from './ui/components.js';
|
|
8
|
+
import type {State, SystemMetrics} from './types.js';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* View icons mapping.
|
|
10
12
|
*/
|
|
11
13
|
const viewIcons: Record<string, string> = {
|
|
12
14
|
overview: 'dashboard',
|
|
15
|
+
trace: 'bolt',
|
|
13
16
|
errors: 'error',
|
|
14
17
|
access: 'list_alt',
|
|
15
|
-
cache: 'cached',
|
|
16
18
|
pools: 'database',
|
|
19
|
+
stats: 'query_stats',
|
|
17
20
|
config: 'settings',
|
|
18
21
|
system: 'terminal',
|
|
19
22
|
};
|
|
@@ -28,14 +31,23 @@ const state: State = {
|
|
|
28
31
|
lastRequestCount: 0,
|
|
29
32
|
lastErrorCount: 0,
|
|
30
33
|
lastUpdateTime: Date.now(),
|
|
31
|
-
|
|
34
|
+
lastBucketTimestamp: 0,
|
|
35
|
+
nextRefreshTimeout: null,
|
|
36
|
+
nextRefreshTime: 0,
|
|
37
|
+
countdownInterval: null,
|
|
32
38
|
history: {
|
|
33
39
|
labels: [],
|
|
34
40
|
requests: [],
|
|
35
41
|
avgResponseTimes: [],
|
|
42
|
+
p95ResponseTimes: [],
|
|
43
|
+
p99ResponseTimes: [],
|
|
44
|
+
cpuUsage: [],
|
|
45
|
+
memoryUsage: [],
|
|
36
46
|
poolUsage: {},
|
|
37
47
|
},
|
|
38
48
|
charts: {},
|
|
49
|
+
metricsMin: {},
|
|
50
|
+
metricsMax: {},
|
|
39
51
|
};
|
|
40
52
|
|
|
41
53
|
/**
|
|
@@ -48,101 +60,277 @@ export async function clearCache(poolName: string): Promise<void> {
|
|
|
48
60
|
await updateStatus();
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
const STORAGE_KEYS = {
|
|
64
|
+
REFRESH_INTERVAL: 'admin_refresh_interval',
|
|
65
|
+
HISTORY_DURATION: 'admin_history_duration',
|
|
66
|
+
AUTO_REFRESH: 'admin_auto_refresh',
|
|
67
|
+
THEME: 'theme',
|
|
68
|
+
LAST_VIEW: 'admin_last_view',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set the UI to offline state.
|
|
73
|
+
*
|
|
74
|
+
* @param error - The error object.
|
|
75
|
+
*/
|
|
76
|
+
function setOfflineState(error: unknown): void {
|
|
77
|
+
console.error('Connection lost:', error);
|
|
78
|
+
|
|
79
|
+
const dot = document.getElementById('server-status-dot');
|
|
80
|
+
const text = document.getElementById('server-status-text');
|
|
81
|
+
if (dot) dot.className = 'dot offline';
|
|
82
|
+
if (text) text.textContent = 'OFFLINE';
|
|
83
|
+
|
|
84
|
+
const btnPause = document.getElementById('btn-pause');
|
|
85
|
+
if (btnPause) btnPause.style.display = 'none';
|
|
86
|
+
|
|
87
|
+
const btnResume = document.getElementById('btn-resume');
|
|
88
|
+
if (btnResume) btnResume.style.display = 'none';
|
|
89
|
+
|
|
90
|
+
// Update system view status if visible
|
|
91
|
+
const systemStatusDot = document.querySelector('#system-status .dot');
|
|
92
|
+
const systemStatusText = document.getElementById('system-status-text');
|
|
93
|
+
if (systemStatusDot) systemStatusDot.className = 'dot offline';
|
|
94
|
+
if (systemStatusText) systemStatusText.textContent = 'OFFLINE';
|
|
95
|
+
|
|
96
|
+
// Show offline modal
|
|
97
|
+
const modal = document.getElementById('offline-modal');
|
|
98
|
+
if (modal && modal.style.display !== 'flex') {
|
|
99
|
+
modal.style.display = 'flex';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Schedule next attempt in 60s
|
|
103
|
+
scheduleNextRefresh(60000);
|
|
104
|
+
startCountdown(60000);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start the countdown timer for the offline modal.
|
|
109
|
+
*
|
|
110
|
+
* @param durationMs - The duration in milliseconds.
|
|
111
|
+
*/
|
|
112
|
+
function startCountdown(durationMs: number): void {
|
|
113
|
+
if (state.countdownInterval) clearInterval(state.countdownInterval);
|
|
114
|
+
|
|
115
|
+
const endTime = Date.now() + durationMs;
|
|
116
|
+
const el = document.getElementById('offline-countdown');
|
|
117
|
+
const btnText = document.getElementById('reconnect-text');
|
|
118
|
+
|
|
119
|
+
if (btnText) btnText.textContent = 'Try Reconnect';
|
|
120
|
+
|
|
121
|
+
const update = () => {
|
|
122
|
+
if (!el) return;
|
|
123
|
+
const remaining = Math.max(0, Math.ceil((endTime - Date.now()) / 1000));
|
|
124
|
+
el.textContent = `Retrying in ${remaining}s...`;
|
|
125
|
+
if (remaining <= 0 && state.countdownInterval) {
|
|
126
|
+
clearInterval(state.countdownInterval);
|
|
127
|
+
state.countdownInterval = null;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
update();
|
|
132
|
+
state.countdownInterval = setInterval(update, 1000);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Stop the countdown timer.
|
|
137
|
+
*/
|
|
138
|
+
function stopCountdown(): void {
|
|
139
|
+
if (state.countdownInterval) {
|
|
140
|
+
clearInterval(state.countdownInterval);
|
|
141
|
+
state.countdownInterval = null;
|
|
142
|
+
}
|
|
143
|
+
const el = document.getElementById('offline-countdown');
|
|
144
|
+
if (el) el.textContent = '';
|
|
145
|
+
}
|
|
146
|
+
|
|
51
147
|
/**
|
|
52
148
|
* Update server status.
|
|
53
149
|
*/
|
|
54
150
|
async function updateStatus(): Promise<void> {
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
151
|
+
// Show spinner if manually triggered or retrying
|
|
152
|
+
const spinner = document.getElementById('reconnect-spinner');
|
|
153
|
+
const icon = document.getElementById('reconnect-icon');
|
|
154
|
+
const btnText = document.getElementById('reconnect-text');
|
|
155
|
+
|
|
156
|
+
if (spinner) spinner.style.display = 'inline-block';
|
|
157
|
+
if (icon) icon.style.display = 'none';
|
|
158
|
+
if (btnText) btnText.textContent = 'Connecting...';
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const newStatus = await typedApi.getStatus();
|
|
163
|
+
|
|
164
|
+
// Check if we were offline
|
|
165
|
+
const modal = document.getElementById('offline-modal');
|
|
166
|
+
const wasOffline = modal?.style.display === 'flex';
|
|
167
|
+
|
|
168
|
+
if (wasOffline && modal) {
|
|
169
|
+
modal.style.display = 'none';
|
|
170
|
+
stopCountdown();
|
|
171
|
+
}
|
|
58
172
|
|
|
59
|
-
|
|
173
|
+
// Reset button state
|
|
174
|
+
if (spinner) spinner.style.display = 'none';
|
|
175
|
+
if (icon) icon.style.display = 'inline-block';
|
|
176
|
+
if (btnText) btnText.textContent = 'Try Reconnect';
|
|
60
177
|
|
|
61
|
-
|
|
62
|
-
|
|
178
|
+
// Hydrate history on first load or if history is provided
|
|
179
|
+
if (newStatus.history && state.history.labels.length === 0) {
|
|
180
|
+
hydrateHistory(state, newStatus.history);
|
|
181
|
+
}
|
|
63
182
|
|
|
64
|
-
|
|
65
|
-
|
|
183
|
+
state.status = newStatus;
|
|
184
|
+
|
|
185
|
+
if (!newStatus.metrics || !newStatus.pools) return;
|
|
186
|
+
|
|
187
|
+
// Use server provided interval stats if available
|
|
188
|
+
const latestBucket = newStatus.history?.[newStatus.history.length - 1];
|
|
189
|
+
const intervalSec = (newStatus.intervalMs ?? 5000) / 1000;
|
|
190
|
+
const reqPerSec = latestBucket ? latestBucket.requests / intervalSec : 0;
|
|
191
|
+
const avgResponseTime = latestBucket ? latestBucket.durationAvg : newStatus.metrics.avgResponseTime;
|
|
192
|
+
|
|
193
|
+
state.lastRequestCount = newStatus.metrics.requestCount;
|
|
194
|
+
state.lastErrorCount = newStatus.metrics.errorCount;
|
|
195
|
+
state.lastUpdateTime = now;
|
|
196
|
+
|
|
197
|
+
// Update Min/Max metrics
|
|
198
|
+
if (newStatus.system) {
|
|
199
|
+
const sys = newStatus.system;
|
|
200
|
+
const metrics: SystemMetrics = {
|
|
201
|
+
heapUsed: sys.memory.heapUsed,
|
|
202
|
+
heapTotal: sys.memory.heapTotal,
|
|
203
|
+
rss: sys.memory.rss,
|
|
204
|
+
external: sys.memory.external,
|
|
205
|
+
cpuUser: sys.cpu.user,
|
|
206
|
+
cpuSystem: sys.cpu.system,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
updateMinMaxMetrics(metrics, state.metricsMin, state.metricsMax);
|
|
210
|
+
}
|
|
66
211
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
212
|
+
// Only update charts if we have a new bucket from the server
|
|
213
|
+
if (latestBucket && latestBucket.timestamp > state.lastBucketTimestamp) {
|
|
214
|
+
state.lastBucketTimestamp = latestBucket.timestamp;
|
|
215
|
+
const timeLabel = new Date(latestBucket.timestamp).toLocaleTimeString();
|
|
216
|
+
updateCharts(state, timeLabel, reqPerSec, avgResponseTime, newStatus.pools);
|
|
217
|
+
} else if (!latestBucket && state.history.labels.length === 0) {
|
|
218
|
+
// If we have no history yet, initialize with a zero point to avoid empty charts
|
|
219
|
+
const timeLabel = new Date().toLocaleTimeString();
|
|
220
|
+
updateCharts(state, timeLabel, 0, 0, newStatus.pools);
|
|
221
|
+
}
|
|
70
222
|
|
|
71
|
-
|
|
72
|
-
|
|
223
|
+
const sidebarVersion = document.getElementById('sidebar-version');
|
|
224
|
+
if (sidebarVersion) sidebarVersion.textContent = `v${newStatus.version}`;
|
|
73
225
|
|
|
74
|
-
|
|
75
|
-
|
|
226
|
+
const uptimeVal = document.getElementById('uptime-val');
|
|
227
|
+
if (uptimeVal) uptimeVal.textContent = formatDuration(newStatus.uptime);
|
|
76
228
|
|
|
77
|
-
|
|
78
|
-
|
|
229
|
+
const startTimeVal = document.getElementById('start-time-val');
|
|
230
|
+
if (startTimeVal) startTimeVal.textContent = `Started: ${formatDateTime(newStatus.startTime)}`;
|
|
79
231
|
|
|
80
|
-
|
|
81
|
-
|
|
232
|
+
const reqPerSecVal = document.getElementById('req-per-sec');
|
|
233
|
+
if (reqPerSecVal) reqPerSecVal.textContent = reqPerSec.toFixed(2);
|
|
82
234
|
|
|
83
|
-
|
|
84
|
-
|
|
235
|
+
const maxReqPerSecVal = document.getElementById('max-req-per-sec');
|
|
236
|
+
if (maxReqPerSecVal) maxReqPerSecVal.textContent = newStatus.metrics.maxRequestsPerSecond.toFixed(2);
|
|
85
237
|
|
|
86
|
-
|
|
87
|
-
|
|
238
|
+
const avgRespTimeVal = document.getElementById('avg-resp-time');
|
|
239
|
+
if (avgRespTimeVal) avgRespTimeVal.textContent = `${newStatus.metrics.avgResponseTime.toFixed(1)}ms`;
|
|
88
240
|
|
|
89
|
-
|
|
90
|
-
|
|
241
|
+
const maxRespTimeVal = document.getElementById('max-resp-time');
|
|
242
|
+
if (maxRespTimeVal) maxRespTimeVal.textContent = `${newStatus.metrics.maxResponseTime.toFixed(1)}ms`;
|
|
91
243
|
|
|
92
|
-
|
|
93
|
-
|
|
244
|
+
const errCount = document.getElementById('err-count');
|
|
245
|
+
if (errCount) errCount.textContent = newStatus.metrics.errorCount.toLocaleString();
|
|
94
246
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
247
|
+
// Update Cache Overview
|
|
248
|
+
let totalHits = 0;
|
|
249
|
+
let totalMisses = 0;
|
|
250
|
+
newStatus.pools.forEach((p) => {
|
|
251
|
+
if (p.cache) {
|
|
252
|
+
totalHits += p.cache.procedureName.hits + p.cache.argument.hits;
|
|
253
|
+
totalMisses += p.cache.procedureName.misses + p.cache.argument.misses;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const totalRequests = totalHits + totalMisses;
|
|
257
|
+
const hitRate = totalRequests > 0 ? Math.round((totalHits / totalRequests) * 100) : 0;
|
|
105
258
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
259
|
+
const cacheHitRateVal = document.getElementById('cache-hit-rate-val');
|
|
260
|
+
if (cacheHitRateVal) {
|
|
261
|
+
cacheHitRateVal.textContent = `${hitRate}%`;
|
|
262
|
+
cacheHitRateVal.style.color = hitRate > 80 ? 'var(--success)' : hitRate > 50 ? 'var(--warning)' : 'var(--danger)';
|
|
263
|
+
}
|
|
264
|
+
const cacheHitsVal = document.getElementById('cache-hits-val');
|
|
265
|
+
if (cacheHitsVal) cacheHitsVal.textContent = totalHits.toLocaleString();
|
|
266
|
+
const cacheMissesVal = document.getElementById('cache-misses-val');
|
|
267
|
+
if (cacheMissesVal) cacheMissesVal.textContent = totalMisses.toLocaleString();
|
|
268
|
+
|
|
269
|
+
const dot = document.getElementById('server-status-dot');
|
|
270
|
+
const text = document.getElementById('server-status-text');
|
|
271
|
+
if (dot) dot.className = 'dot ' + newStatus.status;
|
|
272
|
+
if (text) text.textContent = newStatus.status.toUpperCase();
|
|
273
|
+
|
|
274
|
+
const btnPause = document.getElementById('btn-pause');
|
|
275
|
+
if (btnPause) btnPause.style.display = newStatus.status === 'running' ? 'flex' : 'none';
|
|
276
|
+
|
|
277
|
+
const btnResume = document.getElementById('btn-resume');
|
|
278
|
+
if (btnResume) btnResume.style.display = newStatus.status === 'paused' ? 'flex' : 'none';
|
|
279
|
+
|
|
280
|
+
if (state.currentView === 'errors') await refreshErrors();
|
|
281
|
+
if (state.currentView === 'trace') await refreshTrace();
|
|
282
|
+
if (state.currentView === 'pools') refreshPools(newStatus);
|
|
283
|
+
if (state.currentView === 'stats') refreshStats(newStatus);
|
|
284
|
+
if (state.currentView === 'config') refreshConfig(state);
|
|
285
|
+
if (state.currentView === 'system') refreshSystem(newStatus, state);
|
|
286
|
+
|
|
287
|
+
// Schedule next refresh if auto-refresh is on
|
|
288
|
+
if (autoRefreshToggle?.checked && refreshIntervalSelect) {
|
|
289
|
+
const interval = parseInt(refreshIntervalSelect.value);
|
|
290
|
+
scheduleNextRefresh(interval);
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
const modal = document.getElementById('offline-modal');
|
|
294
|
+
// Only set offline state if not already shown to avoid resetting the timer repeatedly
|
|
295
|
+
if (modal?.style.display !== 'flex') {
|
|
296
|
+
setOfflineState(err);
|
|
297
|
+
} else {
|
|
298
|
+
// Already offline, reschedule next attempt
|
|
299
|
+
scheduleNextRefresh(60000);
|
|
300
|
+
startCountdown(60000);
|
|
301
|
+
// Reset button state on failure
|
|
302
|
+
if (spinner) spinner.style.display = 'none';
|
|
303
|
+
if (icon) icon.style.display = 'inline-block';
|
|
304
|
+
if (btnText) btnText.textContent = 'Try Reconnect';
|
|
305
|
+
}
|
|
110
306
|
}
|
|
111
|
-
|
|
112
|
-
if (cacheHitsVal) cacheHitsVal.textContent = totalHits.toLocaleString();
|
|
113
|
-
const cacheMissesVal = document.getElementById('cache-misses-val');
|
|
114
|
-
if (cacheMissesVal) cacheMissesVal.textContent = totalMisses.toLocaleString();
|
|
115
|
-
|
|
116
|
-
const dot = document.getElementById('server-status-dot');
|
|
117
|
-
const text = document.getElementById('server-status-text');
|
|
118
|
-
if (dot) dot.className = 'dot ' + newStatus.status;
|
|
119
|
-
if (text) text.textContent = newStatus.status.toUpperCase();
|
|
120
|
-
|
|
121
|
-
const btnPause = document.getElementById('btn-pause');
|
|
122
|
-
if (btnPause) btnPause.style.display = newStatus.status === 'running' ? 'flex' : 'none';
|
|
307
|
+
}
|
|
123
308
|
|
|
124
|
-
|
|
125
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Schedule the next refresh using recursive setTimeout.
|
|
311
|
+
*
|
|
312
|
+
* @param delayMs - Delay in milliseconds.
|
|
313
|
+
*/
|
|
314
|
+
function scheduleNextRefresh(delayMs: number): void {
|
|
315
|
+
if (state.nextRefreshTimeout) {
|
|
316
|
+
clearTimeout(state.nextRefreshTimeout);
|
|
317
|
+
state.nextRefreshTimeout = null;
|
|
318
|
+
}
|
|
126
319
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (state.currentView === 'system') refreshSystem(newStatus);
|
|
320
|
+
state.nextRefreshTime = Date.now() + delayMs;
|
|
321
|
+
state.nextRefreshTimeout = setTimeout(() => {
|
|
322
|
+
void updateStatus();
|
|
323
|
+
}, delayMs);
|
|
132
324
|
}
|
|
133
325
|
|
|
134
326
|
/**
|
|
135
|
-
* Start the refresh timer.
|
|
327
|
+
* Start the refresh timer (wrapper for compatibility).
|
|
136
328
|
*/
|
|
137
329
|
function startRefreshTimer(): void {
|
|
138
|
-
stopRefreshTimer();
|
|
139
|
-
const checkbox = document.getElementById('auto-refresh-toggle') as HTMLInputElement | null;
|
|
140
330
|
const intervalSelect = document.getElementById('refresh-interval') as HTMLSelectElement | null;
|
|
141
|
-
if (
|
|
331
|
+
if (intervalSelect) {
|
|
142
332
|
const interval = parseInt(intervalSelect.value);
|
|
143
|
-
|
|
144
|
-
void updateStatus();
|
|
145
|
-
}, interval);
|
|
333
|
+
scheduleNextRefresh(interval);
|
|
146
334
|
}
|
|
147
335
|
}
|
|
148
336
|
|
|
@@ -150,9 +338,9 @@ function startRefreshTimer(): void {
|
|
|
150
338
|
* Stop the refresh timer.
|
|
151
339
|
*/
|
|
152
340
|
function stopRefreshTimer(): void {
|
|
153
|
-
if (state.
|
|
154
|
-
|
|
155
|
-
state.
|
|
341
|
+
if (state.nextRefreshTimeout) {
|
|
342
|
+
clearTimeout(state.nextRefreshTimeout);
|
|
343
|
+
state.nextRefreshTimeout = null;
|
|
156
344
|
}
|
|
157
345
|
}
|
|
158
346
|
|
|
@@ -160,26 +348,20 @@ function stopRefreshTimer(): void {
|
|
|
160
348
|
* Update history points labels to be time-based.
|
|
161
349
|
*/
|
|
162
350
|
function updateHistoryLabels(): void {
|
|
163
|
-
const intervalSelect = document.getElementById('refresh-interval') as HTMLSelectElement | null;
|
|
164
351
|
const historySelect = document.getElementById('chart-history-points') as HTMLSelectElement | null;
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
const intervalMs = parseInt(intervalSelect.value);
|
|
168
|
-
const points = [30, 60, 120];
|
|
352
|
+
if (!historySelect) return;
|
|
169
353
|
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
if (!option) return;
|
|
354
|
+
Array.from(historySelect.options).forEach((option) => {
|
|
355
|
+
const durationSeconds = parseInt(option.value);
|
|
173
356
|
|
|
174
|
-
const totalSeconds = (pts * intervalMs) / 1000;
|
|
175
357
|
let timeStr = '';
|
|
176
|
-
if (
|
|
177
|
-
timeStr = `${
|
|
178
|
-
} else if (
|
|
179
|
-
timeStr = `${Math.round(
|
|
358
|
+
if (durationSeconds < 60) {
|
|
359
|
+
timeStr = `${durationSeconds}s`;
|
|
360
|
+
} else if (durationSeconds < 3600) {
|
|
361
|
+
timeStr = `${Math.round(durationSeconds / 60)} min`;
|
|
180
362
|
} else {
|
|
181
|
-
const hours =
|
|
182
|
-
timeStr = hours === Math.round(hours) ? `${hours} hours` : `${hours.toFixed(1)} hours`;
|
|
363
|
+
const hours = durationSeconds / 3600;
|
|
364
|
+
timeStr = hours === Math.round(hours) ? `${hours} hour${hours > 1 ? 's' : ''}` : `${hours.toFixed(1)} hours`;
|
|
183
365
|
}
|
|
184
366
|
option.textContent = `Last ${timeStr}`;
|
|
185
367
|
});
|
|
@@ -189,31 +371,37 @@ function updateHistoryLabels(): void {
|
|
|
189
371
|
document.querySelectorAll('nav button').forEach((btnEl) => {
|
|
190
372
|
const btn = btnEl as HTMLButtonElement;
|
|
191
373
|
btn.onclick = async () => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
374
|
+
try {
|
|
375
|
+
const view = btn.dataset.view;
|
|
376
|
+
if (!view) return;
|
|
377
|
+
state.currentView = view;
|
|
378
|
+
localStorage.setItem(STORAGE_KEYS.LAST_VIEW, view);
|
|
379
|
+
document.querySelectorAll('.view').forEach((vEl) => {
|
|
380
|
+
const v = vEl as HTMLElement;
|
|
381
|
+
v.style.display = 'none';
|
|
382
|
+
});
|
|
383
|
+
const viewEl = document.getElementById('view-' + view);
|
|
384
|
+
if (viewEl) viewEl.style.display = 'block';
|
|
385
|
+
document.querySelectorAll('nav button').forEach((b) => b.classList.remove('active'));
|
|
386
|
+
btn.classList.add('active');
|
|
387
|
+
const viewTitleEl = document.getElementById('view-title');
|
|
388
|
+
if (viewTitleEl) {
|
|
389
|
+
// Get text content excluding the icon span
|
|
390
|
+
const textNodes = Array.from(btn.childNodes).filter((node) => node.nodeType === Node.TEXT_NODE);
|
|
391
|
+
const btnText = textNodes.map((node) => node.textContent?.trim()).join('');
|
|
392
|
+
const icon = viewIcons[view] || 'chevron_right';
|
|
393
|
+
viewTitleEl.innerHTML = `<span class="material-symbols-rounded">${icon}</span>${btnText}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (view === 'errors') await refreshErrors();
|
|
397
|
+
if (view === 'trace') await refreshTrace();
|
|
398
|
+
if (view === 'pools') refreshPools(state.status);
|
|
399
|
+
if (view === 'stats') refreshStats(state.status);
|
|
400
|
+
if (view === 'config') refreshConfig(state);
|
|
401
|
+
if (view === 'system') refreshSystem(state.status, state);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
setOfflineState(err);
|
|
210
404
|
}
|
|
211
|
-
|
|
212
|
-
if (view === 'errors') await refreshErrors();
|
|
213
|
-
if (view === 'access') await refreshAccess();
|
|
214
|
-
if (view === 'pools') refreshPools(state.status);
|
|
215
|
-
if (view === 'config') refreshConfig(state);
|
|
216
|
-
if (view === 'system') refreshSystem(state.status);
|
|
217
405
|
};
|
|
218
406
|
});
|
|
219
407
|
|
|
@@ -221,6 +409,7 @@ const autoRefreshToggle = document.getElementById('auto-refresh-toggle') as HTML
|
|
|
221
409
|
if (autoRefreshToggle) {
|
|
222
410
|
autoRefreshToggle.onchange = (e: Event) => {
|
|
223
411
|
const target = e.target as HTMLInputElement;
|
|
412
|
+
localStorage.setItem(STORAGE_KEYS.AUTO_REFRESH, String(target.checked));
|
|
224
413
|
if (target.checked) startRefreshTimer();
|
|
225
414
|
else stopRefreshTimer();
|
|
226
415
|
};
|
|
@@ -229,78 +418,197 @@ if (autoRefreshToggle) {
|
|
|
229
418
|
const refreshIntervalSelect = document.getElementById('refresh-interval') as HTMLSelectElement | null;
|
|
230
419
|
if (refreshIntervalSelect) {
|
|
231
420
|
refreshIntervalSelect.onchange = () => {
|
|
232
|
-
|
|
421
|
+
localStorage.setItem(STORAGE_KEYS.REFRESH_INTERVAL, refreshIntervalSelect.value);
|
|
422
|
+
|
|
423
|
+
// Recalculate maxHistoryPoints for the new interval
|
|
424
|
+
const intervalMs = parseInt(refreshIntervalSelect.value);
|
|
425
|
+
const durationSeconds = parseInt(chartHistorySelect?.value ?? '60');
|
|
426
|
+
state.maxHistoryPoints = Math.max(1, Math.floor(durationSeconds / (intervalMs / 1000)));
|
|
427
|
+
|
|
428
|
+
// Clear history to force re-hydration from server on next update
|
|
429
|
+
state.history.labels = [];
|
|
430
|
+
state.history.requests = [];
|
|
431
|
+
state.history.avgResponseTimes = [];
|
|
432
|
+
state.history.cpuUsage = [];
|
|
433
|
+
state.history.memoryUsage = [];
|
|
434
|
+
state.history.poolUsage = {};
|
|
435
|
+
|
|
233
436
|
updateHistoryLabels();
|
|
234
|
-
refreshSystem(state.status);
|
|
437
|
+
refreshSystem(state.status, state);
|
|
438
|
+
void updateStatus();
|
|
235
439
|
};
|
|
236
440
|
}
|
|
237
441
|
|
|
238
442
|
const chartHistorySelect = document.getElementById('chart-history-points') as HTMLSelectElement | null;
|
|
239
443
|
if (chartHistorySelect) {
|
|
240
444
|
chartHistorySelect.onchange = () => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
445
|
+
localStorage.setItem(STORAGE_KEYS.HISTORY_DURATION, chartHistorySelect.value);
|
|
446
|
+
const intervalSelect = document.getElementById('refresh-interval') as HTMLSelectElement | null;
|
|
447
|
+
if (!intervalSelect) return;
|
|
448
|
+
const intervalMs = parseInt(intervalSelect.value);
|
|
449
|
+
const durationSeconds = parseInt(chartHistorySelect.value);
|
|
450
|
+
state.maxHistoryPoints = Math.max(1, Math.floor(durationSeconds / (intervalMs / 1000)));
|
|
451
|
+
|
|
452
|
+
// Clear history to force re-hydration from server on next update
|
|
453
|
+
state.history.labels = [];
|
|
454
|
+
state.history.requests = [];
|
|
455
|
+
state.history.avgResponseTimes = [];
|
|
456
|
+
state.history.cpuUsage = [];
|
|
457
|
+
state.history.memoryUsage = [];
|
|
458
|
+
state.history.poolUsage = {};
|
|
459
|
+
|
|
249
460
|
void updateStatus();
|
|
250
461
|
};
|
|
251
462
|
}
|
|
252
463
|
|
|
253
|
-
const manualRefreshBtn = document.getElementById('manual-refresh');
|
|
464
|
+
const manualRefreshBtn = document.getElementById('manual-refresh') as HTMLButtonElement | null;
|
|
254
465
|
if (manualRefreshBtn) {
|
|
255
466
|
manualRefreshBtn.onclick = () => {
|
|
256
|
-
void updateStatus
|
|
467
|
+
void withLoading(manualRefreshBtn, updateStatus);
|
|
257
468
|
};
|
|
258
469
|
}
|
|
259
470
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
void typedApi.post('api/server/pause').then(() => {
|
|
265
|
-
void updateStatus();
|
|
266
|
-
});
|
|
471
|
+
const errorFilterInput = document.getElementById('error-filter') as HTMLInputElement | null;
|
|
472
|
+
if (errorFilterInput) {
|
|
473
|
+
errorFilterInput.onkeydown = (e) => {
|
|
474
|
+
if (e.key === 'Enter') void refreshErrors();
|
|
267
475
|
};
|
|
268
476
|
}
|
|
269
477
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
478
|
+
bindLoadingButton('error-load-btn', refreshErrors);
|
|
479
|
+
bindLoadingButton('trace-load-btn', refreshTrace);
|
|
480
|
+
bindLoadingButton('access-load-btn', refreshAccess);
|
|
481
|
+
|
|
482
|
+
const traceFilterInput = document.getElementById('trace-filter') as HTMLInputElement | null;
|
|
483
|
+
if (traceFilterInput) {
|
|
484
|
+
traceFilterInput.onkeydown = (e) => {
|
|
485
|
+
if (e.key === 'Enter') void refreshTrace();
|
|
276
486
|
};
|
|
277
487
|
}
|
|
278
488
|
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
if (confirm('
|
|
283
|
-
|
|
489
|
+
const traceClearBtn = document.getElementById('trace-clear-btn') as HTMLButtonElement | null;
|
|
490
|
+
if (traceClearBtn) {
|
|
491
|
+
traceClearBtn.onclick = async () => {
|
|
492
|
+
if (confirm('Are you sure you want to clear all traces?')) {
|
|
493
|
+
await withLoading(traceClearBtn, async () => {
|
|
494
|
+
await typedApi.clearTraces();
|
|
495
|
+
await refreshTrace();
|
|
496
|
+
});
|
|
284
497
|
}
|
|
285
498
|
};
|
|
286
499
|
}
|
|
287
500
|
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
await typedApi.
|
|
293
|
-
await
|
|
294
|
-
|
|
501
|
+
const traceStatusToggle = document.getElementById('trace-status-toggle') as HTMLButtonElement | null;
|
|
502
|
+
if (traceStatusToggle) {
|
|
503
|
+
traceStatusToggle.onclick = async () => {
|
|
504
|
+
await withLoading(traceStatusToggle, async () => {
|
|
505
|
+
const {enabled: currentEnabled} = await typedApi.getTraceStatus();
|
|
506
|
+
await typedApi.toggleTrace(!currentEnabled);
|
|
507
|
+
await refreshTrace();
|
|
508
|
+
});
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const accessFilterInput = document.getElementById('access-filter') as HTMLInputElement | null;
|
|
513
|
+
if (accessFilterInput) {
|
|
514
|
+
accessFilterInput.onkeydown = (e) => {
|
|
515
|
+
if (e.key === 'Enter') void refreshAccess();
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const statsRowLimitSelect = document.getElementById('stats-row-limit') as HTMLSelectElement | null;
|
|
520
|
+
if (statsRowLimitSelect) {
|
|
521
|
+
statsRowLimitSelect.onchange = () => {
|
|
522
|
+
refreshStats(state.status);
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Danger Zone Actions
|
|
527
|
+
bindLoadingButton('btn-pause', async () => {
|
|
528
|
+
if (confirm('Are you sure you want to pause the server?')) {
|
|
529
|
+
await typedApi.post('api/server/pause');
|
|
530
|
+
await updateStatus();
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
bindLoadingButton('btn-resume', async () => {
|
|
535
|
+
await typedApi.post('api/server/resume');
|
|
536
|
+
await updateStatus();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
bindLoadingButton('btn-stop', async () => {
|
|
540
|
+
if (confirm('Are you sure you want to STOP the server? This action cannot be undone from the web interface.')) {
|
|
541
|
+
await typedApi.post('api/server/stop');
|
|
542
|
+
alert('Server stop requested. The interface will now become unresponsive.');
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
bindLoadingButton('btn-clear-all-cache', async () => {
|
|
547
|
+
if (confirm('Are you sure you want to clear ALL caches?')) {
|
|
548
|
+
await typedApi.post('api/cache/clear');
|
|
549
|
+
await updateStatus();
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const btnReconnect = document.getElementById('btn-reconnect') as HTMLButtonElement | null;
|
|
554
|
+
if (btnReconnect) {
|
|
555
|
+
btnReconnect.onclick = () => {
|
|
556
|
+
void withLoading(btnReconnect, updateStatus);
|
|
295
557
|
};
|
|
296
558
|
}
|
|
297
559
|
|
|
560
|
+
// Close modal on escape key
|
|
561
|
+
window.addEventListener('keydown', (e) => {
|
|
562
|
+
if (e.key === 'Escape') {
|
|
563
|
+
const errorModal = document.getElementById('error-modal');
|
|
564
|
+
if (errorModal) errorModal.style.display = 'none';
|
|
565
|
+
const traceModal = document.getElementById('trace-modal');
|
|
566
|
+
if (traceModal) traceModal.style.display = 'none';
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
298
570
|
// Initialize
|
|
571
|
+
|
|
572
|
+
// Load persisted settings
|
|
573
|
+
const savedRefreshInterval = localStorage.getItem(STORAGE_KEYS.REFRESH_INTERVAL);
|
|
574
|
+
if (savedRefreshInterval && refreshIntervalSelect) {
|
|
575
|
+
refreshIntervalSelect.value = savedRefreshInterval;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const savedHistoryDuration = localStorage.getItem(STORAGE_KEYS.HISTORY_DURATION);
|
|
579
|
+
if (savedHistoryDuration && chartHistorySelect) {
|
|
580
|
+
chartHistorySelect.value = savedHistoryDuration;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const savedAutoRefresh = localStorage.getItem(STORAGE_KEYS.AUTO_REFRESH);
|
|
584
|
+
if (savedAutoRefresh !== null && autoRefreshToggle) {
|
|
585
|
+
autoRefreshToggle.checked = savedAutoRefresh === 'true';
|
|
586
|
+
}
|
|
587
|
+
|
|
299
588
|
initTheme(state);
|
|
300
589
|
initCharts(state);
|
|
301
590
|
updateHistoryLabels();
|
|
591
|
+
|
|
592
|
+
// Apply initial maxHistoryPoints based on loaded settings
|
|
593
|
+
if (refreshIntervalSelect && chartHistorySelect) {
|
|
594
|
+
const intervalMs = parseInt(refreshIntervalSelect.value);
|
|
595
|
+
const durationSeconds = parseInt(chartHistorySelect.value);
|
|
596
|
+
state.maxHistoryPoints = Math.max(1, Math.floor(durationSeconds / (intervalMs / 1000)));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Restore last view
|
|
600
|
+
const lastView = localStorage.getItem(STORAGE_KEYS.LAST_VIEW);
|
|
601
|
+
if (lastView) {
|
|
602
|
+
const btn = document.querySelector<HTMLButtonElement>(`nav button[data-view="${lastView}"]`);
|
|
603
|
+
if (btn) {
|
|
604
|
+
btn.click();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
302
608
|
void updateStatus()
|
|
303
609
|
.then(() => {
|
|
304
|
-
|
|
610
|
+
if (autoRefreshToggle?.checked) {
|
|
611
|
+
startRefreshTimer();
|
|
612
|
+
}
|
|
305
613
|
})
|
|
306
614
|
.catch(console.error);
|