Skip to content

feat(alerts): workflow failure alerts - #2731

Open
zdumitru wants to merge 76 commits into
stagingfrom
feature/workflow-failure-alerts
Open

zdumitru wants to merge 76 commits into
stagingfrom
feature/workflow-failure-alerts

Conversation

@zdumitru

@zdumitru zdumitru commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Draft: workflow failure alerts. Review feedback is addressed (replies on each comment); staging is merged in; the PR environment runs this branch on a fresh database.

What it does

Every organization is told when a workflow that runs on its own (Schedule, Event, Block, Webhook, Transfer, Pyth Price; never Manual) keeps failing:

  • Default for every org, no setup: email to the owners after 5 failed runs in a row, or 6 hours of failing with at least 2 failures in a row, whichever comes first.
  • A scheduled workflow whose run does not start on time is reported too (grace twice its interval, 2 to 10 minutes). Turning a workflow back on, changing its schedule or lifting an org halt restarts that clock, so none of them raise a false "not running".
  • Two "wallet out of funds" failures in a row alert at once.
  • Runs refused before they start (plan run limit, paid features, unpaid pay-as-you-go, an owner with no account) are not failures: a schedule that keeps being refused is reported as not running with the refusal reason, and any other workflow after 2 refusals. Each refused workflow gets its own alert, page, reminders and recovery, with a link to Plans or Billing. Refusals recorded while a workflow was off, deactivated or its org halted are ignored once that is over.
  • Runs that fail on KeeperHub's side (system errors) never count toward an alert.
  • Methods: email (org members), Slack, Discord, Telegram, PagerDuty, each "Always" or "Backup only"; up to 20 per org or workflow. A method that cannot send cannot be saved; every method, including a workflow's own, is checked daily (Slack only proves its token there; a deleted Slack channel is found on the next send). A broken one pauses itself and the alert recipients, the org owners and, for a workflow's own method, that workflow's people are emailed; that notice names the workflow and links to it. Backups fire after 15 minutes (PagerDuty 2).
  • One alert per streak, a daily reminder (up to 7), a recovery message. A workflow that fails again within 30 minutes of recovering continues the same incident (same PagerDuty incident, no new alert), and the recovery waits for 30 healthy minutes. 3 or more failing at once are grouped (refused workflows never are).
  • A message still unsent an hour after it was due is dropped instead of sent late; a PagerDuty resolve keeps trying for 24 hours.
  • Free plan: 100 alert emails a month; chat and PagerDuty are never counted.
  • Org-wide pause for maintenance (up to 7 days, at most 5 pauses or changes an hour per org since each one tells everyone; Resume always works). While paused, every member sees a yellow banner at the top of every page (above any billing banner, not instead of it). Per-workflow mute, own rules and optional "turn this workflow off when it reaches the limit" (count rule and out of funds only; the time rule never turns a workflow off). A halted org's incidents stay open and quiet.
  • Settings > Alerts page (readable by every member, editable by owners and admins), in three tabs; the tab is in the URL. Overview: a Status card with an On / Paused switch (turning it off opens the pause form; the switch stays on until the pause is confirmed; turning it on resumes) and a Needs attention card listing org problems and every workflow that needs attention (failing, or with a broken own method). Setup: who gets alerts (email recipients and methods with Test, Preview and a per-method Check and save, since each is tested before it is kept), then when to alert (rules), with the workflows that have their own rules. Rules and email recipients save themselves a moment after an edit, with a Saved line on the card and Undo in the toast. Sent alerts: the log.
  • Sent alerts: a table of When, What, By (the person who paused, resumed or tested, with the email or wallet address they signed in with; else KeeperHub), Method, Sent to and Result. It opens on alerts, reminders, recoveries and method notices (pause notices and tests one chip away) and says how many were not delivered; tests are recorded; filters on every column behind one toggle; a workflow filter from a link shows as a chip; numbered pages (First, Previous, Next, Last); CSV export with the same filters.
  • Workflow Properties > Alerts: Org rules and Custom rules tabs, both laid out as "Who gets alerts" then "When to alert" (with the reminder and recovery settings), and one Mute area outside the tabs. Custom rules start from the org's values and save themselves after an edit; the toolbar's Save sends them too.
  • Defaults and recipients: new orgs alert after 3 failed runs and email the owners. Every recipient list keeps an owner or admin who can get email; people who leave the org drop off, and a list with nobody left goes back to the owners.
  • Mutes cannot go unnoticed: a mute lasts at most 7 days ("until it works again" included) and needs a reason. Muting, changing, unmuting and the end of a mute notify the people who got the workflow's alerts before the change plus every owner (in the same transaction as the change), show in Sent alerts and in Overview > Needs attention (with Unmute), and are audited with before and after; at most 6 mute changes per workflow per hour. When a mute ends, the notice gives the runs that failed meanwhile and a still-failing workflow is alerted again at once. People removed from a recipient list (also when it leaves the default) are told, and so are the owners.
  • Privacy: a wallet member's step-up inbox is shown only to them; everyone else sees their wallet login (settings, workflow panel, sent alerts, analytics).
  • Analytics stays about runs: alerts are never listed or counted there. The failed run that set off an alert (the newest failure before it) shows an "Alert sent" chip with the time and methods, linking to Sent alerts filtered to that workflow.
  • Our on-call is paged if the alert processing or the scheduler stalls (refused schedules are not counted as a scheduler stall).
  • Chat text is fitted to each provider's limit, outside text is escaped (no mentions or disguised links), and secrets in error text are masked.

