Files
homelab-dashboard/app/main.py
chrisryn c952a3b56e Initial commit: Homelab Infrastructure Dashboard
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
2026-01-30 21:03:25 -06:00

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()