Commit Graph
83 Commits
Author SHA1 Message Date
marcandClaude Opus 5 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>
2026-09-08 22:23:21 +02:00
marcandClaude Opus 5 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>
2026-09-08 21:33:08 +02:00
marcandClaude Opus 5 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>
2026-09-07 19:56:54 +02:00
marcandClaude Opus 5 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>
2026-09-07 01:10:25 +02:00
marcandClaude Opus 5 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>
2026-09-06 23:31:59 +02:00
marcandClaude Opus 5 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>
2026-09-06 23:31:39 +02:00
marcandClaude Opus 5 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>
2026-09-06 23:16:25 +02:00
marcandClaude Opus 5 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>
2026-09-06 23:14:40 +02:00
marcandClaude Opus 5 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>
2026-09-06 03:37:41 +02:00
marcandClaude Opus 5 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>
2026-09-06 03:25:33 +02:00
marcandClaude Opus 5 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>
2026-09-05 18:11:25 +02:00
marcandClaude Opus 5 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>
2026-09-05 08:01:48 +02:00
marcandClaude Opus 5 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>
2026-09-05 07:50:19 +02:00
marcandClaude Opus 5 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>
2026-09-05 00:04:30 +02:00
marcandClaude Opus 5 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>
2026-09-04 13:07:36 +02:00
marcandClaude Opus 5 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>
2026-09-04 09:06:53 +02:00
marcandClaude Opus 5 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>
2026-09-04 01:16:06 +02:00
marcandClaude Opus 5 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>
2026-09-04 00:56:28 +02:00
marcandClaude Opus 5 105ce8de4a fix(calendar): Termine anklickbar, Ganztags-Datum korrekt, Wochenraster mit Stunden
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>
2026-09-03 23:47:08 +02:00
marcandClaude Sonnet 5 ba33706d2e fix(calendar): Symbol in der Kopfleiste ergänzt
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>
2026-09-03 23:36:21 +02:00
marcandClaude Sonnet 5 27a026421f feat(calendar): Phase 3 – Weboberfläche mit Monats-, Wochen- und Listenansicht
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>
2026-09-03 23:14:28 +02:00
marcandClaude Sonnet 5 4d635fff20 feat(calendar): Phase 2 – mehrere Kalender, Export/Import als .ics
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>
2026-09-03 23:03:26 +02:00
marcandClaude Sonnet 5 3f1b2b8a14 feat(calendar): Standard-Zeitzone pro Kalender vorbereiten
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>
2026-09-03 22:56:28 +02:00
marcandClaude Sonnet 5 469274171c fix(calendar): GET auf CalDAV-Sammlungen antwortet statt 404
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>
2026-09-03 22:50:07 +02:00
marcandClaude Sonnet 5 d6e1854f1b fix(calendar): klare Fehlermeldung für GET auf CalDAV-Sammlungen
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>
2026-09-03 22:45:24 +02:00
marcandClaude Sonnet 5 87788817f8 feat(calendar): Phase 1 – CalDAV lesend und schreibend, ein Kalender pro Nutzer
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>
2026-09-03 22:27:33 +02:00
marcandClaude Sonnet 5 e44bed0a34 docs: Migrationsausfall nachgestellt und Notfallweg dokumentiert
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>
2026-09-03 22:08:09 +02:00
marcandClaude Opus 5 0cea81978f docs(kalender): sechs Grenzen statt eines Plugin-Systems
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>
2026-09-03 21:56:20 +02:00
marcandClaude Opus 5 5808ff5ef8 docs(kalender): Plugin-Frage, Teamkalender über Gruppen, Oberfläche und Farben
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>
2026-09-03 21:44:34 +02:00
marcandClaude Opus 5 6f0efc8e7b docs(kalender): Festlegungen, Felder, Clients und das Teilen nachgetragen
Teilen war in der ersten Fassung zu pauschal abgeräumt. Es meint drei
Dinge: dasselbe Konto auf mehreren Geräten (ab Phase 1), ein Kalender für
einen anderen Nutzer (Tabelle plus Bedingung in ListCalendars, keine
Protokollarbeit) und ein öffentlicher Abo-Link (Aether hat Freigabelinks
schon). Nur Einladungen per E-Mail bleiben draußen.

