render_excel(report): one worksheet per location — interval table, a line chart,
and a Last/Base/Δ summary per window. Metric-driven, so it tracks whatever metric
set is configured.
- orchestrator: render report.xlsx alongside report.html, attach it to the email
(dry-run until SMTP set), expose xlsx_path. Never lets a spreadsheet error sink
the report.
- reports router: /list includes xlsx_url when present; new
GET /archive/{date}/xlsx serves the saved spreadsheet.
- UI: Recent-reports rows get an "Excel" download link.
Verified: real Feb data -> valid .xlsx (sheet per NRL, interval table + chart +
summary with real values), attachment path runs, both archive routes registered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Terra-View side of the daily night-vs-baseline sound report for the John Myler
24/7 job. Engine is built and verified end-to-end against real meter data;
SMTP send + scheduler/capture wiring still pending.
- ingest: refactor upload_nrl_data into a callable ingest_nrl_zip(location_id,
zip_bytes, db) sharing one core with the HTTP endpoint. Capture the .rnh
percentile map + weightings into session metadata; dedup on store-name +
start time. Ingest stays metric-agnostic (every Leq column preserved).
- report_pipeline.py: metric registry, Evening/Nighttime windows, correct
aggregation (Lmax=max, Ln=arithmetic, Leq=logarithmic), baseline = typical
night, per-location + per-project builders.
- report_renderers.py: HTML email-body renderer (Last/Base/delta layout).
- report_email.py: config-driven SMTP via stdlib (env vars) with a dry-run
fallback so the pipeline runs without credentials.
- report_orchestrator.py: compute -> render -> always write report.html +
report.json to disk -> best-effort email.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>