Browse documentation

Frontend Specs

Section 8 — Alert Manager (`/admin/alerts`)

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard.

docs/specs/frontend/admin-spec-03b-alerts.md
On this page

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard. Layout: 3-tab interface: Rules, History, Channels. 1200px max-width. Sidebar: carbon (#12161E) background. Notes: All alert rules have configurable thresholds, severity levels, and notification channels. Critical alerts trigger browser notifications.


8.1 Alert Rules List#

1. Purpose#

CRUD interface for defining platform-wide alert thresholds. Admins create rules like "Queue depth > 100 = warning" or "API failure rate > 15% = critical". Rules are evaluated by a background job every 60 seconds.

2. Visual Layout#

Section A: Active Rules Table (top, 70% width)

  • Table columns: Rule Name | Metric | Condition | Threshold | Severity | Status | Channels | Actions
  • Rule Name: text-base text-primary (e.g., "Queue Depth Warning")
  • Metric: Badge with metric type (e.g., QUEUE_DEPTH, API_FAILURE_RATE, COST_DAILY, SIGNUP_DROP, SYSTEM_DOWN)
  • Condition: Operator badge (>, <, =, >=, <=, CONTAINS, NOT_CONTAINS)
  • Threshold: Number + unit (e.g., 100 jobs, 15%, $50, 5 signups)
  • Severity:
    • INFO: bg-gray-50 + text-gray-700 + blue dot
    • WARNING: bg-amber-50 + text-amber-700 + amber dot
    • CRITICAL: bg-red-50 + text-red-700 + red dot
  • Status: Toggle switch ON/OFF (active rules are ON)
  • Channels: Icon pills — email icon, Slack icon, webhook icon, PagerDuty icon (if configured). Grayed out if not enabled for this rule.
  • Actions: "Edit" (pencil), "Delete" (trash), "Duplicate" (copy icon). 3-dot menu for overflow.

Section B: Rule Stats (top, 30% width)

  • Vertical stack of 4 stat cards:
    • "Total Rules: {N}"
    • "Active: {N} / Inactive: {N}"
    • "Triggered Today: {N}"
    • "Most Triggered: {rule name}"
  • Each card: white background, shadow-sm, 8px radius, text-lg value in text-primary.

Section C: Create Rule Button (below table)

  • Full-width sticky bar at bottom: "+ Create New Rule" button (Ember Orange CTA) → opens modal (see 8.3).
  • Also shows: "Rules are evaluated every 60 seconds. Last evaluation: {time}."

Section D: Rule Detail Panel (slide-in from right)

  • Click row → slide-in panel with full rule config:
    • Rule name, description, metric, condition, threshold, severity
    • Evaluation window (e.g., "Last 5 minutes" for rate-based metrics)
    • Cooldown period (e.g., "Don't re-alert for 30 minutes")
    • Channels with per-channel test buttons
    • Trigger history: last 10 times this rule triggered, with timestamps and resolved status

3. Data Source (tRPC endpoint)#

admin.getAlertRules.useQuery(undefined, { refetchInterval: 60000 }); // 1min standard
admin.createAlertRule.useMutation({ input: AlertRuleInputSchema });
admin.updateAlertRule.useMutation({ id: z.string(), input: AlertRuleInputSchema });
admin.deleteAlertRule.useMutation({ id: z.string() });
admin.toggleAlertRule.useMutation({ id: z.string(), enabled: z.boolean() });
admin.testAlertRule.useMutation({ id: z.string() }); // Simulate trigger

4. Zod Schema#

const AlertMetricSchema = z.enum([
  "QUEUE_DEPTH",
  "API_FAILURE_RATE",
  "API_LATENCY_MS",
  "COST_DAILY",
  "COST_HOURLY",
  "SIGNUP_COUNT",
  "SIGNUP_DROP_PERCENT",
  "CHURN_RATE",
  "ACTIVE_CLIENTS",
  "SYSTEM_DOWN",
  "INTEGRATION_DOWN",
  "CONTENT_APPROVAL_BACKLOG",
  "STORAGE_USAGE_PERCENT",
  "CPU_USAGE_PERCENT",
  "MEMORY_USAGE_PERCENT",
]);

const AlertConditionSchema = z.enum([">", "<", "=", ">=", "<=", "CONTAINS", "NOT_CONTAINS"]);

const AlertSeveritySchema = z.enum(["INFO", "WARNING", "CRITICAL"]);

const AlertChannelSchema = z.enum(["EMAIL", "SLACK", "WEBHOOK", "PAGERDUTY"]);

const AlertRuleSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  description: z.string().max(500).optional(),
  metric: AlertMetricSchema,
  condition: AlertConditionSchema,
  threshold: z.union([z.number(), z.string()]), // number for numeric, string for text
  severity: AlertSeveritySchema,
  enabled: z.boolean().default(true),
  evaluationWindowMinutes: z.number().min(1).max(1440).default(5), // 5min default
  cooldownMinutes: z.number().min(0).max(1440).default(30), // 30min default
  channels: z.array(AlertChannelSchema).min(1), // at least one channel
  createdAt: z.date(),
  updatedAt: z.date(),
  createdBy: z.string(),
  triggerCount24h: z.number().default(0),
  lastTriggeredAt: z.date().optional(),
  lastResolvedAt: z.date().optional(),
});

const AlertRuleInputSchema = AlertRuleSchema.omit({
  id: true, createdAt: true, updatedAt: true, createdBy: true,
  triggerCount24h: true, lastTriggeredAt: true, lastResolvedAt: true,
});

5. Fetch Frequency#

  • Standard (60000ms / 1min): Rules don't change frequently. 1min polling sufficient.
  • On-demand: After any CRUD mutation, invalidate and refetch.
  • SSE: When a rule is triggered (fires), push to /admin/alerts page to update "Triggered Today" count and "Last Triggered" timestamps.

6. Data Manipulations#

  • Threshold formatting:
    • QUEUE_DEPTH{N} jobs
    • API_FAILURE_RATE{N}%
    • COST_DAILY${N}
    • API_LATENCY_MS{N}ms (or {N}s if >= 1000)
    • STORAGE_USAGE_PERCENT{N}%
  • Evaluation window: "Last 5 minutes" → {N} min / {N} hr (e.g., "5 min", "1 hr").
  • Cooldown: "30 min" → "30 min" / "1 hr" / "2 hr".
  • Trigger count sparkline: Next to "Triggered Today" stat, a mini bar chart (7 bars, last 7 days) showing daily trigger counts.
  • Sorting: Default by severity (CRITICAL → WARNING → INFO) then by last triggered (most recent first).
  • Filtering: Filter by metric, severity, status, channel. Search by name.
  • Bulk actions: Select multiple rules → bulk enable/disable/delete (top bar appears with count).

7. Rationale#

  • Table with inline actions: Rules are configuration data, not content. Tables are the right pattern for CRUD. Inline actions (edit, toggle, delete) reduce clicks.
  • Stats sidebar: At-a-glance summary. "Triggered Today" tells admin if the system is noisy. "Most Triggered" identifies poorly tuned rules.
  • Severity color coding: Red = stop what you're doing and fix. Amber = investigate soon. Gray = FYI. Immediate visual prioritization.
  • 1min polling: Rules change when admins edit them. No need for real-time. Trigger events use SSE to push active alert counts.
  • Test button: Before saving a new rule, admin can test it to see if it would trigger given current conditions. Prevents false positive alerts.
  • Cooldown: Prevents alert fatigue. If a queue is at 101 jobs for 3 hours, the admin doesn't get 180 emails. One alert, then silence for 30 minutes.
  • Evaluation window: Rate-based metrics (failure rate) need a window. "5% failure rate" means nothing without a time window. "5% failure rate in the last 5 minutes" is actionable.

8. Interaction Flows#

  • Create rule: Click "+ Create New Rule" → modal opens (see 8.3) → fill form → click "Save" → validation → mutation → toast "Rule created" → row added to table → SSE subscription starts for this rule.
  • Edit rule: Click pencil → slide-in panel with edit form → change threshold → "Save" → toast "Rule updated" → row updates. If threshold was lowered and would now trigger, immediate evaluation + alert sent.
  • Toggle rule: Click toggle → immediate UI change → mutation → if fails, toggle reverts. No confirmation for toggle (safe to re-enable).
  • Delete rule: Click trash → confirmation modal: "Delete '{name}'? This rule will stop evaluating immediately." → "Delete" → toast "Rule deleted" → row removed.
  • Test rule: Click "Test" in detail panel → system simulates current metric against threshold → shows result: "Would trigger: YES/NO" with computed value (e.g., "Current queue depth: 47. Threshold: 100. Would NOT trigger.").
  • Duplicate rule: Click copy → new modal pre-filled with original values + " (Copy)" appended to name → edit as needed → save.
  • Bulk select: Checkbox on row → top bar appears: "3 selected" + "Enable" / "Disable" / "Delete" buttons → action applies to all selected.
  • Impersonation: Read-only. Create, edit, delete, toggle, test all disabled. Detail panel viewable but no actions.

