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.
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>
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.