The Incident Inbox is the single fleet-wide surface for every active and historical incident on your account, reachable from any page via the header bell or the /app/inbox route. It introduces an acknowledge sub-state that silences push notifications only for an incident the operator has accepted, while Slack, email, and webhook keep flowing for the team's audit trail. Every closed incident retains its AI diagnosis, closure cause, and timestamps - so the postmortem question "what fired this morning?" becomes a one-tab lookup instead of a Slack archaeology session.
The Inbox is the fleet view; the per-monitor Active Incident panel is the deep view. Both surfaces share the same backend data and the same AI handoff CTA.
Why this exists: the cross-monitor triage gap
The per-monitor Active Incident panel shipped first because most users start with 1-3 monitors. An Agency operator running 30+ monitors for clients hits a workflow wall the per-monitor surface cannot solve:
- No fleet view. The dashboard is a monitor view; it shows current health per row but does not list "what's open across the whole portfolio." Operators iterate row-by-row, eyeballing badges.
- No acknowledge state. Once an incident fires, push devices keep buzzing on every check-tick transition. There is no "I see it, I'm working it" verb that mutes the page without resolving prematurely.
- No closed archive. Once
monitor.healthflips back to green, the per-monitor panel disappears. The AI diagnosis, the regression context, and the closure cause vanish from the UI - even though the data persists on the document.
The Inbox closes all three gaps with one model change (a nullable acknowledgedAt timestamp on the existing Incident document) and two surfaces (the bell + the page).
The three-state lifecycle
Every incident moves through 3 states, computed from 2 fields on the document (status and acknowledgedAt):
| State | Definition | Push | Slack / Email / Webhook | Visible In |
|---|---|---|---|---|
| Open | status='open' AND acknowledgedAt unset | Fires | Fires | Open tab + bell badge |
| Acknowledged | status='open' AND acknowledgedAt set | Silenced | Fires | Acknowledged tab |
| Closed | status='closed' | n/a | n/a | Closed tab |
The split is intentional: Acknowledged is semantically still open for downstream systems (alert continues, AI diagnosis quota remains locked, the Active Incident panel keeps rendering). The bucket is purely a UI shape so the operator can scope what they're looking at.
The acknowledge asymmetry: push silenced, audit trail intact
Acknowledge silences push notifications only. Slack, email, and webhook continue to fire on every transition for the duration of the incident. This is the deliberate asymmetry that separates UpMonitor's acknowledge from the all-or-nothing model in legacy paging tools.
Why selective silencing is the right call
- Push is paging-grade. It exists to wake an on-call engineer. Once the engineer says "I see it," waking them again is noise - they'll work it until it's resolved.
- Slack, email, webhook are audit-grade. A team running an incident channel needs every state-change post to land in
#incidents, even after one teammate has acknowledged. Cutting Slack on ack would orphan the channel. - Webhook is automation-grade. A downstream pipeline (paging escalation, status-page updater, AI agent) needs the full state stream to reason correctly. Silencing the webhook on ack would make the operator's manual action invisible to the automation.
The product copy mirrors this: the toast on a successful acknowledge reads "Push silenced. Slack and email continue for the audit trail."
How it works under the hood
Every time the alert pipeline is about to enqueue a push notification, the dispatcher reads the current open incident for that monitor and skips push if acknowledgedAt is set. Slack, email, and webhook calls are independent awaits in the same dispatch function and are unaffected. A failed incident lookup defaults to "send push" - the alert path is never broken by an infra hiccup on the inbox query.
The header notification bell
The bell renders in the topnav for every authenticated user. It serves three jobs:
- Awareness. A red badge on the bell means "something is open and not yet acknowledged." A bell without a badge means "all clear."
- Quick triage. Clicking the bell opens a 5-row dropdown showing the most recent open-and-not-acknowledged incidents. Each row deep-links to the relevant
/app/monitor/<id>for the full Active Incident panel. - Hand-off to the inbox page. The "View all incidents" footer link goes to
/app/inbox?tab=openfor the full triage surface.
Polling cadence
The bell polls GET /v1/incidents?status=open&acknowledged=false&limit=5 every 60 seconds while the app is foregrounded. On visibilitychange → visible (the user returns from another tab or app), the bell refreshes immediately - so a coffee-break operator sees the latest state without waiting for the next interval.
The 60-second polling window is the deliberate trade. Realtime listeners (onSnapshot) would force the Firestore SDK into the topnav bundle on every page; REST polling matches the existing app pattern and the cost stays predictable. Up-to-60s staleness on the bell is acceptable for a triage surface; the Active Incident panel is the real-time path once the operator clicks through.
Why the bell is hidden for guests
Unauthenticated visitors see no bell. There is no incident scoped to a guest, the polling endpoint requires auth, and a flickering empty bell on the landing page would only train users to ignore it. The bell appears the moment a user logs in - and the first incident they ever encounter is the surface where they meet it.
The /app/inbox page
The page extends the bell's three-row preview into a full triage workbench with 3 tabs, 2 multi-select filters, 1 date-range filter, and cursor pagination.
Tabs
- Open - count badge equals the bell badge. Rows show the failing checker chip, severity dot (red or amber), opened-ago timestamp, and an 🤖 indicator when an AI diagnosis is attached. Three row actions: Acknowledge, Resolve, View.
- Acknowledged - the same row layout, minus Acknowledge (already done). Two row actions: Resolve, View. Push remains silenced for these incidents until they close.
- Closed - historical archive. One row action: View. The chip strip swaps "opened-ago" for "closed-ago" and adds a closure cause: "Resolved by you" or "Auto-recovered."
Filters
- Monitor - multi-select chips populated from your monitor list. Empty selection means "all monitors I own."
- Checker - multi-select chips populated from the canonical checker registry. Filter to "show me every SSL incident" or "show me every DNSSEC incident" without scrolling.
- Date range - visible only on the Closed tab. Last 7 / 30 / 90 days / All time discrete buckets. Closed-tab data can grow indefinitely; the bucket lets you scope retrospectives without paginating through a year of green-resolves.
Filter state is mirrored to the URL query string (/app/inbox?tab=open&monitor=abc,def&checker=ssl). The URL is reload-safe and shareable - paste it in Slack, the recipient lands on exactly your view.
Pagination
The list loads 25 rows at a time with a "Load more" button. Pagination uses an opaque cursor (the previous page's last sort key plus the document id) so new incidents arriving at the head do not duplicate or skip rows mid-scroll.
Re-open semantics: a new incident, not a re-flip
If you manually resolve an incident while the monitor is still red, the next non-green check from that monitor opens a brand-new incident (a separate document with its own ID, its own AI quota allocation, and its own row in the Open tab). The old incident stays Closed.
Why a new document instead of a flip
- Clean lineage. A flapping monitor that ping-pongs red → resolved → red 5 times in an hour produces 5 rows. Each row reflects an explicit operator intervention. Squashing them into one row that re-opens hides the churn signal.
- Quota-correct. The AI Diagnostic quota is one diagnosis per incident. A new incident gets a fresh slot - because manually resolving while still red is the operator saying "I gave up on the manual fix; let the AI try again." That's exactly when a new diagnosis should run.
- Audit-correct. A re-flipped document would lose the original
closedAttimestamp - destroying the historical record of how long the manual attempt lasted.
If the monitor is already green when you click Resolve, the incident closes cleanly and no new one opens.
How this differs from PagerDuty, Better Stack, and Datadog
The competitive cluster all ship some form of acknowledge, but the asymmetries differ. UpMonitor's value lives in 3 specific gaps competitors leave open:
Gap 1: Selective channel silencing on ack
- PagerDuty - acknowledge silences the page for the on-call but the incident page in the dashboard stays the source of truth. Slack notifications via PagerDuty's app are configurable but coupled to the page state. (PagerDuty acknowledge docs)
- Better Stack - "claim" stops further escalation but the Slack thread for the incident keeps updating. The asymmetry exists, but it's an emergent property, not a stated guarantee.
- UpMonitor - asymmetric silencing is a stated product guarantee. Acknowledge silences push only. Slack, email, and webhook continue unconditionally. The toast says it. The docs say it. The behavior matches the words.
Gap 2: AI diagnosis preserved on closed incidents
- PagerDuty / Datadog - the incident timeline records the events and any human-added notes, but the AI-generated root cause (when present) is bound to the active state and rolls off the dashboard once closed.
- UpMonitor - the AI Diagnosis lives on the Incident document and renders identically in the Closed tab. The 🤖 chip, the root cause, and the remediation prompt are all available for retrospective review without re-running anything.
Gap 3: Re-open creates a new lineage
- Most platforms - "re-open" or "un-resolve" reuses the same incident ID. A 5-flap incident shows as 1 row that opened-and-closed 5 times.
- UpMonitor - a new non-green check after manual resolve creates a new incident document. The Closed tab shows 5 separate rows, each with its own closure timestamp, closure cause, and AI diagnosis. The flap is visible at a glance.
Endpoint reference
The Inbox is built on three new endpoints, each scoped to the authenticated user:
| Method | Path | Purpose |
|---|---|---|
GET | /v1/incidents | List with filters: status, acknowledged, monitorId, relatedChecker, since, limit (max 100), cursor. |
POST | /v1/incidents/:id/acknowledge | Sets acknowledgedAt. Idempotent on already-acknowledged. 409 on closed. |
POST | /v1/incidents/:id/resolve | Closes the incident manually. 409 if already closed. |
Authorization is owner-only at the repository layer - cross-tenant access is impossible. The legacy POST /v1/monitors/:id/incident/resolve is preserved unchanged for back-compat with the per-monitor resolve button.
Required permissions and quotas
The Inbox itself is unmetered - listing, acknowledging, and resolving incidents do not count against any quota on any tier. The AI Diagnosis attached to each incident still respects the monthly diagnostic quota (Free 5/mo, PRO 50/mo, Agency unlimited), but listing or acknowledging an incident never burns a slot.
The 60-second bell poll is rate-limited under the same shared 20-requests-per-minute budget as every other authenticated endpoint - well below the cap for any normal usage.
Related
- Active Incident - the per-monitor deep view; the Inbox is the fleet view, the Active Incident panel is the diagnostic detail
- Model Context Protocol (MCP) Integration - how the UpMonitor MCP server exposes incident context to AI agents
- Agentic Setup Guide - per-tool install for Claude Code, Cursor, Claude Desktop, ChatGPT
- CLI Reference -
upmonitor monitor show <id>and theupmonitor://protocol handler for shell-side triage