claude-code-router-config 1.0.1 → 1.1.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 +169 -8
- package/cli/analytics.js +509 -0
- package/cli/benchmark.js +342 -0
- package/cli/commands.js +300 -0
- package/config/smart-intent-router.js +543 -0
- package/docs/v1.1.0-FEATURES.md +752 -0
- package/logging/enhanced-logger.js +410 -0
- package/logging/health-monitor.js +472 -0
- package/logging/middleware.js +384 -0
- package/package.json +29 -6
- package/plugins/plugin-manager.js +607 -0
- package/templates/README.md +161 -0
- package/templates/balanced.json +111 -0
- package/templates/cost-optimized.json +96 -0
- package/templates/development.json +104 -0
- package/templates/performance-optimized.json +88 -0
- package/templates/quality-focused.json +105 -0
- package/web-dashboard/public/css/dashboard.css +575 -0
- package/web-dashboard/public/index.html +308 -0
- package/web-dashboard/public/js/dashboard.js +512 -0
- package/web-dashboard/server.js +352 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// Dashboard JavaScript
|
|
2
|
+
class Dashboard {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.apiBase = '/api';
|
|
5
|
+
this.currentTab = 'overview';
|
|
6
|
+
this.refreshInterval = null;
|
|
7
|
+
this.init();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async init() {
|
|
11
|
+
this.setupEventListeners();
|
|
12
|
+
await this.loadInitialData();
|
|
13
|
+
this.startAutoRefresh();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setupEventListeners() {
|
|
17
|
+
// Tab navigation
|
|
18
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
19
|
+
item.addEventListener('click', (e) => {
|
|
20
|
+
const tabName = e.target.dataset.tab;
|
|
21
|
+
this.switchTab(tabName);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Analytics period change
|
|
26
|
+
const periodSelect = document.getElementById('analytics-period');
|
|
27
|
+
if (periodSelect) {
|
|
28
|
+
periodSelect.addEventListener('change', () => {
|
|
29
|
+
this.loadAnalyticsData();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Config template change
|
|
34
|
+
const templateSelect = document.getElementById('template-select');
|
|
35
|
+
if (templateSelect) {
|
|
36
|
+
templateSelect.addEventListener('change', () => {
|
|
37
|
+
this.updateTemplatePreview();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switchTab(tabName) {
|
|
43
|
+
// Update nav items
|
|
44
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
45
|
+
item.classList.remove('active');
|
|
46
|
+
});
|
|
47
|
+
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
|
48
|
+
|
|
49
|
+
// Update content
|
|
50
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
51
|
+
content.classList.remove('active');
|
|
52
|
+
});
|
|
53
|
+
document.getElementById(tabName).classList.add('active');
|
|
54
|
+
|
|
55
|
+
this.currentTab = tabName;
|
|
56
|
+
|
|
57
|
+
// Load tab-specific data
|
|
58
|
+
this.loadTabData(tabName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async loadTabData(tabName) {
|
|
62
|
+
switch (tabName) {
|
|
63
|
+
case 'overview':
|
|
64
|
+
await this.loadOverviewData();
|
|
65
|
+
break;
|
|
66
|
+
case 'analytics':
|
|
67
|
+
await this.loadAnalyticsData();
|
|
68
|
+
break;
|
|
69
|
+
case 'health':
|
|
70
|
+
await this.loadHealthData();
|
|
71
|
+
break;
|
|
72
|
+
case 'config':
|
|
73
|
+
await this.loadConfigData();
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async loadInitialData() {
|
|
79
|
+
try {
|
|
80
|
+
await Promise.all([
|
|
81
|
+
this.loadOverviewData(),
|
|
82
|
+
this.loadConfigData()
|
|
83
|
+
]);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
this.showError('Failed to load initial data');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async loadOverviewData() {
|
|
90
|
+
try {
|
|
91
|
+
const [todayResponse, providersResponse] = await Promise.all([
|
|
92
|
+
fetch(`${this.apiBase}/analytics/today`),
|
|
93
|
+
fetch(`${this.apiBase}/health/providers`)
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const today = await todayResponse.json();
|
|
97
|
+
const providers = await providersResponse.json();
|
|
98
|
+
|
|
99
|
+
this.updateTodayStats(today.data);
|
|
100
|
+
this.updateProviderStatus(providers.data);
|
|
101
|
+
this.updateLastUpdated();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Failed to load overview data:', error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async loadAnalyticsData() {
|
|
108
|
+
try {
|
|
109
|
+
const period = document.getElementById('analytics-period')?.value || 'week';
|
|
110
|
+
const response = await fetch(`${this.apiBase}/analytics/summary`);
|
|
111
|
+
const data = await response.json();
|
|
112
|
+
|
|
113
|
+
this.updateAnalyticsDisplay(data.data);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('Failed to load analytics data:', error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async loadHealthData() {
|
|
120
|
+
try {
|
|
121
|
+
const [providersResponse, systemResponse] = await Promise.all([
|
|
122
|
+
fetch(`${this.apiBase}/health/providers`),
|
|
123
|
+
fetch(`${this.apiBase}/health/system`)
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const providers = await providersResponse.json();
|
|
127
|
+
const system = await systemResponse.json();
|
|
128
|
+
|
|
129
|
+
this.updateHealthProviders(providers.data);
|
|
130
|
+
this.updateSystemHealth(system.data);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Failed to load health data:', error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async loadConfigData() {
|
|
137
|
+
try {
|
|
138
|
+
const [configResponse, templatesResponse] = await Promise.all([
|
|
139
|
+
fetch(`${this.apiBase}/config/current`),
|
|
140
|
+
fetch(`${this.apiBase}/config/templates`)
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
const config = await configResponse.json();
|
|
144
|
+
const templates = await templatesResponse.json();
|
|
145
|
+
|
|
146
|
+
this.updateConfigDisplay(config.data);
|
|
147
|
+
this.updateTemplateOptions(templates.data);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Failed to load config data:', error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
updateTodayStats(data) {
|
|
154
|
+
if (!data) return;
|
|
155
|
+
|
|
156
|
+
document.getElementById('today-requests').textContent = data.requests || 0;
|
|
157
|
+
document.getElementById('today-tokens').textContent = this.formatNumber(data.tokens || 0);
|
|
158
|
+
document.getElementById('today-cost').textContent = `$${(data.cost || 0).toFixed(4)}`;
|
|
159
|
+
document.getElementById('today-latency').textContent = `${data.avgLatency || 0}ms`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
updateProviderStatus(providers) {
|
|
163
|
+
const container = document.getElementById('provider-status-grid');
|
|
164
|
+
if (!container) return;
|
|
165
|
+
|
|
166
|
+
container.innerHTML = '';
|
|
167
|
+
|
|
168
|
+
providers.forEach(provider => {
|
|
169
|
+
const statusDiv = document.createElement('div');
|
|
170
|
+
statusDiv.className = `provider-status ${provider.status}`;
|
|
171
|
+
statusDiv.innerHTML = `
|
|
172
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem;">${provider.name}</div>
|
|
173
|
+
<div style="font-size: 0.875rem; color: var(--text-secondary);">${provider.status}</div>
|
|
174
|
+
`;
|
|
175
|
+
container.appendChild(statusDiv);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
updateAnalyticsDisplay(data) {
|
|
180
|
+
if (!data) return;
|
|
181
|
+
|
|
182
|
+
const container = document.getElementById('detailed-stats');
|
|
183
|
+
if (!container) return;
|
|
184
|
+
|
|
185
|
+
const showDetailed = document.getElementById('show-detailed')?.checked;
|
|
186
|
+
const showCosts = document.getElementById('show-costs')?.checked;
|
|
187
|
+
|
|
188
|
+
let html = `
|
|
189
|
+
<div class="metric">
|
|
190
|
+
<span class="label">Total Requests</span>
|
|
191
|
+
<span class="value">${this.formatNumber(data.totalRequests || 0)}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="metric">
|
|
194
|
+
<span class="label">Total Tokens</span>
|
|
195
|
+
<span class="value">${this.formatNumber(data.totalTokens || 0)}</span>
|
|
196
|
+
</div>
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
if (showCosts) {
|
|
200
|
+
html += `
|
|
201
|
+
<div class="metric">
|
|
202
|
+
<span class="label">Total Cost</span>
|
|
203
|
+
<span class="value">$${(data.totalCost || 0).toFixed(4)}</span>
|
|
204
|
+
</div>
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
html += `
|
|
209
|
+
<div class="metric">
|
|
210
|
+
<span class="label">Average Latency</span>
|
|
211
|
+
<span class="value">${data.avgLatency || 0}ms</span>
|
|
212
|
+
</div>
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
if (showDetailed && data.providers) {
|
|
216
|
+
html += '<h4 style="margin-top: 1rem; margin-bottom: 0.5rem;">Provider Breakdown</h4>';
|
|
217
|
+
Object.entries(data.providers).forEach(([provider, count]) => {
|
|
218
|
+
html += `
|
|
219
|
+
<div class="metric">
|
|
220
|
+
<span class="label">${provider}</span>
|
|
221
|
+
<span class="value">${this.formatNumber(count)}</span>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
container.innerHTML = html;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
updateHealthProviders(providers) {
|
|
231
|
+
const container = document.getElementById('health-providers');
|
|
232
|
+
if (!container) return;
|
|
233
|
+
|
|
234
|
+
container.innerHTML = '';
|
|
235
|
+
|
|
236
|
+
providers.forEach(provider => {
|
|
237
|
+
const statusClass = provider.status === 'healthy' ? 'status-healthy' : 'status-unhealthy';
|
|
238
|
+
const statusDiv = document.createElement('div');
|
|
239
|
+
statusDiv.className = `metric`;
|
|
240
|
+
statusDiv.innerHTML = `
|
|
241
|
+
<span class="label">${provider.name}</span>
|
|
242
|
+
<span class="status-badge ${statusClass}">${provider.status}</span>
|
|
243
|
+
`;
|
|
244
|
+
container.appendChild(statusDiv);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
updateSystemHealth(data) {
|
|
249
|
+
if (!data) return;
|
|
250
|
+
|
|
251
|
+
document.getElementById('system-uptime').textContent = this.formatDuration(data.uptime);
|
|
252
|
+
document.getElementById('system-memory').textContent = this.formatBytes(data.memory?.used || 0);
|
|
253
|
+
document.getElementById('system-node').textContent = data.nodeVersion || '-';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
updateConfigDisplay(config) {
|
|
257
|
+
const container = document.getElementById('config-display');
|
|
258
|
+
if (!container) return;
|
|
259
|
+
|
|
260
|
+
container.innerHTML = `
|
|
261
|
+
<pre style="background: var(--background); padding: 1rem; border-radius: var(--radius); overflow-x: auto;">
|
|
262
|
+
${JSON.stringify(config, null, 2)}
|
|
263
|
+
</pre>
|
|
264
|
+
`;
|
|
265
|
+
|
|
266
|
+
// Update provider config
|
|
267
|
+
const providerContainer = document.getElementById('provider-config');
|
|
268
|
+
if (providerContainer && config.Providers) {
|
|
269
|
+
providerContainer.innerHTML = '';
|
|
270
|
+
config.Providers.forEach(provider => {
|
|
271
|
+
providerContainer.innerHTML += `
|
|
272
|
+
<div class="metric">
|
|
273
|
+
<span class="label">${provider.name}</span>
|
|
274
|
+
<span class="value">${provider.models.length} models</span>
|
|
275
|
+
</div>
|
|
276
|
+
`;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Update router config
|
|
281
|
+
const routerContainer = document.getElementById('router-config');
|
|
282
|
+
if (routerContainer && config.Router) {
|
|
283
|
+
routerContainer.innerHTML = '';
|
|
284
|
+
Object.entries(config.Router).forEach(([key, value]) => {
|
|
285
|
+
routerContainer.innerHTML += `
|
|
286
|
+
<div class="metric">
|
|
287
|
+
<span class="label">${key}</span>
|
|
288
|
+
<span class="value">${JSON.stringify(value)}</span>
|
|
289
|
+
</div>
|
|
290
|
+
`;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
updateTemplateOptions(templates) {
|
|
296
|
+
const select = document.getElementById('template-select');
|
|
297
|
+
if (!select) return;
|
|
298
|
+
|
|
299
|
+
// Keep current selection
|
|
300
|
+
const currentValue = select.value;
|
|
301
|
+
|
|
302
|
+
// Clear existing options (except the first one)
|
|
303
|
+
while (select.children.length > 1) {
|
|
304
|
+
select.removeChild(select.lastChild);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
templates.forEach(template => {
|
|
308
|
+
const option = document.createElement('option');
|
|
309
|
+
option.value = template.name;
|
|
310
|
+
option.textContent = template.description || template.name;
|
|
311
|
+
select.appendChild(option);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Restore previous selection if it still exists
|
|
315
|
+
if (currentValue) {
|
|
316
|
+
select.value = currentValue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async refreshData() {
|
|
321
|
+
this.showLoading();
|
|
322
|
+
try {
|
|
323
|
+
await this.loadTabData(this.currentTab);
|
|
324
|
+
this.showSuccess('Data refreshed');
|
|
325
|
+
} catch (error) {
|
|
326
|
+
this.showError('Failed to refresh data');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
startAutoRefresh() {
|
|
331
|
+
this.refreshInterval = setInterval(() => {
|
|
332
|
+
this.loadTabData(this.currentTab);
|
|
333
|
+
}, 30000); // Refresh every 30 seconds
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
stopAutoRefresh() {
|
|
337
|
+
if (this.refreshInterval) {
|
|
338
|
+
clearInterval(this.refreshInterval);
|
|
339
|
+
this.refreshInterval = null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Utility methods
|
|
344
|
+
formatNumber(num) {
|
|
345
|
+
if (num >= 1000000) {
|
|
346
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
347
|
+
} else if (num >= 1000) {
|
|
348
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
349
|
+
}
|
|
350
|
+
return num.toString();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
formatBytes(bytes) {
|
|
354
|
+
if (bytes >= 1073741824) {
|
|
355
|
+
return (bytes / 1073741824).toFixed(2) + ' GB';
|
|
356
|
+
} else if (bytes >= 1048576) {
|
|
357
|
+
return (bytes / 1048576).toFixed(2) + ' MB';
|
|
358
|
+
} else if (bytes >= 1024) {
|
|
359
|
+
return (bytes / 1024).toFixed(2) + ' KB';
|
|
360
|
+
}
|
|
361
|
+
return bytes + ' B';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
formatDuration(seconds) {
|
|
365
|
+
if (seconds >= 3600) {
|
|
366
|
+
return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
|
|
367
|
+
} else if (seconds >= 60) {
|
|
368
|
+
return Math.floor(seconds / 60) + 'm ' + Math.floor(seconds % 60) + 's';
|
|
369
|
+
}
|
|
370
|
+
return Math.floor(seconds) + 's';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
updateLastUpdated() {
|
|
374
|
+
const element = document.getElementById('last-updated');
|
|
375
|
+
if (element) {
|
|
376
|
+
element.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
showLoading() {
|
|
381
|
+
document.getElementById('connection-status').textContent = 'Loading...';
|
|
382
|
+
document.getElementById('connection-status').className = 'status-badge status-warning';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
showSuccess(message) {
|
|
386
|
+
document.getElementById('connection-status').textContent = message;
|
|
387
|
+
document.getElementById('connection-status').className = 'status-badge status-healthy';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
showError(message) {
|
|
391
|
+
document.getElementById('connection-status').textContent = message;
|
|
392
|
+
document.getElementById('connection-status').className = 'status-badge status-unhealthy';
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Global functions for button clicks
|
|
397
|
+
window.dashboard = null;
|
|
398
|
+
|
|
399
|
+
async function refreshData() {
|
|
400
|
+
if (window.dashboard) {
|
|
401
|
+
await window.dashboard.refreshData();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function testAllProviders() {
|
|
406
|
+
try {
|
|
407
|
+
const response = await fetch('/api/health/providers');
|
|
408
|
+
const data = await response.json();
|
|
409
|
+
alert(`Provider tests completed:\n${data.data.map(p => `${p.name}: ${p.status}`).join('\n')}`);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
alert('Failed to test providers');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function openConfig() {
|
|
416
|
+
alert('Configuration editor would open here');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function viewLogs() {
|
|
420
|
+
alert('Log viewer would open here');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function exportAnalytics() {
|
|
424
|
+
try {
|
|
425
|
+
const format = prompt('Export format (json/csv):', 'json');
|
|
426
|
+
if (format) {
|
|
427
|
+
window.location.href = `/api/analytics/export?format=${format}`;
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
alert('Failed to export analytics');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function refreshHealthStatus() {
|
|
435
|
+
try {
|
|
436
|
+
const response = await fetch('/api/health/providers');
|
|
437
|
+
const data = await response.json();
|
|
438
|
+
alert('Health status refreshed');
|
|
439
|
+
} catch (error) {
|
|
440
|
+
alert('Failed to refresh health status');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function runHealthChecks() {
|
|
445
|
+
alert('Running comprehensive health checks...');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function applyTemplate() {
|
|
449
|
+
const select = document.getElementById('template-select');
|
|
450
|
+
if (select && select.value) {
|
|
451
|
+
alert(`Applying template: ${select.value}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function backupConfig() {
|
|
456
|
+
alert('Configuration backup would be created here');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function validateConfig() {
|
|
460
|
+
alert('Configuration validation would run here');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function editConfig() {
|
|
464
|
+
alert('Configuration editor would open here');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function reloadConfig() {
|
|
468
|
+
alert('Configuration would be reloaded here');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function updateTemplatePreview() {
|
|
472
|
+
const select = document.getElementById('template-select');
|
|
473
|
+
if (select && select.value) {
|
|
474
|
+
console.log(`Template preview: ${select.value}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function closeModal() {
|
|
479
|
+
document.getElementById('modal').classList.add('hidden');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function showModal(title, content, actionText = null, actionCallback = null) {
|
|
483
|
+
const modal = document.getElementById('modal');
|
|
484
|
+
const modalTitle = document.getElementById('modal-title');
|
|
485
|
+
const modalBody = document.getElementById('modal-body');
|
|
486
|
+
const modalAction = document.getElementById('modal-action');
|
|
487
|
+
|
|
488
|
+
modalTitle.textContent = title;
|
|
489
|
+
modalBody.innerHTML = content;
|
|
490
|
+
|
|
491
|
+
if (actionText && actionCallback) {
|
|
492
|
+
modalAction.textContent = actionText;
|
|
493
|
+
modalAction.style.display = 'block';
|
|
494
|
+
modalAction.onclick = actionCallback;
|
|
495
|
+
} else {
|
|
496
|
+
modalAction.style.display = 'none';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
modal.classList.remove('hidden');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Initialize dashboard when DOM is loaded
|
|
503
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
504
|
+
window.dashboard = new Dashboard();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Cleanup on page unload
|
|
508
|
+
window.addEventListener('beforeunload', () => {
|
|
509
|
+
if (window.dashboard) {
|
|
510
|
+
window.dashboard.stopAutoRefresh();
|
|
511
|
+
}
|
|
512
|
+
});
|