Skip to content

feat: Slack on-call mentions, paging coverage, worker resilience, PROXY protocol and org-relative private regions - #215

Merged
fclairamb merged 106 commits into
mainfrom
batch/2026-08-12
Aug 13, 2026
Merged

fclairamb merged 106 commits into
mainfrom
batch/2026-08-12

Conversation

@fclairamb

@fclairamb fclairamb commented Aug 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Ten specs, each implemented in isolation and gated behind an independent completeness audit before being archived. Four needed a fix round; nothing was archived on a passing build alone.

Notifications

  • Slack channel alerts can mention the on-call person. New user_integration_identities table maps org members to Slack users per workspace, auto-matched by email after OAuth with a manual override. Mentions cover every human target on escalation step 1, deduplicated; resolved/reopened stay mention-free; a missing identity degrades to plain text and never blocks the send. Defaults on for newly created Slack integrations, off for existing ones.
  • Admins can see paging coverage per member (channel types + verified flags only — never values), with warning badges next to anyone on-call reachable by email alone, and can pre-provision phone/WhatsApp contacts in unverified state. An admin can never create or flip a contact to verified.

Reliability

  • The job runner backs off exponentially (100 ms → 30 s, jittered) and collapses repeated identical errors instead of spinning at CPU speed — the incident behind this filled a 460 GB disk at ~30 GB/h.
  • The server fails fast on structural database faults (missing table, corrupt file), classified once in db/dbfault, surfaced as a 503 and then a clean shutdown. Transient faults are unchanged and still retry.

Agents / networking

  • Private regions are stored org-relatively, so renaming an org no longer strands agents. Includes a migration that repairs installs already broken by a past rename.
  • tlsedge speaks PROXY protocol, so custom domains behind a TLS passthrough keep the real client IP for rate limiting. Fail-closed: untrusted headers are ignored, and an empty trusted-CIDR list fails startup.

Dashboard

  • Contextual docs links across the app, command-palette entries for the Organization and Account sections, a single unified region param on check detail (no reload on change), live relative "Last seen" for private-location agents, and an empty-state fix for check groups with zero checks.

Docs

  • Agent identity docs rewritten: the shipped image is distroless, so the previously documented base64-inside-the-container commands cannot work. Now documents the two supported shapes (SP_AGENT_KEYS via env var or Secret) and a working sidecar extraction route, instead of leaving SP_AGENT_PRINT_KEYS as the de-facto path.

Notable for review

  • Migration 011_v0_14_0 rewrites existing rows across agents, checks, jobs, org defaults, enrollment tokens and results. Idempotent, dedupes before rewriting to respect two unique indexes, verified against real Postgres. The release cycle's scratch migrations were consolidated into this single file per wiki/conventions/database.md, with a golden-schema diff confirming the result is byte-identical on both engines.
  • Behaviour change: the server now exits on a structural DB fault rather than retrying forever.

No breaking API changes.

Test plan

  • Backend: make build-backend lint-back test green
  • dash0: make build-dash0 + eslint, no new errors over the known baseline
  • Docs: make build-docs green, no broken links
  • E2E: Playwright suites pass, including new coverage for Slack member mapping, paging coverage, docs links, command palette, region unification and relative timestamps
  • Migration verified on real Postgres and SQLite, plus a golden-schema diff after consolidation

fclairamb and others added 30 commits August 12, 2026 19:47
The guard at live-updates.spec.ts counted 3 list fetches where 1 was
allowed. Instrumenting the counted requests with their query string and
their offset from the moment the counter is armed shows it is the
MEASUREMENT WINDOW, not a refetch storm:

  [diag] listFetches=1
    +302ms ?with=last_result&q=E2E+Steady+State+…&limit=100&sort=group
  [diag-after-transition] listFetches=3
    +302ms ?with=last_result&q=E2E+Steady+State+…&limit=100&sort=group
    +8970ms ?limit=10
    +8970ms ?with=last_result&q=E2E+Steady+State+…&limit=100&sort=group

Three defects add up to exactly the CI number (1 + 2 = 3):

