meshai/work/meshai/notifications
Matt Johnson c0572b04a1 feat: user-crafted scheduled announcements (custom_announcements)
Free-text broadcasts the owner types straight into the GUI on a clock-slot
schedule (daily / interval_days / weekly / monthly) -- no placeholders, no
data sources, no templating, no SQL from the user.

- v30 migration: custom_announcements table (own explicit channel list per
  row, new rows start disabled).
- CustomAnnouncementScheduler (notifications/scheduled/custom_announcements.py):
  60s tick modelled on ReminderScheduler; monthly day-of-month clamped via
  calendar.monthrange; restart-safe dedup keyed on the local calendar date
  of last_sent_at; spacing_seconds roll-call pacing between announcements
  firing in the same tick.
- Dispatcher.dispatch_scheduled_custom_broadcast(): delivers to the
  announcement's own channel list (no toggle/region_routes matrix), one
  mesh_broadcasts_out audit row per target, cold-start grace.
- New announcement_routes.py router: GET/POST/PUT/DELETE /api/announcements
  + POST .../preview (wire text + char/byte count, never sends). No
  send-now endpoint anywhere.
- Wired into notifications/pipeline/__init__.py (alongside ReminderScheduler)
  and dashboard/server.py; single_packet_max_chars runtime override added
  in main.py's budget list.

57 new tests (recurrence kinds, Feb-29/28 clamp, restart-safe dedup, pacing,
budget truncation, full input validation, multi-target audit rows, no
send-anywhere guarantee). Full suite: 2090 passed, 0 failed (up from 2033
baseline), 9 pre-existing warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 18:20:59 +00:00
..
formatters fix: honour town_anchors.enabled in alert anchor-resolution queries 2026-08-16 02:58:00 +00:00
gating fix(fire): honor tombstoned_at as a durable mute in the gate decider 2026-08-16 01:20:18 +00:00
pipeline feat: user-crafted scheduled announcements (custom_announcements) 2026-08-16 18:20:59 +00:00
reminders fix(reminders): resolve Active: fire location via _fire_anchor, not county/state 2026-08-16 18:00:12 +00:00
renderers chore(central-ripout 2e): split central_normalizer.py; eliminate the "central" name (#165) 2026-07-17 17:15:50 -06:00
scheduled feat: user-crafted scheduled announcements (custom_announcements) 2026-08-16 18:20:59 +00:00
__init__.py refactor: move source tree into work/, multi-stage Docker build, fix satpass 2026-06-16 03:40:31 +00:00
categories.py feat(ipaws): add FEMA IPAWS-OPEN civil-alert adapter (disabled by default) (#138) 2026-07-16 09:05:01 -06:00
channels.py feat(meshcore): route MeshCore cells to room servers (open rooms) (#110) 2026-07-09 22:14:42 -06:00
clock.py refactor(phase0): source-agnostic formatter/gating scaffold + harness (inert) (#28) 2026-07-04 12:55:48 -06:00
cutover.py fix(hydro): restore USGS stream-gauge flood alerts (silently dead since the all-native flip) (#156) 2026-07-17 14:07:04 -06:00
danger_zone_correlator.py refactor(danger-zones): drop min_severity + position_max_age; table snow (grayed/inert) 2026-06-21 06:56:22 +00:00
env_reporter.py feat(adapters): WZDx daily work-zone summary + FIRMS restart-safe cold-start (#108) 2026-07-09 19:13:17 -06:00
evac_phase.py fix(ipaws): route county-only alerts through evac-phase detection 2026-08-16 01:19:46 +00:00
events.py refactor: move source tree into work/, multi-stage Docker build, fix satpass 2026-06-16 03:40:31 +00:00
router.py Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing (#15) 2026-07-03 14:57:24 -06:00
shadow.py fix(shadow): forward source kwarg to gating decider in shadow_gate (#31) 2026-07-04 18:37:12 -06:00
summarizer.py refactor: move source tree into work/, multi-stage Docker build, fix satpass 2026-06-16 03:40:31 +00:00