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

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