Jelajahi Sumber

reworking webhook functionality

Co-authored-by: Copilot <copilot@github.com>
Medowar 1 bulan lalu
induk
melakukan
9efeed3a66
9 mengubah file dengan 261 tambahan dan 137 penghapusan
  1. 69 27
      admin/index.php
  2. 19 22
      data/config.json
  3. 19 2
      data/state.json
  4. 0 2
      docs/API.md
  5. 41 13
      docs/CONFIG.md
  6. 3 21
      openapi.yaml
  7. 110 24
      src/AlertService.php
  8. 0 22
      src/ConfigRepository.php
  9. 0 4
      src/MonitorService.php

+ 69 - 27
admin/index.php

@@ -312,6 +312,14 @@ function renderAdmin(array $config, string $rawConfigText, ?string $message, str
 
             const countChildren = (node, selector) => node.querySelectorAll(selector).length;
 
+            const toggleTelegramFields = (row) => {
+              const typeSelect = row.querySelector('select[name*="[type]"]');
+              const telegramFields = row.querySelector('.telegram-fields');
+              if (typeSelect && telegramFields) {
+                telegramFields.style.display = typeSelect.value === 'telegram' ? '' : 'none';
+              }
+            };
+
             document.addEventListener('click', (event) => {
               const addType = event.target.getAttribute('data-add');
               const removeType = event.target.getAttribute('data-remove');
@@ -348,6 +356,18 @@ function renderAdmin(array $config, string $rawConfigText, ?string $message, str
                 }
               }
             });
+
+            document.addEventListener('change', (event) => {
+              if (event.target.matches('select[name*="[type]"]')) {
+                const row = event.target.closest('[data-webhook-row]');
+                if (row) {
+                  toggleTelegramFields(row);
+                }
+              }
+            });
+
+            // Initialize existing webhook rows
+            document.querySelectorAll('[data-webhook-row]').forEach(toggleTelegramFields);
         </script>
     </body>
     </html>
