Runbook & troubleshooting
Operational reference. Context in architecture; the API surface in usage.
Daily operation
# status
curl -s localhost:8420/api/v1/health
# {"status": "ok", "pending": 2}
# logs (docker)
docker compose logs -f festin
# restart
docker compose restart festin
Credentials
| Situation | Action |
|---|---|
| Empty database | POST /api/v1/auth/register — first user becomes admin |
| Populated database | only an existing admin can create users (same endpoint with Bearer) |
| Admin password lost | wipe users and re-bootstrap: DELETE FROM users; in the DB, restart, register |
| Viewer needs admin | admin flips the role in Users view (or PATCH /users/{id}) |
# manual bootstrap
curl -X POST localhost:8420/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"username": "admin", "password": "CAMBIAME"}'
Backup & restore
# SAFE backup while running (SQLite .backup API)
docker compose exec festin python -c \
"import sqlite3; sqlite3.connect('/data/festin.db').backup('/data/backup.db')"
docker compose cp festin:/data/backup.db ./festin-$(date +%F).db
# restore: stop service, replace file, start
docker compose stop festin
docker cp festin-2026-09-07.db festin:/data/festin.db
docker compose start festin
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 on register while logged in as admin | middleware exempting register (old bug) | verify /auth/register is NOT in JWTMiddleware.DEFAULT_EXEMPT_PATHS |
| Login fails in browser, curl works | stale token or residual register-mode in localStorage | localStorage.clear() + reload |
| Stale JS/CSS in the browser | asset cache | hard reload; assets are versioned ?v=N — bump on change |
Scan stuck in running | executor task died | check server logs; the row stays running (no watchdog yet) |
| Scheduled scans all fire after restart | last_run is in-memory | expected; first scheduler tick re-fires every schedule |
403 forbidden on a mutation | viewer role | log in as admin, or PATCH /users/{id} |
409 creating project/domain | name already exists (domains are globally unique) | pick another name |
| Cannot delete project id 1 | default project is protected | by design |
| Cannot delete a user | it's you, or the last admin | promote another admin first |
| passlib/bcrypt crash | passlib 1.7 vs bcrypt 4.x | auth.py uses bcrypt directly — don't reinstall passlib |
| aiohttp middleware error "not callable" | old-style middleware | use the _wrap_middleware() pattern in serve.py |
uv run fails parsing pyproject | [dependency-groups] TOML layout | keep dev = [...] as a table under [dependency-groups] |
| Tests hang | missing timeout | ALWAYS timeout 130 uv run pytest --timeout=30 -q |
Health & metrics
GET /api/v1/health → {"status": "ok", "pending": N} where pending = queued scan jobs (in-memory). Wire it to your uptime monitor; it's public by design.
For deeper metrics, watch the DB directly:
-- scans by status
SELECT status, COUNT(*) FROM scans GROUP BY status;
-- findings per day (last 7 days)
SELECT substr(s.started_at,1,10) day, severity, COUNT(*)
FROM findings f JOIN scans s ON s.id=f.scan_id
GROUP BY day, severity ORDER BY day DESC;
Reset a demo environment
Deployment checklist
FESTIN_JWT_SECRETset to a random value- demo DB deleted
- admin bootstrapped with a strong password
- TLS in front (proxy/ingress)
/auth/loginrate-limited at the proxy- backups scheduled (
.backupmethod) curl /api/v1/healthwired to monitoring