Add homelab dashboards: system, LLM, and UPS on one domain
Three dashboards served by one FastAPI app with a SQLite history store
(raw / 1m / 1h) and hand-written SVG charts: no Prometheus, no Grafana,
no CDN, no build step. Nothing is fetched from the internet at runtime.
* system: a stdlib-only agent (agent/host_agent.py) reports /proc and
/sys counters; the dashboard turns them into rates over a labelled
window, so every number says how it was measured.
* llm: gufo/llama.cpp and vLLM /metrics are parsed by an in-house
exposition parser. Measured delta/elapsed throughput beats a native
per-request gauge, and each value carries its provenance.
* ups: a NUT wire-protocol client plus health checks that name what
they looked at. Commands are gated by NUT_ALLOW_ACTIONS,
DASH_READ_ONLY and a DANGER confirmation, re-checked server-side.
Missing values render as n/a rather than 0, and an unconfigured source
answers 404 {"configured": false} without dialling anything, so "not
deployed here" never looks like an outage.
253 tests at 94% coverage against captured real fixtures; ruff and
pyright clean.
LLM-Generated: true
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""Startup wiring: `python -m dashboards` / the `dashboards` console script.
|
||||
|
||||
Two things are worth pinning: the reload path must hand uvicorn an import string (an
|
||||
app object cannot be reloaded), and debugpy must stay completely inert unless asked.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from dashboards import __main__ as entry
|
||||
from dashboards import debug as debug_module
|
||||
from dashboards.config import Settings
|
||||
|
||||
|
||||
class FakeUvicorn(ModuleType):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("uvicorn")
|
||||
self.calls: list[tuple[Any, dict[str, Any]]] = []
|
||||
|
||||
def run(self, target: Any, **kwargs: Any) -> None:
|
||||
self.calls.append((target, kwargs))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_uvicorn(monkeypatch: pytest.MonkeyPatch) -> FakeUvicorn:
|
||||
module = FakeUvicorn()
|
||||
monkeypatch.setitem(sys.modules, "uvicorn", module)
|
||||
return module
|
||||
|
||||
|
||||
def test_main_serves_the_assembled_app(fake_uvicorn: FakeUvicorn, tmp_path: Any,
|
||||
monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Settings is frozen on purpose, so the module attribute is replaced, not poked. The DB
|
||||
# path is private: a test must not open the developer's running database.
|
||||
monkeypatch.setattr(entry, "SETTINGS",
|
||||
Settings(host="0.0.0.0", port=8090, db_path=str(tmp_path / "main.sqlite3")))
|
||||
monkeypatch.delenv("DASH_RELOAD", raising=False)
|
||||
entry.main()
|
||||
(target, kwargs), = fake_uvicorn.calls
|
||||
assert kwargs == {"host": "0.0.0.0", "port": 8090}
|
||||
# FastAPI keeps included routers in a private wrapper, so the honest check is a request
|
||||
assert isinstance(target, FastAPI)
|
||||
with TestClient(target) as client:
|
||||
assert client.get("/api/health").status_code == 200
|
||||
assert client.get("/ups").status_code == 200
|
||||
|
||||
|
||||
def test_reload_mode_passes_an_import_string(fake_uvicorn: FakeUvicorn, tmp_path: Any,
|
||||
monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(entry, "SETTINGS", Settings(db_path=str(tmp_path / "reload.sqlite3")))
|
||||
# uvicorn cannot reload an object it was handed, only a module:app string
|
||||
monkeypatch.setenv("DASH_RELOAD", "1")
|
||||
entry.main()
|
||||
(target, kwargs), = fake_uvicorn.calls
|
||||
assert target == "dashboards.app:app"
|
||||
assert kwargs["reload"] is True
|
||||
|
||||
|
||||
def test_debugpy_is_inert_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("DEBUGPY", raising=False)
|
||||
assert debug_module.maybe_listen() is False
|
||||
|
||||
|
||||
def test_debugpy_listens_when_asked(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls: list[tuple[str, int]] = []
|
||||
fake = ModuleType("debugpy")
|
||||
fake.listen = lambda addr: calls.append(addr) # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "debugpy", fake)
|
||||
monkeypatch.setenv("DEBUGPY", "yes")
|
||||
monkeypatch.setenv("DEBUGPY_PORT", "5699")
|
||||
assert debug_module.maybe_listen() is True
|
||||
assert calls == [("0.0.0.0", 5699)]
|
||||
|
||||
|
||||
def test_debugpy_missing_is_a_message_not_a_crash(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("DEBUGPY", "1")
|
||||
# a None entry makes `import debugpy` raise ImportError, as it does in a prod venv
|
||||
monkeypatch.setitem(sys.modules, "debugpy", None) # type: ignore[arg-type]
|
||||
assert debug_module.maybe_listen() is False
|
||||
Reference in New Issue
Block a user