PyObservability 0.1.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

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