main
115
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f064eb62c7 |
fix(drawing): zwei stille Wege in den Datenverlust
Feinschliff vor dem Ausrollen, beim Durchlesen mit fremdem Blick gefunden und
beide nachgestellt.
Ein Fehler beim Laden ergab ein leeres Zeichenbrett. fetch wirft bei 404 oder
403 nicht. Der Rumpf {"error":"file not found"} ist gültiges JSON, JSON.parse
gelang, elements war undefined – und die Zeichnung ging leer auf. Sie sah aus,
als wäre sie verloren. Wer dann speichert, macht es wahr. Gegen die laufende
Instanz nachgestellt, bevor ich es behoben habe.
Weiterzeichnen während des Speicherns ging verloren. Der Upload lief mit dem
Stand von Sekunde 0; die Fassungsnummer wurde hinterher aus aktuell.current
gelesen – und das war inzwischen ein anderer Stand. Die neuen Striche galten
als gespeichert, ohne je hochgegangen zu sein, und verschwanden beim Schliessen
ohne Rückfrage. Jetzt wird der Stand einmal festgehalten: Er geht hoch, seine
Nummer wird vermerkt, das Vorschaubild zeigt denselben.
Das Laden steht dafür jetzt in ladeZeichnung() statt im Effekt – der Weg hat
drei Fallen, und zwei enden in verlorener Arbeit; das gehört prüfbar. Vier neue
Tests, unter Mutation nachgewiesen: Ohne die Statusprüfung werden zwei rot.
Bewusst so gelassen: Leserecht fällt erst beim Speichern auf. FileItem trägt
kein Rechtefeld, der Text-Editor verhält sich seit jeher genauso, und das
Zeichenbrett bleibt offen – die Arbeit ist also nicht weg.
Geprüft: 57 Vitest-Tests, alle acht Selbsttest-Suiten grün, alle Go-Pakete
grün, tsc sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
cedcb1ba6f |
fix(drawing): Rückfrage nach dem Speichern, Vorschaubild in der Liste, eigener Dialog
Drei Punkte aus Marcs Test. „Close without saving?" kam, obwohl gerade gespeichert war. Der Schalter „geändert" wurde in onChange gesetzt – und das feuert bei jeder Kleinigkeit, auch bei Mauszeiger, Auswahl und Bildlauf. Sekunden nach dem Speichern stand er wieder auf offen. Jetzt zählt getSceneVersion den Inhalt statt der Bedienung. Das Vorschaubild kam nicht in der Liste an. Der Editor lädt es nach dem Speichern hoch, die Liste kannte aber nur die Antwort des Uploads – und die entstand, bevor es das Bild gab. Ohne Neuladen blieb die Kachel leer. Sie wird jetzt ein zweites Mal benachrichtigt. Rückfragen laufen über den ConfirmDialog des Projekts statt über window.confirm. Beim Schliessen über die Zurück-Geste gibt der Griff false zurück, solange der Dialog steht: Sonst wäre der Verlaufseintrag schon verbraucht, und ein „Cancel" liesse die nächste Zurück-Geste aus der Anwendung führen. Nebenbei: Die Beschriftung des Schliessen-Knopfes versprach „Esc". Den Griff gab es nie – und er wäre falsch, weil das Zeichenbrett die Taste selbst braucht (Abwählen, Werkzeug verlassen). Wer sie abfängt, schliesst die Zeichnung, während jemand nur ein Rechteck abwählen wollte. Versprechen entfernt statt Griff gebaut. Geprüft: tsc sauber, 53 Vitest-Tests, Zeichnungs-Selbsttest 18/18. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dd68bc594d |
feat(drawing): Vorschaubilder und ein Zeichenbrett, das zur Oberfläche passt
Vorschaubilder entstehen sonst serverseitig aus den hochgeladenen Bytes – bei
einer Zeichnung steht dort JSON, daraus wird ohne Browser kein Bild. Wer sie
gerade bearbeitet, hat aber genau den Browser, der sie ohnehin zeichnet.
Neu: POST /api/v1/files/{id}/thumbnail. Der Editor schickt nach dem Speichern
ein PNG hinterher.
Den Bytes wird dabei nicht geglaubt: Sie werden nicht abgelegt, sondern durch
denselben Weg geschickt wie jedes andere Vorschaubild – dekodieren,
verkleinern, neu kodieren. Danach ist es mit Sicherheit ein Bild und hat die
übliche Grösse; nichts anderes kann im thumbs-Bereich landen. Dazu Schreibrecht
auf die Datei: Wer sie nicht ändern darf, bestimmt auch nicht, wie sie in der
Liste aussieht.
Das Hochladen läuft nach dem Speichern und ohne darauf zu warten. Ein
fehlgeschlagenes Vorschaubild ist ein Schönheitsfehler, kein Grund, das
Speichern scheitern zu lassen – und bleibt deshalb stumm.
Politur am Zeichenbrett: dunkles Thema (ein weisses Brett in dunkler Oberfläche
war der auffälligste Bruch), „Unsaved" als Marke statt eines Punktes hinter dem
Namen, Strg+S, Dateisymbol und Name ohne Endung.
„Open" und „Save to…" sind aus. Beide gehen am Browser vorbei: „Open" ersetzte
den Inhalt dieser Zeichnung durch eine fremde Datei, „Save to…" schriebe eine
zweite Fassung in den Download-Ordner, die niemand wiederfindet.
Selbsttest 12 → 18 Prüfungen, darunter die beiden Ablehnungen (kein Bild → 400,
fremder Typ → 415).
Geprüft: go build/vet sauber, alle Go-Pakete grün, 261 Prüfungen der acht
Selbsttest-Suiten grün, 53 Vitest-Tests, tsc sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2e1eaf9cbf |
fix(drawing): Zeichnung liess sich nicht wieder oeffnen
Anlegen und bearbeiten ging, beim zweiten Öffnen blieb das Zeichenbrett leer.
Die erste Fassung schrieb den appState roh mit JSON.stringify heraus. Darin
steckt `collaborators`, und das ist eine Map. JSON kennt keine Maps; es wurde
`{}` daraus. Beim Laden bekam Excalidraw ein leeres Objekt, wo es eine Map
erwartet, und brach ab. Beim ersten Öffnen fiel das nicht auf – da war noch
nichts gespeichert.
Jetzt über die Werkzeuge des Pakets: serializeAsJSON schreibt nur, was eine
Zeichnung ausmacht, restore setzt beim Laden zusammen, was fehlt. Selbst zu
prüfen hiesse, Excalidraws Feldliste nachzubauen und bei jeder Fassung
nachzuziehen.
Eine Sache erledigt restore allerdings nicht – nachgemessen, nicht angenommen:
Ein bereits verunglücktes collaborators: {} reicht es unverändert durch; die
Map entsteht nur, wenn das Feld ganz fehlt. Neue Dateien haben es nicht mehr,
aber die aus jener Stunde wären dauerhaft kaputt geblieben. Deshalb wird es
beim Laden zusätzlich entfernt – Marcs bereits angelegte Zeichnungen öffnen
damit wieder.
Fünf Tests in drawing.test.ts, darunter einer, der festhält, dass restore
allein nicht reicht. Zwei Stolpersteine dabei, in vite.config.ts vermerkt:
Excalidraw importiert roughjs/bin/rough ohne Endung (Nodes Auflösung verlangt
sie, der Bündler nicht) und fasst beim Laden ein Canvas an, das jsdom nicht
hat.
Geprüft: tsc sauber, 53 Vitest-Tests, Zeichnungs-Selbsttest 12/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6fc157bc07 |
feat(files): Zeichnen und Mindmaps mit Excalidraw
Eine Zeichnung ist eine Datei, kein neues Ding – das ist die ganze Entscheidung dahinter. .excalidraw ist JSON; damit erbt sie Ordner, Gruppenordner, Papierkorb, Versionen, Freigabelinks, interne Freigaben und die Suche. Keine neue Tabelle, kein zweites Rechtemodell. Gespeichert wird über denselben Upload wie alles andere, wie beim Text-Editor in der Vorschau. Drei Sorgen, alle nachgemessen statt gehofft: Das Hauptbündel wächst von 484,11 auf 485,22 kB – das Zeichenbrett ist ein eigenes Stück (584 kB + 142 kB CSS) und wird nachgeladen wie der PDF-Betrachter. Wer nie zeichnet, holt es nie. Die Schriften: Die Richtlinie ist default-src 'self' ohne font-src, Excalidraw hätte seine handgezeichneten Schriften von aussen geladen und still eine Ersatzschrift gezeigt. Sie werden jetzt beim Bauen kopiert und über window.EXCALIDRAW_ASSET_PATH gefunden – gesetzt im Modul, nicht per Inline-Skript, das wäre von derselben Richtlinie blockiert. Xiaolai bleibt draussen: 13 der 14 MB für die chinesische Schrift. Die Reihenfolge der Dateiarten: .excalidraw wird vor code und text geprüft, sonst öffnete eine Zeichnung im Texteditor als Zahlenkolonne (json steht in codeExt). Kein gleichzeitiges Arbeiten. Die Komponente könnte es, aber den Transport bringt sie nicht mit – das wäre ein zweiter Dienst neben Aether. Solange es ihn nicht gibt, überschreibt der Zweite den Ersten. Zwei Dinge fangen das ab: Vor dem Speichern wird geprüft, ob sich die Fassung geändert hat, und dann gefragt statt überschrieben. Und Zeichnungen behalten ihre Fassungen von der ersten Minute an. Der zweite Punkt ist erst durch den Selbsttest aufgefallen: Versionierung ist sonst bewusst aus (Migration 000006), damit der Speicher nicht vollläuft. Für Zeichnungen stimmt die Rechnung nicht – sie werden in sich immer wieder bearbeitet und sind ein paar Kilobyte gross. Ohne sie wäre die Warnung im Editor ein Versprechen ohne Deckung gewesen. Neuer Selbsttest mit 12 Prüfungen: Er prüft die These nach, auf der alles beruht – wenn eine Zeichnung wirklich eine Datei ist, muss sie können, was Dateien können. Nebenbei: db-tests.sh liess je Lauf ein anonymes Postgres-Volume liegen (rund 49 MB). `docker rm -v` nimmt es jetzt mit; zehn verwaiste entfernt. Geprüft: go build/vet sauber, alle 15 Go-Pakete grün, 255 Prüfungen der acht Selbsttest-Suiten grün, 48 Vitest-Tests, tsc sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
662c0ebdc1 |
security(rescan): Notbremse vor dem harten Löschen
internal/rescan ist das einzige Stück Aether, das hart löscht: DELETE FROM files, kein Papierkorb. Entfernt wird, was beim Durchgang über die Platte nicht gesehen wurde. Der Ernstfall ist nicht ausgedacht. LocalFS.Walk gibt für ein nicht vorhandenes Verzeichnis nil zurück – kein Fehler, keine Einträge. Genau der Fingerabdruck eines Bind-Mounts, der nach einem Neustart nicht hochkam, oder eines Tippfehlers in DATA_PATH. Dann gilt jede Datei als verwaist, und ein einziger Aufruf mit ?anwenden=true löscht den gesamten Bestand aus der Datenbank. Die Bytes lägen noch da, aber niemand wüsste mehr, dass es sie gibt – und der GC räumte sie 24 Stunden später ab. Der GC hat diese Bremse seit dem 12.09. Beide gleichen Platte und Datenbank ab, nur aus entgegengesetzter Richtung, und beide haben denselben Fehlerfall: Eine Seite antwortet fälschlich mit „leer". Der GC löscht dann Blobs – die Datenbank weiss hinterher noch, was fehlt. Hier fällt die Zeile selbst. Die gefährlichere Seite war die ungesicherte. Zwei Bremsen: Die Notbremse (nichts gefunden, Datenbank kennt Dateien) lässt sich nicht übersteuern. Die Mengenbremse (mehr als die Hälfte) verlangt &bestaetigt=true. Die leere Installation ist ausgenommen. Dazu umgebaut: erst sammeln, dann entscheiden, dann löschen. Vorher wurde mitten in der Schleife gelöscht; die Gesamtzahl kennt man dort erst, wenn die Hälfte schon weg ist. Der Bericht nennt den Grund und ob er bestätigbar ist – die Oberfläche soll keinen Knopf anbieten, der nichts bewirkt. Ein abgebrochener Lauf steht rot in der Karte und im Audit-Log; gerade der, denn er ist der Hinweis darauf, dass mit dem Datenordner etwas nicht stimmt. Fünf Tests, unter Mutation nachgewiesen. Der Ernstfall läuft am echten Gerät: fünf Zeilen in der Datenbank, ein Wurzelverzeichnis ohne users/ – nach Apply stehen alle fünf noch, auch mit Bestätigung. audit und thumbnails bekommen bewusst keine Go-Tests; die Begründung steht in BEFUNDE.md statt als ewig offener Punkt. Nebenbefund behoben: kalender-teilen-selbsttest.py leckte je Lauf ein App-Passwort des Besitzers. Nach achtzehn Läufen war maxAppPasswords = 20 erreicht, und zwei Suiten scheiterten mit 409, ohne dass am Code etwas kaputt war. Geprüft: go build/vet sauber, alle Go-Pakete grün, 243 Prüfungen der sieben Selbsttest-Suiten grün, 48 Vitest-Tests, tsc sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6fab2388ca |
security(sharing): Freigabelink prüft die Prüfsumme, Gruppenordner teilbar
Beim Lesen von internal/sharing fiel eine ungleiche Zusage auf: Der angemeldete Download prüft seit dem 12.09. die SHA-256-Summe beim Ausliefern und lässt die Verbindung fallen, wenn sie nicht stimmt. Der öffentliche Freigabelink lief durch ein blankes io.Copy – ungeprüft. Ausgerechnet der Weg nach draussen. Wer die Datei angemeldet holt, ist der Eigentümer und merkt, wenn etwas kaputt ist; wer einen Link bekommt, merkt es nicht. In CLAUDE.md steht das unter „nicht verhandelbar": beim Upload berechnen, beim Download verifizieren. Die Regel galt an einer von zwei Stellen. Die Mechanik liegt jetzt in files/verified.go und wird von beiden gerufen. Nachgewiesen mit gekipptem Bit auf der Platte: Über den Freigabelink kommt die Datei ebenfalls nicht mehr vollständig an. Gruppenordner: Create übergab user.ID statt des aufgelösten Prinzipals – eine Datei im Gruppenordner liess sich nicht verlinken, auch nicht vom Mitglied. Marcs Entscheidung: Das soll gehen, der Empfänger sieht ohnehin nur die Datei, nie den Ordner darum. Verlangt wird Schreibrecht: Einen Link zu erzeugen heisst, die Datei öffentlich zu machen; wer sie nur geliehen lesen darf, soll sie nicht weiterreichen können (403). expires_in_hours hatte keine Obergrenze. time.Duration rechnet in Nanosekunden; ab etwa 2,6 Mio Stunden läuft int64 über, die Dauer wird negativ, und der Link ist abgelaufen in dem Moment, in dem er entsteht – ohne Fehlermeldung. Jetzt zehn Jahre, darüber 400. Dazu X-Content-Type-Options: nosniff, das der angemeldete Weg längst setzt. Sechs Go-Tests für internal/sharing, unter Mutation nachgewiesen: Eigentümerprüfung entfernt, Ablaufprüfung entfernt und Token verkürzt machen jeweils genau den zugehörigen Test rot. Selbsttests erweitert: sharing 24 → 31 Prüfungen, pruefsumme 14 → 16. Geprüft: go build/vet sauber, alle Go-Pakete grün, 241 Prüfungen der sieben Selbsttest-Suiten grün, 48 Vitest-Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a7528d7365 |
test(frontend): Wächter gegen deutschen Oberflächentext – und fünf Reste
Konsolidierung: Bestandsaufnahme auf den Stand vom 24.09. gebracht (Punkte 1, 2, 2b und die Tests für documents/tags sind erledigt; sharing, rescan, audit und thumbnails bleiben offen; jsdom ist seit dem 18.09. drin). Beim Nachsehen fielen fünf Übersetzungsreste auf: label="Ganztägig", aria-label="Kalendermenü", label="Bearbeiten" und zweimal "Löschen". Alle in **doppelt** gequoteten JSX-Attributen – die Ersetzungsliste hatte nur die einfach gequoteten, und das damalige Prüfskript hielt Einzelwörter ohne Leerzeichen für Bezeichner. Die Prüfung steht deshalb jetzt als Test, nicht als Skript. Sie läuft bei jedem npm test mit. Der erste Wächter taugte nichts, und das kam nur durch die Mutation heraus: Er prüfte allein auf Umlaute und liess "Papierkorb ist leer" durch. Wer einen Wächter baut, muss ihn einmal angreifen. Jetzt Umlaute plus eine Wortliste mit Wortgrenzen, nachgewiesen an vier Mutationen. Kommentare werden vorher entfernt – mit einem kleinen Durchlauf statt zeilenweise: Blockkommentare haben Fortsetzungszeilen ohne führenden Stern, die beim ersten Versuch fünf Fehlalarme erzeugten. Ehrlich dazu: Das ist ein Netz, kein Beweis. Ein deutscher Satz ohne Umlaut und ohne gelistetes Wort käme durch. Steht so im Test. @types/node kam dazu, weil der Test den Quelltext von der Platte liest. Geprüft: tsc sauber, 48 Vitest-Tests, alle 234 Prüfungen der sieben Selbsttest-Suiten grün. Gebautes Bündel gegengeprüft – deutsche Texte weg, englische da. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e1259dd55c |
refactor(calendar): Wochentage und Monate aus Intl statt aus sechs Arrays
Sechs handgeschriebene Arrays in drei Dateien, und Intl kam im ganzen Projekt nicht vor – obwohl jeder Browser es seit Jahren mitbringt. Die Doppelpflege war nicht nur lästig, sie war eine Falle: DayDialog begann seine Woche am Sonntag, die anderen am Montag, jeweils passend zu ihrer Indizierung. Beide waren für sich richtig. Wer aber eines der Arrays in eine neue Ansicht kopiert hätte, hätte sich einen Off-by-one gebaut, der nur an sieben Tagen im Jahr auffällt. Jetzt eine Ordnung (Montag zuerst, wie weekdayIndex) plus eine ausdrücklich benannte zweite Liste für alles, was direkt mit getDay() indiziert – statt eines Umrechnens am Aufrufort, wo der Off-by-one sonst entsteht. Verhaltensgleich belegt: Die erzeugten Namen stimmen zeichengenau mit den gelöschten Arrays überein, vor dem Austausch verglichen. Die Sprache ist fest auf 'en' gesetzt, nicht die des Browsers. Intl.DateTimeFormat(undefined, …) nähme die Einstellung des Rechners und setzte auf einem deutschen System „Montag" neben ein englisches „Repeat" – genau der Mischmasch von vorgestern, nur durch die Hintertür. Kommt eine Sprachwahl (IDEAS.md), ist uiLocale die eine Stelle, an der sie andockt. Fünf neue Tests, unter Mutation nachgewiesen: Nimmt man dem Sonntag seinen Platz vorn, wird der Test rot. Geprüft: tsc sauber, 46 Vitest-Tests, alle 234 Prüfungen der sieben Selbsttest-Suiten grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8586664fae |
feat(ui): Rückmeldung zu Ende verdrahtet – und zwei Lügen entfernt
Die Meldungen sprachen bisher nur im Dateibrowser. Papierkorb, Freigabe-Links,
„Für mich freigegeben", Profil, Theming, Audit und die Nutzerliste schwiegen
weiter – und das war schlechter als vorher: Wer gelernt hat, dass Aether
Bescheid gibt, hält Schweigen für Erfolg.
Nicht blind verdrahtet. Sechs Stellen bleiben absichtlich still, samt Grund in
TESTING.md: die Suche (bricht bei jedem Anschlag die vorige ab), das Kopieren
der Prüfsumme (ohne HTTPS gibt es keine Zwischenablage, der Wert steht ja da),
die Theming-Vorgaben, die Absender-Vorschläge, die OCR-Abfrage im Hintergrund
und useAuth. Wo ein Dialog seinen Fehler bei sich anzeigt, bleibt es dabei –
dort gehört er hin, nicht an den Bildschirmrand.
Dabei zwei Fälle gefunden, die schlimmer sind als Schweigen:
**ProfilePage meldete „gespeichert", wenn das Speichern fehlgeschlagen war.**
`updateProfile(...).catch(() => {})`, danach ungeprüft das Häkchen.
**TagEditDialog riet die Ursache:** Jeder Fehler meldete „That name is already
taken" – auch ein Netzausfall, auch ein 500er. Jetzt steht dort der Satz des
Servers. Im TagDialog war genau das schon einmal aufgefallen und behoben
worden; hier stand es noch.
Dazu zwei Aufrufe ganz ohne Absicherung: das Wiederherstellen aus dem
Papierkorb und das Löschen eines Freigabe-Links. Und das Kopieren eines
Links scheiterte ohne HTTPS stumm – der Knopf tat schlicht nichts.
Eigener Fehler nebenbei behoben: Ich hatte deutsche Anführungszeichen in
englische Meldungen gesetzt.
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>
|
||
|
|
16258640ad |
docs(ideas): Mehrsprachigkeit notiert, samt der 144 Server-Meldungen
Gemessen statt geschätzt: ~986 Zeichenketten im Frontend, 144 Fehlermeldungen im Backend. Der Punkt, den man dabei vergisst, ist der zweite Teil: Die Server-Meldungen landen seit dieser Woche in den Toasts. Wer nur das Frontend übersetzt, bekommt eine deutsche Oberfläche, die bei jedem Fehler auf Englisch umschaltet. Dazu der Intl-Teil, der sich auch ohne jede Übersetzung lohnt: sechs handgeschriebene Wochentag- und Monats-Arrays, und Intl wird nirgends benutzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7005b2ce01 |
docs(testing): Prüfpunkte gegen die Platzhalter-Geister
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4208676aa9 |
fix(ui): Platzhalter blitzt auch beim Tab-Wechsel nicht mehr auf
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> |
||
|
|
0591a40d12 |
feat(calendar): Oberfläche auf Englisch, Standardkalender heißt „Personal"
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> |
||
|
|
1d10c6bd0e |
feat(ui): Platzhalter beim Laden und ordentliche Leerzustände
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> |
||
|
|
94a803bf7a |
feat(ui): Meldungen mit Rückgängig, dazu jsdom und ein Fehler in der letzten Behebung
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>
|
||
|
|
7d45941c03 |
refactor(office): toten JWT-Schlüssel entfernen, Doku richtigstellen
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> |
||
|
|
944b2c63e2 |
feat(office): mobileEdit am Telefon, PDF-Umweg entfällt
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> |
||
|
|
6042f6f2f6 |
test(tags): bleibt ein fremdes Etikett fremd?
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> |
||
|
|
76176a45bc |
test(documents): die drei stillen Zustaende der OCR-Warteschlange
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> |
||
|
|
2cc02941b9 |
fix(files): beschaedigte Downloads werden gemeldet und abgebrochen
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> |
||
|
|
c26bcba012 |
fix(bg): Hintergrundarbeit reisst den Prozess nicht mehr mit
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
|
||
|
|
89f0817dcb |
docs: Bestandsaufnahme 12.09.2026
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> |
||
|
|
ef604973c8 |
security(deps): drei erreichbare Luecken in x/image und x/text geschlossen
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> |
||
|
|
9f476c56cf |
docs(ideas): Aufgabenansicht -- der Weg dorthin heisst tasks.org
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> |
||
|
|
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> |
||
|
|
5d90c0a2d2 |
feat(calendar): Kalender an Nutzer und Gruppen freigeben
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> |
||
|
|
8fdde182b7 |
feat(calendar): Abgleich nur der Aenderungen (sync-collection, RFC 6578)
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> |
||
|
|
df9d2d6ded |
docs(env): fuenf gelesene Schluessel fehlten in .env.example
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> |
||
|
|
70c963744c |
docs: Wiederherstellung ohne Postgres und Update geprobt, nicht behauptet
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>
|
||
|
|
e9f69f7d7f |
feat(uploads): Fortschritt in MB, Tempo und Restzeit; Abbruchkreuz rot
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> |
||
|
|
511213b40d |
feat(uploads): laufenden Upload abbrechen; fix(pdf): Zoomgeste entlasten
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> |
||
|
|
ea90f43e77 |
refactor(frontend): PdfViewer und FileBrowser aufteilen, Tests dazu
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> |
||
|
|
8e27c703d2 |
fix(ui): eine Breite fuer Seitenleiste und Kopfleiste
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> |
||
|
|
fbaf24bdbd |
fix(api): leere Listen als [] statt null -- Kalender blieb weiss
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>
|
||
|
|
55dcf517f2 |
feat(admin): Abgleich zwischen Platte und Datenbank
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>
|
||
|
|
fce8053d09 |
fix(web): Updates kommen an, ohne den Cache von Hand zu leeren
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>
|
||
|
|
ec8f162fd5 |
security(sharing): Selbsttest fuer Freigabelinks, drei Befunde behoben
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> |
||
|
|
1e1f35955d |
fix(preview): Blaettern bleibt bei einer Art Datei
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> |
||
|
|
ac61df9796 |
docs(calendar): Kalender im README und Stand im Plan nachtragen
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>
|
||
|
|
f71ab710e1 |
feat(calendar): Kalender anlegen, umbenennen, loeschen -- vom Client aus
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>
|
||
|
|
820ffdde6a |
style(calendar): Leerzeile im PROPFIND-Weg
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> |
||
|
|
128d54035a |
feat(calendar): getctag und calendar-color fuer CalDAV-Clients
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>
|
||
|
|
d7b5c1cd5f |
test(calendar): CalDAV-Selbsttest, und fremde Benutzerpfade sauber abweisen
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> |
||
|
|
cc35161e74 |
feat(calendar): Kalenderwahl wieder als Ausklappliste, weiterhin mit Farbe
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> |
||
|
|
7a1f0837f5 |
feat(calendar): Kalenderwahl im Termin mit Farbe statt nur Text
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> |
||
|
|
77bf16b27f |
feat(calendar): Termin einem Kalender zuordnen und umhaengen
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>
|
||
|
|
688bb13bde |
feat(calendar): einzelnen Termin aus einer Reihe streichen
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> |
||
|
|
0c9cc69493 |
feat(calendar): Wiederholungsmuster, Terminfarbe synchronisierbar, Selbsttest
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>
|
||
|
|
79a7cc6a3d |
feat(calendar): Wiederholungen, Tagesansicht, Terminfarben, Farbpunkt als Schalter
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> |