Files
Aether/kalender-plan.md
marcandClaude Opus 5 c8c4095299 docs: IDEAS.md angelegt; Abo-Link und Aufgabenansicht vertagt
Beide haben einen guten Grund, aber keinen Anlass -- und eine Funktion ohne
Anlass zu bauen ist der Weg, auf dem eine Anwendung unuebersichtlich wird.
Die Ueberlegungen stehen samt dem, was daran nicht trivial ist, in IDEAS.md
statt verloren zu gehen.

Beim Abo-Link sind das drei Dinge: nur lesend ohne Ausnahme; ein Schalter
"nur Belegt/Frei", weil ein Abo sonst jeden Termintitel mitgibt; und dass
ein Abo regelmaessig von einer Maschine abgerufen wird -- Zaehler und
Fehlversuchssperre der Datei-Freigabelinks passen darauf nicht.

Dazu ein Widerspruch, der beim Nachsehen auffiel: "Kalender in der
HoneyOS-Leiste" steht seit dem ersten Entwurf in Phase 4, aber HoneyOS hat
sich am 29.08.2026 ausdruecklich gegen Widgets entschieden, die Daten aus
Aether oder InkNote holen. Der technische Grund dafuer ist inzwischen weg
(gleiche Domain), der inhaltliche nicht. Das gehoert entschieden, nicht
gebaut -- deshalb im Plan vermerkt statt umgesetzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 13:50:37 +02:00

22 KiB
Raw Permalink Blame History

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 Stand
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. ✅
2 Mehrere Kalender, Farbe, Umbenennen, Löschen. Export als .ics, Import einer bestehenden Datei. ✅
3 Weboberfläche in Aether: Monat, Woche, Liste; Termin anlegen, ändern, löschen. Der Teil, der Spaß macht – und dessen Fehler man sieht. ✅
4 sync-collection ✅, Teilen mit anderen Nutzern ✅. Aufgabenansicht und Abo-Link vertagt → IDEAS.md. Kalender in der HoneyOS-Leiste: widerspricht einer Entscheidung in HoneyOS, siehe unten. im Wesentlichen erledigt
5 Kontakte (CardDAV). Dieselbe Maschinerie, dieselbe Bibliothek – und einfacher, weil es dort keine Wiederholungen gibt. später entscheiden

Über die Phasen hinaus dazugekommen, weil es sich beim Prüfen als nötig erwies:

  • Termin umhängen in einen anderen Kalender, unter Erhalt der UID – per Ziehen in der Oberfläche, am Handy mit langem Drücken.
  • Einzelnen Termin aus einer Reihe streichen (EXDATE), statt nur die ganze Reihe löschen zu können.
  • getctag und calendar-color, die go-webdav nicht kennt: aus der Anfrage genommen und der Antwort selbst wieder angehängt (internal/calendar/propfind.go).
  • Kalender vom Client aus verwalten – MKCALENDAR, erweitertes MKCOL, PROPPATCH, DELETE einer Sammlung (internal/calendar/collections.go). Dafür Migration 23: Bei MKCALENDAR sucht der Client die Adresse aus, die seither in path_segment steht. Leer = es gilt weiterhin die UUID, damit eingerichtete Geräte ihre Kalender unter der gewohnten Adresse behalten.
  • Zwei Selbsttests, einer je Zugangsweg: scripts/kalender-selbsttest.py (JSON, 36 Prüfungen) und scripts/caldav-selbsttest.py (CalDAV, 62). Beide Wege führen auf dieselben Daten, aber über völlig verschiedenen Code – ein grüner Lauf des einen sagt nichts über den anderen.

Abgleich nur der Änderungen (Phase 4, erledigt 12.09.2026)

Bis hierher lief jeder Durchgang so: Der Client fragt das ctag ab, sieht es verändert und holt daraufhin die volle Terminliste. Eine verschobene Besprechung kostet damit denselben Verkehr wie ein Neuanfang – bei einem Jahr Termine, jedes Mal, alle fünfzehn Minuten. Bezahlt wird das in Akku und Datenvolumen des Telefons.

sync-collection (RFC 6578) dreht das um: Der Client sagt „ich bin auf Stand 41" und bekommt zurück, was seither geschah, plus ein neues Token.

Kein zweiter Zähler. Das ctag ist bereits eine Zahl je Kalender, die bei jedem Schreiben um eins steigt – also genau die Sequenz, die hier gebraucht wird. Jedes Objekt merkt sich in sync_seq den Stand, bei dem es zuletzt angefasst wurde. Ein eigener Sync-Zähler daneben wäre eine zweite Wahrheit über denselben Sachverhalt gewesen, und zwei Wahrheiten laufen auseinander.

Grabsteine. Eine gelöschte Zeile kann nicht mehr erzählen, dass es sie gab. cal_tombstones hält deshalb UID und Stand jedes gelöschten Termins fest – sonst bliebe er auf jedem Gerät stehen, das während des Löschens nicht verbunden war. Das ist der teuerste Fehler dieser Art, weil er unsichtbar ist.

Grabsteine stehen 180 Tage; beim Räumen wandert sync_floor mit. Ein Token darunter lässt sich nicht mehr vollständig beantworten, und dann ist die ehrliche Antwort 403 <valid-sync-token/> – der Client gleicht daraufhin voll ab. Eine lückenhafte Antwort wäre schlimmer als eine Absage: Der Client hielte sie für den ganzen Unterschied.

Drei Feinheiten, die beim Bauen auffielen:

  • Beim ersten Abgleich keine Grabsteine. Wer den Kalender noch gar nicht kennt, braucht keine Löschmeldungen für Termine, die er nie gesehen hat.
  • Auferstehung. Wird eine gelöschte UID neu angelegt, muss ihr Grabstein weg – sonst bekäme der Client die Datei und ihre Todesmeldung im selben Zug.
  • Anführungszeichen am ETag stehen außerhalb der Maskierung. xml.EscapeText macht daraus sonst &#34;; gültiges XML, aber genug Clients vergleichen ETags als rohe Zeichenkette und sähen bei jedem Durchgang eine Änderung.

Dazu zwei Eigenschaften, die go-webdav gar nicht kennt und die jetzt denselben Weg gehen wie getctag und calendar-color (propfind.go): sync-token und supported-report-set. Ohne die zweite probiert DAVx5 sync-collection nicht einmal aus.

Geprüft in scripts/caldav-selbsttest.py (86 statt 62 Prüfungen), darunter der Fall, auf den es ankommt: ohne Änderung kommt nichts zurück.

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. ✅ 12.09.2026.

    Gebaut wurde es nicht als eigene Tabelle, wie hier ursprünglich geplant. acl_entries beantwortet für Ordner und Dateien längst genau diese Frage – wer darf was, direkt oder über eine Gruppe. Ein zweites Rechtesystem daneben hieße zwei Antworten auf dieselbe Frage, mit eigener Gruppenauflösung, eigenem Widerruf, eigenem Prüfpfad; die Lücke dazwischen fällt erst auf, wenn jemand sieht, was er nicht sehen soll. Migration 25 hängt deshalb nur eine dritte Ressourcenspalte an (resource_calendar_id).

    Der Gewinn dabei: Freigeben an eine Gruppe kostete keine Zeile extra.

    Drei Entscheidungen, die nicht offensichtlich waren:

    • Ein fremder Kalender steht immer unter seiner UUID, nie unter dem Pfadsegment des Eigentümers. Das Segment hat sich der Eigentümer ausgesucht („…/arbeit/"), und zwei Leute dürfen denselben Namen wählen – im Kalenderbestand des Empfängers lägen dann zwei Kalender auf derselben Adresse (Access.Ref).
    • Alle Wege laufen durch RequireCalendar. CalDAV und die Weboberfläche sind zwei getrennte Zugänge zu denselben Daten; ein Recht, das nur einer von beiden prüft, ist kein Recht. Im CalDAV-Teil genügte dafür, resolveCalendar umzustellen – dort kommt jede Operation vorbei.
    • Wer ein Recht geliehen hat, darf es nicht weiterverleihen. Auch mit Schreibrecht bleiben Umbenennen, Löschen und Freigeben beim Eigentümer.

    Geprüft in scripts/kalender-teilen-selbsttest.py (41 Prüfungen), und zwar jede über beide Zugänge. Der Test beginnt damit, dass der Empfänger vorher nichts sieht – ohne diesen Anfang beweist der Rest nichts.

  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. Vertagt am 12.09.2026 – kein Anlass, solange es nur ein Konto gibt. Die Überlegung samt der drei Dinge, die daran nicht trivial sind, steht in IDEAS.md.

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.

Kalender in der HoneyOS-Leiste – steht im Widerspruch

Der Punkt steht seit dem ersten Entwurf in Phase 4. Beim Nachsehen am 12.09.2026 stellte sich heraus: HoneyOS hat sich am 29.08.2026 dagegen entschieden. In honey-os/IDEAS.md, Abschnitt „Bewusst nicht geplant":

Keine Widgets, die Daten aus Aether oder InkNote holen (gestrichen 29.08.2026). […] der Weg über den HoneyBar-Proxy zeigt HoneyOS in den Apps, statt die Apps in HoneyOS nachzubauen.

Dazu die Grenze, die dort gezogen wurde: „Nichts, was in einer App lebt, darf hier ein zweites Zuhause bekommen."

Zwei Pläne, zwei Richtungen – das gehört entschieden, nicht gebaut. Zur Vollständigkeit, was sich seither geändert hat: Der technische Grund ist weg. Seit dem 29.08.2026 liegen Aether und HoneyOS unter derselben Domain, die Sitzung wird also mitgeschickt; die Leiste läuft ohnehin in Aethers Seite und könnte den Kalender dort gleichursprünglich abfragen, ohne CORS und ohne einen zweiten Schlüssel. Übrig bleibt der inhaltliche Grund, und der ist der eigentliche: Soll der Schreibtisch Inhalte der Apps zeigen oder nicht?

Solange das offen ist, bleibt der Punkt hier stehen und wird nicht gebaut.

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.

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. Geschrieben wird sie als CSS3-Name (tomato), nicht als Hex-Wert – so verlangt es RFC 7986, und so liest DAVx5 sie. Der genaue Wert steht zusätzlich in X-AETHER-COLOR, damit die eigene Palette verlustfrei zurückkommt.

Am Gerät geprüft (06.09.2026): Die Kalenderfarbe kommt an – aber erst, nachdem in DAVx5 einmal Kalenderliste aktualisieren angetippt wurde. Beim normalen Abgleich fragt DAVx5 calendar-color gar nicht ab; ein Mitschnitt der Anfragen hat das gezeigt. Wer das nicht weiß, sucht den Fehler auf der Serverseite, wo keiner ist.

Emojis im Kalendernamen sind kein Protokollthema – der Name ist Text. „🏠 Haushalt" kommt überall an.

Migrationen: geprüft, was bei einem Fehler wirklich passiert

Nicht angenommen, sondern nachgestellt: eine absichtlich kaputte Migration in einer Wegwerf-Datenbank laufen lassen. Ergebnis, damit hier niemand rät:

1/u ok
error: relation "cal_calendars" already exists
→ schema_migrations steht auf  version=2, dirty=true
→ jeder weitere Start:  "Dirty database version 2. Fix and force version."

Der zweite Teil ist der eigentliche Befund: Die eine kaputte Datei bleibt nicht lokal begrenzt. main.go bricht bei einem Migrationsfehler mit log.Fatal ab – das gilt für jede Migration, nicht nur für die des Kalenders, und stand schon vor diesem Vorhaben so im Code. Eine hängende Migration verhindert also den Start der ganzen Cloud, bis jemand von Hand migrate force <version> ausführt. Genau das darf einem „Extra" nicht passieren – deshalb drei Sicherungen, bevor die erste Kalendermigration überhaupt geschrieben wird:

  1. Nur CREATE TABLE IF NOT EXISTS, nie ALTER an Fremdem. Idempotent: Selbst wenn eine Datei mitten in einem Mehr-Tabellen-Schritt abbricht, ist ein erneuter Lauf nach migrate force ein Nichts-tun statt eines zweiten Fehlers.
  2. Erst auf der Dev-Instanz, hoch und runter, mit denselben 40 bestehenden Migrationen davor – nicht in einer leeren Testdatenbank wie beim Nachstellen hier.
  3. Ein pg_dump vor dem ersten produktiven Ausrollen. Sekundensache, und die Rückversicherung für den Fall, den die ersten beiden Punkte eigentlich schon ausschließen sollen.

Und für den Fall, dass es trotzdem einmal passiert – nicht nur beim Kalender, bei jeder künftigen Migration –, gehört die Notfallzeile in TESTING.md:

docker exec aether-db psql -U aether -d aether \
  -c "UPDATE schema_migrations SET dirty=false;"

Kein Datenverlust dabei: Die fehlgeschlagene Migration lief in einer Transaktion und wurde zurückgerollt (im Test oben existierte cal_calendars danach nicht) – die Zeile hebt nur die Sperre auf, nichts sonst.

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.