Der bisherige Selbsttest prueft die JSON-Schnittstelle, also den Weg der
Weboberflaeche. Der CalDAV-Weg -- der, den DAVx5 und Thunderbird gehen --
war nie automatisch geprueft, obwohl er ueber voellig anderen Code
laeuft. Ein gruener Lauf des einen sagt nichts ueber den anderen.
scripts/caldav-selbsttest.py geht ihn jetzt ab: Anmeldung (auch die
Regel, dass das Kontopasswort hier nicht gilt), Auffinden ueber
.well-known samt der Umleitung, der Clients mit GET folgen,
Kalenderliste, PUT/GET/DELETE, If-Match, calendar-query mit Zeitfenster,
calendar-multiget, beide Richtungen zwischen Web und CalDAV
(Ganztagsdatum mit exklusivem DTEND, Terminfarbe als CSS3-Name, RRULE,
EXDATE) und das Umhaengen aus Client-Sicht.
Dabei gefunden und behoben: Ein Pfad, der auf einen *anderen* Benutzer
zeigt, wurde mit einem leeren 207 beantwortet. Kein Datenleck -- die
Antwort ist leer, und der Backend nimmt den angemeldeten Benutzer, nicht
den aus dem Pfad. Aber der Client sieht dann einen Kalenderbestand ohne
Kalender und meldet "nichts gefunden" statt "falsche Adresse": die
stille Variante genau des Fehlers, der bei der Einrichtung schon einmal
Stunden gekostet hat. Jetzt 404. Bewusst nur ablehnen, was sicher fremd
ist; alles Unbekannte geht weiter an go-webdav.
Zwei Luecken bleiben und werden vom Skript als Hinweis gemeldet, weil
go-webdav v0.7.0 nur einen festen Satz Eigenschaften beantwortet und
keinen Haken bietet, eigene dazuzulegen: getctag fehlt (Clients listen
bei jedem Abgleich alles auf, statt billig nach Aenderungen zu fragen)
und calendar-color fehlt (der Kalender erscheint im Client in dessen
Vorgabefarbe -- Terminfarben sind nicht betroffen, die stehen im .ics).
39 Pruefungen gruen, JSON-Selbsttest weiter 36 gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Anlegen war die Kalenderauswahl schon da (der oberste ist die
Vorgabe), beim Bearbeiten war sie festgestellt -- mit der Begruendung,
ein Umhaengen sei Loeschen und Neuanlegen unter neuer Kennung. Das
stimmt so nicht: In CalDAV wechselt dabei nur die Sammlung, UID und
Inhalt bleiben. Genau das macht der neue Endpunkt
POST /api/v1/calendars/{id}/events/{uid}/move {"calendar_id": …}
Er aendert nur die Zugehoerigkeit der Zeile und erhoeht das ctag auf
**beiden** Seiten. Mit nur einem holte ein Client die andere Haelfte
erst beim naechsten Vollabgleich, und der Termin stuende
zwischenzeitlich doppelt oder gar nicht im Telefon.
Bewusst ein eigener Endpunkt und kein weiteres Feld im PATCH: Ein Umzug
laesst sich auch ohne jede Aenderung am Termin ausloesen, und dafuer den
ganzen Termin mitschicken zu muessen waere eine Einladung, nebenbei
etwas zu ueberschreiben. Im Formular laeuft deshalb erst das PATCH und
dann der Umzug -- andersherum stuende der Termin nach einem Fehler
verschoben, aber unveraendert da, die verwirrendere Haelfte.
Dazu die Zieh-Geste: einen Termin auf einen Kalender in der
Seitenleiste ziehen, am Handy nach kurzem Gedrueckthalten. Ueber
Pointer-Events statt ueber die Drag-and-Drop-Schnittstelle des
Browsers -- die gibt es auf Android nicht, und genau dort soll die
Geste ja auch funktionieren. Ist die Kalenderliste zugeklappt (am Handy
der Regelfall), erscheint waehrend des Ziehens eine Leiste unten in
Daumennaehe, sonst liefe die Geste ins Leere.
Zwei Faelle, die sonst aergern: Wer in einen ausgeblendeten Kalender
zieht, bekommt ihn eingeblendet -- der Termin waere sonst scheinbar
spurlos verschwunden. Und der Klick, der einem Ziehen hinterherkommt,
wird verschluckt, statt den Termindialog zu oeffnen.
Eine Reihe zieht vollstaendig um; sie ist eine einzige Datei, kein
Buendel von Einzelterminen. Das sagt das Formular auch an.
Selbsttest um elf Pruefungen erweitert (36 gruen): Umzug angenommen,
Kennung bleibt, aus der Quelle weg und im Ziel da, ctag auf beiden
Seiten gestiegen, Umzug auf sich selbst folgenlos, fremder Kalender
404, Reihe vollstaendig umgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Löschen eines Serientermins gibt es jetzt zwei Wege statt einem:
"Nur diesen Termin" und "Ganze Reihe". Das sind zwei verschiedene
Absichten, und sie sind nicht ineinander umkehrbar -- eine einzelne
Ja/Nein-Rückfrage musste sich bisher für eine davon entscheiden, und die
andere war gar nicht erreichbar.
"Nur diesen" wird als EXDATE in der Reihe vermerkt, so wie das
iCalendar-Format eine Ausnahme ausdrückt: Die Reihe behält ihre Regel
und ihre UID, jeder CalDAV-Client versteht es ohne Zutun. Bewusst nicht
über ein Aufspalten in zwei Reihen gelöst -- das änderte die UID, und
alle Geräte hielten den Rest für einen neuen Termin.
Ein zweites Streichen desselben Termins bleibt folgenlos (sonst liefe
die Datei mit der Zeit mit doppelten EXDATE-Zeilen voll), und bei einem
Einzeltermin ohne Wiederholung sind beide Wege dasselbe -- der Server
löscht dann einfach, statt den Aufrufer mit einer Unterscheidung zu
behelligen, die es dort nicht gibt.
Selbsttest um sechs Prüfungen erweitert (25 grün): Reihe steht,
Streichen liefert 204, der gestrichene Termin fehlt und der Rest bleibt,
EXDATE steht im .ics, zweimal Streichen ist harmlos, ganze Reihe löschen
räumt alles weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Terminfarbe: Sie kam auf dem Telefon nie an. RFC 7986 schreibt für COLOR
einen CSS3-*Namen* vor, wir schrieben Hex – DAVx5 liest die Eigenschaft
über eine Namenstabelle und verwirft alles andere, also blieb der Termin
im Samsung-Kalender farblos. Jetzt beides nebeneinander:
COLOR:tomato für fremde Clients (damit die Farbe ankommt)
X-AETHER-COLOR:#ea4335 für die eigene Palette (damit sie genau bleibt)
Beim Lesen gewinnt der genaue Wert; ein von außen eingefärbter Termin
wird über die Namenstabelle übersetzt. Nächstgelegener Ton per Abstand im
RGB-Würfel (color.go).
Wiederholungen können jetzt mehr als "alle N":
- wöchentlich an ausgewählten Wochentagen (Mo–So, Mehrfachauswahl). Der
Wochentag des Starts bleibt fest angehakt – eine Reihe, die ihren
eigenen ersten Termin auslässt, ist keine.
- monatlich und jährlich wahlweise am festen Kalendertag (am 4.) oder am
n-ten Wochentag (am 1. Freitag). Beide Angaben leitet der Server aus
dem Startdatum ab, statt sie getrennt entgegenzunehmen – so kann die
Regel gar nicht erst auf einen anderen Tag zeigen als der Termin.
- Der fünfte Wochentag eines Monats wird bewusst zu "letzter": Einen
fünften Freitag hat nicht jeder Monat, BYDAY=5FR ließe die Reihe sonst
in etwa jedem zweiten Monat stillschweigend ausfallen.
Regeln, die das Formular weiterhin nicht abbildet, bleiben lesbar, aber
nicht bearbeitbar (409) – unverändert.
Dazu scripts/kalender-selbsttest.py, 19 Prüfungen gegen die laufende
Instanz, in TESTING.md verlinkt. Alle grün, unter anderem:
am 1. Freitag monatlich -> 04.09. 02.10. 06.11. 04.12.
am 1. Freitag im September -> 2026-09-04, 2027-09-03
Mo+Fr wöchentlich -> 07./11./14./18./21./25.09.
ganztägig am 3. -> bleibt der 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fünf Wünsche aus dem Durchtesten:
1. Wiederholungen einstellbar (täglich/wöchentlich/monatlich/jährlich,
alle N, Ende nie/nach Anzahl/an einem Datum). Der eigentliche Aufwand
lag nicht im Formular, sondern im Aufklappen: eine wöchentliche Reihe
erschien sonst als *ein* Eintrag. Der Server klappt sie jetzt im
angefragten Zeitfenster auf (?from=&to=, rrule-go über go-icals
RecurrenceSet), die Ansichten fragen ihren sichtbaren Ausschnitt an.
Geprüft: 5 Termine wöchentlich, nach Umstellung auf 14-tägig korrekt
neu gerechnet, Fenster einer Woche liefert genau einen.
Regeln, die das Formular nicht abbildet (etwa jeder zweite Dienstag
im Monat), werden weiterhin korrekt *angezeigt* -- geprüft, die vier
Termine stimmen -- aber nicht zum Bearbeiten angeboten (409). Ein
halb verstandenes Speichern würde die Regel sonst still beschneiden.
2. Tag wird erst geöffnet, statt sofort einen Termin anzulegen: Klick
auf einen Tag zeigt dessen Termine als Liste, dort wählt man einen
aus oder legt bewusst einen neuen an.
3. Eigene Farbe je Termin (COLOR nach RFC 7986), leer = Kalenderfarbe.
4. Kalenderliste ohne Kästchen: der Farbpunkt selbst ist der Schalter --
gefüllt heißt sichtbar, leerer Ring ausgeblendet.
5. Ganztägig als Schiebeschalter statt Kästchen -- es stellt das
Formular um, das ist keine Nebenangabe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei gemeldete Fehler, alle nachgestellt:
1. Ein vorhandener Termin ließ sich nicht öffnen. Der Klick auf den
Termin reichte an die Tageszelle darunter durch, die sofort 'Neuer
Termin' öffnete und den Bearbeiten-Dialog überschrieb. Fehlendes
stopPropagation.
2. Klick auf den 3. legte den Termin am 2. an. Ein ganztägiger Termin
ist ein *Datum*, kein Zeitpunkt - die Oberfläche rechnete ihn über
new Date(...).toISOString() trotzdem in UTC um, was östlich von UTC
den Vortag ergibt. Nachgestellt und bestätigt: DTSTART landete als
20260902 statt 20260903. Ganztägige Termine laufen jetzt über
dates.ts (allDayISO/eventDay), damit Dialog und Raster dieselbe
Rechnung benutzen. Nebenbefund mitgefixt: DTEND war gleich DTSTART,
was RFC 5545 verbietet (DTEND ist exklusiv) - ein eintägiger Termin
am 3. schreibt jetzt DTEND 20260904, nach außen bleibt die
Schnittstelle beim letzten belegten Tag.
3. 'Fühlt sich starr an': Wochenansicht ist jetzt ein echtes
Stundenraster (Klick trifft die Stunde statt Mitternacht, ganztägige
in eigener Zeile, überlappende Termine nebeneinander, Jetzt-Linie),
mehrtägige Termine stehen in jeder Zelle statt nur am Starttag, neue
Termine schlagen die nächste volle Stunde bzw. 9 Uhr vor,
Kalenderauswahl im Termindialog, Escape schließt, Hover-Rückmeldung,
Kalenderliste auf schmalen Schirmen einklappbar.
Außerdem: ein neu angelegter Kalender blieb unsichtbar, weil er in
keiner Sichtbar-Liste stand - die Liste merkt sich jetzt die
ausgeblendeten statt der sichtbaren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neue Seite CalendarPage (Menüpunkt 'Calendar' in der Seitenleiste,
zwischen Home und Papierkorb) mit Kalenderliste zum Anlegen/Umbenennen/
Löschen/Ein- und Ausblenden, Export/Import als .ics über die
Phase-2-API, und Termine anlegen/ändern/löschen über einen neuen
JSON-Endpunkt.
Backend: neuer events_api.go-Handler unter /api/v1/calendars/{id}/events
(Session-Auth) neben dem bestehenden CalDAV-Protokoll – ein Termin, den
die Web-Oberfläche anlegt, ist ein ganz normales CalDAV-Objekt und
synchronisiert genauso zu DAVx5 wie umgekehrt. Ein per CalDAV mit RRULE
angelegter, wiederkehrender Termin zeigt sich hier nur (recurring:true),
lässt sich aber bewusst nicht über dieses vereinfachte Formular
bearbeiten (409) – das würde die Wiederholungsregel eher beschädigen
als korrekt anpassen.
Gegen den laufenden Container getestet: Kalender/Termin anlegen, per
Web-API ändern und löschen, ein per CalDAV mit RRULE angelegter
Wiederkehrer taucht als recurring:true auf und blockt PATCH mit 409.
TypeScript-Build (tsc + vite build) sauber.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neuer APIHandler unter /api/v1/calendars (Session-Auth, wie der Rest
der API) neben dem bestehenden CalDAV-Handler (App-Passwörter,
/caldav/) – zwei Handler, zwei Auth-Modelle, wie schon bei /dav.
- GET/POST /api/v1/calendars – Liste, Anlegen
- PATCH/DELETE /api/v1/calendars/{id} – Umbenennen, Farbe, Zeitzone,
Löschen (go-webdav kennt PROPPATCH und Kalender-Löschen nicht – dafür
gibt's im CalDAV-Protokoll selbst keinen Weg)
- GET .../export – alle Termine eines Kalenders als eine .ics-Datei,
VTIMEZONE-Blöcke dedupliziert
- POST .../import – .ics mit mehreren VEVENT/VTODO einlesen, jede
Komponente wird ein eigenes Objekt wie bei einem einzelnen CalDAV-PUT
Gegen den laufenden Container getestet (Session-Login, CSRF-Token):
Anlegen, Umbenennen, zwei Termine importieren, sauberer Re-Export,
Löschen, und dass ohne Login 401 statt Daten kommt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neue Spalte cal_calendars.timezone (Default UTC). CalDAV selbst braucht
sie nicht - jeder Termin trägt seine Zeitzone bereits im eigenen .ics,
und die TZID-Auflösung darin läuft schon korrekt über die
System-Zeitzonendatenbank. Gebraucht wird das Feld erst von der
künftigen Web-Oberfläche zum Anlegen neuer Termine (Phase 3) als
Vorgabe, wenn der Nutzer keine eigene Zeitzone wählt - jetzt schon
angelegt, damit dafür keine weitere Migration nötig wird.
Gegen den laufenden Container geprüft: Migration lief durch, Home-Set-
PROPFIND (nutzt die geänderten SELECTs) weiterhin grün.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Manche Clients folgen dem 308-Redirect von /.well-known/caldav mit GET
statt PROPFIND (curl -L reproduziert das exakt), landen dann auf einer
Sammlung (Principal/Home-Set/Kalender) und bekamen dort bisher ein
hartes 404 von GetCalendarObject – ein Client, der so die
Erreichbarkeit prüft, bricht die Einrichtung dann vorzeitig ab, bevor
er je ein PROPFIND schickt. GET/HEAD auf Nicht-.ics-Pfade antworten
jetzt mit einer schlichten 200-Textantwort statt an den CalDAV-Backend
durchgereicht zu werden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
go-webdav leitet GET/HEAD auf jeden Pfad unverändert an
GetCalendarObject weiter, auch auf Principal- oder Home-Set-Pfade (ein
Browser kann nur GET, kein PROPFIND). Ohne Prüfung hielt
parseCalendarPath dann z.B. das Wort 'caldav' aus dem Pfad für eine
Kalender-ID und gab einen internen UUID-Parsing-Fehler nach außen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neues, eigenständiges Modul internal/calendar: implementiert
caldav.Backend (emersion/go-webdav) gegen zwei neue Tabellen
(cal_calendars, cal_objects). Endpunkt /caldav/ mit eigener Basic Auth
(nur App-Passwörter, wie /dav), plus /.well-known/caldav für die
automatische Einrichtung nach RFC 6764.
Gegen echte Container getestet (nicht nur go build): Discovery-Kette
Principal -> Home-Set -> automatisch angelegter Standardkalender,
PUT/GET/DELETE eines Termins, REPORT mit Zeitraumfilter (Treffer und
Nicht-Treffer), If-Match/If-None-Match-Vorbedingungen (412), und dass
ein fremder Nutzer auf einen Kalender-Pfad mit 404 statt 403 abgewiesen
wird.
Zwei Fallen unterwegs gefunden und dokumentiert:
- go-webdav vergleicht von CurrentUserPrincipal/CalendarHomeSetPath
zurückgegebene Pfade direkt gegen r.URL.Path (inklusive Prefix) und
bestimmt den Ressourcentyp allein an der Segmentzahl dahinter – der
Nutzername beim Principal steht deshalb in einem Segment
(principals-max), nicht als Unterpfad.
- chi kennt REPORT/MKCALENDAR nicht von Haus aus (nur PROPFIND & Co.
waren für /dav bereits registriert) und hätte sie sonst mit 405
abgewiesen, bevor der CalDAV-Handler überhaupt lief.
Migration 000021 nur mit CREATE TABLE IF NOT EXISTS, wie in
kalender-plan.md festgelegt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>