b4b703ceeeac6d8401f5d3256ba3b52e454f56a2
The merge of add-backend-api into main committed unresolved conflicts
and corrupted uploads; the API could not import on main.
- backend/app/settings.py, deploy/backup-dashboard-api.service,
collectors/veeam/README.md: remove conflict markers, restoring the
add-backend-api versions
- backend/app/__init__.py, backend/tests/__init__.py: were 1024 null
bytes ("source code string cannot contain null bytes"); now empty
- Remove the accidental Backup-Dashboard gitlink at the repo root and a
duplicate collectors/veeam/MockVeeam.ps1 (identical to tests/)
- Restore the 60-minute collector interval from 17ee303, which the
merge reverted to 15, and update the docs to match
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 (basic auth, TLS)──▶ /api/* ─▶│ SQLite │
└─ static files: public/ └──────────────┘
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/: scripts that report results from backup products (Veeam B&R).
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: nginx basic auth over HTTPS (
deploy/nginx.conf). Every user can see every tenant. Planned: Microsoft 365 sign-in through oauth2-proxy with the Entra ID provider and nginxauth_request. That's an nginx-only change. The frontend already tells the user to reload when a session expires (HTTP 401). - 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.
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
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 create users withhtpasswd -B. - 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.
Description
Proof of concept backup dashboard, intended to display backup jobs from multiple sources
395 KiB
Languages
PowerShell
52.5%
Python
19.1%
JavaScript
18.1%
CSS
5.9%
HTML
4.4%