Skip to content

feat(statuspages): drag-and-drop resource reordering + dash conventions - #41

Merged
fclairamb merged 411 commits into
mainfrom
docs/competitor-research-and-size-rule
May 8, 2026
Merged

fclairamb merged 411 commits into
mainfrom
docs/competitor-research-and-size-rule

Conversation

@fclairamb

@fclairamb fclairamb commented May 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

Status pages

  • Drag-and-drop resource reordering with @dnd-kit (PointerSensor + KeyboardSensor for accessibility). The grey GripVertical icon is now a real drag handle; existing up/down chevrons keep working.
  • New backend endpoint `POST /api/v1/orgs/$org/status-pages/$page/sections/$section/resources/reorder` that renumbers in a single transaction and rejects mismatched UID lists with `VALIDATION_ERROR`.
  • Sequential-position migration (postgres 020, sqlite 019) — every resource and section was created with `position=0`, so the swap-positions logic was a no-op. `CreateSection`/`CreateResource` now compute `max(position)+1` when omitted.
  • Optimistic cache update on reorder so dnd-kit's drop animation doesn't briefly snap the dragged row back to its old slot before re-rendering.
  • Searchable CheckPicker replaces the plain Select in the Add-Check-to-Section dialog (debounced server-side search, keyboard-navigable, exclude-uids).

Dashboard conventions

  • Started edit-on-route convention (editing always navigates to a dedicated route, never opens a modal). Documented in `web/dash0/CLAUDE.md`; OnCallScheduleForm extracted as the shared component for create + edit routes (~260 lines removed from `on-call.new.tsx`).
  • Name-first auto-slug helper extracted to `@/lib/utils` (`slugify`) and adopted by escalation-policies create form. Slug auto-derives from name until the user types into it. Documented in `design-reference.tsx` so future forms copy the exact shape.

Specs (`specs/todos/`)

Five spec drafts covering alerting/incident design ideas — each independently shippable:

  • `2026-05-08-01` validating-check-state — transient `validating` status surfaces "failure noticed, confirmation pending"
  • `2026-05-08-02` time-based confirmation/recovery periods — replace count thresholds with wall-clock seconds
  • `2026-05-08-03` severity primitive for escalation steps — bundles channel-types so a step can "page critical"
  • `2026-05-08-04` outage-vs-incident split — separate operational alerts from customer-facing timelines (multi-phase)
  • `2026-05-08-05` on-call multi-rotation + override-as-event

Docs (housekeeping)

`docs/CLAUDE.md` size rule (500-line soft / 800-line hard cap) added and applied; long competitor docs reorganized into per-topic subdirectories. `docs/research/alerting-patterns.md` consolidates the synthesis used to seed the specs above.

Test plan

  • `make lint` and `make test`
  • Postgres + SQLite migrations apply cleanly on a populated DB; positions become distinct
  • Status-page editor: drag a resource within a section; verify order persists after refresh and the row doesn't snap back during the drop animation
  • Add Check via the new searchable picker; type to filter
  • On-call: open create page, verify extracted form still works end-to-end
  • Escalation-policy create: type a name, slug auto-fills; edit the slug, then keep typing the name — slug stays manually-edited

🤖 Generated with Claude Code

fclairamb and others added 30 commits May 5, 2026 07:09
Closes the audit gap: previously an operator who removed
SP_ENCRYPTION_MASTER_KEY only saw the generic "encryption disabled"
warning. Now the startup auto-migrate path also counts non-NULL
config_private/settings_private rows and emits a separate WARN naming
the count, so the silent failure mode (workers skipping every encrypted
job) is loud at boot rather than mysterious at runtime.
The job runner walks one step of an escalation policy: loads the
step+policy+incident, exits if the incident has been acked/snoozed/
resolved (belt-and-braces; the cancel sweep should already have killed
the row), fans out to targets, emits incident.escalated, and on the
last step of a repeat-eligible policy schedules the next cycle.

Target dispatch:
- connection target → enqueue a NotificationJob (re-uses per-channel
  formatting).
