> [← open.SERVICE v2](<Überblick.md>) # Wiederholungen und Termine Ein Wartungsplan trägt eine **Cadence**. Zwei Arten stehen zur Verfügung: - **`rrule`** — eine Wiederholungsregel nach RFC 5545 (`FREQ=WEEKLY;INTERVAL=2`), gespeichert als blanker RRULE-String plus `dtstart` und optional `until`. Sie deckt Wochentage, Monatstage und Einmaltermine (`COUNT=1`) gleichermaßen ab. - **`manual`** — kein Rhythmus; die Durchführung entsteht von Hand. `meter` und `event` sind im Schema vorgesehen, aber nicht belegt. Sensorgesteuerte Wartung läuft nicht über eine eigene Cadence, sondern über [[Developer/open.SERVICE/Alarme als Wartungsauslöser|Alarme]]. ## Termine sind virtuell Termine werden aus der Regel **gerechnet**, wann immer sie gebraucht werden, und stehen nicht in der Datenbank. Daraus folgt: **wer die Regel ändert, ändert auch die Vergangenheit.** Ein Plan, dessen Frequenz von wöchentlich auf monatlich gestellt wird, hatte rückwirkend andere Termine. Dasselbe gilt für ein verschobenes `DTSTART`. „Fällig" ist deshalb kein Zustand, den man abhaken oder zählen kann, sondern ein Urteil, das bei jedem Zugriff neu gefällt wird. Die Occurrence-Mathematik liegt in **`@opendash/schedule`** und arbeitet auf Primitiven (`OccurrenceRule`, `OccurrenceException`, `ToleranceConfig`). Frontend und serverseitiger Takt rechnen darüber dieselben Termine — ein verschobener Termin wird nicht an einem Tag angezeigt und an einem anderen gemeldet. Die wichtigsten Einstiegspunkte: `occurrencesInRange`, `occurrenceInstancesInRange` (wendet Ausnahmen an), `nextOccurrences`, `nextDue`, `isPaused` / `pauseAdjustedFrom`, `upsertException` / `removeException` und `occurrenceStatus`. > [!tip] Bereiche mit einem Durchgang holen > `occurrencesInRange` holt einen Zeitraum mit **einem** `between()`-Aufruf. Wer > stattdessen `after(cursor)` in einer Schleife aufruft, macht die Kosten > quadratisch zum Alter des Plans — bei hundert Plänen über ein Jahr ist das der > Unterschied zwischen Millisekunden und über einer Sekunde. ## Ausnahmen einzelner Termine Verschobene und übersprungene Einzeltermine liegen als `cadenceExceptions` neben der Regel — nicht als `EXDATE`/`RDATE`: ```ts interface CadenceException { date: string; // ISO-Zeitstempel des ursprünglichen Termins moveTo?: string; // ISO-Zeitstempel des Ersatztermins skip?: boolean; // true, wenn der Termin entfällt reason?: string; } ``` Der Grund für das eigene Modell: eine Ausnahme trägt hier **Begründung und Historie** — wer hat wann warum verschoben. `EXDATE` kann nur „findet nicht statt" sagen, und bei einer Wartung ist genau der Rest der Teil, der geprüft wird. > [!warning] Ausnahmen hängen am Zeitstempel des ursprünglichen Termins > Alles, was die Serie verschiebt — ein neues `DTSTART`, eine nachträglich > gesetzte Zeitzone —, entwertet damit die vorhandenen Ausnahmen. Sie müssen dann > mitgezogen werden. ## Was von RFC 5545 wo ankommt „Unterstützt" bedeutet auf vier Ebenen etwas anderes: | Ebene | Kann | | ------------- | ----------------------------------------------------- | | Speicher | jede Zeichenkette — keine Grenze | | Rechnung | praktisch die ganze Norm | | Editor | die gängigen Fälle **plus verlustfreie Durchleitung** | | Kalender-Sync | die Teilmenge, die Outlook selbst kennt | **Die Rechnung ist maßgeblich.** Der Editor vereinfacht eine Regel nicht, nur weil er sie nicht in Bedienelemente übersetzen kann: was er nicht modelliert, reist unverändert mit und wird beim Speichern wieder angehängt. Eine importierte Jahresserie behält damit ihren Monat, und `BYMONTHDAY=-1` bleibt `BYMONTHDAY=-1`. Die Kurzbeschreibung macht das sichtbar — „Monatlich (am ersten Dienstag) **+ 2 weitere Regel(n)**". | RRULE-Teil | Rechnung | Editor | Outlook | Anmerkung | | ---------------------------------- | :------: | ---------------------- | :-----: | ---------------------------------------------------- | | `FREQ=DAILY/WEEKLY/MONTHLY/YEARLY` | ✅ | ✅ Auswahl | ✅ | | | `FREQ=HOURLY` | ✅ | ✅ Auswahl | ❌ | Outlook hat kein stündliches Muster | | `FREQ=MINUTELY` / `SECONDLY` | ✅ | ◐ durchgeleitet | ❌ | nicht in der Auswahl, bleibt aber erhalten | | `INTERVAL` | ✅ | ✅ Zahl | ✅ | | | `COUNT` | ✅ | ✅ (`1` = einmalig) | ✅ | andere Werte reisen unverändert mit | | `UNTIL` | ✅ | ✅ als `cadence.until` | ✅ | das Feld gewinnt gegen ein `UNTIL` **im** String | | `BYDAY=MO,FR` (wöchentlich) | ✅ | ✅ Wochentagsknöpfe | ✅ | | | `BYDAY=+1TU` (erster Dienstag) | ✅ | ✅ „am Wochentag" | ✅ | | | `BYDAY=-1FR` (letzter Freitag) | ✅ | ✅ „letzter" | ✅ | | | `BYSETPOS` (letzter Werktag) | ✅ | ✅ mehrere Wochentage | ❌ | Outlook kennt nur *einen* Wochentag je Position | | `BYMONTHDAY=15` | ✅ | ✅ Zahl | ✅ | | | `BYMONTHDAY=-1` (letzter Tag) | ✅ | ◐ durchgeleitet | ❌ | rechnet richtig, ist im Editor aber kein Knopf | | `BYMONTH=3` | ✅ | ◐ durchgeleitet | ✅ | | | `BYYEARDAY`, `BYWEEKNO` | ✅ | ◐ durchgeleitet | ❌ | | | `BYHOUR`, `BYMINUTE`, `BYSECOND` | ✅ | ◐ durchgeleitet | ❌ | die Uhrzeit kommt normalerweise aus `dtstart` | | `WKST` | ✅ | ◐ durchgeleitet | ✅ | relevant bei `INTERVAL>1` und `FREQ=WEEKLY` | | `EXDATE` / `RDATE` | ❌ | ❌ eigenes Modell | ❌ | stattdessen `cadenceExceptions` | | mehrere `RRULE`-Zeilen je Termin | ❌ | ❌ | ❌ | ein Plan = eine Regel; für zwei Rhythmen zwei Pläne | ✅ voll · ◐ rechnet und bleibt erhalten, aber ohne eigene Bedienoberfläche · ❌ nicht abgebildet ### Die häufigen Muster ``` FREQ=MONTHLY;BYDAY=+1TU erster Dienstag im Monat FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 letzter Werktag im Monat FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1 erster Werktag im Monat FREQ=MONTHLY;INTERVAL=3;BYDAY=+2WE zweiter Mittwoch im Quartal ``` Ein einzelner Wochentag wird als `BYDAY=+1TU` geschrieben und nicht als `BYDAY=TU;BYSETPOS=1`: beide rechnen gleich, aber nur die erste Form überträgt der Kalender-Sync als echte Serie. ## Fallstricke **`BYMONTHDAY=31` überspringt Monate.** Die Norm verlangt das — es gibt keinen 31. im Februar. Wer „letzter Tag des Monats" meint, braucht `BYMONTHDAY=-1`. **`FREQ=YEARLY;BYDAY=+1TU` ist nicht „erster Dienstag im Januar"**, sondern der erste Dienstag des Jahres. Deshalb bietet der Editor die Position nur für monatliche Regeln an; jährliche Regeln ohne `BYMONTH` leiten Monat und Tag aus `dtstart` ab. **`UNTIL` gibt es zweimal.** Das Ende einer Serie gehört in `cadence.until`. Ein `UNTIL` im Regelstring bleibt erhalten, verliert aber gegen das Feld, sobald dieses gesetzt ist. **Zeitzonen** haben ein eigenes Kapitel: [[Developer/open.SERVICE/Zeitzonen|Zeitzonen]].