From eb114f68e2895627c6db7838224a3491aaeb3bf3 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Thu, 19 Mar 2026 14:00:19 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20Phase=205=20=E2=80=94=20Ale?= =?UTF-8?q?rting=20&=20Monitoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Controllers/AlertRulesController.cs | 84 +++ .../Controllers/AlertsController.cs | 50 ++ .../GrpcServices/AgentGrpcService.cs | 8 +- .../src/NexusRMM.Api/Hubs/IRmmHubClient.cs | 3 + Backend/src/NexusRMM.Api/Program.cs | 3 + .../Services/AlertEvaluationService.cs | 96 ++++ Frontend/src/App.tsx | 7 +- Frontend/src/api/client.ts | 25 + Frontend/src/api/types.ts | 39 ++ Frontend/src/hooks/useAgentSignalR.ts | 6 + Frontend/src/pages/AlertsPage.tsx | 497 ++++++++++++++++++ 11 files changed, 815 insertions(+), 3 deletions(-) create mode 100644 Backend/src/NexusRMM.Api/Controllers/AlertRulesController.cs create mode 100644 Backend/src/NexusRMM.Api/Controllers/AlertsController.cs create mode 100644 Backend/src/NexusRMM.Api/Services/AlertEvaluationService.cs create mode 100644 Frontend/src/pages/AlertsPage.tsx diff --git a/Backend/src/NexusRMM.Api/Controllers/AlertRulesController.cs b/Backend/src/NexusRMM.Api/Controllers/AlertRulesController.cs new file mode 100644 index 0000000..b1cd336 --- /dev/null +++ b/Backend/src/NexusRMM.Api/Controllers/AlertRulesController.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NexusRMM.Core.Models; +using NexusRMM.Infrastructure.Data; + +namespace NexusRMM.Api.Controllers; + +[ApiController] +[Route("api/v1/alert-rules")] +public class AlertRulesController : ControllerBase +{ + private readonly RmmDbContext _db; + public AlertRulesController(RmmDbContext db) => _db = db; + + [HttpGet] + public async Task GetAll() => + Ok(await _db.AlertRules.OrderBy(r => r.Name).ToListAsync()); + + [HttpGet("{id:int}")] + public async Task GetById(int id) + { + var rule = await _db.AlertRules.FindAsync(id); + return rule is null ? NotFound() : Ok(rule); + } + + [HttpPost] + public async Task Create([FromBody] CreateAlertRuleRequest req) + { + var rule = new AlertRule + { + Name = req.Name, + MetricPath = req.MetricPath, + Operator = req.Operator, + Threshold = req.Threshold, + Severity = req.Severity, + Enabled = true, + }; + _db.AlertRules.Add(rule); + await _db.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = rule.Id }, rule); + } + + [HttpPut("{id:int}")] + public async Task Update(int id, [FromBody] UpdateAlertRuleRequest req) + { + var rule = await _db.AlertRules.FindAsync(id); + if (rule is null) return NotFound(); + + if (req.Name is not null) rule.Name = req.Name; + if (req.MetricPath is not null) rule.MetricPath = req.MetricPath; + if (req.Operator is not null) rule.Operator = req.Operator; + if (req.Threshold.HasValue) rule.Threshold = req.Threshold.Value; + if (req.Severity.HasValue) rule.Severity = req.Severity.Value; + if (req.Enabled.HasValue) rule.Enabled = req.Enabled.Value; + + await _db.SaveChangesAsync(); + return Ok(rule); + } + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + var rule = await _db.AlertRules.FindAsync(id); + if (rule is null) return NotFound(); + _db.AlertRules.Remove(rule); + await _db.SaveChangesAsync(); + return NoContent(); + } +} + +public record CreateAlertRuleRequest( + string Name, + string MetricPath, + string Operator, + double Threshold, + AlertSeverity Severity); + +public record UpdateAlertRuleRequest( + string? Name, + string? MetricPath, + string? Operator, + double? Threshold, + AlertSeverity? Severity, + bool? Enabled); diff --git a/Backend/src/NexusRMM.Api/Controllers/AlertsController.cs b/Backend/src/NexusRMM.Api/Controllers/AlertsController.cs new file mode 100644 index 0000000..2283cec --- /dev/null +++ b/Backend/src/NexusRMM.Api/Controllers/AlertsController.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NexusRMM.Infrastructure.Data; + +namespace NexusRMM.Api.Controllers; + +[ApiController] +[Route("api/v1/alerts")] +public class AlertsController : ControllerBase +{ + private readonly RmmDbContext _db; + public AlertsController(RmmDbContext db) => _db = db; + + [HttpGet] + public async Task GetAll([FromQuery] bool? acknowledged = null) + { + var query = _db.Alerts + .Include(a => a.Rule) + .Include(a => a.Agent) + .AsQueryable(); + + if (acknowledged.HasValue) + query = query.Where(a => a.Acknowledged == acknowledged.Value); + + var alerts = await query + .OrderByDescending(a => a.CreatedAt) + .Take(200) + .Select(a => new + { + a.Id, a.Message, a.Severity, a.Acknowledged, a.CreatedAt, + AgentId = a.AgentId.ToString(), + AgentHostname = a.Agent.Hostname, + RuleId = a.RuleId, + RuleName = a.Rule.Name, + }) + .ToListAsync(); + + return Ok(alerts); + } + + [HttpPost("{id:long}/acknowledge")] + public async Task Acknowledge(long id) + { + var alert = await _db.Alerts.FindAsync(id); + if (alert is null) return NotFound(); + alert.Acknowledged = true; + await _db.SaveChangesAsync(); + return Ok(new { alert.Id, alert.Acknowledged }); + } +} diff --git a/Backend/src/NexusRMM.Api/GrpcServices/AgentGrpcService.cs b/Backend/src/NexusRMM.Api/GrpcServices/AgentGrpcService.cs index 09c8b70..ceb366c 100644 --- a/Backend/src/NexusRMM.Api/GrpcServices/AgentGrpcService.cs +++ b/Backend/src/NexusRMM.Api/GrpcServices/AgentGrpcService.cs @@ -3,6 +3,7 @@ using Grpc.Core; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using NexusRMM.Api.Hubs; +using NexusRMM.Api.Services; using NexusRMM.Core.Models; using NexusRMM.Infrastructure.Data; using NexusRMM.Protos; @@ -16,12 +17,14 @@ public class AgentGrpcService : AgentService.AgentServiceBase private readonly RmmDbContext _db; private readonly ILogger _logger; private readonly IHubContext _hub; + private readonly AlertEvaluationService _alertService; - public AgentGrpcService(RmmDbContext db, ILogger logger, IHubContext hub) + public AgentGrpcService(RmmDbContext db, ILogger logger, IHubContext hub, AlertEvaluationService alertService) { _db = db; _logger = logger; _hub = hub; + _alertService = alertService; } public override async Task Enroll(EnrollRequest request, ServerCallContext context) @@ -102,6 +105,9 @@ public class AgentGrpcService : AgentService.AgentServiceBase await _hub.Clients.All .AgentStatusChanged(request.AgentId, "Online", DateTime.UtcNow.ToString("O")); + // Alert-Engine: Metriken gegen Regeln auswerten + await _alertService.EvaluateAsync(agentId, agent.Hostname, request.Metrics); + return response; } diff --git a/Backend/src/NexusRMM.Api/Hubs/IRmmHubClient.cs b/Backend/src/NexusRMM.Api/Hubs/IRmmHubClient.cs index 00ca46b..f94ccbf 100644 --- a/Backend/src/NexusRMM.Api/Hubs/IRmmHubClient.cs +++ b/Backend/src/NexusRMM.Api/Hubs/IRmmHubClient.cs @@ -13,4 +13,7 @@ public interface IRmmHubClient /// Command-Ergebnis verfügbar (an agent-Gruppe gepusht) Task CommandResultUpdated(string taskId, string agentId, bool success, int exitCode); + + /// Neuer Alert ausgelöst (an alle Clients gepusht) + Task AlertTriggered(string agentId, string agentHostname, string ruleName, string message, string severity); } diff --git a/Backend/src/NexusRMM.Api/Program.cs b/Backend/src/NexusRMM.Api/Program.cs index 27a65f9..10efc55 100644 --- a/Backend/src/NexusRMM.Api/Program.cs +++ b/Backend/src/NexusRMM.Api/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.EntityFrameworkCore; using NexusRMM.Api.GrpcServices; using NexusRMM.Api.Hubs; +using NexusRMM.Api.Services; using NexusRMM.Infrastructure.Data; var builder = WebApplication.CreateBuilder(args); @@ -22,6 +23,8 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddScoped(); + builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => diff --git a/Backend/src/NexusRMM.Api/Services/AlertEvaluationService.cs b/Backend/src/NexusRMM.Api/Services/AlertEvaluationService.cs new file mode 100644 index 0000000..b2f6981 --- /dev/null +++ b/Backend/src/NexusRMM.Api/Services/AlertEvaluationService.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using NexusRMM.Api.Hubs; +using NexusRMM.Core.Models; +using NexusRMM.Infrastructure.Data; +using NexusRMM.Protos; + +namespace NexusRMM.Api.Services; + +/// +/// Wertet HeartbeatRequest-Metriken gegen alle aktiven AlertRules aus. +/// Wird vom AgentGrpcService nach jedem Heartbeat aufgerufen. +/// +public class AlertEvaluationService +{ + private readonly RmmDbContext _db; + private readonly IHubContext _hub; + private readonly ILogger _logger; + + public AlertEvaluationService( + RmmDbContext db, + IHubContext hub, + ILogger logger) + { + _db = db; + _hub = hub; + _logger = logger; + } + + public async Task EvaluateAsync(Guid agentId, string agentHostname, SystemMetrics metrics) + { + var rules = await _db.AlertRules + .Where(r => r.Enabled) + .ToListAsync(); + + foreach (var rule in rules) + { + var metricValue = ExtractMetricValue(metrics, rule.MetricPath); + if (metricValue is null) continue; + + if (!EvaluateCondition(metricValue.Value, rule.Operator, rule.Threshold)) continue; + + // Duplikat-Schutz: kein neuer Alert wenn eines innerhalb der letzten 15 Min. existiert + var recentAlert = await _db.Alerts + .AnyAsync(a => a.AgentId == agentId + && a.RuleId == rule.Id + && a.CreatedAt > DateTime.UtcNow.AddMinutes(-15)); + + if (recentAlert) continue; + + var message = $"{rule.Name}: {rule.MetricPath} {rule.Operator} {rule.Threshold} " + + $"(aktuell: {metricValue.Value:F1})"; + + var alert = new Alert + { + RuleId = rule.Id, + AgentId = agentId, + Message = message, + Severity = rule.Severity, + Acknowledged = false, + CreatedAt = DateTime.UtcNow, + }; + + _db.Alerts.Add(alert); + await _db.SaveChangesAsync(); + + _logger.LogWarning("Alert ausgelöst: {Message} für Agent {AgentId}", message, agentId); + + await _hub.Clients.All.AlertTriggered( + agentId.ToString(), + agentHostname, + rule.Name, + message, + rule.Severity.ToString()); + } + } + + private static double? ExtractMetricValue(SystemMetrics metrics, string metricPath) => + metricPath switch + { + "cpu_usage_percent" => metrics.CpuUsagePercent, + "memory_usage_percent" => metrics.MemoryUsagePercent, + _ => null + }; + + private static bool EvaluateCondition(double value, string op, double threshold) => + op switch + { + ">" => value > threshold, + ">=" => value >= threshold, + "<" => value < threshold, + "<=" => value <= threshold, + "==" => Math.Abs(value - threshold) < 0.001, + _ => false + }; +} diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index dbf4c89..b8cb4f3 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -1,9 +1,10 @@ import { useState } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { LayoutDashboard, Ticket, Menu, X } from 'lucide-react' +import { LayoutDashboard, Ticket, Bell, Menu, X } from 'lucide-react' import { DashboardPage } from './pages/DashboardPage' import { AgentDetailPage } from './pages/AgentDetailPage' import TicketsPage from './pages/TicketsPage' +import AlertsPage from './pages/AlertsPage' import { cn } from './lib/utils' const queryClient = new QueryClient({ @@ -15,7 +16,7 @@ const queryClient = new QueryClient({ }, }) -type Page = 'dashboard' | 'agent-detail' | 'tickets' +type Page = 'dashboard' | 'agent-detail' | 'tickets' | 'alerts' interface NavItem { id: Page @@ -26,6 +27,7 @@ interface NavItem { const navItems: NavItem[] = [ { id: 'dashboard', label: 'Dashboard', icon: }, { id: 'tickets', label: 'Tickets', icon: }, + { id: 'alerts', label: 'Alerts', icon: }, ] function AppContent() { @@ -107,6 +109,7 @@ function AppContent() { )} {page === 'tickets' && } + {page === 'alerts' && } ) diff --git a/Frontend/src/api/client.ts b/Frontend/src/api/client.ts index 9926c3e..4bcd092 100644 --- a/Frontend/src/api/client.ts +++ b/Frontend/src/api/client.ts @@ -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(`/tickets/${id}`, { method: 'PUT', body: JSON.stringify(data) }), } + +// Alert Rules +export const alertRulesApi = { + list: () => request('/alert-rules'), + create: (data: CreateAlertRuleRequest) => + request('/alert-rules', { method: 'POST', body: JSON.stringify(data) }), + update: (id: number, data: UpdateAlertRuleRequest) => + request(`/alert-rules/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + delete: (id: number) => + request(`/alert-rules/${id}`, { method: 'DELETE' }), +} + +// Alerts +export const alertsApi = { + list: (acknowledged?: boolean) => { + const param = acknowledged !== undefined ? `?acknowledged=${acknowledged}` : '' + return request(`/alerts${param}`) + }, + acknowledge: (id: number) => + request<{ id: number; acknowledged: boolean }>(`/alerts/${id}/acknowledge`, { method: 'POST' }), +} diff --git a/Frontend/src/api/types.ts b/Frontend/src/api/types.ts index 550636d..900c95d 100644 --- a/Frontend/src/api/types.ts +++ b/Frontend/src/api/types.ts @@ -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 +} diff --git a/Frontend/src/hooks/useAgentSignalR.ts b/Frontend/src/hooks/useAgentSignalR.ts index 15891a6..f627bcc 100644 --- a/Frontend/src/hooks/useAgentSignalR.ts +++ b/Frontend/src/hooks/useAgentSignalR.ts @@ -22,6 +22,12 @@ export function useGlobalSignalR() { // Einzelnen Agent-Cache aktualisieren queryClient.invalidateQueries({ queryKey: ['agent', agentId] }) }) + + connection.on('AlertTriggered', (_agentId: string, agentHostname: string, ruleName: string, _message: string, _severity: string) => { + console.debug('[SignalR] AlertTriggered', agentHostname, ruleName) + // Alerts-Liste invalidieren + queryClient.invalidateQueries({ queryKey: ['alerts'] }) + }) }, }) diff --git a/Frontend/src/pages/AlertsPage.tsx b/Frontend/src/pages/AlertsPage.tsx new file mode 100644 index 0000000..5d9ff95 --- /dev/null +++ b/Frontend/src/pages/AlertsPage.tsx @@ -0,0 +1,497 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { Bell, Plus, Trash2, Check, AlertTriangle, Info, AlertCircle } from 'lucide-react' +import { alertsApi, alertRulesApi } from '../api/client' +import type { AlertSeverity, AlertRule, CreateAlertRuleRequest } from '../api/types' +import { cn } from '../lib/utils' + +function getSeverityStyle(severity: AlertSeverity) { + return { + Critical: 'bg-red-500/20 text-red-400 border border-red-500/30', + Warning: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30', + Info: 'bg-blue-500/20 text-blue-400 border border-blue-500/30', + }[severity] +} + +function getSeverityIcon(severity: AlertSeverity) { + return { + Critical: , + Warning: , + Info: , + }[severity] +} + +const metricPathOptions = [ + { value: 'cpu_usage_percent', label: 'CPU-Auslastung (%)' }, + { value: 'memory_usage_percent', label: 'RAM-Auslastung (%)' }, +] + +const operatorOptions = [ + { value: '>', label: '>' }, + { value: '>=', label: '>=' }, + { value: '<', label: '<' }, + { value: '<=', label: '<=' }, + { value: '==', label: '==' }, +] + +const severityOptions = [ + { value: 'Info' as AlertSeverity, label: 'Info' }, + { value: 'Warning' as AlertSeverity, label: 'Warning' }, + { value: 'Critical' as AlertSeverity, label: 'Critical' }, +] + +interface CreateRuleForm { + name: string + metricPath: string + operator: string + threshold: string + severity: AlertSeverity +} + +export default function AlertsPage() { + const [activeTab, setActiveTab] = useState<'alerts' | 'rules'>('alerts') + const [alertFilter, setAlertFilter] = useState<'all' | 'unacknowledged' | 'acknowledged'>('unacknowledged') + const [showCreateRuleModal, setShowCreateRuleModal] = useState(false) + const [formData, setFormData] = useState({ + name: '', + metricPath: 'cpu_usage_percent', + operator: '>', + threshold: '', + severity: 'Warning', + }) + + const queryClient = useQueryClient() + + // Queries + const alertsQuery = useQuery({ + queryKey: ['alerts'], + queryFn: () => alertsApi.list(), + }) + + const rulesQuery = useQuery({ + queryKey: ['alert-rules'], + queryFn: () => alertRulesApi.list(), + }) + + // Mutations + const acknowledgeMutation = useMutation({ + mutationFn: (id: number) => alertsApi.acknowledge(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alerts'] }) + }, + }) + + const createRuleMutation = useMutation({ + mutationFn: (data: CreateAlertRuleRequest) => alertRulesApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alert-rules'] }) + setShowCreateRuleModal(false) + setFormData({ + name: '', + metricPath: 'cpu_usage_percent', + operator: '>', + threshold: '', + severity: 'Warning', + }) + }, + }) + + const updateRuleMutation = useMutation({ + mutationFn: ({ id, enabled }: { id: number; enabled: boolean }) => + alertRulesApi.update(id, { enabled }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alert-rules'] }) + }, + }) + + const deleteRuleMutation = useMutation({ + mutationFn: (id: number) => alertRulesApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alert-rules'] }) + }, + }) + + // Filter alerts + const filteredAlerts = alertsQuery.data?.filter((alert) => { + if (alertFilter === 'acknowledged') return alert.acknowledged + if (alertFilter === 'unacknowledged') return !alert.acknowledged + return true + }) || [] + + // Handlers + const handleCreateRule = async () => { + if (!formData.name.trim() || !formData.threshold) { + alert('Bitte alle erforderlichen Felder ausfüllen') + return + } + try { + await createRuleMutation.mutateAsync({ + name: formData.name, + metricPath: formData.metricPath, + operator: formData.operator, + threshold: parseFloat(formData.threshold), + severity: formData.severity, + }) + } catch (err) { + console.error('Fehler beim Erstellen der Regel:', err) + } + } + + const handleDeleteRule = (id: number) => { + if (window.confirm('Möchten Sie diese Regel wirklich löschen?')) { + deleteRuleMutation.mutate(id) + } + } + + const handleToggleRuleEnabled = (rule: AlertRule) => { + updateRuleMutation.mutate({ id: rule.id, enabled: !rule.enabled }) + } + + return ( +
+ {/* Header */} +
+ +

