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.mdOn this page
- 8.1 Alert Rules List
- 1. Purpose
- 2. Visual Layout
- 3. Data Source (tRPC endpoint)
- 4. Zod Schema
- 5. Fetch Frequency
- 6. Data Manipulations
- 7. Rationale
- 8. Interaction Flows
- 9. Error States
- 10. Role-Based Variations
- 8.2 Alert History Feed
- 1. Purpose
- 2. Visual Layout
- 3. Data Source (tRPC endpoint)
- 4. Zod Schema
- 5. Fetch Frequency
- 6. Data Manipulations
- 7. Rationale
- 8. Interaction Flows
- 9. Error States
- 10. Role-Based Variations
- 8.3 Alert Channels Configuration
- 1. Purpose
- 2. Visual Layout
- 3. Data Source (tRPC endpoint)
- 4. Zod Schema
- 5. Fetch Frequency
- 6. Data Manipulations
- 7. Rationale
- 8. Interaction Flows
- 9. Error States
- 10. Role-Based Variations
Role access: ADMIN only.
VIEWER/EDITOR/CLIENTredirected to/dashboard. Layout: 3-tab interface: Rules, History, Channels.1200pxmax-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-basetext-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.,
100jobs,15%,$50,5signups) - Severity:
INFO:bg-gray-50+text-gray-700+ blue dotWARNING:bg-amber-50+text-amber-700+ amber dotCRITICAL: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,8pxradius,text-lgvalue intext-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/alertspage to update "Triggered Today" count and "Last Triggered" timestamps.
6. Data Manipulations#
- Threshold formatting:
QUEUE_DEPTH→{N} jobsAPI_FAILURE_RATE→{N}%COST_DAILY→${N}API_LATENCY_MS→{N}ms(or{N}sif >= 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}"
- Active (not acknowledged):
- 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-lgvalue +text-smlabel 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/systemintegrations. - 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-whitewithshadow-sm.CONFIGUREDcards have green top border.ERRORcards 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
- All required fields filled →
- 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_CONFIGUREDwith 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.