- user / all_admins / schedule → resolve to user emails and send
  directly via the email service. V1 reality: email is the only cleanly
  per-user channel; richer per-user routing waits on a follow-up spec.
  Schedule failures (no users / not yet active) emit
  incident.escalation_failed but do not abort subsequent steps.

The incidentUid key in the job config matches what
jobsvc.CancelPendingForIncident looks for, so ack/snooze/resolve drops
the rest of the policy without extra cancellation wiring.

Includes: GetEscalationPolicyStep DB method (postgres + sqlite),
incident.escalation_failed event type, GetEscalationPolicy now accepts
empty orgUID for runtime callers that only know the policy UID,
SetOnCallResolver seam to be wired by escalationruntime.
…e guard

Hooking incident.created emission: resolve the policy (check's
escalation_policy_uid, then group's, then nil) and schedule cycle 0
of its steps via ScheduleEscalationCycle. The cancel sweep keyed on
incidentUid will drop them on ack/snooze/resolve with no extra wiring.

Wired the on-call resolver as a closure in server.go so the escalation
step job can call oncallschedules.Service.Resolve without an import
cycle (jobtypes → oncallschedules would be one).

DeletePolicy now returns 409 ESCALATION_POLICY_IN_USE when an open
incident's check or group still references the policy — prevents the
"pending escalation jobs reference a deleted policy" footgun.
Two integration tests with an in-memory sqlite DB:

- ScheduleEscalationCycle persists three jobs at the right cumulative
  delays (startedAt + d0, + d0+d1, + d0+d1+d2), with the last one
  flagged IsLastStep so the next-cycle reschedule fires.
- jobsvc.CancelPendingForIncident drops escalation_step jobs by
  matching incidentUid in the config — verifies the headline
  invariant that ack/snooze/resolve cancellation requires no extra
  wiring beyond the existing notification-job sweep.

Also drops bun's `default:0` tag from EscalationPolicyStep.DelayMinutes:
the tag was causing bun to omit non-zero int values from INSERT
statements (every persisted step came back with delay=0 regardless
of the Go value). The DB-level default in the migration is sufficient.
- /orgs/$org/escalation-policies — list of policies with repeat info.
- /orgs/$org/escalation-policies/new — wizard: basics, steps with
  per-step delay and target list (user / schedule / connection /
  all_admins), repeat config.
- /orgs/$org/escalation-policies/$slug — same shape as the wizard,
  loaded from the policy. Surfaces the 409 ESCALATION_POLICY_IN_USE
  error from delete via a try/catch alert.
- API hooks: useEscalationPolicies, useEscalationPolicy,
  useCreateEscalationPolicy, useUpdateEscalationPolicy,
  useDeleteEscalationPolicy.
- Sidebar: adds the entry under On-call, ArrowUpRight icon.
- i18n: escalation namespace in en/fr/de/es; escalationPolicies key
  added to the nav namespace per locale.

V1 frontend deliberately ships with the simpler layout (form rather
than a draggable timeline visual). The data model and APIs back the
richer UI; visual polish lands as a follow-up once shape stabilizes
in real use.
…meline

- Check create/update now accepts escalationPolicyUid (and clears via
  empty string on PATCH). Wired through CheckUpdate model into both
  postgres and sqlite UpdateCheck implementations. CheckResponse now
  surfaces the field so the dashboard can show what's assigned.
- Incident detail page gets a dedicated EscalationTimelineCard that
  filters events by incident.escalated / incident.escalation_failed and
  renders one row per fired or failed step with the position and repeat
  cycle, separately from the generic event log.

Group-level UI assignment and the keyEscalationThreshold rename
(adaptive-resolution vocabulary cleanup) deferred — they're flagged
in the spec's risks section and tracked separately.
Closes the v1 spec gap from the previous pass:

- AnnotationCanvas: pointer-driven overlay over the captured screenshot
  with normalized [0,1] coordinates so drawings stay accurate when the
  preview is resized between draw and submit.
- AnnotationToolbar: select / rect / arrow / text + undo. Tools render
  in red (#ef4444) at 3px stroke.
- bakeAnnotations rasterizes the overlay onto a copy of the screenshot
  at submit time so the outgoing PNG ships with the markup baked in.
- renderAnnotations is exported as a pure function — same code paths
  used for the live preview and the baked PNG.
- bug-report.spec.ts: four scenarios — desktop happy path, keyboard
  shortcut, mobile event-triggered (toolbar hidden via sm:block), and
  feature flag off (icon hidden, shortcut no-op).
- i18n: tool_select / tool_rect / tool_arrow / tool_text / undo for
  en/fr/de/es.
Send membership_request_new.html to all org admins on request create or
re-open; send membership_request_decision.html to the requester on
approve or reject. Failures are logged but never block the API call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…letion

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers: create / re-open / duplicate / unknown org; owner cancel and
non-owner forbidden; approve creates membership and reject persists
reason; admin re-approve of rejected row bypasses cooldown; cooldown
window and cancellation re-request semantics; defensive guard against
a stored unsafe auto-join regex.

Tests caught a real bug: GetMembershipRequest used a non-existent
'mr.uid' alias instead of the bun-generated 'membership_request.uid'
table reference (fixed in both postgres and sqlite stores).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fclairamb and others added 16 commits May 7, 2026 19:56
Routes the same handlers under both /orgs/:org/channels and
/orgs/:org/connections. Identical request/response shapes, identical
auth, identical errors — verified by TestChannelsAliasMatchesConnections.

PR-1 of the channel/connection naming alignment. Subsequent PRs:
- PR-2: dashboard hooks switch to /channels
- PR-3: backend rename (IntegrationConnection → Channel)
- PR-4: dashboard type rename (Connection → Channel)
- PR-5 (deferred): drop /connections alias once external clients move
- PR-6 (deferred): rename DB tables
Switches the five top-level hooks (useConnections, useConnection,
useCreateConnection, useUpdateConnection, useDeleteConnection) to call
/api/v1/orgs/$org/channels{,/uid}. The check-binding paths
(/checks/$check/connections) are unchanged — those aren't aliased yet,
they migrate in PR-3.

The Connection TS type stays on Connection here; PR-4 renames it.
…ynthesis, size rule

Adds a docs/CLAUDE.md hard rule (500-line soft target, 800-line hard cap)
with a 6-step split procedure, then applies it: every competitor analysis
over 800 lines is reorganized into a per-topic subdirectory.

- New: competitors/hyperping/ (6 files) — multi-region confirmation logic,
  outage-vs-incident model, /start heartbeat endpoint, auto Web Vitals,
  i18n at the data-model level.
- Refreshed + split: competitors/betterstack/ (8 files, was 1004 lines) —
  adds depth on confirmation_period, recovery_period (flap-resets), 3-of-N
  region quorum, the four escalation step types (escalation, time_branching,
  metadata_branching, instructions), severities, ack/snooze model, status-page
  subscriber model.
- Mechanical splits: competitors/pingdom/ (was 1235), uptimerobot/ (1068),
  gatus/ (879) — content preserved verbatim.
- New: research/alerting-patterns.md — synthesis of 12 grouped patterns
  worth borrowing (recoveryPeriod, instructions step type, /start endpoint,
  status-page subscribers, multi-region quorum, HMAC-signed webhooks, three-mode
  maintenance, ...). Input for future specs, not a spec itself.
- Indexes updated (README, full-list, comparison) to point at every new leaf
  file. No file remains over the 800-line hard cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Work in progress on spec
specs/todos/2026-05-07-04-edit-on-route-convention-and-oncall-edit-page.md.

- specs/todos/2026-05-07-04-...: drafts the convention (editing always
  navigates to a dedicated route, never opens a modal) and the on-call
  schedule edit-page migration as the proving ground.
- web/dash0/CLAUDE.md: documents the convention so future contributors
  see it immediately.
- web/dash0/src/components/oncall/on-call-schedule-form.tsx: extract the
  shared multi-section form so create and edit routes can both consume it.
- web/dash0/src/routes/orgs/$org/on-call.new.tsx: switched to the extracted
  form (~260 lines removed).
- web/dash0/src/routes/orgs/$org/on-call.{$slug,index}.tsx: prep for the
  new edit route — Edit button targets and detail-page cleanup in flight.
- web/dash0/src/routes/orgs/$org/channels.index.tsx,
  escalation-policies.index.tsx: align listing-page styling alongside.
- locales/{de,en,es,fr}/oncall.json: minor i18n adjustments matching the
  form extraction.

Note: the new route file on-call.$slug.edit.tsx has not been created yet;
this commit captures the in-flight extraction. Finish the spec to
complete the migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ally move resources

The status-page section editor's up/down chevron buttons were no-ops because
every resource (and every section) was created with position=0. The dashboard's
swap logic exchanges position values between two adjacent rows, but swapping
0↔0 leaves the data unchanged; the list-by-position-then-created_at order then
stays frozen on every refresh.

- service.go: CreateSection and CreateResource now compute max(position)+1 when
  the request omits position, via two new helpers (resolveSectionPosition /
  resolveResourcePosition). Behavior when position IS supplied is unchanged.
- db.Service + postgres.go + sqlite.go: new MaxStatusPageSectionPosition and
  MaxStatusPageResourcePosition methods returning COALESCE(MAX(position), 0).
- migration 020 (postgres) / 019 (sqlite): one-time renumber of existing rows
  by (partition by parent ORDER BY created_at, uid). Idempotent on re-runs.
- service_test.go: three regression tests pinning sequential-position
  assignment for resources + sections plus the swap-reorders-list contract.
- slack_test.go: mock stubs for the two new db.Service methods.

The frontend swap logic in status-pages.$statusPageUid.index.tsx is unchanged
— it works correctly once positions are distinct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The grey GripVertical dots on the left of each resource row were a
non-interactive visual affordance. They now act as a real drag handle:
click-and-drag (or keyboard with Space + arrow keys) to reorder resources
within a section. The existing up/down chevron buttons keep working.

Backend
- New POST /api/v1/orgs/$org/status-pages/$page/sections/$section/resources/reorder
  accepting {uids: [...]} that renumbers in a single transaction.
- Service layer validates the supplied UID list is exactly the section's
  current resource set (no missing, extra, or duplicate UIDs) before
  applying — returns 422 with VALIDATION_ERROR otherwise.
- New db.Service method ReorderStatusPageResources using bun.RunInTx.
- Two regression tests pin the rewrites-positions-by-UID-order contract
  and the mismatched-UID rejection.

Frontend
- @dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilities added.
- DndContext + SortableContext wraps each section's resource list with
  PointerSensor (4px activation) and KeyboardSensor; closestCenter +
  verticalListSortingStrategy.
- ResourceRow becomes useSortable; the GripVertical icon is the drag
  handle (grab cursor, focus ring for keyboard) with an aria-label.
- onDragEnd computes arrayMove and calls useReorderResources with the
  new UID order. Errors surface via toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dnd-kit's drop animation interpolates from the dragged element's release
position back to its underlying DOM position. We were only updating the
list after the server roundtrip, so the dragged row's DOM slot was still
the *old* index when the drop animation ran — the row briefly snapped
back to the start position before re-rendering at the new one.

useReorderResources now applies an optimistic update via onMutate: it
patches the cached useStatusPage payload immediately, swapping the
section's resources into the new order. By the time dnd-kit draws the
drop animation, the underlying DOM is already at the destination slot,
so the animation is a no-op visually.

onError restores the snapshot; onSettled invalidates to reconcile with
the server (no behavioral change, just moved from onSuccess so we always
re-sync even on failure).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reuse the existing shared/check-picker component (server-side search via
useChecks?q=, debounced, keyboard-navigable, exclude-uids support) so
users can type to find a check in the Add-Check-to-Section dialog
instead of scrolling the full list.

- AddResourceDialog drops its `checks` prop (CheckPicker queries by
  itself) and tracks both the selected uid and label so the trigger
  button shows the chosen check's name without re-fetching.
- SectionCard no longer needs `checks`, and the page no longer calls
  useChecks at all — the picker handles its own data.
- Closing the dialog clears the selection so reopening starts fresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…search

Distilled from the May-2026 Hyperping + BetterStack research pass
(docs/competitors/{hyperping,betterstack}/, docs/research/alerting-patterns.md)
into specs/todos/. Each spec is independently shippable; spec 04 is the
largest and explicitly multi-phase.

- 2026-05-08-01-validating-check-state.md: a transient `validating`
  CheckStatus surfaces "we noticed a failure but the confirmation
  hasn't fired yet" instead of leaving the dashboard green up to the
  alert moment. Borrows BetterStack's status enum.
- 2026-05-08-02-time-based-confirmation-and-recovery-periods.md:
  replace count-based incidentThreshold + recoveryThreshold with
  wall-clock confirmationPeriod + recoveryPeriod (seconds, flap-reset
  on recovery). One-shot migration; no second knob, no opt-in. Per
  user direction: drop the count logic outright.
- 2026-05-08-03-severity-primitive-for-escalation-steps.md: a
  per-org Severity resource bundles channel-types (email/sms/voice/
  push/critical_push/slack/...) so an escalation step can "page
  critical" instead of duplicating a step per channel. Default seed of
  Low / Default / Critical per org.
- 2026-05-08-04-outage-vs-incident-split.md: split the conflated
  incidents table into Outage (operational alert, raw, ack/snooze/
  resolve) and Incident (customer-facing, editable timeline updates,
  status-page-attached). Four phases: rename → introduce new type →
  API cutover → auto-publish.
- 2026-05-08-05-on-call-multi-rotation-and-override-as-event.md:
  one schedule contains N rotations (enabling follow-the-sun
  natively), and overrides become events with isOverride=true
  instead of a parallel resource class. Sequenced after the user's
  in-flight spec-04 oncall edit page lands.
- Extract slugify() from status-page-form.tsx into web/dash0/src/lib/utils.ts
  so the new escalation-policies create form can reuse it.
- escalation-policies.new.tsx: swap field order (Name first, Slug second),
  auto-derive the slug from the name until the user types into the slug
  field — slugManuallyEdited freezes the auto-fill so manual overrides
  aren't clobbered on subsequent name edits.
- design-reference.tsx: document the pattern with a live NameSlugExample
  + the import snippet so future contributors copy the exact shape.
@fclairamb fclairamb changed the title docs: competitor research deep-dive + statuspage UX polish feat(statuspages): drag-and-drop resource reordering + dash conventions May 8, 2026
fclairamb and others added 7 commits May 8, 2026 13:08
…rch-and-size-rule

# Conflicts:
#	server/go.mod
#	server/go.sum
golangci-lint err113 rejects ad-hoc fmt.Errorf as dynamic errors.
Promote the "resource not in section" message to a package-level
sentinel (errResourceNotInSection) and wrap it with %w in both the
postgres and sqlite ReorderStatusPageResources implementations.

Behavior is unchanged — handler still surfaces a 422 on mismatched
UIDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes 8 Playwright failures that came from local-main features whose
tests were never run against origin's CI before this merge.

Backend
- escalationpolicies/handler.go was treating the URL `:org` slug as
  the organization UID, which made every list query return [] and every
  create POST 500 with an FK violation against organizations(uid).
  Add Service.ResolveOrgUID and call it at the top of every handler
  (List/Create/Get/Update/Delete). New ErrOrgNotFound surfaces as 404
  ORGANIZATION_NOT_FOUND.

Dashboard
- routes/orgs/$org/organization.tsx: render null while AuthContext is
  loading instead of redirecting `!user?.isAdmin` immediately. The
  redirect was firing before the user payload landed, bouncing every
  admin off the Members/Settings/Requests sub-pages.
- components/ui/card.tsx: CardTitle now renders as <h3>. Tests use
  getByRole("heading", ...) and the previous <div> wasn't matched.
- routes/orgs/$org/checks.new.tsx: forward `data.labels` to the
  CreateCheck request the same way the edit page does, so a label
  added in the create form actually persists through the round-trip.
- components/shared/check-form.tsx: relax the slug validator to
  3-50 chars (was 3-20). Backend already allows up to 50 — auto-
  generated heartbeat slugs like "heartbeat-heartbeat-2" (21 chars)
  were rejected on the edit page, blocking submission.

E2E
- e2e/membership-requests.spec.ts: change `page.goto("/no-org")` to
  `page.goto("no-org")`. The leading slash was resolving against the
  host, bypassing the `/dash0/` baseURL prefix.
- e2e/channels.spec.ts: webhook button locator was `/^webhook$/i` but
  the button's accessible name is "Webhook Generic JSON webhook to any
  HTTP endpoint" — switch to `/^webhook\b/i`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without the explicit association the Radix Checkbox (rendered as a
<button>) didn't toggle on label-text clicks: the label-wrapping
implicit association only reliably fires for native <input>.

The channels e2e test toggles a channel by clicking its name (the
realistic user gesture), so this dropped the click on the floor —
connectionsDirty stayed false, the edit submit skipped the
PUT /checks/$uid/connections call, and the binding never persisted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 001 declared the column with SQL DEFAULT 1 (sqlite) /
DEFAULT true (postgres). Combined with Bun's INSERT semantics — bool
fields with their zero value get omitted, leaving the SQL DEFAULT to
fill in — every newly-created channel ended up is_default=true even
when the request body explicitly sent {"isDefault": false}.

The downstream effect is real: the check-create handler auto-attaches
all default connections to every new check, so every channel ever
created via the dashboard was getting silently bound to every new
check. Confirmed by curl: POST /channels with isDefault:false comes
back with isDefault:true.

Aligning the SQL DEFAULT with the model's NewIntegrationConnection
default (false). Test mode is fresh-DB on every run, so this flips
behaviour for the e2e suite immediately. Existing channels in
deployed DBs already have whatever value was written; no data
migration needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---

fix(check-form): always send connectionUids in edit submit

Drop the connectionsDirty optimization. The dirty flag was hiding
this very class of bug — a click that didn't actually toggle the
underlying state would leave dirty=false, the edit submit would
skip the PUT /connections call, and the binding state would silently
desync from what the user clicked.

Also wires the Notify-via Checkbox to its label via id+htmlFor so
clicking the channel name (a realistic user gesture) triggers the
toggle reliably — the implicit label/descendant association is
flaky for Radix's button-based Checkbox.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Followup to dropping the dirty optimization on connection toggling —
the state setter was kept by mistake, breaking dash0's strict tsc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous pattern /\/channels\/[^/]+$/ matched /channels/new too, so
when the create-channel mutation lagged, waitForURL satisfied
immediately on the still-on-form URL. detailUrl.split('/').pop()
then returned 'new', and the later
`bindings.data.some(b => b.uid === 'new')` always failed.

Tightening to a UUID match forces a real wait for the post-create
navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fclairamb
fclairamb merged commit cf910d4 into main May 8, 2026
10 checks passed
@fclairamb
fclairamb deleted the docs/competitor-research-and-size-rule branch May 8, 2026 12:44
@fclairamb fclairamb mentioned this pull request May 7, 2026
fclairamb added a commit that referenced this pull request Jun 23, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.0](v0.0.0...v0.1.0)
(2026-06-22)


### Features

* add check type registry, sample configs, notification senders, and
observability integrations
([eea049e](eea049e))
* batch — Warning/Degraded statuses, SSL/Docker check upgrades, MCP
OAuth 2.1, config-as-code
([#85](#85))
([936d830](936d830))
* batch spec implementations (dashboard UI, jobs, notifications, SaaS,
perf) ([#78](#78))
([fe003e4](fe003e4))
* bulk feature integration (discovery, web push, badges, webhooks, and
more) ([#73](#73))
([a976856](a976856))
* custom user-agent, refresh checks, vite plugin switch, CI updates
([#1](#1))
([cc00858](cc00858))
* documentation site served at /docs (co-located in web/docs)
([#88](#88))
([27d9ccd](27d9ccd))
* email check frontend (spec 03)
([#30](#30))
([aebd106](aebd106))
* email inbox foundation via JMAP (spec 01)
([#24](#24))
([0f3b0c1](0f3b0c1))
* email passive checks (spec 02)
([#27](#27))
([ba5536f](ba5536f))
* **entitlements:** trim to MaxSSOUsers + MaxChecksPerMinute + status
page history fixes
([#50](#50))
([a007de0](a007de0))
* **entitlements:** trim to MaxSSOUsers + MaxChecksPerMinute, enforce
both ([#49](#49))
([d25cc5f](d25cc5f))
* group incident correlation (spec 04 backend v1)
([#31](#31))
([1883986](1883986))
* **observability:** instrument hot path + add bench harness
([#60](#60))
([c6604cc](c6604cc))
* **oncall:** searchable timezone dropdown + sidebar logo polish
([#46](#46))
([4396898](4396898))
* per-IP HTTP rate limiting and concurrency limiting
([#59](#59))
([f9cc273](f9cc273))
* soften HTTP rate limits with bounded queues + request timeout
([#63](#63))
([025501b](025501b))
* SolidPing — distributed uptime monitoring platform
([eef4383](eef4383))
* **statuspages:** drag-and-drop resource reordering + dash conventions
([#41](#41))
([cf910d4](cf910d4))


### Bug Fixes

* **deps:** update dependency i18next to v26
([#26](#26))
([d5d1b7e](d5d1b7e))
* **deps:** update dependency lucide-react to v1
([#28](#28))
([cdc8e8c](cdc8e8c))
* **deps:** update dependency react-i18next to v17
([#29](#29))
([13135d8](13135d8))
* **deps:** update dependency recharts to v3.8.1
([#14](#14))
([5d95a3b](5d95a3b))
* **deps:** update go dependencies (non-major)
([#16](#16))
([2450f13](2450f13))
* **deps:** update go dependencies (non-major)
([#19](#19))
([f842310](f842310))
* **deps:** update go dependencies (non-major)
([#37](#37))
([c020eca](c020eca))
* **deps:** update go dependencies (non-major)
([#44](#44))
([ff0bcd0](ff0bcd0))
* **deps:** update go dependencies (non-major)
([#52](#52))
([2e3bc28](2e3bc28))
* **deps:** update go dependencies (non-major)
([#69](#69))
([a65ed4c](a65ed4c))
* **deps:** update go dependencies (non-major)
([#77](#77))
([a0920ff](a0920ff))
* **deps:** update go dependencies (non-major) to v1.4.2
([#86](#86))
([1eb7504](1eb7504))
* **deps:** update module github.com/aws/aws-sdk-go-v2/config to
v1.32.18 ([#72](#72))
([6406537](6406537))
* **deps:** update module github.com/aws/aws-sdk-go-v2/service/s3 to
v1.101.0 ([#39](#39))
([f9c9563](f9c9563))
* **deps:** update module github.com/go-webauthn/webauthn to v0.17.3
([#45](#45))
([593931a](593931a))
* **deps:** update module github.com/go-webauthn/webauthn to v0.17.4
([#70](#70))
([fca1c97](fca1c97))
* **deps:** update module github.com/ibm/sarama to v1.48.1
([#47](#47))
([93ccf3a](93ccf3a))
* **deps:** update module github.com/ibm/sarama to v1.48.2
([#54](#54))
([e84d868](e84d868))
* **deps:** update module github.com/ibm/sarama to v1.49.0
([#64](#64))
([fd61bd7](fd61bd7))
* **deps:** update module github.com/oapi-codegen/runtime to v1.4.1
([#65](#65))
([a3a3e9f](a3a3e9f))
* **deps:** update module github.com/prometheus/common to v0.67.5
([#61](#61))
([095dd6f](095dd6f))
* **deps:** update module github.com/slack-go/slack to v0.24.0
([#75](#75))
([6e86d5b](6e86d5b))
* **deps:** update module golang.org/x/sys to v0.44.0
([#42](#42))
([a245730](a245730))
* **deps:** update module golang.org/x/sys to v0.45.0
([#67](#67))
([49c3d90](49c3d90))
* **deps:** update module golang.org/x/term to v0.43.0
([#43](#43))
([4a934f1](4a934f1))
* **deps:** update module google.golang.org/grpc to v1.81.0
([#36](#36))
([1be183a](1be183a))
* **deps:** update module google.golang.org/grpc to v1.81.1
([#55](#55))
([6bfac2d](6bfac2d))
* status lifecycle improvements and created/running result handling
([#5](#5))
([fc64c7d](fc64c7d))
* **status0:** white page header and de-duplicated footer
([#48](#48))
([77506a7](77506a7))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant