2026-03-02 01:48:51 +01:00
|
|
|
"""Unraid servers status router."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter
|
|
|
|
|
|
|
|
|
|
from server.cache import cache
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
Complete admin backend with login, where all integrations (weather, news,
Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead
of ENV variables. Two-layer config: ENV seeds DB on first start, then DB
is source of truth. Auto-migration system on startup.
Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service,
admin router (protected), test_connections per integration, config.py rewrite.
Frontend: react-router v6, login page, admin layout with sidebar, 8 settings
pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword),
shared IntegrationForm + TestButton components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:37:30 +01:00
|
|
|
from server.config import get_settings
|
2026-03-02 22:41:16 +01:00
|
|
|
from server.services.mqtt_service import mqtt_service
|
2026-03-02 01:48:51 +01:00
|
|
|
from server.services.unraid_service import ServerConfig, fetch_all_servers
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api", tags=["servers"])
|
|
|
|
|
|
|
|
|
|
CACHE_KEY = "servers"
|
|
|
|
|
|
|
|
|
|
|
2026-03-02 22:41:16 +01:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# MQTT enrichment — overlay live system metrics from MQTT topics
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _enrich_from_mqtt(servers: List[Dict[str, Any]]) -> None:
|
|
|
|
|
"""Merge live CPU/RAM data from MQTT ``<prefix>/system`` topics.
|
|
|
|
|
|
|
|
|
|
The Unraid MQTT Agent plugin publishes JSON payloads to topics like
|
|
|
|
|
``unraid-daddelolymp/system`` or ``Adriahub/system`` every ~15 s.
|
|
|
|
|
These contain live ``cpu_usage_percent``, ``ram_usage_percent``, etc.
|
|
|
|
|
that the GraphQL API does not expose.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
store = mqtt_service.store
|
|
|
|
|
|
|
|
|
|
for srv in servers:
|
|
|
|
|
name = srv.get("name", "")
|
|
|
|
|
if not name:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Try common topic patterns
|
|
|
|
|
system_data: Dict[str, Any] | None = None
|
|
|
|
|
for pattern in (
|
|
|
|
|
f"{name}/system", # "Adriahub/system"
|
|
|
|
|
f"unraid-{name.lower()}/system", # "unraid-daddelolymp/system"
|
|
|
|
|
f"unraid-{name}/system", # "unraid-Daddelolymp/system"
|
|
|
|
|
):
|
|
|
|
|
msg = store.get(pattern)
|
|
|
|
|
if msg is not None and isinstance(msg.payload, dict):
|
|
|
|
|
system_data = msg.payload
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not system_data:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# --- CPU ---
|
|
|
|
|
cpu_pct = system_data.get("cpu_usage_percent")
|
|
|
|
|
if cpu_pct is not None:
|
|
|
|
|
srv["cpu"]["usage_pct"] = round(float(cpu_pct), 1)
|
|
|
|
|
|
|
|
|
|
cpu_model = system_data.get("cpu_model")
|
|
|
|
|
if cpu_model:
|
|
|
|
|
srv["cpu"]["brand"] = cpu_model
|
|
|
|
|
|
|
|
|
|
cpu_temp = system_data.get("cpu_temp_celsius")
|
|
|
|
|
if cpu_temp is not None:
|
|
|
|
|
srv["cpu"]["temp_c"] = cpu_temp
|
|
|
|
|
|
|
|
|
|
cores = system_data.get("cpu_cores")
|
|
|
|
|
if cores:
|
|
|
|
|
srv["cpu"]["cores"] = cores
|
|
|
|
|
|
|
|
|
|
threads = system_data.get("cpu_threads")
|
|
|
|
|
if threads:
|
|
|
|
|
srv["cpu"]["threads"] = threads
|
|
|
|
|
|
|
|
|
|
# --- RAM ---
|
|
|
|
|
ram_pct = system_data.get("ram_usage_percent")
|
|
|
|
|
if ram_pct is not None:
|
|
|
|
|
srv["ram"]["pct"] = round(float(ram_pct), 1)
|
|
|
|
|
|
|
|
|
|
ram_total = system_data.get("ram_total_bytes")
|
|
|
|
|
if ram_total:
|
|
|
|
|
srv["ram"]["total_gb"] = round(ram_total / (1024 ** 3), 1)
|
|
|
|
|
|
|
|
|
|
ram_used = system_data.get("ram_used_bytes")
|
|
|
|
|
if ram_used:
|
|
|
|
|
srv["ram"]["used_gb"] = round(ram_used / (1024 ** 3), 1)
|
|
|
|
|
|
|
|
|
|
# --- Uptime ---
|
|
|
|
|
uptime_secs = system_data.get("uptime_seconds")
|
|
|
|
|
if uptime_secs:
|
|
|
|
|
days = uptime_secs // 86400
|
|
|
|
|
hours = (uptime_secs % 86400) // 3600
|
|
|
|
|
srv["uptime"] = f"{days}d {hours}h"
|
|
|
|
|
|
|
|
|
|
srv["online"] = True
|
|
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
"[UNRAID] %s: MQTT enriched — CPU %.1f%% %.0f°C, RAM %.1f%%",
|
|
|
|
|
name,
|
|
|
|
|
srv["cpu"].get("usage_pct", 0),
|
|
|
|
|
srv["cpu"].get("temp_c", 0) or 0,
|
|
|
|
|
srv["ram"].get("pct", 0),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-02 01:48:51 +01:00
|
|
|
@router.get("/servers")
|
|
|
|
|
async def get_servers() -> Dict[str, Any]:
|
|
|
|
|
"""Return status information for all configured Unraid servers.
|
|
|
|
|
|
|
|
|
|
Response shape::
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
"servers": [ ... server dicts ... ]
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# --- cache hit? -----------------------------------------------------------
|
|
|
|
|
cached = await cache.get(CACHE_KEY)
|
|
|
|
|
if cached is not None:
|
2026-03-02 22:41:16 +01:00
|
|
|
# Always overlay fresh MQTT data even on cache hits
|
|
|
|
|
_enrich_from_mqtt(cached.get("servers", []))
|
2026-03-02 01:48:51 +01:00
|
|
|
return cached
|
|
|
|
|
|
|
|
|
|
# --- cache miss -----------------------------------------------------------
|
|
|
|
|
server_configs: List[ServerConfig] = [
|
|
|
|
|
ServerConfig(
|
|
|
|
|
name=srv.name,
|
|
|
|
|
host=srv.host,
|
|
|
|
|
api_key=srv.api_key,
|
|
|
|
|
port=srv.port,
|
|
|
|
|
)
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
Complete admin backend with login, where all integrations (weather, news,
Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead
of ENV variables. Two-layer config: ENV seeds DB on first start, then DB
is source of truth. Auto-migration system on startup.
Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service,
admin router (protected), test_connections per integration, config.py rewrite.
Frontend: react-router v6, login page, admin layout with sidebar, 8 settings
pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword),
shared IntegrationForm + TestButton components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:37:30 +01:00
|
|
|
for srv in get_settings().unraid_servers
|
2026-03-02 01:48:51 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
servers_data: List[Dict[str, Any]] = []
|
|
|
|
|
try:
|
|
|
|
|
servers_data = await fetch_all_servers(server_configs)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.exception("Failed to fetch Unraid server data")
|
|
|
|
|
return {
|
|
|
|
|
"servers": [],
|
|
|
|
|
"error": True,
|
|
|
|
|
"message": str(exc),
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 22:41:16 +01:00
|
|
|
# Overlay live MQTT system metrics
|
|
|
|
|
_enrich_from_mqtt(servers_data)
|
|
|
|
|
|
2026-03-02 01:48:51 +01:00
|
|
|
payload: Dict[str, Any] = {
|
|
|
|
|
"servers": servers_data,
|
|
|
|
|
}
|
|
|
|
|
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
Complete admin backend with login, where all integrations (weather, news,
Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead
of ENV variables. Two-layer config: ENV seeds DB on first start, then DB
is source of truth. Auto-migration system on startup.
Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service,
admin router (protected), test_connections per integration, config.py rewrite.
Frontend: react-router v6, login page, admin layout with sidebar, 8 settings
pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword),
shared IntegrationForm + TestButton components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:37:30 +01:00
|
|
|
await cache.set(CACHE_KEY, payload, get_settings().unraid_cache_ttl)
|
2026-03-02 01:48:51 +01:00
|
|
|
return payload
|