84 tests in collectors/tests, run with Invoke-CollectorTests.ps1 (~15 s, PowerShell 7.2+ and Pester 5.5+): - Module.Tests: the shared module (property lookup, time format, config/lookback, sent-run state and pruning, dry runs, exit codes, 401 detection) - Veeam/Mars/Wsb/Vb365.Tests: each collector run in its own process against its mock and a stand-in ingest API, asserting the behaviours confirmed on real servers: VBR 13 live schedule, copy jobs, MARS UTC times and ErrorCode 0, WSB culture-formatted times and nested VMs, VB365 organization mapping, log prefixes and retries, state and resending, rejected keys - Installers.Tests: config, files and scheduled task from each installer, with stand-ins for icacls / Task Scheduler / the key prompt Collectors run under the test runner's PowerShell, or another shell via -CollectorShell (e.g. powershell.exe, to test under Windows PowerShell 5.1 like the MARS, WSB and VB365 servers). The mocks take their "now" from MOCK_NOW so expectations can't drift with the clock; the WSB mock gains -RunningJobFinished for the running-to-finished case. Each of 12 past bugs, put back one at a time, fails the test for its behaviour (and the runner exits 1). Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.5 KiB
Backup-Dashboard
Proof of concept backup dashboard, intended to display backup jobs from multiple sources.
collectors ──POST /api/v1/ingest (API key)──▶ ┌──────────────┐
│ FastAPI + │
browser ──nginx (TLS)──▶ /api/* ─────────────▶│ SQLite │
│ └─ static files: public/ └──────────────┘
└─ auth_request ─▶ oauth2-proxy ─▶ Microsoft Entra ID (Microsoft 365 sign-in)
public/: static frontend (no build step). Add?mock=1to the URL to use built-in sample data.backend/: FastAPI API backed by a single SQLite file.deploy/: nginx site and systemd unit.collectors/: PowerShell scripts that report results from backup products (Veeam B&R, Azure Backup / MARS, Windows Server Backup, Veeam Backup for M365). Code they share (config, logging, posting, state, installing) is incollectors/common/BackupDashboard.Collector.psm1.
Data model
- Tenant (company) → Job → Run.
- A job is keyed by
(tenant, source_system, job.external_id), so renaming a job at the source updates it instead of creating a duplicate. - A run has
status(running|success|warning|failed),started_atandfinished_at. The dashboard's Last Run is the most recentfinished_at. While a job is running it shows Running, and Last Run still shows when the previous run finished. - Runs older than
BACKUP_DASH_RETENTION_DAYS(default 90, minimum 30) are purged every 6 hours.
Reporting results (collectors)
Each collector (a script on a Veeam server, a webhook relay, etc.) gets its own API key:
cd backend && python manage.py create-key veeam-acme
Then it POSTs one report per run. Send running when a run starts (optional) and the final status
when it ends. Reports with the same started_at (or run.external_id) update the same run, and
retries are safe.
curl -X POST https://backups.example.com/api/v1/ingest \
-H "Authorization: Bearer bdk_..." -H "Content-Type: application/json" \
-d '{
"tenant": "Acme Corp",
"source_system": "Veeam B&R",
"job": { "external_id": "4f1c…", "name": "File Server – Nightly", "next_run_at": "2026-10-05T22:00:00-04:00" },
"run": {
"status": "success",
"started_at": "2026-10-04T22:00:05-04:00",
"finished_at": "2026-10-04T22:47:12-04:00",
"message": "Job completed successfully.",
"size_bytes": 128849018880,
"details": { "repository": "REPO-01" }
}
}'
- All timestamps must include a timezone offset (
Get-Date -Format oin PowerShell does this). job.next_run_atcontrols the Overdue flag. If you omit it, the stored value is kept. If you sendnull, it's cleared.runcan be omitted to only register or update a job.- Unknown fields are rejected (HTTP 422), so typos in collector scripts fail loudly.
API (read)
| Endpoint | Returns |
|---|---|
GET /api/v1/jobs |
All jobs with current status, running_since, last_run_at, next_run_at |
GET /api/v1/jobs/{id} |
One job, plus the last finished run's duration, size, message and details |
GET /api/v1/jobs/{id}/runs?days=30 |
Run history (no messages), newest first |
GET /api/v1/runs/{id} |
One run including message and details |
GET /api/v1/health |
{"status": "ok"} |
The OpenAPI schema is at /api/v1/openapi.json.
Authentication
- Dashboard users: Microsoft 365 sign-in. nginx checks every request with oauth2-proxy, which signs users in with Microsoft Entra ID. Who may sign in is controlled in Entra (users or a group assigned to the app). Every signed-in user can see every tenant. Sessions last 12 hours; signing in again is usually silent. The dashboard shows who's signed in; Sign out also signs out of Microsoft in that browser, otherwise the next visit would sign straight back in.
- Collectors: per-collector bearer keys, checked by the app. Only a SHA-256 hash is stored.
To revoke one, run
python manage.py revoke-key NAME./api/v1/ingestis the only path that doesn't need Microsoft sign-in.
Microsoft 365 sign-in setup
In the Microsoft Entra admin center (entra.microsoft.com), as an admin of your tenant:
- App registrations → New registration. Name:
Backup Dashboard. Supported account types: Accounts in this organizational directory only (single tenant). Redirect URI: platform Web,https://<your dashboard host>/oauth2/callback. Register. - On its Overview, note the Application (client) ID and Directory (tenant) ID.
- Certificates & secrets → New client secret. Copy the secret's Value now (it's only shown once). Note its expiry date: sign-in stops working when it expires, so set a reminder to create a new one before then.
- Enterprise applications → Backup Dashboard → Properties: set Assignment required to Yes. Then Users and groups → Add user/group and add the people (or a group) who should see the dashboard. Everyone else in the tenant is refused by Microsoft before reaching it.
No API permissions beyond the default User.Read are needed.
On the dashboard server:
- Install oauth2-proxy 7.8 or newer from its
releases as
/usr/local/bin/oauth2-proxy, and create its user:sudo useradd --system --no-create-home oauth2-proxy. - Install the config and the client secret, readable only by root and oauth2-proxy:
In
sudo install -d -m 750 -g oauth2-proxy /etc/oauth2-proxy sudo install -m 640 -g oauth2-proxy deploy/oauth2-proxy.cfg /etc/oauth2-proxy/backup-dashboard.cfg sudo sh -c 'umask 027; printf %s "<client secret value>" > /etc/oauth2-proxy/client-secret' && sudo chgrp oauth2-proxy /etc/oauth2-proxy/client-secret/etc/oauth2-proxy/backup-dashboard.cfg, fill in<TENANT_ID>,<APPLICATION_CLIENT_ID>,<DASHBOARD_HOST>, and<COOKIE_SECRET>, generated with:openssl rand -base64 32 | tr -- '+/' '-_' - Install
deploy/oauth2-proxy.serviceinto/etc/systemd/system/, thensudo systemctl daemon-reload && sudo systemctl enable --now oauth2-proxy. Check it withjournalctl -u oauth2-proxy -n 20: it should say "OAuthProxy configured for Microsoft Entra ID". - Install the new
deploy/nginx.conf(keeping your server name and certificate paths), thensudo nginx -t && sudo systemctl reload nginx. Basic auth and its htpasswd file are no longer used.
Troubleshooting: sign-in problems show in journalctl -u oauth2-proxy. Microsoft's own
errors (wrong redirect URI, user not assigned, expired secret) appear on its sign-in page with an
AADSTS… code.
Development
cd backend
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
pytest
python manage.py --create seed-demo # 30 days of fake history in ./backup-dashboard.db
BACKUP_DASH_SERVE_STATIC=1 uvicorn --factory app.main:create_app --reload
# → http://localhost:8000
Collector tests (PowerShell 7 + Pester; see collectors/tests):
./collectors/tests/Invoke-CollectorTests.ps1
Deployment (Linux, nginx)
- Copy the repo to
/opt/backup-dashboard, create a venv at/opt/backup-dashboard/venv, and runpip install -r backend/requirements.txt. - Copy
deploy/backup-dashboard.envto/etc/backup-dashboard/backup-dashboard.env. Both the service andmanage.pyread their settings from it. - Create a
backupdashsystem user, then installdeploy/backup-dashboard-api.serviceinto/etc/systemd/system/and enable it. The database is created on first start. - Copy
public/to/var/www/backup-dashboard/public, installdeploy/nginx.conf, get a certificate with certbot, and set up Microsoft 365 sign-in (see Microsoft 365 sign-in setup). - Run
manage.pyas the service user. It finds the database through the env file:It prints the database path it's using and won't create a new database unless you passcd /opt/backup-dashboard/backend sudo -u backupdash /opt/backup-dashboard/venv/bin/python manage.py create-key veeam-acme--create.
Back up the SQLite file with sqlite3 backup-dashboard.db ".backup copy.db". Don't use a plain cp
while the service is running.