diff --git a/clusters/homelab/apps/hugo/secret.sops.yaml b/clusters/homelab/apps/hugo/secret.sops.yaml index a389abc..a0c36e8 100644 --- a/clusters/homelab/apps/hugo/secret.sops.yaml +++ b/clusters/homelab/apps/hugo/secret.sops.yaml @@ -4,7 +4,7 @@ metadata: name: ENC[AES256_GCM,data:tuyp95XjnJ7hmselZQ==,iv:Qdb1EA8uQaWzHE1xcHvf0ht+iyHmya9AaoNsUVeVCnw=,tag:2tYxvILbN41gOA4jYqAGcQ==,type:str] namespace: ENC[AES256_GCM,data:VkkF3Q==,iv:Kegn4sgJEllCpAbn9GMzUknaQIC96ke608qsBNfjgQ4=,tag:cdZv3OkvkPDjFGCKhwuJ8Q==,type:str] stringData: - token: ENC[AES256_GCM,data:dWN3baAcMkqhWndpz9L5ZG8HvA==,iv:poc+qXxeloPeg0YlMBbnc74GKDg0KsF5bMIZIFQ4XM0=,tag:+2w+RfVShElHQqANpm8xdQ==,type:str] + token: ENC[AES256_GCM,data:sjNXGCiW5sKi45zCg+lfNCsxHUD7A0I5/AHuHNM5Vhwlrk+qoTLJxQ==,iv:arjjTTUal/W5jLrarKIMk4bYVWs/PUW5Yr+sn04mNb8=,tag:wbdluuXxrZ4ZvZB1lC1toA==,type:str] sops: kms: [] gcp_kms: [] @@ -20,8 +20,8 @@ sops: SWM2K2VNRDRaRmpjY3pGdVZjbEM4ZlkKcJ2Ytx9kzQaHbuw8saYIUwwojCqxiOQc gIQe/FlvZ0m8Azfr5BL5NKUcWvMYSQo6si7lngRpYBDIGw/VY6g7AA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-19T17:37:29Z" - mac: ENC[AES256_GCM,data:9tj7ezR4Pmp/336Dos7O0znnGQSC5YqHDdukpstN7Ep6dJXB4rStWqgtQgJklVtcqZ8VB2xxqvzI/P945YxLGeWf9EsXpzF2q+eSD94JUPTcRIumaq0HiSWjK74w3MyZFO1SKWOlYC9xpUsrsF4elILyhnnraY+bY/DUpAdKWgA=,iv:oD2HeT+w0Zhzo0w5UzXC1yEsgXiandiMP2JWmmRD79Q=,tag:zqrtW47PDP6tFrXWvzMxVQ==,type:str] + lastmodified: "2026-04-19T17:52:35Z" + mac: ENC[AES256_GCM,data:ZKmWO8CyAp48Bnv10c+kNBksF27oDLfkKEQ9JsBNcGb171Cxf7r21YZ9g5WGXGPxdFntWDZVDmevRVIRIb4KKMeb7UfukqKz9S4yNHaYzCo637XDohl7B9jyr9yN59kk67csYFW3fiptn96l5+NLimSAU6nSaaDEkB4HlCmi7xs=,iv:2hqcGeezi0T4jgqTVGzLNzBQkiig7mUAfSZ6qWVer60=,tag:bKovesylpjQy29lVOvzo4Q==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.9.4 diff --git a/clusters/homelab/apps/n8n/jarvis-mcp-workflow.json b/clusters/homelab/apps/n8n/jarvis-mcp-workflow.json new file mode 100644 index 0000000..7027251 --- /dev/null +++ b/clusters/homelab/apps/n8n/jarvis-mcp-workflow.json @@ -0,0 +1,671 @@ +{ + "name": "Jarvis — MCP Homelab Agent", + "nodes": [ + { + "parameters": { + "updates": [ + "message" + ], + "additionalFields": {} + }, + "id": "telegram-trigger", + "name": "Telegram Trigger", + "type": "n8n-nodes-base.telegramTrigger", + "typeVersion": 1.1, + "position": [ + 200, + 300 + ] + }, + { + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{ $json.message.text }}", + "operation": "startsWith", + "value2": "/jarvis" + } + ] + } + }, + "id": "check-jarvis-prefix", + "name": "Nur /jarvis Nachrichten", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 420, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Extrahiere die eigentliche Frage nach '/jarvis '\nconst text = $input.first().json.message.text || '';\nconst question = text.replace(/^\\/jarvis\\s*/i, '').trim();\nconst chatId = $input.first().json.message.chat.id;\nconst userId = $input.first().json.message.from.id;\n\nif (!question) {\n throw new Error('Keine Frage nach /jarvis gefunden.');\n}\n\nreturn [{ json: { question, chatId, userId } }];" + }, + "id": "extract-question", + "name": "Frage extrahieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 640, + 220 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://mcp-homelab.mcp-homelab.svc.cluster.local:8000/mcp", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "jsonrpc", + "value": "2.0" + }, + { + "name": "method", + "value": "tools/list" + }, + { + "name": "id", + "value": "1" + } + ] + }, + "options": {} + }, + "id": "mcp-tools-list", + "name": "MCP: Tools auflisten", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 860, + 220 + ], + "notes": "Holt die aktuelle Tool-Liste vom MCP-Server. Diese wird dem LLM als Kontext mitgegeben." + }, + { + "parameters": { + "jsCode": "// Baue den System-Prompt mit Tool-Beschreibungen\nconst tools = $input.first().json.result?.tools || [];\nconst question = $('Frage extrahieren').first().json.question;\n\nconst toolDescriptions = tools.map(t => \n `- ${t.name}: ${t.description}`\n).join('\\n');\n\nconst systemPrompt = `Du bist Jarvis, ein präziser Heimserver-Assistent.\n\nDu hast Zugriff auf folgende MCP-Tools:\n${toolDescriptions}\n\nRegeln:\n- Lies immer erst relevante Daten bevor du antwortest.\n- Antworte auf Deutsch, präzise und ohne Fülltext.\n- Wenn du einen Kubernetes-Fehler siehst, erkläre was ihn verursacht und was als nächstes zu tun ist.\n- Du darfst NIEMALS etwas schreiben oder verändern — nur lesen.\n- Bei Änderungsbedarf sagst du explizit: 'Ich würde empfehlen, folgenden Git-Commit zu machen: ...'`;\n\nreturn [{ json: { systemPrompt, question, tools } }];" + }, + "id": "build-system-prompt", + "name": "System-Prompt bauen", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1080, + 220 + ] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.OLLAMA_BASE_URL }}/api/chat", + "sendBody": true, + "contentType": "json", + "body": "={\n \"model\": \"qwen2.5-coder:14b\",\n \"messages\": [\n { \"role\": \"system\", \"content\": {{ JSON.stringify($json.systemPrompt) }} },\n { \"role\": \"user\", \"content\": {{ JSON.stringify($json.question) }} }\n ],\n \"stream\": false,\n \"tools\": {{ JSON.stringify($json.tools) }}\n}", + "options": { + "timeout": 120000 + } + }, + "id": "ollama-call", + "name": "Ollama (lokal, qwen2.5-coder:14b)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1300, + 100 + ], + "notes": "Primär: lokales Ollama auf dem Windows/RTX4070 Rechner. OLLAMA_BASE_URL z.B. http://192.168.2.XXX:11434" + }, + { + "parameters": { + "method": "POST", + "url": "https://api.anthropic.com/v1/messages", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "x-api-key", + "value": "={{ $env.ANTHROPIC_API_KEY }}" + }, + { + "name": "anthropic-version", + "value": "2023-06-01" + }, + { + "name": "content-type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "json", + "body": "={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 2048,\n \"system\": {{ JSON.stringify($('Build System-Prompt').first().json.systemPrompt) }},\n \"messages\": [\n { \"role\": \"user\", \"content\": {{ JSON.stringify($('Build System-Prompt').first().json.question) }} }\n ]\n}", + "options": { + "timeout": 60000 + } + }, + "id": "anthropic-fallback", + "name": "Anthropic API (Fallback)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1300, + 340 + ], + "notes": "Fallback wenn Ollama nicht erreichbar oder Frage zu komplex. ANTHROPIC_API_KEY als n8n-Variable setzen." + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{ $('Ollama (lokal, qwen2.5:14b)').first().json.error ? 1 : 0 }}", + "operation": "equal", + "value2": 1 + } + ] + } + }, + "id": "ollama-failed", + "name": "Ollama fehlgeschlagen?", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 1520, + 220 + ] + }, + { + "parameters": { + "jsCode": "// Normalisiere Antwort aus Ollama oder Anthropic\nlet answer = '';\n\ntry {\n // Ollama-Format\n const ollamaData = $('Ollama (lokal, qwen2.5:14b)').first().json;\n if (ollamaData?.message?.content) {\n answer = ollamaData.message.content;\n }\n} catch(e) {}\n\nif (!answer) {\n try {\n // Anthropic-Format\n const anthropicData = $('Anthropic API (Fallback)').first().json;\n if (anthropicData?.content?.[0]?.text) {\n answer = anthropicData.content[0].text;\n }\n } catch(e) {}\n}\n\nif (!answer) answer = 'Fehler: Keine Antwort von LLM erhalten.';\n\n// Kuerze auf Telegram-Limit (4096 Zeichen)\nif (answer.length > 4000) {\n answer = answer.substring(0, 3990) + '\\n\\n[... gekuerzt]';\n}\n\nconst chatId = $('Frage extrahieren').first().json.chatId;\nreturn [{ json: { answer, chatId } }];" + }, + "id": "normalize-answer", + "name": "Antwort normalisieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1740, + 220 + ] + }, + { + "parameters": { + "chatId": "={{ $json.chatId }}", + "text": "={{ $json.answer }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "telegram-reply", + "name": "Telegram Antwort senden", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1960, + 220 + ] + }, + { + "parameters": { + "path": "prometheus-alert", + "responseMode": "onReceived", + "options": {} + }, + "id": "prometheus-webhook", + "name": "Prometheus Alert Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 200, + 600 + ], + "webhookId": "prometheus-alert-receiver", + "notes": "Webhook-URL in Alertmanager konfigurieren: http://n8n.n8n.svc.cluster.local:5678/webhook/prometheus-alert" + }, + { + "parameters": { + "jsCode": "// Alertmanager sendet ein Array von Alerts\nconst body = $input.first().json.body || $input.first().json;\nconst alerts = body.alerts || [body];\n\nreturn alerts.map(alert => {\n const severity = alert.labels?.severity || 'unknown';\n const alertname = alert.labels?.alertname || 'UnknownAlert';\n const namespace = alert.labels?.namespace || 'unknown';\n const pod = alert.labels?.pod || alert.labels?.container || '';\n const message = alert.annotations?.message || alert.annotations?.summary || '';\n const status = alert.status || 'firing';\n\n // Severity-Level numerisch fuer spaeteren Vergleich\n const levels = { 'critical': 4, 'high': 3, 'warning': 2, 'info': 1 };\n const level = levels[severity.toLowerCase()] || 2;\n\n return { json: { severity, level, alertname, namespace, pod, message, status } };\n});" + }, + "id": "parse-alert", + "name": "Alert parsen", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 420, + 600 + ] + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{ $json.level }}", + "operation": "largerEqual", + "value2": 3 + } + ] + } + }, + "id": "is-high-or-critical", + "name": "High oder Critical?", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 640, + 600 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://mcp-homelab.mcp-homelab.svc.cluster.local:8000/mcp", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "json", + "body": "={\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"kubectl_events\",\n \"arguments\": { \"namespace\": {{ JSON.stringify($json.namespace) }} }\n },\n \"id\": \"alert-diag-1\"\n}", + "options": { + "timeout": 15000 + } + }, + "id": "mcp-get-events", + "name": "MCP: Events holen", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 860, + 500 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://mcp-homelab.mcp-homelab.svc.cluster.local:8000/mcp", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "json", + "body": "={\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"flux_status\",\n \"arguments\": {}\n },\n \"id\": \"alert-diag-2\"\n}", + "options": { + "timeout": 15000 + } + }, + "id": "mcp-flux-status", + "name": "MCP: Flux-Status holen", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 860, + 620 + ] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.anthropic.com/v1/messages", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "x-api-key", + "value": "={{ $env.ANTHROPIC_API_KEY }}" + }, + { + "name": "anthropic-version", + "value": "2023-06-01" + }, + { + "name": "content-type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "json", + "body": "={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 1024,\n \"system\": \"Du bist ein Kubernetes-Experte. Analysiere den Alert und die bereitgestellten Cluster-Daten. Antworte auf Deutsch. Gib aus: 1) Wahrscheinliche Ursache, 2) Empfohlene Untersuchungsschritte, 3) Falls ein GitOps-Fix nötig ist: genaue YAML-Änderung die als PR eingereicht werden sollte.\",\n \"messages\": [{\n \"role\": \"user\",\n \"content\": {{ JSON.stringify(\n 'Alert: ' + $('Alert parsen').first().json.alertname +\n ' (Severity: ' + $('Alert parsen').first().json.severity + ')\\n' +\n 'Namespace: ' + $('Alert parsen').first().json.namespace + '\\n' +\n 'Message: ' + $('Alert parsen').first().json.message + '\\n\\n' +\n 'Kubernetes Events:\\n' + JSON.stringify($('MCP: Events holen').first().json.result, null, 2) + '\\n\\n' +\n 'Flux Status:\\n' + JSON.stringify($('MCP: Flux-Status holen').first().json.result, null, 2)\n ) }}\n }]\n}", + "options": { + "timeout": 60000 + } + }, + "id": "analyze-alert-llm", + "name": "LLM: Alert analysieren", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1080, + 560 + ] + }, + { + "parameters": { + "jsCode": "const alert = $('Alert parsen').first().json;\nconst analysis = $('LLM: Alert analysieren').first().json;\nconst analysisText = analysis?.content?.[0]?.text || 'Keine LLM-Analyse verfügbar.';\n\n// Severity-Emoji\nconst emoji = { critical: '🔴', high: '🟠', warning: '🟡', info: '🔵' }[alert.severity] || '⚪';\n\nconst msg = `${emoji} *${alert.alertname}* (${alert.severity.toUpperCase()})\n` +\n `Namespace: \\`${alert.namespace}\\`\\n` +\n (alert.pod ? `Pod: \\`${alert.pod}\\`\\n` : '') +\n `\\n${alert.message}\\n\\n` +\n `*Analyse:*\\n${analysisText.substring(0, 2500)}`;\n\nreturn [{ json: { message: msg, severity: alert.severity, level: alert.level } }];" + }, + "id": "format-alert-message", + "name": "Alert-Nachricht formatieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1300, + 560 + ] + }, + { + "parameters": { + "chatId": "={{ $env.TELEGRAM_CHAT_ID }}", + "text": "={{ $json.message }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "telegram-alert", + "name": "Telegram Alert senden", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1520, + 560 + ] + }, + { + "parameters": { + "jsCode": "// Warning/Info: einfacher Alert ohne LLM-Analyse\nconst alert = $json;\nconst emoji = { warning: '🟡', info: '🔵' }[alert.severity] || '⚪';\n\nconst msg = `${emoji} *${alert.alertname}* (${alert.severity})\n` +\n `Namespace: \\`${alert.namespace}\\`\\n` +\n alert.message;\n\nreturn [{ json: { message: msg } }];" + }, + "id": "format-low-alert", + "name": "Einfacher Alert formatieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 860, + 720 + ] + }, + { + "parameters": { + "chatId": "={{ $env.TELEGRAM_CHAT_ID }}", + "text": "={{ $json.message }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "telegram-low-alert", + "name": "Telegram Low-Alert senden", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1080, + 720 + ] + }, + { + "parameters": { + "jsCode": "const allowed = ($env.TELEGRAM_ALLOWED_IDS || '').split(',').map(s => s.trim()).filter(Boolean);\nconst chatId = String($input.first().json.message?.chat?.id || '');\nconst userId = String($input.first().json.message?.from?.id || '');\nif (allowed.length > 0 && !allowed.includes(chatId) && !allowed.includes(userId)) {\n return [];\n}\nreturn $input.all();" + }, + "id": "chat-id-whitelist", + "name": "Chat-ID Whitelist", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 310, + 300 + ] + } + ], + "connections": { + "Telegram Trigger": { + "main": [ + [ + { + "node": "Chat-ID Whitelist", + "type": "main", + "index": 0 + } + ] + ] + }, + "Nur /jarvis Nachrichten": { + "main": [ + [ + { + "node": "Frage extrahieren", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "Frage extrahieren": { + "main": [ + [ + { + "node": "MCP: Tools auflisten", + "type": "main", + "index": 0 + } + ] + ] + }, + "MCP: Tools auflisten": { + "main": [ + [ + { + "node": "System-Prompt bauen", + "type": "main", + "index": 0 + } + ] + ] + }, + "System-Prompt bauen": { + "main": [ + [ + { + "node": "Ollama (lokal, qwen2.5:14b)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama (lokal, qwen2.5:14b)": { + "main": [ + [ + { + "node": "Ollama fehlgeschlagen?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama fehlgeschlagen?": { + "main": [ + [ + { + "node": "Anthropic API (Fallback)", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Antwort normalisieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "Anthropic API (Fallback)": { + "main": [ + [ + { + "node": "Antwort normalisieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "Antwort normalisieren": { + "main": [ + [ + { + "node": "Telegram Antwort senden", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prometheus Alert Webhook": { + "main": [ + [ + { + "node": "Alert parsen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Alert parsen": { + "main": [ + [ + { + "node": "High oder Critical?", + "type": "main", + "index": 0 + } + ] + ] + }, + "High oder Critical?": { + "main": [ + [ + { + "node": "MCP: Events holen", + "type": "main", + "index": 0 + }, + { + "node": "MCP: Flux-Status holen", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Einfacher Alert formatieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "MCP: Events holen": { + "main": [ + [ + { + "node": "LLM: Alert analysieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "MCP: Flux-Status holen": { + "main": [ + [ + { + "node": "LLM: Alert analysieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "LLM: Alert analysieren": { + "main": [ + [ + { + "node": "Alert-Nachricht formatieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "Alert-Nachricht formatieren": { + "main": [ + [ + { + "node": "Telegram Alert senden", + "type": "main", + "index": 0 + } + ] + ] + }, + "Einfacher Alert formatieren": { + "main": [ + [ + { + "node": "Telegram Low-Alert senden", + "type": "main", + "index": 0 + } + ] + ] + }, + "Chat-ID Whitelist": { + "main": [ + [ + { + "node": "Nur /jarvis Nachrichten", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "saveManualExecutions": true, + "saveExecutionProgress": true + }, + "staticData": null, + "tags": [ + "homelab", + "jarvis", + "mcp" + ], + "pinData": {} +} \ No newline at end of file diff --git a/clusters/homelab/infrastructure/alertmanager-jarvis-receiver.yaml b/clusters/homelab/infrastructure/alertmanager-jarvis-receiver.yaml new file mode 100644 index 0000000..b5c565a --- /dev/null +++ b/clusters/homelab/infrastructure/alertmanager-jarvis-receiver.yaml @@ -0,0 +1,40 @@ +# Ergaenze deine bestehende Alertmanager-Konfiguration um diesen Receiver. +# Einzufuegen in: clusters/homelab/infrastructure/kube-prometheus-stack/ +# (wo du Alertmanager konfigurierst — z.B. values.yaml oder alertmanager-config.yaml) + +# alertmanager.yaml Receiver-Abschnitt: +receivers: + - name: "jarvis-webhook" + webhook_configs: + - url: "http://n8n.n8n.svc.cluster.local:5678/webhook/prometheus-alert" + send_resolved: true + max_alerts: 10 + +route: + receiver: "jarvis-webhook" + group_by: ["alertname", "namespace"] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + + routes: + # Critical: sofort, kein Grouping-Delay + - matchers: + - severity="critical" + receiver: "jarvis-webhook" + group_wait: 0s + repeat_interval: 1h + + # High: 30 Sekunden warten, dann senden + - matchers: + - severity="high" + receiver: "jarvis-webhook" + group_wait: 30s + repeat_interval: 2h + + # Warning/Info: normale Rate, nicht zu oft + - matchers: + - severity=~"warning|info" + receiver: "jarvis-webhook" + group_wait: 2m + repeat_interval: 6h