Repository navigation
feat: Discord bot, audit log, SLO burn-rate alerts, path tracing, support inbox, status-page branding & private pages - #246
Merged
Merged
Conversation
…ith the Go plan tests
…so no index can serve them
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hole results table
…ns the whole results table
… for a mixed tier
… of scanning results
…th tier-less controls
…, with tier-less controls
…r the status-page fetch
…he whole results table
…he whole results table
…he whole results table
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The public page view sent no Cache-Control at all while the summary and badge sent an unconditional public, max-age=60 — so the expensive endpoint was uncacheable and the cheap ones authorized shared caches to retain a password-protected page's name, counts and rollup status. A single helper (internal/statuspagecache) now answers who may cache a status-page response: public pages keep public, max-age=60; password and private pages get private, no-store, unlocked or not. The page, summary, badge, Atom feed and the custom-domain SPA shell all route through it, and the public 404/401 answers are gated too so they cannot be cached into an existence oracle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walks the page, summary and badge for all three visibilities and asserts the exact directive, with the public page as the positive control. The assertion most likely to be missed gets its own test: a request carrying a valid unlock cookie gets a real 200 and still private, no-store. Vary is pinned exactly so a new request-derived field cannot ship without extending it, and the private page's 404 is compared byte-for-byte with a missing page's. Also covers the feed and the custom-domain SPA shell, including a DB-backed test that the resolver actually carries the page's visibility — the cache fixtures alone could not have caught a resolver that dropped it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Control Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g public pages on Cookie Three audit findings. The public incident history was the sixth surface and the one that still sent no Cache-Control at all — a header-less 200 is heuristically storable, and the route is on the custom-host allowlist, so the cache in question is the CDN in front of a customer's status domain. The body quotes incident titles and update bodies verbatim: exactly what made the Atom feed a leak. The service now surfaces the page's visibility the way the summary does, and the handler applies the shared helper, gating its 401/404 too. Vary is now split. Public responses carry only X-Forwarded-Proto, which genuinely changes the body; Cookie moves to the gated branch. The old everywhere-Cookie rationale was wrong — Vary does not invalidate anything when a page's visibility flips, so it bought nothing, while Cloudflare, Fastly and Varnish all decline to cache a response carrying it, which would have left this spec's whole point unbuilt. The comment now argues something true. The path-based status0 shell now varies on X-Forwarded-Proto like its custom-domain twin, and carries a comment explaining why it is safe to serve unconditionally cacheable: it resolves page metadata without the unlock grant, so a gated page's name never reaches it. That claim is now pinned by a password-page case in the no-existence-leak test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Control Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dow the API bounds
…er a window the API bounds
…the effective window
The consolidated "RabbitMQ — 2/6 down" view that group incidents used to bake into the row is rebuilt where it belongs — at display time (spec 2026-08-24-14). New `lib/incident-grouping.ts`: a pure `groupIncidentsByCheckGroup(incidents, checks, groups)` returning ordered rows. Ungrouped incidents keep their original position as bare rows; a group takes the position of its first member. Nothing is re-sorted — the server chose the order. Rendered as a PLAIN, non-interactive header row in the incidents list and in the dashboard's Active-incidents card, per the spec's resolved question: no expand/collapse, because there is no per-group open/closed state worth persisting across filters, sorts and pagination. Catalogued in the design reference alongside the collapsible checks-index header it deliberately differs from. Grouped client-side, so `GET /incidents` stays a flat list with no groupBy verb, no second response shape and no group-count field. Two consequences are handled rather than papered over: the count reflects the incidents LOADED, so `groupHeaderCounts` drops the denominator instead of asserting a fleet-wide total it cannot see; and past the checks endpoint's 100-row clamp an incident whose check did not load renders bare rather than under a header with a wrong count. i18n keys in all four locales. Unit tests cover bucketing, position preservation, active-only counting, both unknown-mapping fallbacks, and the denominator-dropping rule. Docs: `wiki/database-model/results-incidents.md` and `wiki/features/notifications-and-escalation.md` now describe group incidents as historical, with the three defects that retired them. The published docs (`features/incidents.md`, `features/check-groups.md`, `intro.md`) described a live correlation mechanism to users — rewritten to what actually happens now, with a v0.18.0 changed-behavior note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fully route-mocked, so it asserts the rendering rule rather than whatever the seeded fixture happens to hold: one plain "RabbitMQ — 2/6 down" header with no expand/collapse affordance, both members rendering as their OWN incident rows beneath it, and the ungrouped incident staying bare. The fixture interleaves the ungrouped incident BETWEEN the two group members, so a grouping pass that reordered the list instead of pulling the second member up to its group's position would fail the row-order assertion. Ran green against a side-car test server, alongside the existing incidents, incident-timestamps, dashboard and check-incidents-slug suites (34 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FindActiveIncidentByGroupUID and FindRecentlyResolvedIncidentByGroupUID existed for the group create/reopen path, which is gone (spec 2026-08-24-14). Nothing reads them any more, and leaving them is not neutral: they are precisely the lookups a future caller would reach for to "reuse the group incident", reintroducing the binding of one check's failure to another check's incident that this spec removed. The member-check operations stay — the readers serve historical rows and the migration test writes them — but the interface now says out loud that they are historical and gain no new production callers. Also retires two comments naming queueGroupNotifications, a function that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d defeat dependency rollup — replace them with per-check incidents aggregated at read time Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… defeat dependency rollup — replace them with per-check incidents aggregated at read time
…ere the parent confirms after its dependents — re-evaluate children when a parent incident opens
Cascade rollup only ever looked backward, at child-incident open, so the ordering that actually dominates in production — a core service's own probe confirming AFTER the consumers that depend on it — left every dependent paging for its whole lifetime. An incident open or reopen now also walks hard descendants downward (same BFS shape, same depth cap, same per-child correlation window, same query as the backward path) and attaches every active, non-suppressed child incident whose window still contains the parent's onset. The attachment is a compare-and-set guarded on paging_suppressed = FALSE, so the forward and backward evaluations converge on one attachment and one event even when they race from two workers. Escalation steps already queued for a retroactively suppressed child now stop firing: incidentNeedsPaging re-checks paging_suppressed at fire time, closing a gap the original dependencies spec had already called for. Rollup success is logged at INFO instead of DEBUG, and a retroactive attachment emits incident.rolled_up on the child's timeline.
Eight cases driven through the real state machine on in-memory SQLite with an injected clock, so every correlation-window assertion is deterministic: child-before-parent (the headline — it fails against the backward-only implementation), parent-before-child, same-instant race, a depth-2 chain whose intermediate node never fails, soft edges ignored, a child outside its own window left alone, and both branches of the parent-resolve re-evaluation. Plus the escalation fire-time gate, with a positive control that an ordinary open incident still pages.
…e where the parent confirms after its dependents — re-evaluate children when a parent incident opens
… where the parent confirms after its dependents — re-evaluate children when a parent incident opens
…no rows in result set" when the guild's linked organization no longer resolves
…ogin A provider link outliving the soft-deleted org or user it points at used to brick every subsequent SSO login for that workspace/guild permanently. The shared resolveLinkedOrganization/resolveLinkedUser helpers now clear the stale link (WARN-logged) and let the caller fall through to its normal create path. Wired into discord, slack, github, gitlab, google, microsoft, oidc, saml and ldap, all of which also gained the missing non-ErrNoRows guard on the provider lookup. DeleteOrg releases the org's provider links so the class cannot recur, and unexpected callback failures now redirect with a generic description while the real error goes to the server log.
…ct_uri A failed provider callback lands on ?error=…&error_description=…; the 401 bounce captures that URL as returnTo and it was handed straight back as redirect_uri, so every retry nested the previous failure one URL-encoding deeper.
Reproduces the production Discord failure end-to-end through the real HandleCallback (guild path, personal-org discord-user-<id> path, user link), the Slack twin, a live-link positive control, the per-connector sweep, the generic error_description, and DeleteOrg releasing its provider links.
… guild's linked organization no longer resolves Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The audit page kept its pagination cursor in component state, so a shared link reproduced the filters but always landed back on page one. Move the cursor into the URL search params (reset on any filter change, so a stale cursor can't page into a result set that no longer exists) and drop the now-redundant refresh button in favour of a DocsLink. Also add DocsLink to the check detail and report-schedules pages, and move the send-test-digest action out of the report-schedules list row into the edit form, where it can act on the saved schedule. E2E coverage extended for all three surfaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g, SLO burn alerts, traceroute diagnostics and support inbox Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fclairamb
enabled auto-merge (squash)
August 24, 2026 23:30
Merged
fclairamb
added a commit
that referenced
this pull request
Aug 25, 2026
🤖 I have created a release *beep* *boop* --- ## [0.18.0](v0.17.0...v0.18.0) (2026-08-25) ### Features * Discord bot, audit log, SLO burn-rate alerts, path tracing, support inbox, status-page branding & private pages ([#246](#246)) ([87b33d7](87b33d7)) ### Bug Fixes * **slack:** register the login callback in the prod manifest ([#251](#251)) ([32fae86](32fae86)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
Closed
Closed
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.
Summary
A large batch: 480 commits, 104 features and 56 fixes, no breaking changes.
Discord bot — a real bot rather than a webhook. Incidents arrive as bot-owned threads that edit in place and carry acknowledge/comment buttons; org members map to Discord identities so on-call mentions reach the right person; thread comments are ingested back onto the incident through a Gateway supervisor. Every inbound interaction is Ed25519-verified, rejecting stale and unsigned requests.
Audit log — every session-minting path, membership change, token operation, config apply and service mutation emits a typed event with actor metadata and a normalized source address. Redaction fails closed on anything it doesn't recognize, failed logins are flood-controlled, retention is swept. Ships with an admin-gated audit page and filters (family, type, actor, target, admin-only IP) with working cursors.
SLO burn-rate alerting — alert policies over an SLO, evaluated multi-window. Burn incidents are a distinct incident kind with their own lifecycle, templates and sweep job.
Path tracing — a pure-Go MTR-style prober with a privilege ladder traces the route when a network failure opens an incident, so you see where packets actually stopped. Pinned never-public.
Support inbox — inbound human messages captured from email, WhatsApp, Telegram, SMS, Slack DMs and Discord DMs into threads, with idempotent capture, abuse ceilings and a mailbox mirror. The JMAP consumer is now safe to run more than once (Message-ID dedup, archive-to-claim, Postgres advisory lock).
Status-page branding & private pages — per-page logo/favicon behind a white-label entitlement, password protection via an unlock cookie, webhook and Slack subscriptions.
Incident screenshots — browser checks can capture a screenshot on failure, on a new generic attachment rail with agent upload, orphan reaping, and attachments kept out of public payloads.
Also: incident acknowledgement actor surfaced everywhere, flapping made visible, check groups handled as groups, custom-domain lifecycle with grace/re-promotion and DNS diagnostics, self-monitoring watchdog, region migration API with ghost-region detection, forced password rotation, UptimeRobot importer, self-healing stale SSO provider links, and a broad pass making result/chart queries seek instead of scan.
Full detail in the
## [Unreleased]section ofCHANGELOG.md.Test plan
make build-backend lint-back test— green, 0 lint issuesmake build-dash0— greenbun run test:unit— 837/837bunx tsc --noEmit— cleanKnown pre-existing flake, unrelated to this batch and tracked separately:
web/dash0/src/api/use-chart-window.test.tsxcan time out under full-suite parallel load (passes in isolation; itswaitForovershoots the 1s default).🤖 Generated with Claude Code