1. The counter was armed before the search debounce fired. `fill()` only
   sets the input; 300 ms later the `q=` query key changes and fetches.
   The row was already visible from the PRE-FILTER list, so the
   visibility assertions settled sooner than that fetch — which then
   landed inside the window and ate the entire budget of one.

2. The counter matched by URL pathname, so it also counted the org
   layout's CommandMenu query (`useChecks(org, {limit: 10})` — the
   `?limit=10` line). Both it and the page's infinite list match the
   checks/checks live roots, so ONE invalidation cost TWO.

3. The window was not immune to a genuine `checks` transition, and the
   test manufactured one itself: a heartbeat check is scheduled like any
   other (default period 60 s) and its passive job writes a
   `No heartbeat received` → down result. Its first run fires within ~2 s
   of creation and races the setup heartbeat — locally 1 run in 8 was
   already `down` before the window opened (failing on the Up badge,
   screenshot: `heartbeat-heartbeat Down 0ms`). Under CI load that run is
   delayed into the window, flips up → down, and publishStatusHint →
   PublishImmediate(KindChecks) invalidates both list roots: the +2.

The polling boundary is ruled out: CHECKS_LIST_POLL_MS is 10 s and every
counted request is timestamped; across 8 local runs no poll tick ever
landed in the ~6 s window.

Fixes, all in the test:
- create the check with a 1 h period so its passive job cannot re-fire
  inside the window, and settle it at `up` through a new
  waitForCheckStatus() barrier that re-sends the heartbeat until the
  server agrees (also fixes the second, independent Up-badge flake; used
  by the sibling checks-list test too);
- arm the counter only after the filtered list response has landed;
- count only this page's list query (`q=<check name>`), never the
  CommandMenu's;
- tally `checks`-kind hints on the live socket and allow one refetch per
  damper interval that could have carried one, so a foreign transition in
  the shared test org is accounted for rather than silently fatal;
- assert the positive control explicitly: the final genuine transition
  must still produce at least one list fetch, so the budget check can
  never pass by counting nothing.

10/10 green with --repeat-each=10 --workers=1 against a CI-equivalent
side-car test server.
waitForCheckStatus() converged on `up` but did not prove it would stay
there: the check's first passive job runs within ~1 s of creation and its
read of the last result can predate the setup heartbeat, so it writes a
`No heartbeat received` → down result *after* the barrier already saw
`up`. That reproduced as 1 failure in 20 (`… heartbeat-heartbeat Down 0ms`
on the Up-badge assertion).

The barrier now takes a stability window and only returns once the status
has held for it, re-sending the heartbeat whenever it has not. Combined
with the steady-state check's 1 h period, a 3 s hold outlasts the single
passive run and the next one is an hour out — the status is settled for
the rest of the test, so no transition (and no legitimate `checks` hint)
can still be in flight when the counter is armed.

20/20 green with --repeat-each=10 --workers=1 across both checks-list
live tests.
CheckGroupSection folded page-level hasNextPage/isFetchingNextPage into
its per-group isLoading, so an empty group (checkCount=0) read as
"still loading" for as long as the infinite-scroll stream had more
pages to fetch -- effectively forever on orgs with many checks. Use
the authoritative group.checkCount to short-circuit straight to the
empty state when not filtering; keep deferring to the stream while
filtering, since checkCount is the unfiltered total. Also adds a
"Add a check to this group" link (pre-filled group) to the empty
state for the unfiltered case.
Playwright coverage for spec 2026-08-12-02: mocks /check-groups and the
paginated /checks endpoint so a group with checkCount 0 and a group
that's fully loaded on page 1 coexist with hasNextPage staying true
(simulating an org with more checks than one page). Asserts the empty
group shows "No checks in this group" plus the new "Add a check to
this group" link immediately, never skeletons, and that the non-empty
group's rows still render fine (regression guard for the existing
"rows/skeletons while streaming" behavior).
…an endless skeleton instead of an empty state
…n endless skeleton instead of an empty state
…all person, and members have no integration identity mapping
…the on-call person, and members have no integration identity mapping
Context-first test helpers, range-index loops over copied structs, strings.Builder
accumulation, shadowed-err renames and a split of the members error mapper and the
Slack escalation builder into per-family helpers. No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on-call person, and members have no integration identity mapping

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… number

