PyObservability 1.4.0__py3-none-any.whl → 2.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.
- pyobservability/config/enums.py +1 -0
- pyobservability/config/settings.py +5 -0
- pyobservability/kuma.py +126 -0
- pyobservability/main.py +59 -16
- pyobservability/static/app.js +398 -28
- pyobservability/static/styles.css +108 -0
- pyobservability/templates/index.html +83 -1
- pyobservability/version.py +1 -1
- {pyobservability-1.4.0.dist-info → pyobservability-2.0.0.dist-info}/METADATA +13 -3
- pyobservability-2.0.0.dist-info/RECORD +17 -0
- {pyobservability-1.4.0.dist-info → pyobservability-2.0.0.dist-info}/WHEEL +1 -1
- pyobservability-1.4.0.dist-info/RECORD +0 -16
- {pyobservability-1.4.0.dist-info → pyobservability-2.0.0.dist-info}/entry_points.txt +0 -0
- {pyobservability-1.4.0.dist-info → pyobservability-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {pyobservability-1.4.0.dist-info → pyobservability-2.0.0.dist-info}/top_level.txt +0 -0
pyobservability/config/enums.py
CHANGED
|
@@ -132,6 +132,11 @@ class EnvConfig(PydanticEnvConfig):
|
|
|
132
132
|
password: str | None = Field(None, validation_alias=alias_choices("PASSWORD"))
|
|
133
133
|
timeout: PositiveInt = Field(300, validation_alias=alias_choices("TIMEOUT"))
|
|
134
134
|
|
|
135
|
+
kuma_url: str | None = None
|
|
136
|
+
kuma_username: str | None = None
|
|
137
|
+
kuma_password: str | None = None
|
|
138
|
+
kuma_timeout: PositiveInt = 5
|
|
139
|
+
|
|
135
140
|
class Config:
|
|
136
141
|
"""Environment variables configuration."""
|
|
137
142
|
|
pyobservability/kuma.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
import socketio
|
|
8
|
+
|
|
9
|
+
from pyobservability.config import settings
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger("uvicorn.default")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UptimeKumaClient:
|
|
15
|
+
"""Client to interact with Uptime Kuma server via Socket.IO.
|
|
16
|
+
|
|
17
|
+
>>> UptimeKumaClient
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize the Uptime Kuma client."""
|
|
23
|
+
self.sio = socketio.Client()
|
|
24
|
+
self.monitors = {}
|
|
25
|
+
self.logged_in = False
|
|
26
|
+
|
|
27
|
+
self.sio.on("monitorList", self._on_monitor_list)
|
|
28
|
+
|
|
29
|
+
def _on_monitor_list(self, data):
|
|
30
|
+
"""Handle incoming monitor list from Uptime Kuma server."""
|
|
31
|
+
LOGGER.debug("Received monitor list from Uptime Kuma server.")
|
|
32
|
+
self.monitors = data
|
|
33
|
+
|
|
34
|
+
def connect(self):
|
|
35
|
+
"""Connect to the Uptime Kuma server via Socket.IO."""
|
|
36
|
+
LOGGER.debug("Connecting to Uptime Kuma server at %s", settings.env.kuma_url)
|
|
37
|
+
self.sio.connect(settings.env.kuma_url)
|
|
38
|
+
|
|
39
|
+
def login(self):
|
|
40
|
+
"""Log in to the Uptime Kuma server."""
|
|
41
|
+
result = {"ok": False}
|
|
42
|
+
|
|
43
|
+
def callback(resp):
|
|
44
|
+
"""Callback to handle login response."""
|
|
45
|
+
result.update(resp or {"ok": False})
|
|
46
|
+
|
|
47
|
+
self.sio.emit(
|
|
48
|
+
"login",
|
|
49
|
+
{
|
|
50
|
+
"username": settings.env.kuma_username,
|
|
51
|
+
"password": settings.env.kuma_password,
|
|
52
|
+
"token": "",
|
|
53
|
+
},
|
|
54
|
+
callback=callback,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
end = time.time() + settings.env.kuma_timeout
|
|
58
|
+
while not result.get("ok") and time.time() < end:
|
|
59
|
+
time.sleep(0.05)
|
|
60
|
+
|
|
61
|
+
if not result.get("ok"):
|
|
62
|
+
raise RuntimeError("Uptime Kuma login failed")
|
|
63
|
+
|
|
64
|
+
self.logged_in = True
|
|
65
|
+
|
|
66
|
+
def get_monitors(self):
|
|
67
|
+
"""Retrieve monitors from the Uptime Kuma server."""
|
|
68
|
+
if not self.sio.connected:
|
|
69
|
+
self.connect()
|
|
70
|
+
|
|
71
|
+
if not self.logged_in:
|
|
72
|
+
self.login()
|
|
73
|
+
|
|
74
|
+
end = time.time() + settings.env.kuma_timeout
|
|
75
|
+
while not self.monitors and time.time() < end:
|
|
76
|
+
time.sleep(0.05)
|
|
77
|
+
|
|
78
|
+
if not self.monitors:
|
|
79
|
+
raise RuntimeError("No monitors received")
|
|
80
|
+
|
|
81
|
+
return self.monitors
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def extract_monitors(payload: Dict[int, Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
85
|
+
"""Convert raw API payload into a list of dicts with name, url, tag_names, host.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
payload: Raw payload from Uptime Kuma server.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List[Dict[str, Any]]:
|
|
92
|
+
List of monitors with relevant fields.
|
|
93
|
+
"""
|
|
94
|
+
monitors = []
|
|
95
|
+
|
|
96
|
+
grouped = {}
|
|
97
|
+
for monitor in payload.values():
|
|
98
|
+
if children_ids := monitor.get("childrenIDs"):
|
|
99
|
+
for child in children_ids:
|
|
100
|
+
grouped[child] = monitor.get("name")
|
|
101
|
+
|
|
102
|
+
for monitor in payload.values():
|
|
103
|
+
url = monitor.get("url").replace("host.docker.internal", urlparse(settings.env.kuma_url).hostname)
|
|
104
|
+
host = urlparse(url).hostname if url else None
|
|
105
|
+
if not host:
|
|
106
|
+
continue
|
|
107
|
+
monitors.append(
|
|
108
|
+
{
|
|
109
|
+
"name": monitor.get("name"),
|
|
110
|
+
"parent": grouped.get(monitor.get("id")),
|
|
111
|
+
"url": url,
|
|
112
|
+
"host": host,
|
|
113
|
+
"tag_names": [tag.get("name") for tag in monitor.get("tags", []) if "name" in tag],
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
return monitors
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def group_by_host(monitors: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
120
|
+
"""Group monitors by host."""
|
|
121
|
+
grouped = defaultdict(list)
|
|
122
|
+
|
|
123
|
+
for monitor in monitors:
|
|
124
|
+
grouped[monitor["host"]].append(monitor)
|
|
125
|
+
|
|
126
|
+
return dict(grouped)
|
pyobservability/main.py
CHANGED
|
@@ -2,16 +2,18 @@ import logging
|
|
|
2
2
|
import pathlib
|
|
3
3
|
import warnings
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
from typing import Dict
|
|
6
7
|
|
|
7
8
|
import uiauth
|
|
8
9
|
import uvicorn
|
|
9
|
-
from fastapi import FastAPI, Request
|
|
10
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
10
11
|
from fastapi.routing import APIRoute, APIWebSocketRoute
|
|
11
12
|
from fastapi.staticfiles import StaticFiles
|
|
12
13
|
from fastapi.templating import Jinja2Templates
|
|
13
14
|
|
|
14
15
|
from pyobservability.config import enums, settings
|
|
16
|
+
from pyobservability.kuma import UptimeKumaClient, extract_monitors, group_by_host
|
|
15
17
|
from pyobservability.transport import websocket_endpoint
|
|
16
18
|
from pyobservability.version import __version__
|
|
17
19
|
|
|
@@ -39,12 +41,33 @@ async def index(request: Request):
|
|
|
39
41
|
TemplateResponse:
|
|
40
42
|
Rendered HTML template with targets and version.
|
|
41
43
|
"""
|
|
42
|
-
|
|
44
|
+
kuma_data = {} if all((settings.env.kuma_url, settings.env.kuma_username, settings.env.kuma_password)) else None
|
|
45
|
+
args = dict(request=request, service_map=kuma_data, targets=settings.env.targets, version=__version__)
|
|
43
46
|
if settings.env.username and settings.env.password:
|
|
44
47
|
args["logout"] = uiauth.enums.APIEndpoints.fastapi_logout.value
|
|
45
48
|
return templates.TemplateResponse("index.html", args)
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
async def kuma():
|
|
52
|
+
"""Kuma endpoint to retrieve monitors from Kuma server.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dict[str, Any]:
|
|
56
|
+
Grouped monitors by host from Kuma server.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
kuma_data = UptimeKumaClient().get_monitors()
|
|
60
|
+
LOGGER.info("Retrieved payload from kuma server.")
|
|
61
|
+
except RuntimeError:
|
|
62
|
+
raise HTTPException(
|
|
63
|
+
status_code=HTTPStatus.SERVICE_UNAVAILABLE.real,
|
|
64
|
+
detail="Unable to retrieve data from kuma server.",
|
|
65
|
+
)
|
|
66
|
+
json_monitors = await extract_monitors(kuma_data)
|
|
67
|
+
LOGGER.info("Extracted JSON monitors from kuma payload.")
|
|
68
|
+
return await group_by_host(json_monitors)
|
|
69
|
+
|
|
70
|
+
|
|
48
71
|
async def health() -> Dict[str, str]:
|
|
49
72
|
"""Health check endpoint.
|
|
50
73
|
|
|
@@ -65,25 +88,35 @@ def include_routes() -> None:
|
|
|
65
88
|
include_in_schema=False,
|
|
66
89
|
),
|
|
67
90
|
)
|
|
91
|
+
kuma_enabled = all((settings.env.kuma_url, settings.env.kuma_username, settings.env.kuma_password))
|
|
68
92
|
if all((settings.env.username, settings.env.password)):
|
|
93
|
+
auth_endpoints = [
|
|
94
|
+
uiauth.Parameters(
|
|
95
|
+
path=enums.APIEndpoints.root,
|
|
96
|
+
function=index,
|
|
97
|
+
methods=[uiauth.enums.APIMethods.GET],
|
|
98
|
+
),
|
|
99
|
+
uiauth.Parameters(
|
|
100
|
+
path=enums.APIEndpoints.ws,
|
|
101
|
+
function=websocket_endpoint,
|
|
102
|
+
route=APIWebSocketRoute,
|
|
103
|
+
),
|
|
104
|
+
]
|
|
105
|
+
if kuma_enabled:
|
|
106
|
+
auth_endpoints.append(
|
|
107
|
+
uiauth.Parameters(
|
|
108
|
+
path=enums.APIEndpoints.kuma,
|
|
109
|
+
function=kuma,
|
|
110
|
+
methods=[uiauth.enums.APIMethods.GET],
|
|
111
|
+
)
|
|
112
|
+
)
|
|
69
113
|
uiauth.protect(
|
|
70
114
|
app=PyObservability,
|
|
71
115
|
username=settings.env.username,
|
|
72
116
|
password=settings.env.password,
|
|
73
117
|
timeout=settings.env.timeout,
|
|
74
118
|
custom_logger=LOGGER,
|
|
75
|
-
params=
|
|
76
|
-
uiauth.Parameters(
|
|
77
|
-
path=enums.APIEndpoints.root,
|
|
78
|
-
function=index,
|
|
79
|
-
methods=[uiauth.enums.APIMethods.GET],
|
|
80
|
-
),
|
|
81
|
-
uiauth.Parameters(
|
|
82
|
-
path=enums.APIEndpoints.ws,
|
|
83
|
-
function=websocket_endpoint,
|
|
84
|
-
route=APIWebSocketRoute,
|
|
85
|
-
),
|
|
86
|
-
],
|
|
119
|
+
params=auth_endpoints,
|
|
87
120
|
)
|
|
88
121
|
else:
|
|
89
122
|
warnings.warn("\n\tRunning PyObservability without any protection.", UserWarning)
|
|
@@ -99,10 +132,20 @@ def include_routes() -> None:
|
|
|
99
132
|
APIWebSocketRoute(
|
|
100
133
|
path=enums.APIEndpoints.ws,
|
|
101
134
|
endpoint=websocket_endpoint,
|
|
102
|
-
)
|
|
135
|
+
),
|
|
103
136
|
)
|
|
137
|
+
if kuma_enabled:
|
|
138
|
+
PyObservability.routes.append(
|
|
139
|
+
APIRoute(
|
|
140
|
+
path=enums.APIEndpoints.kuma,
|
|
141
|
+
endpoint=kuma,
|
|
142
|
+
methods=["GET"],
|
|
143
|
+
include_in_schema=False,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
104
146
|
|
|
105
147
|
|
|
148
|
+
# noinspection PyTypeChecker
|
|
106
149
|
def start(**kwargs) -> None:
|
|
107
150
|
"""Start the FastAPI app with Uvicorn server."""
|
|
108
151
|
settings.env = settings.env_loader(**kwargs)
|
pyobservability/static/app.js
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
// ------------------------------------------------------------
|
|
6
6
|
const MAX_POINTS = 60;
|
|
7
7
|
const targets = window.MONITOR_TARGETS || [];
|
|
8
|
-
const DEFAULT_PAGE_SIZE =
|
|
8
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
9
9
|
const panelSpinners = {};
|
|
10
10
|
|
|
11
|
+
// Tab management
|
|
12
|
+
let currentTab = 'nodes';
|
|
13
|
+
let ws = null;
|
|
14
|
+
let kumaMapData = null;
|
|
15
|
+
let kumaMapLoaded = false;
|
|
16
|
+
|
|
11
17
|
// ------------------------------------------------------------
|
|
12
18
|
// VISUAL SPINNERS
|
|
13
19
|
// ------------------------------------------------------------
|
|
@@ -88,6 +94,10 @@
|
|
|
88
94
|
const certsTableHead = certsTable.querySelector("thead");
|
|
89
95
|
const certsTableBody = certsTable.querySelector("tbody");
|
|
90
96
|
|
|
97
|
+
const endpointsTable = document.getElementById("endpoints-table");
|
|
98
|
+
const endpointsTableHead = endpointsTable.querySelector("thead");
|
|
99
|
+
const endpointsTableBody = endpointsTable.querySelector("tbody");
|
|
100
|
+
|
|
91
101
|
const showCoresCheckbox = document.getElementById("show-cores");
|
|
92
102
|
|
|
93
103
|
// ------------------------------------------------------------
|
|
@@ -117,11 +127,26 @@
|
|
|
117
127
|
const chunk = rows.slice(start, start + state.pageSize);
|
|
118
128
|
|
|
119
129
|
info.textContent =
|
|
120
|
-
`Showing ${start + 1} to ${Math.min(start + state.pageSize, rows.length)} of ${rows.length} entries`;
|
|
130
|
+
`Showing ${rows.length ? start + 1 : 0} to ${rows.length ? Math.min(start + state.pageSize, rows.length) : 0} of ${rows.length} entries`;
|
|
121
131
|
|
|
122
132
|
bodyEl.innerHTML = "";
|
|
123
133
|
chunk.forEach(r => bodyEl.insertAdjacentHTML("beforeend", r));
|
|
124
134
|
|
|
135
|
+
const fillerCount = Math.max(0, state.pageSize - chunk.length);
|
|
136
|
+
const shouldPad = state.page > 1 && fillerCount > 0;
|
|
137
|
+
if (shouldPad) {
|
|
138
|
+
const colCount = state.columns?.length || headEl.querySelectorAll("th").length || 1;
|
|
139
|
+
for (let i = 0; i < fillerCount; i++) {
|
|
140
|
+
const fillerRow = document.createElement("tr");
|
|
141
|
+
fillerRow.className = "placeholder-row";
|
|
142
|
+
for (let c = 0; c < colCount; c++) {
|
|
143
|
+
const cell = document.createElement("td");
|
|
144
|
+
cell.innerHTML = " ";
|
|
145
|
+
fillerRow.appendChild(cell);
|
|
146
|
+
}
|
|
147
|
+
bodyEl.appendChild(fillerRow);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
125
150
|
renderPagination(pages);
|
|
126
151
|
}
|
|
127
152
|
|
|
@@ -305,6 +330,11 @@
|
|
|
305
330
|
const PAG_CERTS = createPaginatedTable(
|
|
306
331
|
certsTable, certsTableHead, certsTableBody
|
|
307
332
|
);
|
|
333
|
+
const PAG_ENDPOINTS = createPaginatedTable(
|
|
334
|
+
endpointsTable,
|
|
335
|
+
endpointsTableHead,
|
|
336
|
+
endpointsTableBody
|
|
337
|
+
);
|
|
308
338
|
|
|
309
339
|
// ------------------------------------------------------------
|
|
310
340
|
// CHART HELPERS
|
|
@@ -336,6 +366,32 @@
|
|
|
336
366
|
});
|
|
337
367
|
}
|
|
338
368
|
|
|
369
|
+
function normalizeServiceMap(serviceMap) {
|
|
370
|
+
const rows = [];
|
|
371
|
+
Object.entries(serviceMap || {}).forEach(([host, services]) => {
|
|
372
|
+
services.forEach(svc => {
|
|
373
|
+
rows.push({
|
|
374
|
+
Node: host,
|
|
375
|
+
Name: svc.name || "",
|
|
376
|
+
Parent: svc.parent || "—",
|
|
377
|
+
Tags: (svc.tag_names || []).join(", "),
|
|
378
|
+
URL: `<a href="${svc.url}" target="_blank">${svc.url}</a>`
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
return rows;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderEndpoints() {
|
|
386
|
+
if (!window.SERVICE_MAP) return;
|
|
387
|
+
|
|
388
|
+
const rows = normalizeServiceMap(window.SERVICE_MAP);
|
|
389
|
+
const columns = ["Node", "Name", "Parent", "Tags", "URL"];
|
|
390
|
+
|
|
391
|
+
PAG_ENDPOINTS.setData(rows, columns);
|
|
392
|
+
hideSpinner("endpoints-table");
|
|
393
|
+
}
|
|
394
|
+
|
|
339
395
|
function makeCoreSparkline(ctx, coreName) {
|
|
340
396
|
const EMPTY_LABELS = Array(MAX_POINTS).fill("");
|
|
341
397
|
const EMPTY_DATA = Array(MAX_POINTS).fill(null);
|
|
@@ -377,8 +433,62 @@
|
|
|
377
433
|
const unifiedCpuCtx = document.getElementById("unified-cpu-chart").getContext("2d");
|
|
378
434
|
const unifiedDiskCtx = document.getElementById("unified-disk-chart").getContext("2d");
|
|
379
435
|
|
|
436
|
+
// Unified tables DOM references
|
|
437
|
+
const unifiedServicesTable = document.getElementById("unified-services-table");
|
|
438
|
+
const unifiedServicesHead = unifiedServicesTable?.querySelector("thead");
|
|
439
|
+
const unifiedServicesBody = unifiedServicesTable?.querySelector("tbody");
|
|
440
|
+
|
|
441
|
+
const unifiedProcessesTable = document.getElementById("unified-processes-table");
|
|
442
|
+
const unifiedProcessesHead = unifiedProcessesTable?.querySelector("thead");
|
|
443
|
+
const unifiedProcessesBody = unifiedProcessesTable?.querySelector("tbody");
|
|
444
|
+
|
|
445
|
+
const unifiedDockerTable = document.getElementById("unified-docker-table");
|
|
446
|
+
const unifiedDockerHead = unifiedDockerTable?.querySelector("thead");
|
|
447
|
+
const unifiedDockerBody = unifiedDockerTable?.querySelector("tbody");
|
|
448
|
+
|
|
449
|
+
const unifiedDisksTable = document.getElementById("unified-disks-table");
|
|
450
|
+
const unifiedDisksHead = unifiedDisksTable?.querySelector("thead");
|
|
451
|
+
const unifiedDisksBody = unifiedDisksTable?.querySelector("tbody");
|
|
452
|
+
|
|
453
|
+
const unifiedPyudiskTable = document.getElementById("unified-pyudisk-table");
|
|
454
|
+
const unifiedPyudiskHead = unifiedPyudiskTable?.querySelector("thead");
|
|
455
|
+
const unifiedPyudiskBody = unifiedPyudiskTable?.querySelector("tbody");
|
|
456
|
+
|
|
457
|
+
const unifiedCertsTable = document.getElementById("unified-certificates-table");
|
|
458
|
+
const unifiedCertsHead = unifiedCertsTable?.querySelector("thead");
|
|
459
|
+
const unifiedCertsBody = unifiedCertsTable?.querySelector("tbody");
|
|
460
|
+
|
|
461
|
+
// Paginated unified tables
|
|
462
|
+
const PAG_UNIFIED_SERVICES = unifiedServicesTable && createPaginatedTable(
|
|
463
|
+
unifiedServicesTable, unifiedServicesHead, unifiedServicesBody
|
|
464
|
+
);
|
|
465
|
+
const PAG_UNIFIED_PROCESSES = unifiedProcessesTable && createPaginatedTable(
|
|
466
|
+
unifiedProcessesTable, unifiedProcessesHead, unifiedProcessesBody
|
|
467
|
+
);
|
|
468
|
+
const PAG_UNIFIED_DOCKER = unifiedDockerTable && createPaginatedTable(
|
|
469
|
+
unifiedDockerTable, unifiedDockerHead, unifiedDockerBody
|
|
470
|
+
);
|
|
471
|
+
const PAG_UNIFIED_DISKS = unifiedDisksTable && createPaginatedTable(
|
|
472
|
+
unifiedDisksTable, unifiedDisksHead, unifiedDisksBody
|
|
473
|
+
);
|
|
474
|
+
const PAG_UNIFIED_PYUDISK = unifiedPyudiskTable && createPaginatedTable(
|
|
475
|
+
unifiedPyudiskTable, unifiedPyudiskHead, unifiedPyudiskBody
|
|
476
|
+
);
|
|
477
|
+
const PAG_UNIFIED_CERTS = unifiedCertsTable && createPaginatedTable(
|
|
478
|
+
unifiedCertsTable, unifiedCertsHead, unifiedCertsBody
|
|
479
|
+
);
|
|
480
|
+
|
|
380
481
|
let unifiedNodes = [];
|
|
381
|
-
const colorPalette = [
|
|
482
|
+
const colorPalette = [
|
|
483
|
+
"#ff0000",
|
|
484
|
+
"#ffff00",
|
|
485
|
+
"#00ff00",
|
|
486
|
+
"#0066ff",
|
|
487
|
+
"#b300ff",
|
|
488
|
+
"#ff7f00",
|
|
489
|
+
"#8b4513",
|
|
490
|
+
"#7f7f7f"
|
|
491
|
+
];
|
|
382
492
|
const nodeColor = {};
|
|
383
493
|
const unifiedCharts = {memory: null, cpu: null, disk: null};
|
|
384
494
|
|
|
@@ -511,6 +621,147 @@
|
|
|
511
621
|
|
|
512
622
|
chart.update("none");
|
|
513
623
|
});
|
|
624
|
+
|
|
625
|
+
// --- Unified tables aggregation ---
|
|
626
|
+
// Helper to get display name for node
|
|
627
|
+
const getNodeLabel = (host) => host.name || host.base_url || "";
|
|
628
|
+
|
|
629
|
+
// Services
|
|
630
|
+
if (PAG_UNIFIED_SERVICES) {
|
|
631
|
+
const svcRows = [];
|
|
632
|
+
metrics.forEach(host => {
|
|
633
|
+
if (!host.metrics) return;
|
|
634
|
+
const m = host.metrics;
|
|
635
|
+
const label = getNodeLabel(host);
|
|
636
|
+
const services = (m.service_stats || m.services || []).filter(s =>
|
|
637
|
+
(s.pname || s.Name || "").toLowerCase().includes(
|
|
638
|
+
svcFilter.value.trim().toLowerCase()
|
|
639
|
+
)
|
|
640
|
+
);
|
|
641
|
+
services.forEach(s => {
|
|
642
|
+
svcRows.push({
|
|
643
|
+
Node: label,
|
|
644
|
+
PID: s.PID ?? s.pid ?? "",
|
|
645
|
+
Name: s.pname ?? s.Name ?? s.name ?? "",
|
|
646
|
+
Status: s.Status ?? s.active ?? s.status ?? s.Active ?? "4",
|
|
647
|
+
CPU: objectToString(s.CPU, s.cpu),
|
|
648
|
+
Memory: objectToString(s.Memory, s.memory),
|
|
649
|
+
Threads: s.Threads ?? s.threads ?? "4",
|
|
650
|
+
"Open Files": s["Open Files"] ?? s.open_files ?? "4"
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
const svcCols = ["Node", "PID", "Name", "Status", "CPU", "Memory", "Threads", "Open Files"];
|
|
655
|
+
PAG_UNIFIED_SERVICES.setData(svcRows, svcCols);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Processes
|
|
659
|
+
if (PAG_UNIFIED_PROCESSES) {
|
|
660
|
+
const procRows = [];
|
|
661
|
+
const procColsSet = new Set(["Node", "PID", "Name", "Status", "CPU", "Memory", "Uptime", "Threads", "Open Files"]);
|
|
662
|
+
metrics.forEach(host => {
|
|
663
|
+
if (!host.metrics) return;
|
|
664
|
+
const m = host.metrics;
|
|
665
|
+
const label = getNodeLabel(host);
|
|
666
|
+
const processes = (m.process_stats || []).filter(p =>
|
|
667
|
+
(p.Name || "").toLowerCase().includes(
|
|
668
|
+
procFilter.value.trim().toLowerCase()
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
processes.forEach(p => {
|
|
672
|
+
const row = {Node: label};
|
|
673
|
+
Object.entries(p).forEach(([k, v]) => {
|
|
674
|
+
procColsSet.add(k);
|
|
675
|
+
row[k] = v;
|
|
676
|
+
});
|
|
677
|
+
procRows.push(row);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
const procCols = Array.from(procColsSet);
|
|
681
|
+
PAG_UNIFIED_PROCESSES.setData(procRows, procCols);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Docker
|
|
685
|
+
if (PAG_UNIFIED_DOCKER) {
|
|
686
|
+
const dockerRows = [];
|
|
687
|
+
const dockerColsSet = new Set(["Node"]);
|
|
688
|
+
metrics.forEach(host => {
|
|
689
|
+
if (!host.metrics || !Array.isArray(host.metrics.docker_stats)) return;
|
|
690
|
+
const label = getNodeLabel(host);
|
|
691
|
+
host.metrics.docker_stats.forEach(s => {
|
|
692
|
+
const row = {Node: label};
|
|
693
|
+
Object.entries(s).forEach(([k, v]) => {
|
|
694
|
+
dockerColsSet.add(k);
|
|
695
|
+
row[k] = v;
|
|
696
|
+
});
|
|
697
|
+
dockerRows.push(row);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
const dockerCols = Array.from(dockerColsSet);
|
|
701
|
+
PAG_UNIFIED_DOCKER.setData(dockerRows, dockerCols);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Disks
|
|
705
|
+
if (PAG_UNIFIED_DISKS) {
|
|
706
|
+
const diskRows = [];
|
|
707
|
+
const diskColsSet = new Set(["Node"]);
|
|
708
|
+
metrics.forEach(host => {
|
|
709
|
+
if (!host.metrics || !Array.isArray(host.metrics.disks_info)) return;
|
|
710
|
+
const label = getNodeLabel(host);
|
|
711
|
+
host.metrics.disks_info.forEach(d => {
|
|
712
|
+
const row = {Node: label};
|
|
713
|
+
Object.entries(d).forEach(([k, v]) => {
|
|
714
|
+
if (k === "Node") return;
|
|
715
|
+
diskColsSet.add(k);
|
|
716
|
+
row[k] = v;
|
|
717
|
+
});
|
|
718
|
+
diskRows.push(row);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
const diskCols = Array.from(diskColsSet);
|
|
722
|
+
PAG_UNIFIED_DISKS.setData(diskRows, diskCols);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// PyUdisk
|
|
726
|
+
if (PAG_UNIFIED_PYUDISK) {
|
|
727
|
+
const pyuRows = [];
|
|
728
|
+
const pyuColsSet = new Set(["Node"]);
|
|
729
|
+
metrics.forEach(host => {
|
|
730
|
+
if (!host.metrics || !Array.isArray(host.metrics.pyudisk_stats)) return;
|
|
731
|
+
const label = getNodeLabel(host);
|
|
732
|
+
host.metrics.pyudisk_stats.forEach(pyu => {
|
|
733
|
+
const row = {Node: label};
|
|
734
|
+
Object.entries(pyu).forEach(([k, v]) => {
|
|
735
|
+
if (k === "Mountpoint") return;
|
|
736
|
+
pyuColsSet.add(k);
|
|
737
|
+
row[k] = v;
|
|
738
|
+
});
|
|
739
|
+
pyuRows.push(row);
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
const pyuCols = Array.from(pyuColsSet);
|
|
743
|
+
PAG_UNIFIED_PYUDISK.setData(pyuRows, pyuCols);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Certificates
|
|
747
|
+
if (PAG_UNIFIED_CERTS) {
|
|
748
|
+
const certRows = [];
|
|
749
|
+
const certColsSet = new Set(["Node"]);
|
|
750
|
+
metrics.forEach(host => {
|
|
751
|
+
if (!host.metrics || !Array.isArray(host.metrics.certificates)) return;
|
|
752
|
+
const label = getNodeLabel(host);
|
|
753
|
+
host.metrics.certificates.forEach(c => {
|
|
754
|
+
const row = {Node: label};
|
|
755
|
+
Object.entries(c).forEach(([k, v]) => {
|
|
756
|
+
certColsSet.add(k);
|
|
757
|
+
row[k] = v;
|
|
758
|
+
});
|
|
759
|
+
certRows.push(row);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
const certCols = Array.from(certColsSet);
|
|
763
|
+
PAG_UNIFIED_CERTS.setData(certRows, certCols);
|
|
764
|
+
}
|
|
514
765
|
}
|
|
515
766
|
|
|
516
767
|
// ------------------------------------------------------------
|
|
@@ -565,6 +816,12 @@
|
|
|
565
816
|
PAG_DISKS.setData([], []);
|
|
566
817
|
PAG_PYUDISK.setData([], []);
|
|
567
818
|
PAG_CERTS.setData([], []);
|
|
819
|
+
if (PAG_UNIFIED_SERVICES) PAG_UNIFIED_SERVICES.setData([], []);
|
|
820
|
+
if (PAG_UNIFIED_PROCESSES) PAG_UNIFIED_PROCESSES.setData([], []);
|
|
821
|
+
if (PAG_UNIFIED_DOCKER) PAG_UNIFIED_DOCKER.setData([], []);
|
|
822
|
+
if (PAG_UNIFIED_DISKS) PAG_UNIFIED_DISKS.setData([], []);
|
|
823
|
+
if (PAG_UNIFIED_PYUDISK) PAG_UNIFIED_PYUDISK.setData([], []);
|
|
824
|
+
if (PAG_UNIFIED_CERTS) PAG_UNIFIED_CERTS.setData([], []);
|
|
568
825
|
}
|
|
569
826
|
|
|
570
827
|
function resetUI() {
|
|
@@ -825,20 +1082,15 @@
|
|
|
825
1082
|
}
|
|
826
1083
|
}
|
|
827
1084
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1085
|
+
// When not in unified ("*") mode, ensure unified panel is hidden and charts cleared
|
|
1086
|
+
unifiedPanel.classList.add("hidden");
|
|
1087
|
+
unifiedNodes = [];
|
|
1088
|
+
Object.keys(unifiedCharts).forEach(key => {
|
|
1089
|
+
if (unifiedCharts[key]) {
|
|
1090
|
+
unifiedCharts[key].destroy();
|
|
1091
|
+
unifiedCharts[key] = null;
|
|
831
1092
|
}
|
|
832
|
-
}
|
|
833
|
-
unifiedPanel.classList.add("hidden");
|
|
834
|
-
unifiedNodes = [];
|
|
835
|
-
Object.keys(unifiedCharts).forEach(key => {
|
|
836
|
-
if (unifiedCharts[key]) {
|
|
837
|
-
unifiedCharts[key].destroy();
|
|
838
|
-
unifiedCharts[key] = null;
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
}
|
|
1093
|
+
});
|
|
842
1094
|
}
|
|
843
1095
|
|
|
844
1096
|
// ------------------------------------------------------------
|
|
@@ -879,29 +1131,147 @@
|
|
|
879
1131
|
});
|
|
880
1132
|
|
|
881
1133
|
// ------------------------------------------------------------
|
|
882
|
-
//
|
|
1134
|
+
// TAB MANAGEMENT
|
|
883
1135
|
// ------------------------------------------------------------
|
|
884
|
-
const
|
|
885
|
-
const
|
|
1136
|
+
const nodesTab = document.getElementById("nodes-tab");
|
|
1137
|
+
const kumaTab = document.getElementById("kuma-tab");
|
|
1138
|
+
const kumaMainTable = document.getElementById("kuma-main-table");
|
|
1139
|
+
const kumaMainThead = kumaMainTable.querySelector("thead");
|
|
1140
|
+
const kumaMainTbody = kumaMainTable.querySelector("tbody");
|
|
1141
|
+
const kumaSearchInput = document.getElementById("kuma-search-input");
|
|
1142
|
+
const controlsDiv = document.querySelector(".controls");
|
|
1143
|
+
|
|
1144
|
+
// Create paginated table for kuma using existing infrastructure
|
|
1145
|
+
const PAG_KUMA_TAB = createPaginatedTable(
|
|
1146
|
+
kumaMainTable, kumaMainThead, kumaMainTbody, 20
|
|
1147
|
+
);
|
|
886
1148
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1149
|
+
let allKumaRows = [];
|
|
1150
|
+
|
|
1151
|
+
function initWebSocket() {
|
|
1152
|
+
if (ws) return;
|
|
1153
|
+
|
|
1154
|
+
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
1155
|
+
ws = new WebSocket(`${protocol}://${location.host}/ws`);
|
|
1156
|
+
|
|
1157
|
+
ws.onopen = () => {
|
|
1158
|
+
ws.send(JSON.stringify({type: "select_target", base_url: selectedBase}));
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
ws.onmessage = evt => {
|
|
1162
|
+
try {
|
|
1163
|
+
const msg = JSON.parse(evt.data);
|
|
1164
|
+
if (msg.type === "metrics") handleMetrics(msg.data);
|
|
1165
|
+
if (msg.type === "error") alert(msg.message);
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
console.error("WS parse error:", err);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
ws.onerror = (err) => {
|
|
1172
|
+
console.error("WebSocket error:", err);
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
ws.onclose = () => {
|
|
1176
|
+
console.log("WebSocket closed");
|
|
1177
|
+
ws = null;
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function closeWebSocket() {
|
|
1182
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1183
|
+
ws.close();
|
|
1184
|
+
ws = null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async function loadKumaMap() {
|
|
1189
|
+
if (kumaMapLoaded) {
|
|
1190
|
+
PAG_KUMA_TAB.setData(allKumaRows, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Show loading state
|
|
1195
|
+
kumaMainThead.innerHTML = '<tr><th colspan="5">Loading...</th></tr>';
|
|
1196
|
+
kumaMainTbody.innerHTML = '';
|
|
890
1197
|
|
|
891
|
-
ws.onmessage = evt => {
|
|
892
1198
|
try {
|
|
893
|
-
const
|
|
894
|
-
if (
|
|
895
|
-
|
|
1199
|
+
const response = await fetch('/kuma');
|
|
1200
|
+
if (!response.ok) throw new Error('Failed to fetch service map');
|
|
1201
|
+
|
|
1202
|
+
kumaMapData = await response.json();
|
|
1203
|
+
kumaMapLoaded = true;
|
|
1204
|
+
|
|
1205
|
+
allKumaRows = normalizeServiceMap(kumaMapData);
|
|
1206
|
+
PAG_KUMA_TAB.setData(allKumaRows, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
896
1207
|
} catch (err) {
|
|
897
|
-
console.error("
|
|
1208
|
+
console.error("Error loading Kuma map:", err);
|
|
1209
|
+
kumaMainThead.innerHTML = '<tr><th>Error</th></tr>';
|
|
1210
|
+
kumaMainTbody.innerHTML = '<tr><td>Error loading Kuma endpoints. Please try again.</td></tr>';
|
|
898
1211
|
}
|
|
899
|
-
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
kumaSearchInput.addEventListener('input', (e) => {
|
|
1215
|
+
const searchTerm = e.target.value.toLowerCase();
|
|
1216
|
+
|
|
1217
|
+
if (!searchTerm) {
|
|
1218
|
+
PAG_KUMA_TAB.setData(allKumaRows, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
1219
|
+
} else {
|
|
1220
|
+
const filtered = allKumaRows.filter(row =>
|
|
1221
|
+
row.Node.toLowerCase().includes(searchTerm) ||
|
|
1222
|
+
row.Name.toLowerCase().includes(searchTerm) ||
|
|
1223
|
+
row.Parent.toLowerCase().includes(searchTerm) ||
|
|
1224
|
+
row.Tags.toLowerCase().includes(searchTerm) ||
|
|
1225
|
+
row.URL.toLowerCase().includes(searchTerm)
|
|
1226
|
+
);
|
|
1227
|
+
PAG_KUMA_TAB.setData(filtered, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
function switchToNodesTab() {
|
|
1232
|
+
currentTab = 'nodes';
|
|
1233
|
+
nodesTab.classList.add('active');
|
|
1234
|
+
kumaTab.classList.remove('active');
|
|
1235
|
+
document.body.classList.remove('kuma-view');
|
|
1236
|
+
document.body.classList.add('nodes-view');
|
|
1237
|
+
|
|
1238
|
+
controlsDiv.classList.remove('invisible');
|
|
1239
|
+
|
|
1240
|
+
closeWebSocket();
|
|
1241
|
+
initWebSocket();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function switchToKumaTab() {
|
|
1245
|
+
currentTab = 'kuma';
|
|
1246
|
+
kumaTab.classList.add('active');
|
|
1247
|
+
nodesTab.classList.remove('active');
|
|
1248
|
+
document.body.classList.add('kuma-view');
|
|
1249
|
+
document.body.classList.remove('nodes-view');
|
|
1250
|
+
|
|
1251
|
+
controlsDiv.classList.add('invisible');
|
|
1252
|
+
|
|
1253
|
+
closeWebSocket();
|
|
1254
|
+
loadKumaMap();
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (nodesTab && kumaTab) {
|
|
1258
|
+
nodesTab.addEventListener('click', switchToNodesTab);
|
|
1259
|
+
kumaTab.addEventListener('click', switchToKumaTab);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// ------------------------------------------------------------
|
|
1263
|
+
// WEBSOCKET (Managed by tabs)
|
|
1264
|
+
// ------------------------------------------------------------
|
|
900
1265
|
|
|
901
1266
|
// ------------------------------------------------------------
|
|
902
1267
|
// INIT
|
|
903
1268
|
// ------------------------------------------------------------
|
|
1269
|
+
renderEndpoints();
|
|
904
1270
|
attachSpinners();
|
|
905
1271
|
resetUI(); // reset UI, keep spinners visible
|
|
906
1272
|
showAllSpinners(); // show spinners until first metrics arrive
|
|
1273
|
+
|
|
1274
|
+
// Initialize nodes view by default
|
|
1275
|
+
document.body.classList.add('nodes-view');
|
|
1276
|
+
initWebSocket();
|
|
907
1277
|
})();
|
|
@@ -32,6 +32,34 @@ html, body {
|
|
|
32
32
|
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
.tab-navigation {
|
|
36
|
+
display: flex;
|
|
37
|
+
gap: 12px;
|
|
38
|
+
flex: 1;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.tab-btn {
|
|
43
|
+
background: transparent;
|
|
44
|
+
color: var(--muted);
|
|
45
|
+
border: none;
|
|
46
|
+
padding: 10px 32px;
|
|
47
|
+
font-size: 16px;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
border-bottom: 3px solid transparent;
|
|
51
|
+
transition: all 0.2s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.tab-btn:hover {
|
|
55
|
+
color: #e6eef8;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.tab-btn.active {
|
|
59
|
+
color: var(--accent);
|
|
60
|
+
border-bottom-color: var(--accent);
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
.brand {
|
|
36
64
|
font-weight: 700;
|
|
37
65
|
font-size: 18px;
|
|
@@ -41,6 +69,11 @@ html, body {
|
|
|
41
69
|
display: flex;
|
|
42
70
|
gap: 8px;
|
|
43
71
|
align-items: center;
|
|
72
|
+
min-width: 300px; /* Maintain space even when hidden */
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.controls.invisible {
|
|
76
|
+
visibility: hidden;
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
.controls select, .controls button {
|
|
@@ -201,6 +234,16 @@ html, body {
|
|
|
201
234
|
text-align: left;
|
|
202
235
|
}
|
|
203
236
|
|
|
237
|
+
.table tbody tr.placeholder-row td {
|
|
238
|
+
color: transparent;
|
|
239
|
+
border-bottom: 1px solid transparent;
|
|
240
|
+
padding: 8px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.table tbody tr.placeholder-row td::after {
|
|
244
|
+
content: "";
|
|
245
|
+
}
|
|
246
|
+
|
|
204
247
|
.pre {
|
|
205
248
|
background: rgba(255, 255, 255, 0.02);
|
|
206
249
|
padding: 8px;
|
|
@@ -425,6 +468,32 @@ input#proc-filter {
|
|
|
425
468
|
font-size: 13px;
|
|
426
469
|
}
|
|
427
470
|
|
|
471
|
+
.unified-tables {
|
|
472
|
+
margin-top: 12px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.unified-tables-title {
|
|
476
|
+
margin: 0 0 8px;
|
|
477
|
+
font-size: 13px;
|
|
478
|
+
color: var(--muted);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.unified-tables-grid {
|
|
482
|
+
display: flex;
|
|
483
|
+
flex-direction: column;
|
|
484
|
+
gap: 12px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.unified-table-panel {
|
|
488
|
+
display: flex;
|
|
489
|
+
flex-direction: column;
|
|
490
|
+
gap: 8px;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.unified-table-panel .table {
|
|
494
|
+
font-size: 12px;
|
|
495
|
+
}
|
|
496
|
+
|
|
428
497
|
body.unified-mode .meta-row,
|
|
429
498
|
body.unified-mode .charts-row > :not(#unified-panel),
|
|
430
499
|
body.unified-mode .details-row,
|
|
@@ -436,3 +505,42 @@ body.unified-mode footer {
|
|
|
436
505
|
body.unified-mode #unified-panel {
|
|
437
506
|
flex: 1;
|
|
438
507
|
}
|
|
508
|
+
|
|
509
|
+
/* Tab view controls */
|
|
510
|
+
body.kuma-view .meta-row,
|
|
511
|
+
body.kuma-view .charts-row,
|
|
512
|
+
body.kuma-view .details-row,
|
|
513
|
+
body.kuma-view .tables-row {
|
|
514
|
+
display: none !important;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
body.kuma-view #kuma-content {
|
|
518
|
+
display: block !important;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
body.nodes-view #kuma-content {
|
|
522
|
+
display: none !important;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#kuma-content {
|
|
526
|
+
display: none;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#kuma-content .panel {
|
|
530
|
+
max-width: 1400px;
|
|
531
|
+
margin: 0 auto;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.kuma-search {
|
|
535
|
+
margin-bottom: 12px;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.kuma-search input {
|
|
539
|
+
width: 100%;
|
|
540
|
+
padding: 10px 16px;
|
|
541
|
+
background: var(--glass);
|
|
542
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
543
|
+
color: inherit;
|
|
544
|
+
border-radius: 6px;
|
|
545
|
+
font-size: 14px;
|
|
546
|
+
}
|
|
@@ -19,8 +19,15 @@
|
|
|
19
19
|
<header class="topbar">
|
|
20
20
|
<div class="brand">Node Monitor</div>
|
|
21
21
|
|
|
22
|
+
{% if service_map is not none %}
|
|
23
|
+
<div class="tab-navigation">
|
|
24
|
+
<button id="nodes-tab" class="tab-btn active">Nodes</button>
|
|
25
|
+
<button id="kuma-tab" class="tab-btn">Uptime Kuma</button>
|
|
26
|
+
</div>
|
|
27
|
+
{% endif %}
|
|
28
|
+
|
|
22
29
|
<div class="controls">
|
|
23
|
-
<label for="node-select">Node:</label>
|
|
30
|
+
<label for="node-select" id="node-select-label">Node:</label>
|
|
24
31
|
<select id="node-select" aria-label="Select node"></select>
|
|
25
32
|
<button id="refresh-btn" title="Force refresh">Refresh</button>
|
|
26
33
|
<button id="logout" title="Logout" onclick="logout()">Logout</button>
|
|
@@ -99,6 +106,52 @@
|
|
|
99
106
|
</div>
|
|
100
107
|
</div>
|
|
101
108
|
<div id="unified-legend" class="unified-legend"></div>
|
|
109
|
+
<div class="unified-tables">
|
|
110
|
+
<div class="unified-tables-grid">
|
|
111
|
+
<div class="panel unified-table-panel">
|
|
112
|
+
<div class="panel-header"><h3>Services</h3></div>
|
|
113
|
+
<table class="table" id="unified-services-table">
|
|
114
|
+
<thead></thead>
|
|
115
|
+
<tbody></tbody>
|
|
116
|
+
</table>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="panel unified-table-panel">
|
|
119
|
+
<div class="panel-header"><h3>Processes</h3></div>
|
|
120
|
+
<table class="table" id="unified-processes-table">
|
|
121
|
+
<thead></thead>
|
|
122
|
+
<tbody></tbody>
|
|
123
|
+
</table>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="panel unified-table-panel">
|
|
126
|
+
<div class="panel-header"><h3>Docker Containers</h3></div>
|
|
127
|
+
<table class="table" id="unified-docker-table">
|
|
128
|
+
<thead></thead>
|
|
129
|
+
<tbody></tbody>
|
|
130
|
+
</table>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="panel unified-table-panel">
|
|
133
|
+
<div class="panel-header"><h3>Disks</h3></div>
|
|
134
|
+
<table class="table" id="unified-disks-table">
|
|
135
|
+
<thead></thead>
|
|
136
|
+
<tbody></tbody>
|
|
137
|
+
</table>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="panel unified-table-panel">
|
|
140
|
+
<div class="panel-header"><h3>PyUdisk Metrics</h3></div>
|
|
141
|
+
<table class="table" id="unified-pyudisk-table">
|
|
142
|
+
<thead></thead>
|
|
143
|
+
<tbody></tbody>
|
|
144
|
+
</table>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="panel unified-table-panel">
|
|
147
|
+
<div class="panel-header"><h3>Certificates</h3></div>
|
|
148
|
+
<table class="table" id="unified-certificates-table">
|
|
149
|
+
<thead></thead>
|
|
150
|
+
<tbody></tbody>
|
|
151
|
+
</table>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
102
155
|
</div>
|
|
103
156
|
</section>
|
|
104
157
|
|
|
@@ -203,10 +256,32 @@
|
|
|
203
256
|
</table>
|
|
204
257
|
</div>
|
|
205
258
|
</div>
|
|
259
|
+
|
|
260
|
+
<div class="panel">
|
|
261
|
+
<div class="panel-header"><h3>Kuma Endpoints</h3></div>
|
|
262
|
+
<div class="panel-body">
|
|
263
|
+
<table class="table" id="endpoints-table">
|
|
264
|
+
<thead></thead>
|
|
265
|
+
<tbody></tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
206
269
|
</section>
|
|
207
270
|
</section>
|
|
208
271
|
</main>
|
|
209
272
|
|
|
273
|
+
<div id="kuma-content" class="container">
|
|
274
|
+
<section class="panel">
|
|
275
|
+
<div class="kuma-search">
|
|
276
|
+
<input type="text" id="kuma-search-input" placeholder="Search Kuma endpoints by node, name, parent, or tags...">
|
|
277
|
+
</div>
|
|
278
|
+
<table class="table" id="kuma-main-table">
|
|
279
|
+
<thead></thead>
|
|
280
|
+
<tbody></tbody>
|
|
281
|
+
</table>
|
|
282
|
+
</section>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
210
285
|
<footer class="footer">
|
|
211
286
|
<div><code>PyObservability: v{{ version }}</code><br>
|
|
212
287
|
<a href="https://github.com/thevickypedia/PyObservability" target="_blank">https://github.com/thevickypedia/PyObservability</a>
|
|
@@ -217,6 +292,13 @@
|
|
|
217
292
|
// inject server-side targets
|
|
218
293
|
window.MONITOR_TARGETS = {{ targets | tojson }};
|
|
219
294
|
</script>
|
|
295
|
+
|
|
296
|
+
{% if service_map is not none %}
|
|
297
|
+
<script>
|
|
298
|
+
window.SERVICE_MAP = {{ service_map | tojson }};
|
|
299
|
+
</script>
|
|
300
|
+
{% endif %}
|
|
301
|
+
|
|
220
302
|
<script>
|
|
221
303
|
const logoutEndpoint = "{{ logout }}";
|
|
222
304
|
|
pyobservability/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "2.0.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyObservability
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Lightweight OS-agnostic observability UI for PyNinja
|
|
5
5
|
Author-email: Vignesh Rao <svignesh1793@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -42,13 +42,16 @@ Requires-Python: >=3.11
|
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: aiohttp==3.13.*
|
|
45
|
-
Requires-Dist: fastapi==0.
|
|
45
|
+
Requires-Dist: fastapi==0.128.*
|
|
46
46
|
Requires-Dist: FastAPI-UI-Auth==0.2.1
|
|
47
47
|
Requires-Dist: Jinja2==3.1.*
|
|
48
48
|
Requires-Dist: pydantic==2.12.*
|
|
49
49
|
Requires-Dist: pydantic-settings==2.12.*
|
|
50
50
|
Requires-Dist: python-dotenv==1.2.*
|
|
51
|
-
Requires-Dist:
|
|
51
|
+
Requires-Dist: python-socketio==5.16.*
|
|
52
|
+
Requires-Dist: requests==2.32.*
|
|
53
|
+
Requires-Dist: uvicorn[standard]==0.40.*
|
|
54
|
+
Requires-Dist: websocket-client==1.9.*
|
|
52
55
|
Provides-Extra: dev
|
|
53
56
|
Requires-Dist: pre-commit; extra == "dev"
|
|
54
57
|
Dynamic: license-file
|
|
@@ -140,6 +143,13 @@ docker run \
|
|
|
140
143
|
- **LOGS_PATH** - Directory path to store log files if `LOG` is set to `file`.
|
|
141
144
|
- **LOG_CONFIG** - Path to a custom logging configuration file.
|
|
142
145
|
|
|
146
|
+
**Uptime Kuma**
|
|
147
|
+
> Uptime Kuma integration can be enabled by setting the following environment variables.
|
|
148
|
+
- **KUMA_URL** - Base URL of the Uptime Kuma server.
|
|
149
|
+
- **KUMA_USERNAME** - Username to authenticate with Uptime Kuma.
|
|
150
|
+
- **KUMA_PASSWORD** - Password to authenticate with Uptime Kuma.
|
|
151
|
+
- **KUMA_TIMEOUT** - Timeout (in seconds) for Uptime Kuma authentication. Defaults to 5s.
|
|
152
|
+
|
|
143
153
|
## License & copyright
|
|
144
154
|
|
|
145
155
|
© Vignesh Rao
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pyobservability/__init__.py,sha256=yVBLyTohBiBKp0Otyl04IggPh8mhg3Er25u6eFyxMto,2618
|
|
2
|
+
pyobservability/kuma.py,sha256=RL6ZsK6ZE3vUufsUQ3r4gnH8uka0k8ARd41GP5YozbU,3634
|
|
3
|
+
pyobservability/main.py,sha256=EbP6xkOvj2cV1cOEmmq6E6QsLOhbvKI_jdpfcWjSz-M,6124
|
|
4
|
+
pyobservability/monitor.py,sha256=i_Xf_DB-qLOp1b9wryekjwHIM8AnMrGTkuEg7e08bcM,7539
|
|
5
|
+
pyobservability/transport.py,sha256=S-84mgf-9yMj0H7VSAmueW9yosX_1XxdyNJC2EuQHQQ,8493
|
|
6
|
+
pyobservability/version.py,sha256=_7OlQdbVkK4jad0CLdpI0grT-zEAb-qgFmH5mFzDXiA,22
|
|
7
|
+
pyobservability/config/enums.py,sha256=rQZh2Q4-9ItQTQgxsYjqw-jNePpWHhvQUrbrQJBG5CI,313
|
|
8
|
+
pyobservability/config/settings.py,sha256=aCz1tZEg8fUA5gHCCDN_m3fgrH1axz2fvdalj4bszGs,6121
|
|
9
|
+
pyobservability/static/app.js,sha256=lE-Oybq50FNr_0SrZJFiJihiXyIcyhw7-SNbGA81ySQ,48675
|
|
10
|
+
pyobservability/static/styles.css,sha256=P6Xg-IAXO3WNeBLGH9Q5HAdNeDMRbFcM5P_cq60Jf00,9405
|
|
11
|
+
pyobservability/templates/index.html,sha256=WQNOT_uHfAvVqOnYc1olYdyrjM11tDpeKwTkbPQ853Q,11834
|
|
12
|
+
pyobservability-2.0.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
13
|
+
pyobservability-2.0.0.dist-info/METADATA,sha256=hu_7k6rRxUXitpfLhkAB6GZxMYQNDeqGaQAHJDKa0Ss,7026
|
|
14
|
+
pyobservability-2.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
15
|
+
pyobservability-2.0.0.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
|
|
16
|
+
pyobservability-2.0.0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
17
|
+
pyobservability-2.0.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
pyobservability/__init__.py,sha256=yVBLyTohBiBKp0Otyl04IggPh8mhg3Er25u6eFyxMto,2618
|
|
2
|
-
pyobservability/main.py,sha256=EJ49ENDnKy07jKHQ0IYEVmsyc_4O3uxCHpn1faQD5VM,4534
|
|
3
|
-
pyobservability/monitor.py,sha256=i_Xf_DB-qLOp1b9wryekjwHIM8AnMrGTkuEg7e08bcM,7539
|
|
4
|
-
pyobservability/transport.py,sha256=S-84mgf-9yMj0H7VSAmueW9yosX_1XxdyNJC2EuQHQQ,8493
|
|
5
|
-
pyobservability/version.py,sha256=8UhoYEXHs1Oai7BW_ExBmuwWnRI-yMG_u1fQAXMizHQ,22
|
|
6
|
-
pyobservability/config/enums.py,sha256=EhvD9kB5EMW3ARxr5KmISmf-rP3D4IKqOIjw6Tb8SB8,294
|
|
7
|
-
pyobservability/config/settings.py,sha256=ylhiT0SARHuzkyon_1otgsO74AfA6aiUKp5uczZQj08,5980
|
|
8
|
-
pyobservability/static/app.js,sha256=lel2ZZH-ifjehzy2d-5UQxOhnBIHxla05xjokW1nXXA,33919
|
|
9
|
-
pyobservability/static/styles.css,sha256=0Vagj7nDac27JC0M870V3yqc1XN4rB5pDYuy4zjit3c,7618
|
|
10
|
-
pyobservability/templates/index.html,sha256=JdGn7Bg9w-4zzcMzX78p0q5760ao7sioaqB1s_8d0Fs,8421
|
|
11
|
-
pyobservability-1.4.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
12
|
-
pyobservability-1.4.0.dist-info/METADATA,sha256=DHbIRiPU0Vi_5PjsT0k5NgMInO8_YgY5azP9fGQM8VQ,6537
|
|
13
|
-
pyobservability-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
pyobservability-1.4.0.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
|
|
15
|
-
pyobservability-1.4.0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
16
|
-
pyobservability-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|