keepup-http 0.5.0__py3-none-any.whl

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.
Files changed (43) hide show
  1. keepup_http/__init__.py +38 -0
  2. keepup_http/api_docs.py +76 -0
  3. keepup_http/body_limit.py +156 -0
  4. keepup_http/events_api.py +237 -0
  5. keepup_http/factory.py +562 -0
  6. keepup_http/lock_routes.py +155 -0
  7. keepup_http/plugin.py +53 -0
  8. keepup_http/routes.py +549 -0
  9. keepup_http/static/css/aos.css +1 -0
  10. keepup_http/static/css/main_nebula.css +232 -0
  11. keepup_http/static/css/main_new.css +869 -0
  12. keepup_http/static/css/tailwind.css +1 -0
  13. keepup_http/static/index_nebula.html +282 -0
  14. keepup_http/static/index_new.html +275 -0
  15. keepup_http/static/js/aos.js +1 -0
  16. keepup_http/static/js/feather-icons.js +13 -0
  17. keepup_http/static/js/layout_bootstrap.js +29 -0
  18. keepup_http/static/js/main_new.js +2076 -0
  19. keepup_http/static/js/tailwind.js +83 -0
  20. keepup_http/static/modules/css/background_tasks.css +233 -0
  21. keepup_http/static/modules/css/cluster.css +16 -0
  22. keepup_http/static/modules/css/event_manager.css +386 -0
  23. keepup_http/static/modules/css/integration_logs.css +33 -0
  24. keepup_http/static/modules/css/metrics.css +115 -0
  25. keepup_http/static/modules/css/modules.css +189 -0
  26. keepup_http/static/modules/css/themes.css +563 -0
  27. keepup_http/static/modules/css/users.css +278 -0
  28. keepup_http/static/modules/js/background_tasks.js +695 -0
  29. keepup_http/static/modules/js/chart.js +14 -0
  30. keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
  31. keepup_http/static/modules/js/cluster.js +374 -0
  32. keepup_http/static/modules/js/event_manager.js +1007 -0
  33. keepup_http/static/modules/js/integration_logs.js +795 -0
  34. keepup_http/static/modules/js/metrics.js +929 -0
  35. keepup_http/static/modules/js/modules.js +1128 -0
  36. keepup_http/static/modules/js/themes.js +684 -0
  37. keepup_http/static/modules/js/users.js +898 -0
  38. keepup_http/web.py +214 -0
  39. keepup_http-0.5.0.dist-info/METADATA +29 -0
  40. keepup_http-0.5.0.dist-info/RECORD +43 -0
  41. keepup_http-0.5.0.dist-info/WHEEL +5 -0
  42. keepup_http-0.5.0.dist-info/entry_points.txt +2 -0
  43. keepup_http-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,929 @@