Dazu Marcs Festlegungen: mehrere Kalender und VTODO von Anfang an,
Anhänge kein Ziel, Erinnerungen weckt die App auf dem Telefon.

Und die Einschränkung, die sonst später überrascht: Androids
Kalenderspeicher kennt keine Aufgaben. Ohne Tasks.org o. ä. bietet DAVx5
sie nicht an, und Samsung Kalender zeigt sie nie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:33:06 +02:00
marcandClaude Opus 5 8f90a7fbd6 docs: Plan für einen Kalender in Aether
Radicale ist für einen einfachen Kalender zu umständlich, vor allem fehlt
die OIDC-Anbindung. Der Plan hält fest, warum das als Modul in Aether
besser aufgehoben ist: Ein CalDAV-Client kann ohnehin kein OIDC, er will
Benutzername und Passwort – und App-Passwörter für DAV gibt es hier schon.

Vor dem Schreiben nachgesehen statt geschätzt: emersion/go-webdav v0.7.0
beantwortet PROPFIND/REPORT selbst, caldav.Filter() filtert, und der
Zeitraumfilter löst Wiederholungen über go-ical und rrule-go auf. Damit
ist der Teil, vor dem ich gewarnt hatte, in der Bibliothek. Zu erfüllen
bleibt eine Schnittstelle mit neun Methoden – Speicherarbeit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:43:56 +02:00
marcandClaude Opus 5 9d152e3677 refactor: App-Umschalter und HONEYBAR_KEY entfernt
Beide waren Ballast, aber aus verschiedenen Gründen.

Der App-Umschalter (EXTERNAL_APPS, AppSwitcher.tsx) hing seit dem
30.07.2026 nirgends mehr in der Oberfläche – auf Marcs Wunsch damals aus
der Seitenleiste genommen, "weil eine gemeinsame Leiste für alle Apps
noch überlegt wird". Diese Leiste gibt es inzwischen: die HoneyBar. Das
Backend las die Variable trotzdem weiter und lieferte external_apps in
der öffentlichen Konfiguration aus. Weg damit, samt Typ und Parser.

HONEYBAR_KEY war der Ausweis für die App-Liste, wenn HoneyOS auf einer
fremden Site liegt. Bei Marc liegen honeyos.duckdns.org und
cloud.honeyos.duckdns.org unter derselben Site, das Sitzungscookie fließt
also mit. Nachgesehen, wie HoneyOS entscheidet: Es prüft zuerst die
Sitzung (SubFromRequest) und greift erst danach zum Schlüssel – die
Leiste kommt hier ohne ihn an ihre Liste. Gedacht war er für Widgets;
die sind vertagt.

Aether schickt damit kein data-key mehr; HoneyOS behält seine Seite der
Prüfung, für den Fall, dass später doch eine App auf fremder Domain dazu
kommt.

Nachgeprüft: go build, go vet und die Tests laufen; das Abbild baut ohne
TypeScript-Fehler; die öffentliche Konfiguration liefert weder
honeybar_key noch external_apps; im gebauten Bundle kommt data-key nicht
mehr vor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:15:28 +02:00
marcandClaude Opus 5 31d51f9bc3 docs(config): Office-Abschnitt der .env.example auf die Schlüssel gekürzt
Die Adressen und Fallstricke stehen im README; hier standen sie ein
zweites Mal. Jetzt sechs Zeilen mit knapper Randnotiz, welche Richtung
welche Adresse meint.

Der Hinweis zu Collabora bleibt: OFFICE_WOPI_URL muss dort die
öffentliche Adresse sein, sonst verweigert der Browser den Rahmen. Das
hat einen Abend gekostet und ist beim Überfliegen nicht zu erraten.

