Skip to content

batch/2026-08-05: status-page availability thresholds, slug max length 100, ?q= URL sync, agents fleet view - #196

Merged
fclairamb merged 35 commits into
mainfrom
batch/2026-08-05
Aug 5, 2026
Merged

fclairamb merged 35 commits into
mainfrom
batch/2026-08-05

Conversation

@fclairamb

Copy link
Copy Markdown
Owner

Batch of four specs from specs/todos/, each implemented by an isolated subagent and gated on an independent completeness audit before archiving. Also carries the fly.io Tokyo agent deployment config.

Specs

2026-08-03-01 — Per-status-page availability color thresholds (sonnet/high)

Per-page green/amber/red thresholds via a new typed settings jsonb column on status_pages, plus a small-bucket calibration guard so a single failed sample can never render a bucket red (fixes the "one failed minute = red hour" cliff on 1-minute checks). PATCH uses whole-section replace (no deep merge) with DisallowUnknownFields; the public payload always exposes resolved effective thresholds. Badges deliberately stay on the global defaults.

2026-08-04-01 — Entity slug max length 100 (sonnet/low)

Slug cap raised to 100 across checks, check groups, status pages and severities, backend and frontend.

The spec's premise was wrong and this PR deviates from it deliberately. It claimed slug limits were "purely regex-side" and instructed not to add a migration. There are in fact DB-level CHECK constraints in both dialects (001_v0_1_0.up.sql: sqlite length(slug) <= 40/50, postgres {2,39}/{2,49}). Shipping it as written would have let a 90-char slug pass validation and then fail at the database — a 500 instead of a validation error. Migration 009_v0_8_0 therefore widens the constraints too, and the spec text was corrected in-tree.

SQLite can't alter constraints, so the four affected tables are rebuilt with the *_new + PRAGMA foreign_keys=OFF pattern already established in 005_v0_4_0. INSERTs name every column explicitly (not select *) to remove the positional-drift hazard, and migration_009_slug_rebuild_test.go seeds every column with distinguishable non-default values before 009 and asserts them byte-for-byte after, plus FK survival and PRAGMA foreign_key_check.

2026-08-04-02 — Search inputs sync to the URL as ?q= (sonnet/medium)

All seven dash0 list pages mirror their search box to ?q=. Local state is seeded from the URL via a lazy initializer and written back debounced with replace: true and a functional updater — validateSearch alone is not enough, since URL-only state is dropped on a cold deep-link under a layout route. E2E covers type→URL, reload, and cold deep-link.

2026-08-05-01 — Fleet-wide Agents tab on the server page (sonnet/medium)

GET /api/v1/system/agents (superadmin) plus a server-page tab listing every agent across the instance, including kind='system' agents that belong to no org and were previously listed nowhere. Includes a >5min staleness cue, because the GC only retires system agents after 7 days — so a dead-for-days agent otherwise looks healthy.

Also included

chore(fly): deploy/fly/fly.nrt.toml for solidping-agent-nrt, the Tokyo system agent staffing the new jp-1 region, with identity pinned via SP_AGENT_KEYS rather than a volume. Also fixes the README's SP_AGENT_NAME guidance, which recommended a /bin/sh ENTRYPOINT that cannot run against the distroless image.

QA

Each spec ran its scoped gate; the coordinator re-ran make build-backend, lint-back, test, build-dash0 and dash0 eslint independently rather than trusting the reports. Backend lint 0 issues, tests green, dash0 eslint held at its 44-error pre-existing baseline with none introduced.

Not included

specs/todos/2026-08-04-03-agent-never-logs-private-keys.md was excluded — a concurrent session owns it.

🤖 Generated with Claude Code

fclairamb and others added 30 commits August 5, 2026 01:38
Add fly.nrt.toml for `solidping-agent-nrt`, the platform check agent
staffing the new `jp-1` (Tokyo) region against the k8xp dev API.

Identity is pinned via the SP_AGENT_KEYS secret rather than a volume:
loadIdentity() prefers SP_AGENT_KEYS over the keys file, and because the
pinned identity is already enrolled, every boot takes the reconnect path.
That keeps the agent row stable across `fly deploy` (which recreates
machines) and avoids persistIdentity(), which logs the private keys in the
clear and is only reachable from the enrollment path. The trade-off — fly
secrets are app-wide, so this must return to the volume model before
scaling past one machine — is documented in the toml.

Sized at 256mb from measured usage: the equivalent in-cluster workers sit
at 27 MiB and 93 MiB.

Also fix the README's SP_AGENT_NAME guidance, which recommended a
`/bin/sh -c` ENTRYPOINT that cannot run against the distroless image, and
would have given every machine in a region the same name.

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

- settings jsonb/text column with typed StatusPageSettings/AvailabilitySettings
- PATCH semantics: no deep merge, unknown keys rejected, explicit null resets
  a section to defaults, absent settings leaves the page untouched
- availabilityToStatus parameterized on the page's effective thresholds and
  a single-failure calibration guard (exactly one failed sample never
  renders down)
- public + admin payloads expose the resolved effective thresholds
… defaults

Badges stay check-scoped on the global thresholds (spec 2026-08-03-01
Decisions) — this only removes the literal duplication, no behavior change.
Extract validateStatusPageUpdate/createRequestAvailability to keep
Create/UpdateStatusPage under the complexity budget; drop named returns
and one-letter identifiers flagged by golangci-lint.
…ion guard, PATCH semantics, public payload

Table-driven, testify/require, t.Parallel(): availabilityToStatus with
custom thresholds and defaults-unchanged positive control; the 1-vs-2
failure calibration guard; validateAvailabilitySettings and
parseSettingsField; end-to-end PATCH semantics (absent, replace,
explicit-null reset, invalid pair rejected); create-time settings; and
the public ViewStatusPage payload exposing resolved effective thresholds.
Two numeric inputs (up/degraded, empty = default) next to Show
Availability, edit form only. Also documents settings/availabilityThresholds
on the StatusPage OpenAPI schema and the settings PATCH semantics.
… form

Authored against the shared status-pages.spec.ts pattern (create → edit →
set/save/reload → clear/save/reload). Could not be run locally: the
:4000 devloop in this worktree is not in SP_RUNMODE=test and must not be
disturbed (a separate session is using it).
…holds

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Backend regexes for checks, check groups, status pages, and severities
now accept 3-100 char slugs (was 3-20/3-40, inconsistent). Updates
error messages and the stale checks/service.go comment to match.
Org slugs, private region slugs, and auto-generation caps are
deliberately left unchanged.
Mirrors backend regex widening: slugify() truncation, check-form and
status-page-form patterns/messages, and the checks-index/check-groups-edit
group slug regexes now all allow 3-100 chars, with comments updated to
match.
…n 009

The spec's premise that "the limit is purely regex-side" was wrong:
check_groups/checks/status_pages/status_page_sections carry DB-level
length CHECK constraints (40/50 chars, mirroring the old Go regex) in
both the sqlite and postgres migration 001. Without widening these
too, a slug between 41/51 and 100 chars would pass the widened Go
regex and then be rejected by the database as a 500, worse than
before.

Postgres: simple drop/add constraint swap. SQLite has no ALTER
COLUMN/DROP CONSTRAINT, so each table is rebuilt with the *_new
create/copy/drop/rename pattern already established by
005_v0_4_0.up.sql, including disabling foreign_keys for the duration
(these tables are FK-referenced parents; with enforcement on, DROP
TABLE fires child ON DELETE actions against live rows before the
rebuilt table is swapped back in) and spelling out explicit column
lists in INSERT ... SELECT instead of `select *`, which is positional
and would silently scramble data on any column-order drift.

Verified via a dedicated test that seeds rows in all four tables
under migrations 001-008 only, then runs 009 and asserts every field
(including the checks table's late-added flap_*/config_sealed/
region_spread columns) and FK relationships survive unchanged.

Also corrects the spec's "no migration needed" text and adds a
Proposal item documenting the required migration.
Adds 100-accepted/101-rejected/3-accepted/2-rejected cases to the
checks/checkgroups/severities/statuspages slug validation tests, plus
an end-to-end test per resource (through the real SQLite migration
chain, not just the in-memory regex) proving a 100-char slug actually
persists now that the DB-level CHECK constraints are widened too.

Also fixes stale slug-length doc comments left over from the earlier
20/39/49-char regexes.
The spec asserted slug length limits were purely regex-side (no
varchar(N) in migrations) and explicitly said not to add a migration.
That was wrong: check_groups/checks/status_pages/status_page_sections
carry DB-level length CHECK constraints mirroring the old regex
bounds. Documents the correction and adds Proposal item 5 covering
the required migration, landed in the previous two commits.
lll, revive context-as-argument, sqlclosecheck and testifylint findings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
make build-backend + lint-back (0 issues) + test all green; make build-dash0
green and dash0 eslint at 44 errors, unchanged from base with none introduced
in the touched files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seeds local search state from the URL on mount (lazy initializer,
same cold-load fix as discovery.new.tsx) and writes the debounced
value back with replace:true so the query is bookmarkable and
survives reload/back-navigation.
status-updates, status-pages, maintenance-windows, integrations,
escalation-policies, and dependencies index pages now mirror their
client-side search/filter box in ?q=, seeded from the URL on mount
(cold-load safe) and written back debounced with replace:true.
dependencies.index.tsx's local filter state is renamed search/setSearch
to match the common pattern; the q URL param and existing
data-testid="dependencies-filter" are unchanged.
Adds a data-testid to the checks search input and a Playwright E2E
(checks-search-url-param.spec.ts) covering typed search -> URL update
-> reload persistence, cold deep-link ?q=, and q surviving groupBy
changes and vice versa. Authored but not run locally: the :4000
devloop here is not SP_RUNMODE=test (test-org login 401s).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New DB method returning every non-deleted agent (org + system kind)
across all organizations, mirrored in sqlite and postgres per the
sync-pg-to-sqlite discipline. Backs the upcoming superadmin fleet view
— today system agents (no owning org) are visible nowhere short of a
direct DB query.
Superadmin-only endpoint listing every agent across all orgs plus
platform-operated system agents. AgentResponse gains optional kind
and org fields (org-scoped ListAgents leaves them omitted since every
row there is implicitly kind=org).
Service-level test asserts both an org agent (with its org slug
attached) and a system agent (no owning org) are returned by
ListAllAgents. Handler-level test exercises the RequireSuperAdmin gate
on the new route: non-superadmin gets 403, superadmin gets 200.

