# Eine Kampagne aus dem eigenen System einplanen

Am Ende legt ein Befehl eine Kampagne für einen Zeitraum in ein Programm, zu den Stunden, die Sie wählen, und ein zweiter nimmt sie wieder heraus. Die Daten können direkt aus Ihrem Marketingkalender oder Ihrem Shopsystem kommen.

> **Typ:** anleitung
> **Für wen:** Entwicklerinnen und Entwickler, die einen Marketingkalender, ein Shopsystem oder ein Kampagnen-Werkzeug anbinden
> **Voraussetzung:** ein Schlüssel `ak_…` der Klasse **Ausspielsteuerung**, ein Programm, das Ihre Displays spielen, und eine Playlist oder Szene mit der Kampagne
> **Ort:** Kommandozeile

## Warum ein Programmpunkt je Tag

Ein Programmpunkt der Art `once` läuft von `startsAt` bis `endsAt` ohne Unterbrechung. Eine Kampagne vom 2. bis 14. November als ein einziger Punkt liefe deshalb durch jede Nacht. Sie würde auch die nächtliche Sendepause verdrängen, denn ein einmaliger Punkt schlägt jeden wiederkehrenden. Das Skript legt stattdessen je Tag einen einmaligen Punkt an, von der Öffnung bis zum Ladenschluss, und gibt allen dieselbe Bezeichnung, damit es sie später findet und entfernt.

| Regel | Wirkung auf die Kampagne |
|---|---|
| Ein einmaliger Punkt schlägt einen wiederkehrenden | Die Kampagne gewinnt in ihren Stunden gegen den Wochenplan. |
| Das kürzere Zeitfenster schlägt das längere | Eine kurze Aktion innerhalb der Kampagnenstunden gewinnt trotzdem. |
| Bei Gleichstand gewinnt der zuletzt angelegte Punkt | Zwei Kampagnen mit denselben Stunden: Die neuere läuft. |
| Außerhalb aller Punkte läuft das Grundprogramm | Nach Ladenschluss kehren die Displays zum Alltag zurück. |

## Schritte

1. **Suchen Sie die Kennungen.** Öffnen Sie Programm und Playlist im Portal: Die Adressen enden mit ihren Kennungen `sch_…` und `pll_…`.

