feat(notifications): daily digests and anomaly alerts, in your time zone - #29
Merged
Merged
Conversation
Reports are addressed to the channel that schedules them, in its time zone (default the host's), produced exactly once per window with a durable cursor, sent late once after downtime with the skipped count, never sent when empty (logged suppressed: empty), and silent inside quiet hours. Anomaly alerts run hourly with fixed thresholds and hysteresis and stay silent unless a check crosses. Specs 00 (D-43, D-44; D-16, D-32, D-34 amended), 02, 03 (API, ports, §9.1-§9.4, new §9.7), 04, 08, 09 and 10.
…e digest route
Additive, schema 1 unchanged: the digest.weekly kind, the chart block
(bars over equal steps; degraded to text where charts is not a
capability) and the optional report field (window, time zone, late,
skipped, manual). Channel rules gain time_zone, digest and anomaly with
their defaults; checkChannelRules validates zones. ChannelView.reports,
host_time_zone on GET /channels, DeliveryRow.report, the charts
capability, the digest and anomaly preview samples, the Daily digest
preset and POST /channels/{id}/digest.
ReportScheduler ticks every 60 s only while a channel schedules a report: per channel it keeps a durable cursor in notification_cursors, produces the newest missed digest window once (late after 5 min, with the skipped count), stores empty days as suppressed: empty, sends silently inside quiet hours, and runs the hourly anomaly checks with hysteresis (a new alert on a crossing, a silent edit on a change, a silent resolved edit when all clear). Reports are addressed notifications planned for their channel only, written with their delivery rows and cursor in one transaction. Pure producers (reports.ts) build the digest and the alert at the channel's content level; schedule.ts does the zone and DST maths. Additive indexed queries: windowCounts, toolLatency (p95 in SQLite), topErrors, windowStats, countByResult. degrade turns the chart block into text bars; restrictContent keeps a message already at its level. ChannelService gains the report views, Send a digest now and the report samples; deleting a channel removes its cursors. The Discord renderer escapes list markers only at line starts.
--notificationChannel accepts digest=daily@HH:MM|weekly:<day>@hh:MM, tz (now the channel's zone) and anomaly=on with anomaly.* thresholds. channels list prints each channel's next digest and its anomaly state; channels preview offers the digest and anomaly samples. Composition builds the report scheduler (capacity from the session service, the reports counter) and starts it after the outbox.
The zone and DST maths of the schedules move to @browserhive/contracts/notifications so the setup wizard shows the same next run as the server; core keeps the zone fallbacks and the dates reports print.
A 📊 mark on Telegram and Discord and the bar_chart tag on ntfy for digests; the real digest of Send a digest now carries no sample note.
…late digests in the log The rules step gains a Reports section: Off · Every day · Every week with a 24-hour time and a weekday, the next run in the channel's zone, a searchable time zone picker defaulting to BrowserHive's zone (also used by quiet hours), and the anomaly switch with its checks; Advanced holds the thresholds. The Daily digest preset switches both on. Cards show the next digest and the anomaly state with Send now, which opens a preview of the real digest and sends it on demand; the channel page has the same action. The delivery log shows a report's window, a late pill and an on-demand pill. Allow this person is disabled with the reason for a principal without channels:write.
The live delivery log republishes every row of a notification, so an older row a newer job superseded no longer looks stale until a reload. The publicUrl check calls a proxy's 502/503/504 page unreachable instead of a login. A masking channel's crash preview shows no image, like the real crash message (its stored frame cannot be masked).
Embed fields always sit below the description, so a fields block that more content follows is written in place as lines; the Discord preview draws -# subtext small, like Discord.
arg1998
marked this pull request as ready for review
September 29, 2026 06:33
…days-only digests (D-45)
…riod, anomaly watches, in-app schedule, Friday weekly default and weekdays-only digests (D-45)
… report settings, weekdays-only and Friday weekly defaults (D-45)
…'s time in its zone, mark on-demand digests in the inbox
…ce a channel is on Reports keep 90 days whatever their inbox state; the act-button audit is 90 days (auditRetentionDays is not a setting). Security, FAQ, MCP clients and telemetry no longer say only telemetry sends data out.
Adds the missing v3 step, puts v4 to v6 in order, and names the two
behaviour changes (sandbox auto, {env:} references in the config file).
…ification troubleshooting publicUrl and allowedHosts join the most used keys; the docs index lists answering from the chat, digests and reports.
…sandbox, harness identity Nothing leaves the machine unless telemetry or a notification channel is on.
…arness identity The data-leaves-your-machine stat names notifications next to telemetry.
…nfigured Channels ship in the same 0.2.0 release, so the combined notes contradicted themselves.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A daily summary and a heads-up when something's off: N3 of the notification channels (plan §10). Each channel can now get a digest (every day at 09:00, optionally weekdays only, or every week on Friday at 17:00; both changeable) in its own time zone, and an hourly anomaly check that stays silent unless something crosses a threshold. After the owner's review, reports also live in the dashboard: one copy per period in the inbox and a new Reports tab, with an in-app schedule that works with no channel at all (D-45).
What it does
Before: channels only carried instant notifications (attention, vault, crashes, tool errors, degradations). The "Daily digest" preset and the
reportscategory existed in name only.After:
suppressed · nothing happened. A digest due inside quiet hours arrives silently.maxSessions, a blocked-request spike against the day before, BrowserHive degraded. A crossing sends one alert listing every active check; a change edits it silently; all clear turns it into Back to normal (silent). Hysteresis (each check clears well below where it fires) prevents flapping. Thresholds are per channel (Advanced).POST /api/v1/channels/{id}/digest).digest=daily@09:00/weekly:mon@08:30,tz=(now the channel's zone),anomaly=onwithanomaly.errorRate|minCalls|attention|blocked|blockedMin|capacity|degraded;channels listprints each channel's next digest and anomaly state;channels preview --sample digest|anomaly.ntfy:,digest:,anomaly:) are removed when it is deleted or a startup channel disappears; the three low N1 leftovers are fixed (the live delivery log republishes every row of a notification so superseded rows don't look stale; a proxy's 502/503/504 page makes the public addressunreachable, notlogin; a masking channel's crash preview shows no image, like the real message). Also found and fixed: the Discord renderer escaped list markers inside a line (\4.2 sshowed a backslash).Reports in the dashboard (owner review follow-up, D-45)
fulllevel in the report's zone. Channels sharing a period — same schedule identity (frequency, weekday, time, weekdays-only, zone) and the same window — share one copy (found by threadreport:digest:<scheduleKey>:<since>:<until>inside the write transaction); 09:00 Berlin and 08:00 London are two periods. Each channel's own row stays read + dismissed (out of the inbox) and names the copy insource_event_id. An empty period has no copy; a digest sent with Send now has its own.system, so they toast exactly when System toasts are on, in the warning tone; "Superseded" / "Back to normal" are silent edits that close the toast.category=reports;typeandcategoryare one facet server-side), digest and radar icons, "on demand" in the meta line; a report row opens its report page.notification_cursors['settings:in-app-reports'](PUTneedschannels:write, the form says so otherwise), evaluated by the same scheduler (cursordigest:in-app), so reports work with zero channels. History: every in-app copy whatever its inbox state (dismissed stays here; report rows are kept 90 days), filters kind / "Sent to" (a channel or BrowserHive only) / period, late · on-demand · active/back-to-normal pills and the channels reached.digest=weekly→ Friday 17:00,digest=daily:weekdays[@HH:MM].Decisions (specs first, 260aad2; D-45 in cf3697b)
notification_cursors, written with the notification and its delivery rows in one transaction (exactly once per window); a rule change re-arms from now (an edit never causes a late digest); late = produced > 5 min after its time; cap = 1 (newest missed window, older ones counted); empty →suppressed: empty; quiet hours → sent silently; content per level (counts: numbers only; titles: + BrowserHive vocabulary and tables; full: + degradation messages and the most blocked domain).resolved; no check during quiet hours (the first check after them reports what is still off); severity error while degraded or at capacity.fulllevel, digests read + never toast, anomaly alerts via threshold watches (unread,systemtoast policy), in-app schedule stored server-wide, Reports chip + tab, 90-day history; alternatives (areportnotification type = table rebuild for a CHECK constraint; per-channel copies in the inbox; a reports table) rejected. D-41/D-43/D-44 record the owner's decisions above; D-43 gains the Friday 17:00 weekly default and weekdays only.reportfield, "a block is added only if every renderer can draw its degraded form"), D-34 (addressed notifications) amended. Spec 03 has a new §9.7.Verification
Fakes / local (head 089e75b):
bun run check✅ (server 3329, dashboard 438),test:goldens54 ✅,build✅,test:integration67 pass / 4 skip / 0 fail, e2e like CI 15 passed / 1 skipped (new: Inbox → Reports chip → report page → Open Overview for this period; the in-app settings saved as weekly Friday 17:00 and switched off; earlier: schedule a digest in a zone, send one now, see it in the log), website build ✅. D-45 suites: the in-app scheduler (one copy for two channels on a period, two for two zones, channel rows naming it, digests read / alerts unread withunread_count, the in-app schedule with zero channels, re-arm without late, weekly Friday 17:00 over seven days, weekdays only with Monday covering the weekend, shared watches, a watch closed when unwanted, back to normal silent, restart, the on-demand copy), calendar tests incl. a DST week with weekdays only, SQLite + in-memory conformance forlistReports/reportChannels/ the type ∪ category facet, retention keeping reports, route cases + auth matrix for the four new endpoints, dashboard toast policy (digest never, anomaly per System), the Reports chip,ReportsSectionweekly default / weekdays only / in-app form, the Reports tab and report page (axe clean). New suites: schedule (DST both ways, host vs channel zone), report producers per level, the anomaly hysteresis table, the scheduler on a fake clock (arming, on time, late + cap, restart, rule change, empty, quiet, paused, the anomaly episode), SQLite + in-memory conformance for the new queries, a 120-seed redaction property test over report strings, renderer goldens for digest / weekly / late / counts / full / anomaly / anomaly-resolved on every platform, reports end to end on SQLite through each adapter against the fakes, and the real ntfy container (binwiederhier/ntfy:v2.28.0: digest + anomaly replaced by back to normal).Real platforms (the owner's local test bot/group, Discord webhook + bot, ntfy.sh topic; values loaded only inside the commands, never printed):
sent.sent.senton time.sent, late, 3 skipped.scripts/notify-live.ts(now also a digest and an anomaly alert edited to back to normal on every platform): Telegram ✅, Discord webhook ✅, Discord bot ✅, ntfy.sh ✅.senton all four, on time, and the dashboard holds exactly one in-app copy (read) listing all four channels; the anomaly alert that fired for the four (same thresholds) is one unread in-app alert.Screenshots (1440/768, light/dark): cards, Reports section, anomaly thresholds, Send a digest now, delivery log with late digests, digest/anomaly previews on Telegram, Discord and ntfy — reviewed locally. D-45 set: inbox with the Reports chip (unread anomaly, read digests), the Reports tab (in-app settings + history with late / on-demand / active pills and channel chips), report pages (daily digest shared by two channels, late weekly in-app-only digest with 3 skipped, anomaly alert), the in-app weekly and weekdays-only controls, the wizard's weekly Friday 17:00 and weekdays-only controls, and the two refreshed shots (Send a digest now without the sample note; ntfy weekly digest with the chart line wrapping cleanly).
Contract change
NotificationMessagestays schema 1 (additive): kinddigest.weekly, blockchart, optionalreport {window, time_zone, late, skipped, manual}.docs/reference/notification-message.schema.jsonregenerated.POST /channels/{channel_id}/digest(sendChannelDigest,channels:write);ChannelView.reports,GET /channelshost_time_zone,DeliveryRow.report, capabilitycharts, preview samplesdigest/anomaly, suppression reasonempty, reasonsmanual.browserhive.notifications.reports{kind,outcome}(outcomein_appadded).GET /notificationsgainscategory[]; newGET /notifications/reports(listReports),GET /notifications/reports/{notification_id}(getReport, 404REPORT_NOT_FOUND),GET/PUT /notifications/report-settings(getReportSettingsnotifications:read,putReportSettingschannels:write);DigestRule.weekdays_only;ChannelReports.digest.weekdays_only; retention keepscategory = 'reports'rows 90 days whatever their read/dismissed state.Phone checklist (owner)
publicUrl).Follow-ups for N4
Slack (Block Kit,
chat.postMessage, the 3-call file upload), Pushover, Teams via Power Automate Workflows (Adaptive Cards), the Apprise-API bridge, email via SMTP (M365 basic auth is off for most tenants) — the handoff has the end-to-end recipe. Decided in review: no second confirmation from the chat, "Back to normal" silent, one late digest final, reports in the dashboard (D-45). Still open: weekly on several days; tuning the in-app anomaly thresholds from the UI (the API accepts a rule).macOS/Windows integration jobs are known-failing on main (#23, #2), not related.