feat: weekly digest + local-time quiet hours

Weekly Digest (send_weekly_digest Celery task):
- Runs every Monday 8:30 AM UTC via beat schedule
- Queries all followed bills updated in the past 7 days per user
- Sends low-priority ntfy push (Priority: low, Tags: newspaper,calendar)
- Creates a NotificationEvent (weekly_digest type) for RSS feed visibility
- Admin can trigger immediately via POST /api/admin/trigger-weekly-digest
- Manual Controls panel now includes "Send Weekly Digest" button

Local-time quiet hours:
- Browser auto-detects IANA timezone via Intl.DateTimeFormat().resolvedOptions().timeZone
- Timezone saved to notification_prefs alongside quiet_hours_start/end on Save
- Dispatcher converts UTC → user's local time (zoneinfo stdlib) before hour comparison
- Falls back to UTC if timezone absent or unrecognised
- Quiet hours UI: 12-hour AM/PM selectors, shows detected timezone as hint
- Clearing quiet hours also clears stored timezone

Co-Authored-By: Jack Levy
This commit is contained in:
Jack Levy
2026-03-01 22:04:54 -05:00
parent a0e7ab4cd3
commit 0de8c83987
10 changed files with 234 additions and 32 deletions

View File

@@ -241,6 +241,8 @@ export const adminAPI = {
apiClient.post("/api/admin/backfill-labels").then((r) => r.data),
resumeAnalysis: () =>
apiClient.post("/api/admin/resume-analysis").then((r) => r.data),
triggerWeeklyDigest: () =>
apiClient.post("/api/admin/trigger-weekly-digest").then((r) => r.data),
getApiHealth: () =>
apiClient.get<ApiHealth>("/api/admin/api-health").then((r) => r.data),
getTaskStatus: (taskId: string) =>

View File

@@ -171,6 +171,7 @@ export interface NotificationSettings {
digest_frequency: "daily" | "weekly";
quiet_hours_start: number | null;
quiet_hours_end: number | null;
timezone: string | null; // IANA name, e.g. "America/New_York"
}
export interface NotificationEvent {