9. Error States#

  • Loading: Skeleton table (8 rows).
  • Empty: "No alert rules configured. Create your first rule to start monitoring." + CTA to create.
  • All inactive: Amber banner: "All rules are disabled. No alerts will fire."
  • Mutation fail: Toast "Failed to save rule. {error message}". Form stays open with changes preserved.
  • Validation error: Inline red text below field. "Threshold must be a number." "At least one channel is required."
  • Duplicate name: "A rule with this name already exists." on blur.
  • Test fail: "Test failed: metric data unavailable. Try again in 30 seconds."
  • Concurrent edit: If another admin edits the same rule, banner: "This rule was updated by {name} at {time}. Reload to see latest values."

10. Role-Based Variations#

  • ADMIN: Full CRUD, toggle, test, bulk actions.
  • Other roles: No access.
  • Impersonation: Read-only. All action buttons disabled. Stats visible but no test. Cannot create, edit, or delete rules.

8.2 Alert History Feed#

1. Purpose#

Chronological log of all triggered alerts. Admins see what happened, when, why, and whether it was resolved. Used for post-mortems, pattern detection, and proving SLA compliance.

2. Visual Layout#

Section A: Filters Bar (top, full width)

  • Date range picker: "Today", "Last 7 days", "Last 30 days", "Custom" (default: Today)
  • Severity filter: Multi-select dropdown (INFO, WARNING, CRITICAL)
  • Metric filter: Multi-select dropdown (all 16 metric types)
  • Status filter: Toggle group — "All", "Active", "Resolved", "Acknowledged"
  • Search: "Search alert text or rule name"
  • "Export CSV" button (right-aligned, secondary)

Section B: Alert Feed (below, full width)

  • Timeline-style feed (not a table). Each alert is a card:
  • Card structure:
    • Left border: 4px colored by severity (red/amber/gray)
    • Top row: Severity badge + Rule name + Timestamp (relative: "2 min ago", "15 min ago", "1 hr ago")
    • Second row: Metric + computed value + threshold (e.g., "Queue depth: 147 jobs (threshold: 100)")
    • Third row: Duration (if resolved: "Active for 12 min" / if still active: "Active for 12 min" in red)
    • Action row: "Acknowledge" button (if active) / "Resolved" badge (if resolved) / "View Details" link
  • Card states:
    • Active (not acknowledged): bg-white, red left border, pulsing red dot indicator on timestamp
    • Acknowledged: bg-white, red left border but no pulse, "Acknowledged by {name} at {time}"
    • Resolved: bg-gray-50, gray left border, "Resolved automatically at {time}" or "Resolved by {name}"
  • Grouping: Consecutive alerts from the same rule within 1 hour are grouped with a "+{N} more" expander.
  • Infinite scroll: Feed loads 50 alerts at a time, auto-loads next 50 on scroll.

Section C: Summary Bar (above feed)

  • Horizontal row: "Total alerts: {N}" | "Critical: {N}" | "Warning: {N}" | "Info: {N}" | "Active: {N}" | "Avg resolution time: {N} min"
  • Each stat: text-lg value + text-sm label below.
  • If active alerts > 0, "Active" stat pulses with red background.

Section D: Alert Detail Modal

  • Click "View Details" → modal:
    • Full rule configuration (read-only)
    • Triggered value history (sparkline of last 24h for that metric)
    • Timeline of events: triggered at → acknowledged at (if) → resolved at (if)
    • Related actions: "Go to affected area" (e.g., link to Workflow Monitor for queue alerts)
    • "Add note" textarea for admin to document incident response

3. Data Source (tRPC endpoint)#

admin.getAlertHistory.useQuery({
  dateRange: z.object({ from: z.date(), to: z.date() }),
  severities: z.array(AlertSeveritySchema).optional(),
  metrics: z.array(AlertMetricSchema).optional(),
  status: z.enum(["ALL", "ACTIVE", "RESOLVED", "ACKNOWLEDGED"]).default("ALL"),
  search: z.string().optional(),
  page: z.number().min(1).default(1),
  pageSize: z.number().min(1).max(100).default(50),
}, { refetchInterval: 30000 }); // 30s frequent

