# Bei Offline eine Nachricht in Teams oder Slack

Am Ende schreibt ein kleiner Empfänger eine Nachricht in Ihren Team-Chat, sobald ein Display offline geht, und noch eine, wenn es zurück ist. Niemand muss die Flotte offen halten, um zu merken, dass der Fernseher im Schaufenster schwarz ist.

> **Typ:** anleitung
> **Für wen:** Entwicklerinnen und Entwickler, die Displays für ein Team betreuen, das in Slack oder Microsoft Teams arbeitet
> **Voraussetzung:** ein Paket mit Webhooks (ab Premium), die Rolle Inhaber für den Endpunkt, ein Schlüssel `ak_…` der Klasse **Nur Lesen** für die Namen der Displays und ein Server mit öffentlicher https-Adresse
> **Ort:** Ihr eigener Server und **Einstellungen › Für Entwickler › Webhooks**

## So funktioniert es

Szenera kann nicht direkt in einen Chat schreiben: Der Chat erwartet sein eigenes Nachrichtenformat, und Szenera signiert jede Zustellung. Dazwischen sitzt ein eigener Empfänger:

1. Szenera schickt das Ereignis `fleet.player.status-changed` mit `playerId`, `from` und `to`.
2. Der Empfänger prüft die Signatur, übergeht Wiederholungen und liest den Namen des Displays über die API.
3. Er schickt eine Nachricht an den eingehenden Webhook Ihres Chats.

Als offline gilt ein Display, das sich fünf Minuten nicht gemeldet hat. Davor ist es kurz `stale`. Der Empfänger meldet nur den Schritt nach `offline` und die Rückkehr nach `online`, damit ein kurzes Wackeln den Chat nicht flutet.

## Schritte

1. **Legen Sie im Chat den eingehenden Webhook an.**
   - **Slack:** Legen Sie eine App mit **Incoming Webhooks** für den Kanal an. Die Adresse beginnt mit `https://hooks.slack.com/`.
   - **Microsoft Teams:** Öffnen Sie im Kanal die Workflows und legen Sie aus einer Vorlage einen Ablauf an, der eingehende Webhook-Anfragen in den Kanal schreibt. Der Ablauf gibt Ihnen eine Adresse, die Adaptive Cards annimmt.

2. **Speichern Sie den Empfänger** auf Ihrem Server und starten Sie ihn mit den vier Werten:

   ```ts
   // offline-alert.ts: node offline-alert.ts
   import { createServer } from 'node:http';
   import { createHmac, timingSafeEqual } from 'node:crypto';

   const SECRET = process.env.WEBHOOK_SECRET ?? ''; // whsec_…
   const API = process.env.SZENERA_API;
   const KEY = process.env.SZENERA_API_KEY; // Klasse Nur Lesen
   const CHAT = process.env.CHAT_WEBHOOK_URL ?? ''; // Slack oder Teams
   const seen = new Set<string>();

   function isFromSignage(rawBody: string, header: string): boolean {
     const expected = createHmac('sha256', SECRET).update(rawBody).digest('base64url');
     const a = Buffer.from(expected);
     const b = Buffer.from(header);
     return a.length === b.length && timingSafeEqual(a, b);
   }

   async function displayName(playerId: string): Promise<string> {
     const res = await fetch(`${API}/v1/players/${playerId}`, {
       headers: { Authorization: `Bearer ${KEY}` },
     });
     return res.ok ? (await res.json()).player.name : playerId;
   }

   function chatMessage(text: string): unknown {
     if (CHAT.startsWith('https://hooks.slack.com/')) return { text };
     return {
       type: 'message',
       attachments: [
         {
           contentType: 'application/vnd.microsoft.card.adaptive',
           content: { type: 'AdaptiveCard', version: '1.4', body: [{ type: 'TextBlock', text, wrap: true }] },
         },
       ],
     };
   }

   createServer((req, res) => {
     const chunks: Buffer[] = [];
     req.on('data', (chunk) => chunks.push(chunk));
     req.on('end', async () => {
       const raw = Buffer.concat(chunks).toString('utf8');
       if (!isFromSignage(raw, String(req.headers['x-signature'] ?? ''))) {
         res.writeHead(401).end();
         return;
       }
       res.writeHead(204).end(); // Binnen 10 Sekunden antworten, danach arbeiten

       const deliveryId = String(req.headers['x-delivery-id']);
       if (seen.has(deliveryId) || req.headers['x-event-type'] !== 'fleet.player.status-changed') return;
       seen.add(deliveryId);

       const { playerId, from, to } = JSON.parse(raw);
       let text = '';
       if (to === 'offline') text = `${await displayName(playerId)} is offline.`;
       if (to === 'online' && from === 'offline') text = `${await displayName(playerId)} is back online.`;
       if (!text) return;

       await fetch(CHAT, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify(chatMessage(text)),
       });
     });
   }).listen(8080);
   ```

   ```bash
   export WEBHOOK_SECRET="whsec_…" CHAT_WEBHOOK_URL="https://hooks.slack.com/services/…"
   node offline-alert.ts
   ```

   Stellen Sie den Empfänger hinter Ihre https-Adresse, zum Beispiel `https://alerts.example.com/szenera`. Die Menge `seen` lebt im Speicher. Startet der Empfänger neu, kann eine Wiederholung eine zweite Nachricht auslösen, was hier nicht schadet. Für alles, was nur einmal geschehen darf, speichern Sie die Kennungen.

