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>