PyObservability 1.1.0__py3-none-any.whl → 1.2.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 +616 -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.2.0.dist-info}/METADATA +9 -1
- pyobservability-1.2.0.dist-info/RECORD +16 -0
- pyobservability-1.1.0.dist-info/RECORD +0 -16
- {pyobservability-1.1.0.dist-info → pyobservability-1.2.0.dist-info}/WHEEL +0 -0
- {pyobservability-1.1.0.dist-info → pyobservability-1.2.0.dist-info}/entry_points.txt +0 -0
- {pyobservability-1.1.0.dist-info → pyobservability-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {pyobservability-1.1.0.dist-info → pyobservability-1.2.0.dist-info}/top_level.txt +0 -0
pyobservability/static/app.js
CHANGED
|
@@ -1,615 +1,658 @@
|
|
|
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
|
+
if (JSON.stringify(state.data) === JSON.stringify(arr)) {
|
|
228
|
+
return; // do not re-render if data didn't change
|
|
229
|
+
}
|
|
230
|
+
headEl.innerHTML = "<tr>" + columns.map(c => `<th>${c}</th>`).join("") + "</tr>";
|
|
231
|
+
state.data = arr.map(row => {
|
|
232
|
+
return "<tr>" + columns.map(c => `<td>${row[c] ?? ""}</td>`).join("") + "</tr>";
|
|
233
|
+
});
|
|
234
|
+
render();
|
|
235
|
+
}
|
|
187
236
|
|
|
188
|
-
|
|
189
|
-
const e = document.createElement("span");
|
|
190
|
-
e.textContent = "…";
|
|
191
|
-
e.style.padding = "4px 6px";
|
|
192
|
-
pagination.appendChild(e);
|
|
193
|
-
}
|
|
237
|
+
return {setData};
|
|
194
238
|
}
|
|
195
239
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
240
|
+
// Instances for each table
|
|
241
|
+
const PAG_SERVICES = createPaginatedTable(
|
|
242
|
+
servicesTable, servicesTable.querySelector("thead"), servicesTableBody, 5
|
|
243
|
+
);
|
|
244
|
+
const PAG_PROCESSES = createPaginatedTable(
|
|
245
|
+
processesTable, processesTable.querySelector("thead"), processesTableBody
|
|
246
|
+
);
|
|
247
|
+
const PAG_DOCKER = createPaginatedTable(
|
|
248
|
+
dockerTable, dockerTableHead, dockerTableBody
|
|
249
|
+
);
|
|
250
|
+
const PAG_DISKS = createPaginatedTable(
|
|
251
|
+
disksTable, disksTableHead, disksTableBody
|
|
252
|
+
);
|
|
253
|
+
const PAG_PYUDISK = createPaginatedTable(
|
|
254
|
+
pyudiskTable, pyudiskTableHead, pyudiskTableBody
|
|
255
|
+
);
|
|
256
|
+
const PAG_CERTS = createPaginatedTable(
|
|
257
|
+
certsTable, certsTableHead, certsTableBody
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// ------------------------------------------------------------
|
|
261
|
+
// CHART HELPERS
|
|
262
|
+
// ------------------------------------------------------------
|
|
263
|
+
function makeMainChart(ctx, label) {
|
|
264
|
+
const EMPTY = Array(MAX_POINTS).fill(null);
|
|
265
|
+
const LABELS = Array(MAX_POINTS).fill("");
|
|
266
|
+
|
|
267
|
+
return new Chart(ctx, {
|
|
268
|
+
type: "line",
|
|
269
|
+
data: {
|
|
270
|
+
labels: [...LABELS],
|
|
271
|
+
datasets: [
|
|
272
|
+
{
|
|
273
|
+
label,
|
|
274
|
+
data: [...EMPTY],
|
|
275
|
+
fill: true,
|
|
276
|
+
tension: 0.2,
|
|
277
|
+
cubicInterpolationMode: "monotone",
|
|
278
|
+
pointRadius: 0
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
options: {
|
|
283
|
+
animation: false, responsive: true, maintainAspectRatio: false,
|
|
284
|
+
scales: {x: {display: false}, y: {beginAtZero: true, suggestedMax: 100}},
|
|
285
|
+
plugins: {legend: {display: false}}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
205
288
|
}
|
|
206
289
|
|
|
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
|
-
}
|
|
290
|
+
function makeCoreSparkline(ctx, coreName) {
|
|
291
|
+
const EMPTY_LABELS = Array(MAX_POINTS).fill("");
|
|
292
|
+
const EMPTY_DATA = Array(MAX_POINTS).fill(null);
|
|
293
|
+
|
|
294
|
+
return new Chart(ctx, {
|
|
295
|
+
type: "line",
|
|
296
|
+
data: {
|
|
297
|
+
labels: [...EMPTY_LABELS],
|
|
298
|
+
datasets: [{
|
|
299
|
+
label: coreName,
|
|
300
|
+
data: [...EMPTY_DATA],
|
|
301
|
+
fill: false,
|
|
302
|
+
tension: 0.2,
|
|
303
|
+
pointRadius: 0
|
|
304
|
+
}]
|
|
305
|
+
},
|
|
306
|
+
options: {
|
|
307
|
+
animation: false,
|
|
308
|
+
responsive: false,
|
|
309
|
+
interaction: false,
|
|
310
|
+
events: [],
|
|
311
|
+
plugins: {legend: {display: false}},
|
|
312
|
+
scales: {
|
|
313
|
+
x: {display: false},
|
|
314
|
+
y: {display: false, suggestedMax: 100}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
289
319
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
320
|
+
const cpuAvgChart = makeMainChart(cpuAvgCtx, "CPU Avg");
|
|
321
|
+
const memChart = makeMainChart(memCtx, "Memory %");
|
|
322
|
+
const loadChart = makeMainChart(loadCtx, "CPU Load");
|
|
293
323
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
324
|
+
// ------------------------------------------------------------
|
|
325
|
+
// CORE CHARTS
|
|
326
|
+
// ------------------------------------------------------------
|
|
327
|
+
const coreMini = {};
|
|
298
328
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
329
|
+
function createCoreChart(coreName) {
|
|
330
|
+
const wrapper = document.createElement("div");
|
|
331
|
+
wrapper.className = "core-mini";
|
|
332
|
+
wrapper.innerHTML = `
|
|
303
333
|
<div class="label">${coreName}</div>
|
|
304
334
|
<canvas width="120" height="40"></canvas>
|
|
305
335
|
<div class="value">—</div>
|
|
306
336
|
`;
|
|
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
|
-
}
|
|
337
|
+
wrapper.style.display = showCoresCheckbox.checked ? "block" : "none";
|
|
338
|
+
coresGrid.appendChild(wrapper);
|
|
339
|
+
|
|
340
|
+
const canvas = wrapper.querySelector("canvas");
|
|
341
|
+
const valEl = wrapper.querySelector(".value");
|
|
342
|
+
const chart = makeCoreSparkline(canvas.getContext("2d"), coreName);
|
|
343
|
+
|
|
344
|
+
coreMini[coreName] = {chart, el: wrapper, valEl};
|
|
345
|
+
return coreMini[coreName];
|
|
329
346
|
}
|
|
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
347
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
348
|
+
function getCoreChart(coreName) {
|
|
349
|
+
return coreMini[coreName] || createCoreChart(coreName);
|
|
350
|
+
}
|
|
360
351
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
352
|
+
function pruneOldCores(latest) {
|
|
353
|
+
for (const name of Object.keys(coreMini)) {
|
|
354
|
+
if (!latest.includes(name)) {
|
|
355
|
+
try {
|
|
356
|
+
coreMini[name].chart.destroy();
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
coreMini[name].el.remove();
|
|
360
|
+
delete coreMini[name];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
365
363
|
}
|
|
366
364
|
|
|
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();
|
|
365
|
+
// ------------------------------------------------------------
|
|
366
|
+
// RESET UI
|
|
367
|
+
// ------------------------------------------------------------
|
|
368
|
+
function resetTables() {
|
|
369
|
+
// Clear all table data immediately
|
|
370
|
+
PAG_SERVICES.setData([], []);
|
|
371
|
+
PAG_PROCESSES.setData([], []);
|
|
372
|
+
PAG_DOCKER.setData([], []);
|
|
373
|
+
PAG_DISKS.setData([], []);
|
|
374
|
+
PAG_PYUDISK.setData([], []);
|
|
375
|
+
PAG_CERTS.setData([], []);
|
|
385
376
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
377
|
+
|
|
378
|
+
function resetUI() {
|
|
379
|
+
firstMessage = true;
|
|
380
|
+
hideSpinners();
|
|
381
|
+
const EMPTY_DATA = Array(MAX_POINTS).fill(null);
|
|
382
|
+
const EMPTY_LABELS = Array(MAX_POINTS).fill("");
|
|
383
|
+
|
|
384
|
+
const resetChart = chart => {
|
|
385
|
+
chart.data.labels = [...EMPTY_LABELS];
|
|
386
|
+
chart.data.datasets[0].data = [...EMPTY_DATA];
|
|
387
|
+
chart.update();
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
resetChart(cpuAvgChart);
|
|
391
|
+
resetChart(memChart);
|
|
392
|
+
resetChart(loadChart);
|
|
393
|
+
|
|
394
|
+
for (const name of Object.keys(coreMini)) {
|
|
395
|
+
try {
|
|
396
|
+
coreMini[name].chart.destroy();
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
coreMini[name].el.remove();
|
|
400
|
+
delete coreMini[name];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
systemEl.textContent = "-";
|
|
404
|
+
ipEl.textContent = "—";
|
|
405
|
+
processorEl.textContent = "—";
|
|
406
|
+
memEl.textContent = "—";
|
|
407
|
+
diskEl.textContent = "—";
|
|
408
|
+
loadEl.textContent = "—";
|
|
403
409
|
}
|
|
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;
|
|
410
|
+
|
|
411
|
+
// ------------------------------------------------------------
|
|
412
|
+
// MISC HELPERS
|
|
413
|
+
// ------------------------------------------------------------
|
|
414
|
+
function pushPoint(chart, value) {
|
|
415
|
+
const ts = new Date().toLocaleTimeString();
|
|
416
|
+
chart.data.labels.push(ts);
|
|
417
|
+
chart.data.datasets[0].data.push(isFinite(value) ? Number(value) : NaN);
|
|
418
|
+
if (chart.data.labels.length > MAX_POINTS) {
|
|
419
|
+
chart.data.labels.shift();
|
|
420
|
+
chart.data.datasets[0].data.shift();
|
|
421
|
+
}
|
|
422
|
+
chart.update("none");
|
|
426
423
|
}
|
|
427
424
|
|
|
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
|
-
}
|
|
425
|
+
function num(x) {
|
|
426
|
+
const n = Number(x);
|
|
427
|
+
return Number.isFinite(n) ? n : null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const round2 = x => Number(x).toFixed(2);
|
|
431
|
+
const formatBytes = x => {
|
|
432
|
+
if (x == null) return "—";
|
|
433
|
+
const u = ["B", "KB", "MB", "GB", "TB"];
|
|
434
|
+
let i = 0, n = Number(x);
|
|
435
|
+
while (n > 1024 && i < u.length - 1) {
|
|
436
|
+
n /= 1024;
|
|
437
|
+
i++;
|
|
438
|
+
}
|
|
439
|
+
return n.toFixed(2) + " " + u[i];
|
|
440
|
+
};
|
|
441
|
+
const objectToString = (...vals) => {
|
|
442
|
+
for (const v of vals) {
|
|
443
|
+
if (v && typeof v === "object")
|
|
444
|
+
return Object.entries(v).map(([a, b]) => `${a}: ${b}`).join("<br>");
|
|
445
|
+
if (v != null) return v;
|
|
446
|
+
}
|
|
447
|
+
return "—";
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
function showSpinner(panelOrTableId) {
|
|
451
|
+
const overlay = panelSpinners[panelOrTableId];
|
|
452
|
+
if (overlay) overlay.classList.remove("hidden");
|
|
554
453
|
}
|
|
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);
|
|
454
|
+
|
|
455
|
+
function hideSpinner(panelOrTableId) {
|
|
456
|
+
const overlay = panelSpinners[panelOrTableId];
|
|
457
|
+
if (overlay) overlay.classList.add("hidden");
|
|
606
458
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
459
|
+
|
|
460
|
+
// ------------------------------------------------------------
|
|
461
|
+
// HANDLE METRICS
|
|
462
|
+
// ------------------------------------------------------------
|
|
463
|
+
let firstMessage = true;
|
|
464
|
+
|
|
465
|
+
function handleMetrics(list) {
|
|
466
|
+
if (firstMessage) {
|
|
467
|
+
hideSpinners();
|
|
468
|
+
firstMessage = false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const now = new Date().toLocaleTimeString();
|
|
472
|
+
|
|
473
|
+
for (const host of list) {
|
|
474
|
+
if (host.base_url !== selectedBase) continue;
|
|
475
|
+
const m = host.metrics || {};
|
|
476
|
+
|
|
477
|
+
// ------------ Static fields ------------
|
|
478
|
+
systemEl.textContent =
|
|
479
|
+
`Node: ${m.node || "-"}\n` +
|
|
480
|
+
`OS: ${m.system || "-"}\n` +
|
|
481
|
+
`Architecture: ${m.architecture || "-"}\n\n` +
|
|
482
|
+
`CPU Cores: ${m.cores || "-"}\n` +
|
|
483
|
+
`Up Time: ${m.uptime || "-"}\n`;
|
|
484
|
+
|
|
485
|
+
if (m.ip_info)
|
|
486
|
+
ipEl.textContent =
|
|
487
|
+
`Private: ${m.ip_info.private || "-"}\n\n` +
|
|
488
|
+
`Public: ${m.ip_info.public || "-"}`;
|
|
489
|
+
|
|
490
|
+
processorEl.textContent =
|
|
491
|
+
`CPU: ${m.cpu_name || "-"}\n\n` +
|
|
492
|
+
`GPU: ${m.gpu_name || "-"}`;
|
|
493
|
+
|
|
494
|
+
if (m.disk_info && m.disk_info[0]) {
|
|
495
|
+
const d = m.disk_info[0];
|
|
496
|
+
diskEl.textContent =
|
|
497
|
+
`Total: ${formatBytes(d.total)}\n` +
|
|
498
|
+
`Used: ${formatBytes(d.used)}\n` +
|
|
499
|
+
`Free: ${formatBytes(d.free)}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (m.memory_info) {
|
|
503
|
+
memEl.textContent =
|
|
504
|
+
`Total: ${formatBytes(m.memory_info.total)}\n` +
|
|
505
|
+
`Used: ${formatBytes(m.memory_info.used)}\n` +
|
|
506
|
+
`Percent: ${round2(m.memory_info.percent)}%`;
|
|
507
|
+
pushPoint(memChart, num(m.memory_info.percent));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ------------ CPU ------------
|
|
511
|
+
let avg = null;
|
|
512
|
+
if (m.cpu_usage) {
|
|
513
|
+
const values = m.cpu_usage.map(num);
|
|
514
|
+
avg = values.reduce((a, b) => a + (b || 0), 0) / values.length;
|
|
515
|
+
pruneOldCores(values.map((_, i) => "cpu" + (i + 1)));
|
|
516
|
+
|
|
517
|
+
values.forEach((v, i) => {
|
|
518
|
+
const core = getCoreChart("cpu" + (i + 1));
|
|
519
|
+
|
|
520
|
+
core.chart.data.labels.push(now);
|
|
521
|
+
core.chart.data.datasets[0].data.push(v || 0);
|
|
522
|
+
if (core.chart.data.labels.length > MAX_POINTS) {
|
|
523
|
+
core.chart.data.labels.shift();
|
|
524
|
+
core.chart.data.datasets[0].data.shift();
|
|
525
|
+
}
|
|
526
|
+
core.chart.update({lazy: true});
|
|
527
|
+
core.valEl.textContent = `${(v || 0).toFixed(1)}%`;
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (avg != null) pushPoint(cpuAvgChart, avg);
|
|
531
|
+
|
|
532
|
+
// ------------ LOAD ------------
|
|
533
|
+
if (m.load_averages) {
|
|
534
|
+
const la = m.load_averages;
|
|
535
|
+
loadEl.textContent =
|
|
536
|
+
`${round2(la.m1)} / ${round2(la.m5)} / ${round2(la.m15)}`;
|
|
537
|
+
pushPoint(loadChart, num(la.m1));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ------------ SERVICES (paginated) ------------
|
|
541
|
+
const services = (m.service_stats || m.services || []).filter(s =>
|
|
542
|
+
(s.pname || s.Name || "").toLowerCase().includes(
|
|
543
|
+
svcFilter.value.trim().toLowerCase()
|
|
544
|
+
)
|
|
545
|
+
);
|
|
546
|
+
if (services.length) {
|
|
547
|
+
const columns = ["PID", "Name", "Status", "CPU", "Memory", "Threads", "Open Files"];
|
|
548
|
+
const cleaned = services.map(s => ({
|
|
549
|
+
PID: s.PID ?? s.pid ?? "",
|
|
550
|
+
Name: s.pname ?? s.Name ?? s.name ?? "",
|
|
551
|
+
Status: s.Status ?? s.active ?? s.status ?? s.Active ?? "—",
|
|
552
|
+
CPU: objectToString(s.CPU, s.cpu),
|
|
553
|
+
Memory: objectToString(s.Memory, s.memory),
|
|
554
|
+
Threads: s.Threads ?? s.threads ?? "—",
|
|
555
|
+
"Open Files": s["Open Files"] ?? s.open_files ?? "—"
|
|
556
|
+
}));
|
|
557
|
+
PAG_SERVICES.setData(cleaned, columns);
|
|
558
|
+
hideSpinner("services-table");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ------------ PROCESSES (paginated) ------------
|
|
562
|
+
const processes = (m.process_stats || []).filter(p =>
|
|
563
|
+
(p.Name || "").toLowerCase().includes(
|
|
564
|
+
procFilter.value.trim().toLowerCase()
|
|
565
|
+
)
|
|
566
|
+
);
|
|
567
|
+
if (processes.length) {
|
|
568
|
+
const columns = ["PID", "Name", "Status", "CPU", "Memory", "Uptime", "Threads", "Open Files"];
|
|
569
|
+
PAG_PROCESSES.setData(processes, columns);
|
|
570
|
+
hideSpinner("processes-table");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ------------ DOCKER, DISKS, PYUDISK, CERTIFICATES ------------
|
|
574
|
+
if (m.docker_stats) {
|
|
575
|
+
const cols = Object.keys(m.docker_stats[0] || {});
|
|
576
|
+
PAG_DOCKER.setData(m.docker_stats, cols);
|
|
577
|
+
hideSpinner("docker-table");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (m.disks_info) {
|
|
581
|
+
const cols = Object.keys(m.disks_info[0] || {});
|
|
582
|
+
PAG_DISKS.setData(m.disks_info, cols);
|
|
583
|
+
hideSpinner("disks-table");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (m.pyudisk_stats) {
|
|
587
|
+
const cols = Object.keys(m.pyudisk_stats[0] || {});
|
|
588
|
+
PAG_PYUDISK.setData(m.pyudisk_stats, cols);
|
|
589
|
+
hideSpinner("pyudisk-table");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (m.certificates) {
|
|
593
|
+
const cols = Object.keys(m.certificates[0] || {});
|
|
594
|
+
PAG_CERTS.setData(m.certificates, cols);
|
|
595
|
+
hideSpinner("certificates-table");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ------------------------------------------------------------
|
|
601
|
+
// EVENT BINDINGS
|
|
602
|
+
// ------------------------------------------------------------
|
|
603
|
+
targets.forEach(t => {
|
|
604
|
+
const opt = document.createElement("option");
|
|
605
|
+
opt.value = t.base_url;
|
|
606
|
+
opt.textContent = t.name || t.base_url;
|
|
607
|
+
nodeSelect.appendChild(opt);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
let selectedBase = nodeSelect.value || (targets[0] && targets[0].base_url);
|
|
611
|
+
nodeSelect.value = selectedBase;
|
|
612
|
+
|
|
613
|
+
nodeSelect.addEventListener("change", () => {
|
|
614
|
+
selectedBase = nodeSelect.value;
|
|
615
|
+
resetUI();
|
|
616
|
+
resetTables();
|
|
617
|
+
showAllSpinners();
|
|
618
|
+
ws.send(JSON.stringify({type: "select_target", base_url: selectedBase}));
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
svcGetAll.addEventListener("change", () => {
|
|
622
|
+
ws.send(JSON.stringify({type: "update_flags", all_services: svcGetAll.checked}));
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
refreshBtn.addEventListener("click", resetUI);
|
|
626
|
+
|
|
627
|
+
showCoresCheckbox.addEventListener("change", () => {
|
|
628
|
+
const visible = showCoresCheckbox.checked;
|
|
629
|
+
Object.values(coreMini).forEach(c => c.el.style.display = visible ? "block" : "none");
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// ------------------------------------------------------------
|
|
633
|
+
// WEBSOCKET
|
|
634
|
+
// ------------------------------------------------------------
|
|
635
|
+
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
636
|
+
const ws = new WebSocket(`${protocol}://${location.host}/ws`);
|
|
637
|
+
|
|
638
|
+
ws.onopen = () => {
|
|
639
|
+
ws.send(JSON.stringify({type: "select_target", base_url: selectedBase}));
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
ws.onmessage = evt => {
|
|
643
|
+
try {
|
|
644
|
+
const msg = JSON.parse(evt.data);
|
|
645
|
+
if (msg.type === "metrics") handleMetrics(msg.data);
|
|
646
|
+
if (msg.type === "error") alert(msg.message);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error("WS parse error:", err);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// ------------------------------------------------------------
|
|
653
|
+
// INIT
|
|
654
|
+
// ------------------------------------------------------------
|
|
655
|
+
attachSpinners();
|
|
656
|
+
resetUI(); // reset UI, keep spinners visible
|
|
657
|
+
showAllSpinners(); // show spinners until first metrics arrive
|
|
615
658
|
})();
|