Initial commit: Homelab Dashboard with YAML configuration

Features:
- Service health monitoring with response times
- Proxmox cluster integration (nodes, VMs, containers)
- PBS backup server monitoring
- Camera viewer with WebRTC (go2rtc)
- Docker container monitoring
- Uptime Kuma integration
- Mobile-friendly responsive design
- YAML-based configuration for easy setup
This commit is contained in:
Dashboard
2026-02-02 20:27:05 +00:00
commit 89cdb022f3
25 changed files with 2437 additions and 0 deletions

298
app/main.py Normal file
View File

@@ -0,0 +1,298 @@
"""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
@app.post("/api/webrtc")
async def webrtc_proxy(request: Request, src: str):
"""Proxy WebRTC offers to go2rtc"""
try:
body = await request.body()
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{GO2RTC_URL}/api/webrtc?src={src}",
content=body,
headers={"Content-Type": "application/sdp"}
)
from fastapi import Response
return Response(
content=response.content,
status_code=response.status_code,
media_type="application/sdp"
)
except Exception as e:
from fastapi import Response
return Response(content=str(e), status_code=500)
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("/cameras", response_class=HTMLResponse)
async def cameras_page(request: Request):
"""Full camera viewer page - mobile friendly."""
from datetime import datetime
return templates.TemplateResponse("cameras.html", {
"request": request,
"cameras": CAMERAS,
"go2rtc_url": GO2RTC_URL,
"now": int(datetime.now().timestamp()),
})
@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()