No description
  • JavaScript 72.7%
  • CSS 18.3%
  • HTML 4.7%
  • Python 2.6%
  • Dockerfile 0.9%
  • Other 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-23 11:03:58 +02:00
api Show llama.cpp router load states; unload the running model 2026-09-23 11:03:58 +02:00
icons Add TimeToLeave service to the stack with departures widget 2026-09-07 13:28:43 +02:00
time_to_leave@2b7ac76eb0 Track time_to_leave as a nested repository 2026-09-07 13:28:48 +02:00
.dockerignore Add backend data aggregator and API proxy services 2026-09-04 14:07:01 +02:00
.gitignore Add Outlook unread-mail widget with device-code login 2026-09-04 17:38:17 +02:00
apple-touch-icon.png Add terminal-style favicon (feathericon) and fix missing COPY in Dockerfile 2026-09-07 09:51:47 +02:00
docker-compose.yml Add time-tracking widget with monthly per-project hours 2026-09-23 10:16:13 +02:00
docker-entrypoint.sh Add TimeToLeave service to the stack with departures widget 2026-09-07 13:28:43 +02:00
Dockerfile Add TimeToLeave service to the stack with departures widget 2026-09-07 13:28:43 +02:00
favicon.png Add terminal-style favicon (feathericon) and fix missing COPY in Dockerfile 2026-09-07 09:51:47 +02:00
favicon.svg Add terminal-style favicon (feathericon) and fix missing COPY in Dockerfile 2026-09-07 09:51:47 +02:00
gpu_stats.py Add GPU stats server for AMD ROCm 2026-09-04 14:06:49 +02:00
index.html Add time-tracking widget with monthly per-project hours 2026-09-23 10:16:13 +02:00
nginx.conf Add llama.cpp servers with unload buttons to the GPU widget 2026-09-23 09:37:09 +02:00
README.md Show llama.cpp router load states; unload the running model 2026-09-23 11:03:58 +02:00
script.js Show llama.cpp router load states; unload the running model 2026-09-23 11:03:58 +02:00
style.css Add time-tracking widget with monthly per-project hours 2026-09-23 10:16:13 +02:00

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: TimeToLeave, 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), TimeToLeave (today's upcoming events with live train/bike leave-by Outlook (unread mail), Netbird status (reachability + latency), Time tracking (current month's per-project hours from the time-track server), Git activity (recent commits from Gitea and GitLab — 3 rows visible, scroll for more), GPU + Ollama + llama.cpp (live AMD ROCm stats: utilization, VRAM, temperature, power; plus loaded Ollama models with their GPU/CPU split, and each llama.cpp server's loaded model, slots, unload button and offline state)
  • 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 real online status (server-side health checks every minute: green = online, amber = 5xx error, red = unreachable; hover the badge for latency/details)
  • 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)
