Marcs Linie: kein Plugin-System bauen, aber dafür sorgen, dass der Kalender wirklich ein Extra bleibt. Das ist als Liste festgehalten, weil gute Absicht in sechs Monaten niemanden bindet: niemand importiert das Paket, es importiert nur auth/acl/pkg, alles unter einem Routenpräfix, eigene Tabellen mit cal_-Präfix, abschaltbar, keine Änderung an bestehenden Paketen außer einer Zeile in main.go. Die erste Regel wird ein Test, der die Importe durchgeht – dann fällt ein Grenzübertritt beim Bauen auf und nicht Jahre später. Migrationen bleiben in der Kernnummerierung. Eine zweite Versionstabelle wäre für ein echtes Plugin richtig, für eine persönliche Cloud ist sie Aufwand ohne Gegenwert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
265 lines
13 KiB
Markdown
265 lines
13 KiB
Markdown
# Kalender in Aether – Plan
|
||
|
||
Stand 02.09.2026. Ausgangspunkt: Radicale ist für einen einfachen selbst
|
||
gehosteten Kalender überraschend umständlich (Konfigurationsdateien, dünne
|
||
Doku, keine OIDC-Anbindung).
|
||
|
||
## Festgelegt (02.09.2026)
|
||
|
||
- **Mehrere Kalender** von Anfang an. Nicht weil es heute drei sein müssen,
|
||
sondern weil es jetzt eine Spalte ist und später ein Umbau.
|
||
- **Aufgaben (VTODO)** werden von Anfang an gespeichert und über CalDAV
|
||
angeboten; die Ansicht in Aether kommt später. Muss vorher feststehen, weil
|
||
jeder Kalender dem Client meldet, was er kann – nachträglich richten die
|
||
Clients ihre Konten neu ein.
|
||
- **Ändern statt neu erzeugen**, sobald unsere eigene Oberfläche schreibt.
|
||
- Anhänge sind kein Ziel. Falls doch, dann als Verweis in Aethers Dateien –
|
||
nie als eingebettete Datei in der `.ics`.
|
||
- Erinnerungen weckt die App auf dem Telefon. Browser-Benachrichtigungen in
|
||
der Weboberfläche: nett für später, kein Drang.
|
||
|
||
## Entscheidung: ein Modul in Aether, kein eigener Dienst
|
||
|
||
Der Grund ist genau das, was bei Radicale gefehlt hat.
|
||
|
||
**Ein CalDAV-Client kann kein OIDC.** DAVx5, Thunderbird und die Kalender-App
|
||
melden sich mit Benutzername und Passwort an – ein Anmeldefenster im Browser
|
||
gibt es dort nicht. Wer OIDC für CalDAV sucht, sucht etwas, das es so nicht
|
||
gibt. Die richtige Antwort sind **App-Passwörter**, und die hat Aether schon:
|
||
Das WebDAV unter `/dav/` läuft bereits darüber.
|
||
|
||
Damit ist die Rechnung einfach:
|
||
|
||
| | Radicale o. ä. | Modul in Aether |
|
||
|---|---|---|
|
||
| Anmeldung Oberfläche | eigenes Konzept, kein OIDC | PocketID, schon da |
|
||
| Anmeldung DAVx5 | eigene Nutzerdatei | App-Passwörter, schon da |
|
||
| Reverse Proxy, TLS | zusätzlicher Eintrag | schon da |
|
||
| Sicherung | zweiter Datenbestand | dieselbe Datenbank |
|
||
| Oberfläche | im Grunde keine | die, die wir bauen |
|
||
|
||
## Was die Bibliothek abnimmt
|
||
|
||
`github.com/emersion/go-webdav` (v0.7.0, Oktober 2025) bringt einen
|
||
CalDAV-Handler mit. Nachgesehen, nicht angenommen:
|
||
|
||
- Der Handler beantwortet `PROPFIND`, `OPTIONS` und `REPORT` selbst und liest
|
||
`calendar-query` wie `calendar-multiget`.
|
||
- `caldav.Filter()` wendet die Filter der Anfrage auf die Termine an.
|
||
- **Der Zeitraumfilter löst Wiederholungen auf:** `matchCompTimeRange` ruft
|
||
`comp.RecurrenceSet(loc)`, und go-ical stützt sich dafür auf
|
||
`teambition/rrule-go`. Genau der Teil, vor dem ich gewarnt hatte – „jeden
|
||
zweiten Dienstag, über die Zeitumstellung hinweg" – steckt in der Bibliothek
|
||
und nicht in unserem Code.
|
||
|
||
Zu erfüllen ist eine Schnittstelle mit **neun Methoden** plus dem
|
||
Nutzer-Principal:
|
||
|
||
CalendarHomeSetPath · CreateCalendar · ListCalendars · GetCalendar
|
||
GetCalendarObject · ListCalendarObjects · QueryCalendarObjects
|
||
PutCalendarObject · DeleteCalendarObject
|
||
|
||
Das ist Speicherarbeit, keine Protokollarbeit. Die Einschätzung „zwei Wochen
|
||
bis DAVx5 läuft, danach ein halbes Jahr Kleinkram" gilt für einen Server von
|
||
Grund auf – hier nicht.
|
||
|
||
## Datenmodell
|
||
|
||
Zwei Tabellen, mehr braucht es nicht:
|
||
|
||
- `calendars` – Besitzer, Name, Farbe, Zeitzone, `ctag` (ändert sich bei jeder
|
||
Änderung, daran erkennt der Client, dass es etwas Neues gibt).
|
||
- `calendar_objects` – Kalender, UID, **das `.ics` roh und unverändert**,
|
||
`etag`, dazu abgeleitet `dtstart`/`dtend` und ein Kennzeichen, ob eine
|
||
Wiederholung dranhängt.
|
||
|
||
**Die wichtigste Regel: das `.ics` byte-genau speichern, wie der Client es
|
||
geschickt hat.** Nicht zerlegen und neu zusammensetzen – sonst gehen
|
||
Eigenschaften verloren, die wir nicht kennen (Anhänge, Teilnehmerstatus,
|
||
Herstellerfelder), und beim nächsten Abgleich fehlt etwas, das vorher da war.
|
||
Die abgeleiteten Spalten sind nur zum Vorsortieren da.
|
||
|
||
## Phasen
|
||
|
||
| | Inhalt | Aufwand |
|
||
|---|---|---|
|
||
| **1** | CalDAV lesend und schreibend, ein Kalender, Anmeldung über App-Passwörter, Auffindpfade (`/.well-known/caldav`, `current-user-principal`, `calendar-home-set`). Ziel: DAVx5 gleicht ab. | 1–2 Abende |
|
||
| **2** | Mehrere Kalender, Farbe, Umbenennen, Löschen. Export als `.ics`, Import einer bestehenden Datei. | 1 Abend |
|
||
| **3** | Weboberfläche in Aether: Monat, Woche, Liste; Termin anlegen, ändern, löschen. Der Teil, der Spaß macht – und dessen Fehler man **sieht**. | 2–3 Abende |
|
||
| **4** | `sync-collection` statt Vollabgleich, Aufgabenansicht, Teilen mit anderen Nutzern, öffentlicher Abo-Link, Kalender in der HoneyOS-Leiste. | offen |
|
||
| **5** | Kontakte (CardDAV). Dieselbe Maschinerie, dieselbe Bibliothek – und **einfacher**, weil es dort keine Wiederholungen gibt. | später entscheiden |
|
||
|
||
## Teilen – drei Bedeutungen, zwei davon gehen
|
||
|
||
„Geteilter Kalender" meint je nach Person etwas anderes:
|
||
|
||
1. **Derselbe Kalender auf mehreren Geräten.** Das ist der Sinn der Sache und
|
||
ab Phase 1 da.
|
||
2. **Ein Kalender, den ein anderer Nutzer sieht** – lesend oder schreibend, wie
|
||
das Teilen in Nextcloud. Geht, und es ist keine Protokollarbeit: Wir kennen
|
||
unsere Nutzer und unsere Kalender, also ist es eine Tabelle
|
||
(`kalender × nutzer × recht`) und eine Bedingung in `ListCalendars`. Der
|
||
Client sieht danach schlicht einen Kalender mehr in seiner Liste. **Phase 4.**
|
||
3. **Ein Kalender an der Adresse eines Links**, den jemand ohne Konto
|
||
abonnieren kann – nur lesend, wie ein Freigabelink für Dateien. Klein, und
|
||
Aether hat die Freigabelinks schon. **Phase 4.**
|
||
|
||
Was **nicht** kommt: Einladungen per E-Mail (iMIP/iTIP) und Frei/Belegt-Abfragen
|
||
über Serviergrenzen hinweg. Das ist der Teil, an dem Groupware groß und
|
||
kompliziert wird – und der einzige, der wirklich wehtut.
|
||
|
||
## Felder – was ein Termin mitbringt
|
||
|
||
Alles aus RFC 5545, und wir müssen dafür nichts „unterstützen": Wir speichern
|
||
die Datei unverändert, also überleben auch Felder, die wir nicht kennen.
|
||
|
||
RRULE/EXDATE Wiederholung · DESCRIPTION Notiz · LOCATION Ort
|
||
VALARM Erinnerung · CATEGORIES · CLASS privat/öffentlich
|
||
ATTENDEE Teilnehmer (gespeichert, aber niemand wird eingeladen)
|
||
DTSTART ohne Uhrzeit = ganztägig
|
||
|
||
**Die Regel, die Daten rettet:** Sobald unsere Oberfläche einen Termin ändert,
|
||
wird die vorhandene Datei eingelesen, die angefassten Felder ersetzt und der
|
||
Rest unberührt zurückgeschrieben. Nie neu erzeugen. Sonst verschiebt man am
|
||
Rechner einen Termin um eine Stunde und die Erinnerung, die das Telefon
|
||
angelegt hatte, ist weg – und das fällt erst Wochen später auf.
|
||
|
||
## Clients, gegen die geprüft wird
|
||
|
||
| Client | Prüft |
|
||
|---|---|
|
||
| **DAVx5 → Samsung Kalender** | der Hauptweg. DAVx5 schreibt in Androids Kalenderspeicher, Samsung Kalender liest daraus. In der App unter „Kalender verwalten" einmal sichtbar schalten, und DAVx5 von der Akkuoptimierung ausnehmen. |
|
||
| **Tasks.org** | Aufgaben. Androids Kalenderspeicher kennt keine – ohne so eine App bietet DAVx5 die Aufgabenlisten gar nicht erst an, und **Samsung Kalender zeigt sie nie**. |
|
||
| **Thunderbird** | der gutmütige Prüfstein am Rechner, Termine und Aufgaben nebeneinander. |
|
||
| **Vivaldi** | Marcs Browser bringt einen CalDAV-Kalender mit. Wie eigenwillig er ist, wissen wir noch nicht. |
|
||
| **HoneyOS** | braucht **kein** CalDAV: Kachel und Leiste fragen Aethers eigene JSON-Schnittstelle. CalDAV ist für fremde Programme. |
|
||
|
||
## Standards und Vorbild
|
||
|
||
RFC 5545 das .ics-Format
|
||
RFC 4791 CalDAV (PUT, REPORT, calendar-query)
|
||
RFC 6764 /.well-known/caldav – damit reicht dem Client der Hostname
|
||
RFC 5397 current-user-principal: „wer bin ich, wo liegen meine Kalender"
|
||
RFC 6578 sync-collection: nur Änderungen holen
|
||
RFC 6352 CardDAV, falls Kontakte dazukommen
|
||
|
||
Wo der Standard Spielraum lässt, halten wir es wie **SabreDAV**: Nextcloud und
|
||
Baïkal stehen beide darauf, das ist also das Verhalten, an das die Clients
|
||
gewöhnt sind. Radicale ist zum Nachlesen gut – kurzer Code –, aber es ist nicht
|
||
die Fassung, gegen die getestet wurde.
|
||
|
||
## Kein Plugin – aber ausklinkbar gebaut
|
||
|
||
Der Projektplan legt es selbst fest (Abschnitt 8): *„ERST sauberer Core.
|
||
Extension-Points erst definieren, wenn der Core stabil ist. Kernfeatures
|
||
bleiben erstklassig, nicht als Plugin."* Ein Plugin-System zu bauen, um darauf
|
||
den Kalender zu setzen, drehte diese Reihenfolge um – und ein Kalender wäre
|
||
sein erster und einziger Nutzer, also entstünde die Schnittstelle nach genau
|
||
einem Fall.
|
||
|
||
Stattdessen so gebaut, dass er später herauszulösen ist:
|
||
|
||
- eigenes Paket `internal/calendar`, eigene Migrationen, eigene Routen
|
||
- angeschlossen in **einer** Zeile in `main.go`
|
||
- greift nach außen nur auf `auth` und `acl` zu, nicht in andere Pakete hinein
|
||
- abschaltbar: kein Kalender aktiv → keine Routen, kein Menüpunkt
|
||
|
||
Damit ist er der natürliche erste Kandidat, wenn es das Plugin-System einmal
|
||
gibt – ohne dass wir es vorher erfinden.
|
||
|
||
### Die Grenzen, an denen sich das entscheidet
|
||
|
||
Kein System, sechs Regeln:
|
||
|
||
1. **Niemand importiert `internal/calendar`.** Kein `files`, kein `sharing`,
|
||
kein `admin`. Der Kalender ist Blatt im Baum, nie Ast.
|
||
2. **Der Kalender importiert nur `auth`, `acl` und `pkg/…`** – dieselben
|
||
Hilfsmittel wie jeder andere Bereich, aber keine fremde Fachlogik.
|
||
3. **Alles unter einem Präfix**: `/api/v1/calendar/…` für die Oberfläche,
|
||
`/caldav/…` für die Geräte. Keine Route irgendwo dazwischen.
|
||
4. **Eigene Tabellen, erkennbar benannt** (`cal_calendars`, `cal_objects`,
|
||
`cal_shares`, `cal_view`). Keine Spalte in einer bestehenden Tabelle.
|
||
5. **Abschaltbar**: aus heißt keine Routen, kein Menüpunkt, keine Migration,
|
||
die etwas anderes anfasst.
|
||
6. **Keine Änderung an bestehenden Paketen**, außer der einen Zeile in
|
||
`main.go` und dem Eintrag in der Ansichtsliste des Frontends.
|
||
|
||
Regel 1 lässt sich festhalten statt hoffen: ein kleiner Test, der die Importe
|
||
durchgeht und fehlschlägt, sobald ein anderes Paket den Kalender zieht. Damit
|
||
merkt man die Grenzüberschreitung beim Bauen und nicht in zwei Jahren beim
|
||
Herauslösen.
|
||
|
||
**Migrationen** bleiben in der fortlaufenden Nummerierung des Kerns
|
||
(`000021_calendar…`). Eine zweite Versionstabelle wäre sauberer für ein echtes
|
||
Plugin, aber für eine persönliche Cloud ist sie Aufwand ohne Gegenwert – und
|
||
die Dateinamen sagen ohnehin, wozu sie gehören.
|
||
|
||
## Teamkalender über Gruppen
|
||
|
||
Aether kennt bereits Gruppen samt Gruppenordnern, und `acl.Access` trägt einen
|
||
**Prinzipal** statt eines Eigentümers – ein Konto *oder* eine Gruppe
|
||
(`IsGroup`). Genau das nehmen wir:
|
||
|
||
| Zweck | Weg |
|
||
|---|---|
|
||
| **Teamkalender** | Kalender gehört einer Gruppe. Wer in der Gruppe ist, hat ihn – ohne einzelne Freigaben. Angelegt in den Admin-Optionen, wie Gruppenordner. |
|
||
| **Persönlich teilen** | Freigabe an ein einzelnes Konto, mit Recht `read` oder `write`. In der Oberfläche ein Schalter „darf auch schreiben". |
|
||
|
||
Beides läuft über dieselbe Prüfung wie bei Dateien: `resolveCalendar` nach dem
|
||
Vorbild von `files.resolveFile`. Kein zweites Rechtekonzept.
|
||
|
||
## Oberfläche
|
||
|
||
**Mehrere Kalender gleichzeitig** – das ist der Normalfall, nicht die Ausnahme.
|
||
Die Ansicht legt sie übereinander; jeder Termin trägt die Farbe seines
|
||
Kalenders.
|
||
|
||
**Sichtbarkeit mit einem Klick**, wie in Google Kalender und der Samsung-App:
|
||
runder, in der Kalenderfarbe gefüllter Haken in der Liste. Klick = an, Klick =
|
||
aus. **Serverseitig je Nutzer gespeichert**, damit es am Rechner und am Handy
|
||
gleich aussieht – dafür eine Tabelle:
|
||
|
||
kalender_ansicht nutzer_id, kalender_id, sichtbar, reihenfolge, farbe_eigen
|
||
|
||
Das `farbe_eigen` ist geschenkt, wenn die Zeile ohnehin existiert: Ein geteilter
|
||
Kalender darf bei dir grün und beim anderen blau sein – so macht es Google auch.
|
||
|
||
**Wichtig:** Diese Einstellung gilt für *unsere* Oberfläche. Was das Telefon
|
||
anzeigt, entscheiden DAVx5 und Samsung Kalender selbst; darauf haben wir keinen
|
||
Zugriff.
|
||
|
||
## Farben
|
||
|
||
Zwei verschiedene Dinge, die man leicht verwechselt:
|
||
|
||
- **Farbe des Kalenders** – das, was du in der Samsung-App siehst. Im Protokoll
|
||
`calendar-color` aus Apples Namensraum. Steht in keinem RFC, ist aber
|
||
de-facto-Standard: Nextcloud, Thunderbird und DAVx5 können es. **Geht.**
|
||
- **Farbe eines einzelnen Termins** – `COLOR` aus RFC 7986. In unserer eigenen
|
||
Oberfläche jederzeit möglich; wie weit DAVx5 und Samsung Kalender das
|
||
weiterreichen, ist offen und muss am Gerät geprüft werden.
|
||
|
||
**Emojis im Kalendernamen** sind kein Protokollthema – der Name ist Text.
|
||
„🏠 Haushalt" kommt überall an.
|
||
|
||
## Die Notausfahrt
|
||
|
||
Ein Kalender, der lautlos einen Termin verschluckt, ist schlimmer als einer,
|
||
der ausfällt. Deshalb:
|
||
|
||
- **Export als `.ics` je Kalender ist Teil von Phase 2, nicht später.** Solange
|
||
du deine Termine jederzeit herausbekommst, ist keine Entscheidung endgültig.
|
||
- Auf Wunsch zusätzlich als Verzeichnis mit einer Datei je Termin – das ist der
|
||
Aufbau, den Radicale liest. Umziehen wäre dann ein Kopiervorgang.
|
||
- Tests mit echten Anfragen, die DAVx5 stellt (mitgeschnitten, nicht erfunden),
|
||
und eine Prüfliste in `TESTING.md` fürs Telefon.
|
||
|
||
## Offene Fragen
|
||
|
||
1. Wie viele Kalender – einer, oder getrennt nach Privat/Arbeit/Geburtstage?
|
||
2. Aufgaben mit dazu (VTODO), oder reine Termine?
|
||
3. Der Kalender in der HoneyOS-Leiste: nur der nächste Termin, oder eine
|
||
eigene Kachel?
|
||
4. Kontakte: ernsthaft geplant oder erst mal offenlassen? Es ändert nichts an
|
||
Phase 1–3, nur an der Benennung der Pakete.
|