Repository navigation
feat: operator notifications, dependency editing on the edit page, and a measured memory baseline - #318
Merged
Conversation
…ated — give it a modern, flashy treatment Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…at and dated — give it a modern, flashy treatment Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-up focal row
The onboarding checklist was five identical flat rectangles on a flat tint,
with progress as a line of text and done rows struck through like deletions.
Rework the presentation only — the container derivation, dismissal writes and
test-alert flow are untouched:
- card chrome: a primary/violet top accent bar, two blurred low-alpha blobs
and shadow-primary on a normal bg-card surface. Every decorative layer is
aria-hidden and pointer-events-none so it cannot intercept a row's
stretched click target. No AuroraPanel (marketing-only, always dark).
- header: a Rocket tile and a real Progress bar. destructiveWhenFull={false},
because the primitive defaults to TRUE and would paint a finished checklist
red — the colour that means "down" everywhere else on this dashboard.
- rows: three tones instead of two. The first still-open step is the focal
"next up" row (primary ring, Next pill, the card's only default CTA),
later pending rows are quiet, done rows recede into an emerald wash with a
filled tick. The strikethrough is gone, and the pending marker is now a
numbered badge that visibly becomes the tick.
- all-set: an emerald→primary strip with a Sparkles glyph, not a grey-green box.
- motion: staggered reveal, tick zoom-in and hover lift, all behind Tailwind's
motion-safe: variant — the first surface in dash0 to set that convention.
Every test id, data-done attribute, the stretched after:absolute link and the
relative z-10 test-alert button survive unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd lack of strikethrough Two new e2e cases against the real dashboard. The first reads the rows' new data-next attribute (carried by rows only, never by the status wells) and asserts the progress bar agrees with them: aria-valuemax = row count, aria-valuenow = done count, exactly one row focal, and that row is the FIRST still-open one. A self-created org opens at 3/5, so the test also bounds the done count strictly between 0 and total — without that, "one focal row" would pass on a degenerate list. The second measures computed text-decoration on a genuinely done row (check is ticked the moment the card appears) with a pending row as the positive control, so the removal of the strikethrough cannot silently regress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd dated — give it a modern, flashy treatment make build-dash0: green. cd web/dash0 && bun run lint: 42 errors / 440 warnings — byte-for-byte the pre-existing base count (the react-hooks debt), zero new findings in any touched file. cd web/dash0 && bun run test:unit: 75 files, 1274 tests green (includes the onboarding locale-parity suite, now covering the new onboarding.nextUp key in de/en/es/fr). Playwright onboarding-checklist.spec.ts + magic-wand.spec.ts + empty-state-onboarding.spec.ts against a SP_RUNMODE=test side-car on :4010: 23 passed, 1 failed. The failure is magic-wand "attaches every check even when the org has more than 100", which 429s bulk-creating 105 checks against the side-car's default rate limit — it passes on the same build with the limits raised, and it never touches the checklist card. Both themes verified by screenshot in the running app: the card at 3/5, the whole dashboard with the card sitting above OverallStatusBanner, and the design-reference section with both fixtures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d dated — give it a modern, flashy treatment Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… and signups Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…messages and signups Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ransport Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…its admin API Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ations Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd the notice job Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ications tab Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rives or a new user signs up Two events that matter most for running an instance used to reach nobody in real time. An inbound support message produced a single email to the instance support mailbox, so an operator who lives in Telegram or Slack only learned about a customer's question by happening to open the unlinked support inbox. A new signup produced nothing at all beyond an optional analytics event, so nobody could welcome a new user or notice one whose signup got stuck with no organization and no checks. A new "Notifications" tab under Server lets a super admin pick which super admins hear about which of those events. Delivery reuses the notification routes each recipient already set up for themselves — email, Telegram, a Slack DM, web push or SMS — so there is no new medium to configure and no webhook to paste. WhatsApp routes are skipped, because Meta will not carry free-form text outside a session, and the skip is logged rather than silent. A recipient who belongs to several organizations with the same address gets one notice, not one per organization. The page is built around the way this feature fails quietly: a recipient with no enabled notification route is flagged in amber next to their name rather than only in the logs, a subscriber who has lost super-admin status keeps a visible row explaining why they are being skipped, and a "Send me a test" button delivers a real notice over the real transport and reports how many routes actually carried it — never a bare success when nothing was delivered. Recipients must be super admins at delivery time, not merely when the configuration was saved, because a notice can quote a customer's support message and support threads are super-admin-only. Bursts are folded the way the support mailbox mirror already folds them: one notice per thread per ten minutes, carrying the count of what it swallowed, under an instance-wide hourly ceiling. Every attempt is counted in solidping_operator_notice_total, so a silent drop stays visible. The email mirror to the support mailbox is unchanged — this is additive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ages and signups Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ding org The Notice -> job payload conversion was an inline field-by-field copy in the dispatcher wiring, and it omitted AboutUserUID. Every queued notice therefore reached the delivering job with an empty subject uid, enrichWithOrganization short-circuited, and no operator was ever told which organization a signup landed in — the one thing the docs page already promises. Nothing caught it because the tests sat on either side of that seam: the job tests hand-built the config, the auth tests asserted at the Notify boundary. The conversion now lives once in jobtypes with a reflection round-trip guard that fails when any Notice field stops travelling, and a new app-level test drives a real password signup through the real dispatcher and asserts both the uid on the queued job and the Org line in the delivered email. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s log prefix Build assigns all five medium closures unconditionally, so opsnotify's nil-closure branch could never fire in a real process: an instance with no Telegram bot token recorded outcome="failed" for every Telegram route, which made "never configured" indistinguishable from "the provider is broken" in solidping_operator_notice_total — the metric added specifically so a silent drop stays visible. Wiring failures now carry an ErrMediumUnavailable sentinel that delivery classifies as skipped, and the new opsnotifywire tests exercise the real Build output rather than a hand-built Deps, with a positive control proving a configured medium still delivers. The delivery log lines also name the notice kind again, so a watchdog digest keeps logging as "Platform watchdog digest ..." instead of disappearing under a generic operator-notice prefix that would break log-based alerting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fold window only collapses a burst inside ONE thread; a hundred different numbers texting once each are a hundred threads, each admitted by its own empty fold state. The hourly ceiling is what stops that becoming a hundred pushes, and nothing exercised it — the existing ceiling test covers the mail mirror's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n settings The amber "no notification routes" warning linked every row to /orgs/$org/account/notifications — the VIEWER'S page. Clicking it on Bob's row took Alice to Alice's settings: it looks actionable, changes nothing about Bob, and is worse than no link at all. Notification routes are per-user and dash0 has no admin route onto somebody else's, so only the viewer's own row links anywhere now; another admin's row states the problem and names who has to fix it. The e2e case asserts the absence of the link, not just the presence of the warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n the spec Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ages and signups Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ges and signups Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…precise, repeatable RSS measurement Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…napshot The RSS a memory question needs is not one number: /proc RSS is anon+file+shmem, kubectl top is the cgroup working set, the Go runtime sees neither the cgo arena nor the binary's rodata, and the OOM killer acts on cgroup anon+kernel. This package reads all four sources and computes the two numbers nobody had: offHeapBytes = RssAnon - (goTotal - heapReleased), and the cgroup unreclaimable total that the bench harness uses as its primary metric. Every reader is total: an absent file (macOS, cgroup v1, no cgroup) yields a zero value with present=false, never an error. Parsers are pure over bytes, so the Linux layouts are fixture-tested from a Mac. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ething /api/mgmt/memory reported one rssBytes and left the reader to guess which RSS it was. It now carries the /proc anon/file/shmem split, smaps_rollup Pss, the container's own cgroup accounting (including anon+kernel, the total an OOM kill is actually decided on), the runtime/metrics memory classes, and the derived off-heap gap the runbook's section 4 asks for but nobody computed. ?gc=1 adds a floor mode (runtime.GC + FreeOSMemory before sampling) so 'live floor' and 'steady state' are two named measurements instead of one ambiguous one; every payload says which mode produced it. Every existing field keeps its name and place. Linux-only sections report present=false off Linux rather than failing the request, and the matching Prometheus gauges emit no series at all where the source is absent - a zero RssAnon would read as 'no anonymous memory', which is worse than silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gement A delta smaller than the baseline's own inter-run spread is not a result - repeating a run moves these numbers by a few percent all on its own. This is the pure core that makes that call: per-run median/p95/max, the spread across K repetitions, and a comparison that flags anything inside the noise floor as not significant and refuses any verdict at all when fewer than two repetitions make the spread unknown. The tests are the negative controls the spec demands: a no-op rerun of the baseline reports nothing significant anywhere, a real reduction is flagged only on the scenario it targets and not on the untouched one, and a delta exactly at the noise floor stays unflagged. Reports carry their own caveats - a --local run prints that it is not authoritative, and comparing a local run against a containerized one is labelled not comparable rather than quietly tabulated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ts itself make bench-memory. Boots a FRESH server per repetition (a reused process carries its page cache, heap and GC phase into the next run - exactly the noise this exists to measure), warms up, samples /api/mgmt/memory on a fixed protocol, and repeats K times so the inter-run spread is a measured number rather than an assumption. Three modes, and every report says which one produced it: docker (the shipped image under --memory/--cpus, real cgroup v2 - authoritative), local (host binary, explicitly labelled non-authoritative), and attach (sample a server somebody else is running - a dev loop, a port-forwarded pod). Fixed scenario set so two runs diff: idle at three role splits, checks at 200/500/1000, login burst, docs crawl, dash0 reload. A scenario that cannot run in the chosen mode is recorded as SKIPPED with the reason, never dropped. Dockerfile.bench compiles the current tree into the production base image using the embedded assets already in it, so a MODIFIED build can be measured and not just a released tag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e run The spec is being archived with its original Problem and Proposal sections intact, but two of their claims were disproven during implementation and both read as authoritative to someone opening this file top-down during an incident. The wrapping predates v0.22.0 by a year, and the suggested repair SQL would break GetOrgParameter rather than fix it. Mark both in place so the correction cannot be missed by a reader who never scrolls to the implementation plan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ecrypt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he detail page The check detail page's Dependencies card was the only editor for a dependency's kind and description, while the edit form — the page you go to when you want to change a check — could only add and remove parents, hard-coding every new edge to kind "hard" and dropping the kind and description of the ones it loaded. Move all mutation into the form's Dependencies section (inline kind select, description input and a destructive remove per row, plus an add row), make the card read-only with an Edit link that deep-links back to that section, and give both lists a proper row surface: one bordered container with divide-y rows, fixed identity/kind/description/actions columns, and a dot-pill kind badge. The edit-page sync gains a third bucket: parents present on both sides whose kind or description changed are PATCHed, via a diffDependencies helper extracted out of the route and unit-tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a "Dependency row" section next to List surface: the bordered divide-y container, the fixed identity/kind/description/actions columns, the in-container empty row, the dashed add-row footer and the dot-pill kind badge. Also adds a locale-parity test for the dependency editor copy, which is now the only place a dependency can be created or retuned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rewrites check-dependencies.spec.ts around the view/edit split: the
dependency is added from the edit page and the detail page is asserted
read-only (link + badge, no pencil, no trash, no picker), plus two new
cases for the paths that were broken — retuning an existing edge hard ->
soft with a description (the toUpdate PATCH bucket) and adding an edge
as soft (the create path no longer hard-codes "hard"). Both were
verified to fail against a deliberately reverted implementation.
The Dependencies card's new Edit link is a second anchor whose href
contains "/edit" on the check detail page, so the suite-wide
a[href*="/edit"] and getByRole("link", {name: /Edit/i}) selectors are
scoped to the page header. For the same reason the card's link carries a
title rather than an aria-label, which would have made getByLabel("Edit")
ambiguous in check-detail.spec.ts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rameter The gc=1 floor mode added a query parameter to /api/mgmt/memory, which changes GetMemoryWithResponse to take a params argument. The committed client was never regenerated, so the tree built locally against the old signature while CI - which regenerates from openapi.yaml before building - failed on all three call sites. Both callers pass nil: the CLI and the integration tests want the steady-state sampling, not the forced-collection floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SPA-shell and docs-404 tests added with the asset streaming change read the real embedded FS, which only carries a built frontend on a developer machine. CI never builds the frontends for the backend job - it writes a one-line placeholder into each embed dir - so the shell test looked for a doctype in "placeholder" and the docs test got a plain http.Error because no 404.html was stubbed. The shell test now compares against whatever the embedded FS holds, which pins the regression more tightly than a doctype substring (the bug served a 404, not a different body), and CI stubs 404.html alongside the other files it already creates for exactly this reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NewEmbedded opened its sql.DB with database/sql's default (unbounded) MaxOpenConns while the embedded server runs with max_connections=10 (3 reserved for superusers). A 50-goroutine concurrency test therefore asks for ~50 backends and everything past the 7th is refused with `53300 sorry, too many clients already` — a test-infrastructure failure that reads exactly like a product bug. Bound the pool inside NewEmbedded itself (MaxOpenConns: 5, MaxIdleConns: 2), reusing applyPoolLimits so there is exactly one place that bounds a pool. Remove the three now-redundant per-test SetMaxOpenConns(4/6) workarounds in tls_storage_postgres_test.go, incident_number_test.go and slug_race_test.go. Document, at New's embedded branch, that Config's pool fields are dropped there on purpose rather than silently. Also stop TestReserveMonthlyUsage_ConcurrentNeverOverruns_Postgres from swallowing reserveErr — it now requires every error to be nil (an over-limit refusal is (false, nil), never an error), so a connection failure fails the test instead of being absorbed into "goroutine lost the race." Add TestApplyPoolLimits_QueuesRatherThanErrors, a driver-agnostic database/sql-level test pinning the production claim: a bounded pool makes callers past MaxOpenConns queue for a connection rather than get refused one, which is what makes bounding a production pool safe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec 2026-09-04-04 analysis claimed NewEmbedded is test-only and production is not affected. That is false: postgres-embedded is a real, user-selectable SP_DATABASE_TYPE reached from main.go's openDB/runMigrations and internal/app/server.go, and NewEmbedded pins max_connections=10 on every path including production. Rewrite the comment at New's embedded branch to say so plainly instead of reasoning about "a real deployment" as if it were hypothetical, and add a brief, accurate note to web/docs/docs/configuration/database.md about the fixed connection ceiling. The bound added in 106757c is unaffected and still correct — it now also fixes a real (if niche) production failure mode, not only a test one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add TestNewEmbedded_PoolBoundedBelowNonSuperuserConnectionLimit: it starts a real embedded-Postgres instance and asserts Service.DB().Stats().MaxOpenConns is positive and below 7 (the server's max_connections=10 minus 3 superuser-reserved slots). This is a cheap, permanent stand-in for the 50-goroutine concurrency tests — if a future change ever widens or drops the applyPoolLimits call in NewEmbedded, this fails immediately instead of waiting for a flaky 53300 to resurface elsewhere. Also fix a misspelling (honouring -> honoring) flagged by golangci-lint's misspell check in the comment added by the previous commit. Positive-control evidence (manual, not committed): with NewEmbedded's applyPoolLimits call commented out, both TestIncrementUsageCounter_Concurrent_Postgres and TestReserveMonthlyUsage_ConcurrentNeverOverruns_Postgres fail with "FATAL: sorry, too many clients already (SQLSTATE=53300)", confirming the 50-goroutine contention is real (not quietly serialized) and that these tests still catch the regression the bound fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ounded pool against a max_connections=10 server, so concurrent tests fail with 53300 Final gate: make build-backend lint-back test all green (0 lint issues, no test failures). Verification from spec 2026-09-04-04, all run for real: - go test ./internal/db/postgres/ -run '...Concurrent...' -count=3: green three times, no 53300. - go test ./internal/db/postgres/ -count=1 (~9.5 min): ok. - go test ./internal/db/ ./internal/db/postgres/ ./internal/handlers/checks/: ok (a one-off internal/db failure from concurrent embedded-postgres binary extraction under -p default was confirmed as the known shared-testcontainer-resource flake, not a regression, by a clean rerun with -p 1). - go test ./internal/notifier/ ./internal/realtime/ ./internal/jobs/jobsvc/: ok with -p 1 (same flake reproduced and cleared the same way under default parallelism); no suite is hurt by the newly bounded pool. No source changes were needed at this step — the two prior commits (106757c and this branch's docs/test follow-ups) already cover the fix, the corrected spec analysis, and the permanent regression guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fclairamb
added a commit
that referenced
this pull request
Sep 4, 2026
## Why `wiki/` holds internal engineering notes and is an input to **no build**. The published docs site is `web/docs/`, which is separate; every other reference to `wiki/` in the tree is a comment pointing at it. So a wiki-only pull request currently spends a ~28-minute E2E run, a Go build, four frontend builds and a full backend test suite proving that prose did not break the compiler. That is not hypothetical — #319 (five files, all under `wiki/`) did exactly that. ## What changed A `changes` job asks the API which files the PR touches and sets one `code` output. The seven build jobs are gated on it. **Nothing else moves** — the jobs, their steps and their ordering are untouched. This is safe *specifically* because of how the existing `ci` gate is written: `always()`, failing only on `failure` or `cancelled`, so a skipped job already satisfies it. That is the same property the release-please skip has relied on all along, and `main` has no required status checks, so there is no check that can hang unreported. If that gate ever starts treating `skipped` as a failure, this must change with it — there is a comment saying so. The gate now also depends on `changes` itself. Without that, a detector that *failed* would skip every build job and leave the gate looking at an all-skipped graph — turning a broken detector into a way to merge untested code. ## Fail-safes The failure mode here is silently not testing things, so the decision fails toward building: - any non-`pull_request` event (pushes to `main`, in particular) always builds; - an empty file list from the API means "the API told us nothing", not "nothing changed" — it always builds; - the wiki-only decision is a POSIX `case` glob, not `grep -qv '^wiki/'`. That last point is not theoretical. `ugrep`, which is `grep` on some developer machines, returns false for inverted-quiet matching even when non-wiki files are present: it classified a **138-file code change as "wiki-only"**, while `grep -cv` on the same input correctly counted 126 non-wiki files. GNU grep on the runner would have behaved correctly, but a check whose meaning depends on which `grep` is installed is not one worth shipping. ## Verification Both live PRs, evaluated with the exact decision logic from the workflow: | PR | Files | Result | |---|---|---| | #319 | 5, all under `wiki/` | `code=false` → skips | | #318 | 138 | `code=true` → builds | YAML parses, and the resulting job graph was checked: all seven build jobs gain `changes` in `needs`, and the `ci` gate depends on it too. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Merged
fclairamb
added a commit
that referenced
this pull request
Sep 4, 2026
release-please can only put the squash-merge subject in the changelog, and #318 carried six specs behind one title — "operator notifications, dependency editing on the edit page, and a measured memory baseline" told a reader nothing about the two bugs it fixed. Expand it into user-facing prose, per wiki/conventions/changelog.md. Three features and two fixes, split out of that single bullet. The internal work in the batch — the memory-profiling runbook and harness, the CI wiki-only skip, and the embedded-Postgres test plumbing — is deliberately left out rather than scoped under deps: none of it changes anything for someone running SolidPing. The embedded-PostgreSQL fix is scoped `database`, not `deps`, on purpose. The docs transform drops every deps-scoped bullet, so filing it there would have erased a real user-facing fix from the published changelog while leaving it in this file. `database` is not in SCOPE_LABELS and so renders as its raw slug, the way `storage` already does in v0.21.2 — worth a follow-up to the lookup table, but not worth holding a release for. Version heading, date and compare link are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fclairamb
added a commit
that referenced
this pull request
Sep 4, 2026
🤖 I have created a release *beep* *boop* --- ## [0.23.0](v0.22.1...v0.23.0) (2026-09-04) ### Features * operator notifications, dependency editing on the edit page, and a measured memory baseline ([#318](#318)) ([7990c78](7990c78)) ### Bug Fixes * **deps:** update github.com/dop251/goja digest to f87b40a ([#312](#312)) ([43bac80](43bac80)) * **deps:** update module github.com/go-jose/go-jose/v4 to v4.1.5 ([#313](#313)) ([48344aa](48344aa)) * **deps:** update module golang.org/x/crypto to v0.56.0 ([#310](#310)) ([e9de249](e9de249)) --- 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.
Five specs from
specs/todos/, implemented sequentially on a batch branch, each gated by an independent completeness audit before being archived. Three of the five needed at least one fix round, and the audits caught two real defects the implementers had reported as done.No database migration — the fix below is a reader change, and existing rows are readable as-is.
What changed for the user
Check credentials can be decrypted by every process, not just the one that generated the key. An organization's data encryption key is stored wrapped as
{"value": …}but was read back raw, so the process that generated the key kept working from its in-memory cache while every other process — each checks worker, and the API itself after its next restart — failed withunknown envelope version. The user-facing advice was "re-save the check's credentials", which cannot help and, after an API restart, stops working entirely because the API can no longer decrypt to merge the update. The reader now accepts the wrapped shape as well as the two legacy ones, a freshly generated key is read back and verified before it is cached so a storage-shape regression fails the first encrypt loudly, and an organization-key failure is now reported as its own cause instead of blaming the check's credentials. Nothing needs rotating or re-entering; the wrapped key was intact all along.Two corrections to the original diagnosis came out of the work, both verified against git history: the wrapping has been there since the first commit rather than since v0.22.0, so cold key loads have been broken since the key store shipped in May — and the remediation SQL the spec proposed would have made things worse, because rewriting the row to a bare string breaks the parameter reader outright. Both are now flagged in the archived spec and documented.
Operators can be paged when a support message arrives or someone signs up. Neither event could reach a human in real time: an inbound support message produced one email to the instance mailbox, and a signup produced nothing beyond an optional analytics event. A new Notifications tab under Server lets a super admin choose which super admins hear about which of those events, delivered over the notification routes each recipient already set up — email, Telegram, a Slack DM, web push or SMS. The page is built around how this feature fails quietly: a recipient with no route is flagged in amber next to their name, a subscriber who has lost super-admin status keeps a visible row explaining why they are skipped, and "Send me a test" reports how many routes actually carried the notice rather than a bare success. Bursts fold the way the support mailbox mirror already folds them, and the email mirror is unchanged.
Check dependencies are edited where every other attribute is edited. The detail page — the view surface for a check — was the only place you could add, retype or delete a dependency, while the edit page offered a weaker editor that could only add and remove parents and hard-coded every new edge to "hard" with no description. The detail page is now read-only with an Edit link, and the edit form carries the whole editor: kind, description, inline staging until Save, and cycle prevention both in the picker and on submit. Both lists also got a real row design, registered in the design reference as a reusable pattern.
The "Getting started" checklist reads like something to finish. It gained depth and a gradient accent, a real progress bar, a single highlighted "next up" step, numbered badges that turn into filled ticks, and a celebratory all-set state. The strikethrough is gone — five struck-through rows looked like a wall of deletions rather than a finished list. Every animation respects
prefers-reduced-motion, which is a new convention for the dashboard and is documented as one.Under the hood
Memory can now be measured instead of eyeballed.
/api/mgmt/memoryreports a real Linux breakdown (process, smaps, cgroup v2, Go runtime classes, and the off-heap figure the runbook asked for), and a newmembenchharness runs the shipped container shape with a warm-up, a sampling window and repeated runs, so a change is judged against its own noise floor rather than a single snapshot. Two of the spec's own premises did not survive contact with the measurements, and the runbook says so: the SQLite driver in the shipped binary is modernc, not mattn, so the driver the build reported was wrong (now fixed) and the off-heap rule rested on it; and idle memory reads ~149 MiB at 45 seconds but ~23–31 MiB at five minutes, so the 60-second warm-up the spec proposed would have measured the startup burst as steady state.Of the seven reduction candidates, two shipped, two were measured and rejected, one was closed by inspection, and the rest are recorded as not attempted with what it would take. The candidate that streams embedded assets instead of copying each one onto the heap is honest about not clearing an RSS gate — the harness cannot resolve it — and ships on an allocation measurement instead.
Verification
The full dash0 Playwright suite was run as a batch-level gate rather than per-spec, which caught two cross-spec selector collisions introduced by the new Edit link. Four failures remained; all four were A/B'd against
origin/mainin isolated worktrees and reproduce there, so none is introduced by this branch. Two are not product failures at all — they only appear when the E2E server is started without the environment variables CI sets. The other two are pre-existing defects now written up as their own specs: anipVersionclear-to-Auto bug, and an unbounded connection pool against the embedded Postgres server.Added since this branch was first opened
The unbounded embedded-Postgres pool mentioned above is no longer just a filed spec — it is fixed here.
NewEmbeddedopened itssql.DBwithdatabase/sql's unbounded default against a server ititself pins at
max_connections=10(7 usable after superuser reservations), so a 50-goroutinetest asked for ~50 backends and everything past the 7th got a hard
53300 sorry, too many clients already. It is now bounded in the constructor via the sameapplyPoolLimitsthenon-embedded path uses, and the three suites that each hand-rolled their own
SetMaxOpenConnsworkaround no longer need to.One correction worth calling out, because the spec got it wrong: the spec concluded this was
purely a test-infrastructure bug on the grounds that
NewEmbeddedis test-only. It is not.postgres-embeddedis a real, user-selectableSP_DATABASE_TYPEreached frommain.go'sopenDBandrunMigrationsand frominternal/app/server.go, so before this change anoperator running embedded Postgres would hit those same hard
53300errors under modestconcurrency. The fix is unchanged — bounding the client turns the errors into queueing — but it
is a product fix as well as a test fix, and the archived spec now carries an amendment saying
so rather than leaving the false premise on the record.
The guard is proven by a positive control, not just a green run: with the bound removed both
concurrency tests fail again with the original
53300, and the Postgres log shows dozens ofsimultaneous rejections, so the contention is still genuinely concurrent.
🤖 Generated with Claude Code