GITLAB_PROXY Forward proxy (HTTP CONNECT) for all GitLab requests — git stats and the online badge (set in .env). Change the URL when the proxy stack moves; unset = direct connection. With the proxy on the docker host use http://host.docker.internal:8118 (the container resolves host.docker.internal via extra_hosts)
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)
LLAMACPP_URLS / LLAMACPP_API_KEY llama.cpp servers shown in the GPU widget — comma-separated list (default: http://100.103.83.12:8850,http://100.103.83.12:8087,http://100.103.83.12:8880); offline servers are shown as such. A single LLAMACPP_URL still works. The servers require an API key for /props + /metricsset LLAMACPP_API_KEY in .env (shared by all servers; the KV-cache/requests rows are omitted when /metrics is unavailable)
TIMETRACK_URL / TIMETRACK_TOKEN Time-track server feeding the time-tracking widget (default: http://100.103.83.12:3008). Set TIMETRACK_TOKEN in .env — it is the server's SYNC_API_TOKEN (bearer)
OUTLOOK_CLIENT_ID Entra ID app client ID for the unread-mail widget (set in .env)
OUTLOOK_TENANT Azure tenant (default: organizations; set to your Directory (tenant) ID if the generic endpoint returns AADSTS50059)
DEPLOYMENT_URL TimeToLeave public base URL, used for Google OAuth redirects (default: http://100.103.83.12:3300)
CORS_ALLOWED_ORIGINS TimeToLeave: comma-separated origins allowed to call its /api/* routes (default: the deployment URL + http://localhost:3300)
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET TimeToLeave Google Calendar sync — optional, unset disables the Google tab (set in .env)
TIMETOLEAVE_URL TimeToLeave backend used by the departures widget updater (default: http://time-to-leave:3000, the compose service)
TIMETOLEAVE_ORIGIN_EXT_ID / TIMETOLEAVE_ORIGIN_NAME / TIMETOLEAVE_BUFFER_MINUTES Departure widget origin station + arrival buffer (defaults: Mödling Bahnhof 1231701, 5 min)
TIMETOLEAVE_ORIGIN_LAT / TIMETOLEAVE_ORIGIN_LNG Departure widget bike start coordinates (default: the TimeToLeave origin address in Mödling)

Outlook unread mail

The Outlook widget (and the unread count) uses the Microsoft Graph API with a one-time device login. Setup:

  1. Register an app in Microsoft Entra ID (portal.azure.com → Entra ID → App registrations → New registration): any name (e.g. "Homelab Dashboard"), account type "Accounts in this organizational directory only", no redirect URI.
  2. In the app: Authentication → Allow public client flows → Yes, then API permissions → Add → Microsoft Graph → Delegated → Mail.Read.
  3. Copy the Application (client) ID — from App registrations → your app → Overview. Beware: this is not the "Directory (tenant) ID" and not the "Object ID". A valid-format but wrong GUID produces AADSTS50059 ("No tenant-identifying information found"). Put it in .env as OUTLOOK_CLIENT_ID=.
  4. Set OUTLOOK_TENANT in .env to your Directory (tenant) ID. Some directories are not resolvable through the generic organizations endpoint (also AADSTS50059 with a valid client ID) — the tenant-specific endpoint always works.
  5. docker compose up -d --build
  6. Log in once:
    docker exec -it homelab-dashboard node /opt/dashboard/outlook-auth.js
    
    Open the printed URL, enter the code and sign in with your work account. The refresh token is persisted in the outlook-tokens volume.

api/update-outlook.js then refreshes unread mail every minute (caching the access token, and re-running outlook-auth.js is only needed if login is revoked). If your tenant requires admin consent, ask your admin to grant it for Mail.Read.

TimeToLeave

The time-to-leave compose service builds the Next.js web app from time_to_leave/ (npm-workspaces monorepo; only apps/web and packages/ are used in the build — mobile/native dirs are pruned via its .dockerignore). It is published on port 3300 (3000/3001 are taken on this host) and appears as the first card on the dashboard.

The app validates DEPLOYMENT_URL / CORS_ALLOWED_ORIGINS at build time (see time_to_leave/apps/web/next.config.ts) and reads them again at runtime, so they are set both as build args and container environment. Transit backends (ÖBB HAFAS, Nominatim, OSRM, Wiener Linien, GTFS) have built-in defaults.

The TimeToLeave departures widget (top widget row, between Today and Outlook) is fed by api/update-timetoleave.js (cron, every 5 min): it takes today's upcoming events from data.json and asks the TimeToLeave app's /api/departure endpoint (added in time_to_leave/apps/web/src/app/api/departure/route.ts) for the latest train connection and the bike option that still arrive in time — the same journey-picking logic the app itself uses. Events show the train connection (departure, changes, delay, final walk) plus a 🚲 by bike line with distance, duration and its own leave-by time; bikeable events without any transit connection fall back to a bike-only chip. The browser only renders timetoleave.json and ticks the countdowns; all HAFAS/geocoding/routing stays inside the TimeToLeave app.

Note: the Google Calendar "Connect" flow sets Secure cookies in production, so it only works when the app is reached over HTTPS (plain HTTP drops the calendar token). ICS URL/file import is unaffected.

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"].

llama.cpp servers + unload button

The llama.cpp block in the GPU widget shows one row per configured server (LLAMACPP_URLS, comma-separated) with one card per model read from GET /models, including the load state: loaded (with port, slots, KV cache, busy requests and an Unload button — two clicks to confirm), 💤 sleeping (--sleep-idle-seconds: auto-unloaded after idle, wakes on the next request), unloaded and loading. Offline servers are displayed as such instead of hiding them.

The button calls POST /models/unload on the selected server, which only exists in router server mode (--models-dir instead of --model; --models-preset keeps per-model parameters, --models-autoload reloads models on demand). With a plain --model start the endpoint is not registered (HTTP 404) and the widget shows a hint instead. Router mode and --sleep-idle-seconds combine fine — the router passes the idle timer to each per-model child process.

The llm-owui service in the Open WebUI stack runs in router mode: model file bind-mounted as models-router/qwen3.8.gguf (keeps the Open WebUI model ID qwen3.8), per-model parameters in llamacpp-router/preset.ini, 10 min idle sleep.

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

  • Online badges are real health checks: api/update-health.js (cron, every minute) pings each service URL from the container with a 5 s timeout and writes health.json; the frontend refreshes badges every 30 s. Any HTTP response below 500 counts as online (401/404 still means the server is up); 5xx shows as "Error", timeouts/DNS failures as "Offline". TLS certificate validity is ignored (reachability only).
  • Calendar dependencies are bundled in api/node_modules so the Docker build works offline. To update them, run npm install in the api/ directory.
  • Port 8888 is published by the Compose file; change it there if needed.