@@ -358,6 +378,9 @@ function renderWebhookRow(int|string $index, array $webhook): string
 {
     $headers = $webhook['headers'] ?? [];
     $headerJson = json_encode($headers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+    $type = $webhook['type'] ?? 'generic';
+    $botToken = $webhook['bot_token'] ?? '';
+    $chatId = $webhook['chat_id'] ?? '';
 
     ob_start();
     ?>
@@ -368,16 +391,23 @@ function renderWebhookRow(int|string $index, array $webhook): string
         </div>
         <div class="field-grid">
             <label>
-                ID
-                <input type="text" name="webhooks[<?= $index ?>][id]" value="<?= htmlspecialchars((string) ($webhook['id'] ?? ''), ENT_QUOTES) ?>">
+                Label
+                <input type="text" name="webhooks[<?= $index ?>][label]" value="<?= htmlspecialchars((string) ($webhook['label'] ?? ''), ENT_QUOTES) ?>" placeholder="z.B. lager-webhook">
             </label>
             <label>
                 Name
                 <input type="text" name="webhooks[<?= $index ?>][name]" value="<?= htmlspecialchars((string) ($webhook['name'] ?? ''), ENT_QUOTES) ?>">
             </label>
+            <label>
+                Typ
+                <select name="webhooks[<?= $index ?>][type]">
+                    <option value="generic" <?= $type === 'generic' ? 'selected' : '' ?>>Generic</option>
+                    <option value="telegram" <?= $type === 'telegram' ? 'selected' : '' ?>>Telegram</option>
+                </select>
+            </label>
             <label>
                 URL
-                <input type="url" name="webhooks[<?= $index ?>][url]" value="<?= htmlspecialchars((string) ($webhook['url'] ?? ''), ENT_QUOTES) ?>">
+                <input type="url" name="webhooks[<?= $index ?>][url]" value="<?= htmlspecialchars((string) ($webhook['url'] ?? ''), ENT_QUOTES) ?>" placeholder="https://example.com/webhook">
             </label>
             <label>
                 Aktiv
@@ -387,8 +417,22 @@ function renderWebhookRow(int|string $index, array $webhook): string
                 </select>
             </label>
         </div>
+        
+        <div class="telegram-fields" style="<?= $type === 'telegram' ? '' : 'display:none;' ?>">
+            <div class="field-grid">
+                <label>
+                    Bot Token (nur für Telegram)
+                    <input type="text" name="webhooks[<?= $index ?>][bot_token]" value="<?= htmlspecialchars((string) $botToken, ENT_QUOTES) ?>" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
+                </label>
+                <label>
+                    Chat ID (nur für Telegram)
+                    <input type="text" name="webhooks[<?= $index ?>][chat_id]" value="<?= htmlspecialchars((string) $chatId, ENT_QUOTES) ?>" placeholder="-100123456789">
+                </label>
+            </div>
+        </div>
+        
         <label>
-            Header als JSON-Objekt
+            Header als JSON-Objekt (für Generic)
             <textarea name="webhooks[<?= $index ?>][headers_json]"><?= htmlspecialchars((string) $headerJson, ENT_QUOTES) ?></textarea>
         </label>
     </div>
@@ -407,8 +451,8 @@ function renderEmailRow(int|string $index, array $email): string
         </div>
         <div class="field-grid">
             <label>
-                ID
-                <input type="text" name="emails[<?= $index ?>][id]" value="<?= htmlspecialchars((string) ($email['id'] ?? ''), ENT_QUOTES) ?>">
+                Label
+                <input type="text" name="emails[<?= $index ?>][label]" value="<?= htmlspecialchars((string) ($email['label'] ?? ''), ENT_QUOTES) ?>" placeholder="z.B. lager-team">
             </label>
             <label>
                 Name
@@ -472,9 +516,6 @@ function renderMachineRow(int|string $machineIndex, array $machine): string
 
 function renderSlotRow(int|string $machineIndex, int|string $slotIndex, array $slot): string
 {
-    $webhookIds = implode(',', $slot['webhook_ids'] ?? []);
-    $emailIds = implode(',', $slot['email_ids'] ?? []);
-
     ob_start();
     ?>
     <div class="slot-editor" data-slot-row>
@@ -508,17 +549,9 @@ function renderSlotRow(int|string $machineIndex, int|string $slotIndex, array $s
                 <input type="number" step="0.01" name="machines[<?= $machineIndex ?>][slots][<?= $slotIndex ?>][distance_per_unit]" value="<?= htmlspecialchars((string) ($slot['distance_per_unit'] ?? ''), ENT_QUOTES) ?>">
             </label>
             <label>
-                Alarm unter Bestand
+                Alarm unter Bestand(Anzahl)
                 <input type="number" step="1" min="0" name="machines[<?= $machineIndex ?>][slots][<?= $slotIndex ?>][alert_below_units]" value="<?= htmlspecialchars((string) ($slot['alert_below_units'] ?? ''), ENT_QUOTES) ?>">
             </label>
-            <label>
-                Webhook-IDs
-                <input type="text" name="machines[<?= $machineIndex ?>][slots][<?= $slotIndex ?>][webhook_ids]" value="<?= htmlspecialchars((string) $webhookIds, ENT_QUOTES) ?>" placeholder="ops,lager">
-            </label>
-            <label>
-                Email-IDs
-                <input type="text" name="machines[<?= $machineIndex ?>][slots][<?= $slotIndex ?>][email_ids]" value="<?= htmlspecialchars((string) $emailIds, ENT_QUOTES) ?>" placeholder="lager,service">
-            </label>
         </div>
     </div>
     <?php
@@ -529,20 +562,31 @@ function normalizeWebhooks(array $rows): array
 {
     $items = [];
     foreach ($rows as $row) {
-        $id = trim((string) ($row['id'] ?? ''));
+        $label = trim((string) ($row['label'] ?? ''));
         $url = trim((string) ($row['url'] ?? ''));
-        if ($id === '' && $url === '') {
+        if ($label === '' && $url === '') {
             continue;
         }
 
         $headers = json_decode((string) ($row['headers_json'] ?? '{}'), true);
-        $items[] = [
-            'id' => $id,
+        $type = trim((string) ($row['type'] ?? 'generic'));
+        
+        $item = [
+            'label' => $label,
             'name' => trim((string) ($row['name'] ?? '')),
+            'type' => $type,
             'url' => $url,
             'enabled' => (string) ($row['enabled'] ?? '1') === '1',
             'headers' => is_array($headers) ? $headers : [],
         ];
+        
+        // Add Telegram-specific fields
+        if ($type === 'telegram') {
+            $item['bot_token'] = trim((string) ($row['bot_token'] ?? ''));
+            $item['chat_id'] = trim((string) ($row['chat_id'] ?? ''));
+        }
+        
+        $items[] = $item;
     }
 
     return array_values($items);
@@ -552,14 +596,14 @@ function normalizeEmails(array $rows): array
 {
     $items = [];
     foreach ($rows as $row) {
-        $id = trim((string) ($row['id'] ?? ''));
+        $label = trim((string) ($row['label'] ?? ''));
         $address = trim((string) ($row['address'] ?? ''));
-        if ($id === '' && $address === '') {
+        if ($label === '' && $address === '') {
             continue;
         }
 
         $items[] = [
-            'id' => $id,
+            'label' => $label,
             'name' => trim((string) ($row['name'] ?? '')),
             'address' => $address,
             'enabled' => (string) ($row['enabled'] ?? '1') === '1',
@@ -594,8 +638,6 @@ function normalizeMachines(array $rows): array
                 'empty_distance_mm' => (float) ($slot['empty_distance_mm'] ?? 0),
                 'distance_per_unit' => max(0.01, (float) ($slot['distance_per_unit'] ?? 1)),
                 'alert_below_units' => max(0, (int) ($slot['alert_below_units'] ?? 0)),
-                'webhook_ids' => parseIdList((string) ($slot['webhook_ids'] ?? '')),
-                'email_ids' => parseIdList((string) ($slot['email_ids'] ?? '')),
             ];
         }
 

+ 19 - 22
data/config.json

@@ -4,7 +4,7 @@
         "timezone": "Europe/Berlin",
         "dashboard_refresh_seconds": 15,
         "default_from_email": "automat@med0.de",
-        "base_path": "/automat/"
+        "base_path": "/automat"
     },
     "api": {
         "bearer_token": "demo-esp32-token"
@@ -16,18 +16,29 @@
     "alerts": {
         "webhooks": [
             {
-                "id": "lager-webhook",
+                "label": "lager-webhook",
                 "name": "Lager Webhook",
-                "url": "https://example.com/hooks/lager",
+                "type": "generic",
+                "url": "https://tool.medowar.de/webhook-debug/webhook.php?key=3c282522940cb045",
                 "enabled": false,
                 "headers": {
                     "X-App-Token": "replace-me"
                 }
+            },
+            {
+                "label": "telegram-alerts",
+                "name": "Telegram Alerts",
+                "type": "telegram",
+                "url": "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/sendMessage",
+                "enabled": false,
+                "headers": [],
+                "bot_token": "<YOUR_BOT_TOKEN>",
+                "chat_id": "<YOUR_CHAT_ID>"
             }
         ],
         "emails": [
             {
-                "id": "lager-team",
+                "label": "lager-team",
                 "name": "Lager Team",
                 "address": "lager@example.com",
                 "enabled": true
@@ -47,13 +58,7 @@
                     "full_distance_mm": 80,
                     "empty_distance_mm": 360,
                     "distance_per_unit": 40,
-                    "alert_below_units": 2,
-                    "webhook_ids": [
-                        "lager-webhook"
-                    ],
-                    "email_ids": [
-                        "lager-team"
-                    ]
+                    "alert_below_units": 2
                 },
                 {
                     "sensor_id": "fach-a2",
@@ -62,11 +67,7 @@
                     "full_distance_mm": 75,
                     "empty_distance_mm": 355,
                     "distance_per_unit": 35,
-                    "alert_below_units": 3,
-                    "webhook_ids": [],
-                    "email_ids": [
-                        "lager-team"
-                    ]
+                    "alert_below_units": 3
                 }
             ]
         },
@@ -82,13 +83,9 @@
                     "full_distance_mm": 90,
                     "empty_distance_mm": 390,
                     "distance_per_unit": 43,
-                    "alert_below_units": 2,
-                    "webhook_ids": [
-                        "lager-webhook"
-                    ],
-                    "email_ids": []
+                    "alert_below_units": 2
                 }
             ]
         }
     ]
-}
+}

+ 19 - 2
data/state.json

@@ -1,3 +1,20 @@
 {
-    "slots": {}
-}
+    "slots": {
+        "automat-lobby::fach-a1": {
+            "distance_mm": 184,
+            "fill_ratio": 0.6285714285714286,
+            "fill_percent": 63,
+            "units_estimated": 4,
+            "max_units": 7,
+            "alert_below_units": 2,
+            "state": "ok",
+            "machine_id": "automat-lobby",
+            "machine_name": "Lobby Automat",
+            "sensor_id": "fach-a1",
+            "slot_label": "A1",
+            "product_name": "Cola 0,5l",
+            "measured_at": "2026-05-02T00:35:31+00:00",
+            "updated_at": "2026-05-02T00:35:31+00:00"
+        }
+    }
+}

+ 0 - 2
docs/API.md

@@ -239,8 +239,6 @@ GET https://example.com/monitor/api/v1/status.php
 - `measured_at`
 - `updated_at`
 - `alert_below_units`
-- `webhook_ids`
-- `email_ids`
 
 ### Wichtige Felder in `alerts[]`
 

+ 41 - 13
docs/CONFIG.md

@@ -195,9 +195,7 @@ Beispiel:
   "full_distance_mm": 80,
   "empty_distance_mm": 360,
   "distance_per_unit": 40,
-  "alert_below_units": 2,
-  "webhook_ids": ["lager-webhook"],
-  "email_ids": ["lager-team"]
+  "alert_below_units": 2
 }
 ```
 
@@ -217,10 +215,8 @@ Felder:
   - Differenz im Messwert pro Flasche oder Einheit
 - `alert_below_units`
   - kritische Schwelle
-- `webhook_ids`
-  - Liste von Webhook-IDs aus `alerts.webhooks`
-- `email_ids`
-  - Liste von Email-IDs aus `alerts.emails`
+
+**Hinweis**: Webhook- und Email-Alarme werden global über die `alerts.webhooks` und `alerts.emails` Konfiguration gesteuert. Alle aktivierten Webhooks und Emails erhalten Alarmbenachrichtigungen.
 
 ## Kalibrierung eines Fachs
 
@@ -232,18 +228,50 @@ Für eine brauchbare Bestandsschätzung sollten pro Fach mindestens diese Werte
 4. Die durchschnittliche Differenz pro Flasche als `distance_per_unit` eintragen.
 5. Einen passenden Schwellwert für `alert_below_units` festlegen.
 
-## Referenzen zwischen Bereichen
+## Webhook-Typen
+
+Die Konfiguration unterstützt verschiedene Webhook-Typen:
 
-Slots referenzieren Alarmziele nicht direkt über URLs oder Email-Adressen, sondern über IDs:
+### Generic
+Standard-Webhook, der ein JSON-Payload mit allen relevanten Informationen sendet.
+
+```json
+{
+  "label": "lager-webhook",
+  "name": "Lager Webhook",
+  "type": "generic",
+  "url": "https://example.com/hooks/lager",
+  "enabled": true,
+  "headers": {
+    "X-App-Token": "your-token"
+  }
+}
+```
+
+### Telegram
+Spezieller Webhook für Telegram-Benachrichtigungen. Sendet formatierte Nachrichten mit Markdown.
+
+```json
+{
+  "label": "telegram-alerts",
+  "name": "Telegram Alerts",
+  "type": "telegram",
+  "url": "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/sendMessage",
+  "enabled": true,
+  "bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
+  "chat_id": "-100123456789"
+}
+```
+
+## Referenzen zwischen Bereichen
 
-- `webhook_ids` referenziert `alerts.webhooks[].id`
-- `email_ids` referenziert `alerts.emails[].id`
+Alarmziele werden global über `alerts.webhooks` und `alerts.emails` konfiguriert. Alle aktivierten Webhooks und Emails erhalten Benachrichtigungen bei Alarmen.
 
-Das macht die Konfiguration wartbarer, weil ein Alarmziel zentral geändert werden kann.
+Das macht die Konfiguration wartbarer, weil Alarmziele zentral geändert werden können.
 
 ## Tipps für produktive Nutzung
 
-- IDs nur aus stabilen ASCII-Zeichen vergeben
+- Labels nur aus stabilen ASCII-Zeichen vergeben
 - `machine_id` und `sensor_id` niemals nachträglich leichtfertig ändern
 - für jeden neuen Sensor zuerst Kalibrierwerte eintragen
 - Testalarme mit einem absichtlich kritischen Messwert prüfen

+ 3 - 21
openapi.yaml

@@ -216,11 +216,7 @@ paths:
                             state: ok
                             measured_at: '2026-04-15T19:20:00+00:00'
                             updated_at: '2026-04-15T19:20:02+00:00'
-                            alert_below_units: 2
-                            webhook_ids:
-                              - lager-webhook
-                            email_ids:
-                              - lager-team
+                          alert_below_units: 2
                     alerts:
                       - id: alert_680004979d8512.07480974
                         created_at: '2026-04-15T19:20:02+00:00'
@@ -240,11 +236,11 @@ paths:
                           measured_at: '2026-04-15T19:19:59+00:00'
                         deliveries:
                           webhooks:
-                            - id: lager-webhook
+                            - label: lager-webhook
                               success: false
                               message: Webhook nicht gefunden oder deaktiviert.
                           emails:
-                            - id: lager-team
+                            - label: lager-team
                               success: true
                               message: Email versendet.
         '405':
@@ -446,8 +442,6 @@ components:
         - measured_at
         - updated_at
         - alert_below_units
-        - webhook_ids
-        - email_ids
       properties:
         machine_id:
           type: string
@@ -507,18 +501,6 @@ components:
           type: integer
           minimum: 0
           example: 2
-        webhook_ids:
-          type: array
-          items:
-            type: string
-          example:
-            - lager-webhook
-        email_ids:
-          type: array
-          items:
-            type: string
-          example:
-            - lager-team
     SlotState:
       type: string
       enum:

+ 110 - 24
src/AlertService.php

@@ -78,34 +78,29 @@ final class AlertService
     private function sendWebhooks(array $slot, array $payload): array
     {
         $results = [];
+        $config = $this->configRepository->getConfig();
+        $webhooks = $config['alerts']['webhooks'] ?? [];
 
-        foreach ($slot['webhook_ids'] ?? [] as $webhookId) {
-            $webhook = $this->configRepository->getWebhookById((string) $webhookId);
-            if ($webhook === null || !($webhook['enabled'] ?? false) || empty($webhook['url'])) {
+        foreach ($webhooks as $webhook) {
+            $label = $webhook['label'] ?? '';
+            
+            if (!($webhook['enabled'] ?? false) || empty($webhook['url'])) {
                 $results[] = [
-                    'id' => $webhookId,
+                    'label' => $label,
                     'success' => false,
-                    'message' => 'Webhook nicht gefunden oder deaktiviert.',
+                    'message' => 'Webhook deaktiviert oder URL fehlt.',
                 ];
                 continue;
             }
 
-            $headers = [
-                'Content-Type: application/json',
-            ];
-
-            foreach (($webhook['headers'] ?? []) as $headerName => $headerValue) {
-                if ($headerName === '' || $headerValue === '') {
-                    continue;
-                }
-                $headers[] = $headerName . ': ' . $headerValue;
-            }
+            $formattedPayload = $this->formatWebhookPayload($webhook, $payload);
+            $headers = $this->buildWebhookHeaders($webhook);
 
             $context = stream_context_create([
                 'http' => [
                     'method' => 'POST',
                     'header' => implode("\r\n", $headers),
-                    'content' => json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
+                    'content' => json_encode($formattedPayload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                     'timeout' => 5,
                     'ignore_errors' => true,
                 ],
@@ -114,7 +109,7 @@ final class AlertService
             $response = @file_get_contents((string) $webhook['url'], false, $context);
             $success = $response !== false;
             $results[] = [
-                'id' => $webhookId,
+                'label' => $label,
                 'success' => $success,
                 'message' => $success ? 'Webhook gesendet.' : 'Webhook fehlgeschlagen.',
             ];
@@ -123,20 +118,111 @@ final class AlertService
         return $results;
     }
 
+    private function findWebhookByLabel(array $webhooks, string $label): ?array
+    {
+        foreach ($webhooks as $webhook) {
+            if (($webhook['label'] ?? '') === $label) {
+                return $webhook;
+            }
+        }
+        return null;
+    }
+
+    private function formatWebhookPayload(array $webhook, array $payload): array
+    {
+        $type = $webhook['type'] ?? 'generic';
+        
+        if ($type === 'telegram') {
+            return $this->formatTelegramPayload($webhook, $payload);
+        }
+        
+        return $this->formatGenericPayload($webhook, $payload);
+    }
+
+    private function formatGenericPayload(array $webhook, array $payload): array
+    {
+        return [
+            'event' => $payload['event'],
+            'machine' => [
+                'id' => $payload['machine_id'],
+                'name' => $payload['machine_name'],
+            ],
+            'slot' => [
+                'label' => $payload['slot_label'],
+                'sensor_id' => $payload['sensor_id'],
+                'product_name' => $payload['product_name'],
+            ],
+            'state' => [
+                'current' => $payload['state'],
+                'previous' => $payload['previous_state'],
+                'units_estimated' => $payload['units_estimated'],
+                'max_units' => $payload['max_units'],
+                'fill_percent' => $payload['fill_percent'],
+                'distance_mm' => $payload['distance_mm'],
+            ],
+            'measured_at' => $payload['measured_at'],
+        ];
+    }
+
+    private function formatTelegramPayload(array $webhook, array $payload): array
+    {
+        $botToken = $webhook['bot_token'] ?? '';
+        $chatId = $webhook['chat_id'] ?? '';
+        
+        $eventEmoji = $payload['event'] === 'critical' ? '🚨' : '✅';
+        $eventText = $payload['event'] === 'critical' ? 'ALARM' : 'Entwarnung';
+        
+        $message = $eventEmoji . " *" . $eventText . "*\n\n";
+        $message .= "*Automat:* " . $payload['machine_name'] . "\n";
+        $message .= "*Fach:* " . $payload['slot_label'] . " (" . $payload['product_name'] . ")\n";
+        $message .= "*Bestand:* " . $payload['units_estimated'] . " von " . $payload['max_units'] . " (" . $payload['fill_percent'] . "%)\n";
+        $message .= "*Messwert:* " . $payload['distance_mm'] . " mm\n";
+        $message .= "*Zeitpunkt:* " . $payload['measured_at'] . "\n";
+        
+        return [
+            'chat_id' => $chatId,
+            'text' => $message,
+            'parse_mode' => 'Markdown',
+        ];
+    }
+
+    private function buildWebhookHeaders(array $webhook): array
+    {
+        $headers = [];
+        $type = $webhook['type'] ?? 'generic';
+        
+        if ($type === 'telegram') {
+            $headers[] = 'Content-Type: application/json';
+        } else {
+            $headers[] = 'Content-Type: application/json';
+        }
+
+        foreach (($webhook['headers'] ?? []) as $headerName => $headerValue) {
+            if ($headerName === '' || $headerValue === '') {
+                continue;
+            }
+            $headers[] = $headerName . ': ' . $headerValue;
+        }
+
+        return $headers;
+    }
+
     private function sendEmails(array $slot, array $payload): array
     {
         $config = $this->configRepository->getConfig();
         $appName = $config['app']['name'] ?? 'Getränkeautomat Monitor';
         $from = $config['app']['default_from_email'] ?? 'monitor@example.local';
         $results = [];
+        $emails = $config['alerts']['emails'] ?? [];
 
-        foreach ($slot['email_ids'] ?? [] as $emailId) {
-            $recipient = $this->configRepository->getEmailById((string) $emailId);
-            if ($recipient === null || !($recipient['enabled'] ?? false) || empty($recipient['address'])) {
+        foreach ($emails as $email) {
+            $label = $email['label'] ?? '';
+            
+            if (!($email['enabled'] ?? false) || empty($email['address'])) {
                 $results[] = [
-                    'id' => $emailId,
+                    'label' => $label,
                     'success' => false,
-                    'message' => 'Email-Empfänger nicht gefunden oder deaktiviert.',
+                    'message' => 'Email-Empfänger deaktiviert oder Adresse fehlt.',
                 ];
                 continue;
             }
@@ -166,14 +252,14 @@ final class AlertService
             ];
 
             $success = @mail(
-                (string) $recipient['address'],
+                (string) $email['address'],
                 $subject,
                 $body,
                 implode("\r\n", $headers)
             );
 
             $results[] = [
-                'id' => $emailId,
+                'label' => $label,
                 'success' => $success,
                 'message' => $success ? 'Email versendet.' : 'Email-Versand fehlgeschlagen.',
             ];

+ 0 - 22
src/ConfigRepository.php

@@ -76,26 +76,4 @@ final class ConfigRepository
 
         return null;
     }
-
-    public function getWebhookById(string $id): ?array
-    {
-        foreach ($this->getConfig()['alerts']['webhooks'] ?? [] as $webhook) {
-            if (($webhook['id'] ?? '') === $id) {
-                return $webhook;
-            }
-        }
-
-        return null;
-    }
-
-    public function getEmailById(string $id): ?array
-    {
-        foreach ($this->getConfig()['alerts']['emails'] ?? [] as $email) {
-            if (($email['id'] ?? '') === $id) {
-                return $email;
-            }
-        }
-
-        return null;
-    }
 }

+ 0 - 4
src/MonitorService.php

@@ -95,8 +95,6 @@ final class MonitorService
             'product_name' => $slot['product_name'] ?? '',
             'measured_at' => $measuredAt,
             'updated_at' => gmdate(DATE_ATOM),
-            'webhook_ids' => array_values($slot['webhook_ids'] ?? []),
-            'email_ids' => array_values($slot['email_ids'] ?? []),
         ]);
 
         $this->stateRepository->setSlotState($machineId, $sensorId, $slotState);
@@ -137,8 +135,6 @@ final class MonitorService
             'measured_at' => null,
             'updated_at' => null,
             'alert_below_units' => (int) ($slot['alert_below_units'] ?? 0),
-            'webhook_ids' => array_values($slot['webhook_ids'] ?? []),
-            'email_ids' => array_values($slot['email_ids'] ?? []),
         ];
 
         if ($existingState === null) {