admin.acknowledgeAlert.useMutation({ alertId: z.string() });
admin.resolveAlert.useMutation({ alertId: z.string() });
admin.addAlertNote.useMutation({ alertId: z.string(), note: z.string().max(1000) });
admin.exportAlertHistory.useMutation({ // returns CSV download URL
  dateRange: z.object({ from: z.date(), to: z.date() }),
  severities: z.array(AlertSeveritySchema).optional(),
  metrics: z.array(AlertMetricSchema).optional(),
});

4. Zod Schema#

const AlertHistoryItemSchema = z.object({
  id: z.string().uuid(),
  ruleId: z.string().uuid(),
  ruleName: z.string(),
  severity: AlertSeveritySchema,
  metric: AlertMetricSchema,
  triggeredValue: z.union([z.number(), z.string()]),
  threshold: z.union([z.number(), z.string()]),
  condition: AlertConditionSchema,
  triggeredAt: z.date(),
  acknowledgedAt: z.date().optional(),
  acknowledgedBy: z.string().optional(),
  resolvedAt: z.date().optional(),
  resolvedBy: z.string().optional(),
  resolvedAutomatically: z.boolean().default(false),
  durationMinutes: z.number().optional(), // computed: resolvedAt - triggeredAt
  notes: z.array(z.object({
    text: z.string(),
    createdAt: z.date(),
    createdBy: z.string(),
  })).optional(),
});

const AlertHistoryResponseSchema = z.object({
  items: z.array(AlertHistoryItemSchema),
  total: z.number(),
  activeCount: z.number(),
  criticalCount: z.number(),
  warningCount: z.number(),
  infoCount: z.number(),
  avgResolutionMinutes: z.number().optional(),
  hasMore: z.boolean(),
});

5. Fetch Frequency#

  • Frequent (30000ms / 30s): Active alerts need to be seen quickly. 30s polling.
  • On demand: Filters changed, pagination, export.
  • SSE: When new alert triggers, push to feed immediately. When alert is acknowledged or resolved by another admin, push status update.

6. Data Manipulations#

  • Relative timestamps: < 60s → "Just now"; < 60min → "{N} min ago"; < 24h → "{N} hr ago"; else → "{date} {time}".
  • Duration: Active alerts show live duration (auto-incrementing every second). "Active for 12 min 34 sec".
  • Value formatting: Same as alert rules (jobs, %, $, ms).
  • Grouping: Alerts from same rule within 1 hour collapsed into single card with "+{N} more" expander. Click expander → sub-cards appear below.
  • Severity color: Border-left + badge color. Active alerts get pulsing red dot.
  • Resolution time: Computed as (resolvedAt - triggeredAt) / 60000. Avg computed across all resolved alerts in the selected period.
  • Export: CSV with columns: Date, Rule, Severity, Metric, Value, Threshold, Duration, Status, Acknowledged By, Resolved By, Notes.

7. Rationale#

  • Feed, not table: Alerts are temporal events. A feed is the right mental model — like a news feed or Twitter timeline. Tables are for structured data that you compare side-by-side.
  • Card with border color: Severity is the primary dimension. Left border color lets you scan for critical alerts in a long feed.
  • Pulsing active indicator: Active alerts need attention. A subtle pulse (CSS animation) draws the eye without being annoying.
  • 30s polling: New alerts need to be discovered quickly. But active alert durations update every second client-side, so the UI feels live even with 30s data refresh.
  • Grouping: Prevents feed spam. If a rule triggers 20 times in 10 minutes, you don't want 20 cards. One card with "+19 more" keeps the feed readable.
  • Acknowledge workflow: Acknowledging an alert means "I see this, I'm handling it." Other admins know not to panic. Critical for team coordination during incidents.
  • Resolution tracking: SLA metric. "How long from trigger to resolution?" Avg resolution time tells you if the team is responsive.
  • Notes: Incident documentation. Admin writes "Increased worker count from 5 to 10. Queue cleared at 14:32." → audit trail + knowledge base.

8. Interaction Flows#

  • Filter change: Change severity dropdown → feed reloads with new filter → URL query params updated (shareable link).
  • Acknowledge: Click "Acknowledge" → optimistic UI: card stops pulsing, shows "Acknowledged by you at {time}" → mutation → toast "Alert acknowledged" → SSE pushes to other admins.
  • Resolve: Click "Resolve" → "Resolved by you at {time}" → card background shifts to gray → toast "Alert resolved" → stats update.
  • Add note: Click "View Details" → modal → type in "Add note" → "Save Note" → note appended to timeline → toast "Note added".
  • Export: Click "Export CSV" → mutation → returns download URL → browser downloads CSV.
  • Infinite scroll: Scroll to bottom → loader appears → next 50 load → append to feed. If no more, show "No more alerts."
  • Date range: Click "Today" (default) → feed shows today's alerts. Click "Last 7 days" → expands. Click "Custom" → date picker appears.
  • Impersonation: Read-only. Acknowledge, resolve, add note, export all disabled. Can view feed and details.

