Features: - Real-time Proxmox cluster monitoring (nodes, LXC containers) - Camera integration with go2rtc streams - Arr stack download progress monitoring - PBS backup status - Docker container status - Uptime Kuma service health - FastAPI backend with HTMX frontend
266 lines
10 KiB
Python
266 lines
10 KiB
Python
"""Enhanced FastAPI dashboard v2 with all integrations."""
|
|
from fastapi import FastAPI, Request, Form
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse, RedirectResponse
|
|
import httpx
|
|
from datetime import datetime
|
|
|
|
from app.config import (
|
|
SERVICES, PROXMOX_NODES, PROXMOX_API_TOKEN, PROXMOX_API_SECRET,
|
|
PBS_URL, PBS_API_TOKEN, PBS_API_SECRET,
|
|
CATEGORIES, GO2RTC_URL, CAMERAS, SABNZBD_URL, SABNZBD_API_KEY,
|
|
UPTIME_KUMA_URL, UPTIME_KUMA_STATUS_PAGE, DOCKER_HOSTS,
|
|
PROMETHEUS_URL, SERVICE_ICONS, SERVICE_GROUPS,
|
|
get_services_by_category, get_favorites, get_critical_services, get_service_icon,
|
|
load_settings, save_settings, DEFAULT_SETTINGS
|
|
)
|
|
from app.services.health import (
|
|
check_all_services, get_all_proxmox_metrics, get_camera_list,
|
|
get_sabnzbd_queue, get_uptime_kuma_status, get_docker_containers,
|
|
get_docker_container_counts, get_pbs_status, get_storage_pools,
|
|
get_recent_events, get_cluster_uptime, get_prometheus_metrics
|
|
)
|
|
|
|
app = FastAPI(title="DeathStar Dashboard", version="2.0.0")
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
# Add custom filters
|
|
templates.env.globals["get_service_icon"] = get_service_icon
|
|
templates.env.globals["SERVICE_ICONS"] = SERVICE_ICONS
|
|
|
|
last_check = {"time": None}
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def dashboard(request: Request):
|
|
"""Main dashboard page."""
|
|
services_status = await check_all_services(SERVICES)
|
|
nodes_status = await get_all_proxmox_metrics(PROXMOX_NODES, PROXMOX_API_TOKEN, PROXMOX_API_SECRET)
|
|
docker_counts = await get_docker_container_counts(DOCKER_HOSTS)
|
|
settings = load_settings()
|
|
|
|
online_count = sum(1 for s in services_status.values() if s.status == "online")
|
|
total_count = len(services_status)
|
|
critical_down = [s.name for s in get_critical_services()
|
|
if services_status.get(s.name) and services_status[s.name].status != "online"]
|
|
cluster_uptime = get_cluster_uptime(nodes_status)
|
|
|
|
last_check["time"] = datetime.now()
|
|
|
|
return templates.TemplateResponse("dashboard.html", {
|
|
"request": request,
|
|
"services_by_category": get_services_by_category(),
|
|
"services_status": services_status,
|
|
"nodes_status": nodes_status,
|
|
"favorites": get_favorites(),
|
|
"categories": CATEGORIES,
|
|
"online_count": online_count,
|
|
"total_count": total_count,
|
|
"critical_down": critical_down,
|
|
"last_check": last_check["time"].strftime("%H:%M:%S") if last_check["time"] else "Never",
|
|
"cameras": CAMERAS,
|
|
"go2rtc_url": GO2RTC_URL,
|
|
"docker_counts": docker_counts,
|
|
"cluster_uptime": cluster_uptime,
|
|
"settings": settings,
|
|
"service_groups": SERVICE_GROUPS,
|
|
})
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "healthy"}
|
|
|
|
@app.get("/api/services", response_class=HTMLResponse)
|
|
async def api_services(request: Request):
|
|
"""HTMX endpoint for services."""
|
|
services_status = await check_all_services(SERVICES)
|
|
return templates.TemplateResponse("partials/services.html", {
|
|
"request": request,
|
|
"services_by_category": get_services_by_category(),
|
|
"services_status": services_status,
|
|
"categories": CATEGORIES,
|
|
"service_groups": SERVICE_GROUPS,
|
|
})
|
|
|
|
@app.get("/api/nodes", response_class=HTMLResponse)
|
|
async def api_nodes(request: Request):
|
|
"""HTMX endpoint for Proxmox nodes."""
|
|
nodes_status = await get_all_proxmox_metrics(PROXMOX_NODES, PROXMOX_API_TOKEN, PROXMOX_API_SECRET)
|
|
cluster_uptime = get_cluster_uptime(nodes_status)
|
|
return templates.TemplateResponse("partials/nodes.html", {
|
|
"request": request,
|
|
"nodes_status": nodes_status,
|
|
"cluster_uptime": cluster_uptime,
|
|
})
|
|
|
|
@app.get("/api/nodes-expanded", response_class=HTMLResponse)
|
|
async def api_nodes_expanded(request: Request):
|
|
"""HTMX endpoint for expanded Proxmox nodes with VMs/containers."""
|
|
nodes_status = await get_all_proxmox_metrics(PROXMOX_NODES, PROXMOX_API_TOKEN, PROXMOX_API_SECRET)
|
|
return templates.TemplateResponse("partials/nodes_expanded.html", {
|
|
"request": request,
|
|
"nodes_status": nodes_status,
|
|
})
|
|
|
|
@app.get("/api/favorites", response_class=HTMLResponse)
|
|
async def api_favorites(request: Request):
|
|
"""HTMX endpoint for favorites."""
|
|
services_status = await check_all_services(SERVICES)
|
|
return templates.TemplateResponse("partials/favorites.html", {
|
|
"request": request,
|
|
"favorites": get_favorites(),
|
|
"services_status": services_status,
|
|
})
|
|
|
|
@app.get("/api/cameras", response_class=HTMLResponse)
|
|
async def api_cameras(request: Request):
|
|
"""HTMX endpoint for cameras."""
|
|
return templates.TemplateResponse("partials/cameras.html", {
|
|
"request": request,
|
|
"cameras": CAMERAS,
|
|
"go2rtc_url": GO2RTC_URL,
|
|
})
|
|
|
|
@app.get("/api/downloads", response_class=HTMLResponse)
|
|
async def api_downloads(request: Request):
|
|
"""HTMX endpoint for downloads."""
|
|
queue = await get_sabnzbd_queue(SABNZBD_URL, SABNZBD_API_KEY)
|
|
return templates.TemplateResponse("partials/downloads.html", {
|
|
"request": request,
|
|
"queue": queue,
|
|
})
|
|
|
|
@app.get("/api/status-banner", response_class=HTMLResponse)
|
|
async def api_status_banner(request: Request):
|
|
"""HTMX endpoint for status banner."""
|
|
services_status = await check_all_services(SERVICES)
|
|
critical_down = [s.name for s in get_critical_services()
|
|
if services_status.get(s.name) and services_status[s.name].status != "online"]
|
|
online_count = sum(1 for s in services_status.values() if s.status == "online")
|
|
last_check["time"] = datetime.now()
|
|
|
|
return templates.TemplateResponse("partials/status_banner.html", {
|
|
"request": request,
|
|
"critical_down": critical_down,
|
|
"online_count": online_count,
|
|
"total_count": len(services_status),
|
|
"last_check": last_check["time"].strftime("%H:%M:%S"),
|
|
})
|
|
|
|
@app.get("/api/docker", response_class=HTMLResponse)
|
|
async def api_docker(request: Request):
|
|
"""HTMX endpoint for Docker containers."""
|
|
containers = await get_docker_containers(DOCKER_HOSTS)
|
|
counts = await get_docker_container_counts(DOCKER_HOSTS)
|
|
return templates.TemplateResponse("partials/docker.html", {
|
|
"request": request,
|
|
"containers": containers,
|
|
"hosts": DOCKER_HOSTS,
|
|
"counts": counts,
|
|
})
|
|
|
|
@app.get("/api/uptime", response_class=HTMLResponse)
|
|
async def api_uptime(request: Request):
|
|
"""HTMX endpoint for Uptime Kuma status."""
|
|
uptime_data = await get_uptime_kuma_status(UPTIME_KUMA_URL, UPTIME_KUMA_STATUS_PAGE)
|
|
return templates.TemplateResponse("partials/uptime.html", {
|
|
"request": request,
|
|
"uptime": uptime_data,
|
|
"uptime_kuma_url": UPTIME_KUMA_URL,
|
|
})
|
|
|
|
@app.get("/api/pbs", response_class=HTMLResponse)
|
|
async def api_pbs(request: Request):
|
|
"""HTMX endpoint for PBS status."""
|
|
pbs_status = await get_pbs_status(PBS_URL, PBS_API_TOKEN, PBS_API_SECRET)
|
|
return templates.TemplateResponse("partials/pbs.html", {
|
|
"request": request,
|
|
"pbs": pbs_status,
|
|
})
|
|
|
|
@app.get("/api/storage", response_class=HTMLResponse)
|
|
async def api_storage(request: Request):
|
|
"""HTMX endpoint for storage pools."""
|
|
pools = await get_storage_pools(PROXMOX_NODES, PROXMOX_API_TOKEN, PROXMOX_API_SECRET)
|
|
return templates.TemplateResponse("partials/storage.html", {
|
|
"request": request,
|
|
"pools": pools,
|
|
})
|
|
|
|
@app.get("/api/events", response_class=HTMLResponse)
|
|
async def api_events(request: Request):
|
|
"""HTMX endpoint for recent events."""
|
|
events = get_recent_events()
|
|
return templates.TemplateResponse("partials/events.html", {
|
|
"request": request,
|
|
"events": events[-10:],
|
|
})
|
|
|
|
@app.get("/api/camera-snapshot/{camera}")
|
|
async def camera_snapshot(camera: str):
|
|
"""Proxy camera snapshot from go2rtc."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
response = await client.get(f"{GO2RTC_URL}/api/frame.jpeg?src={camera}")
|
|
if response.status_code == 200:
|
|
return StreamingResponse(
|
|
iter([response.content]),
|
|
media_type="image/jpeg",
|
|
headers={"Cache-Control": "no-cache"}
|
|
)
|
|
except:
|
|
pass
|
|
return StreamingResponse(iter([b""]), media_type="image/jpeg")
|
|
|
|
@app.get("/api/status")
|
|
async def api_status_json():
|
|
"""JSON endpoint for all status data."""
|
|
services_status = await check_all_services(SERVICES)
|
|
nodes_status = await get_all_proxmox_metrics(PROXMOX_NODES, PROXMOX_API_TOKEN, PROXMOX_API_SECRET)
|
|
|
|
return {
|
|
"services": {name: {"status": s.status, "response_time_ms": s.response_time_ms}
|
|
for name, s in services_status.items()},
|
|
"nodes": [{"name": n.name, "status": n.status, "cpu": n.cpu_percent,
|
|
"memory": n.memory_percent, "disk": n.disk_percent, "uptime_hours": n.uptime_hours}
|
|
for n in nodes_status]
|
|
}
|
|
|
|
# Settings page
|
|
@app.get("/settings", response_class=HTMLResponse)
|
|
async def settings_page(request: Request):
|
|
"""Settings page."""
|
|
settings = load_settings()
|
|
return templates.TemplateResponse("settings.html", {
|
|
"request": request,
|
|
"settings": settings,
|
|
"all_services": SERVICES,
|
|
"categories": CATEGORIES,
|
|
})
|
|
|
|
@app.post("/settings")
|
|
async def save_settings_handler(request: Request):
|
|
"""Save settings."""
|
|
form = await request.form()
|
|
settings = load_settings()
|
|
|
|
# Update settings from form
|
|
settings["refresh_interval"] = int(form.get("refresh_interval", 30))
|
|
settings["theme"] = form.get("theme", "dark")
|
|
settings["show_response_times"] = form.get("show_response_times") == "on"
|
|
settings["show_icons"] = form.get("show_icons") == "on"
|
|
|
|
# Handle favorites (multi-select)
|
|
favorites = form.getlist("favorites")
|
|
if favorites:
|
|
settings["favorites"] = favorites
|
|
|
|
save_settings(settings)
|
|
return RedirectResponse(url="/settings?saved=1", status_code=303)
|
|
|
|
@app.get("/api/settings", response_class=JSONResponse)
|
|
async def get_settings_api():
|
|
"""Get settings as JSON."""
|
|
return load_settings()
|