- Add melange.yaml for distroless package build - Add apko.yaml for minimal container image - Add GitHub Actions workflow for melange/apko CI build - Add Kubernetes manifests (deployment, service, configmap, secret, redis) - Update Dockerfile, .gitignore, and docker-compose.yml
APC UPS Dashboard
A production-ready FastAPI + Redis dashboard for monitoring multiple APC UPS devices via apcupsd NIS. Single-admin auth (argon2 + session cookies), CSRF, rate limiting, security headers, SSE live updates, 7-day history, connection-health tracking, battery degradation trends, HTML email alerts with batching and silent-hours, and Prometheus metrics.
Features
Monitoring
- Multiple UPS managed dynamically via UI/REST (stored in Redis)
- Real-time dashboard with SSE, fleet-overview panel, per-UPS state indicators
- 7-day snapshot history, per-minute watts averages, daily energy + cost
- Connection-health tracker — COMMLOST detection after 3× interval, recovery INFO alerts
- Battery-health sampling (1/min) with 7-day trend + decline %
- CSV export (history/events/energy), event log, alert management + ack
Alerts (email-only)
- Severity taxonomy: CRITICAL (ONBATT/COMMLOST/RUNTIME_LOW/BCHARGE_LOW), WARNING (LOAD_HIGH/REPLACEBATT/SELFTEST_FAIL/TEMP_HIGH/XFER_BURST/VOLT_DEV), INFO (REACHABLE/LINE_RESTORED)
- Coalesced HTML emails (one per UPS per poll cycle), severity-colored
- 30-min per-message cooldown, silent-hours deferral (CRITICAL always delivered), retries via tenacity
- Test-email button and optional daily summary
Security & ops
- Single-admin auth (argon2 password, signed session cookie, double-submit CSRF)
- SSRF host validation (rejects loopback/link-local; private IPs gated by
ALLOW_PRIVATE_IPS) - SMTP password only from env — never persisted to Redis
- Subprocess timeout on
apcaccess(10s), rate-limiting on auth/config, security headers + CSP - Non-root container (UID 10001), pinned
python:3.12.7-slimmulti-stage build /healthz,/readyz,/metrics(Prometheus), JSON structured logs with request-ID correlation- GitHub Actions pipeline runs ruff + pytest before building/publishing the image
Quick start (docker-compose)
cp .env.example .env
# Required in production — edit .env:
# SESSION_SECRET=<run: python -c "import secrets; print(secrets.token_urlsafe(48))">
# SMTP_PASSWORD=<your SMTP password, if using alerts>
docker compose up --build
Open http://localhost:10280 — the app redirects to /setup where you create the admin account on first boot. Subsequent visits require login.
Environment variables
| Var | Required | Default | Notes |
|---|---|---|---|
SESSION_SECRET |
yes (prod) | ephemeral | 32+ random bytes, signs session cookies |
ADMIN_USERNAME |
no | admin |
|
ADMIN_PASSWORD_HASH |
no | unset | Skip setup wizard by pre-seeding an argon2 hash |
REDIS_URL |
no | redis://redis:6379/0 |
Supports redis://:pw@host:port/db |
SMTP_PASSWORD |
no | unset | Read only from env, never stored |
ALLOW_PRIVATE_IPS |
no | true |
Set false to block RFC1918 UPS hosts |
TRUST_PROXY |
no | false |
Enable Secure cookies + HSTS when behind HTTPS proxy |
LOG_LEVEL |
no | INFO |
DEBUG/INFO/WARNING/ERROR |
SESSION_MAX_AGE_SECONDS |
no | 1209600 |
14 days |
RATE_LIMIT_ENABLED |
no | true |
Disable in tests |
TZ |
no | UTC |
Generate values:
# SESSION_SECRET
python -c "import secrets; print(secrets.token_urlsafe(48))"
# ADMIN_PASSWORD_HASH (optional pre-seed)
python -c "from passlib.hash import argon2; print(argon2.hash('mysecret'))"
Heads-up —
$escaping in.env: argon2 hashes look like$argon2id$v=19$m=...$<salt>$<hash>. Docker Compose treats$wordas variable substitution in.env, so pasting a raw hash produces warnings (The "argon2id" variable is not set…) and the container receives a mangled, unverifiable hash — you won't be able to log in. Quoting does not help. Either double every$($$argon2id$$v=19$$m=...), or skip this variable entirely and use the/setupwizard (recommended).
apcupsd server requirements
Your remote APC UPS hosts must run apcupsd with the Network Information Server (NIS) enabled:
- Install:
sudo apt-get install apcupsd - Edit
/etc/apcupsd/apcupsd.conf:UPSTYPE usb NISIP 0.0.0.0 NISPORT 3551 NETSERVER on - Restart:
sudo systemctl restart apcupsd - Test:
apcaccess statusandnc -vz <server-ip> 3551
Development
python3.12 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
# Run tests (uses fakeredis)
pytest tests/
# With coverage
coverage run -m pytest tests/ && coverage report
# Lint
ruff check .
# Local dev server (needs apcaccess binary + REDIS_URL env)
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
Persistence
Config and history live in Redis via an AOF-backed redis-data volume. To wipe state:
docker compose down && docker volume rm apcupsd-client_redis-data
API (summary)
| Path | Auth | Notes |
|---|---|---|
/ (dashboard), /config, /events, /alerts, /settings |
session | Jinja pages |
/login, /setup, /logout |
open/session | |
/healthz, /readyz, /metrics |
open | Prom metrics on /metrics |
GET /api/stream |
session | SSE snapshots |
GET /api/ups, /api/ups/{name}/{status|history|events|energy|health|battery_health} |
session | |
GET /api/ups/fleet/overview |
session | Aggregate fleet summary |
GET /api/ups/{name}/export?format=csv&kind=history|events|energy&since_days=N |
session | Streaming CSV |
GET /api/events, /api/alerts, /api/alerts/active |
session | |
POST /api/alerts/{id}/ack |
session+CSRF | |
GET/POST/PUT/DELETE /api/config/{ups,smtp,ui}/... |
session (+CSRF for writes) | Rate-limited |
License
MIT