> [← 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]].