PyObservability 0.0.3__py3-none-any.whl → 1.0.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.
- pyobservability/config/settings.py +31 -2
- pyobservability/main.py +12 -32
- pyobservability/monitor.py +146 -168
- pyobservability/static/app.js +366 -185
- pyobservability/static/styles.css +62 -6
- pyobservability/templates/index.html +48 -11
- pyobservability/transport.py +71 -0
- pyobservability/version.py +1 -1
- {pyobservability-0.0.3.dist-info → pyobservability-1.0.0.dist-info}/METADATA +1 -1
- pyobservability-1.0.0.dist-info/RECORD +16 -0
- pyobservability-0.0.3.dist-info/RECORD +0 -15
- {pyobservability-0.0.3.dist-info → pyobservability-1.0.0.dist-info}/WHEEL +0 -0
- {pyobservability-0.0.3.dist-info → pyobservability-1.0.0.dist-info}/entry_points.txt +0 -0
- {pyobservability-0.0.3.dist-info → pyobservability-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {pyobservability-0.0.3.dist-info → pyobservability-1.0.0.dist-info}/top_level.txt +0 -0
pyobservability/static/app.js
CHANGED
|
@@ -5,6 +5,43 @@
|
|
|
5
5
|
// ------------------------------------------------------------
|
|
6
6
|
const MAX_POINTS = 60;
|
|
7
7
|
const targets = window.MONITOR_TARGETS || [];
|
|
8
|
+
const DEFAULT_PAGE_SIZE = 15;
|
|
9
|
+
const panelSpinners = {};
|
|
10
|
+
|
|
11
|
+
// ------------------------------------------------------------
|
|
12
|
+
// VISUAL SPINNERS
|
|
13
|
+
// ------------------------------------------------------------
|
|
14
|
+
function attachSpinners() {
|
|
15
|
+
// panels (charts/tables)
|
|
16
|
+
document.querySelectorAll(".panel").forEach(box => {
|
|
17
|
+
const overlay = document.createElement("div");
|
|
18
|
+
overlay.className = "loading-overlay";
|
|
19
|
+
overlay.innerHTML = `<div class="spinner"></div>`;
|
|
20
|
+
box.style.position = "relative";
|
|
21
|
+
box.appendChild(overlay);
|
|
22
|
+
panelSpinners[box.id || box.querySelector('table,canvas')?.id || Symbol()] = overlay;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// meta cards (system/static metrics)
|
|
26
|
+
document.querySelectorAll(".meta-card").forEach(card => {
|
|
27
|
+
const overlay = document.createElement("div");
|
|
28
|
+
overlay.className = "loading-overlay";
|
|
29
|
+
overlay.innerHTML = `<div class="spinner"></div>`;
|
|
30
|
+
card.style.position = "relative";
|
|
31
|
+
card.appendChild(overlay);
|
|
32
|
+
panelSpinners[card.querySelector(".meta-value")?.id || Symbol()] = overlay;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function showAllSpinners() {
|
|
37
|
+
Object.keys(panelSpinners).forEach(id => showSpinner(id));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hideSpinners() {
|
|
41
|
+
document.querySelectorAll(".loading-overlay").forEach(x => {
|
|
42
|
+
x.classList.add("hidden");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
8
45
|
|
|
9
46
|
// ------------------------------------------------------------
|
|
10
47
|
// DOM REFERENCES
|
|
@@ -12,8 +49,10 @@
|
|
|
12
49
|
const nodeSelect = document.getElementById("node-select");
|
|
13
50
|
const refreshBtn = document.getElementById("refresh-btn");
|
|
14
51
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
52
|
+
const systemEl = document.getElementById("system");
|
|
53
|
+
const ipEl = document.getElementById("ip-info");
|
|
54
|
+
const processorEl = document.getElementById("processor");
|
|
55
|
+
|
|
17
56
|
const memEl = document.getElementById("memory");
|
|
18
57
|
const diskEl = document.getElementById("disk");
|
|
19
58
|
const loadEl = document.getElementById("cpuload");
|
|
@@ -24,18 +63,170 @@
|
|
|
24
63
|
|
|
25
64
|
const coresGrid = document.getElementById("cores-grid");
|
|
26
65
|
|
|
66
|
+
const servicesTable = document.getElementById("services-table");
|
|
27
67
|
const servicesTableBody = document.querySelector("#services-table tbody");
|
|
28
68
|
const svcFilter = document.getElementById("svc-filter");
|
|
69
|
+
const svcGetAll = document.getElementById("get-all-services");
|
|
70
|
+
|
|
71
|
+
const processesTable = document.getElementById("processes-table");
|
|
72
|
+
const processesTableBody = processesTable.querySelector("tbody");
|
|
73
|
+
const procFilter = document.getElementById("proc-filter");
|
|
29
74
|
|
|
30
75
|
const dockerTable = document.getElementById("docker-table");
|
|
31
76
|
const dockerTableHead = dockerTable.querySelector("thead");
|
|
32
77
|
const dockerTableBody = dockerTable.querySelector("tbody");
|
|
33
78
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
79
|
+
const disksTable = document.getElementById("disks-table");
|
|
80
|
+
const disksTableHead = disksTable.querySelector("thead");
|
|
81
|
+
const disksTableBody = disksTable.querySelector("tbody");
|
|
82
|
+
|
|
83
|
+
const pyudiskTable = document.getElementById("pyudisk-table");
|
|
84
|
+
const pyudiskTableHead = pyudiskTable.querySelector("thead");
|
|
85
|
+
const pyudiskTableBody = pyudiskTable.querySelector("tbody");
|
|
86
|
+
|
|
87
|
+
const certsTable = document.getElementById("certificates-table");
|
|
88
|
+
const certsTableHead = certsTable.querySelector("thead");
|
|
89
|
+
const certsTableBody = certsTable.querySelector("tbody");
|
|
36
90
|
|
|
37
91
|
const showCoresCheckbox = document.getElementById("show-cores");
|
|
38
92
|
|
|
93
|
+
// ------------------------------------------------------------
|
|
94
|
+
// PAGINATION HELPERS
|
|
95
|
+
// ------------------------------------------------------------
|
|
96
|
+
function createPaginatedTable(tableEl, headEl, bodyEl, pageSize = DEFAULT_PAGE_SIZE) {
|
|
97
|
+
const info = document.createElement("div");
|
|
98
|
+
info.className = "pagination-info";
|
|
99
|
+
tableEl.insertAdjacentElement("beforebegin", info);
|
|
100
|
+
|
|
101
|
+
const state = {
|
|
102
|
+
data: [],
|
|
103
|
+
page: 1,
|
|
104
|
+
pageSize
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const pagination = document.createElement("div");
|
|
108
|
+
pagination.className = "pagination";
|
|
109
|
+
tableEl.insertAdjacentElement("afterend", pagination);
|
|
110
|
+
|
|
111
|
+
function render() {
|
|
112
|
+
const rows = state.data;
|
|
113
|
+
const pages = Math.ceil(rows.length / state.pageSize) || 1;
|
|
114
|
+
state.page = Math.max(1, Math.min(state.page, pages));
|
|
115
|
+
|
|
116
|
+
const start = (state.page - 1) * state.pageSize;
|
|
117
|
+
const chunk = rows.slice(start, start + state.pageSize);
|
|
118
|
+
|
|
119
|
+
info.textContent =
|
|
120
|
+
`Showing ${start + 1} to ${Math.min(start + state.pageSize, rows.length)} of ${rows.length} entries`;
|
|
121
|
+
|
|
122
|
+
bodyEl.innerHTML = "";
|
|
123
|
+
chunk.forEach(r => bodyEl.insertAdjacentHTML("beforeend", r));
|
|
124
|
+
|
|
125
|
+
renderPagination(pages);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderPagination(pages) {
|
|
129
|
+
pagination.innerHTML = "";
|
|
130
|
+
|
|
131
|
+
const makeBtn = (txt, cb, active = false, disabled = false) => {
|
|
132
|
+
const b = document.createElement("button");
|
|
133
|
+
b.textContent = txt;
|
|
134
|
+
if (active) b.classList.add("active");
|
|
135
|
+
if (disabled) {
|
|
136
|
+
b.disabled = true;
|
|
137
|
+
b.style.opacity = "0.5";
|
|
138
|
+
b.style.cursor = "default";
|
|
139
|
+
}
|
|
140
|
+
b.onclick = disabled ? null : cb;
|
|
141
|
+
pagination.appendChild(b);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// --- Previous ---
|
|
145
|
+
makeBtn("Previous", () => { state.page--; render(); }, false, state.page === 1);
|
|
146
|
+
|
|
147
|
+
const maxVisible = 5;
|
|
148
|
+
|
|
149
|
+
if (pages <= maxVisible + 2) {
|
|
150
|
+
// Show all
|
|
151
|
+
for (let p = 1; p <= pages; p++) {
|
|
152
|
+
makeBtn(p, () => { state.page = p; render(); }, p === state.page);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Big list → use ellipsis
|
|
156
|
+
const showLeft = 3;
|
|
157
|
+
const showRight = 3;
|
|
158
|
+
|
|
159
|
+
if (state.page <= showLeft) {
|
|
160
|
+
// First pages
|
|
161
|
+
for (let p = 1; p <= showLeft + 1; p++) {
|
|
162
|
+
makeBtn(p, () => { state.page = p; render(); }, p === state.page);
|
|
163
|
+
}
|
|
164
|
+
addEllipsis();
|
|
165
|
+
makeBtn(pages, () => { state.page = pages; render(); });
|
|
166
|
+
} else if (state.page >= pages - showRight + 1) {
|
|
167
|
+
// Last pages
|
|
168
|
+
makeBtn(1, () => { state.page = 1; render(); });
|
|
169
|
+
addEllipsis();
|
|
170
|
+
for (let p = pages - showRight; p <= pages; p++) {
|
|
171
|
+
makeBtn(p, () => { state.page = p; render(); }, p === state.page);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// Middle
|
|
175
|
+
makeBtn(1, () => { state.page = 1; render(); });
|
|
176
|
+
addEllipsis();
|
|
177
|
+
for (let p = state.page - 1; p <= state.page + 1; p++) {
|
|
178
|
+
makeBtn(p, () => { state.page = p; render(); }, p === state.page);
|
|
179
|
+
}
|
|
180
|
+
addEllipsis();
|
|
181
|
+
makeBtn(pages, () => { state.page = pages; render(); });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Next ---
|
|
186
|
+
makeBtn("Next", () => { state.page++; render(); }, false, state.page === pages);
|
|
187
|
+
|
|
188
|
+
function addEllipsis() {
|
|
189
|
+
const e = document.createElement("span");
|
|
190
|
+
e.textContent = "…";
|
|
191
|
+
e.style.padding = "4px 6px";
|
|
192
|
+
pagination.appendChild(e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function setData(arr, columns) {
|
|
197
|
+
if (JSON.stringify(state.data) === JSON.stringify(arr)) {
|
|
198
|
+
return; // do not re-render if data didn't change
|
|
199
|
+
}
|
|
200
|
+
headEl.innerHTML = "<tr>" + columns.map(c => `<th>${c}</th>`).join("") + "</tr>";
|
|
201
|
+
state.data = arr.map(row => {
|
|
202
|
+
return "<tr>" + columns.map(c => `<td>${row[c] ?? ""}</td>`).join("") + "</tr>";
|
|
203
|
+
});
|
|
204
|
+
render();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { setData };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Instances for each table
|
|
211
|
+
const PAG_SERVICES = createPaginatedTable(
|
|
212
|
+
servicesTable, servicesTable.querySelector("thead"), servicesTableBody, 5
|
|
213
|
+
);
|
|
214
|
+
const PAG_PROCESSES = createPaginatedTable(
|
|
215
|
+
processesTable, processesTable.querySelector("thead"), processesTableBody
|
|
216
|
+
);
|
|
217
|
+
const PAG_DOCKER = createPaginatedTable(
|
|
218
|
+
dockerTable, dockerTableHead, dockerTableBody
|
|
219
|
+
);
|
|
220
|
+
const PAG_DISKS = createPaginatedTable(
|
|
221
|
+
disksTable, disksTableHead, disksTableBody
|
|
222
|
+
);
|
|
223
|
+
const PAG_PYUDISK = createPaginatedTable(
|
|
224
|
+
pyudiskTable, pyudiskTableHead, pyudiskTableBody
|
|
225
|
+
);
|
|
226
|
+
const PAG_CERTS = createPaginatedTable(
|
|
227
|
+
certsTable, certsTableHead, certsTableBody
|
|
228
|
+
);
|
|
229
|
+
|
|
39
230
|
// ------------------------------------------------------------
|
|
40
231
|
// CHART HELPERS
|
|
41
232
|
// ------------------------------------------------------------
|
|
@@ -59,20 +250,9 @@
|
|
|
59
250
|
]
|
|
60
251
|
},
|
|
61
252
|
options: {
|
|
62
|
-
animation: false,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
spanGaps: false,
|
|
66
|
-
scales: {
|
|
67
|
-
x: { display: false },
|
|
68
|
-
y: {
|
|
69
|
-
beginAtZero: true,
|
|
70
|
-
suggestedMax: 100
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
plugins: {
|
|
74
|
-
legend: { display: false }
|
|
75
|
-
}
|
|
253
|
+
animation: false, responsive: true, maintainAspectRatio: false,
|
|
254
|
+
scales: { x: { display: false }, y: { beginAtZero: true, suggestedMax: 100 }},
|
|
255
|
+
plugins: { legend: { display: false } }
|
|
76
256
|
}
|
|
77
257
|
});
|
|
78
258
|
}
|
|
@@ -98,7 +278,6 @@
|
|
|
98
278
|
responsive: false,
|
|
99
279
|
interaction: false,
|
|
100
280
|
events: [],
|
|
101
|
-
spanGaps: false,
|
|
102
281
|
plugins: { legend: { display: false } },
|
|
103
282
|
scales: {
|
|
104
283
|
x: { display: false },
|
|
@@ -109,11 +288,11 @@
|
|
|
109
288
|
}
|
|
110
289
|
|
|
111
290
|
const cpuAvgChart = makeMainChart(cpuAvgCtx, "CPU Avg");
|
|
112
|
-
const memChart = makeMainChart(memCtx,
|
|
113
|
-
const loadChart = makeMainChart(loadCtx,
|
|
291
|
+
const memChart = makeMainChart(memCtx, "Memory %");
|
|
292
|
+
const loadChart = makeMainChart(loadCtx, "CPU Load");
|
|
114
293
|
|
|
115
294
|
// ------------------------------------------------------------
|
|
116
|
-
// CORE
|
|
295
|
+
// CORE CHARTS
|
|
117
296
|
// ------------------------------------------------------------
|
|
118
297
|
const coreMini = {};
|
|
119
298
|
|
|
@@ -125,6 +304,7 @@
|
|
|
125
304
|
<canvas width="120" height="40"></canvas>
|
|
126
305
|
<div class="value">—</div>
|
|
127
306
|
`;
|
|
307
|
+
wrapper.style.display = showCoresCheckbox.checked ? "block" : "none";
|
|
128
308
|
coresGrid.appendChild(wrapper);
|
|
129
309
|
|
|
130
310
|
const canvas = wrapper.querySelector("canvas");
|
|
@@ -152,234 +332,224 @@
|
|
|
152
332
|
// ------------------------------------------------------------
|
|
153
333
|
// RESET UI
|
|
154
334
|
// ------------------------------------------------------------
|
|
335
|
+
function resetTables() {
|
|
336
|
+
// Clear all table data immediately
|
|
337
|
+
PAG_SERVICES.setData([], []);
|
|
338
|
+
PAG_PROCESSES.setData([], []);
|
|
339
|
+
PAG_DOCKER.setData([], []);
|
|
340
|
+
PAG_DISKS.setData([], []);
|
|
341
|
+
PAG_PYUDISK.setData([], []);
|
|
342
|
+
PAG_CERTS.setData([], []);
|
|
343
|
+
}
|
|
344
|
+
|
|
155
345
|
function resetUI() {
|
|
156
|
-
|
|
346
|
+
firstMessage = true;
|
|
347
|
+
hideSpinners();
|
|
157
348
|
const EMPTY_DATA = Array(MAX_POINTS).fill(null);
|
|
158
349
|
const EMPTY_LABELS = Array(MAX_POINTS).fill("");
|
|
159
350
|
|
|
160
|
-
|
|
351
|
+
const resetChart = chart => {
|
|
161
352
|
chart.data.labels = [...EMPTY_LABELS];
|
|
162
353
|
chart.data.datasets[0].data = [...EMPTY_DATA];
|
|
163
354
|
chart.update();
|
|
164
|
-
}
|
|
355
|
+
};
|
|
165
356
|
|
|
166
|
-
// Reset main charts (CPU Avg, Memory %, CPU Load)
|
|
167
357
|
resetChart(cpuAvgChart);
|
|
168
358
|
resetChart(memChart);
|
|
169
359
|
resetChart(loadChart);
|
|
170
360
|
|
|
171
|
-
// Remove all per-core mini charts
|
|
172
361
|
for (const name of Object.keys(coreMini)) {
|
|
173
362
|
try { coreMini[name].chart.destroy(); } catch {}
|
|
174
363
|
coreMini[name].el.remove();
|
|
175
364
|
delete coreMini[name];
|
|
176
365
|
}
|
|
177
366
|
|
|
178
|
-
|
|
367
|
+
systemEl.textContent = "-";
|
|
179
368
|
ipEl.textContent = "—";
|
|
180
|
-
|
|
369
|
+
processorEl.textContent = "—";
|
|
181
370
|
memEl.textContent = "—";
|
|
182
371
|
diskEl.textContent = "—";
|
|
183
372
|
loadEl.textContent = "—";
|
|
184
|
-
|
|
185
|
-
servicesTableBody.innerHTML = "";
|
|
186
|
-
dockerTableHead.innerHTML = "";
|
|
187
|
-
dockerTableBody.innerHTML = "";
|
|
188
|
-
disksTableBody.innerHTML = "";
|
|
189
|
-
certsEl.textContent = "—";
|
|
190
373
|
}
|
|
191
374
|
|
|
192
375
|
// ------------------------------------------------------------
|
|
193
|
-
// HELPERS
|
|
376
|
+
// MISC HELPERS
|
|
194
377
|
// ------------------------------------------------------------
|
|
195
378
|
function pushPoint(chart, value) {
|
|
196
379
|
const ts = new Date().toLocaleTimeString();
|
|
197
380
|
chart.data.labels.push(ts);
|
|
198
381
|
chart.data.datasets[0].data.push(isFinite(value) ? Number(value) : NaN);
|
|
199
|
-
|
|
200
382
|
if (chart.data.labels.length > MAX_POINTS) {
|
|
201
383
|
chart.data.labels.shift();
|
|
202
384
|
chart.data.datasets[0].data.shift();
|
|
203
385
|
}
|
|
204
|
-
|
|
205
386
|
chart.update("none");
|
|
206
387
|
}
|
|
207
388
|
|
|
208
|
-
function num(x) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function formatStringOrObject(x) {
|
|
389
|
+
function num(x) { const n = Number(x); return Number.isFinite(n) ? n : null; }
|
|
390
|
+
const round2 = x => Number(x).toFixed(2);
|
|
391
|
+
const formatBytes = x => {
|
|
214
392
|
if (x == null) return "—";
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
393
|
+
const u = ["B","KB","MB","GB","TB"];
|
|
394
|
+
let i = 0, n = Number(x);
|
|
395
|
+
while (n > 1024 && i < u.length-1) { n/=1024; i++; }
|
|
396
|
+
return n.toFixed(2) + " " + u[i];
|
|
397
|
+
};
|
|
398
|
+
const objectToString = (...vals) => {
|
|
399
|
+
for (const v of vals) {
|
|
400
|
+
if (v && typeof v === "object")
|
|
401
|
+
return Object.entries(v).map(([a,b])=>`${a}: ${b}`).join("<br>");
|
|
402
|
+
if (v != null) return v;
|
|
403
|
+
}
|
|
404
|
+
return "—";
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
function showSpinner(panelOrTableId) {
|
|
408
|
+
const overlay = panelSpinners[panelOrTableId];
|
|
409
|
+
if (overlay) overlay.classList.remove("hidden");
|
|
219
410
|
}
|
|
220
411
|
|
|
221
|
-
function
|
|
222
|
-
const
|
|
223
|
-
|
|
412
|
+
function hideSpinner(panelOrTableId) {
|
|
413
|
+
const overlay = panelSpinners[panelOrTableId];
|
|
414
|
+
if (overlay) overlay.classList.add("hidden");
|
|
224
415
|
}
|
|
225
416
|
|
|
226
417
|
// ------------------------------------------------------------
|
|
227
|
-
// METRICS
|
|
418
|
+
// HANDLE METRICS
|
|
228
419
|
// ------------------------------------------------------------
|
|
420
|
+
let firstMessage = true;
|
|
421
|
+
|
|
229
422
|
function handleMetrics(list) {
|
|
423
|
+
if (firstMessage) {
|
|
424
|
+
hideSpinners();
|
|
425
|
+
firstMessage = false;
|
|
426
|
+
}
|
|
427
|
+
|
|
230
428
|
const now = new Date().toLocaleTimeString();
|
|
231
429
|
|
|
232
430
|
for (const host of list) {
|
|
233
431
|
if (host.base_url !== selectedBase) continue;
|
|
234
432
|
const m = host.metrics || {};
|
|
235
433
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
434
|
+
// ------------ Static fields ------------
|
|
435
|
+
systemEl.textContent =
|
|
436
|
+
`Node: ${m.node || "-"}\n` +
|
|
437
|
+
`OS: ${m.system || "-"}\n` +
|
|
438
|
+
`Architecture: ${m.architecture || "-"}\n\n` +
|
|
439
|
+
`CPU Cores: ${m.cores || "-"}\n` +
|
|
440
|
+
`Up Time: ${m.uptime || "-"}\n`;
|
|
441
|
+
|
|
442
|
+
if (m.ip_info)
|
|
443
|
+
ipEl.textContent =
|
|
444
|
+
`Private: ${m.ip_info.private || "-"}\n\n` +
|
|
445
|
+
`Public: ${m.ip_info.public || "-"}`;
|
|
446
|
+
|
|
447
|
+
processorEl.textContent =
|
|
448
|
+
`CPU: ${m.cpu_name || "-"}\n\n` +
|
|
449
|
+
`GPU: ${m.gpu_name || "-"}`;
|
|
450
|
+
|
|
451
|
+
if (m.disk_info && m.disk_info[0]) {
|
|
452
|
+
const d = m.disk_info[0];
|
|
453
|
+
diskEl.textContent =
|
|
454
|
+
`Total: ${formatBytes(d.total)}\n` +
|
|
455
|
+
`Used: ${formatBytes(d.used)}\n` +
|
|
456
|
+
`Free: ${formatBytes(d.free)}`;
|
|
246
457
|
}
|
|
247
458
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
pushPoint(memChart, num(percent));
|
|
255
|
-
} else {
|
|
256
|
-
memEl.textContent = "NO DATA"
|
|
459
|
+
if (m.memory_info) {
|
|
460
|
+
memEl.textContent =
|
|
461
|
+
`Total: ${formatBytes(m.memory_info.total)}\n` +
|
|
462
|
+
`Used: ${formatBytes(m.memory_info.used)}\n` +
|
|
463
|
+
`Percent: ${round2(m.memory_info.percent)}%`;
|
|
464
|
+
pushPoint(memChart, num(m.memory_info.percent));
|
|
257
465
|
}
|
|
258
466
|
|
|
259
|
-
//
|
|
467
|
+
// ------------ CPU ------------
|
|
260
468
|
let avg = null;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const c = getCoreChart(core);
|
|
276
|
-
c.chart.data.labels.push(now);
|
|
277
|
-
c.chart.data.datasets[0].data.push(v ?? 0);
|
|
278
|
-
|
|
279
|
-
if (c.chart.data.labels.length > MAX_POINTS) {
|
|
280
|
-
c.chart.data.labels.shift();
|
|
281
|
-
c.chart.data.datasets[0].data.shift();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
c.chart.update("none");
|
|
285
|
-
c.valEl.textContent = `${(v ?? 0).toFixed(1)}%`;
|
|
469
|
+
if (m.cpu_usage) {
|
|
470
|
+
const values = m.cpu_usage.map(num);
|
|
471
|
+
avg = values.reduce((a,b)=>a+(b||0),0) / values.length;
|
|
472
|
+
pruneOldCores(values.map((_,i)=>"cpu"+(i+1)));
|
|
473
|
+
|
|
474
|
+
values.forEach((v,i)=>{
|
|
475
|
+
const core = getCoreChart("cpu"+(i+1));
|
|
476
|
+
|
|
477
|
+
core.chart.data.labels.push(now);
|
|
478
|
+
core.chart.data.datasets[0].data.push(v||0);
|
|
479
|
+
if (core.chart.data.labels.length > MAX_POINTS) {
|
|
480
|
+
core.chart.data.labels.shift();
|
|
481
|
+
core.chart.data.datasets[0].data.shift();
|
|
286
482
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
else if (typeof detail === "number") {
|
|
291
|
-
avg = detail;
|
|
292
|
-
}
|
|
483
|
+
core.chart.update({ lazy: true });
|
|
484
|
+
core.valEl.textContent = `${(v||0).toFixed(1)}%`;
|
|
485
|
+
});
|
|
293
486
|
}
|
|
294
|
-
|
|
295
487
|
if (avg != null) pushPoint(cpuAvgChart, avg);
|
|
296
488
|
|
|
297
|
-
//
|
|
298
|
-
if (m.
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const m15 = load.m15 ?? load[2];
|
|
304
|
-
|
|
305
|
-
loadEl.textContent = `${round2(m1)} / ${round2(m5)} / ${round2(m15)}`;
|
|
306
|
-
pushPoint(loadChart, num(m1) ?? 0);
|
|
307
|
-
} else {
|
|
308
|
-
loadEl.textContent = load;
|
|
309
|
-
pushPoint(loadChart, num(load));
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
loadEl.textContent = "NO DATA"
|
|
489
|
+
// ------------ LOAD ------------
|
|
490
|
+
if (m.load_averages) {
|
|
491
|
+
const la = m.load_averages;
|
|
492
|
+
loadEl.textContent =
|
|
493
|
+
`${round2(la.m1)} / ${round2(la.m5)} / ${round2(la.m15)}`;
|
|
494
|
+
pushPoint(loadChart, num(la.m1));
|
|
313
495
|
}
|
|
314
496
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
} else {
|
|
336
|
-
servicesTableBody.innerHTML = `<tr><td colspan="5">NO DATA</td></tr>`;
|
|
497
|
+
// ------------ SERVICES (paginated) ------------
|
|
498
|
+
const services = (m.service_stats || m.services || []).filter(s =>
|
|
499
|
+
(s.pname || s.Name || "").toLowerCase().includes(
|
|
500
|
+
svcFilter.value.trim().toLowerCase()
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
if (services.length) {
|
|
504
|
+
const columns = ["PID","Name","Status","CPU","Memory","Threads","Open Files"];
|
|
505
|
+
const cleaned = services.map(s => ({
|
|
506
|
+
PID: s.PID ?? s.pid ?? "",
|
|
507
|
+
Name: s.pname ?? s.Name ?? s.name ?? "",
|
|
508
|
+
Status: s.Status ?? s.active ?? s.status ?? s.Active ?? "—",
|
|
509
|
+
CPU: objectToString(s.CPU, s.cpu),
|
|
510
|
+
Memory: objectToString(s.Memory, s.memory),
|
|
511
|
+
Threads: s.Threads ?? s.threads ?? "—",
|
|
512
|
+
"Open Files": s["Open Files"] ?? s.open_files ?? "—"
|
|
513
|
+
}));
|
|
514
|
+
PAG_SERVICES.setData(cleaned, columns);
|
|
515
|
+
hideSpinner("services-table");
|
|
337
516
|
}
|
|
338
517
|
|
|
339
|
-
//
|
|
340
|
-
const
|
|
518
|
+
// ------------ PROCESSES (paginated) ------------
|
|
519
|
+
const processes = (m.process_stats || []).filter(p =>
|
|
520
|
+
(p.Name || "").toLowerCase().includes(
|
|
521
|
+
procFilter.value.trim().toLowerCase()
|
|
522
|
+
)
|
|
523
|
+
);
|
|
524
|
+
if (processes.length) {
|
|
525
|
+
const columns = ["PID","Name","Status","CPU","Memory","Uptime","Threads","Open Files"];
|
|
526
|
+
PAG_PROCESSES.setData(processes, columns);
|
|
527
|
+
hideSpinner("processes-table");
|
|
528
|
+
}
|
|
341
529
|
|
|
342
|
-
|
|
343
|
-
|
|
530
|
+
// ------------ DOCKER, DISKS, PYUDISK, CERTIFICATES ------------
|
|
531
|
+
if (m.docker_stats) {
|
|
532
|
+
const cols = Object.keys(m.docker_stats[0] || {});
|
|
533
|
+
PAG_DOCKER.setData(m.docker_stats, cols);
|
|
534
|
+
hideSpinner("docker-table");
|
|
535
|
+
}
|
|
344
536
|
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const columns = Object.keys(dockerList[0]);
|
|
350
|
-
dockerTableHead.innerHTML =
|
|
351
|
-
"<tr>" + columns.map(c => `<th>${c}</th>`).join("") + "</tr>";
|
|
352
|
-
|
|
353
|
-
// Create rows
|
|
354
|
-
dockerList.forEach(c => {
|
|
355
|
-
const row = "<tr>" +
|
|
356
|
-
columns.map(col => `<td>${c[col] ?? ""}</td>`).join("") +
|
|
357
|
-
"</tr>";
|
|
358
|
-
dockerTableBody.insertAdjacentHTML("beforeend", row);
|
|
359
|
-
});
|
|
537
|
+
if (m.disks_info) {
|
|
538
|
+
const cols = Object.keys(m.disks_info[0] || {});
|
|
539
|
+
PAG_DISKS.setData(m.disks_info, cols);
|
|
540
|
+
hideSpinner("disks-table");
|
|
360
541
|
}
|
|
361
542
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const tr = document.createElement("tr");
|
|
367
|
-
tr.innerHTML = `
|
|
368
|
-
<td>${d.name || d.device_id || ""}</td>
|
|
369
|
-
<td>${d.size || d.total || ""}</td>
|
|
370
|
-
<td>${(d.mountpoints || []).join(", ")}</td>
|
|
371
|
-
`;
|
|
372
|
-
disksTableBody.appendChild(tr);
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
disksTableBody.innerHTML = `<tr><td colspan="3">NO DATA</td></tr>`;
|
|
543
|
+
if (m.pyudisk_stats) {
|
|
544
|
+
const cols = Object.keys(m.pyudisk_stats[0] || {});
|
|
545
|
+
PAG_PYUDISK.setData(m.pyudisk_stats, cols);
|
|
546
|
+
hideSpinner("pyudisk-table");
|
|
376
547
|
}
|
|
377
548
|
|
|
378
|
-
// ------------------- CERTIFICATES -------------------
|
|
379
549
|
if (m.certificates) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
550
|
+
const cols = Object.keys(m.certificates[0] || {});
|
|
551
|
+
PAG_CERTS.setData(m.certificates, cols);
|
|
552
|
+
hideSpinner("certificates-table");
|
|
383
553
|
}
|
|
384
554
|
}
|
|
385
555
|
}
|
|
@@ -394,22 +564,26 @@
|
|
|
394
564
|
nodeSelect.appendChild(opt);
|
|
395
565
|
});
|
|
396
566
|
|
|
397
|
-
let selectedBase =
|
|
398
|
-
nodeSelect.value || (targets[0] && targets[0].base_url);
|
|
567
|
+
let selectedBase = nodeSelect.value || (targets[0] && targets[0].base_url);
|
|
399
568
|
nodeSelect.value = selectedBase;
|
|
400
569
|
|
|
401
570
|
nodeSelect.addEventListener("change", () => {
|
|
402
571
|
selectedBase = nodeSelect.value;
|
|
403
572
|
resetUI();
|
|
573
|
+
resetTables();
|
|
574
|
+
showAllSpinners();
|
|
575
|
+
ws.send(JSON.stringify({ type: "select_target", base_url: selectedBase }));
|
|
404
576
|
});
|
|
405
577
|
|
|
578
|
+
svcGetAll.addEventListener("change", () => {
|
|
579
|
+
ws.send(JSON.stringify({ type: "update_flags", all_services: svcGetAll.checked }));
|
|
580
|
+
})
|
|
581
|
+
|
|
406
582
|
refreshBtn.addEventListener("click", resetUI);
|
|
407
583
|
|
|
408
584
|
showCoresCheckbox.addEventListener("change", () => {
|
|
409
585
|
const visible = showCoresCheckbox.checked;
|
|
410
|
-
Object.values(coreMini).forEach(c =>
|
|
411
|
-
c.el.style.display = visible ? "block" : "none";
|
|
412
|
-
});
|
|
586
|
+
Object.values(coreMini).forEach(c => c.el.style.display = visible ? "block" : "none");
|
|
413
587
|
});
|
|
414
588
|
|
|
415
589
|
// ------------------------------------------------------------
|
|
@@ -418,10 +592,15 @@
|
|
|
418
592
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
419
593
|
const ws = new WebSocket(`${protocol}://${location.host}/ws`);
|
|
420
594
|
|
|
595
|
+
ws.onopen = () => {
|
|
596
|
+
ws.send(JSON.stringify({ type: "select_target", base_url: selectedBase }));
|
|
597
|
+
};
|
|
598
|
+
|
|
421
599
|
ws.onmessage = evt => {
|
|
422
600
|
try {
|
|
423
601
|
const msg = JSON.parse(evt.data);
|
|
424
602
|
if (msg.type === "metrics") handleMetrics(msg.data);
|
|
603
|
+
if (msg.type === "error") alert(msg.message);
|
|
425
604
|
} catch (err) {
|
|
426
605
|
console.error("WS parse error:", err);
|
|
427
606
|
}
|
|
@@ -430,5 +609,7 @@
|
|
|
430
609
|
// ------------------------------------------------------------
|
|
431
610
|
// INIT
|
|
432
611
|
// ------------------------------------------------------------
|
|
433
|
-
|
|
612
|
+
attachSpinners();
|
|
613
|
+
resetUI(); // reset UI, keep spinners visible
|
|
614
|
+
showAllSpinners(); // show spinners until first metrics arrive
|
|
434
615
|
})();
|