Die Frist von 220 ms galt nur beim Wechsel *innerhalb* der Dateiansicht. Beim
Wechsel zwischen den Menüpunkten entsteht die Seite jedes Mal neu – damit war
jeder Tab-Wechsel ein „erstes Laden", und genau dort war die Regel „beim ersten
Mal sofort zeigen" falsch herum. Die Frist gilt jetzt immer; 220 ms Leere ist
das ruhigere Bild, man kommt ohnehin von einer anderen Seite.
Als gemeinsamer Haken `useSkeleton`, statt die Logik in fünf Seiten zu
wiederholen.
Dabei wäre beinahe ein schlimmerer Geist entstanden: Im Wartefenster hat kurz
der **Leerzustand** aufgeblitzt – „Nothing shared with you yet", eine
Zehntelsekunde bevor die Liste kam. Der Platzhalter sieht nur nach Daten aus,
ein Satz behauptet etwas. Das Wartefenster bleibt jetzt überall leer.
Dazu drei Reste der Kalender-Übersetzung, die mein Suchmuster übersehen hatte,
weil sie in Template-Strings standen: der Löschen-Dialog des Kalenders und der
Wochentag-Hinweis im Terminfenster. Und das Datum in der Listenansicht stand
noch im deutschen Format („18. September 2026") in einer englischen
Oberfläche – jetzt „September 18, 2026".
Geprüft: tsc sauber, 41 Vitest-Tests, alle 234 Prüfungen der sieben
Selbsttest-Suiten grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Kalenderseite war durchgehend deutsch, die übrige Oberfläche englisch –
80 Stellen in sechs Dateien. Kommentare bleiben deutsch; das Ersetzen hat
Kommentarzeilen übersprungen.
Shell, TagChip und UserMenu standen nur wegen ihrer Kommentare auf der
Verdachtsliste, deren Oberfläche war längst englisch.
Der automatisch angelegte Kalender hieß „Persönlich" und heißt jetzt
„Personal". Migration 000027 zieht die bestehenden nach, bewusst eng gefasst:
nur Zeilen mit Name *und* Farbe des Automaten. Wer sich selbst einen Kalender
„Persönlich" angelegt hat, behält seinen Namen – das ist seine Beschriftung,
nicht unsere. Nachgeprüft: Die API meldet danach 'Personal', nicht nur
„Migration gelaufen".
Dazu zwei Punkte aus Marcs Rückmeldung:
fix(ui): Der Leerzustand der Freigabe-Links stand auf anderer Höhe als die
übrigen – die Seite hatte ich beim Umbau übersehen. Nutzt jetzt EmptyState
wie alle anderen.
fix(ui): Beim Durchklicken von Ordner zu Ordner blitzte für einen Moment ein
Feld grauer Kästen auf. Sie sahen aus wie Dateien, die gleich wieder
verschwinden – „Geister". Der Platzhalter kommt jetzt erst nach 220 ms; ist
die Liste vorher da, wird er nie sichtbar, und solange bleibt die vorherige
stehen (load() löscht sie nicht). Nur beim allerersten Laden kommt er sofort:
Da gibt es nichts, was stehenbleiben könnte.
Nebenbei: Ein fehlgeschlagenes Laden eines Ordners ist nicht mehr stumm.
Vorher sah es aus wie ein Ordner, der zufällig denselben Inhalt hat wie der
vorige.
Geprüft: go build/vet sauber, tsc sauber, 41 Vitest-Tests, alle 234 Prüfungen
der sieben Selbsttest-Suiten grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Laden stand ein Kreisel in der Mitte. Der sagt „warte", aber nicht,
worauf – und wenn die Daten kommen, klappt das Layout um. Jetzt steht dort ein
Platzhalter in der Form des Kommenden: Kacheln in der Kachelansicht, Zeilen in
der Liste. Die Maße sind aus FileViews.tsx abgeschrieben, nicht geschätzt;
weichen sie ab, springt es beim Wechsel wieder und der Zweck wäre dahin.
Leerzustände sagen jetzt, warum nichts da ist, und bieten den Knopf an, der an
dieser Stelle weiterhilft. Der leere Papierkorb bekommt bewusst keinen: Er ist
ein guter Zustand, kein Mangel, und ein Knopf, der nur da ist damit einer da
ist, führt nirgendwohin.
Beides respektiert prefers-reduced-motion.
Nebenbei gefunden: `@keyframes spin` lag als Inline-<style> in **sechs**
Komponenten, und zwei Nutzer (AuditTab, SearchBox) hatten gar keine eigene
Kopie. Dass es lief, war Zufall – @keyframes gilt fürs ganze Dokument, es
genügte also, dass irgendeine der sechs gerade im Bild war. Kaputt wäre es an
dem Tag gewesen, an dem jemand die letzte Kopie entfernt. Steht jetzt einmal
in index.css.
Nicht angefasst: der Sprachmix. CalendarPage ist durchgehend deutsch, die
übrige Oberfläche englisch, betroffen sind sechs Dateien. Eine halbe
Übersetzung wäre schlimmer als keine – das gehört in einem Zug gemacht, samt
der Entscheidung, welche Sprache die Oberfläche spricht. In TESTING.md notiert.
Geprüft: tsc sauber, 41 Vitest-Tests, alle 234 Prüfungen der sieben
Selbsttest-Suiten grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vorher bestätigte nichts irgendetwas, und Fehler verschwanden still: Die
Handler fingen sie mit `catch { /* bleibt stehen */ }` ab. Wer eine Datei
löschen wollte und nicht durfte, sah genau dasselbe wie jemand, bei dem es
klappte – nichts.
Neu: components/Toasts.tsx. Kein Context, sondern Modulzustand, damit auch
api/client.ts melden kann, ohne Komponente zu sein. Löschen, Umbenennen und
Verschieben bekommen ein „Undo" – beim Löschen ist das der Papierkorb plus
einen Aufruf, kostet also fast nichts. Ordner löschen bekommt bewusst keines:
Der Ordner ist wirklich weg, nur seine Dateien liegen im Papierkorb, und ein
Knopf, der etwas anderes tut als er verspricht, wäre schlimmer als keiner.
Dazu die erste Animation im Projekt – und gleich die Regel dazu:
prefers-reduced-motion wird respektiert.
**jsdom ist jetzt in den devDependencies**, und der erste DOM-Test hat sofort
einen Fehler in der Behebung von vorgestern gefunden:
Der Zähler für selbst ausgelöste Verlaufsschritte setzt voraus, dass jedes
`back()` auch ein `popstate` nach sich zieht. Das gilt nicht – folgt im selben
Durchlauf ein `pushState`, verwirft der Browser den Sprung. Der Zähler blieb
dann stehen, und die nächste echte Zurück-Geste wurde verschluckt: Statt die
Ansicht zu schließen, hätte man die Anwendung verlassen.
Jetzt wird erst im nächsten Durchlauf entschieden und geprüft, ob der eigene
Eintrag noch obenauf liegt. Nur dann kommt es zum Sprung – und dann folgt das
`popstate` garantiert. Der Zuhörer hängt am Modul statt an der Ansicht; vorher
entfernte ihn deren Aufräumer, bevor der eigene Sprung ausgeführt war.
Ehrlich dokumentiert: Den ursprünglichen Fehler bilden diese Tests **nicht**
ab. jsdom verwirft den Sprung, das verhängnisvolle popstate entsteht dort nie
– mit der alten, kaputten Fassung blieben alle Tests grün. Dafür bräuchte es
einen echten Browser. Steht so in TESTING.md und im Test selbst.
Geprüft: tsc sauber, 41 Vitest-Tests (4 neue unter Mutation nachgewiesen),
alle 234 Prüfungen der sieben Selbsttest-Suiten grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Löschen von convert.go ist Aethers einzige Verwendung des JWT-Schlüssels
mitgegangen: signJWT stand dort. Zurück blieb ein Feld auf dem Client, das
niemand liest, plus Config-Eintrag und Aufrufparameter – Go meldet ungenutzte
Strukturfelder nicht, deshalb lief der Build durch. Eine Einstellung, die
nichts mehr tut, ist schlimmer als keine: Wer sie setzt, glaubt, sie wirke.
OFFICE_JWT_SECRET bleibt in der .env, wird aber vom **Office-Container**
gelesen, nicht von Aether. Geprüft mit Euro-Office: JWT_ENABLED=true und
Aether ohne jedes Signieren – der WOPI-Weg läuft.
Doku richtiggestellt, nachdem Marc den Compose-Schnipsel angezweifelt hat:
- „Euro-Office ist hier nie gelaufen" stimmte nicht mehr. Jetzt steht da, was
tatsächlich geprüft ist (Abbild, Start, Discovery, CheckFileInfo, Ausliefern,
mobileEdit) und was nicht: PutFile, also das Zurückschreiben.
- Der Absatz zu /ConvertService.ashx beschrieb gelöschten Code.
- OFFICE_PUBLIC_URL: die Falle benannt, die einen Tag gekostet hat. Stand dort
localhost, lief alles am Docker-Rechner und blieb am Telefon schwarz – dort
ist localhost das Telefon. Ohne Fehlermeldung.
- Zur Portbindung: Die Office-Schnipsel binden auf 0.0.0.0, anders als Aether
selbst. Das ist nötig, weil der Browser direkt hin muss – steht jetzt dabei,
samt dem sauberen Weg über einen Reverse Proxy.
Geprüft: go build/vet sauber, alle 234 Prüfungen der sieben Selbsttest-Suiten
grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Office-Dokument öffnete am Telefon als gewandeltes PDF, weil OnlyOffices
Oberfläche auf 400 Pixeln nichts vom Dokument übrig lässt. Die Begründung war
falsch: Die Ermittlungsliste bietet zu jeder Endung mehrere Aktionen an, und
OnlyOffice/Euro-Office führen neben `edit` auch `mobileEdit` – dieselbe
Adresse mit `?mobile=1`, dahinter eine für kleine Schirme gebaute Oberfläche.
`EditURL` kannte nur `edit` und `view`; die Handy-Fassung lag die ganze Zeit
in der Tabelle und wurde nie abgerufen.
Entschieden wird am Zeigegerät, nicht an der Fensterbreite – ein schmales
Fenster auf dem Schreibtisch ist immer noch eine Maus.
Mit dem Anlass fällt die Notlösung: preview.go (229 Zeilen), convert.go (271)
samt Test, files.preview_version (Migration 000026), die Sonderabfrage im GC,
die Route und dreizehn Fundstellen im Frontend. convert.go hatte keinen
anderen Aufrufer – die gesamte Umwandlung nach PDF existierte allein für
diese Ansicht. Wer ein PDF braucht, exportiert es im Editor. Die Dateien
unter previews/ sammelt der GC ein.
Zwei Fehler, die dabei sichtbar wurden:
fix(history): „Edit" schloss die Datei, statt den Editor zu öffnen. Beim
Wechsel räumt React erst die alte Ansicht ab; der Aufräumer von useBackClose
ruft history.back(), das verzögert wirkt und deshalb den Eintrag verbrauchte,
den der Editor gerade angelegt hatte. Ein Zähler für selbst ausgelöste
Schritte verschluckt genau diese popstate.
fix(filetypes): `.txt` ging gar nicht auf. Es stand in officeExtensions und
lief damit in den Office-Zweig statt in den eingebauten Text-Editor – am
Telefon in die PDF-Umwandlung, die daran scheiterte. `md`, `log` und `rst`
standen nie drin; das war die Asymmetrie.
Dazu Euro-Office als Compose-Profil (Abbild 6,99 GB, nachgemessen – die
Ankündigung nennt 5) und die Android-App als Idee.
Geprüft: go build/vet sauber, tsc sauber, 37 Vitest-Tests, alle 234
Prüfungen der sieben Selbsttest-Suiten grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
internal/tags hatte null Tests, und die Rechtepruefung steckt dort nicht in
einer if-Zeile, sondern in WHERE-Klauseln. Ein Tippfehler darin faellt beim
Lesen nicht auf: Die Abfrage laeuft, sie liefert nur zu viel.
Fuenf Tests auf dieselbe Frage -- Liste, Umbenennen, Loeschen, Anhaengen an
die eigene Datei, und vor allem das Auflisten der **Dateien** unter einem
Etikett. Der letzte Weg gibt Dateinamen, Ordner und Groessen heraus; wer
dort eine fremde Etikett-ID erraet, saehe einen Ausschnitt aus fremden
Unterlagen. Dazu Gruppen: Mitglied ja, Nichtmitglied nein, und wer die
Gruppe verlaesst, verliert den Zugriff im selben Moment.
Gegen kaputten Code gegengeprueft: Eigentuemerpruefung aus FilesWithTag
entfernt -> "Bert sieht 1 fremde Dateien: Gehaltsabrechnung.pdf". Pruefung
in SetFieldsForFile ausgehebelt -> zwei Tests rot.
**Ein Verdacht erwies sich als falsch, und das ist der nuetzlichere
Ausgang:** List bietet Gruppen-Etiketten an, SetFieldsForFile vergleicht
gegen den Prinzipal der Datei -- sah nach einem Widerspruch aus. Ist keiner;
die Oberflaeche filtert bereits (TagDialog.tsx:69). Und die Ablehnung ist
nicht Foermlichkeit, sondern die tragende Absicherung: FilesWithTag prueft,
ob man das *Etikett* sehen darf, nicht die *Datei*. Das ist nur deshalb
harmlos, weil ein Etikett stets denselben Prinzipal hat wie die Dateien
darunter. Der Test haelt jetzt diesen Zusammenhang fest, nicht bloss eine
Fehlermeldung.
Nebenbei: tags hat zwei Fehlermarken fuer dieselbe Aussage (ErrNotFound und
ErrCorrespondentNotFound). Kein Fehler, aber eine Stolperstelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
internal/documents hatte null Tests -- und ist das Paket, dessen Versagen am
teuersten ist, weil es **still** ist: Die Volltextsuche findet dann einfach
nichts, und das sieht aus wie "steht nicht drin".
Acht Tests, nicht auf Abdeckung gezielt, sondern auf die Zustaende, in denen
die Schlange haengenbleiben kann, ohne dass es auffaellt: ein Auftrag, der
nie aufgibt; ein Auftrag, der nach hartem Abbruch fuer immer auf 'running'
steht; ein Text nach alter Regel, der nie erneuert wird. Dazu geloeschte
Dateien, erneuter Upload und Complete als Einheit.
scripts/db-tests.sh baut eine Wegwerf-Datenbank, spielt die Migrationen ein
und laesst die Tests laufen. Ohne das haetten sie sich mangels TEST_DSN
immer nur uebersprungen -- Dekoration, genau wie vitest es war.
**Gegengeprueft an absichtlich kaputtem Code, und der zweite Versuch blieb
gruen:** maxAttempts von 3 auf 9999 gesetzt -- alle Tests weiter in Ordnung,
nur die Laufzeit stieg von 0,2 auf 27 Sekunden. Sie schleiften ueber die
Konstante und setzten die Zahl voraus, statt sie zu pruefen. So verschwindet
eine Entscheidung unbemerkt aus einer Anwendung. Jetzt haelt ein eigener
Test die Zahl fest und sagt, wo die Begruendung hingehoert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Pruefsumme wurde beim Ausliefern mitgerechnet und das Ergebnis
weggeworfen -- ein gekipptes Bit wurde erkannt und verschwiegen.
Jetzt: Logeintrag mit Datei-ID, Speicherschluessel, erwarteter und
gelesener Summe, dazu panic(http.ErrAbortHandler), den chis Recoverer
ausdruecklich durchreicht.
Damit das wirkt, werden die letzten 32 KiB zurueckgehalten. Sonst waere die
Datei beim Erkennen laengst vollstaendig beim Empfaenger und der Abbruch
kaeme zu spaet. Erst wenn die Summe stimmt, darf der Schluss hinterher;
sonst bekommt die Gegenstelle weniger Bytes als angekuendigt und wertet das
als Fehler statt als Datei.
scripts/pruefsumme-selbsttest.py, 14 Pruefungen: kippt ein Bit auf der
Platte (nicht im Programm), in der Mitte der Datei, damit es nicht schon an
der Laenge auffaellt; zwei Groessen ueber und unter den 32 KiB; und zuerst
die Gegenprobe, dass eine heile Datei durchkommt.
**Dabei gefunden:** Der neue Logeintrag blieb aus, obwohl der Abbruch
funktionierte. zerolog.Ctx liefert einen *abgeschalteten* Logger, wenn im
Kontext keiner steckt -- und Aether legte nie einen hinein. Alle 34
log.Ctx-Aufrufe im Baum schrieben ins Nichts, darunter die Warnung ueber
verwaiste Blobs beim Zurueckrollen einer Fassung.
Behoben zweifach: zerolog.DefaultContextLogger beim Start (deckt auch
Hintergrundarbeit ab) und ein Logger mit request_id im Anfragekontext. Zwei
Tests halten es fest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Vorschaubild entstand in einer nackten Goroutine. Chis Recoverer haengt
am Anfragebehandler und sieht davon nichts -- eine Panik im Bilddekodierer
nahm den ganzen Go-Prozess mit, ausgeloest durch einen gewoehnlichen
Upload. Die Bibliothek ist seit ef60497 aktuell; das schloss **diese**
Luecke. Dies hier schliesst die **Gattung**.
pkg/bg mit zwei Funktionen, und der Unterschied ist keine Feinheit:
* bg.Go(name, fn) -- Goroutine, die eine Panik ueberlebt
* bg.Run(name, fn) -- dasselbe ohne eigene Goroutine, fuer Schleifen
Faengt man eine Panik um den **ganzen** Arbeiter ab, ueberlebt der Prozess
-- aber der Arbeiter ist tot, und von da an wird still keine Datei mehr
erkannt. Der OCR-Arbeiter sichert deshalb jeden **Auftrag** einzeln ab.
Umgestellt: Vorschaubild, Bestand nachtragen, Freigabe-Zaehler,
Speicherbereinigung, OCR-Arbeiter. Eine nackte Goroutine bleibt mit
Begruendung im Code -- die Signalbehandlung; ein recover braechte dort
nichts, weil main ohnehin wartete.
Aufgefangene Paniken werden mit Stapel und Herkunft protokolliert. Vier
Tests, darunter der wichtige: Eine Schleife laeuft nach einer Panik im
dritten Durchgang weiter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Einmal durch die ganze Cloud, geordnet nach dem, was passieren kann.
Gemessen statt geschaetzt: Routen aus main.go extrahiert und gegen die
Rechtepruefungen abgeglichen, govulncheck ueber den ganzen Baum, Zeilen und
Testumfang je Paket gezaehlt.
Drei Befunde, ein Abschnitt darueber, was in Ordnung ist (eine Liste nur aus
Maengeln gibt ein falsches Bild), und eine vorgeschlagene Reihenfolge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
govulncheck meldete drei Luecken, die Aether nicht nur mitschleppt, sondern
auch **aufruft**:
* GO-2026-6222 -- uebermaessige Speicheranforderung beim VP8L-Dekodieren
(x/image). Erreichbar ueber jeden Bild-Upload.
* GO-2026-5061 -- Panik bei unstimmiger Alphakanal-Groesse in x/image/webp.
Erreichbar ueber denselben Weg.
* GO-2026-5970 -- Endlosschleife bei ungueltiger Eingabe (x/text), ueber
pgxpool.New.
x/image 0.42.0 -> 0.45.0, x/text 0.38.0 -> 0.41.0 (0.41 verlangt x/image).
Danach meldet govulncheck: keine.
Alle sechs Selbsttest-Suiten gruen (36 86 24 20 13 41), go vet und go test
sauber.
Die zweite Haelfte des Problems bleibt offen und steht in BEFUNDE.md: Das
Vorschaubild wird in einer **nackten Goroutine** erzeugt, und dort faengt
chis Recoverer nichts. Eine Panik im Dekodierer nimmt den ganzen Prozess
mit -- unabhaengig davon, welche Luecke gerade offen ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Praezisiert, warum sie vertagt ist und was der Anlass waere. Neu darin:
* Android hat keinen eingebauten Aufgabenspeicher -- fuer Termine gibt es
einen Systemanbieter, fuer Aufgaben nicht. Die Task-App **ist** der
Speicher, deshalb geht ohne sie gar nichts.
* Der Bereich "Erinnerungen" im Samsung Kalender ist ein Fenster in die
Samsung-App, kein Teil des Kalenders. Am Geraet ablesbar: Beim Termin
fragt Samsung nach dem Kalender, bei der Erinnerung nicht -- es gibt nur
einen Ort, an den sie gehen kann.
* Was beim Bauen zu bedenken waere: Eine Aufgabe hat kein Pflichtdatum. Alle
drei heutigen Ansichten ordnen nach Zeit; deshalb eine eigene Ansicht statt
Einstreuen in den Monat.
* Dazu der Gegentest, falls die Einschaetzung falsch ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Nicht als eigene Tabelle, wie im Plan vorgesehen: acl_entries beantwortet
fuer Ordner und Dateien laengst dieselbe Frage -- wer darf was, direkt oder
ueber eine Gruppe. Ein zweites Rechtesystem daneben waere eine zweite
Antwort auf dieselbe Frage, mit eigener Gruppenaufloesung, eigenem
Widerruf, eigenem Pruefpfad; die Luecke dazwischen faellt erst auf, wenn
jemand sieht, was er nicht sehen soll. Migration 25 haengt deshalb nur eine
dritte Ressourcenspalte an.
Der Gewinn: 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 Eigentuemers -- das hat der sich ausgesucht, und zwei
Leute duerfen denselben Namen waehlen. Sonst laegen im Kalenderbestand
des Empfaengers zwei Kalender auf derselben Adresse.
* Alle Wege laufen durch RequireCalendar. CalDAV und Weboberflaeche sind
zwei getrennte Zugaenge zu denselben Daten; ein Recht, das nur einer von
beiden prueft, ist kein Recht. Im CalDAV-Teil genuegte, resolveCalendar
umzustellen -- dort kommt jede Operation vorbei.
* Wer ein Recht geliehen hat, darf es nicht weiterverleihen: Umbenennen,
Loeschen und Freigeben bleiben auch mit Schreibrecht beim Eigentuemer.
Oberflaeche: fremde Kalender tragen Eigentuemer und "(nur lesen)", das
Menue zeigt nur, was auch geht, und ein Termin laesst sich gar nicht erst
in einen Kalender ziehen, den der Server danach ablehnen wuerde.
scripts/kalender-teilen-selbsttest.py, 41 Pruefungen, jede ueber beide
Zugaenge. Er beginnt damit, dass der Empfaenger vorher nichts sieht --
ohne diesen Anfang beweist der Rest nichts.
Dabei hatte der Test selbst eine Falle: Mit festem Testdatum lief die
Terminliste aus ihrem Standardzeitraum, und die Pruefung "beim Eigentuemer
ist nichts angekommen" bestand **leer**.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bis hierher holte jeder Durchgang die volle Terminliste: ctag abfragen,
Aenderung sehen, alles neu laden. Eine verschobene Besprechung kostete
damit so viel wie ein Neuanfang -- bei einem Jahr Termine, alle fuenfzehn
Minuten, auf Akku und Datenvolumen des Telefons.
Kein zweiter Zaehler: Das ctag **ist** bereits eine Zahl je Kalender, die
bei jedem Schreiben um eins steigt. Jedes Objekt merkt sich in sync_seq den
Stand, bei dem es zuletzt angefasst wurde (Migration 24).
Grabsteine (cal_tombstones) halten geloeschte UIDs fest -- ohne sie bliebe
ein geloeschter Termin auf jedem Geraet stehen, das gerade nicht verbunden
war. Sie stehen 180 Tage; beim Raeumen wandert sync_floor mit, und ein
Token darunter bekommt 403 <valid-sync-token/> statt einer Antwort mit
fehlenden Loeschungen.
go-webdav kennt fuer REPORT nur calendar-query und calendar-multiget, und
weder sync-token noch supported-report-set. Beide gehen denselben Weg wie
getctag und calendar-color. Ohne supported-report-set probiert DAVx5
sync-collection gar nicht erst aus.
Zwei Fehler noch vor dem Ausliefern gefunden:
* Die Anfuehrungszeichen am ETag standen **innerhalb** der Maskierung --
xml.EscapeText machte daraus ". Gueltig, aber genug Clients
vergleichen ETags roh und saehen bei jedem Durchgang eine Aenderung.
* pruneTombstones nahm die erste Zeile eines DELETE ... RETURNING als
Maximum. Das hat keine Reihenfolge -- der sync_floor waere zu niedrig
gesetzt worden, und ein Client dazwischen haette Loeschungen verpasst.
caldav-selbsttest.py: 62 -> 86 Pruefungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OCR_ENABLED, OCR_LANGUAGES, OCR_MAX_PAGES, OCR_TIMEOUT und
OFFICE_INSECURE_TLS werden von config.go gelesen, standen aber in keiner
Vorlage. Alle haben Vorgaben, es bricht also nichts -- aber OCR_MAX_PAGES=50
deckelt die Texterkennung stillschweigend bei fuenfzig Seiten, und wer lange
Scans ablegt, kann das nicht wissen, wenn es nirgends steht.
Gegenprobe: kein von config.go gelesener Schluessel fehlt jetzt noch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beides auf einer eigens aufgesetzten Instanz durchgespielt:
* **Update einer laufenden Instanz**: Schema 19 -> 23 mit Daten in der
Datenbank. Konten, Ordner, Dateien und Etikettenzuordnungen unveraendert,
kein dirty-Zustand, /health 200. Ein Neuaufbau bringt nichts, was das
Update nicht auch bringt.
* **Grosser Wiederherstellungsfall**: Container und Datenbank weggeworfen,
nur die Dateien aus der Sicherung zurueck. Funktioniert -- aber nur, wenn
die Konten vorher mit *demselben* Benutzernamen wieder angelegt werden.
Der Ordnername auf der Platte ist die Zuordnung. Ohne diesen Schritt
bleibt der Abgleich stehen und sagt es je Datei ("Konto ... gibt es
nicht"); er raet nicht.
Danach standen Ordnerkette, Unterordner, Umlaute, Groessen, Pruefsummen
und Dateitypen wieder da, der Download lieferte den urspruenglichen
Inhalt, .trash und .versions blieben aussen vor.
Dazu aufgeschrieben, was aus einer reinen Dateisicherung **nicht**
zurueckkommt: Etiketten, Absender, Belegdatum, Freigabelinks, fruehere
Fassungen, Kalender und Termine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Kreuz faerbt sich beim Darueberfahren rot -- aber nur, solange der
Upload laeuft. Dasselbe Zeichen tut zwei Dinge: eine Meldung wegraeumen
ist folgenlos, einen laufenden Vorgang fallen zu lassen nicht.
Unter dem Balken steht jetzt "4.2 MB / 18.0 MB · 1.8 MB/s · 8 s left".
Die Zahlen kamen schon immer vom XMLHttpRequest, sie wurden nur
weggeworfen. Neu gerechnet wird nur das Tempo -- der Browser meldet
Bytes, nicht deren Ableitung.
Zwei Daempfungen, aus zwei Gruenden:
* Das Tempo aus einem einzelnen Abstand springt zwischen dem Doppelten
und der Haelfte. Gleitender Mittelwert (0,7/0,3) ueber Messpunkte von
mindestens 0,2 s.
* Die Anzeige wird hoechstens alle 150 ms nachgezogen. Der Browser meldet
im 50-ms-Takt; bei drei Dateien waeren das sechzig Neuaufbauten der
Liste je Sekunde fuer eine Zahl, die niemand so schnell liest.
Jeder Teil erscheint erst, wenn er stimmt: Groesse sofort, Tempo nach dem
zweiten Messpunkt, Restzeit erst mit beidem. Eine Schaetzung aus dem
Nichts waere schlimmer als keine -- man richtet sich danach.
formatDuration in src/format.ts, 5 Tests (37 gesamt).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zoom
----
Jede Zoomstufe warf die Leinwaende weg und liess PDF.js neu malen -- eine
Zusammenziehgeste hat leicht zwanzig davon, bei A4 und doppelter
Pixeldichte je rund acht Megabyte. Noetig war das nie: Die Leinwand liegt
mit width:100% im Seitenkasten, der Browser streckt sie also von selbst.
Gezeichnet wird jetzt 120 ms nach der letzten Stufe. Die Textebene wandert
korrekt mit (PDF.js setzt sie in Prozent), nur die Trefferkaesten sitzen in
Pixeln -- die werden waehrend der Geste geraeumt und danach neu gemalt.
Dabei ein aelterer Verlust: Jedes Oeffnen zeichnete einmal umsonst, weil
der Zeichner lief, bevor die Einpassung gemessen war. Jetzt wartet er.
Upload abbrechen
----------------
Das Kreuz an der Fortschrittsmeldung laesst einen laufenden Upload fallen;
eine noch wartende Datei faengt gar nicht erst an. Ein Abbruch ist kein
Fehler und verschwindet wie eine fertige Meldung.
Auf dem Server bleibt nichts liegen -- das ist die Bauweise (Temp-Datei,
Umbenennen erst am Ende), aber eine Bauweise ist keine Pruefung. Deshalb
scripts/upload-abbruch-selbsttest.py: kappt eine echte Verbindung auf einem
rohen Socket, 13 Pruefungen. Der teuerste Fall ist abgedeckt -- beim
Ueberschreiben mit Versionierung schiebt der Server die alte Fassung
kurzzeitig zur Seite; sie bleibt in Groesse, Pruefsumme und Inhalt heil.
Downloads haben bewusst kein Kreuz: Sie gehen als Anker bzw. Formular an
den Browser, dessen Download-Liste sie besitzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FileBrowser 1177 -> 712 Zeilen, PdfViewer 1193 -> 848. Herausgeloest
wurde, was fuer sich zusammenhaengt:
hooks/useUploads Warteschlange, Ordnerbaeume, Ablage
components/files/ Werkzeugleiste, Dialoge, Menues, Reihenfolge
components/pdf/ Werkzeugleiste, Zoomstufen, Seitenmasse, Suche
src/format.ts formatBytes -- lag in einer *Seite*
Dabei gefundene und behobene Fehler:
* Ordner-Upload haengte Fortschritt und Fehler an die falsche Zeile,
sobald in einem Ordner Dateien *nach* einem Unterordner lagen: Die
Zeilen standen in Fundreihenfolge, hochgeladen wurde nach Ordnern
gruppiert, und ein mitlaufender Zaehler verband beides falsch.
* formatBytes gab es zweimal, zeichengleich (Dateibrowser + Admin).
* Beide Sortierungen liefen bei *jedem* Bild -- der Rahmen beim Ziehen
ist eines je Mausbewegung. Jetzt gemerkt.
* Die Suche im PDF las bei jedem Tastendruck den Text aller Seiten neu.
Jetzt 160 ms Wartezeit; "Searching..." bleibt auch bei einem Fehler
nicht mehr stehen.
* Seitenmasse werden in Buendeln statt streng nacheinander geholt.
Pruefinfrastruktur: vitest stand auf 'jsdom', ohne dass jsdom je
installiert war -- "npm test" brach ab, bevor es etwas ausfuehrte. Jetzt
'node' und 32 Tests fuer die herausgeloesten reinen Funktionen. Das
Abbild baut nur noch mit gruenen Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kopfleiste blendete den Menue-Knopf ab 720px aus, die Seitenleiste stand
aber erst ab 768px fest im Bild. Dazwischen lag ein Band von 48 Pixeln
ohne Menue und ohne Knopf dorthin -- ein aufgeklapptes Fold landet mit
rund 757px genau darin.
Die Grenze steht jetzt einmal in src/breakpoints.ts.
Dazu zwei Folgefehler derselben Sache:
* Der abdunkelnde Grund lag mit z-index 40 ueber der fest stehenden
Leiste (die hat dort keinen). Per Randwisch aufgeschoben, war die
Leiste danach sichtbar, aber taub. Er blendet sich ab 720px aus.
* Der Randwisch loest breit gar nicht mehr aus, und Aufklappen bei
aufgeschobener Leiste setzt sie zurueck -- sonst schluckte die
Zurueck-Geste einmal ins Leere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Konto ohne Kalender bekam auf GET /api/v1/calendars die Antwort
`null`, und die Ansicht brach ab:
for (const cal of calendars) -> "e is not iterable"
Nicht der Kalender ist schuld, sondern eine Eigenheit, die jeden
Listenendpunkt betrifft: In Go ist eine Liste ohne Eintraege oft nil,
und encoding/json schreibt dafuer null statt []. Jede Store-Funktion,
die mit `var out []T` anfaengt, liefert das bei leerem Ergebnis --
Favoriten, Papierkorb, Versionen, Freigaben, Etiketten. Der Kalender war
nur der, der daran zerbricht.
Deshalb die Regel dort, wo alle Antworten durchlaufen: response.JSON
macht aus einer nil-Liste eine leere. Einzelne Objekte und nil bleiben
unberuehrt. In den Stores waere es die falsche Stelle -- es gibt
Dutzende Listenendpunkte, und der naechste faengt wieder mit
`var out []T` an. Eine Regel, an die man sich erinnern muss, ist keine.
Dazu zwei Dinge, die derselbe Fall ans Licht brachte:
- Der Web-Weg legte, anders als CalDAV, keinen Standardkalender an.
Ein neues Konto stand vor einer leeren Ansicht und musste erraten,
dass es selbst einen anlegen soll -- waehrend ein CalDAV-Client, der
sich vorher verbunden hatte, bereits einen sah, den die Oberflaeche
nicht zeigte.
- Die Ansicht faengt eine unerwartete Antwort jetzt selbst ab (?? []).
Der Server schickt kein null mehr; eine Seite sollte daran trotzdem
nicht zerbrechen.
Drei Tests in pkg/response: leere Liste wird [], befuellte bleibt
unveraendert, Objekte und nil bleiben, was sie sind.
Zur Korrektur meiner frueheren Erklaerung: Die fehlenden Cache-Header
waren ein echtes Problem, aber nicht die Ursache dieses Fehlers. Der
gemeldete Bundle-Name stimmte diesmal mit dem ausgelieferten ueberein --
daran war zu sehen, dass es kein alter Stand sein konnte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aether fuehrt zwei Buecher: die Bytes auf der Platte und die Eintraege in
der Datenbank. Wer Dateien von aussen hineinlegt -- per SFTP oder aus
einer Sicherung zurueckgeholt --, erzeugt nur das eine. Die Datei liegt
da, Aether weiss nichts von ihr, und der GC haelt sie nach 24 Stunden
fuer einen Rest und loescht sie.
Neu deshalb internal/rescan, die Gegenrichtung zum GC, und ein Knopf
unter Admin -> Audit. Drei Faelle:
Datei da, kein Eintrag Eintrag anlegen, samt Ordnerkette,
Pruefsumme und Dateityp
Eintrag da, Datei fort Eintrag entfernen, dazu Vorschaubild und
fruehere Fassungen
von aussen ersetzt Groesse und Pruefsumme uebernehmen
Zwei Schritte, nie einer: Check liest nur, Apply aendert. Ein Knopf, der
beides in einem taete, gaebe niemandem die Gelegenheit, vorher
hinzusehen -- und was hier geloescht wird, ist nicht der Papierkorb.
Nicht angefasst werden .trash/ und .versions/. Beide liegen im selben
Baum, gehoeren aber dem System: Wuerden sie uebernommen, tauchten
geloeschte Dateien wieder auf und jede fruehere Fassung wuerde zu einer
eigenen Datei.
Zwei Fallen, die erst der Praxistest zeigte:
- Der Dateityp muss aus Endung *oder Inhalt* kommen. Beim Hochladen
schickt der Browser ihn mit; eine per SFTP abgelegte Datei bringt
niemanden mit, der ihn kennt. Dazu hat das Alpine-Abbild keine
/etc/mime.types, und Gos eingebaute Liste kennt nicht einmal .txt --
jede Textdatei waere als application/octet-stream gelandet.
- COALESCE($1, $2) ohne Cast: Postgres haelt die Parameter dann fuer
Text und bricht mit "operator does not exist: uuid = text" ab. Die
Ordnerkette wurde nie angelegt.
Die Pruefsumme wird trotz des vollstaendigen Lesevorgangs berechnet. Ein
Eintrag ohne sie waere ein Eintrag, dessen Beschaedigung beim
Herunterladen niemand bemerkt -- genau die Zusage, die Aether sonst
ueberall einhaelt.
scripts/rescan-selbsttest.py mit 20 Pruefungen in fuenf Gruppen, darunter
"Pruefen aendert nichts" und "Papierkorb bleibt unangetastet". Nebenbei
raeumt der Freigabe-Selbsttest jetzt auch seinen Papierkorb und nimmt
einen eindeutigen Kontonamen je Lauf -- ein abgebrochener Lauf blockierte
sonst jeden weiteren mit 409.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nach einem Update sah man die alte Anwendung -- und wer dann einen
Bereich anklickte, der erst beim Klick nachgeladen wird (Kalender,
PDF-Betrachter), bekam entweder gar nichts oder einen Fehler, der nach
einem Programmfehler aussieht ("e is not iterable"). Nachgemessen: Der
Browser lud index-BlqR-CVf.js, ausgeliefert wurde index-arO1lebV.js.
Ursache: Der Server schickte fuer index.html **gar kein**
Cache-Control. Fehlt es, schaetzt der Browser anhand von Last-Modified
selbst, wie lange die Antwort wohl gilt -- bei einer Datei, die seit
Tagen unveraendert war, sind das schnell Stunden. Er benutzte also
weiter die alte Seite, die auf Bausteine zeigt, die es nach dem Update
nicht mehr gibt. Auch die Fetches des Service Workers laufen durch
diesen Cache, weshalb selbst dessen "Netz zuerst" nicht half.
Jetzt die uebliche Zweiteilung, die sich aus den Dateinamen ergibt:
/assets/… public, max-age=31536000, immutable
Der Inhalts-Hash steht im Namen. Anderer Inhalt = anderer
Name, also darf der Browser sie fuer immer behalten.
alles sonst no-cache
index.html, sw.js, das Manifest. "no-cache" heisst nicht
"nicht speichern", sondern "vor dem Benutzen rueckfragen" --
mit ETag antwortet der Server meist mit 304 und ein paar
Byte.
Dazu ein Sicherheitsnetz fuer den Fall, dass doch einmal ein alter
Baustein angefordert wird: vite:preloadError laedt die Seite einmal neu.
Einmal, per sessionStorage-Merkmal -- laege der Fehler woanders, drehte
sich die Seite sonst endlos im Kreis.
Das ist der Unterschied zwischen "ich leere eben den Cache" und einem
Menschen, der am Handy vor einer Seite sitzt, auf der nichts passiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Freigabelinks sind der einzige Teil von Aether, den man ohne Anmeldung
erreicht -- und hatten als einziger Bereich keinen einzigen Test. Der
neue scripts/sharing-selbsttest.py geht sie mit 24 Pruefungen ab, mit
einem eigens angelegten Zweitnutzer, ohne den sich "fremde Dateien
bleiben fremd" gar nicht pruefen laesst.
Beim ersten Lauf sofort drei Befunde:
1. Der Download-Zaehler blieb auf 0. Die Zaehlung lief in einer Goroutine
mit dem Kontext der Anfrage -- der wird abgebrochen, sobald die
Antwort draussen ist, und die Zaehlung verlor das Rennen fast immer.
Jetzt context.WithoutCancel mit eigener Frist.
2. owner_id, file_id und die Link-Kennung standen in der oeffentlichen
Antwort. Das ganze Datenbankobjekt durchzureichen war bequem, gab aber
die Kennung des Besitzers an jeden heraus, der den Link hat: Zwei
Links desselben Besitzers liessen sich einander zuordnen, ohne dass
irgendwer angemeldet ist. Jetzt nur noch, was die oeffentliche Seite
braucht.
3. Passwoerter liessen sich unbegrenzt durchprobieren. Der oeffentliche
Freigabepfad lag ausserhalb jeder Ratenbegrenzung, und weil jeder
Versuch Argon2 rechnet, war das zugleich ein billiger Weg, den Server
zu beschaeftigen. Jetzt dieselbe Fehlversuchssperre wie bei /dav und
/caldav: zehn Fehlschlaege in 15 Minuten, dann 15 Minuten Pause.
Die Sperre hat eine Folge fuers Testen selbst: Nach einem Lauf ist die
eigene IP 15 Minuten fuer passwortgeschuetzte Links gesperrt. Deshalb
steht die Probe am Ende, und der Test erkennt zu Beginn, ob eine Sperre
aus einem frueheren Lauf noch steht, statt reihenweise Fehlschlaege zu
melden, die keine sind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Pfeile (und die Pfeiltasten) in der Vorschau liefen bisher ueber
alles Anzeigbare. Wer Bilder durchsah, landete mitten in der Reihe auf
einem PDF -- das ganz anders bedient wird: Seiten statt Dateien, eigene
Zoomleiste, auf schmalen Fenstern gar keine Pfeile.
Jetzt blaettert die Vorschau nur noch durch Dateien derselben Art wie
die gerade offene. Bewusst nicht nur fuer Bilder gesondert geloest,
obwohl es dort auffiel: Dieselbe Ueberraschung gaebe es beim Sprung von
einem PDF in ein Video oder in eine Textdatei.
Solange nichts offen ist, zaehlt weiterhin alles Anzeigbare -- sonst
waere die Liste leer, bevor sie ueberhaupt gebraucht wird.
Nebenwirkung mit Absicht: Gibt es nur ein Bild im Ordner, verschwinden
die Pfeile ganz, statt im Kreis durch fremde Dateitypen zu fuehren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Kalender kam im README bislang mit keinem Wort vor -- weder in der
Phasentabelle noch als Abschnitt, obwohl er ein eigener Zugangsweg mit
eigener Anmeldung ist. Jetzt steht er neben WebDAV, wo er hingehoert:
gleiche Adresse-plus-App-Passwort-Logik, gleiche Sperre nach zehn
Fehlversuchen.
Zwei Dinge stehen dort ausdruecklich, weil sie beim Einrichten sonst
eine halbe Stunde Suche kosten:
- Die Kalenderfarbe holt DAVx5 nur beim Erfassen der Sammlungen, nicht
bei jedem Abgleich. Sie bleibt im Telefon alt, bis man dort einmal
"Kalenderliste aktualisieren" antippt. Am Geraet nachgemessen.
- Kalender verwalten kann die Kalender-App des Telefons gar nicht;
Android ueberlaesst das der Sync-App.
Im Plan die Phasen 1 bis 3 auf erledigt, und darunter das, was ueber die
Phasen hinaus dazukam, weil es sich beim Pruefen als noetig erwies:
Umhaengen, EXDATE, getctag/calendar-color, Kalenderverwaltung vom Client
aus, die beiden Selbsttests.
Abbild veroeffentlicht als 2026.09.06 und latest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bisher konnte ein CalDAV-Client Termine verwalten, aber keine Kalender:
MKCALENDAR -> 405 anlegen
PROPPATCH -> 501 umbenennen, umfaerben
DELETE -> 404 loeschen
go-webdav deckt davon nur MKCOL ab, und selbst da verwarf unser Backend
die vom Client gewaehlte Adresse. Alle drei Verben faengt der Handler
jetzt vorher ab (collections.go).
Der Kern steckt aber nicht in den Verben, sondern in der Adresse: Bei
MKCALENDAR sucht *der Client* aus, wo der Kalender liegen soll
("…/calendars/max/arbeit/"). Unsere Kalender lagen bisher zwingend unter
ihrer UUID -- der Client haette angelegt und auf seine eigene Adresse
sofort ein 404 bekommen.
Deshalb Migration 23 mit einer Spalte path_segment: gesetzt = die
gewaehlte Adresse, NULL = es gilt weiterhin die UUID. Bestehende
Kalender bleiben damit unter genau der Adresse erreichbar, unter der die
eingerichteten Geraete sie kennen -- ein Wechsel haette jedes Telefon
dazu gebracht, seine Termine fuer geloescht zu halten. Aufgeloest wird
beides in einer Abfrage (GetCalendarByRef), die gewaehlte Adresse hat
Vorrang.
PROPPATCH antwortet mit einem Status je Eigenschaft, auch wenn alles
geklappt hat: Es kann teilweise gelingen, und ein nacktes 200
verschwiege, welcher Teil angekommen ist. Was wir nicht aendern koennen,
bekommt 403 statt stillem Schlucken -- ein Client, der seine Aenderung
fuer angekommen haelt, ist schlimmer dran als einer mit klarer Absage.
Ebenso eine unlesbare Farbe: absagen statt raten.
DELETE auf eine Sammlung loescht den Kalender mitsamt allen Terminen --
das ist in WebDAV seine Bedeutung.
CalDAV-Selbsttest von 45 auf 62 Pruefungen: anlegen, unter der
gewaehlten Adresse erreichbar, Farbe aus dem MKCALENDAR uebernommen,
Termin darin ablegen und abrufen, zweites Anlegen auf dieselbe Adresse
405, umbenennen und umfaerben mit Status je Eigenschaft, loeschen, danach
404 -- und dasselbe noch einmal ueber MKCOL, das Verb, das DAVx5 nimmt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uebrig geblieben vom Ein- und Ausbau einer Diagnose-Zeile, mit der zu
klaeren war, warum die Kalenderfarbe nicht im Telefon ankam. Die Antwort:
DAVx5 fragt die Farbe beim normalen Sync gar nicht ab, nur beim Erfassen
der Sammlungen -- nach dem Aktualisieren der Kalenderliste war sie da.
Die Diagnose selbst hat es nie ins Repository geschafft, sie lief nur im
gebauten Abbild. Weg musste sie trotzdem: Sie schrieb jede
PROPFIND-Antwort vollstaendig ins Protokoll, auf INFO-Ebene, und
Kalendernamen gehoeren nicht in Logdateien.
Beide Selbsttests gruen (45 CalDAV, 36 JSON).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beide Eigenschaften fragt jeder verbreitete Client ab, und beide kennt
go-webdav v0.7.0 nicht:
getctag die billige Frage "hat sich in diesem Kalender etwas
geaendert?". Ohne sie laedt ein Client bei jedem
Abgleich die Liste aller Termine herunter, um sie zu
vergleichen -- richtig, aber teuer an Daten und Akku.
calendar-color die Farbe des Kalenders. Ohne sie zeigt das Telefon
ihn in seiner eigenen Vorgabefarbe, und die Farbgebung
dort hat mit der in der Cloud nichts mehr zu tun.
Die Bibliothek beantwortet einen festen Satz Eigenschaften und bietet
keinen Haken, eigene dazuzulegen (server.go, propFindCalendar). Deshalb
faengt der Handler PROPFIND jetzt ab: Er nimmt die beiden aus der
Anfrage heraus, bevor go-webdav sie zu sehen bekommt, und haengt sie der
Antwort als eigenen propstat-Block wieder an.
Das Herausnehmen ist der Punkt, an dem es sonst schiefgeht: Ohne das
meldet go-webdav sie als "nicht gefunden", und dieselbe Eigenschaft
stuende zweimal in derselben Antwort -- einmal mit 404, einmal mit 200.
Was ein Client daraus macht, ist Gluecksache.
Zwei Fallen dabei, beide abgedeckt: Fragt ein Client *nur* nach diesen
Eigenschaften, bliebe ein leeres <prop> uebrig, das go-webdav abweist --
dann schiebt Aether resourcetype als Platzhalter unter. Und die Farbe
geht als #RRGGBBAA hinaus, mit Alpha-Anteil, weil manche Clients sie
sonst gar nicht anzeigen.
Der Neuaufbau des Anfragerumpfs ist verlustfrei, weil in einem PROPFIND
ausschliesslich leere Elemente stehen: Name und Namensraum sind alles,
was eine Eigenschaft dort ausmacht.
Unit-Tests fuer die XML-Arbeit (propfind_test.go), CalDAV-Selbsttest von
39 auf 45 Pruefungen: beide Eigenschaften mit 200, Apple-Schreibweise,
ctag aendert sich bei Aenderungen (ein festgefrorenes ctag ist schlimmer
als gar keins), Anfrage nur nach ihnen, Farben in der Kalenderliste, und
ohne Nachfrage kommt auch nichts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Die Plaketten aus dem vorigen Stand nahmen bei mehreren Kalendern viel
Platz im Formular und liefen ueber mehrere Zeilen um. Also zurueck zur
Ausklappliste -- aber nicht zurueck zum reinen Text.
Ein natives <select> kam dafuer nicht in Frage: Dessen Eintraege lassen
sich in keinem Browser verlaesslich einfaerben, und im zugeklappten
Zustand schon gar nicht. Deshalb eine eigene Liste, die im Ruhezustand
aussieht wie die anderen Felder daneben, aber den Farbpunkt des
Kalenders traegt -- gefuellt fuer den gewaehlten, leerer Ring fuer die
anderen, dieselbe Sprache wie in der Kalenderliste.
Eine Falle dabei: Escape muss beim Schliessen der Liste angehalten
werden. Sonst laeuft es weiter bis zum Fenster, wo der Termindialog
lauscht, und macht gleich das ganze Formular zu -- mitsamt allem, was
schon eingetippt war.
Selbsttest weiter 36 gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Farbe ist das, woran man einen Kalender im Raster wiedererkennt --
im Termindialog stand aber nur der Name in einem Auswahlfeld. Wer
"Arbeit" waehlt, soll dabei schon das Blau sehen, in dem der Termin
gleich erscheint.
Deshalb Plaketten statt <select>: Farbpunkt plus Name, alle nebeneinander
sichtbar, kein Aufklappen noetig. Der Punkt spricht dieselbe Sprache wie
ueberall sonst im Kalender -- gefuellt heisst gewaehlt, ein leerer Ring
heisst nicht gewaehlt, genau wie in der Kalenderliste und bei der
Terminfarbe.
Die gewaehlte Plakette bekommt nur einen Hauch der Kalenderfarbe als
Grund. Acht kraeftig ausgefuellte Toene nebeneinander erschluegen sonst
das ganze Formular, in dem die Farbe eigentlich der Nebendarsteller ist.
Als CalendarPicker in controls.tsx, wo schon Toggle und ColorSwatches
liegen -- damit die naechste Stelle, die eine Kalenderwahl braucht,
nicht wieder bei null anfaengt.
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>
Jede andere Seite zeigt neben dem Titel ein Strichsymbol (Papierkorb,
Geteiltes, Etiketten, ...) - beim Kalender fehlte es, PageHeader bekam
keinen icon-Prop. Dasselbe Kalendersymbol wie in der Seitenleiste.
Co-Authored-By: Claude Sonnet 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>
Marcs Sorge ernst genommen und geprüft statt behauptet: eine
absichtlich kaputte Migration in einer Wegwerf-Datenbank laufen lassen.
Ergebnis: schema_migrations bleibt auf dirty=true stehen, und main.go
bricht bei jedem weiteren Start mit log.Fatal ab – das gilt für jede
Migration, nicht nur die des Kalenders, und stand schon vorher so im
Code.
Drei Sicherungen vor der ersten Kalendermigration: nur CREATE TABLE IF
NOT EXISTS (idempotent, nie ALTER an Fremdem), erst auf der Dev-Instanz
hoch und runter, ein pg_dump vor dem ersten produktiven Ausrollen.
Die Notfallzeile (UPDATE schema_migrations SET dirty=false) steht jetzt
ganz oben in TESTING.md – nicht nur für den Kalender, für jede künftige
Migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Kein Plugin: Der Projektplan legt in Abschnitt 8 selbst fest, dass
Extension-Points erst nach einem stabilen Core kommen und Kernfeatures
nicht als Plugin gebaut werden. Ein Plugin-System vorzuziehen hieße, die
Schnittstelle nach genau einem Nutzer zu entwerfen. Stattdessen ein
eigenes Paket, in einer Zeile angeschlossen, abschaltbar – der natürliche
erste Kandidat, wenn es das System einmal gibt.
Teamkalender brauchen nichts Neues: acl.Access trägt bereits einen
Prinzipal statt eines Eigentümers, Konto oder Gruppe. Ein Kalender, der
einer Gruppe gehört, ist der Teamkalender; persönliche Freigabe bleibt
die Freigabe an ein Konto mit read/write.
Oberfläche festgehalten: mehrere Kalender übereinander, Sichtbarkeit per
Klick auf den farbigen Haken, serverseitig je Nutzer gespeichert – samt
eigener Farbe je Nutzer, die in derselben Zeile fast nichts kostet.
Farben getrennt notiert: Kalenderfarbe (calendar-color, de facto überall
unterstützt) und Terminfarbe (RFC 7986, Weiterreichung ans Telefon
ungeprüft).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>