feat(reports): per-project report config + automatic morning run

Add SoundReportConfig (one row per project) + the scheduler tick that runs the
nightly report on its own:
- model SoundReportConfig (enabled, report_time, metric_keys, baseline range,
  recipients, last_run_date) — new table, auto-created by create_all (no migration).
- GET/PUT /api/projects/{id}/reports/config with validation.
- SchedulerService.run_due_reports(): each loop, for every enabled config past
  its report_time, run last night's report once (dedup via last_run_date),
  writing the file + emailing (dry-run until SMTP is set).
- UI: gear button beside "Night Report" opens a settings modal (enable, time,
  baseline range, metrics, recipients) that GET/PUTs the config.

Verified: table registers + auto-creates, config CRUD + validation, tick
runs/dedups, templates render and gate to sound projects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:08:58 +00:00
parent c5b5045603
commit b2c54caebd
4 changed files with 271 additions and 2 deletions
+26
View File
@@ -218,6 +218,32 @@ class ProjectModule(Base):
__table_args__ = (UniqueConstraint("project_id", "module_type", name="uq_project_module"),)
class SoundReportConfig(Base):
"""
Per-project configuration for the automated nightly sound report
(FTP report pipeline). One row per project. Read by the morning tick in
SchedulerService and by the manual /reports endpoints (as defaults).
New table → created by Base.metadata.create_all() on startup; no migration
needed (only a rebuild/restart).
"""
__tablename__ = "sound_report_configs"
id = Column(String, primary_key=True, default=lambda: __import__('uuid').uuid4().__str__())
project_id = Column(String, nullable=False, index=True, unique=True) # FK to projects.id
enabled = Column(Boolean, default=False, nullable=False) # run the daily report?
report_time = Column(String, default="08:00", nullable=False) # local HH:MM to run/send
metric_keys = Column(String, default="lmax,l01,l10,l90", nullable=False) # csv of metric keys
baseline_start = Column(Date, nullable=True) # baseline-week range
baseline_end = Column(Date, nullable=True)
recipients = Column(Text, nullable=True) # csv; falls back to REPORT_SMTP_RECIPIENTS env
last_run_date = Column(Date, nullable=True) # evening-date of the last reported night (dedup)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class MonitoringLocation(Base):
"""
Monitoring locations: generic location for monitoring activities.