Repository navigation
Batch 2026-07-08: 24 specs (enterprise auth, session/auth fixes, monitoring UI) + infra fixes - #118
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a one-click MCP connector setup experience: a new "AI assistants" dashboard page (Organization section) with the instance MCP URL, per-client install affordances (Claude, Claude Code, Cursor, VS Code, generic/other), read-only-scope and self-hosted-caveat callouts, plus a docs rewrite that leads with the URL-only OAuth flow and demotes the manual bearer-token flow to a headless/CI fallback section.
…name Every incident.* event (created/escalated/escalation_failed/resolved/ reopened/acknowledged/unacknowledged/snoozed/unsnoozed, single + group variants) now sets CheckUID on the event row and includes check_uid, check_slug, check_name in the payload. Previously acknowledged/unack/ snoozed/unsnoozed had none of these, and created/resolved/reopened had check_uid+check_slug but not check_name. emitEvent now sets event.CheckUID from incident.CheckUID in one place, covering the created/escalated/resolved/reopened funnel. The four direct-event paths (ack/unack/snooze/unsnooze) and the escalation-step job's soft-failure event each get a best-effort GetCheck lookup so the payload can carry check_slug/check_name too.
activation.Emit gains a trailing variadic extra ...models.JSONMap param. Two reserved keys (_incident_uid, _check_uid) are popped off and set on the event row (IncidentUID/CheckUID) instead of being written to Payload; everything else merges into the stored payload alongside "source". All existing call sites keep compiling unchanged since the new param is optional. Wires it up for: - org.activation.first_notification_configured (channel creation + Freebox pairing in integrations/service.go): payload gains channel_uid, channel_name, channel_type from the just-created Connection. - org.activation.first_incident_paged (job_notification.go): links the event to the incident/check that triggered the first page via the reserved UID keys, plus check_slug/check_name in the payload.
…ctivity event-display.tsx: getEventCheckName now falls back to payload.check_slug when check_name is absent (covers pre-existing incident events that only ever captured the slug). Adds getEventChannelName/getEventChannelUid for the notification-channel enrichment, and a distinct Rocket icon for org.activation.* milestones (previously fell through to the generic Calendar). RecentActivityList (dashboard card) now renders BOTH the incident link and the check link when their UIDs are present, instead of only ever showing a single check link gated on payload.check_name. The channel-name variant of "First Notification Configured" links the channel name to its integration edit page when channel_uid is present, falling back to the generic sentence for historical events lacking the new payload fields. i18n: fill every missing types.*/descriptions.* key for the full 18-value EventType constant list (escalation_failed, reopened, unacknowledged, snoozed, unsnoozed, status_update.*, signup_completed, first_result_received, first_incident_paged) across en/de/es/fr, keeping this repo's existing all-locales-key-parity convention. Leaves the pre-existing incident.opened key untouched (no matching backend constant, out of scope here).
…nline
CopyableValue and CopyErrorButton in notifications.$notificationUid.tsx
duplicated the same clipboard mechanics already factored out into
copyable-code.tsx's copyToClipboard(). Extract a shared CopyableInline
primitive (inline value + button, or bare button via inline={false}) and
register it on the design reference page.
…ifecycle Adds event_check_enrichment_test.go covering the full incident lifecycle through ProcessCheckResult (created, resolved, escalated, reopened) and the direct-event paths (acknowledge, unacknowledge, snooze, unsnooze), asserting every emitted event has CheckUID set on the row plus check_uid/check_slug/check_name in the payload. Also pins the no-backfill contract: a hand-built "old shape" event round-trips through ListEvents with no row CheckUID and no synthesized check_name. Adds two activation tests: extra payload + reserved UID keys (_incident_uid/_check_uid) round-trip onto the event row without leaking into the stored payload, and the plain channel-payload shape used by first_notification_configured. Required creating real check/incident rows for the FK-backed columns — an initial version using placeholder UUIDs passed vacuously (Emit swallows the FK failure and inserts nothing, which ListEvents then reported as zero events, not a failure). Also splits Emit's extra-merging loop into a small applyExtra helper to keep cyclomatic complexity under the linter's threshold.
Extends mockDashboard() to accept an events fixture, and adds three dashboard E2E tests: an incident.created row renders both the incident link and a named check link; a historical event missing check_name falls back to check_slug for the check link text; and a real channel created via the API surfaces its name as a link to the integration page in the first_notification_configured row. All three verified passing against the local :4000 test-mode devloop.
integrations/service_test.go: TestCreateChannelEmitsEnrichedActivationEvent drives CreateIntegration through a real webhook creation and asserts the resulting first_notification_configured event carries channel_uid/ channel_name/channel_type. test/integration/incident_notifications_audit_test.go: TestFirstIncidentPagedEventIsEnriched reuses the existing notificationAuditSetup/triggerIncident harness to run a real NotificationJobRun end to end and asserts first_incident_paged links to the triggering incident/check (IncidentUID/CheckUID on the row) with check_slug/check_name in the payload.
…07-06-2 Recent activity: incident/check links, activation event rendering, notification channel name. - Backend: every incident.* lifecycle event (created/escalated/ escalation_failed/resolved/reopened/acknowledged/unacknowledged/ snoozed/unsnoozed, single + group) now sets CheckUID on the row and includes check_uid/check_slug/check_name in the payload. - activation.Emit gains an optional extra payload (variadic models.JSONMap) with two reserved keys (_incident_uid/_check_uid) that link the resulting event row to an incident/check. Wired up for first_notification_configured (channel_uid/channel_name/channel_type) and first_incident_paged (incident/check linkage + check_name). - Frontend: RecentActivityList renders both the incident link and a named check link (falling back check_name -> check_slug -> generic label); first_notification_configured shows the channel name linked to its integration page; a distinct icon for org.activation.* milestones; every EventType constant now has an i18n label across en/de/es/fr. - Tests: 9 Go tests pinning the enriched payloads across the incident lifecycle + both activation call sites, 3 new Playwright E2E tests for the dashboard Recent activity card.
Consolidates CopyableValue and CopyErrorButton in notifications.$notificationUid.tsx into a shared CopyableInline primitive in copyable-code.tsx.
…ntion) Postgres: DISTINCT ON (check_uid) + organization_uid predicate to ride results_raw_idx. SQLite: equivalent ROW_NUMBER() OVER (PARTITION BY check_uid ...) window-function CTE, joined back to the base table so the rn column never leaks into the models.Result scan. Both backends now stay behaviorally identical per the sync-pg-to-sqlite convention. Threads organization_uid through the three call sites (ListChecks, GetCheck, checkworker's executePassiveJob) and the mock in slack_test.go. Previously both backends fetched every raw row for the requested checks and kept only the newest per check in a Go loop — with the default 24h raw retention, a 1-minute check could return up to 1,440 rows to keep 1.
… scoping Seeds 50 raw rows across two checks on both backends and asserts the map returns exactly one row per check (the newest by period_start), plus a second test proving the organization_uid predicate actually scopes results and doesn't leak across organizations.
…shot The auth Service froze a value copy of config.AuthConfig in NewService, before InitializeSystemConfig applies the system-parameter overlay onto the shared *config.Config. Fields the overlay mutates were therefore stale. Audit of every s.cfg.X read on the Service: - RegistrationEmailPattern (Register): overlaid -> read live fullCfg.Auth. Fixes the visible bug where registration reported enabled but 403'd. - SessionMaxDuration (resolveSessionMaxDuration): overlaid -> read live; corrected the misleading doc-comment that claimed s.cfg was runtime-current. - JWTSecret (5 sign/validate sites + passkey_session.go 2 sites): overlaid AND auto-generated post-NewService by ensureJWTSecret -> read live at sign AND validate. The rest of the system (unsubscribe, feedback/files signed URLs, incident magic links, email) already uses the live secret; the frozen Service copy meant session JWTs were signed with the empty/default pre-overlay secret. - AccessTokenExpiry, RefreshTokenExpiry: NOT overlaid -> left frozen, with a struct comment documenting the boot-only-vs-live split. OAuth provider config (Google/GitHub/... and OIDC/SAML/LDAP) lives at the top level of config.Config and its provider services hold a live *config.Config, so no provider field is read from the Service's frozen snapshot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
golangci-lint (goconst) flagged the repeated "name" map-key literal in client.go. CI runs golangci-lint over the whole server tree with only-new-issues disabled, so this pre-existing finding fails the backend-lint job. Extract it into the existing apiKey* constant block, mirroring apiKeyChannel/apiKeyText/etc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…of 401 GetUserInfo unconditionally resolved claims.OrgSlug via GetOrganizationBySlug, which errors with ErrOrganizationNotFound (mapped to 401) for the empty slug a zero-org session carries — silently destroying a legitimate session on the next page load. Split out getUserInfoNoOrg: for an empty OrgSlug, skip the org and membership-role lookups, return Organization: nil and role "" (RoleSuperAdmin for superadmins), mirroring completeLogin's resolvedOrg==nil branch. The non-empty-OrgSlug path is unchanged. MeResponse.Organization gains omitempty so a nil org is an absent field, not null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MeResponse.organization is now optional (matches AuthResponse). The up-front forced refresh for tokens missing expires_at now only fires when a refresh token exists — a zero-org session has none by design, so the unconditional refresh escalated to logout. With no refresh token, /auth/me is the arbiter: a 200 (org absent) keeps the session, a 401/403 still clears it in the catch, so zombie-loop protection for legacy sessions with a refresh token is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ves the user holding a token scoped to no org
…ow unbounded The rotating log writer preferred to cut the active file at a line boundary at or after maxSize, but when an incoming chunk contained no newline it appended the whole chunk without rotating (rotationCut returned -1). A backend hot-looping on an error — or emitting one pathologically long line — therefore grew logs/backend.log without any bound; a stuck dev-test loop produced a multi-GB backend.log this way and helped fill the disk. Enforce a hard ceiling of 2*maxSize: the line-boundary preference still applies below it, but once the active file would reach the ceiling it is force-cut mid-line, so total on-disk size stays bounded by 2*maxSize*(backups+1) whatever the child emits. Adds TestRotatingWriterHardCapsNewlinelessStream covering the previously-uncovered no-newline path (would breach the ceiling pre-fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # server/cmd/devloop/rotate.go # server/cmd/devloop/rotate_test.go # server/go.mod # server/go.sum
… is still valid The zombie-loop fix made every 4401 close run refresh-or-give-up, so a server-initiated 4401 on a token that has NOT expired (transient server state, proxy hiccup, restart) tore the session down instead of reconnecting. This regressed the pre-existing live-updates e2e "reconnect after a server-initiated close (4401-style)" test. Gate the refresh-or-give-up path on the token being unprovable-as-live (expired or unknown expiry), mirroring the run() loop's pre-dial check: when the access token still has time left, a 4401 is transient — reconnect with the current token. The zombie case (past/absent expiry + refused refresh) still gives up. Adds a unit test for the still-valid-token reconnect path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h the Service TestRegistrationOverlayPathEnablesRegister models the real boot ordering: the Service is constructed with an EMPTY AuthConfig, then registration_email_pattern is applied ONLY by mutating the shared *config.Config pointer afterward (as the systemconfig overlay does between NewServer and route setup). Register must then succeed, and GET /api/v1/auth/providers registrationEnabled must agree in both the unset (both off) and overlay-set (both on) states. Baking the pattern into the AuthConfig passed to NewService would not reproduce the bug. TestSessionMaxDurationSystemWideViaOverlay pins that a system-wide session_max_duration applied via the overlay (no org override) is honored.
fclairamb
force-pushed
the
batch/2026-07-08
branch
from
July 9, 2026 21:30
5afe5b3 to
b16e2cc
Compare
fclairamb
enabled auto-merge (squash)
July 9, 2026 21:30
Merged
fclairamb
added a commit
that referenced
this pull request
Jul 9, 2026
🤖 I have created a release *beep* *boop* --- ## [0.2.2](v0.2.1...v0.2.2) (2026-07-09) ### Features * **auth:** enterprise SSO — generic OAuth2/OIDC provider (discovery + ID-token validation, `email_verified` gating against account-takeover), SAML 2.0 SP (`crewjam/saml`, full assertion validation, metadata endpoint), and LDAP/AD bind auth (search-then-bind, RFC-4515 escaping, super-admin lockout guard), all sharing `maxSsoUsers` entitlement counting and `UserProvider` linking ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **auth:** per-org configurable max session duration — `auth.session_max_duration` system parameter with a per-org override on the org settings API ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **dash0:** multi-region response-time chart shows red failure segments and always-visible failure dots even on dense views ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **dash0:** colored status badges in "Checks at a glance" and the pinned check-result box ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **incidents:** restyled ack magic-link page with a countdown redirect to the incident ([#118](#118)) ([4f9cfeb](4f9cfeb)) ### Bug Fixes * **auth:** sessions no longer die ~1h after login — repaired the token-refresh chain and an OAuth handoff that dropped the refresh token ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **auth:** creating an org from `/no-org` now mints an org-scoped session token instead of leaving every API call 403'ing ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **auth:** `auth.Service` reads overlaid `AuthConfig` fields (registration-disabled, session cap) live instead of from a frozen snapshot ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **auth:** a zero-org session is kept alive — `/auth/me` returns no-org info instead of 401-logging-out the user ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **dash0:** deep links survive login — every post-login path resolves through a shared `returnTo` resolver with a tightened same-origin open-redirect guard ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **dash0:** uptime-bar badge shows the full 7d/30d/90d history instead of only 1/2/3 days ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **dash0:** `validateSearch` boolean query-params (`showSuppressed`, `graphFull`) no longer silently no-op from string-vs-bool coercion ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **checks:** `created` lifecycle results are no longer swallowed by aggregation ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **slack:** channel/user pickers paginate (cursor) so large workspaces are fully searchable ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **devloop:** hard-cap the rotating log so a newline-less/hot-looping child can't grow `logs/backend.log` without bound ([#118](#118)) ([4f9cfeb](4f9cfeb)) * **deps:** update go dependencies (non-major) ([#113](#113)) ([8d8debd](8d8debd)) * **deps:** update go dependencies (non-major) ([#115](#115)) ([d53dafa](d53dafa)) * **deps:** update go dependencies (non-major) ([#116](#116)) ([179d2f5](179d2f5)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
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.
Integration branch for the 2026-07 spec batch: 24 specs implemented, QA'd, and each gated behind an independent completeness/security audit before archiving, plus two infrastructure fixes. 225 commits.
Enterprise auth (largest item — staged in 3 audited parts + cross-cutting review)
go-oidc, config UI). A security audit caught and fixed an account-takeover vector — auto-linking by email is now gated on a realemail_verified: trueclaim, matching Google/Discord.crewjam/saml; signature/Conditions/Audience/Recipient/InResponseTo all enforced; metadata endpoint; config UI).maxSsoUsersentitlement counting andUserProviderlinking; global-only config, one instance each.Auth / session correctness
auth.session_max_duration(system param + per-org override); fixes several silent refresh failures and an OAuth handoff dropping the refresh token./no-orgnow mints an org-scoped session token (previously every API call 403'd for a first-time org creator).auth.Serviceread a frozenAuthConfigsnapshot — per-org overlay changes (registration-disabled toggle, session cap) were silently ignored because the Service cached config at construction; it now reads the overlaid fields live./auth/meand was bounced to login;/auth/menow returns no-org info and the dash keeps a zero-org session alive invalidateSession.orgRedirect, default, org-picker, and the already-authenticated effect) now resolves its destination through one sharedresolveDestinationhelper, so a deep link survives login instead of dumping the user on the org root. Tightened the open-redirect guard to a same-origin relative-path check (rejects absolute ///hostURLs); verified OAuthredirect_uriround-trips through the callback.Monitoring / dashboard
createdlifecycle results no longer swallowed by aggregation.validateSearchboolean query-params (showSuppressed,graphFull) fixed (string-vs-bool coercion) + earlier 07-06/07-07 specs.Test / tooling
Infrastructure fixes (this session)
logs/backend.logwithout bound (this filled the disk); now bounded to2*maxSize*(backups+1).goconstlint — extractedapiKeyNameconstant so the whole-repogolangci-lintCI gate passes (only-new-issuesis disabled in CI).Each spec was archived only after a fresh, skeptical, independent audit returned clean; security-sensitive specs (auth) went through multiple audit+fix rounds.
🤖 Generated with Claude Code