Commit Graph

652 Commits

Author SHA1 Message Date
hsiegeln
1b6e6ce40c refactor(alerts/ui): replace undefined CSS vars in wizard.module.css
Replace undefined tokens (--muted, --fg, --accent, --border,
--amber-bg) with DS tokens (--text-muted, --text-primary, --amber,
--border-subtle, --space-sm|md). Drop .promoteBanner — replaced by
DS Alert in follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:16:47 +02:00
hsiegeln
0037309e4f chore(alerts/ui): remove obsolete AlertRow.tsx
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>
2026-04-21 10:15:24 +02:00
hsiegeln
3e81572477 refactor(alerts/ui): rewrite Silences with DataTable + FormField + ConfirmDialog
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>
2026-04-21 10:14:19 +02:00
hsiegeln
23f3c3990c refactor(alerts/ui): rewrite Rules list with DataTable + Dropdown + ConfirmDialog
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>
2026-04-21 10:12:25 +02:00
hsiegeln
436a0e4d4c refactor(alerts/ui): rewrite History as DataTable + DateRangePicker
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>
2026-04-21 10:10:10 +02:00
hsiegeln
a74785f64d refactor(alerts/ui): rewrite All alerts as DataTable + SegmentedTabs filter
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>
2026-04-21 10:07:38 +02:00
hsiegeln
588e0b723a refactor(alerts/ui): rewrite Inbox as DataTable with expandable rows
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>
2026-04-21 10:05:39 +02:00
hsiegeln
c87c77c1cf refactor(alerts/ui): slim alerts-page.module.css to layout-only DS tokens
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>
2026-04-21 10:03:20 +02:00
hsiegeln
b16ea8b185 feat(alerts/ui): add shared renderAlertExpanded for DataTable rows
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>
2026-04-21 10:01:39 +02:00
hsiegeln
4a63149338 feat(alerts/ui): add formatRelativeTime helper
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>
2026-04-21 10:00:15 +02:00
hsiegeln
a2b2ccbab7 feat(alerts/ui): add severityToAccent helper for DataTable rowAccent
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>
2026-04-21 09:57:58 +02:00
hsiegeln
efa8390108 fix(alerting): reject null fireMode on ExchangeMatchCondition + repair in-flight rows
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m2s
CI / docker (push) Successful in 1m20s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
SonarQube / sonarqube (push) Successful in 5m31s
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>
2026-04-20 20:05:55 +02:00
hsiegeln
e590682f8f refactor(ui/alerts): address code-review findings on alerting-enums
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m3s
CI / docker (push) Successful in 1m22s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
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>
2026-04-20 19:26:16 +02:00
hsiegeln
83837ada8f refactor(ui/alerts): derive option lists + form-state types from schema.d.ts
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m8s
CI / docker (push) Successful in 1m15s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
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>
2026-04-20 19:02:52 +02:00
hsiegeln
09b49f096c feat(alerting): per-severity breakdown on unread-count DTO
Spec §13 calls for the notification bell to colour-code by highest
unread severity (CRITICAL → error, WARNING → amber, INFO → muted).
The old { count } DTO forced the UI to pick one static colour, so
NotificationBell shipped with a TODO. Grow the contract instead:

  UnreadCountResponse = { total, bySeverity: { CRITICAL, WARNING, INFO } }

Guarantees:
- every severity is always present with a >=0 value (no undefined
  keys on the wire), so the UI can branch without defaults.
- total = sum of bySeverity values — kept explicit on the wire for
  cheap top-line display, not recomputed client-side.

Backend
- AlertInstanceRepository: replaces countUnreadForUser(long) with
  countUnreadBySeverityForUser returning Map<AlertSeverity, Long>.
  One SQL round-trip per (env, user) — GROUP BY ai.severity over the
  same NOT EXISTS(alert_reads) filter.
- UnreadCountResponse.from(Map) normalises and defensively copies;
  missing severities default to 0.
- InAppInboxQuery.countUnread now returns the DTO, caches the full
  response (still 5s TTL) so severity breakdown gets the same
  hit-rate as the total did before.
- AlertController just hands the DTO back.

Breaking change — no backwards-compat shim: the `count` field is
gone. UI and tests updated in the same commit; there are no other
API consumers in the tree.

Frontend
- Regenerated openapi.json + schema.d.ts against a fresh build of
  the new backend.
- NotificationBell branches badge colour on the highest unread
  severity (CRITICAL > WARNING > INFO) via new CSS variants.
- Tests cover all four paths: zero, critical-present, warning-only,
  info-only.

