- Apply GTK-like color scheme (dark greys, #f76b2f accent) across cards, buttons, glows and category colors - Service cards use the official logo icons; Netbird card shows live reachability + latency instead of a separate top widget - Top widgets: Today (calendar events with in-progress highlighting), Git activity (3 rows visible, scrollable), GPU with live ROCm stats and loaded Ollama models incl. GPU/CPU split - Open WebUI card: inline prompt line with model selection - Calendar section grouping events by day below the services - data.json fetch with boot retry; GPU/Ollama poll every 5 s - Today entries wrap long titles; README documents widgets, environment variables and the .env secrets pattern
Homelab Dashboard
A small, self-contained single-page dashboard that links all your homelab services. Static site (HTML + CSS + JS), served by a lightweight nginx image. The optional calendar widget needs the container backend to refresh Google/ICS feeds, but the rest of the dashboard is pure static files.
Services
Internal: Open WebUI, ComfyUI, Gitea, Jellyfin, Immich, Home Assistant, Audio Server / Dubplate, Nextcloud, Vaultwarden, Nginx Proxy Manager, Netbird.
External: gitlab-ixsol, gem360.
Features
- Top widgets: Today (calendar events for today, with in-progress highlighting), Netbird status (reachability + latency), Git activity (recent commits from Gitea and GitLab — 3 rows visible, scroll for more), GPU + Ollama (live AMD ROCm stats: utilization, VRAM, temperature, power; plus loaded Ollama models with their GPU/CPU split)
- Inline Open WebUI prompt line with model selection in the Open WebUI card
(requires
OPENWEBUI_TOKEN) - Responsive grid of service cards with icons, categories and a live "Online" indicator
- Open (new tab) + Copy URL per service
- Live search / filter across names, categories and URLs
- Live local clock
- Calendar section aggregating multiple ICS feeds (refreshed every 15 min in the container)
- Dark, themeable design; respects
prefers-reduced-motion
Run with Docker
docker compose up -d --build
Then open http://localhost:8888.
The dashboard is static, but the widgets are populated by a small Node.js updater
(api/update-data.js) that runs inside the container and aggregates ICS feeds,
Netbird status, git activity and Open WebUI models every 15 minutes. A second
Node process (api/server.js) proxies chat prompts to Open WebUI.
Environment variables
Secrets (API keys) live in .env (gitignored) — docker compose reads it
automatically. Non-secret settings go directly in docker-compose.yml:
| Variable | Purpose |
|---|---|
TZ |
Timezone used for the "Today" widget (e.g. Europe/Vienna) |
OPENWEBUI_TOKEN |
Open WebUI API key — enables the prompt line + model list (set in .env) |
GITEA_TOKEN |
Gitea API token — private repo stats (public repos work without it) |
GITLAB_TOKEN |
GitLab API token — private project stats (public projects work without it) |
GITEA_URL / GITLAB_URL / NETBIRD_URL / OPENWEBUI_URL |
Override the default service URLs |
GPU_STATS_URL |
GPU stats upstream (default: the gpu-stats compose service) |
OLLAMA_URL |
Ollama instance for loaded-model stats (default: http://100.103.83.12:11435) |
GPU stats service
The gpu-stats compose service runs gpu_stats.py, a tiny HTTP
server that queries rocm-smi. It needs the host's ROCm install and GPU device
nodes, so the compose file mounts /opt/rocm and passes /dev/kfd + /dev/dri.
Set GPU_NAME in docker-compose.yml to your card's name (rocm-smi can't read it
inside the container without libdrm_amdgpu). The widget polls it every 5 s
through /api/gpu-stats.
To run it on the host instead: python3 gpu_stats.py, set
GPU_STATS_URL=http://host.docker.internal:9101/stats on the dashboard service
and add extra_hosts: ["host.docker.internal:host-gateway"].
Without Docker (quick dev)
Any static file server works for the dashboard itself, but the widgets will show errors because the data fetcher and chat proxy run server-side:
# from this directory
python3 -m http.server 8888
# or: npx serve . -l 8888
Edit services
All services live in one array at the top of script.js.
Add / remove / reorder entries there:
{ section: "internal", name: "My Service", url: "http://10.0.0.5:9000", cat: "infra", icon: "⚙️" }
section is either "internal" or "external".
cat is any key from the CATEGORIES map (or add your own).
Build the image manually
docker build -t homelab-dashboard .
docker run -d --name homelab-dashboard -p 8888:80 --restart unless-stopped homelab-dashboard
Notes
- The "Online" badge is cosmetic (a static site can't probe your services from the browser due to CORS). If you want real health checks later, a tiny backend that pings each URL would be the next step.
- Calendar dependencies are bundled in
api/node_modulesso the Docker build works offline. To update them, runnpm installin theapi/directory. - Port 8888 is published by the Compose file; change it there if needed.