The duplicate guard in AddMemberContact read the ROUTE-JOINED contact list, so a
live contact with no route was invisible to it. The add then fell through to the
upsert, which matched the (user, org, type, value) unique key and landed on that
very row, and the unconditional verified-stamp clear wiped a stamp the member had
earned — quietly making them unpageable on a number that worked. A contact goes
routeless whenever the member drops the route but keeps the number, so this is an
ordinary state, not a corrupt one.

The guard is now a type/value lookup (findLiveContact) that sees every live row
regardless of routing, and refuses it like any other pre-existing contact. The
unconditional clear stays: it still covers the soft-delete resurrection path,
where the upsert un-deletes a row that may carry an old verified stamp.

Also surfaces the email-only warning badge on the on-call schedule DETAIL page
roster, which rendered plain names with no coverage signal — reusing the existing
EmailOnlyBadge and coverage hook, no new component and no new i18n keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on-call person, and members have no integration identity mapping

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n-call person, and members have no integration identity mapping
…ng search

Verified at 375px width: the search+refresh+new-token+docs-link row
had no flex-wrap, so the new DocsLink icon squeezed the search input
down to a few visible characters. Wrapping the row (and giving the
search box a min-width) fixes this — see screenshot verification in
the implementation notes.
The private-locations doc told operators to run `base64`/`kubectl cp`
inside the agent container to recover SP_AGENT_KEYS, but the shipped
runtime is gcr.io/distroless/base-debian13:nonroot — no shell, no
base64, no tar — so every kubectl path silently failed and pushed
operators toward SP_AGENT_PRINT_KEYS, which leaks private key material
into the log pipeline.

Verified against the real ghcr.io/fclairamb/solidping:latest image:
`docker exec ... base64` and `/bin/sh` both fail with "executable file
not found"; a filesystem export confirms no base64/tar/sh/busybox
anywhere in the image; `docker cp <container>:path -` still works
(daemon-side, no exec) and the alpine-helper-container base64 command
works against the named volume.

Rewrites the extraction section to lead with "keep it in the pod" and
reworks the Kubernetes section into two options, PVC-backed identity
first (with the fsGroup:65532 / Recreate / optional enrollment-Secret
details a real production deployment needed) and the SP_AGENT_KEYS +
Secret alternative kept honest about how the value is sourced.
… wiki

Same bug as private-locations.md: the internal "Bootstrapping
SP_AGENT_KEYS" note told engineers to exec base64/kubectl cp inside a
distroless container. Adds the internal why (distroless is a
deliberate security choice; SP_AGENT_PRINT_KEYS must not become the
standard k8s bootstrap) and points at the PVC pattern our own
production deployment uses, cross-linking the public doc's manifest
instead of duplicating it.
deploy/fly/README.md already correctly documented the image as
distroless with no /bin/sh (for the wrapper-entrypoint note) but still
told operators to run `fly ssh console -C "base64 -w0 ..."` a few
lines above it — self-contradictory, and doubly broken since fly ssh
console also needs hallpass, which this image doesn't carry. Replaces
it with the same last-resort SP_AGENT_PRINT_KEYS guidance used in the
other two docs.
…un base64 inside a distroless image, which cannot work
…n base64 inside a distroless image, which cannot work
@fclairamb fclairamb changed the title Batch 2026-08-12: Slack on-call mentions, worker resilience, PROXY protocol, org-relative private regions feat: Slack on-call mentions, paging coverage, worker resilience, PROXY protocol and org-relative private regions Aug 13, 2026
@fclairamb
fclairamb enabled auto-merge (squash) August 13, 2026 18:38
Link to the parent /orgs/$org/organization and /orgs/$org/account
routes (not their current redirect targets) so the palette keeps
working if the default child route changes. New nav.organization and
nav.account i18n keys across en/fr/de/es, distinct from the existing
group-heading keys.
…directs

