Skip to content

feat(notifications): daily digests and anomaly alerts, in your time zone - #29

Merged
arg1998 merged 28 commits into
mainfrom
feat/notifications-n3-digests
Sep 29, 2026
Merged

arg1998 merged 28 commits into
mainfrom
feat/notifications-n3-digests

Conversation

@arg1998

@arg1998 arg1998 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

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 reports category existed in name only.

After:

  • Digest — sessions started and live, tool calls and errors (with the previous period's rate), attention requests (answered, median wait, timed out, waiting now), vault fills by result, blocked requests with the top pattern, the slowest tool's p95 against the previous period, the top errors, open problems, a text-bar chart of tool calls per hour, a table per harness, and a link to the Overview for exactly that period. The Daily digest preset sets it up in one click (plus anomaly alerts, nothing instant).
  • In your time zone — each channel has an IANA zone, BrowserHive's own by default (read at each evaluation). Digest times and quiet hours follow it through DST: a skipped local time fires shifted by the gap, a repeated one fires once.
  • Late, once; never empty — after downtime the most recent missed digest is sent, marked late, with "N earlier digests were skipped while BrowserHive was off". A period with no activity sends nothing and is logged suppressed · nothing happened. A digest due inside quiet hours arrives silently.
  • Anomaly alerts — hourly on the trailing hour: tool-call error rate, attention waiting too long, sessions at 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).
  • Send a digest now — preview the real digest for the period ending now, exactly as the platform will show it, then send it on demand (card, channel page, POST /api/v1/channels/{id}/digest).
  • Dashboard — Reports section in the wizard (Off · Every day · Every week, 24-hour time, weekday, next run, searchable time zone picker defaulting to BrowserHive's zone, anomaly switch with its checks; thresholds under Advanced), a Reports line on each card (next digest in the channel's zone, "Watching for anomalies" or the active checks, Send now), the delivery log shows each report's window with late / on demand pills, previews for the digest and anomaly samples on every platform.
  • CLI / startup channels — digest=daily@09:00 / weekly:mon@08:30, tz= (now the channel's zone), anomaly=on with anomaly.errorRate|minCalls|attention|blocked|blockedMin|capacity|degraded; channels list prints each channel's next digest and anomaly state; channels preview --sample digest|anomaly.
  • N2 follow-ups — Allow this person is disabled with "Needs the channels:write permission" for principals without it (no 403 surprise); a channel's cursors (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 address unreachable, not login; 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 s showed a backslash).

Reports in the dashboard (owner review follow-up, D-45)

  • One in-app copy per period. Every report also gets one in-app row, built at the full level 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 thread report: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 in source_event_id. An empty period has no copy; a digest sent with Send now has its own.
  • Badge and toasts. A digest arrives already read (never the badge, never a toast, whatever the preferences). Anomaly alerts come from watches (one per distinct effective thresholds, so two channels with the same thresholds give one in-app alert per episode): unread (badge), type 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.
  • Inbox. A Reports chip in the Type facet (category=reports; type and category are one facet server-side), digest and radar icons, "on demand" in the meta line; a report row opens its report page.
  • Notifications → Reports tab. Reports in BrowserHive: the wizard's Reports section in an in-app form (digest off by default: every day [+ weekdays only] / every week, time, zone defaulting to BrowserHive's; anomaly switch off by default), saved server-wide in notification_cursors['settings:in-app-reports'] (PUT needs channels:write, the form says so otherwise), evaluated by the same scheduler (cursor digest: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.
  • Report page. The message drawn natively: facts as tiles, the chart as an SVG bar chart with a hidden data table, tables as real tables, footer lines; window and zone, late (with skipped) / on-demand markers, Open Overview for this period, Sent to with each delivery's status linking to the delivery log; opening it marks the copy read.
  • Weekly Friday 17:00, weekdays only. "Every week" defaults to Friday 17:00 and covers the full seven days; "Every day" runs every day, with an optional Weekdays only (Monday's digest covers the weekend). Startup flag: digest=weekly → Friday 17:00, digest=daily:weekdays[@HH:MM].
  • Recorded decisions (no behaviour change): no second confirmation for Approve/Reject from the chat (D-41); "Back to normal" stays silent (D-44); after downtime one late digest is final (D-43).
  • Polish found in the visual pass: a chart's "peak 1,525 calls" keeps together when a phone wraps the line (no-break spaces; digest goldens re-blessed, whitespace only).

Decisions (specs first, 260aad2; D-45 in cf3697b)

  • D-43 Scheduled reports — addressed per channel (window, zone, level and thresholds are per channel; the outbox plans a report for its channel only and the channel's filters don't apply: the schedule is the opt-in; the in-app row is stored read + dismissed); time zone default = the host's, read at each evaluation; DST rules above; durable cursor in 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).
  • D-44 Anomaly alerts — the check table with fire/clear levels, crossing → new alert, change → silent edit, clear → silent resolved; no check during quiet hours (the first check after them reports what is still off); severity error while degraded or at capacity.
  • D-45 Reports in the dashboard — one in-app copy per period (schedule + window + zone), full level, digests read + never toast, anomaly alerts via threshold watches (unread, system toast policy), in-app schedule stored server-wide, Reports chip + tab, 90-day history; alternatives (a report notification 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.
  • D-16, D-32 (chart block, report field, "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:goldens 54 ✅, build ✅, test:integration 67 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 with unread_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 for listReports / 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, ReportsSection weekly 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):

  • Send a digest now: Telegram (Rich Message with tables), Discord webhook, Discord bot, ntfy.sh — all sent.
  • Anomaly alert fired on all four at the first check after real activity (45 % errors) — all sent.
  • Scheduled digest at 02:05 America/Toronto — all four sent on time.
  • Late digest on Telegram and ntfy.sh (daemon stopped, cursor moved three days back, restarted 6 min after the 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 ✅.
  • D-45, one period on four platforms: Telegram, Discord webhook, Discord bot and ntfy.sh on the same daily schedule — the scheduled digest was sent on 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.
  • D-45, in-app only: a daemon with no channel at all and the in-app daily digest + anomaly switch on produced the digest on time (read, "Only in BrowserHive") and an unread anomaly alert, with no delivery rows.

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

  • NotificationMessage stays schema 1 (additive): kind digest.weekly, block chart, optional report {window, time_zone, late, skipped, manual}. docs/reference/notification-message.schema.json regenerated.
  • OpenAPI: POST /channels/{channel_id}/digest (sendChannelDigest, channels:write); ChannelView.reports, GET /channels host_time_zone, DeliveryRow.report, capability charts, preview samples digest/anomaly, suppression reason empty, reasons manual.
  • Database: no migration (schema v6; new cursor keys only). New metric browserhive.notifications.reports{kind,outcome} (outcome in_app added).
  • D-45: GET /notifications gains category[]; new GET /notifications/reports (listReports), GET /notifications/reports/{notification_id} (getReport, 404 REPORT_NOT_FOUND), GET/PUT /notifications/report-settings (getReportSettings notifications:read, putReportSettings channels:write); DigestRule.weekdays_only; ChannelReports.digest.weekdays_only; retention keeps category = 'reports' rows 90 days whatever their read/dismissed state.

Phone checklist (owner)

  1. Telegram: open the "📊 Daily digest" and the "Something looks off" messages — tables readable, text bars aligned, "Open Overview" works (with publicUrl).
  2. Discord (webhook and bot): the digest reads top to bottom (facts, chart, top errors, harnesses, window line in small text).
  3. ntfy (Android/iOS): digest shows with the 📊 tag; the anomaly alert is replaced by "Back to normal" (after the next hourly check with no errors) without a new sound.
  4. Late digest: its first line says "Sent late …" and how many were skipped.
  5. Dashboard: set a digest a few minutes ahead in another zone and check "Next digest" on the card; press Send now.
  6. Dashboard (D-45): with two channels on the same schedule, the bell shows one digest (no badge, no pop-up); the anomaly alert raises the badge and pops up once; Notifications → Reports lists both, and the report page's Open Overview for this period opens the right window.
  7. Reports tab: switch on Reports in BrowserHive with no channel, pick Every week (it shows Friday 17:00) or Every day + Weekdays only, save, and see the next digest.

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.

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
arg1998 marked this pull request as ready for review September 29, 2026 06:33
…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.
@arg1998
arg1998 merged commit 7873a06 into main Sep 29, 2026
23 of 25 checks passed
@arg1998
arg1998 deleted the feat/notifications-n3-digests branch September 29, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant