Files
dashboard/README.md
T
fegger dfe246b7ca Add Outlook unread-mail widget with device-code login
- outlook-auth.js: one-time device-code flow against Microsoft login;
  stores the refresh token in the outlook-tokens compose volume
- update-outlook.js: per-minute cron fetching the unread count and the
  latest unread messages via Microsoft Graph; caches access tokens and
  persists rotated refresh tokens
- Widget is hidden until the mailbox is actually authorized, so it
  stays out of sight while (admin) consent is pending
- New external card for Outlook (Office 365) with the official icon
- compose: pass OUTLOOK_CLIENT_ID/OUTLOOK_TENANT/GITEA_TOKEN from .env
- README: Entra ID app registration steps and the tenant-ID hint for
  directories not resolvable via the organizations endpoint
2026-09-04 17:38:17 +02:00

6.6 KiB

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

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.

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

  • 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.