3. **Legen Sie den Endpunkt an,** im Portal unter **Einstellungen › Für Entwickler › Webhooks** mit **Endpunkt anlegen**. Tragen Sie die Adresse unter **Endpunkt-URL** ein, wählen Sie unter **Abonnierte Ereignisse** `fleet.player.status-changed` und klicken Sie auf **Anlegen**. Kopieren Sie das Secret aus **Signatur-Secret — nur jetzt sichtbar** nach `WEBHOOK_SECRET` und starten Sie den Empfänger neu.

   Die Route `POST /v1/webhooks` geht auch, aber nur mit dem Zugangs-Token einer Anmeldung als Inhaber. Ein API-Schlüssel kann die Webhook-Rechte nicht tragen, siehe [Einen Webhook-Endpunkt anlegen](/de/webhooks/create-endpoint).

4. **Probieren Sie es aus.** Ziehen Sie den Netzwerkstecker eines Displays oder schließen Sie den Browser-Tab eines Web-Displays. Nach etwa fünf Minuten kommt die Nachricht im Chat an. Verbinden Sie es wieder, folgt die zweite.

## Ergebnis prüfen

- Im **Zustell-Log** des Endpunkts zeigt jede Zustellung **Zugestellt** mit `HTTP 204`.
- Der Chat zeigt den Namen des Displays, nicht seine Kennung. Steht dort die Kennung, kann der Schlüssel das Display nicht lesen.
- Eine Zustellung, die Sie im Zustell-Log mit **Erneut zustellen** wiederholen, löst keine zweite Nachricht aus.

## Wenn etwas nicht klappt

| Symptom | Bedeutung | Lösung |
|---|---|---|
| Das Zustell-Log zeigt `HTTP 401` | Die Signatur passt nicht: falsches Secret, oder ein Proxy verändert den Rumpf. | Das Secret neu kopieren und sicherstellen, dass nichts den Rumpf vor der Prüfung umformt. |
| Das Zustell-Log zeigt **Zugestellt**, aber der Chat bleibt leer | Der Chat weist die Nachricht ab. | Die Antwort des Chats protokollieren. Slack antwortet bei falschem Format mit `invalid_payload`, ein Teams-Ablauf zeigt den gescheiterten Lauf in seinem Verlauf. |
| Der Chat zeigt die Kennung statt des Namens | `GET /v1/players/{id}` scheitert: Der Schlüssel fehlt, ist abgelaufen oder hat kein `fleet.player.read`. | Einen Schlüssel der Klasse **Nur Lesen** verwenden. |
| Nach fünf Minuten kommt keine Zustellung | Der Endpunkt hat das Ereignis nicht abonniert, oder das Display war schon offline. | **Abonnierte Ereignisse** prüfen, siehe [Wenn keine Zustellung ankommt](/de/webhooks/troubleshooting). |

## Weiterführendes

- [Die Signatur einer Zustellung prüfen](/de/webhooks/verify-signature)
- [Was gesendet wird und wie wiederholt wird](/de/webhooks/delivery-and-retries)
- [Ereignistypen: Inhalte, Programme, Geräte, Apps](/de/webhooks/event-types-content-devices)