Changes outside the alerts code

  • lib/workflow/refused-runs.ts: every run refused before it starts is recorded the same way (skipped, not billable, category and reason), on the internal dispatch, webhook (plan limit and paid feature, which recorded nothing before), execute, MCP call, Pyth and executor paths. Refusals of one workflow and reason share an hourly row with a refusal count; under a flood the row is updated at most once a minute. Billing decisions, HTTP responses and metrics are unchanged.
  • Unpaid pay-as-you-go runs on execute, webhook and MCP call now end as skipped instead of error, like the executor's; the 402 bodies are unchanged.
  • The runs list shows a refused run's reason, "Refused at least N times between …" and a Plans or Billing link.
  • components/navigation-guard.tsx: an unsaved-changes guard for router navigation, Back, org switch and New Workflow (used by the Alerts page).
  • app/settings/(hub)/layout.tsx passes the user's orgs with alerts on, so the Alerts menu item draws with the rest instead of after a request. SectionHeader gains an optional wide description. /api/notifications/status returns alertsPausedUntil and the nobody-reachable flag to every member (the failing-workflows notice stays with owners and admins).
  • The top banner slot (components/app-banner-slot.tsx, app-banner.tsx, billing/quota-banner.tsx) stacks an alerts banner above a billing banner and offsets the page by both.
  • lib/analytics/alert-sent.ts marks those runs on the page being listed (one query per page, only failed workflow runs); the runs list, filters, numbers, API and MCP tool are otherwise as on staging.

