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
This commit is contained in:
265
app/main.py
Normal file
265
app/main.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user