1
+ // Where to load our own libraries from, resolved at module level.
2
+ // document.currentScript is only readable while the script is executing: inside
3
+ // an async function it is already null, and previously the fallback path ran
4
+ // instead of this one.
5
+ // The path is taken from the file itself rather than hardcoded, because the
6
+ // shell's mount point is an application setting (KeepupSettings.shell_mount).
7
+ const METRICS_ASSET_BASE = (function () {
8
+ const src = document.currentScript && document.currentScript.src;
9
+ return src ? src.substring(0, src.lastIndexOf('/')) : '';
10
+ })();
11
+
12
+ if (typeof window.metricsGlobals === 'undefined') {
13
+ window.metricsGlobals = {
14
+ cpuChart: null,
15
+ ramChart: null,
16
+ metricsAutoRefreshInterval: null,
17
+ isAutoRefreshEnabled: false,
18
+ isMetricsInitialized: false,
19
+ chartJsLoaded: false
20
+ };
21
+ }
22
+
23
+ async function renderMetrics() {
24
+ // Guard against re-initialization
25
+ if (window.metricsGlobals.isMetricsInitialized) {
26
+ console.log('Metrics module already initialized');
27
+ return;
28
+ }
29
+
30
+ console.log('Initializing metrics module...');
31
+
32
+ // First load Chart.js and the adapter
33
+ try {
34
+ await loadChartJsDependencies();
35
+ } catch (error) {
36
+ console.error('Failed to load Chart.js:', error);
37
+ showNotification('Failed to load chart library', 'error');
38
+ return;
39
+ }
40
+
41
+ addMetricsStyles();
42
+ addMetricsNavigation();
43
+ addMetricsSection();
44
+ setupMetricsPolling();
45
+
46
+ window.metricsGlobals.isMetricsInitialized = true;
47
+ console.log('Metrics module initialized');
48
+ }
49
+
50
+ async function loadChartJsDependencies() {
51
+ if (window.metricsGlobals.chartJsLoaded) {
52
+ console.log('Chart.js already loaded');
53
+ return;
54
+ }
55
+
56
+ if (typeof Chart !== 'undefined') {
57
+ console.log('Chart.js already available globally');
58
+ window.metricsGlobals.chartJsLoaded = true;
59
+ return;
60
+ }
61
+
62
+ console.log('Loading Chart.js and the adapter...');
63
+
64
+ // Both files ship with the package next to this section: the section
65
+ // belongs to the framework and cannot rely on an application's static
66
+ // assets -- there is more than one application.
67
+ await loadScript(`${METRICS_ASSET_BASE}/chart.js`);
68
+ await loadScript(`${METRICS_ASSET_BASE}/chartjs-adapter-date-fns.bundle.min.js`);
69
+
70
+ window.metricsGlobals.chartJsLoaded = true;
71
+ console.log('Chart.js and the adapter loaded successfully');
72
+ }
73
+
74
+ function loadScript(src) {
75
+ return new Promise((resolve, reject) => {
76
+ // Check whether the script is already loaded
77
+ if (document.querySelector(`script[src="${src}"]`)) {
78
+ console.log(`Script already loaded: ${src}`);
79
+ resolve();
80
+ return;
81
+ }
82
+
83
+ const script = document.createElement('script');
84
+ script.src = src;
85
+ script.onload = () => {
86
+ console.log(`Script loaded: ${src}`);
87
+ resolve();
88
+ };
89
+ script.onerror = () => {
90
+ console.error(`Failed to load script: ${src}`);
91
+ reject(new Error(`Could not load script: ${src}`));
92
+ };
93
+ document.head.appendChild(script);
94
+ });
95
+ }
96
+
97
+ function addMetricsNavigation() {
98
+ const mainNav = document.getElementById('main-nav');
99
+ if (!mainNav) return;
100
+
101
+ // Check whether the nav item is already added
102
+ if (document.querySelector('[data-target="metrics-section"]')) {
103
+ return;
104
+ }
105
+
106
+ const metricsNavItem = document.createElement('a');
107
+ metricsNavItem.href = '#';
108
+ metricsNavItem.className = 'nav-item flex items-center px-6 py-3 text-blue-100 hover:bg-blue-600 admin-only hidden';
109
+ metricsNavItem.setAttribute('data-target', 'metrics-section');
110
+ metricsNavItem.innerHTML = `
111
+ <i data-feather="bar-chart-2" class="w-5 h-5 mr-3"></i>
112
+ <span class="nav-text">System Metrics</span>
113
+ `;
114
+
115
+ metricsNavItem.addEventListener('click', function(e) {
116
+ e.preventDefault();
117
+ handleMetricsNavigation.call(this, e);
118
+ });
119
+
120
+ const modulesNavContainer = document.getElementById('modules-nav-container');
121
+ if (modulesNavContainer) {
122
+ modulesNavContainer.appendChild(metricsNavItem);
123
+ }
124
+
125
+ // Show only for admins
126
+ if (currentUser && currentUser.role === ROLE_ADMIN) {
127
+ metricsNavItem.classList.remove('hidden');
128
+ }
129
+
130
+ feather.replace();
131
+ }
132
+
133
+ /**
134
+ * Metrics navigation handler
135
+ */
136
+ function handleMetricsNavigation(e) {
137
+ e.preventDefault();
138
+
139
+ if (!checkAuth()) return;
140
+ if (currentUser.role !== ROLE_ADMIN) {
141
+ showNotification('Access denied. Administrator rights are required.', 'error');
142
+ return;
143
+ }
144
+
145
+ const navItems = document.querySelectorAll('.nav-item');
146
+
147
+ navItems.forEach(navItem => {
148
+ navItem.classList.remove('text-white', 'bg-blue-800');
149
+ navItem.classList.add('text-blue-100', 'hover:bg-blue-600');
150
+ });
151
+
152
+ this.classList.remove('text-blue-100', 'hover:bg-blue-600');
153
+ this.classList.add('text-white', 'bg-blue-800');
154
+
155
+ const contentSections = document.querySelectorAll('.content-section');
156
+ contentSections.forEach(section => {
157
+ section.classList.remove('active');
158
+ });
159
+
160
+ const targetId = this.dataset.target;
161
+ const targetSection = document.getElementById(targetId);
162
+
163
+ if (targetSection) {
164
+ targetSection.classList.add('active');
165
+ document.getElementById('current-section-title').textContent = 'System Metrics';
166
+
167
+ // Load data when switching to the section
168
+ loadMetricsData();
169
+ }
170
+ }
171
+
172
+ // Add the metrics section
173
+ function addMetricsSection() {
174
+ // Check whether the section already exists
175
+ if (document.getElementById('metrics-section')) {
176
+ return;
177
+ }
178
+
179
+ const metricsSection = document.createElement('div');
180
+ metricsSection.id = 'metrics-section';
181
+ metricsSection.className = 'content-section';
182
+
183
+ metricsSection.innerHTML = `
184
+ <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
185
+ <div class="flex justify-between items-center mb-6">
186
+ <h2 class="text-2xl font-bold text-gray-800">System Monitoring</h2>
187
+ <div class="flex space-x-2">
188
+ <button id="refreshMetricsBtn" data-action="metrics.refresh"
189
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
190
+ <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
191
+ Refresh
192
+ </button>
193
+ <button id="autoRefreshToggle" data-action="metrics.toggle-auto-refresh"
194
+ class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
195
+ <i data-feather="play" class="w-4 h-4 mr-2"></i>
196
+ Auto-refresh
197
+ </button>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Instance status -->
202
+ <div class="mb-6">
203
+ <h3 class="text-lg font-semibold mb-4">Instance Status</h3>
204
+ <div id="instancesStatus" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
205
+ <div class="text-center py-4">
206
+ <i data-feather="loader" class="w-6 h-6 animate-spin mx-auto mb-2 text-gray-400"></i>
207
+ <p class="text-gray-500">Loading instance status...</p>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <!-- Metric charts -->
213
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
214
+ <!-- CPU chart -->
215
+ <div class="bg-gray-50 rounded-lg p-4">
216
+ <div class="flex justify-between items-center mb-4">
217
+ <h3 class="text-lg font-semibold text-gray-800">CPU Load (%)</h3>
218
+ <div class="flex space-x-2">
219
+ <select id="cpuTimeRange" data-action="metrics.update-charts"
220
+ class="text-sm border rounded px-2 py-1">
221
+ <option value="1">1 hour</option>
222
+ <option value="6">6 hours</option>
223
+ <option value="24" selected>24 hours</option>
224
+ <option value="168">7 days</option>
225
+ </select>
226
+ </div>
227
+ </div>
228
+ <div id="cpuChartContainer" class="h-64">
229
+ <canvas id="cpuChart"></canvas>
230
+ </div>
231
+ </div>
232
+
233
+ <!-- RAM chart -->
234
+ <div class="bg-gray-50 rounded-lg p-4">
235
+ <div class="flex justify-between items-center mb-4">
236
+ <h3 class="text-lg font-semibold text-gray-800">RAM Usage (MB)</h3>
237
+ <div class="flex space-x-2">
238
+ <select id="ramTimeRange" data-action="metrics.update-charts"
239
+ class="text-sm border rounded px-2 py-1">
240
+ <option value="1">1 hour</option>
241
+ <option value="6">6 hours</option>
242
+ <option value="24" selected>24 hours</option>
243
+ <option value="168">7 days</option>
244
+ </select>
245
+ </div>
246
+ </div>
247
+ <div id="ramChartContainer" class="h-64">
248
+ <canvas id="ramChart"></canvas>
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ <!-- Detailed metrics table -->
254
+ <div class="bg-white rounded-lg shadow-sm overflow-hidden">
255
+ <div class="px-6 py-4 border-b border-gray-200">
256
+ <h3 class="text-lg font-semibold text-gray-800">Detailed Metrics</h3>
257
+ </div>
258
+ <div class="overflow-x-auto">
259
+ <table class="responsive-table min-w-full divide-y divide-gray-200">
260
+ <thead class="bg-gray-50">
261
+ <tr>
262
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
263
+ Instance
264
+ </th>
265
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
266
+ CPU (%)
267
+ </th>
268
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
269
+ RAM (MB)
270
+ </th>
271
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
272
+ Updated At
273
+ </th>
274
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
275
+ Status
276
+ </th>
277
+ </tr>
278
+ </thead>
279
+ <tbody id="metricsTableBody" class="bg-white divide-y divide-gray-200">
280
+ <tr>
281
+ <td colspan="5" class="px-6 py-4 text-center text-gray-500">
282
+ Loading metrics...
283
+ </td>
284
+ </tr>
285
+ </tbody>
286
+ </table>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ `;
291
+
292
+ const mainContent = document.querySelector('main');
293
+ if (mainContent) {
294
+ mainContent.appendChild(metricsSection);
295
+ }
296
+
297
+ // Initialize the charts
298
+ initializeMetricsCharts();
299
+ }
300
+
301
+ // Global variables for the charts
302
+ let cpuChart = null;
303
+ let ramChart = null;
304
+ let metricsAutoRefreshInterval = null;
305
+ let isAutoRefreshEnabled = false;
306
+
307
+ // Initialize the charts
308
+ function initializeMetricsCharts() {
309
+ // Check whether the charts are already initialized
310
+ if (window.metricsGlobals.cpuChart || window.metricsGlobals.ramChart) {
311
+ console.log('Charts already initialized');
312
+ return;
313
+ }
314
+
315
+ // Check whether Chart.js is loaded
316
+ if (typeof Chart === 'undefined') {
317
+ console.error('Chart.js not loaded');
318
+ showNotification('Chart library not loaded', 'error');
319
+ return;
320
+ }
321
+
322
+ const cpuCtx = document.getElementById('cpuChart')?.getContext('2d');
323
+ const ramCtx = document.getElementById('ramChart')?.getContext('2d');
324
+
325
+ if (!cpuCtx || !ramCtx) {
326
+ console.warn('Canvas elements for the charts not found');
327
+ return;
328
+ }
329
+
330
+ try {
331
+ // CPU chart
332
+ window.metricsGlobals.cpuChart = new Chart(cpuCtx, {
333
+ type: 'line',
334
+ data: {
335
+ labels: [],
336
+ datasets: []
337
+ },
338
+ options: {
339
+ responsive: true,
340
+ maintainAspectRatio: false,
341
+ scales: {
342
+ y: {
343
+ beginAtZero: true,
344
+ max: 100,
345
+ title: {
346
+ display: true,
347
+ text: 'Usage percentage (%)'
348
+ }
349
+ },
350
+ x: {
351
+ type: 'time',
352
+ time: {
353
+ unit: 'minute',
354
+ displayFormats: {
355
+ minute: 'HH:mm',
356
+ hour: 'HH:mm'
357
+ }
358
+ },
359
+ title: {
360
+ display: true,
361
+ text: 'Time'
362
+ }
363
+ }
364
+ },
365
+ plugins: {
366
+ legend: {
367
+ display: true,
368
+ position: 'top'
369
+ },
370
+ tooltip: {
371
+ mode: 'index',
372
+ intersect: false
373
+ }
374
+ }
375
+ }
376
+ });
377
+
378
+ // RAM chart
379
+ window.metricsGlobals.ramChart = new Chart(ramCtx, {
380
+ type: 'line',
381
+ data: {
382
+ labels: [],
383
+ datasets: []
384
+ },
385
+ options: {
386
+ responsive: true,
387
+ maintainAspectRatio: false,
388
+ scales: {
389
+ y: {
390
+ beginAtZero: true,
391
+ title: {
392
+ display: true,
393
+ text: 'Memory (MB)'
394
+ }
395
+ },
396
+ x: {
397
+ type: 'time',
398
+ time: {
399
+ unit: 'minute',
400
+ displayFormats: {
401
+ minute: 'HH:mm',
402
+ hour: 'HH:mm'
403
+ }
404
+ },
405
+ title: {
406
+ display: true,
407
+ text: 'Time'
408
+ }
409
+ }
410
+ },
411
+ plugins: {
412
+ legend: {
413
+ display: true,
414
+ position: 'top'
415
+ },
416
+ tooltip: {
417
+ mode: 'index',
418
+ intersect: false
419
+ }
420
+ }
421
+ }
422
+ });
423
+
424
+ console.log('Metric charts initialized successfully');
425
+ } catch (error) {
426
+ console.error('Failed to initialize charts:', error);
427
+ showNotification('Failed to create charts', 'error');
428
+ }
429
+ }
430
+
431
+ // Load metrics data
432
+ async function loadMetricsData() {
433
+ if (currentUser.role !== ROLE_ADMIN) return;
434
+
435
+ try {
436
+ const token = localStorage.getItem('authToken');
437
+ const response = await fetch('/api/admin/metrics/system', {
438
+ headers: {
439
+ 'Authorization': `Bearer ${token}`
440
+ }
441
+ });
442
+
443
+ if (response.ok) {
444
+ const metricsData = await response.json();
445
+ updateMetricsUI(metricsData);
446
+ } else {
447
+ throw new Error('Failed to load metrics');
448
+ }
449
+ } catch (error) {
450
+ console.error('Failed to load metrics:', error);
451
+ showNotification('Failed to load system metrics', 'error');
452
+ }
453
+ }
454
+
455
+ // Load historical data for the charts
456
+ async function loadHistoricalMetrics() {
457
+ if (currentUser.role !== ROLE_ADMIN) return;
458
+
459
+ try {
460
+ const cpuTimeRange = document.getElementById('cpuTimeRange').value;
461
+ const ramTimeRange = document.getElementById('ramTimeRange').value;
462
+
463
+ const token = localStorage.getItem('authToken');
464
+ const response = await fetch(`/api/admin/metrics/history?cpu_hours=${cpuTimeRange}&ram_hours=${ramTimeRange}`, {
465
+ headers: {
466
+ 'Authorization': `Bearer ${token}`
467
+ }
468
+ });
469
+
470
+ if (response.ok) {
471
+ const historicalData = await response.json();
472
+ updateMetricsCharts(historicalData);
473
+ }
474
+ } catch (error) {
475
+ console.error('Failed to load historical metrics:', error);
476
+ }
477
+ }
478
+
479
+ // Update the metrics UI
480
+ function updateMetricsUI(metricsData) {
481
+ updateInstancesStatus(metricsData.instances);
482
+ updateMetricsTable(metricsData.instances);
483
+ updateMetricsCharts(metricsData.historical);
484
+ }
485
+
486
+ // Update instance status
487
+ function updateInstancesStatus(instances) {
488
+ const container = document.getElementById('instancesStatus');
489
+ if (!container) return;
490
+
491
+ if (!instances || instances.length === 0) {
492
+ container.innerHTML = `
493
+ <div class="col-span-full text-center py-4 text-gray-500">
494
+ <i data-feather="alert-circle" class="w-8 h-8 mx-auto mb-2"></i>
495
+ <p>No instance data</p>
496
+ </div>
497
+ `;
498
+ feather.replace();
499
+ return;
500
+ }
501
+
502
+ container.innerHTML = instances.map(instance => {
503
+ const statusClass = instance.is_online ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
504
+ const statusText = instance.is_online ? 'Online' : 'Offline';
505
+
506
+ const cpuUsage = instance.current_metrics?.cpu_percent || 0;
507
+ const ramUsage = instance.current_metrics?.ram_mb || 0;
508
+
509
+ const cpuClass = cpuUsage > 80 ? 'text-red-600' : cpuUsage > 60 ? 'text-yellow-600' : 'text-green-600';
510
+ const ramClass = ramUsage > 800 ? 'text-red-600' : ramUsage > 500 ? 'text-yellow-600' : 'text-green-600';
511
+
512
+ return `
513
+ <div class="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow">
514
+ <div class="flex justify-between items-start mb-3">
515
+ <h4 class="font-semibold text-gray-800">${keepupEscapeHtml(instance.instance_id)}</h4>
516
+ <span class="px-2 py-1 text-xs rounded-full ${statusClass}">
517
+ ${statusText}
518
+ </span>
519
+ </div>
520
+
521
+ <div class="space-y-2 text-sm">
522
+ <div class="flex justify-between">
523
+ <span class="text-gray-600">CPU:</span>
524
+ <span class="font-medium ${cpuClass}">${cpuUsage.toFixed(1)}%</span>
525
+ </div>
526
+ <div class="flex justify-between">
527
+ <span class="text-gray-600">RAM:</span>
528
+ <span class="font-medium ${ramClass}">${ramUsage.toFixed(0)} MB</span>
529
+ </div>
530
+ <div class="flex justify-between">
531
+ <span class="text-gray-600">Updated:</span>
532
+ <span class="text-gray-500 text-xs">${formatRelativeTime(instance.last_update)}</span>
533
+ </div>
534
+ </div>
535
+
536
+ ${instance.is_online ? `
537
+ <div class="mt-3 pt-3 border-t border-gray-200">
538
+ <div class="flex space-x-2">
539
+ <button data-action="metrics.restart"
540
+ data-instance-id="${keepupEscapeHtml(instance.instance_id)}"
541
+ class="flex-1 px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded hover:bg-blue-200">
542
+ Restart
543
+ </button>
544
+ <button data-action="metrics.show-details"
545
+ data-instance-id="${keepupEscapeHtml(instance.instance_id)}"
546
+ class="flex-1 px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded hover:bg-gray-200">
547
+ Details
548
+ </button>
549
+ </div>
550
+ </div>
551
+ ` : ''}
552
+ </div>
553
+ `;
554
+ }).join('');
555
+
556
+ feather.replace();
557
+ }
558
+
559
+ // Update the metrics table
560
+ function updateMetricsTable(instances) {
561
+ const tbody = document.getElementById('metricsTableBody');
562
+ if (!tbody) return;
563
+
564
+ if (!instances || instances.length === 0) {
565
+ tbody.innerHTML = `
566
+ <tr>
567
+ <td colspan="5" class="px-6 py-4 text-center text-gray-500">
568
+ No metrics data
569
+ </td>
570
+ </tr>
571
+ `;
572
+ return;
573
+ }
574
+
575
+ tbody.innerHTML = instances.map(instance => {
576
+ const cpuUsage = instance.current_metrics?.cpu_percent || 0;
577
+ const ramUsage = instance.current_metrics?.ram_mb || 0;
578
+ const lastUpdate = instance.last_update ? new Date(instance.last_update).toLocaleString() : 'Unknown';
579
+
580
+ const statusClass = instance.is_online ? 'text-green-600' : 'text-red-600';
581
+ const statusText = instance.is_online ? 'Active' : 'Inactive';
582
+
583
+ const cpuClass = cpuUsage > 80 ? 'text-red-600 font-semibold' : cpuUsage > 60 ? 'text-yellow-600' : 'text-green-600';
584
+ const ramClass = ramUsage > 800 ? 'text-red-600 font-semibold' : ramUsage > 500 ? 'text-yellow-600' : 'text-green-600';
585
+
586
+ return `
587
+ <tr class="hover:bg-gray-50">
588
+ <td class="px-6 py-4 whitespace-nowrap">
589
+ <div class="text-sm font-medium text-gray-900">${keepupEscapeHtml(instance.instance_id)}</div>
590
+ </td>
591
+ <td class="px-6 py-4 whitespace-nowrap">
592
+ <span class="text-sm ${cpuClass}">${cpuUsage.toFixed(1)}%</span>
593
+ </td>
594
+ <td class="px-6 py-4 whitespace-nowrap">
595
+ <span class="text-sm ${ramClass}">${ramUsage.toFixed(0)} MB</span>
596
+ </td>
597
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
598
+ ${lastUpdate}
599
+ </td>
600
+ <td class="px-6 py-4 whitespace-nowrap">
601
+ <span class="text-sm ${statusClass}">${statusText}</span>
602
+ </td>
603
+ </tr>
604
+ `;
605
+ }).join('');
606
+ }
607
+
608
+ // Update the charts
609
+ function updateMetricsCharts(historicalData) {
610
+ if (!historicalData || !cpuChart || !ramChart) return;
611
+
612
+ // Update the CPU chart
613
+ updateChartData(cpuChart, historicalData.cpu, 'CPU Usage (%)');
614
+
615
+ // Update the RAM chart
616
+ updateChartData(ramChart, historicalData.ram, 'RAM Usage (MB)');
617
+ }
618
+
619
+ // Update chart data
620
+ function updateChartData(chart, data, labelPrefix) {
621
+ if (!data || !chart) return;
622
+
623
+ const datasets = [];
624
+ const instanceColors = {
625
+ 'instance-1': 'rgb(59, 130, 246)',
626
+ 'instance-2': 'rgb(16, 185, 129)',
627
+ 'instance-3': 'rgb(245, 158, 11)'
628
+ };
629
+
630
+ // Group data by instance
631
+ const instances = [...new Set(data.map(item => item.instance_id))];
632
+
633
+ instances.forEach(instanceId => {
634
+ const instanceData = data
635
+ .filter(item => item.instance_id === instanceId)
636
+ .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
637
+
638
+ const color = instanceColors[instanceId] || generateRandomColor();
639
+
640
+ datasets.push({
641
+ label: `${labelPrefix} - ${instanceId}`,
642
+ data: instanceData.map(item => ({
643
+ x: new Date(item.timestamp),
644
+ y: item.metric_value
645
+ })),
646
+ borderColor: color,
647
+ backgroundColor: color + '20',
648
+ tension: 0.4,
649
+ fill: false
650
+ });
651
+ });
652
+
653
+ chart.data.datasets = datasets;
654
+ chart.update();
655
+ }
656
+
657
+ // Generate a random color
658
+ function generateRandomColor() {
659
+ const r = Math.floor(Math.random() * 255);
660
+ const g = Math.floor(Math.random() * 255);
661
+ const b = Math.floor(Math.random() * 255);
662
+ return `rgb(${r}, ${g}, ${b})`;
663
+ }
664
+
665
+ // Format relative time
666
+ function formatRelativeTime(timestamp) {
667
+ if (!timestamp) return 'Unknown';
668
+
669
+ const now = new Date();
670
+ const time = new Date(timestamp);
671
+ const diffMs = now - time;
672
+ const diffMins = Math.floor(diffMs / 60000);
673
+
674
+ if (diffMins < 1) return 'Just now';
675
+ if (diffMins < 60) return `${diffMins} min ago`;
676
+
677
+ const diffHours = Math.floor(diffMins / 60);
678
+ if (diffHours < 24) return `${diffHours} h ago`;
679
+
680
+ const diffDays = Math.floor(diffHours / 24);
681
+ return `${diffDays} d ago`;
682
+ }
683
+
684
+ // Toggle auto-refresh
685
+ function toggleAutoRefresh() {
686
+ const button = document.getElementById('autoRefreshToggle');
687
+
688
+ if (window.metricsGlobals.isAutoRefreshEnabled) {
689
+ // Turn off auto-refresh
690
+ clearInterval(window.metricsGlobals.metricsAutoRefreshInterval);
691
+ window.metricsGlobals.isAutoRefreshEnabled = false;
692
+ button.innerHTML = '<i data-feather="play" class="w-4 h-4 mr-2"></i>Auto-refresh';
693
+ button.className = 'px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center';
694
+ showNotification('Auto-refresh disabled', 'info');
695
+ } else {
696
+ // Turn on auto-refresh
697
+ window.metricsGlobals.isAutoRefreshEnabled = true;
698
+ window.metricsGlobals.metricsAutoRefreshInterval = setInterval(loadMetricsData, 30000);
699
+ button.innerHTML = '<i data-feather="pause" class="w-4 h-4 mr-2"></i>Stop';
700
+ button.className = 'px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 flex items-center';
701
+ showNotification('Auto-refresh enabled (30 sec)', 'success');
702
+ loadMetricsData();
703
+ }
704
+
705
+ feather.replace();
706
+ }
707
+
708
+ // Restart instance
709
+ async function restartInstance(instanceId) {
710
+ if (!confirm(`Are you sure you want to restart instance ${instanceId}?`)) return;
711
+
712
+ try {
713
+ const token = localStorage.getItem('authToken');
714
+ const response = await fetch(`/api/admin/instances/${instanceId}/restart`, {
715
+ method: 'POST',
716
+ headers: {
717
+ 'Authorization': `Bearer ${token}`
718
+ }
719
+ });
720
+
721
+ if (response.ok) {
722
+ showNotification(`Instance ${instanceId} is restarting...`, 'success');
723
+ // Refresh data after 5 seconds
724
+ setTimeout(loadMetricsData, 5000);
725
+ } else {
726
+ // The cluster refuses a restart it cannot carry out, and says why.
727
+ const answer = await response.json().catch(() => ({}));
728
+ showNotification(answer.detail || 'Failed to restart instance', 'error');
729
+ }
730
+ } catch (error) {
731
+ console.error('Failed to restart instance:', error);
732
+ showNotification('Failed to restart instance', 'error');
733
+ }
734
+ }
735
+
736
+ // Show instance details
737
+ async function showInstanceDetails(instanceId) {
738
+ try {
739
+ const token = localStorage.getItem('authToken');
740
+ const response = await fetch(`/api/admin/instances/${instanceId}`, {
741
+ headers: {
742
+ 'Authorization': `Bearer ${token}`
743
+ }
744
+ });
745
+
746
+ if (response.ok) {
747
+ const instanceDetails = await response.json();
748
+ showInstanceDetailsModal(instanceDetails);
749
+ } else {
750
+ throw new Error('Failed to load details');
751
+ }
752
+ } catch (error) {
753
+ console.error('Failed to load instance details:', error);
754
+ showNotification('Failed to load instance details', 'error');
755
+ }
756
+ }
757
+
758
+ // Modal window with instance details
759
+ function showInstanceDetailsModal(instance) {
760
+ const modal = document.createElement('div');
761
+ modal.id = 'instanceDetailsModal';
762
+ modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
763
+
764
+ modal.innerHTML = `
765
+ <div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
766
+ <div class="flex justify-between items-center p-6 border-b">
767
+ <h3 class="text-xl font-bold text-gray-800">Instance Details: ${keepupEscapeHtml(instance.instance_id)}</h3>
768
+ <button data-close-details class="text-gray-500 hover:text-gray-700 p-2">
769
+ <i data-feather="x" class="w-6 h-6"></i>
770
+ </button>
771
+ </div>
772
+
773
+ <div class="p-6">
774
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
775
+ <div class="bg-gray-50 rounded-lg p-4">
776
+ <h4 class="font-semibold text-gray-700 mb-3">System Metrics</h4>
777
+ <div class="space-y-2 text-sm">
778
+ <div class="flex justify-between">
779
+ <span>CPU Usage:</span>
780
+ <span class="font-medium">${instance.current_metrics?.cpu_percent?.toFixed(1) || 0}%</span>
781
+ </div>
782
+ <div class="flex justify-between">
783
+ <span>RAM Usage:</span>
784
+ <span class="font-medium">${instance.current_metrics?.ram_mb?.toFixed(0) || 0} MB</span>
785
+ </div>
786
+ <div class="flex justify-between">
787
+ <span>Disk Space:</span>
788
+ <span class="font-medium">${instance.current_metrics?.disk_usage?.toFixed(1) || 0}%</span>
789
+ </div>
790
+ </div>
791
+ </div>
792
+
793
+ <div class="bg-gray-50 rounded-lg p-4">
794
+ <h4 class="font-semibold text-gray-700 mb-3">Status</h4>
795
+ <div class="space-y-2 text-sm">
796
+ <div class="flex justify-between">
797
+ <span>Status:</span>
798
+ <span class="font-medium ${instance.is_online ? 'text-green-600' : 'text-red-600'}">
799
+ ${instance.is_online ? 'Online' : 'Offline'}
800
+ </span>
801
+ </div>
802
+ <div class="flex justify-between">
803
+ <span>Uptime:</span>
804
+ <span class="font-medium">${instance.uptime || 'Unknown'}</span>
805
+ </div>
806
+ <div class="flex justify-between">
807
+ <span>Last Updated:</span>
808
+ <span class="font-medium">${instance.last_update ? new Date(instance.last_update).toLocaleString() : 'Unknown'}</span>
809
+ </div>
810
+ </div>
811
+ </div>
812
+ </div>
813
+
814
+ <div class="bg-gray-50 rounded-lg p-4">
815
+ <h4 class="font-semibold text-gray-700 mb-3">Recent Events</h4>
816
+ <div class="text-sm text-gray-600">
817
+ ${instance.recent_events && instance.recent_events.length > 0 ?
818
+ instance.recent_events.map(event => `
819
+ <div class="flex justify-between py-1 border-b border-gray-200 last:border-b-0">
820
+ <span>${keepupEscapeHtml(event.message)}</span>
821
+ <span class="text-gray-500">${new Date(event.timestamp).toLocaleString()}</span>
822
+ </div>
823
+ `).join('') :
824
+ '<p class="text-gray-500 text-center py-2">No events</p>'
825
+ }
826
+ </div>
827
+ </div>
828
+ </div>
829
+
830
+ <div class="flex justify-end p-6 border-t bg-gray-50">
831
+ <button data-close-details class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
832
+ Close
833
+ </button>
834
+ </div>
835
+ </div>
836
+ `;
837
+
838
+ document.body.appendChild(modal);
839
+ // The buttons of this modal, bound where it was put in the page: it is
840
+ // created once per opening, so delegation would only add an indirection
841
+ // (keepup-93).
842
+ modal.querySelectorAll('[data-close-details]').forEach(button =>
843
+ button.addEventListener('click', () => closeInstanceDetailsModal()));
844
+ feather.replace();
845
+ }
846
+
847
+ // Close the details modal
848
+ function closeInstanceDetailsModal() {
849
+ const modal = document.getElementById('instanceDetailsModal');
850
+ if (modal) {
851
+ modal.remove();
852
+ }
853
+ }
854
+
855
+ // Set up periodic polling
856
+ function setupMetricsPolling() {
857
+ // Automatically refresh data every 60 seconds while the section is active
858
+ setInterval(() => {
859
+ const metricsSection = document.getElementById('metrics-section');
860
+ if (metricsSection && metricsSection.classList.contains('active') && isAutoRefreshEnabled) {
861
+ loadMetricsData();
862
+ }
863
+ }, 60000);
864
+ }
865
+
866
+ /**
867
+ * Update charts when the time range changes
868
+ */
869
+ function updateMetricsCharts(historicalData) {
870
+ if (!historicalData || !window.metricsGlobals.cpuChart || !window.metricsGlobals.ramChart) {
871
+ console.warn('Charts not initialized or no data');
872
+ return;
873
+ }
874
+ updateChartData(window.metricsGlobals.cpuChart, historicalData.cpu, 'CPU Usage (%)');
875
+ updateChartData(window.metricsGlobals.ramChart, historicalData.ram, 'RAM Usage (MB)');
876
+ }
877
+
878
+ const metricsStyles = `
879
+ .metrics-instance-card {
880
+ transition: all 0.3s ease;
881
+ }
882
+
883
+ .metrics-instance-card:hover {
884
+ transform: translateY(-2px);
885
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
886
+ }
887
+
888
+ .metrics-chart-container {
889
+ position: relative;
890
+ height: 256px;
891
+ }
892
+
893
+ .metrics-status-online {
894
+ background: linear-gradient(135deg, #10b981, #059669);
895
+ }
896
+
897
+ .metrics-status-offline {
898
+ background: linear-gradient(135deg, #ef4444, #dc2626);
899
+ }
900
+
901
+ .metrics-status-warning {
902
+ background: linear-gradient(135deg, #f59e0b, #d97706);
903
+ }
904
+ `;
905
+
906
+ function addMetricsStyles() {
907
+ if (document.getElementById('metrics-styles')) return;
908
+
909
+ const styleSheet = document.createElement('style');
910
+ styleSheet.id = 'metrics-styles';
911
+ styleSheet.textContent = metricsStyles;
912
+ document.head.appendChild(styleSheet);
913
+ }
914
+
915
+ document.addEventListener('DOMContentLoaded', function() {
916
+ addMetricsStyles();
917
+ });
918
+
919
+ // The actions this section's markup asks the shell to take (keepup-93). The
920
+ // instance cards are rebuilt on every refresh, so their buttons name an action
921
+ // and carry the instance's id instead of a script built into the markup; a
922
+ // name nothing registered does nothing.
923
+ KeepupActions.register({
924
+ 'metrics.refresh': () => loadMetricsData(),
925
+ 'metrics.toggle-auto-refresh': () => toggleAutoRefresh(),
926
+ 'metrics.update-charts': () => updateMetricsCharts(),
927
+ 'metrics.restart': (element) => restartInstance(element.dataset.instanceId),
928
+ 'metrics.show-details': (element) => showInstanceDetails(element.dataset.instanceId),
929
+ });