9. Error States#

  • Loading: Skeleton feed (5 cards with shimmer).
  • Empty (filtered): "No alerts match your filters. Try adjusting severity or date range."
  • Empty (no alerts ever): "No alerts triggered yet. Rules are running — you'll see alerts here when thresholds are crossed."
  • Export fail: Toast "Export failed. Try a smaller date range." (for large datasets).
  • Acknowledge fail: Toast "Failed to acknowledge. Alert may have been resolved by another admin." → UI syncs to latest state.
  • No connection: Red banner "Alert feed unavailable. Check integration status." → link to /admin/system integrations.
  • SSE disconnect: Subtle amber dot in header: "Live updates paused. Reconnecting..." → auto-reconnects.

10. Role-Based Variations#

  • ADMIN: Full access — acknowledge, resolve, add notes, export, filter, view details.
  • Other roles: No access.
  • Impersonation: Read-only. All action buttons hidden or disabled. Feed and detail view accessible.

8.3 Alert Channels Configuration#

1. Purpose#

Configure notification channels for alert delivery. Admins set up email (SMTP), Slack webhooks, custom webhooks, and PagerDuty integration. Each channel can be tested before saving.

2. Visual Layout#

Section A: Channel Cards (top, full width)

  • 4 cards in a row (desktop), 2x2 (tablet), stacked (mobile). Each card:
    • Icon + name: Envelope icon "Email", Slack icon "Slack", Globe icon "Webhook", Bell icon "PagerDuty"
    • Status badge: CONFIGURED (green) / NOT_CONFIGURED (gray) / ERROR (red)
    • Config summary:
      • Email: "SMTP: {host}:{port} | From: {address}"
      • Slack: "Webhook: {url truncated}"
      • Webhook: "URL: {url truncated} | Method: POST"
      • PagerDuty: "Service: {name} | Integration Key: ••••••••"
    • Test button: "Send Test Alert" (secondary button)
    • Edit button: "Configure" (primary button, Ember Orange)
  • Card background: bg-white with shadow-sm. CONFIGURED cards have green top border. ERROR cards have red top border.

Section B: Channel Edit Modal (per card)

  • Click "Configure" → modal opens with channel-specific form:
    • Email: SMTP host, port, username, password (masked), from address, from name, TLS toggle. "Test & Save" button.
    • Slack: Webhook URL input, channel name, username, icon emoji. "Test & Save" button.
    • Webhook: URL input, HTTP method select (POST/PUT), headers key-value pairs (dynamic add/remove), payload template textarea (JSON with variables like {{ruleName}}, {{severity}}, {{message}}). "Test & Save" button.
    • PagerDuty: Integration key (masked), service name, severity mapping (INFO→low, WARNING→high, CRITICAL→critical). "Test & Save" button.
  • All fields have inline validation. "Test" sends a test alert to the channel without saving. If test succeeds, green toast. If fails, red toast with error.

3. Data Source (tRPC endpoint)#

admin.getAlertChannels.useQuery(undefined, { refetchInterval: 300000 }); // 5min static
admin.updateAlertChannel.useMutation({
  channel: z.enum(["EMAIL", "SLACK", "WEBHOOK", "PAGERDUTY"]),
  config: z.record(z.any()), // channel-specific config
});
admin.testAlertChannel.useMutation({
  channel: z.enum(["EMAIL", "SLACK", "WEBHOOK", "PAGERDUTY"]),
  config: z.record(z.any()).optional(), // test with unsaved config
});

4. Zod Schema#

const EmailChannelConfigSchema = z.object({
  host: z.string().min(1),
  port: z.number().min(1).max(65535).default(587),
  username: z.string().min(1),
  password: z.string().min(1),
  fromAddress: z.string().email(),
  fromName: z.string().min(1).default("RankFlow AI Alerts"),
  tls: z.boolean().default(true),
});

const SlackChannelConfigSchema = z.object({
  webhookUrl: z.string().url(),
  channel: z.string().min(1).default("#alerts"),
  username: z.string().min(1).default("RankFlow Bot"),
  iconEmoji: z.string().default(":warning:"),
});

