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