kyleandClaude Opus 5.5 c5fe69a813 Add a Pester test suite for the collectors
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]>
2026-10-08 13:36:27 -06:00
2026-10-05 03:57:48 +00:00

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=1 to 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 in collectors/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_at and finished_at. The dashboard's Last Run is the most recent finished_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 o in PowerShell does this).
  • job.next_run_at controls the Overdue flag. If you omit it, the stored value is kept. If you send null, it's cleared.
  • run can 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/ingest is 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:

  1. 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.
  2. On its Overview, note the Application (client) ID and Directory (tenant) ID.
  3. 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.
  4. 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:

  1. 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.
  2. Install the config and the client secret, readable only by root and oauth2-proxy:
    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
    
    In /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 -- '+/' '-_'
  3. Install deploy/oauth2-proxy.service into /etc/systemd/system/, then sudo systemctl daemon-reload && sudo systemctl enable --now oauth2-proxy. Check it with journalctl -u oauth2-proxy -n 20: it should say "OAuthProxy configured for Microsoft Entra ID".
  4. Install the new deploy/nginx.conf (keeping your server name and certificate paths), then sudo 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)

  1. Copy the repo to /opt/backup-dashboard, create a venv at /opt/backup-dashboard/venv, and run pip install -r backend/requirements.txt.
  2. Copy deploy/backup-dashboard.env to /etc/backup-dashboard/backup-dashboard.env. Both the service and manage.py read their settings from it.
  3. Create a backupdash system user, then install deploy/backup-dashboard-api.service into /etc/systemd/system/ and enable it. The database is created on first start.
  4. Copy public/ to /var/www/backup-dashboard/public, install deploy/nginx.conf, get a certificate with certbot, and set up Microsoft 365 sign-in (see Microsoft 365 sign-in setup).
  5. Run manage.py as the service user. It finds the database through the env file:
    cd /opt/backup-dashboard/backend
    sudo -u backupdash /opt/backup-dashboard/venv/bin/python manage.py create-key veeam-acme
    
    It prints the database path it's using and won't create a new database unless you pass --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.

S
Description
Proof of concept backup dashboard, intended to display backup jobs from multiple sources
Readme
395 KiB
Languages
PowerShell 52.5%
Python 19.1%
JavaScript 18.1%
CSS 5.9%
HTML 4.4%