keepup-admin 0.1.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 (86) hide show
  1. keepup/THIRD-PARTY.md +44 -0
  2. keepup/__init__.py +41 -0
  3. keepup/api_versions.py +100 -0
  4. keepup/audit.py +499 -0
  5. keepup/auth/__init__.py +7 -0
  6. keepup/auth/config.py +128 -0
  7. keepup/auth/dependencies.py +485 -0
  8. keepup/auth/dto/__init__.py +0 -0
  9. keepup/auth/dto/token.py +31 -0
  10. keepup/auth/factory.py +28 -0
  11. keepup/auth/login_throttle.py +151 -0
  12. keepup/auth/oidc.py +245 -0
  13. keepup/auth/oidc_policy.py +111 -0
  14. keepup/auth/oidc_routes.py +288 -0
  15. keepup/auth/panel_session.py +220 -0
  16. keepup/auth/permissions.py +59 -0
  17. keepup/auth/providers/__init__.py +0 -0
  18. keepup/auth/providers/base.py +168 -0
  19. keepup/auth/providers/local.py +180 -0
  20. keepup/auth/routes.py +660 -0
  21. keepup/auth/seed_accounts.py +322 -0
  22. keepup/auth/session_lifetime.py +104 -0
  23. keepup/auth/signing_key.py +138 -0
  24. keepup/auth/websocket.py +86 -0
  25. keepup/cluster.py +634 -0
  26. keepup/db.py +663 -0
  27. keepup/events.py +764 -0
  28. keepup/factory.py +484 -0
  29. keepup/instance.py +46 -0
  30. keepup/integrations.py +260 -0
  31. keepup/locks.py +412 -0
  32. keepup/logging_setup.py +690 -0
  33. keepup/metrics.py +818 -0
  34. keepup/metrics_retention.py +376 -0
  35. keepup/modules.py +572 -0
  36. keepup/notification_bus.py +355 -0
  37. keepup/plugins/__init__.py +9 -0
  38. keepup/plugins/admin.py +246 -0
  39. keepup/plugins/base.py +234 -0
  40. keepup/plugins/enablement.py +184 -0
  41. keepup/plugins/registry.py +171 -0
  42. keepup/plugins/route_mask.py +338 -0
  43. keepup/plugins/routes.py +376 -0
  44. keepup/roles.py +17 -0
  45. keepup/scheduler.py +93 -0
  46. keepup/schema.py +295 -0
  47. keepup/sections.json +104 -0
  48. keepup/settings.py +184 -0
  49. keepup/static/css/aos.css +1 -0
  50. keepup/static/css/main_nebula.css +232 -0
  51. keepup/static/css/main_new.css +852 -0
  52. keepup/static/css/tailwind.css +1 -0
  53. keepup/static/index_nebula.html +293 -0
  54. keepup/static/index_new.html +286 -0
  55. keepup/static/js/aos.js +1 -0
  56. keepup/static/js/feather-icons.js +13 -0
  57. keepup/static/js/main_new.js +1861 -0
  58. keepup/static/js/tailwind.js +83 -0
  59. keepup/static/modules/css/background_tasks.css +233 -0
  60. keepup/static/modules/css/cluster.css +16 -0
  61. keepup/static/modules/css/event_manager.css +386 -0
  62. keepup/static/modules/css/integration_logs.css +33 -0
  63. keepup/static/modules/css/metrics.css +115 -0
  64. keepup/static/modules/css/modules.css +189 -0
  65. keepup/static/modules/css/themes.css +563 -0
  66. keepup/static/modules/css/users.css +278 -0
  67. keepup/static/modules/js/background_tasks.js +657 -0
  68. keepup/static/modules/js/chart.js +14 -0
  69. keepup/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
  70. keepup/static/modules/js/cluster.js +363 -0
  71. keepup/static/modules/js/event_manager.js +979 -0
  72. keepup/static/modules/js/integration_logs.js +767 -0
  73. keepup/static/modules/js/metrics.js +908 -0
  74. keepup/static/modules/js/modules.js +1086 -0
  75. keepup/static/modules/js/themes.js +653 -0
  76. keepup/static/modules/js/users.js +784 -0
  77. keepup/tables.py +302 -0
  78. keepup/themes.py +496 -0
  79. keepup/web.py +182 -0
  80. keepup_admin-0.1.0.dist-info/METADATA +117 -0
  81. keepup_admin-0.1.0.dist-info/RECORD +86 -0
  82. keepup_admin-0.1.0.dist-info/WHEEL +5 -0
  83. keepup_admin-0.1.0.dist-info/licenses/LICENSE +202 -0
  84. keepup_admin-0.1.0.dist-info/licenses/NOTICE +22 -0
  85. keepup_admin-0.1.0.dist-info/licenses/THIRD-PARTY.md +44 -0
  86. keepup_admin-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,908 @@
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" onclick="loadMetricsData()"
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" onclick="toggleAutoRefresh()"
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" onchange="updateMetricsCharts()"
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" onchange="updateMetricsCharts()"
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">${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 onclick="restartInstance('${instance.instance_id}')"
540
+ class="flex-1 px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded hover:bg-blue-200">
541
+ Restart
542
+ </button>
543
+ <button onclick="showInstanceDetails('${instance.instance_id}')"
544
+ class="flex-1 px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded hover:bg-gray-200">
545
+ Details
546
+ </button>
547
+ </div>
548
+ </div>
549
+ ` : ''}
550
+ </div>
551
+ `;
552
+ }).join('');
553
+
554
+ feather.replace();
555
+ }
556
+
557
+ // Update the metrics table
558
+ function updateMetricsTable(instances) {
559
+ const tbody = document.getElementById('metricsTableBody');
560
+ if (!tbody) return;
561
+
562
+ if (!instances || instances.length === 0) {
563
+ tbody.innerHTML = `
564
+ <tr>
565
+ <td colspan="5" class="px-6 py-4 text-center text-gray-500">
566
+ No metrics data
567
+ </td>
568
+ </tr>
569
+ `;
570
+ return;
571
+ }
572
+
573
+ tbody.innerHTML = instances.map(instance => {
574
+ const cpuUsage = instance.current_metrics?.cpu_percent || 0;
575
+ const ramUsage = instance.current_metrics?.ram_mb || 0;
576
+ const lastUpdate = instance.last_update ? new Date(instance.last_update).toLocaleString() : 'Unknown';
577
+
578
+ const statusClass = instance.is_online ? 'text-green-600' : 'text-red-600';
579
+ const statusText = instance.is_online ? 'Active' : 'Inactive';
580
+
581
+ const cpuClass = cpuUsage > 80 ? 'text-red-600 font-semibold' : cpuUsage > 60 ? 'text-yellow-600' : 'text-green-600';
582
+ const ramClass = ramUsage > 800 ? 'text-red-600 font-semibold' : ramUsage > 500 ? 'text-yellow-600' : 'text-green-600';
583
+
584
+ return `
585
+ <tr class="hover:bg-gray-50">
586
+ <td class="px-6 py-4 whitespace-nowrap">
587
+ <div class="text-sm font-medium text-gray-900">${instance.instance_id}</div>
588
+ </td>
589
+ <td class="px-6 py-4 whitespace-nowrap">
590
+ <span class="text-sm ${cpuClass}">${cpuUsage.toFixed(1)}%</span>
591
+ </td>
592
+ <td class="px-6 py-4 whitespace-nowrap">
593
+ <span class="text-sm ${ramClass}">${ramUsage.toFixed(0)} MB</span>
594
+ </td>
595
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
596
+ ${lastUpdate}
597
+ </td>
598
+ <td class="px-6 py-4 whitespace-nowrap">
599
+ <span class="text-sm ${statusClass}">${statusText}</span>
600
+ </td>
601
+ </tr>
602
+ `;
603
+ }).join('');
604
+ }
605
+
606
+ // Update the charts
607
+ function updateMetricsCharts(historicalData) {
608
+ if (!historicalData || !cpuChart || !ramChart) return;
609
+
610
+ // Update the CPU chart
611
+ updateChartData(cpuChart, historicalData.cpu, 'CPU Usage (%)');
612
+
613
+ // Update the RAM chart
614
+ updateChartData(ramChart, historicalData.ram, 'RAM Usage (MB)');
615
+ }
616
+
617
+ // Update chart data
618
+ function updateChartData(chart, data, labelPrefix) {
619
+ if (!data || !chart) return;
620
+
621
+ const datasets = [];
622
+ const instanceColors = {
623
+ 'instance-1': 'rgb(59, 130, 246)',
624
+ 'instance-2': 'rgb(16, 185, 129)',
625
+ 'instance-3': 'rgb(245, 158, 11)'
626
+ };
627
+
628
+ // Group data by instance
629
+ const instances = [...new Set(data.map(item => item.instance_id))];
630
+
631
+ instances.forEach(instanceId => {
632
+ const instanceData = data
633
+ .filter(item => item.instance_id === instanceId)
634
+ .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
635
+
636
+ const color = instanceColors[instanceId] || generateRandomColor();
637
+
638
+ datasets.push({
639
+ label: `${labelPrefix} - ${instanceId}`,
640
+ data: instanceData.map(item => ({
641
+ x: new Date(item.timestamp),
642
+ y: item.metric_value
643
+ })),
644
+ borderColor: color,
645
+ backgroundColor: color + '20',
646
+ tension: 0.4,
647
+ fill: false
648
+ });
649
+ });
650
+
651
+ chart.data.datasets = datasets;
652
+ chart.update();
653
+ }
654
+
655
+ // Generate a random color
656
+ function generateRandomColor() {
657
+ const r = Math.floor(Math.random() * 255);
658
+ const g = Math.floor(Math.random() * 255);
659
+ const b = Math.floor(Math.random() * 255);
660
+ return `rgb(${r}, ${g}, ${b})`;
661
+ }
662
+
663
+ // Format relative time
664
+ function formatRelativeTime(timestamp) {
665
+ if (!timestamp) return 'Unknown';
666
+
667
+ const now = new Date();
668
+ const time = new Date(timestamp);
669
+ const diffMs = now - time;
670
+ const diffMins = Math.floor(diffMs / 60000);
671
+
672
+ if (diffMins < 1) return 'Just now';
673
+ if (diffMins < 60) return `${diffMins} min ago`;
674
+
675
+ const diffHours = Math.floor(diffMins / 60);
676
+ if (diffHours < 24) return `${diffHours} h ago`;
677
+
678
+ const diffDays = Math.floor(diffHours / 24);
679
+ return `${diffDays} d ago`;
680
+ }
681
+
682
+ // Toggle auto-refresh
683
+ function toggleAutoRefresh() {
684
+ const button = document.getElementById('autoRefreshToggle');
685
+
686
+ if (window.metricsGlobals.isAutoRefreshEnabled) {
687
+ // Turn off auto-refresh
688
+ clearInterval(window.metricsGlobals.metricsAutoRefreshInterval);
689
+ window.metricsGlobals.isAutoRefreshEnabled = false;
690
+ button.innerHTML = '<i data-feather="play" class="w-4 h-4 mr-2"></i>Auto-refresh';
691
+ button.className = 'px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center';
692
+ showNotification('Auto-refresh disabled', 'info');
693
+ } else {
694
+ // Turn on auto-refresh
695
+ window.metricsGlobals.isAutoRefreshEnabled = true;
696
+ window.metricsGlobals.metricsAutoRefreshInterval = setInterval(loadMetricsData, 30000);
697
+ button.innerHTML = '<i data-feather="pause" class="w-4 h-4 mr-2"></i>Stop';
698
+ button.className = 'px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 flex items-center';
699
+ showNotification('Auto-refresh enabled (30 sec)', 'success');
700
+ loadMetricsData();
701
+ }
702
+
703
+ feather.replace();
704
+ }
705
+
706
+ // Restart instance
707
+ async function restartInstance(instanceId) {
708
+ if (!confirm(`Are you sure you want to restart instance ${instanceId}?`)) return;
709
+
710
+ try {
711
+ const token = localStorage.getItem('authToken');
712
+ const response = await fetch(`/api/admin/instances/${instanceId}/restart`, {
713
+ method: 'POST',
714
+ headers: {
715
+ 'Authorization': `Bearer ${token}`
716
+ }
717
+ });
718
+
719
+ if (response.ok) {
720
+ showNotification(`Instance ${instanceId} is restarting...`, 'success');
721
+ // Refresh data after 5 seconds
722
+ setTimeout(loadMetricsData, 5000);
723
+ } else {
724
+ throw new Error('Restart failed');
725
+ }
726
+ } catch (error) {
727
+ console.error('Failed to restart instance:', error);
728
+ showNotification('Failed to restart instance', 'error');
729
+ }
730
+ }
731
+
732
+ // Show instance details
733
+ async function showInstanceDetails(instanceId) {
734
+ try {
735
+ const token = localStorage.getItem('authToken');
736
+ const response = await fetch(`/api/admin/instances/${instanceId}`, {
737
+ headers: {
738
+ 'Authorization': `Bearer ${token}`
739
+ }
740
+ });
741
+
742
+ if (response.ok) {
743
+ const instanceDetails = await response.json();
744
+ showInstanceDetailsModal(instanceDetails);
745
+ } else {
746
+ throw new Error('Failed to load details');
747
+ }
748
+ } catch (error) {
749
+ console.error('Failed to load instance details:', error);
750
+ showNotification('Failed to load instance details', 'error');
751
+ }
752
+ }
753
+
754
+ // Modal window with instance details
755
+ function showInstanceDetailsModal(instance) {
756
+ const modal = document.createElement('div');
757
+ modal.id = 'instanceDetailsModal';
758
+ modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
759
+
760
+ modal.innerHTML = `
761
+ <div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
762
+ <div class="flex justify-between items-center p-6 border-b">
763
+ <h3 class="text-xl font-bold text-gray-800">Instance Details: ${instance.instance_id}</h3>
764
+ <button onclick="closeInstanceDetailsModal()" class="text-gray-500 hover:text-gray-700 p-2">
765
+ <i data-feather="x" class="w-6 h-6"></i>
766
+ </button>
767
+ </div>
768
+
769
+ <div class="p-6">
770
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
771
+ <div class="bg-gray-50 rounded-lg p-4">
772
+ <h4 class="font-semibold text-gray-700 mb-3">System Metrics</h4>
773
+ <div class="space-y-2 text-sm">
774
+ <div class="flex justify-between">
775
+ <span>CPU Usage:</span>
776
+ <span class="font-medium">${instance.current_metrics?.cpu_percent?.toFixed(1) || 0}%</span>
777
+ </div>
778
+ <div class="flex justify-between">
779
+ <span>RAM Usage:</span>
780
+ <span class="font-medium">${instance.current_metrics?.ram_mb?.toFixed(0) || 0} MB</span>
781
+ </div>
782
+ <div class="flex justify-between">
783
+ <span>Disk Space:</span>
784
+ <span class="font-medium">${instance.current_metrics?.disk_usage?.toFixed(1) || 0}%</span>
785
+ </div>
786
+ </div>
787
+ </div>
788
+
789
+ <div class="bg-gray-50 rounded-lg p-4">
790
+ <h4 class="font-semibold text-gray-700 mb-3">Status</h4>
791
+ <div class="space-y-2 text-sm">
792
+ <div class="flex justify-between">
793
+ <span>Status:</span>
794
+ <span class="font-medium ${instance.is_online ? 'text-green-600' : 'text-red-600'}">
795
+ ${instance.is_online ? 'Online' : 'Offline'}
796
+ </span>
797
+ </div>
798
+ <div class="flex justify-between">
799
+ <span>Uptime:</span>
800
+ <span class="font-medium">${instance.uptime || 'Unknown'}</span>
801
+ </div>
802
+ <div class="flex justify-between">
803
+ <span>Last Updated:</span>
804
+ <span class="font-medium">${instance.last_update ? new Date(instance.last_update).toLocaleString() : 'Unknown'}</span>
805
+ </div>
806
+ </div>
807
+ </div>
808
+ </div>
809
+
810
+ <div class="bg-gray-50 rounded-lg p-4">
811
+ <h4 class="font-semibold text-gray-700 mb-3">Recent Events</h4>
812
+ <div class="text-sm text-gray-600">
813
+ ${instance.recent_events && instance.recent_events.length > 0 ?
814
+ instance.recent_events.map(event => `
815
+ <div class="flex justify-between py-1 border-b border-gray-200 last:border-b-0">
816
+ <span>${event.message}</span>
817
+ <span class="text-gray-500">${new Date(event.timestamp).toLocaleString()}</span>
818
+ </div>
819
+ `).join('') :
820
+ '<p class="text-gray-500 text-center py-2">No events</p>'
821
+ }
822
+ </div>
823
+ </div>
824
+ </div>
825
+
826
+ <div class="flex justify-end p-6 border-t bg-gray-50">
827
+ <button onclick="closeInstanceDetailsModal()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
828
+ Close
829
+ </button>
830
+ </div>
831
+ </div>
832
+ `;
833
+
834
+ document.body.appendChild(modal);
835
+ feather.replace();
836
+ }
837
+
838
+ // Close the details modal
839
+ function closeInstanceDetailsModal() {
840
+ const modal = document.getElementById('instanceDetailsModal');
841
+ if (modal) {
842
+ modal.remove();
843
+ }
844
+ }
845
+
846
+ // Set up periodic polling
847
+ function setupMetricsPolling() {
848
+ // Automatically refresh data every 60 seconds while the section is active
849
+ setInterval(() => {
850
+ const metricsSection = document.getElementById('metrics-section');
851
+ if (metricsSection && metricsSection.classList.contains('active') && isAutoRefreshEnabled) {
852
+ loadMetricsData();
853
+ }
854
+ }, 60000);
855
+ }
856
+
857
+ /**
858
+ * Update charts when the time range changes
859
+ */
860
+ function updateMetricsCharts(historicalData) {
861
+ if (!historicalData || !window.metricsGlobals.cpuChart || !window.metricsGlobals.ramChart) {
862
+ console.warn('Charts not initialized or no data');
863
+ return;
864
+ }
865
+ updateChartData(window.metricsGlobals.cpuChart, historicalData.cpu, 'CPU Usage (%)');
866
+ updateChartData(window.metricsGlobals.ramChart, historicalData.ram, 'RAM Usage (MB)');
867
+ }
868
+
869
+ const metricsStyles = `
870
+ .metrics-instance-card {
871
+ transition: all 0.3s ease;
872
+ }
873
+
874
+ .metrics-instance-card:hover {
875
+ transform: translateY(-2px);
876
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
877
+ }
878
+
879
+ .metrics-chart-container {
880
+ position: relative;
881
+ height: 256px;
882
+ }
883
+
884
+ .metrics-status-online {
885
+ background: linear-gradient(135deg, #10b981, #059669);
886
+ }
887
+
888
+ .metrics-status-offline {
889
+ background: linear-gradient(135deg, #ef4444, #dc2626);
890
+ }
891
+
892
+ .metrics-status-warning {
893
+ background: linear-gradient(135deg, #f59e0b, #d97706);
894
+ }
895
+ `;
896
+
897
+ function addMetricsStyles() {
898
+ if (document.getElementById('metrics-styles')) return;
899
+
900
+ const styleSheet = document.createElement('style');
901
+ styleSheet.id = 'metrics-styles';
902
+ styleSheet.textContent = metricsStyles;
903
+ document.head.appendChild(styleSheet);
904
+ }
905
+
906
+ document.addEventListener('DOMContentLoaded', function() {
907
+ addMetricsStyles();
908
+ });