const WebhookChannelConfigSchema = z.object({
  url: z.string().url(),
  method: z.enum(["POST", "PUT"]).default("POST"),
  headers: z.array(z.object({ key: z.string(), value: z.string() })).default([]),
  payloadTemplate: z.string().default('{"rule":"{{ruleName}}","severity":"{{severity}}","message":"{{message}}"}'),
});

const PagerDutyChannelConfigSchema = z.object({
  integrationKey: z.string().min(1),
  serviceName: z.string().min(1),
  severityMapping: z.object({
    INFO: z.enum(["info", "warning", "error", "critical"]).default("info"),
    WARNING: z.enum(["info", "warning", "error", "critical"]).default("warning"),
    CRITICAL: z.enum(["info", "warning", "error", "critical"]).default("critical"),
  }).default({ INFO: "info", WARNING: "warning", CRITICAL: "critical" }),
});

const AlertChannelConfigSchema = z.discriminatedUnion("channel", [
  z.object({ channel: z.literal("EMAIL"), config: EmailChannelConfigSchema }),
  z.object({ channel: z.literal("SLACK"), config: SlackChannelConfigSchema }),
  z.object({ channel: z.literal("WEBHOOK"), config: WebhookChannelConfigSchema }),
  z.object({ channel: z.literal("PAGERDUTY"), config: PagerDutyChannelConfigSchema }),
]);

5. Fetch Frequency#

  • Static (300000ms / 5min): Channel configs change rarely. 5min polling.
  • On-demand: After save/test, invalidate and refetch.

6. Data Manipulations#

  • URL masking: Webhook URLs shown as https://hooks.slack.com/services/••••••••/•••••••• — only domain and first path segment visible.
  • Key masking: PagerDuty integration key shown as ••••••••{last4}.
  • Status derivation:
    • All required fields filled → CONFIGURED
    • Any required field empty → NOT_CONFIGURED
    • Last test failed → ERROR
  • Test result: Inline below test button: green "Test alert delivered in 234ms" or red "Test failed: 401 Unauthorized. Check integration key."

7. Rationale#

  • Card per channel: 4 channels, 4 cards. Visual clarity. Admin sees at a glance which channels are ready.
  • Test before save: Prevents broken alerts. Admin tests the channel → confirms delivery → then saves. No "oops, I typo'd the webhook URL and lost 3 hours of alerts."
  • Webhook payload template: Flexible for power users. Variables like {{ruleName}} get replaced at trigger time. JSON template validated on save.
  • PagerDuty severity mapping: PagerDuty has its own severity levels. Mapping ensures RankFlow CRITICAL → PagerDuty critical, not PagerDuty info.
  • 5min polling: Channel configs are almost static. No need for real-time.
  • Status on card: Green top border = good. Red = broken. Immediate visual state without clicking.

8. Interaction Flows#

  • Configure channel: Click "Configure" → modal opens with current config (or empty defaults) → edit fields → "Test" → wait for result → if green, click "Save" → toast "Channel configured" → card updates status.
  • Test with unsaved config: Edit fields → "Test" → sends test using current form values (not saved values) → result shown → if green, save. If red, fix and retest.
  • Delete config: "Remove configuration" button at bottom of modal → confirmation → clears config → card returns to NOT_CONFIGURED.
  • Webhook headers: Click "Add header" → key/value inputs appear → click X to remove. Empty headers ignored on save.
  • Payload template help: "?" icon next to textarea → tooltip shows available variables: {{ruleName}}, {{severity}}, {{metric}}, {{value}}, {{threshold}}, {{timestamp}}, {{url}}.
  • Impersonation: Read-only. All cards show "Configure" and "Test" buttons disabled. No modal access.

9. Error States#

  • Loading: Skeleton cards (4 cards with shimmer).
  • Test fail: Inline red text with specific error. "Connection timeout after 10s." "401 Unauthorized — check API key." "Invalid webhook URL format."
  • Save fail: Toast "Failed to save channel config. {error}". Modal stays open.
  • Partial config: If some fields filled but not all, card shows NOT_CONFIGURED with amber text "Complete required fields."
  • SMTP test fail: Specific error message. "Could not connect to SMTP server. Check host and port." or "Authentication failed. Check username and password."
  • No channels: "No channels configured. At least one channel is required for alerts to fire."

10. Role-Based Variations#

  • ADMIN: Full configure, test, save, delete access.
  • Other roles: No access.
  • Impersonation: Read-only. Cards visible but all actions disabled. No modal access.