feat(alerts): DS alignment + AGENT_LIFECYCLE + single-inbox redesign #146
Reference in New Issue
Block a user
Delete Branch "feat/alerts-ds-alignment"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Three layers of work, landed together because they all rework the same alerts area:
Alerts UI brought onto the design system. Inbox / All / History / Rules / Silences pages rewritten to use
DataTable,ButtonGroup,Toggle,Dropdown,ConfirmDialog,DateRangePicker, and shared page-header + expanded-row helpers. Severity badges and state chips standardised. Wizard banners → DSAlert; removed local CSS var leaks.New
AGENT_LIFECYCLEalert condition (backend + UI). Six-entry allowlist enum (REGISTERED / RE_REGISTERED / DEREGISTERED / WENT_STALE / WENT_DEAD / RECOVERED) with per-subject fire-mode — onealert_instanceper(agent, eventType, timestamp)via a_subjectFingerprintin the evaluator firing context. IncludesAgentEventRepository.findInWindow,AgentLifecycleEvaluator, rule-editor condition form, and Mustache variable registry updates. V15 (exchange id) and V16 (generalised fingerprint) migrations back it.Alerts inbox redesign — the big one. Single filterable inbox replaces the Inbox / All / History split. Ack, read, and delete become global timestamp flags on
alert_instances(no more per-useralert_readstable). Driven by a spec + 16-task plan indocs/superpowers/specs/anddocs/superpowers/plans/.What the inbox redesign changes
Data model (V17 migration):
ACKNOWLEDGEDfromalert_state_enum(ack is now orthogonal —acked_attimestamp only).read_at+deleted_atcolumns toalert_instances.alert_readstable.state IN ('PENDING','FIRING') AND deleted_at IS NULLso ack doesn't close the slot and soft-delete frees it.Backend endpoints (
AlertControllerunder/api/v1/environments/{envSlug}/alerts):GET /gains tri-stateacked+readquery params; always excludes soft-deleted rows.POST /{id}/acknow setsacked_atonly; no state change.POST /{id}/read+POST /bulk-readwrite toalert_instances.read_at(no morealert_readsjoin).POST /bulk-ack(VIEWER+),DELETE /{id}(OPERATOR+, soft-delete),POST /bulk-delete(OPERATOR+),POST /{id}/restore(OPERATOR+, undo).AlertInstanceRepository.filterInEnvLive(ids, envId)collapses the prior N+1findByIdloop to one SQL round-trip.AlertDtogainsreadAt;deletedAtstays off the wire.UI:
/alerts/inboxpage with four filter dimensions (Severity, Status, Hide acked, Hide read — defaults: FIRING + hide-acked + hide-read). Row actions: Ack · Mark read · Silence rule… · Delete. Bulk toolbar with confirmation modal for delete.SilenceRuleMenucomponent — DSDropdownwith 1h / 8h / 24h / Custom presets; Custom navigates to/alerts/silences?ruleId=…and prefills the form.useRestoreAlert.AllAlertsPage+HistoryPageremoved (status filter in the inbox covers those cases). Sidebar trims to Inbox · Rules · Silences. Stale/alerts/all//alerts/history404 per clean-break policy.AlertStateChip+ CMD-K deep-link-search updated for the three-state enum.Docs:
HOWTO.mdrewritten to cover a brand-new-environment walkthrough viadocker-compose.yml(full stack — PG + ClickHouse + server + UI)..claude/rules/app-classes.md,.claude/rules/ui.md, andCLAUDE.mdupdated for the new surface.Test plan
Backend:
mvn clean verify— confirm BUILD SUCCESS.mvn -pl cameleer-server-app -Dtest='*Alert*,V17MigrationIT,V12MigrationIT' test— expect 122/0 green. CoversV17MigrationIT(enum drop + column add + table drop + index predicate),PostgresAlertInstanceRepositoryIT(23 tests includingfilterInEnvLive_excludes_other_env_and_soft_deleted,bulkMarkRead_respects_deleted_at,ack_setsAckedAtAndLeavesStateFiring,findOpenForRule_skips_soft_deleted,restore_clears_deleted_at),AlertControllerIT(15 tests includingread_is_global_other_users_see_readAt_set,delete_non_operator_returns_403,bulkDelete_only_affects_matching_env),AlertStateTransitionsTest,AlertingFullLifecycleIT,AlertingEnvIsolationIT,AgentLifecycleEvaluatorTest.ACKNOWLEDGEDrow pre-migration, confirm post-migrationstate='FIRING' AND acked_at IS NOT NULL.UI:
cd ui && npm run build— TS strict + Vite bundle green.cd ui && npx vitest run— 76/76 tests green (includes newInboxPage.test.tsx: default filters, Hide-acked toggle, Ack visibility, bulk-delete dialog count, role-gated delete, undo toast).Manual smoke (via
docker compose up -d --build):/alerts/inboxdefault view shows only unread-firing alerts (FIRING + hide-acked + hide-read)./alerts/silences./alerts/silences?ruleId=<id>with the Rule ID prefilled.readAtset./alerts/alland/alerts/historyreturn 404.AGENT_LIFECYCLErule in the wizard; trigger a stale/dead event; one alert per agent+event fires.🤖 Generated with Claude Code
Surfaced during second smoke: 1. Notification bell moved — was first child of TopBar (left of breadcrumb); now rendered inside the `environment` slot so it sits between the env selector and the user menu, matching user expectations. 2. Content tabs (Exchanges/Dashboard/Runtime/Deployments) hidden on `/alerts/*` — the operational tabs don't apply there. 3. Inbox / All alerts filters now actually filter. `AlertController.list` accepts only `limit` — `state`/`severity` query params are dropped server-side. Move `useAlerts` to fetch once per env (limit 200) and apply filters client-side via react-query `select`, with a stable queryKey so filter toggles are instant and don't re-request. True server-side filter needs a backend change (follow-up). 4. Novice-friendly labels: - Inbox subtitle: "99 firing · 100 total" → "99 need attention · 100 total in inbox" - All alerts filter: Open/Firing/Acked/All → "Currently open"/"Firing now"/"Acknowledged"/"All states" - All alerts subtitle: "N shown" → "N matching your filter" - History subtitle: "N resolved" → "N resolved alert(s) in range" - Rules subtitle: "N total" → "N rule(s) configured" - Silences subtitle: "N active" → "N active silence(s)" or "Nothing silenced right now" - Column headers: "State" → "Status", rules "Kind" → "Type", rules "Targets" → "Notifies" - Buttons: "Ack" → "Acknowledge", silence "End" → "End early" Updated alerts.test.tsx and e2e selector to match new behavior/labels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Backend: `GET /environments/{envSlug}/alerts` now accepts optional multi-value `state=…` and `severity=…` query params. Filters are pushed down to PostgresAlertInstanceRepository, which appends `AND state::text = ANY(?)` / `AND severity::text = ANY(?)` to the inbox query (null/empty = no filter). `AlertInstanceRepository.listForInbox` gained a 7-arg overload; the old 5-arg form is preserved as a default delegate so existing callers (evaluator, AlertingFullLifecycleIT, PostgresAlertInstanceRepositoryIT) compile unchanged. `InAppInboxQuery.listInbox` also has a new filtered overload. UI: InboxPage severity filter migrated from `SegmentedTabs` (single-select, no color cues) to `ButtonGroup` (multi-select with severity-coloured dots), matching the topnavbar status-filter pattern. `useAlerts` forwards the filters as query params and cache-keys on the filter tuple so each combo is independently cached. Unit + hook tests updated to the new contract (5 UI tests + 8 Java unit tests passing). OpenAPI types regenerated from the fresh local backend.- GET /alerts gains tri-state acked + read query params - new endpoints: DELETE /{id} (soft-delete), POST /bulk-delete, POST /bulk-ack, POST /{id}/restore - requireLiveInstance 404s on soft-deleted rows; restore() reads the row regardless - BulkReadRequest → BulkIdsRequest (shared body for bulk read/ack/delete) - AlertDto gains readAt; deletedAt stays off the wire - InAppInboxQuery.listInbox threads acked/read through to the repo (7-arg, no more null placeholders) - SecurityConfig: new matchers for bulk-ack (VIEWER+), DELETE/bulk-delete/restore (OPERATOR+) - AlertControllerIT: persistence assertions on /read + /bulk-read; full coverage for new endpoints - InAppInboxQueryTest: updated to 7-arg listInbox signature Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>New endpoints visible to the SPA: DELETE /alerts/{id}, POST /alerts/{id}/restore, POST /alerts/bulk-delete, POST /alerts/bulk-ack. GET /alerts gains tri-state acked / read query params. AlertDto now includes readAt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>