Dashboard layout fix (CSP regression)
- The dashboard layout broke in v3.9.1–v3.9.3 because the Content-Security-Policy
style-src/script-srccarried a per-requestnonce. Per the CSP spec, when a nonce is present'unsafe-inline'is ignored, so every inlinestyle="..."attribute and JS-applied style was blocked by the browser. - Symptoms: tiny AQI number, unstyled AQI/temp/hum grid items, wrong air-raid map sizing, broken mini-graphs.
- Fix: dropped the nonce from
style-src/script-src(kept'unsafe-inline'), so inline styles/JS are allowed again — the dashboard now renders identically to v3.9.0. - Added
media-srcso the Web Push "ding" sound is permitted.
- Image rebuilt and published as
webyhomelab/power-safety-ua:3.9.4(and:3.9,:latest).
Performance, Reliability & Observability Improvements
- HTTP request duration & counters (power_http_requests_total, power_http_request_duration_seconds)
- Loop health gauge + restart counters (power_loop_health, power_loop_restarts_total)
- Telegram message/error counters (power_telegram_messages_total, power_telegram_errors_total)
- Schedule sync & air raid alert counters (power_schedule_syncs_total, power_air_raid_alerts_total)
- Metadata info gauge (version, role)
- monitor_loop, alerts_loop, schedule_loop, metrics_collector_loop
- Exponential backoff to 300s max on failure
- loop_health gauge: 1=healthy, 0=down/restarting
- Graceful shutdown via SIGTERM/SIGINT handlers (both API and worker containers)
- PRAGMA journal_mode=WAL — better concurrent performance
- PRAGMA synchronous=NORMAL — balance of speed and durability
- PRAGMA busy_timeout=5000 — 5s timeout for locked DB
- vacuum_db() support for periodic maintenance
- event_log_errors Prometheus counter
- Worker healthcheck: checks power_safety.db file existence
- stop_grace_period: 30s for both containers (graceful shutdown)
- Removed pids_limit (conflict with Docker Compose v2.40.3)
- Added localhost:5050 to default ALLOWED_ORIGINS
- All API endpoints now auto-measured for duration
- CircuitBreaker, ScheduleChangeDetection, SSRFBlocklist
- LoopBackoff, HealthEndpoints with new metrics
- Files changed: 11 (4 new, 7 modified)
- Lines added: 541, removed: 216
- Tests: 107/108 passing (1 pre-existing mock scope issue)
- Ruff: clean, Bandit: 0 new issues
Comprehensive Security, Architecture & PWA Improvements
- C1: Fix SafetyNetReactRequest regex to accept
down/techactions (safety-net UI was broken) - C2: Redact
secret_keyandadmin_tokenfrom admin API response (were leaking secrets) - C3: Add file lock for
event_log.jsonwrites in concurrent container scenario - C4: Migrate 6 blocking TelegramClient calls in webhook to async
_async_telegram_post - C5: Wrap
get_air_raid_alert()inasyncio.to_thread()in alerts loop - C6: Make
admin_service_restartDocker-aware - C7: Unify version across all sources
- Replace ~97
print()calls with structlog across 7 files - Add
ThreadPoolExecutor(max_workers=4)replacing 12threading.Threadspawns - Remove stale module-level credential snapshots, add lazy getters
- Add Python 3.13 matrix, bump coverage threshold 30→50
- Add paths-ignore, Trivy image scan, requirements-dev.txt
- Add anyio thread limiter config (100 threads)
- Remove
user-scalable=nofrom viewport (WCAG compliance) - Add
<noscript>fallback, GZipMiddleware - Fix manifest.json, add Cache-Control headers
- Add
/api/versionendpoint,notificationcloselistener
- Add
CHANGELOG.md,SECURITY.md,CODEOWNERS
- Total changes: 5 PRs (#109, #111, #113, #116, #118)
- Files changed: 19
- Tests: 90/90 passing (was 85)
- Ruff: clean, Bandit: 0 issues
Admin Panel Fix — Query Token Access Restored
- Fix: restored
?t=query-param support in/adminandcheck_admin_token(broken in v3.7.1) - Migrated admin.html all API calls from
?t=${token}toX-Admin-Tokenheader for security - Updated
telegram_bot_token_maskedreference in admin UI - Added missing
Queryimport
CI Fixes & Dependency Updates
- Fix CI:
tzdata==2026a→2026.2(PyPI compatibility) - Fix CI: synced
ruff==0.15.20across local/CI (version mismatch caused format drift) - Fix CI: restored
B404/B603/B607bandit skips (safe subprocess usage) - Bump: fastapi 0.135.3→0.139.0
- Bump: numpy 2.1.3→2.5.1
- Bump: uvicorn 0.34.0→0.50.2
- Bump: pydantic 2.13.0→2.13.4
- Bump: slowapi 0.1.1→0.1.10
- Bump: GitHub actions (checkout@v7, setup-python@v6, codeql-action@v4)
- Cleanup: 10 stale Dependabot branches removed
Security Hardening, Rate Limiting, Supply-Chain Security & Refactoring
- Webhook Secret Validation: Telegram webhook now validates
X-Telegram-Bot-Api-Secret-Tokenheader (C1) - Path Traversal Fix:
restore_backupsanitizes filenames viaos.path.basename+ dot detection (C2) - Token Masking: Admin API
/api/admin/datamasks bot token (****XXXX) instead of leaking it (C3) - Rate Limiting:
slowapiadded — all endpoints rate-limited (webhook 60/min, admin 30/min, push 10/min, etc.) (C4) - Admin Token Security: Removed query-parameter
?t=— onlyX-Admin-Tokenheader accepted;secrets.compare_digestused everywhere (H1, H2) - Pydantic Validation: 8 endpoints migrated from
dict = Body()to strict Pydantic models with regex patterns and bounds (H3) - SSRF Protection:
fetch_customblocklist now usesipaddressmodule, covers all private ranges (RFC 1918, cloud metadata169.254.169.254, IPv6) (H4) - Async HTTP: Replaced blocking
requests.postin webhook handler withhttpx.AsyncClient(H5)
security_opt: no-new-privileges:true,cap_drop: [ALL],pids_limit: 100added to docker-compose.dockerignorefixed:.venv/excluded (342MB savings), expanded exclusionsbootstrap.pyTOCTOU race condition fixed (os.O_CREAT | os.O_EXCL)- Trivy SARIF uploads to GitHub Security tab
- SLSA provenance + SBOM generation enabled
dependabot.yml(pip + docker + github-actions, weekly).pre-commit-config.yaml(ruff + pre-commit-hooks)- CI:
pull_requesttrigger, concurrency control,ruff format --check,pytest --cov --cov-fail-under=30 - Dockerfile: removed
dpkg --force-all --purge tar, added OCI labels, reduced workers 4→2
app/paths.py— centralized data path constantsapp/config_runtime.py— cached config access with 30s TTLapp/reports/package — extracted shared code from generate_*.pyapp/_version.py— single source of truth for versiondb.py—init_db()runs once (was called on every insert)- Hardcoded personal Telegram ID removed from all fallbacks
get_air_raid_alert()wrapped inasyncio.to_thread
requests2.33.0 → 2.34.2httpx0.27.0 → 0.28.1pydantic-settings2.2.1 → 2.7.0tzdata2025.1 → 2026aslowapi0.1.1 addedmatplotlib,numpy,pandaspinned to exact versions
- 85 tests (+14 new): safety_net state machine, SSRF parser, path traversal
- Webhook secret 403 rejection test, rate limit test, edge cases
- Lines changed: +3,259 −2,577
- Files: 39 changed, 14 new
- Ruff: clean
- Bandit: Low-severity only (pre-existing subprocess with literal args)