2. **Speichern Sie das Skript:**

   ```ts
   // campaign.ts
   // node campaign.ts add <sch_…> <bezeichnung> <pll_…> <von> <bis> <öffnet> <schließt>
   // node campaign.ts remove <sch_…> <bezeichnung>
   const API = process.env.SZENERA_API;
   const KEY = process.env.SZENERA_API_KEY; // Klasse Ausspielsteuerung
   const TIME_ZONE = 'Europe/Berlin';

   async function call(method: string, path: string, body?: unknown, idempotencyKey?: string) {
     const headers: Record<string, string> = {
       Authorization: `Bearer ${KEY}`,
       'Content-Type': 'application/json',
     };
     if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
     const res = await fetch(`${API}${path}`, {
       method,
       headers,
       body: body === undefined ? undefined : JSON.stringify(body),
     });
     if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`);
     return res.json();
   }

   // Eine Ortszeit an diesem Tag mit dem Versatz der Zeitzone, +01:00 im Winter und +02:00 im Sommer
   function at(day: string, time: string): string {
     const zone = new Intl.DateTimeFormat('en', { timeZone: TIME_ZONE, timeZoneName: 'longOffset' })
       .formatToParts(new Date(`${day}T12:00:00Z`))
       .find((part) => part.type === 'timeZoneName')?.value ?? 'GMT';
     return `${day}T${time}:00${zone === 'GMT' ? '+00:00' : zone.slice(3)}`;
   }

   function nextDay(day: string): string {
     const date = new Date(`${day}T00:00:00Z`);
     date.setUTCDate(date.getUTCDate() + 1);
     return date.toISOString().slice(0, 10);
   }

   const [action, scheduleId, label, playlistId, from, to, opens, closes] = process.argv.slice(2);

   if (action === 'add') {
     for (let day = from; day <= to; day = nextDay(day)) {
       await call(
         'POST',
         `/v1/schedules/${scheduleId}/items`,
         {
           label,
           content: { kind: 'playlist', playlistId },
           recurrence: { kind: 'once', startsAt: at(day, opens), endsAt: at(day, closes) },
         },
         `campaign-${encodeURIComponent(label)}-${day}`,
       );
       console.log(`added ${day} ${opens} to ${closes}`);
     }
   }

   if (action === 'remove') {
     const { schedule } = await call('GET', `/v1/schedules/${scheduleId}`);
     for (const item of schedule.items.filter((entry) => entry.label === label)) {
       await call('DELETE', `/v1/schedules/${scheduleId}/items/${item.id}`);
       console.log(`removed ${item.recurrence.startsAt}`);
     }
   }
   ```

3. **Legen Sie die Kampagne an:**

   ```bash
   node campaign.ts add sch_01KWVAFRM0ZETPAN0000000001 "Herbstaktion" \
     pll_01KWV91ZS0PRGRAMM000000001 2026-11-02 2026-11-14 08:00 20:00
   ```

   ```text
   added 2026-11-02 08:00 to 20:00
   added 2026-11-03 08:00 to 20:00
   …
   ```

   Der Idempotenz-Schlüssel entsteht aus Bezeichnung und Tag. Bricht das Skript mittendrin ab, starten Sie es binnen 24 Stunden erneut: Tage, die schon da sind, entstehen nicht doppelt.

4. **Entfernen Sie sie,** wenn die Kampagne abgesagt oder vorbei ist:

   ```bash
   node campaign.ts remove sch_01KWVAFRM0ZETPAN0000000001 "Herbstaktion"
   ```

Vergangene Punkte laufen nicht mehr, bleiben aber im Programm, bis sie jemand entfernt. Ein Programm hält höchstens 200 Punkte, entfernen Sie eine beendete Kampagne also über ihre Bezeichnung.

## Ergebnis prüfen

- Der Wochenplan des Programms im Portal zeigt je Tag einen Block mit der Bezeichnung der Kampagne, siehe [Den Wochenplan lesen](https://docs.szenera.com/de/programs/read-weekly-plan).
- In den Stunden der Kampagne spielen die Displays mit diesem Programm die Playlist.
- Nach `remove` steht in `GET /v1/schedules/{id}` kein Punkt mit dieser Bezeichnung mehr.

## Wenn etwas nicht klappt

| Symptom | Bedeutung | Lösung |
|---|---|---|
| `422 Schedule.InvalidRecurrence` | Die Zeiten passen nicht, etwa weil das Ende vor dem Beginn liegt. | Öffnung und Ladenschluss als `08:00` und `20:00` angeben. |
| `422 Schedule.ItemLimitReached` | Das Programm hält schon 200 Punkte. | Beendete Kampagnen über ihre Bezeichnung entfernen. |
| `422 Schedule.ContentNotFound` | Die Playlist-Kennung gibt es nicht. | Die Kennung in der Portal-Adresse prüfen. |
| `422 Validation.IdempotencyKeyReused` | Dieselbe Bezeichnung und derselbe Tag kamen binnen 24 Stunden mit anderen Stunden. | Die Kampagne entfernen und unter neuer Bezeichnung wieder anlegen. |
| Die Kampagne erscheint auf einem Display nicht | Dort läuft ein anderes Programm, eine Übersteuerung ist aktiv, oder ein kürzerer Punkt gewinnt. | Siehe [Wie entscheidet ein Display, was es zeigt?](https://docs.szenera.com/de/programs/about-programs). |

## Weiterführendes

- [Eine Notfallmeldung an einem Standort zeigen](/de/recipes/emergency-message)
- [Idempotenz von Anfang an](/de/getting-started/idempotency)
- [Befehle für Ihre Organisation](/de/cli/commands-organization)
