feat: implement Phase 5 — Alerting & Monitoring

Backend:
- AlertEvaluationService: evaluates metrics against AlertRules after each heartbeat
  - Supports cpu_usage_percent and memory_usage_percent metric paths
  - Operators: >, >=, <, <=, ==
  - 15-minute dedup window to prevent alert spam
- AlertRulesController: full CRUD for alert rules (GET/POST/PUT/DELETE)
- AlertsController: list with acknowledged filter + POST acknowledge endpoint
- IRmmHubClient: added AlertTriggered push method
- Program.cs: AlertEvaluationService registered as Scoped

Frontend:
- AlertsPage: two-tab layout (active alerts + rules)
  - Alerts tab: severity badges, acknowledge button, all/unack/ack filter
  - Rules tab: condition display, enabled toggle, delete with confirm
  - Create rule modal with MetricPath/Operator/Threshold/Severity selects
- api/types.ts: AlertRule, AlertItem, CreateAlertRuleRequest types
- api/client.ts: alertRulesApi and alertsApi
- useAgentSignalR: handles AlertTriggered → invalidates alerts query
- App.tsx: Alerts nav item with Bell icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-19 14:00:19 +01:00
parent d17df20f5e
commit eb114f68e2
11 changed files with 815 additions and 3 deletions

View File

@@ -6,6 +6,10 @@ import type {
CreateTaskRequest,
CreateTicketRequest,
UpdateTicketRequest,
AlertRule,
AlertItem,
CreateAlertRuleRequest,
UpdateAlertRuleRequest,
} from './types'
const BASE_URL = '/api/v1'
@@ -48,3 +52,24 @@ export const ticketsApi = {
update: (id: number, data: UpdateTicketRequest) =>
request<Ticket>(`/tickets/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
}
// Alert Rules
export const alertRulesApi = {
list: () => request<AlertRule[]>('/alert-rules'),
create: (data: CreateAlertRuleRequest) =>
request<AlertRule>('/alert-rules', { method: 'POST', body: JSON.stringify(data) }),
update: (id: number, data: UpdateAlertRuleRequest) =>
request<AlertRule>(`/alert-rules/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id: number) =>
request<void>(`/alert-rules/${id}`, { method: 'DELETE' }),
}
// Alerts
export const alertsApi = {
list: (acknowledged?: boolean) => {
const param = acknowledged !== undefined ? `?acknowledged=${acknowledged}` : ''
return request<AlertItem[]>(`/alerts${param}`)
},
acknowledge: (id: number) =>
request<{ id: number; acknowledged: boolean }>(`/alerts/${id}/acknowledge`, { method: 'POST' }),
}

View File

@@ -93,3 +93,42 @@ export interface UpdateTicketRequest {
status?: TicketStatus
priority?: TicketPriority
}
export interface AlertRule {
id: number
name: string
metricPath: string
operator: string
threshold: number
severity: AlertSeverity
enabled: boolean
}
export interface AlertItem {
id: number
message: string
severity: AlertSeverity
acknowledged: boolean
createdAt: string
agentId: string
agentHostname: string
ruleId: number
ruleName: string
}
export interface CreateAlertRuleRequest {
name: string
metricPath: string
operator: string
threshold: number
severity: AlertSeverity
}
export interface UpdateAlertRuleRequest {
name?: string
metricPath?: string
operator?: string
threshold?: number
severity?: AlertSeverity
enabled?: boolean
}