Also implements the new mockDBService.ListAllAgents method required by
the db.Service interface change.
New superadmin-only tab listing every deployed agent — org-enrolled
and platform-operated system agents alike — with Kind/Org/Name/Region/
Fingerprint/Last seen/Status columns. Modeled on the existing
AgentsCard, using regionDisplayLabel for regions and a 30s refetch
like useAgents.

Highlights an active agent whose lastSeenAt is older than 5 minutes
with a "Stale" warning badge: connected agents refresh last_seen_at on
every ping tick, but the GC job only retires a silent system agent
after 7 days, so without this cue a dead-for-days agent looked fine.
tabs.agents plus the full agents.* section (title, description, empty
state, column headers, stale badge) in en/fr/de/es.
The "system agents are listed nowhere / a mgmt read view is
deliberately out of scope" note is stale now that GET
/api/v1/system/agents and the dash0 server-page Agents tab ship.
Also documents the new route in the agents API spec page.
fclairamb and others added 5 commits August 5, 2026 03:50
Coordinator-verified: make build-backend, lint-back (0 issues) and test all
green; make build-dash0 green and dash0 eslint at 44 errors, unchanged from
base. The one added warning (react-refresh/only-export-components) matches the
existing sibling server.*.tsx route files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `?q=` URL sync added in 2026-08-04-02 fired its write-back effect
unconditionally on mount, including when the URL already matched the
(empty) search box. `navigate` is anchored to the list route via `from`,
so that redundant call re-targets the route from wherever the router has
since moved.

On a logged-out deep link that is fatal: the /orgs/$org guard has already
bounced to /login, the still-mounted list page navigates back to itself,
and the guard bounces again — this time folding login's own search params
into returnTo. Each pass nests returnTo one level deeper, so the URL grows
without bound until the renderer hangs with no document at all. Any
logged-out deep link to checks, dependencies, escalation-policies,
integrations, maintenance-windows, status-pages or status-updates hit it,
which is what took out the login deep-link E2E test.

Only write when the URL is actually stale. Verified as a positive control:
the three new logged-out deep-link tests fail (hung renderer, 40s timeout)
against a build without the guard and pass with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clicking Save fires a PATCH from the SPA with no document navigation, so
the `waitForLoadState("networkidle")` that followed it could resolve
before the request was even issued. The `page.reload()` right after then
raced the write and re-rendered the previous server state — the test read
"Notification connection" where it expected "All admins", and "All admins"
where it expected "User". This was failing all three attempts on main.

Wait for the PATCH response itself. Ran green 3x in a row locally.

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

live-updates.spec.ts:350 flaked in CI: the row stayed visible for the whole
15s window. The single UI budget had to cover server-side incident resolution
AND the 3s LIVE_INVALIDATE_MIN_INTERVAL_MS damper AND the refetch, so a slow
server under CI load consumed the budget meant for the live path.

Poll the incidents API until the incident is actually inactive first, then
assert the UI hid the row. The UI assertion now measures only what the test is
about — that the live path delivers a server-side change without a reload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fclairamb
fclairamb merged commit 977e832 into main Aug 5, 2026
11 checks passed
@fclairamb
fclairamb deleted the batch/2026-08-05 branch August 5, 2026 06:37
fclairamb added a commit that referenced this pull request Aug 5, 2026
Correct the release cut. Squash-merging #196 collapsed its seven feat: commits
into a single non-conventional subject, so release-please saw no feature and
proposed a 0.7.2 patch with a changelog listing only the DNS fix — for a
release that ships four features and a migration named 009_v0_8_0.

Set the version to 0.8.0 and write the changelog from the squashed commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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