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.
- keepup/THIRD-PARTY.md +44 -0
- keepup/__init__.py +41 -0
- keepup/api_versions.py +100 -0
- keepup/audit.py +499 -0
- keepup/auth/__init__.py +7 -0
- keepup/auth/config.py +128 -0
- keepup/auth/dependencies.py +485 -0
- keepup/auth/dto/__init__.py +0 -0
- keepup/auth/dto/token.py +31 -0
- keepup/auth/factory.py +28 -0
- keepup/auth/login_throttle.py +151 -0
- keepup/auth/oidc.py +245 -0
- keepup/auth/oidc_policy.py +111 -0
- keepup/auth/oidc_routes.py +288 -0
- keepup/auth/panel_session.py +220 -0
- keepup/auth/permissions.py +59 -0
- keepup/auth/providers/__init__.py +0 -0
- keepup/auth/providers/base.py +168 -0
- keepup/auth/providers/local.py +180 -0
- keepup/auth/routes.py +660 -0
- keepup/auth/seed_accounts.py +322 -0
- keepup/auth/session_lifetime.py +104 -0
- keepup/auth/signing_key.py +138 -0
- keepup/auth/websocket.py +86 -0
- keepup/cluster.py +634 -0
- keepup/db.py +663 -0
- keepup/events.py +764 -0
- keepup/factory.py +484 -0
- keepup/instance.py +46 -0
- keepup/integrations.py +260 -0
- keepup/locks.py +412 -0
- keepup/logging_setup.py +690 -0
- keepup/metrics.py +818 -0
- keepup/metrics_retention.py +376 -0
- keepup/modules.py +572 -0
- keepup/notification_bus.py +355 -0
- keepup/plugins/__init__.py +9 -0
- keepup/plugins/admin.py +246 -0
- keepup/plugins/base.py +234 -0
- keepup/plugins/enablement.py +184 -0
- keepup/plugins/registry.py +171 -0
- keepup/plugins/route_mask.py +338 -0
- keepup/plugins/routes.py +376 -0
- keepup/roles.py +17 -0
- keepup/scheduler.py +93 -0
- keepup/schema.py +295 -0
- keepup/sections.json +104 -0
- keepup/settings.py +184 -0
- keepup/static/css/aos.css +1 -0
- keepup/static/css/main_nebula.css +232 -0
- keepup/static/css/main_new.css +852 -0
- keepup/static/css/tailwind.css +1 -0
- keepup/static/index_nebula.html +293 -0
- keepup/static/index_new.html +286 -0
- keepup/static/js/aos.js +1 -0
- keepup/static/js/feather-icons.js +13 -0
- keepup/static/js/main_new.js +1861 -0
- keepup/static/js/tailwind.js +83 -0
- keepup/static/modules/css/background_tasks.css +233 -0
- keepup/static/modules/css/cluster.css +16 -0
- keepup/static/modules/css/event_manager.css +386 -0
- keepup/static/modules/css/integration_logs.css +33 -0
- keepup/static/modules/css/metrics.css +115 -0
- keepup/static/modules/css/modules.css +189 -0
- keepup/static/modules/css/themes.css +563 -0
- keepup/static/modules/css/users.css +278 -0
- keepup/static/modules/js/background_tasks.js +657 -0
- keepup/static/modules/js/chart.js +14 -0
- keepup/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
- keepup/static/modules/js/cluster.js +363 -0
- keepup/static/modules/js/event_manager.js +979 -0
- keepup/static/modules/js/integration_logs.js +767 -0
- keepup/static/modules/js/metrics.js +908 -0
- keepup/static/modules/js/modules.js +1086 -0
- keepup/static/modules/js/themes.js +653 -0
- keepup/static/modules/js/users.js +784 -0
- keepup/tables.py +302 -0
- keepup/themes.py +496 -0
- keepup/web.py +182 -0
- keepup_admin-0.1.0.dist-info/METADATA +117 -0
- keepup_admin-0.1.0.dist-info/RECORD +86 -0
- keepup_admin-0.1.0.dist-info/WHEEL +5 -0
- keepup_admin-0.1.0.dist-info/licenses/LICENSE +202 -0
- keepup_admin-0.1.0.dist-info/licenses/NOTICE +22 -0
- keepup_admin-0.1.0.dist-info/licenses/THIRD-PARTY.md +44 -0
- 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
|
+
});
|