Beim Kürzen selbst wieder in die dokumentierte Falle getappt: zwei leere
Werte mit Kommentar dahinter. Diesmal nachgemessen statt geglaubt – ein
Container mit so einer Zeile bekommt LEER=[# Browser → Office], während
ein gefüllter Wert den Kommentar verliert. Beide Kommentare stehen jetzt
in der Zeile darüber, die ganze Datei ist gegengeprüft.

Nebenbei: veralteter deploy/-Verweis im Kommentar von AppSwitcher.tsx
auf build/ gezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:10:08 +02:00
marcandClaude Opus 5 86da975424 config: 0.0.0.0 als Vorgabe für APP_BIND im Betrieb
Auf Marcs Wunsch. Die bisherige Vorgabe 127.0.0.1 passt nur, wenn der
Reverse Proxy auf demselben Rechner läuft – steht er woanders (bei Marc
auf einem VPS), kommt er an loopback gar nicht heran, und die Vorgabe war
schlicht der falsche Wert für den häufigeren Fall.

Der Preis steht jetzt an allen drei Stellen dabei, statt nur gemeint zu
sein: Der Port ist dann auch aus dem eigenen Netz offen, und TLS endet
erst am Proxy. Im README dazu eine Tabelle, welcher Wert zu welchem
Aufbau gehört – eigener Rechner, anderer Rechner, Tunnel, oder Proxy im
selben Docker-Netz (dann braucht es gar kein ports:).

build/docker-compose.yml behält 127.0.0.1: Das ist die
Entwicklungsumgebung auf diesem Rechner, dort ist loopback richtig.

Gegengeprüft: Frische Kopie aus den Beispieldateien veröffentlicht auf
0.0.0.0; APP_BIND=127.0.0.1 in der .env engt wieder auf loopback ein.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:06:29 +02:00
marcandClaude Opus 5 69912081dd docs(config): .env.example beschreibt nur noch den Betrieb
Vier Schlüssel darin steuerten den mitgelieferten Collabora-Container
statt Aether: OFFICE_ALIASGROUP, OFFICE_SSL, OFFICE_SSL_TERMINATION,
OFFICE_PORT. Wer Office selbst betreibt, richtet das in dessen eigener
Compose-Datei ein – in einer Vorlage für den Betrieb der Anwendung haben
sie nichts zu suchen.

Sie stehen jetzt kommentiert in build/docker-compose.yml, wo sie wirken;
die Erklärung zu aliasgroup1 (regulärer Ausdruck, Punkte maskieren, Port
anhängen) ist dorthin mitgewandert. Die Rückfallwerte im Compose gab es
schon, ein frischer Aufbau ohne diese vier läuft also weiter.

Meilisearch ist als "noch nicht gebaut" gekennzeichnet, statt einen
Schalter vorzutäuschen: Kein Go-Code liest die beiden Werte, gesucht wird
mit der Volltextsuche von Postgres.

Der Rest bleibt ausführlich – die Vorlage ist auch die Übersicht, was
möglich ist.

Gegengeprüft: Beide Compose-Dateien lösen mit der gekürzten Vorlage als
einziger .env sauber auf; Collabora bekommt aus den Rückfällen
aliasgroup1=http://app:8080 und ssl.enable=false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:03:34 +02:00
marcandClaude Opus 5 471679ff64 refactor(repo): deploy/ heißt jetzt build/ – der Name sagte das Gegenteil
"deploy" klang nach Ausrollen, drin steht das Gegenteil: das Bau-Rezept
und das Compose, das aus dem Quelltext baut. Ausgerollt wird mit
docker-compose.example.yml im Wurzelverzeichnis. Marc ist genau darüber
gestolpert.

Zwei Dinge mussten dafür vorher fest werden, sonst hätte das Umbenennen
die Entwicklungsdatenbank gekostet: Compose leitet den Projektnamen aus
dem Ordnernamen ab und hätte unter "build" ein neues, leeres Projekt mit
neuem, leerem Volume angelegt. Der Projektname steht deshalb jetzt im
Compose (name: aether), und das Volume trägt seinen alten Namen
ausdrücklich weiter (deploy_postgres_data) – unschön, aber die Daten
bleiben.

Ebenfalls angepasst: .gitignore. Dort stand deploy/.env; nach dem
Umbenennen wäre build/.env nur noch von der allgemeinen .env-Zeile
gedeckt gewesen.

Nachgeprüft nach dem Umzug: Abbild baut, Stapel startet, 113 Dateien in
der Datenbank wie vorher, Volume unverändert deploy_postgres_data,
GET / → 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 09:24:09 +02:00
marcandClaude Opus 5 a7a67da040 docs(config): eine .env.example statt zweier, die auseinanderliefen
Es gab sie doppelt: schlank im Wurzelverzeichnis (für den Betrieb) und
vollständig unter deploy/ (für die Entwicklung). Gedacht war das als
Arbeitsteilung, geworden ist es Drift. Der produktiven fehlten zwölf
Schlüssel – darunter SESSION_MAX_AGE, STORAGE_PATH und die
HoneyOS-Leiste –, und sie empfahl für OFFICE_WOPI_URL weiterhin
app:8080, also genau den Wert, an dem Collabora den Rahmen verweigert.

Jetzt eine Datei im Wurzelverzeichnis für beides, nach Abschnitten
geordnet, mit einem eigenen Abschnitt für die vier Werte, die nur den
Entwicklungs-Compose betreffen. deploy/.env.example ist weg, die
Verweise in README, TESTING und deploy/docker-compose.yml zeigen auf die
verbliebene Datei.

Gegengeprüft: Jeder Schlüssel, den config.go liest, kommt darin vor
(40 Stück, kein Rest). Ein Container mit dieser Datei als env_file
liefert die erwarteten Werte, auch den maskierten OFFICE_ALIASGROUP.

Dabei selbst in die dokumentierte Falle getappt und wieder heraus:
"OFFICE_PROVIDER=  # collabora | onlyoffice" macht den Kommentar zum
Wert. Der Test aus TESTING.md hat es gefunden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 00:59:00 +02:00
marcandClaude Opus 5 aa8af418f2 docs(plan): gleitende Sitzung und der Fund zu store.MaxAge nachgetragen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 23:56:09 +02:00
marcandClaude Opus 5 093ff5140e feat(auth): gleitende Sitzung – Frist ab letzter Benutzung statt ab Anmeldung
Marc flog trotz täglicher Nutzung heraus: Die Frist lief ab dem
Anmelden. Ein Ablauf, der den Anwesenden trifft und nicht den
Abwesenden, erkauft keine Sicherheit, sondern nur Ärger.

Jetzt verlängert jeder angemeldete Aufruf die Sitzung – höchstens einmal
pro Stunde, sonst trüge jede Antwort ein Set-Cookie (beim Laden einer
Dateiliste dreißig Stück). Vorgabe 30 Tage ab letzter Benutzung;
bestehende .env-Werte bleiben unangetastet.

security(auth): Frist galt nur im Browser, nicht für die Signatur

Beim Bauen gefunden, war schon vorher drin: NewSessionStore ersetzte
store.Options, ohne store.MaxAge() zu rufen. Die Codecs behielten damit
gorillas Voreinstellung von 30 Tagen. Wirkung: Der Browser warf das
Cookie nach SESSION_MAX_AGE weg – wer den Wert vorher herauskopiert
hatte, konnte ihn 30 Tage lang weiterbenutzen. Wer die Frist kürzte, um
sie zu kürzen, hatte nichts gekürzt.

Mit Test festgehalten, und der Test ist gegengeprüft: Ohne den Aufruf
schlägt er fehl ("abgelaufene Sitzung gilt weiter"), mit ihm läuft er
durch.

Ein Fehler steckte dabei zuerst im Test selbst: gorilla legt die
entschlüsselte Sitzung im Zusammenhang der Anfrage ab. Zweimal dieselbe
Anfrage zu befragen prüfte den Zwischenspeicher, nicht die Frist. Jetzt
baut jede Prüfung eine frische Anfrage.

Live gegengeprüft: Anmeldung setzt das Cookie, drei angemeldete Aufrufe
danach setzen keins (die Bremse greift).

Dazu die gemessenen Leerlaufwerte der Office-Server im README statt der
Zahlen aus deren Dokumentation (Collabora 51 MB, OnlyOffice 396 MB).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 01:14:13 +02:00
marcandClaude Opus 5 6c53651568 fix(office): PDF am Telefon nur bei OnlyOffice, Collabora behält den Editor
Die Regel galt für beide Anbieter – das war falsch. Marcs Kritik betraf
nur OnlyOffice: Es baut auf dem Telefon dieselbe Oberfläche auf wie am
PC. Collabora bringt für kleine Schirme eine eigene Ansicht mit, die
taugt; dort wäre der Umweg über ein PDF ein Rückschritt.

Der Anbieter steht jetzt in der öffentlichen Konfiguration
(office_provider), und die Oberfläche entscheidet daran. Der Endpunkt
selbst kann weiterhin beides.

Nachgeholt: Der ganze Weg ist jetzt auch mit Collabora gemessen, nicht
nur mit OnlyOffice – gegen die Dev-Instanz, die damit läuft.
  erster Abruf 0,347 s, zweiter 0,009 s, eine Datei auf der Platte
  (OnlyOffice zum Vergleich: 1,05 s – es muss die Datei erst abholen)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 01:07:08 +02:00
marcandClaude Opus 5 065372359b feat(pdf): Zoom in festen Fünferschritten statt in fünf Prozent
Gemeint waren runde Zahlen: 65, 70, 75, 80 … Ein fester Faktor ist
rechnerisch die sauberere Größe – er fühlt sich unten wie oben gleich an
–, aber abzulesen war er nicht: Die Anzeige sprang um drei Punkte, dann
um vier, dann um sechs. Wer eine Zahl sieht, erwartet ein Raster.

Weil fünf Punkte bei 25 % ein Fünftel und bei 400 % ein Hundertstel
sind, kann die Schwelle der Geste nicht mehr fest sein. Verlangt wird
jetzt genau die Fingerbewegung, die dem nächsten Schritt entspricht,
mit zwei Prozent als Untergrenze gegen Zittern.

Ein Fehler, den erst die Messung zeigte: 1,1 × 100 ergibt in Fließkomma
110.00000000000001, und die "nächste kleinere Fünferzahl" davon ist
wieder 110. Der Zoom blieb deshalb nach genau einer Stufe stehen –
sichtbar nur beim Zuziehen, weil es aufwärts eine Stufe später gegriffen
hätte. Vor der Rechnung wird jetzt gerundet.

Gemessen, eine lange Geste in jede Richtung:
  auf:  65 → 70 → 75 → 80 → 85 → 90 → 95 → 100 → 105 → 110 → 115 → 120 → 125 → 130
  zu:   125 → 120 → 115 → 110 → 105 → 100 → 95 → 90 → 85 → 80 → 75

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 00:57:31 +02:00
marcandClaude Opus 5 ded1134f6a feat(pdf): Zoomstufen von 10 % auf 5 % verfeinert
Marc las am Telefon Sprünge von acht Prozentpunkten – die Stufe ist
relativ, nicht in Punkten: Bei 65 % sind zehn Prozent sieben Punkte, bei
200 % zwanzig. Fünf Prozent treffen das Gemeinte besser.

Die Schwelle der Geste folgt der Stufenweite (vorher 8 % / 7 %), sonst
ließe sich die feinere Weite mit den Fingern gar nicht treffen.

Kein Ruckeln dadurch: Die Zeichenschleife bricht eine laufende
Darstellung ab, sobald sich der Maßstab ändert – von zwanzig Stufen
einer Geste wird nur die letzte fertig gemalt.

Gemessen, dieselbe Geste:
  65 → 68 → 71 → 75 → 79 → 83 → 87 → 91 → 96 → 101 → 106 → 111 → 117 → 123 → 129 %

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 00:52:37 +02:00
marcandClaude Opus 5 9c5cda75d7 feat(office): Dokumente am Telefon als PDF ansehen statt bearbeiten
Am Finger öffnet ein Office-Dokument jetzt das gewandelte PDF im eigenen
Betrachter; "Edit" daneben führt trotzdem in den Editor. Mit Maus ändert
sich nichts.

Der Web-Editor ist eine vollständige Textverarbeitung – zwischen seinen
Leisten bleibt auf 400 Pixeln vom Dokument ein Rechteck. Collabora und
OnlyOffice verhalten sich gleich; deshalb kein besserer Bedienmodus,
sondern ein anderes Werkzeug. Gewandelt wird von dem Server, der auch
bearbeitet: Sonst sähe man unterwegs ein LibreOffice-Layout eines
Dokuments, für das man OnlyOffice gewählt hat.

Je Dokument gibt es GENAU EIN PDF, und das ist keine Frage des
Aufräumens, sondern des Schlüssels: previews/<id>.pdf trägt die Fassung
nicht im Namen, eine neue Fassung überschreibt. files.preview_version
sagt, ob das Abgelegte noch passt.

Sicherheit: Die Adresse, die OnlyOffice zurückgibt, wird von uns
abgerufen – der Wähler des Transports lässt nur OFFICE_ALLOWED_HOSTS zu,
zusätzlich eine ausdrückliche Prüfung mit Test. JWT-Signatur (HS256) für
die eigene API des Dokumentenservers. Fremde Kennung 404, ohne Anmeldung
401. Das Ergebnis muss mit %PDF- beginnen, sonst landete eine
Fehlerseite im Zwischenspeicher.

Gemessen gegen einen echten OnlyOffice-Server im Docker-Netz:
  erster Abruf 1,05 s -> zweiter 0,009 s
  erneut hochgeladen -> Fassung 2, preview_version 2, weiterhin 1 Datei
  GC: herrenloses PDF von vorgestern weg, echtes bleibt
  endgültig gelöscht -> PDF weg; Papierkorb -> PDF bleibt
  ohne Anmeldung 401, fremde Kennung 404, Nicht-Office 415

Dazu im Admin-Bereich neu: Platzbedarf der abgeleiteten Daten
(Vorschaubilder, PDFs, Profilbilder). Sie zählen gegen kein Kontingent –
ein Ordner, der wächst, ohne dass ihn jemand sieht, ist eine stille
Überraschung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 23:30:49 +02:00
marcandClaude Opus 5 84ae570305 docs(office): aliasgroup1 und OFFICE_WOPI_URL richtiggestellt
Der Collabora-Schnipsel empfahl `aliasgroup1: http://app:8080` und
`OFFICE_WOPI_URL=http://app:8080`. Beides kann nicht funktionieren,
sobald der Browser die Cloud unter einer anderen Adresse sieht als der
Docker-Dienstname – also immer:

  * Collabora baut aus WOPISrc sein frame-ancestors. Steht dort der
    interne Name, lehnt der Browser die Einbettung ab.
  * aliasgroup1 ist die Positivliste der WOPI-Hosts. Steht der Host
    nicht drin: "No authorized hosts found matching the target host".

Beide Zeilen nennen jetzt die öffentliche Adresse, aliasgroup1 als
regulären Ausdruck mit Port. Dazu die Begründung im Text (samt der
Folge, dass Collabora die Datei dann über den Reverse Proxy holt), zwei
Zeilen in der Fehlertabelle und derselbe Hinweis in der .env.example.

Außerdem der Kenntnisstand zu OnlyOffice nachgezogen: Start, Umwandlung
nach PDF und JWT sind hier inzwischen gemessen, der WOPI-Kreis nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 22:27:20 +02:00
marcandClaude Opus 5 0723d853f0 feat(pdf): Finger und Mausrad zoomen in Zehnteln statt in Vierteln
Die Leiter der Knöpfe (0,5 / 0,75 / 1 / 1,25 / …) ist für eine Geste zu
grob – man schießt über die Größe hinaus, die man gerade sucht. Finger
und Mausrad gehen jetzt zehn Prozent pro Schritt, die Knöpfe behalten
ihre Leiter: Ein Klick soll etwas bewegen, eine Geste ist ein Regler.

Multiplikativ, nicht in festen Prozentpunkten: Von 50 auf 60 % ist ein
Fünftel mehr, von 300 auf 310 % ein Dreißigstel. Zehn Prozent vom
aktuellen Wert fühlen sich überall gleich an. Auf ganze Prozent
gerundet, sonst stünde 71,50000000000001 % in der Leiste.

Gemessen, dieselbe Geste wie vorher: 65 % -> 116 % statt 65 % -> 125 %,
zurück auf 71 % statt 50 %.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:17:57 +02:00
marcandClaude Opus 5 574ba3e6c5 feat(mobile): Zurück-Geste schließt die Ansicht, zwei Finger zoomen das PDF
Zwei Dinge, die am Telefon fehlten – beide in der installierten App
besonders schmerzhaft, weil es dort keine Adresszeile als Rückweg gibt.

1. Zurück schloss bisher die Anwendung, nicht die Vorschau. Vorschau,
   Office-Editor und die aufgeschobene Seitenleiste kamen im Verlauf des
   Browsers gar nicht vor: Der einzige Eintrag war die Dateiliste. Jetzt
   legt jede bildschirmfüllende Ansicht beim Öffnen einen leeren Eintrag
   an (die Adresse bleibt gleich – eine Vorschau ist kein Ort für ein
   Lesezeichen) und verbraucht ihn beim Schließen wieder, egal ob über
   die Geste, Escape oder den Knopf. Ein Stapel sorgt dafür, dass bei
   übereinanderliegenden Ansichten nur die oberste reagiert.

   Collabora blendet auf kleinen Schirmen seinen eigenen Schließen-Knopf
   aus (closeButtonEnabled && !isSmallScreenDevice) – ohne diesen Weg
   käme man aus einem Dokument am Telefon nur über die Kopfzeile heraus.

2. Die Zusammenziehgeste vergrößerte die ganze Seite statt des PDFs:
   Leiste und Knöpfe wuchsen mit, das Dokument blieb grob. touch-action
   nimmt dem Browser die Geste, ein eigener Zuhörer setzt unsere
   Zoomstufen an ihre Stelle, verankert an der Mitte zwischen den
   Fingern – dieselbe Regel wie bei Strg+Mausrad.

Gemessen in Chromium mit Touch-Emulation, 412x900:
  Vorschau offen -> Verlauf 2->3, Zurück -> Vorschau zu, Anwendung bleibt
  Esc -> Eintrag wieder weg (ein weiteres Zurück verlässt die App)
  Aufziehen 65% -> 125%, Zuziehen -> 50%, visualViewport.scale bleibt 1

Ein Befund aus dieser Messung steckt im Code: Zuerst kam pro Geste nur
eine Stufe, weil sich der Zuhörer bei jeder Zoomänderung neu anmeldete
und der gemerkte Fingerabstand dabei verlorenging. Abstand und Maßstab
liegen deshalb in Referenzen, der Zuhörer meldet sich einmal an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:25:59 +02:00
marcandClaude Opus 5 face318131 docs(plan): Idee notiert – Dateien im Papierkorb ansehen (vertagt)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 15:01:21 +02:00
marcandClaude Opus 5 105b1a0e74 fix(trash): Vorschaubilder auch im Papierkorb ausliefern
GetThumbnail schlug die Datei mit is_deleted = false nach – im
Papierkorb also 404, und dort stand für jedes Bild nur ein graues
Quadrat. Man entschied über "endgültig löschen", ohne zu sehen, was es
war.

Schlägt der erste Griff fehl, wird jetzt im Papierkorb nachgesehen. Das
Vorschaubild überlebt das weiche Löschen ohnehin: Entfernt wird es erst
beim endgültigen Löschen, und der GC zählt es bis dahin als benutzt.

Geprüft gegen die laufende Instanz: Bild hochladen, löschen,
Vorschaubild abrufen -> 200, image/jpeg, 64x64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 14:21:55 +02:00
marcandClaude Opus 5 451f359734 feat(uploads): fertige Meldungen verschwinden von selbst
Nach einem Stapel-Upload blieb unten rechts für jede Datei eine Karte
stehen, bis man sie einzeln wegklickte oder die Seite neu lud. Bei
vielen Dateien war der Bildschirm zu.

Fertige Meldungen räumen sich jetzt nach vier Sekunden selbst ab.
Fehler bleiben: Sie tragen die einzige Auskunft, die es sonst nirgends
gibt. Zusätzlich stehen höchstens vier Karten gleichzeitig, der Rest
wird gezählt – der Bildschirm läuft ja schon während des Hochladens
voll, nicht erst danach. Gescheiterte Dateien stehen dabei oben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:14:13 +02:00
marcandClaude Opus 5 8d95088271 feat(mobile): Wischen vom linken Rand holt die Seitenleiste
Bisher gab es dafuer nur den Knopf oben links - am Telefon die Ecke,
die man einhaendig am schlechtesten erreicht.

Vier Bedingungen halten die Geste aus dem Weg: nur im 44-px-Streifen am
Rand (sonst waeren waagerecht scrollbare Bereiche nicht mehr zu
bedienen), mindestens 60 px Weg, doppelt so weit waagerecht wie
senkrecht (sonst oeffnet sie beim Blaettern), hoechstens 800 ms.

44 px und nicht 20: Androids Zurueck-Geste belegt die aeussersten ~20
px, was dort beginnt erreicht die Seite nie.

Aus einer bildschirmfuellenden Ansicht heraus tut sie nichts - dort
wischt man durch Seiten, und die Leiste laege ohnehin dahinter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 11:24:23 +02:00