PyObservability 1.4.1__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.
@@ -9,6 +9,7 @@ class APIEndpoints(StrEnum):
9
9
  """
10
10
 
11
11
  health = "/health"
12
+ kuma = "/kuma"
12
13
  root = "/"
13
14
  ws = "/ws"
14
15
 
@@ -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
 
@@ -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 typing import Any, Dict
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
- args: Dict[str, Any] = dict(request=request, targets=settings.env.targets, version=__version__)
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)
@@ -8,6 +8,12 @@
8
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
  // ------------------------------------------------------------
@@ -320,6 +330,11 @@
320
330
  const PAG_CERTS = createPaginatedTable(
321
331
  certsTable, certsTableHead, certsTableBody
322
332
  );
333
+ const PAG_ENDPOINTS = createPaginatedTable(
334
+ endpointsTable,
335
+ endpointsTableHead,
336
+ endpointsTableBody
337
+ );
323
338
 
324
339
  // ------------------------------------------------------------
325
340
  // CHART HELPERS
@@ -351,6 +366,32 @@
351
366
  });
352
367
  }
353
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
+
354
395
  function makeCoreSparkline(ctx, coreName) {
355
396
  const EMPTY_LABELS = Array(MAX_POINTS).fill("");
356
397
  const EMPTY_DATA = Array(MAX_POINTS).fill(null);
@@ -438,8 +479,16 @@
438
479
  );
439
480
 
440
481
  let unifiedNodes = [];
441
- // TODO: Update colorPalette to use contrasting colors
442
- const colorPalette = ["#63b3ff", "#ff99c8", "#7dd3fc", "#fbbf24", "#a3e635", "#f87171", "#c084fc", "#38bdf8"];
482
+ const colorPalette = [
483
+ "#ff0000",
484
+ "#ffff00",
485
+ "#00ff00",
486
+ "#0066ff",
487
+ "#b300ff",
488
+ "#ff7f00",
489
+ "#8b4513",
490
+ "#7f7f7f"
491
+ ];
443
492
  const nodeColor = {};
444
493
  const unifiedCharts = {memory: null, cpu: null, disk: null};
445
494
 
@@ -1082,29 +1131,147 @@
1082
1131
  });
1083
1132
 
1084
1133
  // ------------------------------------------------------------
1085
- // WEBSOCKET
1134
+ // TAB MANAGEMENT
1086
1135
  // ------------------------------------------------------------
1087
- const protocol = location.protocol === "https:" ? "wss" : "ws";
1088
- const ws = new WebSocket(`${protocol}://${location.host}/ws`);
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
+ );
1089
1148
 
1090
- ws.onopen = () => {
1091
- ws.send(JSON.stringify({type: "select_target", base_url: selectedBase}));
1092
- };
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 = '';
1093
1197
 
1094
- ws.onmessage = evt => {
1095
1198
  try {
1096
- const msg = JSON.parse(evt.data);
1097
- if (msg.type === "metrics") handleMetrics(msg.data);
1098
- if (msg.type === "error") alert(msg.message);
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"]);
1099
1207
  } catch (err) {
1100
- console.error("WS parse error:", err);
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>';
1101
1211
  }
1102
- };
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
+ // ------------------------------------------------------------
1103
1265
 
1104
1266
  // ------------------------------------------------------------
1105
1267
  // INIT
1106
1268
  // ------------------------------------------------------------
1269
+ renderEndpoints();
1107
1270
  attachSpinners();
1108
1271
  resetUI(); // reset UI, keep spinners visible
1109
1272
  showAllSpinners(); // show spinners until first metrics arrive
1273
+
1274
+ // Initialize nodes view by default
1275
+ document.body.classList.add('nodes-view');
1276
+ initWebSocket();
1110
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 {
@@ -472,3 +505,42 @@ body.unified-mode footer {
472
505
  body.unified-mode #unified-panel {
473
506
  flex: 1;
474
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>
@@ -98,7 +105,6 @@
98
105
  <canvas id="unified-disk-chart" class="chart"></canvas>
99
106
  </div>
100
107
  </div>
101
- {# TODO: Make the following tables into multiple div containers #}
102
108
  <div id="unified-legend" class="unified-legend"></div>
103
109
  <div class="unified-tables">
104
110
  <div class="unified-tables-grid">
@@ -250,10 +256,32 @@
250
256
  </table>
251
257
  </div>
252
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>
253
269
  </section>
254
270
  </section>
255
271
  </main>
256
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
+
257
285
  <footer class="footer">
258
286
  <div><code>PyObservability: v{{ version }}</code><br>
259
287
  <a href="https://github.com/thevickypedia/PyObservability" target="_blank">https://github.com/thevickypedia/PyObservability</a>
@@ -264,6 +292,13 @@
264
292
  // inject server-side targets
265
293
  window.MONITOR_TARGETS = {{ targets | tojson }};
266
294
  </script>
295
+
296
+ {% if service_map is not none %}
297
+ <script>
298
+ window.SERVICE_MAP = {{ service_map | tojson }};
299
+ </script>
300
+ {% endif %}
301
+
267
302
  <script>
268
303
  const logoutEndpoint = "{{ logout }}";
269
304
 
@@ -1 +1 @@
1
- __version__ = "1.4.1"
1
+ __version__ = "2.0.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyObservability
3
- Version: 1.4.1
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.122.*
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: uvicorn[standard]==0.38.*
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
  &copy; 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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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=BqOI5y46o1G1RWC9bF1DPL-YM68lGYPmZt1pn6FZFZs,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=l6m8mwEU4z5d2wQF7Fa6tFITW3HX3M5RaLhBJ0jFXrM,43322
9
- pyobservability/static/styles.css,sha256=DRJ4kw-LDlXMcQXxFd8cEDuDC_ZfwZgARAjn0zDWwRk,8172
10
- pyobservability/templates/index.html,sha256=Z_r1Gq0QNxEwTL4_2NPQ1cKqLpbQoJC88E5leyjo07s,10786
11
- pyobservability-1.4.1.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
12
- pyobservability-1.4.1.dist-info/METADATA,sha256=QUIDftx5D229DYD03NsspRYP2Imp0u_rVVlzW4-UPHo,6537
13
- pyobservability-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- pyobservability-1.4.1.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
15
- pyobservability-1.4.1.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
16
- pyobservability-1.4.1.dist-info/RECORD,,