How it is built

  • Migrations 0166_workflow_failure_alerts (dated after staging's 0165): alert_* tables, prune indexes, and two triggers (queue a workflow when a run ends in success, error or skipped; record when a paused workflow is turned back on); 0167 (the run that opened an alert, for the Analytics chip); 0168 (partial index on users.step_up_email, @requires-db-prep: create it CONCURRENTLY on staging and add db-prepped-staging before merge); 0169 (default threshold 3, who paused and when a mute started, existing mutes capped at 7 days). lock_timeout is set; no existing table changes except the new index.
  • Once-a-minute internal cron /api/internal/failure-alerts: evaluate, missed schedules, deliver with retries and backups, health checks, prune. Only evaluate and deliver failures fail the job.
  • Rollout flag FAILURE_ALERTS_ROLLOUT (off / all / orgs:<ids> / percent:<n>). Outside the rollout every alert route answers 404, nothing is delivered or checked, and the queue is cleared each cycle.

Verified

  • Unit: the full unit suite has no failures beyond the 31 that also fail on staging with a local .env; 477 alert, notification, analytics and settings tests after the latest changes.
  • DB (real Postgres): 152 tests on the latest head (alert tests incl. concurrent pauses, step-up privacy, a workflow's own dead method, history paging and every filter, the alerts-only log and undelivered count, recorded tests, and that only the triggering run gets the Analytics chip, for its own org only; plus the analytics filter and checksum tests); earlier: Pyth 15, executor 221, integration 1,298.
  • Playwright failure-alerts.test.ts (15 tests) on a production build: the three tabs, rules saving themselves with Undo, a refused method, the Status switch, Sent alerts, the workflow tabs, the Analytics chip opening Sent alerts for its workflow, and no sideways scroll on every tab at 390, 768 and 1280 (earlier also 1024 and 1920). Also checked at 1280 and 390: stacked banners while paused, the Status card states, Sent alerts table, filters and paging, Preview per method, Add method keeping the editor in place, and the Settings menu drawing all items at once. Owners and admins can change every setting (route test per write); plain members get 403.
  • Migrations applied to an empty database; drizzle-kit generate reports no drift.
  • Ten internal review rounds (engineering, QA, UX and customer, repo conventions, and an outside reviewer); the last round is ready from all five.

Not done yet

  • Mark ready for review.

Every organization is told when a workflow that runs on its own fails
5 runs in a row: by default the owners get an email, with no settings
row needed, so existing organizations are covered too.

- A trigger on workflow_executions queues the workflow whenever a run
  ends, which covers every code path that finishes a run.
- A once-a-minute internal cron (failure-alerts) derives the streak from
  run history, opens one incident per streak (unique open per workflow),
  and resolves it on the next automated success.
- Delivery goes through an outbox with retries (1, 2, 5, 15, 30 min, then
  hourly for 24 h) and error classes. A message still undelivered after
  15 minutes (2 for PagerDuty), or failed for good, goes to the backup
  channels and to email.
- Dead channels are paused and the org is told once. A weekly silent
  check finds them without sending anything.
- Free plan: 100 alert emails a month; only accepted emails count.
- Rollout via FAILURE_ALERTS_ROLLOUT (all on staging and PR envs, off in
  prod). Only failures after an org is switched on count, and owners of
  orgs with earlier runs get a one-time notice listing what is failing.

sendEmail now has a detailed variant that reports the provider status,
and backup-channel outcomes carry the HTTP status and whether to retry.
…history

Settings, Notifications gets a Workflow failure notifications card:
alert after N failed runs in a row (default 5) with an optional time
limit, daily reminder and recovery switches, member email list (wallet
members without a verified email shown but not selectable), and
Slack, Discord, Telegram and PagerDuty channels, each set to Always or
Only as a backup. Every channel has a Test button, also before it is
saved; a new channel shows Untested until a test passes. The PagerDuty
test defaults to a change event that pages nobody. The email test goes
to you, or to everyone ticked. Tests are rate limited, never count
toward the Free plan's emails, and are audited.

A Recent notifications card lists every alert, reminder, recovery and
backup with where it went and what happened, filterable by channel,
50 at a time, with a CSV export.

A workflow's Properties get a Notifications on fail section: the org
default it inherits, a failing-streak line, Custom (per field, so
untouched fields keep following the org) and a time-boxed mute of up to
30 days.

Owners and admins of an org that nobody could be told about get a
dark red banner linking to the settings, and a Workflows failing item
in the user menu. Alert links open the workflow on its Runs tab.

The channel editor reuses the PagerDuty node's service picker, the
connection dialogs and the integration icons.
- The evaluator's queue lease went through a raw query as a JS Date,
  which the sql template sends as its local-time display string and
  reads back in the process's time zone, so outside UTC the lease never
  matched and the release query failed. The lease now stays in
  Postgres's own text form and the recheck time is sent as ISO.
- Counting starts ten minutes before the evaluator first sees an org,
  so the failed run that brought the workflow to it is counted.
- A failed PagerDuty page sends the backup email even to people who
  already have the alert by email: it means nobody on call was woken.
- One-time notices (notifications switched on, a channel paused) no
  longer count toward the Free plan's monthly alert emails.
- History rows say which channel a pause notice is about and why a
  backup was sent; the CSV export gains a note column.
- The paused-channel line no longer doubles the full stop, and a failed
  test no longer repeats the HTTP status the message already names.
- The mute form's button is Mute alerts, so it is not confused with the
  Mute button on the failing banner, and the mute end shows date and
  time without seconds.
Reviewed from the user's side and cut down:

- The settings card saves as you go, with a Saved indicator. A channel
  that was added and tested can no longer be lost by leaving without
  pressing Save at the bottom of a long card.
- The card shows the threshold, who gets email and the channels. The
  time limit, daily reminder and recovery message move under More
  options; the time limit is a checkbox instead of an Off dropdown.
- One Send me a test email button instead of a button plus a recipient
  picker.
- A paused channel shows one Paused badge, and the general untested
  warning and the delivery paragraph are gone; How delivery works
  expands on demand.
- Telegram explains how to find a chat ID.
- In Properties, the Org default / Custom / Muted tabs are replaced by a
  summary and two buttons: Mute (1 hour, 24 hours, 7 days, until it
  works again, or a date, one click) and Customize. A mute that has
  ended no longer shows as muted.
Correctness
- Incidents of workflows that are paused, deleted or switched to a
  manual trigger are closed by a sweep each cycle; before, nothing
  noticed and reminders and PagerDuty pages went on.
- Every way an incident closes resolves its PagerDuty page on the same
  key, whether or not recovery messages are on; pages still in flight
  get the resolve slightly later.
- An incident and its alert rows are written in one transaction, and an
  open incident with no alert after two minutes is re-queued.
- Grouped alerts use the org's backup channels.
- Queued messages carry a unique dedup key, since a unique index over a
  nullable incident id let grouped alerts and notices duplicate.
- Rows are sent in small batches and each row's clock restarts before
  its send, so a slow batch is never taken over and sent twice.
- A refused email key on our side keeps retrying instead of failing.
- A PagerDuty service in maintenance counts as not delivered, so the
  backups fire, without pausing the channel; pages use the node's own
  event builder.
- Settings saves run in one transaction under a per-org lock; channels
  are unique per destination, and a duplicate is refused with a message.
- A test of a saved channel uses that channel's stored connection.
- OAuth tokens are held to their organization like API keys; the
  workflow route authenticates before looking the workflow up.
- Deleting a connection no longer deletes its channels silently: the
  next send pauses the channel and tells the org.
- The launch notice is claimed on its own flag, so orgs that saved
  settings first still get it.
- The cron route answers 500 when a stage failed; the unread heartbeat
  table is gone. Old history is pruned after 90 days.

Reuse
- One org-recipients module for quota warnings and failure alerts, the
  existing getOrgRole, a shared emailLayout in lib/email, the PagerDuty
  change-event path and Slack retry rule shared with the plugins, view
  types and labels shared between server and browser, Notice moved to
  components/ui and used by PagerDuty fields and this feature, roleLabel,
  timeAgo and truncateAddress.

UI and copy
- Plain-language provider errors, org name first in chat, settings link
  in every alert email, Open the workflow link, RPC and gas hints.
- Failing now list, error states with Retry, phone layouts for channel
  and history rows, banner link always visible, unshared connections
  explained, Undo after removing a channel, focus kept when Mute or
  Customize opens, and accessibility fixes.
- No alert, reminder or backup for an incident that already closed; every
  attempted PagerDuty page is resolved, and closing an incident is one
  transaction
- One delivery budget per cycle, checked before every row; unreached rows go
  back to the queue; retry and delivery times use the time of the send
- Reminders oldest first, skipped incidents move on; inactive-incident sweep
  filtered in SQL; lost-alert requeue ordered and rollout-aware
- Trigger rules reuse the workflow store; shared scope and recipient helpers
- A channel whose connection was deleted no longer blocks saving other
  settings; Undo puts a paused channel back paused; removals run before
  updates; duplicate channels answer 409
- Failure notifications in the phone workflow sheet; Escape closes Mute and
  Customize; "Test failed" badge
- Consistent wording, history labels matching the emails, shared date
  format with time zone, warning colour defined once, toasts replace each
  other, plainer Telegram help, grouped email links to all failing workflows
- Indexes: drop a redundant one, add incidents(workflow_id), backup index
  without delivered rows
…fications in analytics

- Per-workflow "Pause this workflow when it reaches the limit" in the
  Customize panel. When its alert goes out the workflow is turned off (the
  Enable switch's column), the alert says so, and the pause is audited. The
  incident stays open while the workflow stays off; if it keeps failing after
  being turned back on it is paused again and people are told. A mute does
  not stop a pause from being announced. There is no org-wide equivalent.
- Analytics runs list: failure-notification deliveries as a third source,
  "System notifications", included by default, filtered by status, search
  and project, excluded by network, gas and duration filters; KPI cards and
  charts still count runs only. MCP list_executions and API docs updated.
- Message kind labels and history notes shared from view-types.
…r saves

- Alerts is its own settings page, readable by every member: When to notify
  (rules with their own Save), How to notify (one card per method: icon,
  target, status, Test, Save; email cannot be removed), Failing now, Recent
  notifications; Notifications keeps the Execution digest and a pointer
- A new or changed Telegram, Discord, Slack or PagerDuty target is checked
  before it can be saved; channels are checked daily and a paused channel
  that works again comes back on its own; a single chat method with nobody
  reachable by email gets a backup suggestion
- Org-wide pause for maintenance (up to 7 days, with a reason): people are
  told when it starts and ends, nothing alerts or auto-pauses meanwhile, and
  what kept failing is reported on resume
- Scheduled workflows that do not start within 10 minutes of their cron or
  interval are reported, and close with a running-again message; a reaper
  watchdog pages our on-call if alerts stop being processed
- Alerts read as urgent: "Action needed: X failed 5 times in a row", the
  runs it succeeded before, the last error and direct links to the failing
  workflow and its runs; email, chat and PagerDuty share one text; tests send
  this real format marked [Test]; "See what people get" preview
- Pause fixes from review round 3: pausing a workflow that is already failing,
  a resume recorded by a trigger on workflows.enabled so a later manual pause
  closes the incident, one transaction for pause + claim + message
- Analytics: notification rows keep their own labels, never count in totals,
  facets or KPIs; compound cursor so paging never skips tied rows; faster
  project and search filters
Server
- Grouped alerts only for workflows that follow the org's people and
  methods; PagerDuty group field and warning severity for missed runs
- "Until it works" mutes end after 30 days; stale saves refused (version,
  409) so two admins cannot undo each other's changes
- Org pause still turns off workflows set to pause at their limit and holds
  the message until the pause ends; extending a pause is race-safe
- Free plan: the last 10 emails kept for new failures, warnings at 80 and
  100, missed runs never counted; reachability ignores methods whose
  connection is gone and a used-up email cap
- Missed runs: schedule-aware grace (2x interval, 2 to 10 min), due check
  before loading settings, rotating scan, scheduler outages page our team
  first and reach customers after 30 min, "started again but failed"
- Wallet out of funds: two shortfalls in a row (chain or pre-send check)
  alert at once with a fund-the-wallet next step
- Telegram chat ID format checked on save; Slack's daily check no longer
  re-enables a paused channel; history of open alerts is never pruned;
  exhaustive method switches; per-cycle limits for the launch scan
- Messages: one vocabulary (Alerts), reminder N of 7 with a last-reminder
  line, amber Check for missed runs, per-workflow errors in grouped alerts,
  recovery with duration, correct gas hints, email-limit warning email
- Notifications status exposes alertsEnabled; API and MCP list alert rows
  only when asked; shared delivery state labels

UI
- Alerts page: Status card (all quiet, failing, paused, unreachable, single
  method, email limit), rules, collapsed method rows with honest states and
  last checked/delivered, workflows with their own settings, sent alerts
- Hidden when alerts are off for the org; stale-save Reload; unsaved
  changes guard; chat ID validation; org pause in the workflow panel;
  width fixes

Also: comments trimmed across the branch's server code
Analytics lists runs only again, so the card is titled Workflow Runs and the
list_executions source takes workflow and direct, as on staging.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

…n, saves re-queue only on rule changes

- A wallet member's step-up inbox no longer leaks: the Sent alerts To filter
  matches what the log shows, and the Test reply and stored provider text are
  masked the same way (one shared helper).
- A refused incident closes only on a run that was really accepted (shared
  acceptedRunSql), not on a phantom, pending or platform-failed row.
- The launch notice is claimed and queued in one transaction.
- The Analytics "Alert sent" chip uses the run that opened the incident, stored
  once in the new opening_execution_id column (migration 0167, with an index).
- Org and workflow saves re-queue evaluation only when something the evaluator
  reads changed (threshold, time limit, mute, auto-pause), not on recipient or
  method edits.
- Docs describe the runs list's optional alert object.
Workflow panel:
- Saves go one at a time, each on the latest version; a draft is sent once.
- An edit is sent when the panel closes; an unfinished or failed one says it
  was not kept.
- "Use org rules" can no longer be undone by Run, Save or closing the panel,
  and a failed switch says so.
- The tab changes only when the rules in use change.
- A failed save shows a toast and "Not saved", with Try again only for server
  or network failures.
- Alert edits keep their own unsaved flag: the Save dot shows it, Run's save
  check does not, so an alert save never hides an unsaved canvas.
- A Test result applies to the method list as it is now.

Org page:
- A newer save moves only the fields the user has not edited.
- An older reply is ignored.
- An invalid value shows "Not saved" instead of "Saved".

Cleanup: dead Save props removed, one warning palette, one refusal helper.
const canManage = data.canManage;
const rulesBlocked =
canManage &&
rules &&
A "not running" incident no longer ends on a phantom placeholder or a queued
row no worker has claimed, and such rows no longer hide a schedule that stopped
starting runs from the missed scan. A run our platform failed still counts as
started: the schedule fired, and counting it keeps a platform outage from
telling every customer their schedules stopped. latestRunSince takes a named
mode ("started" or "accepted") instead of a positional flag.
Sent alerts masks wallet members' step-up inboxes by looking users up by
step_up_email on every page and To filter; the column had no index. Partial on
step_up_email IS NOT NULL, so it holds only wallet members with an inbox.
Marked @requires-db-prep: users is a hot table, so an operator creates it
CONCURRENTLY on staging and prod before merge and the migration is a no-op there.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

…s gets alerts

Silencing a workflow can no longer go unnoticed:
- A mute lasts at most 7 days, "until it works again" included, and needs a
  reason (3 to 200 characters). Existing mutes are capped by migration 0169.
- Muting, changing, unmuting and the end of a mute send a notice to the people
  who got the workflow's alerts before the change plus every owner (or
  reachable admins when no owner can get email), queued in the same
  transaction, shown in Sent alerts. At most 6 mute changes per workflow per
  hour.
- When a mute ends, the notice gives the runs that failed meanwhile, and a
  workflow still failing is alerted again at once.
- People taken off a recipient list are told, also when a list leaves the
  default, and so are the owners.
- Separate audit actions for mutes and unmutes, with before and after.

Recipients and defaults:
- New orgs alert after 3 failed runs and email the owners.
- Every list keeps an owner or admin who can get email; people who leave
  drop off, and a list with nobody left goes back to the owners.
- Who paused alerts is shown with their sign-in account.

Sent alerts can be filtered by workflow name or ID. The workflow and history
APIs are documented in docs/api/failure-alerts.md.
…le tabs

- The workflow panel has one Mute area outside the Org rules / Custom rules
  tabs: a reason is required, and the form says who will be told. Muted, it
  shows who, until when and why, with Unmute and Change or extend.
- Overview's Needs attention has Setup and Workflows subsections in one
  format; every active mute is listed (failing ones first, in red) with an
  Unmute button.
- Org rules and Custom rules read "Who gets alerts" then "When to alert",
  with the reminder and recovery settings; "Disable workflow" wording.
- The last owner or admin who can get email cannot be unticked.
- Sent alerts: workflow filter, mute notes, clearer By column on phones.
- Alerts page copy: scope note, workflows can use their own setup, Status
  description, no duplicate "Alerts reach" or "Last alert delivered" lines.
- The settings column keeps its width when a tab scrolls.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

A mute and a success created in the same instant raced the database clock
against the test runner's, so "ends only on a success after it began" could
miss in CI. The tests now start the mute minutes earlier.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

Reserving it on phones shifted every settings page 10px off centre, where
scrollbars overlay anyway. Tablets and desktops keep it, so a tab that scrolls
is no narrower than one that does not.
…mid-word

Sent alerts:
- Four columns (When, What, Sent to, Result): who acted shows under the kind,
  the method icon sits before the address.
- "Alerts only | People's actions | Everything" replaces the pause-notices
  chip; People's actions (mutes, pauses, removals, tests) is a new history
  scope, documented in docs/api/failure-alerts.md.
- "All delivered", or "N not delivered" as a button that shows only those.
- Mute and pause rows always give a reason or "No reason given"; mute notes
  read "Mute ran out · no failures while muted" and show the end in local time.
- An empty result says so inside the table, with Clear filters.
- Relative times with the exact time on hover, the viewer's time zone in the
  header, a sticky header, one toolbar row; "Alert" is now "New failure".

Emails break only before "@" (never mid-word or at a hyphen) in Sent alerts,
the Email card and both recipient pickers, which now show the name with the
email below it.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

@zdumitru

zdumitru commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator Author

Since the review, e36ec44, 09b5aca, 275eb18, 2dfb1f8 and 7e4510f address the comments above. Later commits add follow-ups from the owner. A mute now needs a reason, lasts at most 7 days, is announced to the people who would get alerts, and shows in Needs attention. There is one mute control, outside the rule tabs. An owner or admin always gets alerts (f705e1e, 9a21bc5). The Sent alerts log is more compact (e80f622). Migration 0168 is @requires-db-prep: the staging index has to exist before db-prep-check passes.

… the org rules

- Email different people opens the same Email card as Settings > Alerts.
- The workflow's connection picker lists the viewer's unshared
  connections greyed out, as Settings does.
- Testing a saved workflow method reloads its health; drafts stay.
- The Org rules tab drops "Change them in Settings"; the footer link and
  the unreachable notice both open Setup.
…od save

- page above 100000 is refused with 400 instead of overflowing the offset.
- After a Test, methods' health is merged only from a reply of the same
  version, so an older reply cannot bring back a method's old target.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

…r digest

- Rows of orgs outside this process's rollout wait and retry instead of
  being skipped, so a widening rollout mid-deploy loses no alert.
- Mute ending counts runs the way the streak does; a deleted workflow's
  or deactivated org's mute ends without a notice.
- Mute and pause ends a few minutes past 7 days are trimmed, not
  refused; ends without a zone are read as UTC.
- Names, reasons and subjects in every message are one line with no
  control or bidi characters; mute reasons are cleaned when saved.
- Daily reminders for org-rule workflows go out as one message per
  person, like first alerts.
- Mute errors show in the mute panel; a JSON body that is not an
  object gets 400; an edit made just before a reload is still saved.
- Emails wrap only before "@" and are never cut off; touch targets,
  landscape banner text and several labels follow the UX review.
- Migrations folded into 0166 (alert tables) and 0167 (users index).
- Org-level routes documented; CSV, wallet and name helpers reused.
- Daily reminders: reminders due later the same day join the org's one
  message; fewer than 3 left after claiming go out one by one; claims
  in one lock order; grouped and single reminders fail separately.
- Digest reads as a daily reminder: counts add up, not-running ones are
  listed first, it says how to stop it, no "shared cause" line.
- A mute ending on refused runs no longer says the workflow works.
- Prune keeps grouped rows with two indexable conditions; rollout-held
  rows move in one update and expire as outside the rollout.
- Overview says "stopped working" for a method that failed but is not
  paused; a muted row only adds "Still failing" when it is.
- A reload during the autosave delay asks first; one refresh path with
  one failure message; the reload send shares the save's body.
- Emails keep their brackets on the same line; tablet banner uses the
  short text; mute buttons are 44px tall on touch; clearer clock-ahead
  refusal and mute helper text.
- parseEndTime in its own module; nameOf used in the pause route and
  test sends; API docs for optional version and the reminder flags.
- Reminders due later the same day join only when that makes a shared
  message; none goes out early on its own. A claim skips an incident
  closed since it was read; a group that drops under 3 gives every
  claim back and the due ones go out alone.
- A save leaving out remindDaily or notifyRecovery keeps them as they
  are, like channels.
- Leaving with an unsaved edit starts the save while the page is alive
  (keepalive), so leaving or staying it lands.
- The panel says a muted workflow is still turned off at its limit;
  muted Overview rows are yellow; the digest says how to stop it and
  its plain text reads "In <org>, ...".
- Email pattern without nested repeats; prune checks incidentIds is an
  array; nameOf beside pauseActorName; shared HOUR_MS and the digest
  window in constants; own toast id for refresh failures.
- Digest reads "N workflows are still failing" and names the "Remind
  once a day" setting; Overview lists unmuted failures before muted.
- The reminder pull-in query returns only rows not yet due.
- A page going away sends a draft still queued behind another save,
  and never a draft that already saved or failed.
- The pause form uses the server's limits and shared time units; the
  audit leaves out reminder flags a save did not send.
- Tests: the digest wording, reminder flags kept when left out, and
  that the save leaves before the "Leave site?" answer.
- Leaving with an edit queued behind a running save sends it at once
  while the page is alive (a stale version is refused, never
  overwrites); otherwise the save starts as before.
- The last saved draft is forgotten when stored rules are replaced, so
  a page going away never skips a real change.
- The leave test checks the saved value, staying and leaving; shared
  HOUR_MS in the workflow panel.
@zdumitru

zdumitru commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator Author

Changes since the last summary (e80f622 to 038a873):

  • Migrations folded: the alert migrations are now one, 0166_workflow_failure_alerts, plus 0167_users_step_up_email_index (the former 0168, still @requires-db-prep). Nothing here has shipped, so staging and prod apply the two files; databases that already ran the old 0166-0169 skip them and end with the same schema.
  • Workflow panel: own email recipients use the same Email card as Settings; unshared connections are listed greyed out; a Test refreshes the method's status; one link to the org rules.
  • Daily reminders: org-rule workflows still failing share one daily message per person instead of one each.
  • Delivery and mutes: rows of an org outside a process's rollout wait and retry instead of being skipped (safe while a rollout widens mid-deploy); a mute ending counts runs the way the streak does and says a workflow whose runs are refused is not working; a deleted workflow's mute ends without a notice.
  • Hardening: names, reasons and subjects in every message are one line with no control or bidi characters; mute reasons are cleaned when saved; end times without a zone are read as UTC; a JSON body that is not an object gets 400; Sent alerts pages are capped; the CSV export reuses the shared formula-safe cell helper.
  • API: PUT /api/organizations/{organizationId}/failure-alerts keeps remindDaily and notifyRecovery as they are when left out (like channels); version is optional. The org-level routes (settings, pause, test, preview) are now in docs/api/failure-alerts.md.
  • UI: emails wrap only before "@" and are never cut off; mute errors show in the mute panel; a reload or tab close during the autosave delay asks first and the edit is saved either way; touch targets, the landscape banner and several labels were tightened.

Checks on 038a873: 47 browser tests, 327 unit tests and 196 DB tests pass locally; lint, types, API docs and schema drift are clean.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

This branch was previously deployed

1 inactive deployment
staging — 038a8735 Deployed Nov 6, 2026 by zdumitru via e2e-tests / protocol-coverage-floor #6884
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Triage: reviewed, changes needed from the contributor deploy-pr-environment no-issue-required PR exempt from the issue-first gate run-e2e-tests-ephemeral

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants