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
arg1998 pushed a commit that referenced this pull request Oct 2, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## browserhive@0.2.0

### Minor Changes

- [#17](#17)
[`a4b5b8a`](a4b5b8a)
Thanks [@arg1998](https://github.com/arg1998)! - Choose which browser
sessions use, and run it inside Chromium's sandbox.

- **Sessions run inside Chromium's sandbox wherever your machine allows
it.** The new `--sandbox` setting (`BROWSERHIVE_SANDBOX`) defaults to
`auto`: each browser is tried with the sandbox on its first launch and
keeps it where it works (macOS, Windows, most Linux, and Google Chrome
on Ubuntu). Where it cannot (Ubuntu 23.10+ with the bundled browser,
running as root, Docker), sessions run as before, with one warning in
the log, and `browserhive doctor` explains why with the fix for your
machine. `--sandbox off` is exactly the old behaviour.
- **`--sandbox on` makes the sandbox a guarantee.** The server checks
the configured browser before it opens its port and refuses to start
(exit code 3) when it cannot sandbox, printing the reason and what to do
on your machine, easiest first: an installed browser that does sandbox,
an AppArmor profile, running as a normal user, or `--sandbox auto`.
- **A sandbox the machine cannot provide is now a clear error.**
`launch_session` with `launch_options: { chromiumSandbox: true }` on
such a host used to answer `INTERNAL_ERROR` with "retry with backoff";
it now answers `SANDBOX_UNAVAILABLE`, "retrying will not help", with the
channels that do work.
- **`browserhive init` shows the browsers on your machine and lets you
pick one.** It lists the bundled Chromium, an installed Google Chrome or
Microsoft Edge, whether each can run sandboxed, and the pros and cons of
each for your system. Enter keeps the current choice; a new choice is
saved to your config file after you confirm. It can also install Google
Chrome for you (Google's installer, administrator rights). In scripts:
`--channel chrome --yes`, `--installChrome`; nothing is asked without a
terminal.
- **`browserhive doctor` checks the browser you configured.** A
`defaultChannel=chrome` without Chrome installed is now a failure
instead of a green check. New checks: installed Chrome and Edge, a
browser that has moved more than one major version ahead of the tested
build, managed policies that block automation, the sandbox per browser
with the fix for your OS, and running as root or in a container. `doctor
--printApparmorProfile` prints (never installs) the profile that lets
the bundled browser sandbox on Ubuntu.
- **The dashboard shows browsers and the sandbox.** The System page
lists the browsers found, their versions and whether each runs
sandboxed; a session's Details tab shows its browser version and sandbox
state.
- A missing Google Chrome now names the command that installs it
(`browserhive init --installChrome`), and `--no-sandbox` suggests
`--sandbox`.

- [#20](#20)
[`15871c2`](15871c2)
Thanks [@arg1998](https://github.com/arg1998)! - Read environment
variables in `browserhive.config.json`, and see which variable every
value came from.

- **Keep secrets out of the config file.** A string in
`browserhive.config.json` can now contain `{env:NAME}`, and BrowserHive
reads that environment variable when it starts: `"authTokens":
"ci-runner:{env:CI_TOKEN}"`, `"otelHeaders": { "Authorization": "Bearer
{env:OTLP_TOKEN}" }`, `"otelEndpoint": "http://{env:OTLP_HOST}:4318"`.
The file can be committed and shared while tokens and per-machine values
stay in the environment. It works for every key, in whole values, inside
longer strings, in lists and in header maps; `"maxSessions":
"{env:MAX_SESSIONS}"` accepts exactly what `BROWSERHIVE_MAX_SESSIONS`
would.
- **Defaults for one file everywhere.** `{env:NAME:-default}` uses
`default` when `NAME` is not set or is empty, so the same file works on
a laptop and on a server. `{env:NAME}` without a default is required: if
the variable is missing, startup stops and says which key, which file
and which variable, and how to add a default.
- **Every place shows where a value came from.** The startup log reads
`config: otelEndpoint=… (config-file via $OTLP_HOST) shadows env=…`,
`browserhive config show` prints `config-file via $OTLP_HOST` in the
SOURCE column, `config show --json` and `GET /api/v1/system/config` add
`refs` and `template` fields, and the dashboard's System page shows a
`$OTLP_HOST` chip next to the source. Select it to see the value as
written in the file; a new **Only values from references** switch lists
just those keys.
- **Secrets stay hidden.** For `authTokens`, `otelHeaders`, and any
value whose variable name looks like a credential (it contains `token`,
`secret`, `password` and the like), you see the variable's name, never
its value, and the value is scrubbed from logs. `browserhive doctor` no
longer asks you to `chmod 600` a config file whose `authTokens` only
reference variables, and it warns when a variable was not set so its
default is in use.
- **Mistakes are caught, not ignored.** `${env:NAME}` (the OpenTelemetry
Collector's spelling) stops startup with "Did you mean '{env:NAME}'?";
`{ENV:NAME}` or `{file:…}` stop it too, with how to keep such text
literal (`{{…}}`). Braces that are not references, like `{trace_id}` in
`otelTraceUrlTemplate`, are left alone, so existing config files work as
before. References are not expanded in `BROWSERHIVE_*` variables or
command-line flags (your shell does that); BrowserHive warns if it finds
one there.
- The JSON Schema for the config file (`browserhive config schema`)
accepts a reference for every key, so editors no longer underline
`"stealth": "{env:STEALTH}"`.

- [#21](#21)
[`43d2f5e`](43d2f5e)
Thanks [@arg1998](https://github.com/arg1998)! - See which agent is
connected (Claude Code, Codex, Cursor, OpenCode, Gemini CLI and others),
count sessions and tool calls per agent, and filter by it.

- **Recognised with nothing to configure.** Claude Code and Gemini CLI
over stdio, and Claude Code, Codex, OpenCode, Cursor, VS Code, Cline,
Continue and Zed over HTTP, are recognised from what they already send.
Anything BrowserHive can't place is shown as **Unknown**, which is
counted and filterable like any other agent, never an empty cell.
- **Name any agent in one line.** Add an `X-BH-Agent-Harness` header, a
`BROWSERHIVE_HARNESS` variable in a stdio server's `env` block, or
`?harness=<name>` to the MCP URL when a client only has a URL field.
`X-BH-Agent-Model` / `BROWSERHIVE_MODEL` and `X-BH-Workspace` /
`BROWSERHIVE_WORKSPACE` label the model and the workspace;
`X-BH-Meta-<Name>` headers add a few extra labels. Tool calls can carry
the same in `_meta` (`ai.browserhive/harness`, `ai.browserhive/model`,
`ai.browserhive/workspace`), read on every call.
- **In the dashboard.** The sessions list has a Harness filter and an
Agent column; a session's Details tab has a Client panel (the agent and
how it was recognised, the model or "not reported", the workspace, the
client's name, version and protocol); the Overview has a Harnesses card
with sessions and tool calls per agent; the System page lists live and
recent MCP connections, with the User-Agent, IP, conflicting signals and
extra labels of each. A tool call's details show which agent made it.
- **In the API and telemetry.** `GET /api/v1/sessions` accepts
`harness=` and returns a `harnesses` facet, each session has a
`harness`, tool calls carry `harness` (and `GET /api/v1/tool-calls`
filters by it), and there are two new endpoints: `GET
/api/v1/metrics/harnesses` and `GET /api/v1/system/mcp/connections`.
Traces carry `browserhive.harness` (and the declared model); the
tool-call and live-session metrics gain a `harness` attribute limited to
known names plus `other` and `unknown`.
- **Reported, not verified.** All of this is what the client or your own
configuration says. BrowserHive shows it faithfully and never uses it to
allow or refuse anything. MCP gives a server no way to learn the model,
so a model is shown only when one is declared.
- Existing sessions and databases keep working: the database upgrades on
start (schema v3), sessions from before read Unknown, and every field
that was there before is still there. `BROWSERHIVE_HARNESS`,
`BROWSERHIVE_MODEL` and `BROWSERHIVE_WORKSPACE` are not configuration
settings, so they are no longer rejected as unknown, and a misspelled
one gets a suggestion.

- [#28](#28)
[`9470feb`](9470feb)
Thanks [@arg1998](https://github.com/arg1998)! - Answer from your phone:
Approve, Reject and Mark resolved right in Telegram, Discord and ntfy,
and richer Telegram messages.

- **Answer without opening the dashboard.** Switch on **Answer from the
chat** for a channel, and a notification that waits for you carries
buttons that act: **Mark resolved** and **Reject** for an attention
request, **Approve** and **Deny** for a vault fill. Press one and
BrowserHive does what the same button in the dashboard does, then edits
the message: the buttons disappear and it says who answered ("Resolved
on Telegram by … after 42 s"). Off by default.
- **Only the right person, only once.** On Telegram and Discord only the
accounts on the channel's allow-list may press; by default that is the
person who connected the chat, and anyone else is told their id so you
can add them. Every button works once, for 24 hours, only in its own
chat, and only while the request still waits. Every press is listed
under the new **Notifications → Actions**. The agent learns that you
answered from Telegram, Discord or ntfy, never your chat identity.
- **Discord bot mode.** A Discord channel can now use a bot instead of a
webhook, the only way to press buttons in Discord. The wizard walks
through the Developer Portal, builds the invite link with the minimal
permissions, lists your servers and channels, and links your account
with a **This is me** button. The channel card shows whether the bot is
connected.
- **ntfy answers through a second topic.** Give an ntfy channel a reply
topic, and its buttons make your phone post the answer there;
BrowserHive listens and updates the notification. Works on Android and
iOS.
- **Nothing to expose.** Every connection goes out from your machine
(Telegram long polling, the Discord gateway, an ntfy subscription).
Presses made while BrowserHive was stopped are handled at the next start
when the platform kept them. The webhook channel carries the act actions
as they are, and your receiver answers through the REST API.
- **Telegram Rich Messages.** Telegram notifications now have a heading,
the facts as a table, real tables, collapsible quotes and coloured
buttons, with the screenshot inside the message. If Telegram refuses
one, the classic format is sent instead.
- **Setup and terminal.** Startup channels take `actButtons=true` and
`allow=<user ids>`, `mode=bot` for Discord and `reply=` for ntfy.
`browserhive channels list` shows whether answers reach BrowserHive.
- New REST endpoints: `GET /api/v1/channels/actions` and the Discord bot
setup under `/api/v1/channels/discord/…`; channels report `connection`;
the `channels` WebSocket topic adds `action.recorded`. The database
moves to schema v6 (three new tables; an older release still opens it).

- [#27](#27)
[`fb94fbc`](fb94fbc)
Thanks [@arg1998](https://github.com/arg1998)! - Notifications on your
phone: Telegram, Discord, ntfy and webhook channels, with screenshots,
live updates and messages that delete themselves.

- **Get a message when an agent needs you.** Add a channel under
**Notifications → Channels**: a Telegram bot (one-tap connect, no chat
id to look up), a Discord webhook, an ntfy topic (scan a QR code with
the ntfy app) or a webhook of your own. A wizard shows the exact line to
set the token for how you run BrowserHive, checks that it is set,
previews the message exactly as it will look, and sends a test. Presets
pick what to send (_Needs me now_, _Problems_, _Wrap-ups_); **Advanced**
adds minimum severity, session patterns, harness, quiet hours with a
time zone and the content level.
- **Messages keep up.** When you resolve an attention request, the chat
message is edited in place, silently, and its buttons disappear; a
growing group of tool errors updates its count. Every send, edit and
delete is in the new **Delivery log**, live, with a sentence for
anything that was not sent ("quiet hours", "the platform refused the
token").
- **Screenshots, when you want them.** Off by default, per channel and
category: the page when an agent asked for help (CAPTCHAs included), the
login page before a vault fill (never during one), a crashed session's
last frame. Form fields can be masked.
- **Self-destruct.** Delete messages after a time you choose per
category, or once they are resolved. Telegram only allows 48 hours, so
its timers stop at 47.
- **Links that open on your phone.** The new `publicUrl` key
(`--publicUrl`, `BROWSERHIVE_PUBLIC_URL`) is the address where you reach
the dashboard (a Tailscale name, your reverse proxy, a Cloudflare
tunnel). Notification links use it, its host is trusted without
`allowedHosts`, and the CSRF check accepts it even when your proxy
rewrites `Host`. The System page and `browserhive doctor` check that it
really reaches this BrowserHive.
- **Your accounts, your tokens.** BrowserHive runs no servers or shared
bots. Tokens stay in environment variables; channels store only the
variable names, so a database backup never contains one.
- **For servers and containers**, declare channels at startup with
`--notificationChannel
"telegram:name=phone,token=env:BH_TG_TOKEN,chat=123456"` (repeatable; a
token typed into the flag is refused). They show in the dashboard with a
"from startup" badge.
- **From a terminal:** `browserhive channels list`, `channels test
<name>` and `channels preview <name>`; `browserhive doctor` checks every
channel's variables and `publicUrl`.
- New REST endpoints under `/api/v1/channels` (scopes `channels:read`,
`channels:write`), `GET /api/v1/system/public-url`, a `channels`
WebSocket topic, and `instance_id` in `GET /health`.

- [#25](#25)
[`9fce259`](9fce259)
Thanks [@arg1998](https://github.com/arg1998)! - Notifications now
follow what they announce, and every notification is a versioned message
that can be delivered reliably to other apps.

- **A notification keeps its place as things change.** When you resolve
an attention request or a vault confirmation, reject it, or it times
out, its notification shows the outcome (**resolved**, **expired**) as a
small pill in the bell and on the Notifications page, instead of staying
as if it were still waiting. A toast still on screen for it closes. A
recovered subsystem marks its degradation notification resolved the same
way.
- **Richer notification data in the API.** `GET /api/v1/notifications`
and the `notifications` WebSocket topic add `kind`, `category`,
`severity`, `state`, `revision` and `thread` to every notification.
Existing fields are unchanged.
- **Safer text.** An agent's attention reason and other text copied into
a notification now go through the same redaction as the logs, and page
addresses lose their query strings.
- **Built for delivery to your phone.** Every notification is also a
versioned message document, whose JSON Schema is published in the
reference docs (the webhook channel sends it as is). Delivery to
Telegram, Discord, ntfy and webhooks goes through a new outbox in the
database, with retries and a circuit breaker, so nothing is lost when a
service is down. With no channel configured nothing extra runs.
- The database upgrades on start (schema v5: new columns on
notifications and three new tables; a backup is written first). Older
releases can still open it. Existing notifications are classified from
what they already recorded; nothing is invented for them.

- [#29](#29)
[`7873a06`](7873a06)
Thanks [@arg1998](https://github.com/arg1998)! - A daily summary and a
heads-up when something's off.

- **Daily or weekly digest.** Give a channel a digest (every day at
09:00, optionally weekdays only with Monday covering the weekend, or
every week on Friday at 17:00; day and time are yours to change) and it
gets the period in numbers: sessions, tool calls and errors with the
rate, attention requests and how fast they were answered, vault fills,
blocked requests, the slowest tool against the period before, the top
errors, open problems, a small chart of tool calls per hour and a table
per harness, with a link to the Overview for exactly that period. The
new **Daily digest** preset sets it up in one click.
- **In your time zone.** Each channel has a time zone, BrowserHive's own
unless you pick another, and digests and quiet hours follow it through
daylight saving time.
- **Nothing lost, nothing spammed.** If BrowserHive was off when a
digest was due, the most recent one arrives when it starts again, marked
late, with how many earlier ones were skipped. A day with no activity
sends nothing (the delivery log says so). A digest due in quiet hours
arrives silently.
- **Tell me when something looks off.** An hourly check that stays
silent until a threshold is crossed: many tool calls failing, a request
waiting too long, sessions at the limit, a spike in blocked requests,
BrowserHive degraded. The alert updates itself and says **Back to
normal** when things recover, without flapping. Thresholds can be tuned
per channel.
- **Reports in BrowserHive.** Every digest and anomaly alert also lands
in the dashboard once per period, however many channels it reached: in
the bell and the inbox (a new **Reports** filter), quietly for digests
(no pop-up, no badge) and like a System notification for anomaly alerts.
The new **Notifications → Reports** tab keeps them for 90 days, even
after you dismiss them, with filters and a page per report (its numbers,
chart and tables, the channels it reached, and Open Overview for this
period). It can also run a digest and anomaly alerts for the dashboard
alone, with no channel at all.
- **Send a digest now.** Preview the real digest exactly as your phone
will show it, then send it on demand, from the channel card or `POST
/api/v1/channels/{id}/digest`.
- **Setup and terminal.** Startup channels take `digest=daily@09:00`,
`digest=daily:weekdays` or `digest=weekly` (Friday 17:00;
`weekly:mon@08:30` for another day), `tz=` and `anomaly=on` with
`anomaly.*` thresholds; `browserhive channels list` shows each channel's
next digest, and `channels preview --sample digest|anomaly` renders the
samples.
- **Also:** the Allow this person button explains when you lack
`channels:write`, a deleted channel's reply-topic cursor goes with it,
the live delivery log no longer shows stale superseded rows, and the
public address check reports a proxy's 5xx page as unreachable. The
message contract gains the `digest.weekly` kind, a `chart` block and an
optional `report` field (schema 1, additive); `GET
/api/v1/notifications` takes a `category` filter; new `GET
/api/v1/notifications/reports`, `GET /api/v1/notifications/reports/{id}`
and `GET`/`PUT /api/v1/notifications/report-settings`; no database
migration.

- [#24](#24)
[`6c90ade`](6c90ade)
Thanks [@arg1998](https://github.com/arg1998)! - Closed sessions now
keep showing whether they ran inside Chromium's sandbox, and with which
browser version.

- **Recorded at launch.** When a session's browser starts, BrowserHive
stores whether it runs sandboxed and the browser's real version with the
session. Under the default `--sandbox auto` the answer depends on the
browser and the machine (on Ubuntu, Google Chrome sandboxes and the
bundled Chromium falls back), so it is recorded rather than worked out
later.
- **In the dashboard.** A session's Details tab shows the browser
version and a **sandboxed** / **not sandboxed** state for finished
sessions too, not only while they run. Sessions from before this release
show **not recorded**, with a note that this does not mean the sandbox
was off; a session whose browser never started shows **not launched**.
- **In the API.** `browser: { version, sandboxed }` on `GET
/api/v1/sessions` and `GET /api/v1/sessions/{id}` is now filled for
closed sessions from what was recorded at launch. It is still left out
when nothing was recorded, so read a missing `browser` as "unknown",
never as "not sandboxed".
- The database upgrades on start (schema v4, two new columns, a backup
is written first); older releases can still open it. Nothing is guessed
for existing sessions.

### Patch Changes

- [#22](#22)
[`a7a79d8`](a7a79d8)
Thanks [@arg1998](https://github.com/arg1998)! - The System page's **MCP
connections** list now shows 10 connections per page, with the usual
pager underneath (10, 25 or 50 rows per page, previous/next, "1–10 of
23"). Before, it showed up to 50 at once with no way to see older ones.
For API clients, `GET /api/v1/system/mcp/connections` accepts an
`offset` for paging and returns `total`, the number of stored
connections; existing calls behave exactly as before.

- [#33](#33)
[`f4ebe1a`](f4ebe1a)
Thanks [@arg1998](https://github.com/arg1998)! - OpenTelemetry now
exports the notification, attention, session-launch, WebSocket,
write-queue and browser-memory metrics the docs describe.

- **Metrics that were documented but never sent** now reach your
collector: `browserhive.attention.wait`,
`browserhive.session.launch.duration`, `browserhive.ws.connections`,
`browserhive.ws.buffered_bytes`, `browserhive.ws.frames_dropped`,
`browserhive.db.dropped_writes`, `browserhive.browser.rss_bytes` (each
session's browser with all its processes, every 10 seconds; Linux and
macOS) and `browserhive.process.event_loop_lag`.
- **Attributes the docs promised** are now set: `closed_reason` on
`browserhive.session.lifetime`, `kind` on `browserhive.attention.open`
(vault confirmations count too), `table` on
`browserhive.retention.pruned_rows`.
- **The notification metrics** `browserhive.notifications.deliveries`,
`.actions` and `.reports` are now in the [telemetry
guide](https://browserhive.ai/docs/guide/telemetry). `.reports` now
counts on-demand digests as `manual`, as documented, and no longer
counts a silent revision of an open in-app anomaly alert.
- **Gauges only report what exists**: a closed session's browser memory
or a closed WebSocket's buffered bytes disappears from the next export
instead of repeating its last value. `browserhive.db.dropped_writes`
reports every recorder table from the start, at 0.
- **Every metric has a unit** (`ms`, `By`, or a count such as `{call}`),
and the guide's table lists each one with its type, unit and attributes.
With telemetry off nothing is measured, as before.

- [#15](#15)
[`280ec1f`](280ec1f)
Thanks [@arg1998](https://github.com/arg1998)! - Fixes found while
researching the next features.

- **MCP works behind a port mapping or SSH tunnel.** `/mcp` rejected
every request whose `Host` named a different port (`localhost:8080` for
a server on 9876) while the dashboard kept working. Both now use the
same check, which ignores the port.
- **New `--allowedHosts` setting** (`BROWSERHIVE_ALLOWED_HOSTS`) for the
name a reverse proxy forwards, so the recommended TLS proxy setup works
without rewriting `Host`.
- **Session details show the MCP client that launched the session** —
its name and version, plus the model and workspace when it sends
`X-BH-Agent-Model` / `X-BH-Workspace`. `X-BH-Agent-Harness` no longer
overwrites the workspace. Stdio connections record their client too.
- **Saved storage states keep IndexedDB**, so sites that store their
login there (Firebase Auth, among others) restore logged in.
- **A malformed secret setting is no longer printed in the error.**
`BROWSERHIVE_AUTH_TOKENS` and `otelHeaders` values from env or the
config file are also scrubbed from logs and telemetry now.
- **Takeover input is audited**: one audit row per session and second
with counts only, never the keys typed.
- **`maxSessions` respects container and systemd memory limits** instead
of deriving from the host's full RAM.
- **A data directory on a filesystem that refuses `chmod`** (network
shares, some bind mounts) no longer stops the server from starting; it
logs a warning.
- **Toggling fullscreen in the live view no longer restarts the
stream.**
  - Old MCP connection records are now pruned by retention.
- A helper process (such as the Bitwarden CLI) that exits before reading
its input no longer raises a spurious `UNHANDLED` degradation.
## @browserhive/core@0.2.0

### Patch Changes

- Updated dependencies []:
  - @browserhive/contracts@0.2.0
## @browserhive/dashboard@0.2.0

### Patch Changes

- Updated dependencies []:
  - @browserhive/contracts@0.2.0
## @browserhive/contracts@0.2.0

No changes in this release.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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