Search hits by "organization"/"account", plus assert the resulting
navigation lands on the current redirect target (invitations/profile)
so the parent-path-plus-redirect contract stays proven.
…enu pages test

The new Organization/Account entries share their title text with the
group heading rendered above them ("Organization" heading, "Organization"
entry), which broke the existing exact-text assertion under strict mode.
Scope those assertions to [cmdk-group-heading] and assert the new
entries by testId alongside the pre-existing ones.
@fclairamb
fclairamb merged commit eaa4236 into main Aug 13, 2026
11 checks passed
@fclairamb
fclairamb deleted the batch/2026-08-12 branch August 13, 2026 19:01
…rnetes

The previous revision fixed the broken `base64` extraction commands but
concluded that "Kubernetes has no in-cluster extraction path, full stop",
and recommended a PVC-backed identity on that basis. The premise is wrong:
enrolling in a throwaway Pod where an alpine sidecar shares an emptyDir with
the agent makes the keys file readable without executing anything in the
distroless container. Verified in production while standing up the
@stonal/s3ns-paris agent on GKE Autopilot.

So the Kubernetes section now leads with the stateless pattern — identity in
a Secret, injected as SP_AGENT_KEYS, no volume — structured as enroll →
capture → run, and pipes the value straight from the sidecar into the Secret
so the key material never reaches a terminal, a shell history or a log. The
PVC keeps its place as the alternative for people who would rather the
identity never leave the cluster, with its trade-offs stated: node-pinned,
ReadWriteOnce, unrecoverable if lost.

Also documents SP_NODE_NAME, without which the worker slug is derived from
the truncated pod hostname and every restart lands on a new workers row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fclairamb added a commit that referenced this pull request Aug 13, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.14.0](v0.13.0...v0.14.0)
(2026-08-13)


### Features

* Slack on-call mentions, paging coverage, worker resilience, PROXY
protocol and org-relative private regions
([#215](#215))
([eaa4236](eaa4236))


### Bug Fixes

* **deps:** update module golang.org/x/net to v0.58.0
([#213](#213))
([0b2a7c9](0b2a7c9))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
fclairamb and others added 7 commits August 13, 2026 21:43
…PVC option

Supersedes the PVC-first guidance introduced by spec 2026-08-13-05. That spec
claimed Kubernetes had no in-cluster extraction path and recommended a PVC on
that basis; the claim was wrong. Extraction works fine through a throwaway
enrollment Pod that shares an emptyDir with an ordinary alpine sidecar — the
distroless constraint never applies to the sidecar, which has a shell and
base64 and reads the file through the shared volume.

With extraction available, a PVC buys nothing: it pins the pod to a
ReadWriteOnce volume (forcing strategy: Recreate), makes an otherwise stateless
agent stateful, and turns ordinary volume loss into unrecoverable identity
loss. The supported shapes are now exactly two — SP_AGENT_KEYS from an env var,
or from a Secret — and the steady-state Deployment needs no volume, no fsGroup
and no Recreate strategy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-time module

check-summary-cards.tsx consumes the shared module instead of defining its
own copy, so there is a single implementation of the live-ticking duration
logic ahead of reusing it on the private-locations agents table.
Replace the raw toLocaleString() cell in the private-locations agents table
with LiveDurationAgo (shared with check-summary-cards), so it reads as
'5m ago' and ticks live instead of forcing a mental timestamp diff. The
exact local timestamp stays reachable via a title tooltip on hover, and the
existing 'never' fallback for agents with no lastSeenAt is preserved.

Also registers the shared LiveDurationAgo pattern in the design reference so
it stays the canonical example for future live-relative-time UI.
Stub the agents list (a real agent connection isn't practical from
Playwright, same rationale as deported-agent-wizard.spec.ts) with one agent
that has lastSeenAt and one without. Assert the cell matches /ago/ and
carries a non-empty title tooltip, and that the no-lastSeenAt row still
reads "never" — never assert an exact relative value since it ticks every
second.
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