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.
@@ -1,615 +1,658 @@
1
1
  // app/static/app.js
2
2
  (function () {
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
- });
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
- // 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
- }
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
- function showAllSpinners() {
37
- Object.keys(panelSpinners).forEach(id => showSpinner(id));
38
- }
36
+ function showAllSpinners() {
37
+ Object.keys(panelSpinners).forEach(id => showSpinner(id));
38
+ }
39
39
 
40
- function hideSpinners() {
41
- document.querySelectorAll(".loading-overlay").forEach(x => {
42
- x.classList.add("hidden");
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
- const pagination = document.createElement("div");
108
- pagination.className = "pagination";
109
- tableEl.insertAdjacentElement("afterend", pagination);
46
+ // ------------------------------------------------------------
47
+ // DOM REFERENCES
48
+ // ------------------------------------------------------------
49
+ const nodeSelect = document.getElementById("node-select");
50
+ const refreshBtn = document.getElementById("refresh-btn");
110
51
 
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));
52
+ const systemEl = document.getElementById("system");
53
+ const ipEl = document.getElementById("ip-info");
54
+ const processorEl = document.getElementById("processor");
115
55
 
116
- const start = (state.page - 1) * state.pageSize;
117
- const chunk = rows.slice(start, start + state.pageSize);
56
+ const memEl = document.getElementById("memory");
57
+ const diskEl = document.getElementById("disk");
58
+ const loadEl = document.getElementById("cpuload");
118
59
 
119
- info.textContent =
120
- `Showing ${start + 1} to ${Math.min(start + state.pageSize, rows.length)} of ${rows.length} entries`;
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
- bodyEl.innerHTML = "";
123
- chunk.forEach(r => bodyEl.insertAdjacentHTML("beforeend", r));
64
+ const coresGrid = document.getElementById("cores-grid");
124
65
 
125
- renderPagination(pages);
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
- 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
- };
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
- // --- Previous ---
145
- makeBtn("Previous", () => { state.page--; render(); }, false, state.page === 1);
101
+ const state = {
102
+ data: [],
103
+ page: 1,
104
+ pageSize
105
+ };
146
106
 
147
- const maxVisible = 5;
107
+ const pagination = document.createElement("div");
108
+ pagination.className = "pagination";
109
+ tableEl.insertAdjacentElement("afterend", pagination);
148
110
 
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);
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
- } 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(); });
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
- // --- Next ---
186
- makeBtn("Next", () => { state.page++; render(); }, false, state.page === pages);
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
- function addEllipsis() {
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
- 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();
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
- 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
-
230
- // ------------------------------------------------------------
231
- // CHART HELPERS
232
- // ------------------------------------------------------------
233
- function makeMainChart(ctx, label) {
234
- const EMPTY = Array(MAX_POINTS).fill(null);
235
- const LABELS = Array(MAX_POINTS).fill("");
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
- const cpuAvgChart = makeMainChart(cpuAvgCtx, "CPU Avg");
291
- const memChart = makeMainChart(memCtx, "Memory %");
292
- const loadChart = makeMainChart(loadCtx, "CPU Load");
320
+ const cpuAvgChart = makeMainChart(cpuAvgCtx, "CPU Avg");
321
+ const memChart = makeMainChart(memCtx, "Memory %");
322
+ const loadChart = makeMainChart(loadCtx, "CPU Load");
293
323
 
294
- // ------------------------------------------------------------
295
- // CORE CHARTS
296
- // ------------------------------------------------------------
297
- const coreMini = {};
324
+ // ------------------------------------------------------------
325
+ // CORE CHARTS
326
+ // ------------------------------------------------------------
327
+ const coreMini = {};
298
328
 
299
- function createCoreChart(coreName) {
300
- const wrapper = document.createElement("div");
301
- wrapper.className = "core-mini";
302
- wrapper.innerHTML = `
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
- wrapper.style.display = showCoresCheckbox.checked ? "block" : "none";
308
- coresGrid.appendChild(wrapper);
309
-
310
- const canvas = wrapper.querySelector("canvas");
311
- const valEl = wrapper.querySelector(".value");
312
- const chart = makeCoreSparkline(canvas.getContext("2d"), coreName);
313
-
314
- coreMini[coreName] = { chart, el: wrapper, valEl };
315
- return coreMini[coreName];
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
- resetChart(cpuAvgChart);
358
- resetChart(memChart);
359
- resetChart(loadChart);
348
+ function getCoreChart(coreName) {
349
+ return coreMini[coreName] || createCoreChart(coreName);
350
+ }
360
351
 
361
- for (const name of Object.keys(coreMini)) {
362
- try { coreMini[name].chart.destroy(); } catch {}
363
- coreMini[name].el.remove();
364
- delete coreMini[name];
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
- systemEl.textContent = "-";
368
- ipEl.textContent = "—";
369
- processorEl.textContent = "—";
370
- memEl.textContent = "—";
371
- diskEl.textContent = "—";
372
- loadEl.textContent = "—";
373
- }
374
-
375
- // ------------------------------------------------------------
376
- // MISC HELPERS
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
- chart.update("none");
387
- }
388
-
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 => {
392
- if (x == null) return "—";
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;
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
- return "—";
405
- };
406
-
407
- function showSpinner(panelOrTableId) {
408
- const overlay = panelSpinners[panelOrTableId];
409
- if (overlay) overlay.classList.remove("hidden");
410
- }
411
-
412
- function hideSpinner(panelOrTableId) {
413
- const overlay = panelSpinners[panelOrTableId];
414
- if (overlay) overlay.classList.add("hidden");
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
- const now = new Date().toLocaleTimeString();
429
-
430
- for (const host of list) {
431
- if (host.base_url !== selectedBase) continue;
432
- const m = host.metrics || {};
433
-
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)}`;
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
- // EVENT BINDINGS
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
- // INIT
611
- // ------------------------------------------------------------
612
- attachSpinners();
613
- resetUI(); // reset UI, keep spinners visible
614
- showAllSpinners(); // show spinners until first metrics arrive
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
  })();