Three failing tests pinning Task 4.3's mode-toggle state hygiene:
- clears threshold+windowSeconds on COUNT_IN_WINDOW -> PER_EXCHANGE
- returns to defaults (not stale values) on PER_EXCHANGE -> COUNT_IN_WINDOW
- forces reNotifyMinutes=0 and forDurationSeconds=0 on PER_EXCHANGE
Targets a to-be-introduced pure helper `applyFireModeChange(form, newMode)`
in form-state.ts. Task 4.3 will implement the helper and wire it into
ExchangeMatchForm so the Fire-mode <Select> calls it instead of the current
raw patch({ fireMode }) that leaves stale fields.
Follows backend removal of the field (Task 3.1). Typechecker confirms
zero remaining references. The ExchangeMatchForm linger-input is
visually removed in Task 4.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: the Silences page rendered an always-visible 4-field form strip
above the list, taking room even when the environment had zero silences.
Inconsistent with Rules, which puts a "New rule" action in the page
header and reserves the content area for either the list or an empty
state.
After: header mirrors Rules — title + subtitle on the left, a "New
silence" primary button on the right. The create form moved into a
Modal opened by that button (and by the empty-state's "Create silence"
action). `?ruleId=` deep links still work: the param is read on mount,
prefills the Rule ID field, and auto-opens the modal — preserving the
InboxPage "Silence rule… → Custom…" flow.
Dropped: unused `sectionStyles` import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the ExecutionController removal (0f635576), SearchIndexer
subscribed to ExecutionUpdatedEvent but nothing publishes that event.
Every SearchIndexerStats metric returned always-zero, and the admin
/api/v1/admin/clickhouse/pipeline endpoint that surfaced those stats
carried no signal.
Backend removed:
- core: SearchIndexer, SearchIndexerStats, ExecutionUpdatedEvent
- app: IndexerPipelineResponse DTO, /pipeline endpoint on
ClickHouseAdminController (field + ctor param)
- StorageBeanConfig.searchIndexer bean
UI removed:
- IndexerPipeline type + useIndexerPipeline hook in
api/queries/admin/clickhouse.ts
- Indexer Pipeline card in ClickHouseAdminPage.tsx (plus ProgressBar
import and pipeline* CSS classes)
OpenAPI schema.d.ts + openapi.json regenerated (stale /pipeline path
and IndexerPipelineResponse schema removed).
SearchIndex interface + ClickHouseSearchIndex impl kept — those are
live and used by SearchService + ExchangeMatchEvaluator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the previous describeApiError rollout to the rest of the UI.
Two symptom classes covered:
- Bare e.message / err.message in toast descriptions would render
"undefined" on Spring error bodies (plain objects without a proper
Error prototype). Affected: OidcConfigPage (save/test/delete),
ClaimMappingRulesModal (save + test), AgentHealth (dismiss),
RouteControlBar (route action + replay).
- Inline {String(error)} on load-failure banners would render
"[object Object]". Affected: InboxPage, RulesListPage, SilencesPage,
OutboundConnectionsPage.
Not touched: auth-store, AppsTab, UsersTab — they already guard with
`e instanceof Error` and fall back to a static string; replacing the
fallback with describeApiError would be a behavioral change best
evaluated separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend
- V18 migration adds AGENT_LIFECYCLE to condition_kind_enum. Java
ConditionKind enum shipped with this value but no Postgres migration
extended the type, so any AGENT_LIFECYCLE rule insert failed with
"invalid input value for enum condition_kind_enum".
- ALTER TYPE ... ADD VALUE lives alone in its migration per Postgres
constraint that the new value cannot be referenced in the same tx.
- V18MigrationIT asserts the enum now contains all 7 kinds.
Frontend
- Add describeApiError(e) helper to unwrap openapi-fetch error bodies
(Spring error JSON) into readable strings. String(e) on a plain
object rendered "[object Object]" in toasts — the actual failure
reason was hidden from the user.
- Replace String(e) in all 13 toast descriptions across the alerting
and outbound-connection mutation paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Used by InboxPage's 'Silence rule… → Custom…' flow to carry the alert's
ruleId into the silence creation form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- STATE_ITEMS gains color dots (text-muted/error/success) to match SEVERITY_ITEMS
- onDeleteOne removes the deleted id from the selection Set so a follow-up bulk
action doesn't try to re-delete a tombstoned row
- drop stale comment block that described an alternative SilenceRulesForSelection
implementation not matching the shipped code
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the old FIRING+ACK hardcoded inbox with the single filterable
inbox:
- Filter bar: Severity · Status (PENDING/FIRING/RESOLVED, default FIRING) ·
Hide acked (default on) · Hide read (default on).
- Row actions: Ack, Mark read, Silence rule… (quick menu), Delete
(OPERATOR+, soft delete with undo toast wired to useRestoreAlert).
- Bulk toolbar: Ack N · Mark N read · Silence rules · Delete N
(ConfirmDialog; OPERATOR+).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Used by InboxPage row + bulk actions to silence an alert's underlying
rule for a chosen preset window. 'Custom…' routes to
/alerts/silences?ruleId=<id> (T13 adds the prefill wire).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sidebar Alerts section now just: Inbox · Rules · Silences. The /alerts
redirect still lands in /alerts/inbox; /alerts/all and /alerts/history
routes are gone (no redirect — stale URLs 404 per clean-break policy).
Also updates sidebar-utils.test.ts to match the new 3-entry shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allows alert rules to fire on agent-lifecycle events — REGISTERED,
RE_REGISTERED, DEREGISTERED, WENT_STALE, WENT_DEAD, RECOVERED — rather
than only on current state. Each matching `(agent, eventType, timestamp)`
becomes its own ackable AlertInstance, so outages on distinct agents are
independently routable.
Core:
- New `ConditionKind.AGENT_LIFECYCLE` + `AgentLifecycleCondition` record
(scope, eventTypes, withinSeconds). Compact ctor rejects empty
eventTypes and withinSeconds<1.
- Strict allowlist enum `AgentLifecycleEventType` (six entries matching
the server-emitted types in `AgentRegistrationController` and
`AgentLifecycleMonitor`). Custom agent-emitted event types tracked in
backlog issue #145.
- `AgentEventRepository.findInWindow(env, appSlug, agentId, eventTypes,
from, to, limit)` — new read path ordered `(timestamp ASC, insert_id
ASC)` used by the evaluator. Implemented on
`ClickHouseAgentEventRepository` with tenant + env filter mandatory.
App:
- `AgentLifecycleEvaluator` queries events in the last `withinSeconds`
window and returns `EvalResult.Batch` with one `Firing` per row.
Every Firing carries a canonical `_subjectFingerprint` of
`"<agentId>:<eventType>:<tsMillis>"` in context plus `agent` / `event`
subtrees for Mustache templating.
- `NotificationContextBuilder` gains an `AGENT_LIFECYCLE` branch that
exposes `{{agent.id}}`, `{{agent.app}}`, `{{event.type}}`,
`{{event.timestamp}}`, `{{event.detail}}`.
- Validation is delegated to the record compact ctor + enum at Jackson
deserialization time — matches the existing policy of keeping
controller validators focused on env-scoped / SQL-injection concerns.
Schema:
- V16 migration generalises the V15 per-exchange discriminator on
`alert_instances_open_rule_uq` to prefer `_subjectFingerprint` with a
fallback to the legacy `exchange.id` expression. Scalar kinds still
resolve to `''` and keep one-open-per-rule. Duplicate-key path in
`PostgresAlertInstanceRepository.save` is unchanged — the index is
the deduper.
UI:
- New `AgentLifecycleForm.tsx` wizard form with multi-select chips for
the six allowed event types + `withinSeconds` input. Wired into
`ConditionStep`, `form-state` (validation + defaults: WENT_DEAD,
300 s), and `enums.ts` options. Tests in `enums.test.ts` pin the
new option array.
- `alert-variables.ts` registers `{{agent.app}}`, `{{event.type}}`,
`{{event.timestamp}}`, `{{event.detail}}` leaves for the new kind,
and extends `agent.id`'s availability list to include `AGENT_LIFECYCLE`.
Tests (all passing):
- 5 new JSON-roundtrip cases on `AlertConditionJsonTest` (positive +
empty/zero/unknown-type rejection).
- 5 new evaluator unit tests on `AgentLifecycleEvaluatorTest` (empty
window, multi-agent fingerprint shape, scope forwarding, missing env).
- `NotificationContextBuilderTest` switch now covers the new kind.
- 119 alerting unit tests + 71 UI tests green.
Docs: `.claude/rules/{core,app,ui}` and CLAUDE.md migration list updated.
Inbox: replace 4 parallel outlined buttons with 2 context-aware ones.
When nothing is selected → "Acknowledge all firing" (primary) + "Mark all
read" (secondary). When rows are selected → the same slots become
"Acknowledge N" + "Mark N read" with counts inlined. Primary variant
gives the foreground action proper visual weight; secondary is the
supporting action. No more visually-identical disabled buttons cluttering
the bar.
History: drop the local DateRangePicker. The page now reads
`timeRange` from `useGlobalFilters()` so the top-bar TimeRangeDropdown
(1h / 3h / 6h / Today / 24h / 7d / custom) is the single source of
truth, consistent with every other time-scoped page in the app.
Replace the SegmentedTabs with multi-select ButtonGroup, matching the
topnavbar Completed/Warning/Failed/Running pattern. State dots use the
same palette as AlertStateChip (FIRING=error, ACKNOWLEDGED=warning,
PENDING=muted, RESOLVED=success). Default selection is the three "open"
states — Resolved is off by default and a single click surfaces closed
alerts without navigating to /history.
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.
Round 4 smoke feedback on /alerts:
- Bell now has consistent 12px gap from env selector and user name
(wrap env + bell in flex container inside TopBar's environment prop)
- RuleEditorWizard constrained to max-width 840px (centered) and
upgraded the page title from SectionHeader to h2 pattern used by
the list pages
- Inbox: added select-all checkbox, severity SegmentedTabs filter
(All / Critical / Warning / Info), and bulk-ack actions
(Acknowledge selected + Acknowledge all firing) alongside the
existing mark-read actions
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>
Visual regressions surfaced during browser smoke:
1. Page headers — `SectionHeader` renders as 12px uppercase gray (a
section divider, not a page title). Replace with proper h2 title
+ inline subtitle (`N firing · N total` etc.) and right-aligned
actions, styled from `alerts-page.module.css`.
2. Undefined `--space-*` tokens — the project (and `@cameleer/design-system`)
has never shipped `--space-sm|md|lg|xl`, even though many modules
(SensitiveKeysPage, alerts CSS, …) reference them. The fallback
to `initial` silently collapsed gaps/paddings to 0. Define the
scale in `ui/src/index.css` so every consumer picks it up.
3. List scrolling — DataTable was using default pagination, but with
no flex sizing the whole page scrolled. Add `fillHeight` and raise
`pageSize`/list `limit` to 200 so the table gets sticky header +
internal scroll + pinned pagination footer (Gmail-style). True
cursor-based infinite scroll needs a backend change (filed as
follow-up — `/alerts` only accepts `limit` today).
4. Title column clipping — `.titlePreview` used `white-space: nowrap`
+ fixed `max-width`, truncating message mid-UUID. Switch to a
2-line `-webkit-line-clamp` so full context is visible.
5. Notification bell badge invisible — `NotificationBell.module.css`
referenced undefined tokens (`--fg`, `--hover-bg`, `--bg`,
`--muted`). Map to real DS tokens (`--text-primary`, `--bg-hover`,
`#fff`, `--text-muted`). The admin user currently sees no badge
because the backend `/alerts/unread-count` returns 0 (read
receipts) — that's data, not UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two inline-style color refs in NotifyStep and TriggerStep were still
pointing at the undefined --muted token instead of the DS
--text-muted. Caught by the design-system-alignment verification
grep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote banner and prefill warnings now render as DS Alert components
(info / warning variants). Step body wraps in sectionStyles.section
for card affordance matching other forms in the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The feed-row component is replaced by DataTable column renderers and
the shared renderAlertExpanded content renderer. No callers remain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces raw <table> with DataTable, inline-styled form with proper
FormField hints, and native confirm() end-early with ConfirmDialog
(warning variant). Adds DS EmptyState for no-silences case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces raw <table> with DataTable, raw <select> promote control with
DS Dropdown, and native confirm() delete with ConfirmDialog. Adds DS
EmptyState with CTA for the no-rules case. Uses SectionHeader's
action slot instead of ad-hoc flex wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces custom feed rows with DataTable. Adds a DateRangePicker
filter (client-side) defaulting to the last 7 days. Client-side
range filter is a stopgap; a server-side range param is a future
enhancement captured in the design spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces 4-Button filter row with DS SegmentedTabs and custom row
rendering with DataTable. Shares expandedContent renderer and
severity-driven rowAccent with Inbox.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces custom feed-row layout with the shared DataTable shell used
elsewhere in the app. Adds checkbox selection + bulk "Mark selected
read" toolbar alongside the existing "Mark all read". Uses DS
EmptyState for empty lists, severity-driven rowAccent for unread
tinting, and renderAlertExpanded for row detail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the feed-row classes (.row, .rowUnread, .body, .meta, .time,
.message, .actions, .empty) — these are replaced by DS DataTable +
EmptyState in follow-up tasks. Keep layout helpers for page shell,
toolbar, filter bar, bulk-action bar, title cell, and DataTable
expanded content. All colors / spacing use DS tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the per-row detail block used by Inbox/All/History DataTables
so the three pages share one rendering. Consumes AlertDto fields that
are nullable in the schema; hides missing fields instead of rendering
placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Formats ISO timestamps as `Nm ago` / `Nh ago` / `Nd ago`, falling back
to an absolute locale date string for values older than 30 days. Used
by the alert DataTable Age column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure function mapping the 3-value AlertDto.severity enum to the 2-value
DataTable rowAccent prop. INFO maps to undefined (no tint) because the
DS DataTable rowAccent only supports error|warning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rule editor wizard reset the condition payload on kind-change without
seeding a fireMode default; the ExchangeMatchCondition ctor allowed null to
pass through; AlertEvaluatorJob then NPE-looped every tick on a saved rule.
- core: compact ctor now rejects null fireMode (Jackson-deser path only — all
production callers already pass a value).
- V14: repair existing EXCHANGE_MATCH rows with fireMode=null to
PER_EXCHANGE + perExchangeLingerSeconds=300 (default matches the wizard).
- ui: ConditionStep.onKindChange seeds EXCHANGE_MATCH defaults so the
Select's displayed fallback ("Per exchange") is actually in form state.
- ui: validateStep('condition', ...) now enforces fireMode presence + the
mode-specific fields before the user reaches Review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 83837ada addressing the critical-review feedback:
- Duplicate ConditionKind type consolidated: the one in
api/queries/alertRules.ts (which was nullable — wrong) is gone;
single source of truth lives in this module.
- Module moved out of api/ into pages/Alerts/ where it belongs.
api/ is the data layer; labels + hide lists are view-layer concerns.
- Hidden values formalised: Comparator.EQ and JvmAggregation.LATEST
are intentionally not surfaced in dropdowns (noisy / wrong feature
boundary, see in-file comments). They remain in the type unions so
rules that carry those values save/load correctly — we just don't
advertise them in the UI.
- JvmAggregation declaration order restored to MAX/AVG/MIN (matches
what users saw before 83837ada). LATEST declared last; hidden.
- Snapshot tests for every visible *_OPTIONS array — reviewer signal
in future PRs when a backend enum change or hide-list edit
silently reshapes the dropdown.
- `toOptions` gains a JSDoc noting that label-map declaration order
is load-bearing (ES2015 Object.keys insertion-order guarantee).
- **Honest about the springdoc schema quirk**: the generated
polymorphic condition types resolve to `never` at the TypeScript
level (two conflicting `kind` discriminators — the class-name
literal and the Jackson enum — intersect to never), which silently
defeated `Record<T, string>` exhaustiveness. The previous commit's
"schema-derived enums" claim was accurate only for the flat-field
enums (ConditionKind, Severity, TargetKind); condition-specific
enums (RouteMetric, Comparator, JvmAggregation, ExchangeFireMode)
were silently `never`. Those are now declared as hand-written
string-literal unions with a top-of-file comment spelling out the
issue and the regen-and-compare workflow. Real upstream fix is a
backend-side adjustment to how springdoc emits polymorphic
`@JsonSubTypes` — out of scope for this phase.
Verified: ui build green, 56/56 vitest pass (49 pre-existing + 7
new enum snapshots).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes item 5 on the Plan 03 cleanup triage. The option arrays
("METRICS", "COMPARATORS", KIND_OPTIONS, SEVERITY_OPTIONS, FIRE_MODES)
scattered across RouteMetricForm / JvmMetricForm / ExchangeMatchForm /
ConditionStep / ScopeStep were hand-typed string literals. They drifted
silently — P95_LATENCY_MS appeared in a dropdown without a backend
counterpart (caught at runtime in bcde6678); JvmMetric.LATEST and
Comparator.EQ existed on the backend but were missing from the UI all
along.
Fix: new `ui/src/api/alerting-enums.ts` derives every enum from
schema.d.ts and pairs each with a `Record<T, string>` label map.
TypeScript enforces exhaustiveness — adding or removing a backend
value fails the build of this file until the label map is updated.
Every consumer imports the generated `*_OPTIONS` array.
Covered (schema-derived):
- ConditionKind → CONDITION_KIND_OPTIONS
- Severity → SEVERITY_OPTIONS
- RouteMetric → ROUTE_METRIC_OPTIONS
- Comparator → COMPARATOR_OPTIONS (adds EQ that was missing)
- JvmAggregation → JVM_AGGREGATION_OPTIONS (adds LATEST that was missing)
- ExchangeMatch.fireMode → EXCHANGE_FIRE_MODE_OPTIONS
- AlertRuleTarget.kind → TARGET_KIND_OPTIONS
form-state.ts: `severity: 'CRITICAL' | 'WARNING' | 'INFO'` and
`kind: 'USER' | 'GROUP' | 'ROLE'` literal unions swapped for the
derived `Severity` / `TargetKind` aliases.
Not covered, backend types them as `String` (no `@Schema(allowableValues)`
annotation yet):
- AgentStateCondition.state
- DeploymentStateCondition.states
- LogPatternCondition.level
- ExchangeFilter.status
- JvmMetricCondition.metric
These stay hand-typed with a pointer-comment. Follow-up: add
`@Schema(allowableValues = …)` to the Java record components so the
enums land in schema.d.ts; then fold them into alerting-enums.ts.
Plus: gitnexus index-stats refresh in AGENTS.md/CLAUDE.md from the
post-deploy reindex.
Verified: ui build green, 49/49 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RouteMetricForm dropped P95_LATENCY_MS — not in cameleer-server-core
RouteMetric enum (valid: ERROR_RATE, P99_LATENCY_MS, AVG_DURATION_MS,
THROUGHPUT, ERROR_COUNT).
- initialForm now returns a ready-to-save ROUTE_METRIC condition
(metric=ERROR_RATE, comparator=GT, threshold=0.05, windowSeconds=300),
so clicking through the wizard with all defaults produces a valid rule.
Prevents a 400 'missing type id property kind' + 400 on condition enum
validation if the user leaves the condition step untouched.
Fetches target-env apps (useCatalog) and env-allowed outbound
connections, passes them to prefillFromPromotion, and renders the
returned warnings in an amber banner above the step nav. Warnings list
the field name and the remediation message so users see crossings that
need manual adjustment before saving.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review step dumps a human summary plus raw request JSON, and (when a
setter is supplied) offers an Enabled-on-save Toggle. Promotion prefill
now returns {form, warnings}: clears agent IDs (per-env), flags missing
apps in target env, and flags webhook connections not allowed in target
env. 4 Vitest cases cover copy-name, agent clear, app-missing, and
webhook-not-allowed paths.
The wizard now consumes {form, warnings}; Task 25 renders the warnings
banner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Title and message use MustacheEditor with kind-specific autocomplete.
Preview button posts to the render-preview endpoint and shows rendered
title/message inline. Targets combine users/groups/roles into a unified
Badge pill list. Webhook picker filters to outbound connections allowed
in the current env (spec 6, allowed_environment_ids). Header overrides
use plain Input rather than MustacheEditor for now.
Deviations:
- RenderPreviewRequest is Record<string, never>, so we send {} instead
of {titleTemplate, messageTemplate}; backend resolves from rule state.
- RenderPreviewResponse has {title, message} (plan draft used
renderedTitle/renderedMessage).
- Button size="sm" not "small" (DS only accepts sm|md).
- Target kind field renamed from targetKind to kind to match
AlertRuleTarget DTO.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three numeric inputs for evaluation cadence, for-duration, and
re-notification window, plus a Test evaluate button for saved rules.
TestEvaluateRequest is empty on the wire (server uses the rule id), so
we send {} and rely on the backend to evaluate the current saved state.
Deviation: plan draft passed {condition: toRequest(form).condition} into
the request body. The generated TestEvaluateRequest type is
Record<string, never>, so we send an empty body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each condition kind (ROUTE_METRIC, EXCHANGE_MATCH, AGENT_STATE,
DEPLOYMENT_STATE, LOG_PATTERN, JVM_METRIC) renders its own payload-shape
form. Changing the kind resets the condition payload to {kind, scope} so
stale fields from a previous kind don't leak into the save request.
Deviation: DS Select uses native event-based onChange. Plan draft showed
a value-based signature (onChange(v) => ...).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Name, description, severity, scope-kind radio, and cascading app/route/
agent selectors driven by catalog + agents data. Adjusts condition
routing by clearing routeId/agentId when the app changes.
Deviation: DS Select uses native event-based onChange; plan draft had
a value-based signature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wizard navigates 5 steps (scope/condition/trigger/notify/review) with
per-step validation. form-state module is the single source of truth for
the rule form; initialForm/toRequest/validateStep are unit-tested (6
tests). Step components are stubbed and will be implemented in Tasks
20-24. prefillFromPromotion is a thin wrapper in this commit; Task 24
rewrites it to compute scope-adjustment warnings.
Deviation notes:
- FormState.targets uses {kind, targetId} to match AlertRuleTarget DTO
field names (plan draft had targetKind).
- toRequest casts through Record<string, unknown> so the spread over
the Partial<AlertCondition> union typechecks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotion dropdown builds a /alerts/rules/new URL with promoteFrom, ruleId,
and targetEnv query params — the wizard will read these in Task 24 and
pre-fill the form with source-env prefill + client-side warnings.
AlertRow is reused by AllAlertsPage and HistoryPage. Marking a row as read
happens when its link is followed (the detail sub-route will be added in
phase 10 polish). FIRING rows get an amber left border.
Adds 6 lazy-loaded route entries for the alerting UI (Inbox, All, History,
Rules list, Rule editor wizard, Silences) plus an `/alerts` → `/alerts/inbox`
redirect. Page components are placeholder stubs to be replaced in Phase 5/6/7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds OutboundConnectionsPage (list view with delete), lazy route at
/admin/outbound-connections, and Outbound Connections nav node in the
admin sidebar tree. No test file created — UI codebase has no existing
test infrastructure to build on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>