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

149 lines
6.6 KiB
Markdown

# 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
```bash
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.
4. `docker compose up -d --build`
5. Log in once:
```bash
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`](./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:
```bash
# 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`](./script.js).
Add / remove / reorder entries there:
```js
{ 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
```bash
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.