Alerts

+
+ + {/* Tabs */} +
+ + +
+ + {/* Tab 1: Aktive Alerts */} + {activeTab === 'alerts' && ( +
+ {/* Filter */} +
+ + + +
+ + {/* Loading / Error */} + {alertsQuery.isLoading &&

Lädt...

} + {alertsQuery.isError && ( +

Fehler beim Laden der Alerts

+ )} + + {/* Empty State */} + {alertsQuery.isSuccess && filteredAlerts.length === 0 && ( +

+ Keine aktiven Alerts +

+ )} + + {/* Table */} + {alertsQuery.isSuccess && filteredAlerts.length > 0 && ( +
+ + + + + + + + + + + + + {filteredAlerts.map((alert) => ( + + + + + + + + + ))} + +
ErstelltAgentRegelNachrichtSchweregradAktionen
+ {new Date(alert.createdAt).toLocaleString('de-DE')} + +
+

{alert.agentHostname}

+

{alert.agentId}

+
+
{alert.ruleName}{alert.message} +
+ {getSeverityIcon(alert.severity)} + {alert.severity} +
+
+ +
+
+ )} +
+ )} + + {/* Tab 2: Regeln */} + {activeTab === 'rules' && ( +
+ {/* Create Button */} + + + {/* Loading / Error */} + {rulesQuery.isLoading &&

Lädt...

} + {rulesQuery.isError && ( +

Fehler beim Laden der Regeln

+ )} + + {/* Empty State */} + {rulesQuery.isSuccess && rulesQuery.data.length === 0 && ( +

+ Keine Regeln vorhanden +

+ )} + + {/* Table */} + {rulesQuery.isSuccess && rulesQuery.data.length > 0 && ( +
+ + + + + + + + + + + + + {rulesQuery.data.map((rule) => { + const metricLabel = metricPathOptions.find( + (opt) => opt.value === rule.metricPath, + )?.label || rule.metricPath + return ( + + + + + + + + + ) + })} + +
NameMetrikBedingungSchweregradAktivAktionen
{rule.name}{metricLabel} + {metricLabel} {rule.operator} {rule.threshold} + +
+ {getSeverityIcon(rule.severity)} + {rule.severity} +
+
+ + + +
+
+ )} +
+ )} + + {/* Create Rule Modal */} + {showCreateRuleModal && ( +
+
+

Neue Regel erstellen

+ +
+ {/* Name */} +
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-3 py-2 bg-background border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary" + placeholder="z.B. CPU über 90%" + /> +
+ + {/* MetricPath */} +
+ + +
+ + {/* Operator */} +
+ + +
+ + {/* Threshold */} +
+ + setFormData({ ...formData, threshold: e.target.value })} + className="w-full px-3 py-2 bg-background border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary" + placeholder="z.B. 90" + /> +
+ + {/* Severity */} +
+ + +
+
+ + {/* Buttons */} +
+ + +
+
+
+ )} +
+ ) +}