Tests: 7 unit tests + 12 ITs (incl. new grouping + empty-map)
       + 49 vitest (was 46; +3 severity-branch assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:15:56 +02:00
hsiegeln
579b5f1a04 chore(ui): delete unused usePageVisible hook
Added as a reusable primitive during Plan 03 Task 9, but the intended
consumer (NotificationBell live-region refresh) was removed during
code review, leaving the hook unused. Delete it — YAGNI; reintroduce
when a real consumer shows up.

Verified upstream impact (gitnexus): 0 callers, LOW risk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:02:04 +02:00
hsiegeln
1ebc2fa71e test(ui/alerts): Playwright E2E smoke (sidebar, rule CRUD, CMD-K, silence CRUD)
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m10s
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 2m34s
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
CI / docker (push) Successful in 5m11s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 40s
fixtures.ts: auto-applied login fixture — visits /login?local to skip OIDC
auto-redirect, fills username/password via label-matcher, clicks 'Sign in',
then selects the 'default' env so alerting hooks enable (useSelectedEnv gate).
Override via E2E_ADMIN_USER + E2E_ADMIN_PASS.

alerting.spec.ts: 4 tests against the full docker-compose stack:
 - sidebar Alerts accordion → /alerts/inbox
 - 5-step wizard: defaults-only create + row delete (unique timestamp name
   avoids strict-mode collisions with leftover rules)
 - CMD-K palette via SearchTrigger click (deterministic; Ctrl+K via keyboard
   is flaky when the canvas doesn't have focus)
 - silence matcher-based create + end-early

DS FormField renders labels as generics (not htmlFor-wired), so inputs are
targeted by placeholder or label-proximity locators instead of getByLabel.

Does not exercise fire→ack→clear; that's covered backend-side by
AlertingFullLifecycleIT (Plan 02). UI E2E for that path would need event
injection into ClickHouse, out of scope for this smoke.
2026-04-20 16:18:17 +02:00
hsiegeln
bcde6678b8 fix(ui/alerts): align RouteMetric metric enum with backend; pre-populate ROUTE_METRIC defaults
- 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.
2026-04-20 16:17:59 +02:00
hsiegeln
1ed2d3a611 chore(docker): full-stack docker-compose mirroring deploy/ k8s manifests
Mirrors the k8s manifests in deploy/ as a local dev stack:
  - cameleer-postgres   (matches deploy/cameleer-postgres.yaml)
  - cameleer-clickhouse (matches deploy/cameleer-clickhouse.yaml, default CLICKHOUSE_DB=cameleer)
  - cameleer-server     (built from Dockerfile, env mirrors deploy/base/server.yaml)
  - cameleer-ui         (built from ui/Dockerfile, served on host :8080 to leave :5173 free for Vite dev)

Dockerfile + ui/Dockerfile: REGISTRY_TOKEN is now optional (empty → skip Maven/npm auth).
cameleer-common package is public, so anonymous pulls succeed; private packages still require the token.

Backend defaults tuned for local E2E:
  - RUNTIME_ENABLED=false (no Docker-in-Docker deployments in dev stack)
  - OUTBOUND_HTTP_ALLOW_PRIVATE_TARGETS=true (so webhook tests can target host.docker.internal etc.)
  - UIUSER/UIPASSWORD=admin/admin (matches Playwright E2E_ADMIN_USER/PASS defaults)
  - CORS includes both :5173 (Vite) and :8080 (nginx)
2026-04-20 15:52:24 +02:00
hsiegeln
f4c2cb120b feat(ui/alerts): CMD-K sources for alerts + alert rules
Extends operationalSearchData with open alerts (FIRING|ACKNOWLEDGED) and
all rules. Badges convey severity + state. Selecting an alert navigates to
/alerts/inbox/{id}; a rule navigates to /alerts/rules/{id}. Uses the
existing CommandPalette extension point — no new registry.
2026-04-20 14:09:39 +02:00
hsiegeln
8689643e11 feat(ui/alerts): SilencesPage with matcher-based create + end-early action
Matcher accepts ruleId and/or appSlug. Server enforces endsAt > startsAt
(V12 CHECK constraint) and matcher_matches() at dispatch time (spec §7).
2026-04-20 14:08:27 +02:00
hsiegeln
0191ca4b13 feat(ui/alerts): render promotion warnings in wizard banner
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>
2026-04-20 14:05:08 +02:00
hsiegeln
3963ea5591 feat(ui/alerts): ReviewStep + promotion prefill warnings
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>
2026-04-20 14:04:04 +02:00
hsiegeln
816096f4d1 feat(ui/alerts): NotifyStep (MustacheEditor for title/message/body, targets, webhook bindings)
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>
2026-04-20 14:02:26 +02:00
hsiegeln
d42a6ca6a8 feat(ui/alerts): TriggerStep (evaluation interval, for-duration, re-notify, test-evaluate)
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>
2026-04-20 14:00:33 +02:00
hsiegeln
ef8c60c2b5 feat(ui/alerts): ConditionStep with 6 kind-specific forms
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>
2026-04-20 13:59:55 +02:00
hsiegeln
f48fc750f2 feat(ui/alerts): ScopeStep (name, severity, env/app/route/agent selectors)
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>
2026-04-20 13:58:21 +02:00
hsiegeln
334e815c25 feat(ui/alerts): rule editor wizard shell + form-state module
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>
2026-04-20 13:57:30 +02:00
hsiegeln
7e91459cd6 feat(ui/alerts): RulesListPage with enable/disable, delete, env promotion
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.
2026-04-20 13:52:14 +02:00
hsiegeln
269a63af1f feat(ui/alerts): AllAlertsPage + HistoryPage
AllAlertsPage: state filter chips (Open/Firing/Acked/All).
HistoryPage: RESOLVED filter, respects retention window.
2026-04-20 13:49:52 +02:00
hsiegeln
8d8bae4e18 feat(ui/alerts): InboxPage with ack + bulk-read actions
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.
2026-04-20 13:49:23 +02:00
hsiegeln
891dcaef32 feat(ui/alerts): mount NotificationBell in TopBar
Renders the `<NotificationBell />` as the first child of `<TopBar>` (before
`<SearchTrigger>`). The bell links to `/alerts/inbox` and shows the unread
alert count for the currently selected environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:47:16 +02:00
hsiegeln
54e4217e21 feat(ui/alerts): Alerts sidebar section with Inbox/All/Rules/Silences/History
Adds `buildAlertsTreeNodes` to sidebar-utils and renders an Alerts section
between Applications and Starred in LayoutShell. The section uses an
accordion pattern — entering `/alerts/*` collapses apps/admin/starred and
restores their state on leave.

gitnexus_impact(LayoutContent, upstream) = LOW (0 direct callers; rendered
only by LayoutShell's provider wrapper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:46:49 +02:00
hsiegeln
167d0ebd42 feat(ui/alerts): register /alerts/* routes with placeholder pages
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>
2026-04-20 13:44:44 +02:00
hsiegeln
019e79a362 feat(ui/alerts): MustacheEditor component (CM6 shell with completion + linter)
Wires the mustache-completion source and mustache-linter into a CodeMirror 6
EditorView. Accepts kind (filters variables) and reducedContext (env-only for
connection URLs). singleLine prevents newlines for URL/header fields. Host
ref syncs when the parent replaces value (promotion prefill).
2026-04-20 13:41:46 +02:00
hsiegeln
ac2a943feb feat(ui/alerts): CM6 completion + linter for Mustache templates
completion fires after {{ and narrows as the user types; apply() closes the
tag automatically. Linter raises an error on unclosed {{, a warning on
references that aren't in the allowed-variable set for the current condition
kind. Kind-specific allowed set comes from availableVariables().
2026-04-20 13:39:10 +02:00
hsiegeln
18e6dde67a fix(ui/alerts): align ALERT_VARIABLES registry with NotificationContextBuilder
Plan prose had spec §8 idealized leaves, but the backend NotificationContext
only emits a subset:
  ROUTE_METRIC / EXCHANGE_MATCH → route.id + route.uri (uri added)
  LOG_PATTERN → log.pattern + log.matchCount (renamed from log.logger/level/message)
  app.slug / app.id → scoped to non-env kinds (removed from 'always')
  exchange.link / alert.comparator / alert.window / app.displayName → removed (backend doesn't emit)

Without this alignment the Task 11 linter would (1) flag valid route.uri as
unknown, (2) suggest log.{logger,level,message} as valid paths that render
empty, and (3) flag app.slug on env-wide rules.
2026-04-20 13:35:42 +02:00
hsiegeln
5ddd89f883 feat(ui/alerts): Mustache variable metadata registry for autocomplete
ALERT_VARIABLES mirrors the spec §8 context map. availableVariables(kind)
returns the kind-specific filter (always vars + kind vars). extractReferences
+ unknownReferences drive the inline amber linter. Backend NotificationContext
adds must land here too.
2026-04-20 13:31:55 +02:00
hsiegeln
38083d7c3f fix(ui/alerts): remove dead usePageVisible subscription; align alerts test mock with DTO
NotificationBell used a usePageVisible() subscription that re-rendered on
every visibilitychange without consuming the value. TanStack Query's
refetchIntervalInBackground:false already pauses polling; the extra
subscription was speculative generality. Dropped the import + call + JSDoc
reference; usePageVisible hook + test retained as a reusable primitive.

Also: alerts.test.tsx 'returns the server payload unmodified' asserted a
pre-plan {total, bySeverity} shape, but UnreadCountResponse is actually
{count}. Fixed mock + assertion to {count: 3}.
2026-04-20 13:30:07 +02:00
hsiegeln
197c60126c feat(ui/alerts): NotificationBell with Page Visibility poll pause
Adds a header bell component linking to /alerts/inbox with an unread-count
badge for the selected environment. Polling pauses when the tab is hidden
via TanStack Query's refetchIntervalInBackground:false (already set on
useUnreadCount); the new usePageVisible hook gives components a
re-renders-on-visibility-change signal for future defense-in-depth.

Plan-prose deviation: the plan assumed UnreadCountResponse carries a
bySeverity map for per-severity badge coloring, but the backend DTO only
exposes a scalar `count`. The bell reads `data?.count` and renders a single
var(--error) tint; a TODO references spec §13 for future per-severity work
that would require expanding the DTO.

Tests: usePageVisible toggles on visibilitychange events; NotificationBell
renders the bell with no badge at count=0 and shows "3" at count=3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:26:58 +02:00
hsiegeln
31ee974830 feat(ui/alerts): AlertStateChip + SeverityBadge components
State colors follow the convention from @cameleer/design-system (CRITICAL->error,
WARNING->warning, INFO->auto). Silenced pill stacks next to state for the spec
section 8 audit-trail surface.
2026-04-20 13:21:37 +02:00
hsiegeln
51bc796bec feat(ui/alerts): silence + notification query hooks 2026-04-20 13:16:57 +02:00
hsiegeln
c6c3dd9cfe feat(ui/alerts): alert rule query hooks (CRUD, enable/disable, preview, test-evaluate)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:13:07 +02:00
hsiegeln
82c29f46a5 fix(ui/alerts): bulk-read body uses instanceIds to match BulkReadRequest DTO
Plan 03 prose had 'alertInstanceIds'; backend record is 'instanceIds'.
2026-04-20 13:08:22 +02:00
hsiegeln
83a8912da6 feat(ui/alerts): TanStack Query hooks for /alerts endpoints
Adds env-scoped hooks for the alerts inbox:
- useAlerts (30s poll, background-paused, filter-aware)
- useAlert, useUnreadCount (30s poll)
- useAckAlert, useMarkAlertRead, useBulkReadAlerts (mutations that
  invalidate the alerts query key tree + unread-count)

Test file uses .tsx because the QueryClientProvider wrapper relies on
JSX; vitest picks up both .ts and .tsx via the configured include glob.
Client mock targets the actual export name (`api` in ../client) rather
than the `apiClient` alias that alertMeta re-exports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:07:16 +02:00
hsiegeln
1a8b9eb41b feat(ui/alerts): shared env helper for alerting query hooks 2026-04-20 13:00:49 +02:00
hsiegeln
b2066cdb68 chore(ui): regenerate openapi.json + schema.d.ts from deployed Plan 02 backend
Fetched from http://192.168.50.86:30090/api/v1/api-docs via
`npm run generate-api:live`. Adds TypeScript types for the new alerting
REST surface merged in #140:

- 15 alerting paths under /environments/{envSlug}/alerts/** (rules CRUD,
  enable/disable, render-preview, test-evaluate, inbox, unread-count,
  ack/read/bulk-read, silences CRUD, per-alert notifications)
- 1 flat notification retry path /alerts/notifications/{id}/retry
- 4 outbound-connection admin paths (from Plan 01 #139)

Verified tsc -p tsconfig.app.json --noEmit exits 0 — no existing SPA
call sites break against the fresh types. Plan 03 UI work can consume
these directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:59:38 +02:00
hsiegeln
1260cbe674 chore(ui): add CodeMirror 6 + Playwright config
Install @codemirror/{view,state,autocomplete,commands,language,lint}
and @lezer/common — needed by Phase 3's MustacheEditor (Task 13).
CM6 picked over a raw textarea for its small incremental-rendering
bundle, full ARIA/keyboard support, and pluggable autocomplete +
linter APIs that map cleanly to Mustache token parsing.

Add ui/playwright.config.ts wiring Task 30's E2E smoke:
- testDir ./src/test/e2e, single worker, trace+screenshot on failure
- webServer launches `npm run dev:local` (backend on :8081 required)
- PLAYWRIGHT_BASE_URL env var skips the dev server for CI against a
  pre-deployed UI

Add test:e2e / test:e2e:ui npm scripts and exclude Playwright's
test-results/ and playwright-report/ from git. @playwright/test
itself was already in devDependencies from an earlier task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:55:57 +02:00
hsiegeln
0aa1776b57 chore(ui): add Vitest + Testing Library scaffolding
Prepares for Plan 03 unit tests (MustacheEditor, NotificationBell, wizard step
validation). jsdom environment + jest-dom matchers + canary test verifies the
wiring.
2026-04-20 12:16:22 +02:00
hsiegeln
0c5f1b5740 feat(ui): outbound connection editor — TLS config, test action, env restriction
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:59:19 +02:00