Beim Anlegen war die Kalenderauswahl schon da (der oberste ist die
Vorgabe), beim Bearbeiten war sie festgestellt -- mit der Begruendung,
ein Umhaengen sei Loeschen und Neuanlegen unter neuer Kennung. Das
stimmt so nicht: In CalDAV wechselt dabei nur die Sammlung, UID und
Inhalt bleiben. Genau das macht der neue Endpunkt
POST /api/v1/calendars/{id}/events/{uid}/move {"calendar_id": …}
Er aendert nur die Zugehoerigkeit der Zeile und erhoeht das ctag auf
**beiden** Seiten. Mit nur einem holte ein Client die andere Haelfte
erst beim naechsten Vollabgleich, und der Termin stuende
zwischenzeitlich doppelt oder gar nicht im Telefon.
Bewusst ein eigener Endpunkt und kein weiteres Feld im PATCH: Ein Umzug
laesst sich auch ohne jede Aenderung am Termin ausloesen, und dafuer den
ganzen Termin mitschicken zu muessen waere eine Einladung, nebenbei
etwas zu ueberschreiben. Im Formular laeuft deshalb erst das PATCH und
dann der Umzug -- andersherum stuende der Termin nach einem Fehler
verschoben, aber unveraendert da, die verwirrendere Haelfte.
Dazu die Zieh-Geste: einen Termin auf einen Kalender in der
Seitenleiste ziehen, am Handy nach kurzem Gedrueckthalten. Ueber
Pointer-Events statt ueber die Drag-and-Drop-Schnittstelle des
Browsers -- die gibt es auf Android nicht, und genau dort soll die
Geste ja auch funktionieren. Ist die Kalenderliste zugeklappt (am Handy
der Regelfall), erscheint waehrend des Ziehens eine Leiste unten in
Daumennaehe, sonst liefe die Geste ins Leere.
Zwei Faelle, die sonst aergern: Wer in einen ausgeblendeten Kalender
zieht, bekommt ihn eingeblendet -- der Termin waere sonst scheinbar
spurlos verschwunden. Und der Klick, der einem Ziehen hinterherkommt,
wird verschluckt, statt den Termindialog zu oeffnen.
Eine Reihe zieht vollstaendig um; sie ist eine einzige Datei, kein
Buendel von Einzelterminen. Das sagt das Formular auch an.
Selbsttest um elf Pruefungen erweitert (36 gruen): Umzug angenommen,
Kennung bleibt, aus der Quelle weg und im Ziel da, ctag auf beiden
Seiten gestiegen, Umzug auf sich selbst folgenlos, fremder Kalender
404, Reihe vollstaendig umgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Löschen eines Serientermins gibt es jetzt zwei Wege statt einem:
"Nur diesen Termin" und "Ganze Reihe". Das sind zwei verschiedene
Absichten, und sie sind nicht ineinander umkehrbar -- eine einzelne
Ja/Nein-Rückfrage musste sich bisher für eine davon entscheiden, und die
andere war gar nicht erreichbar.
"Nur diesen" wird als EXDATE in der Reihe vermerkt, so wie das
iCalendar-Format eine Ausnahme ausdrückt: Die Reihe behält ihre Regel
und ihre UID, jeder CalDAV-Client versteht es ohne Zutun. Bewusst nicht
über ein Aufspalten in zwei Reihen gelöst -- das änderte die UID, und
alle Geräte hielten den Rest für einen neuen Termin.
Ein zweites Streichen desselben Termins bleibt folgenlos (sonst liefe
die Datei mit der Zeit mit doppelten EXDATE-Zeilen voll), und bei einem
Einzeltermin ohne Wiederholung sind beide Wege dasselbe -- der Server
löscht dann einfach, statt den Aufrufer mit einer Unterscheidung zu
behelligen, die es dort nicht gibt.
Selbsttest um sechs Prüfungen erweitert (25 grün): Reihe steht,
Streichen liefert 204, der gestrichene Termin fehlt und der Rest bleibt,
EXDATE steht im .ics, zweimal Streichen ist harmlos, ganze Reihe löschen
räumt alles weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Terminfarbe: Sie kam auf dem Telefon nie an. RFC 7986 schreibt für COLOR
einen CSS3-*Namen* vor, wir schrieben Hex – DAVx5 liest die Eigenschaft
über eine Namenstabelle und verwirft alles andere, also blieb der Termin
im Samsung-Kalender farblos. Jetzt beides nebeneinander:
COLOR:tomato für fremde Clients (damit die Farbe ankommt)
X-AETHER-COLOR:#ea4335 für die eigene Palette (damit sie genau bleibt)
Beim Lesen gewinnt der genaue Wert; ein von außen eingefärbter Termin
wird über die Namenstabelle übersetzt. Nächstgelegener Ton per Abstand im
RGB-Würfel (color.go).
Wiederholungen können jetzt mehr als "alle N":
- wöchentlich an ausgewählten Wochentagen (Mo–So, Mehrfachauswahl). Der
Wochentag des Starts bleibt fest angehakt – eine Reihe, die ihren
eigenen ersten Termin auslässt, ist keine.
- monatlich und jährlich wahlweise am festen Kalendertag (am 4.) oder am
n-ten Wochentag (am 1. Freitag). Beide Angaben leitet der Server aus
dem Startdatum ab, statt sie getrennt entgegenzunehmen – so kann die
Regel gar nicht erst auf einen anderen Tag zeigen als der Termin.
- Der fünfte Wochentag eines Monats wird bewusst zu "letzter": Einen
fünften Freitag hat nicht jeder Monat, BYDAY=5FR ließe die Reihe sonst
in etwa jedem zweiten Monat stillschweigend ausfallen.
Regeln, die das Formular weiterhin nicht abbildet, bleiben lesbar, aber
nicht bearbeitbar (409) – unverändert.
Dazu scripts/kalender-selbsttest.py, 19 Prüfungen gegen die laufende
Instanz, in TESTING.md verlinkt. Alle grün, unter anderem:
am 1. Freitag monatlich -> 04.09. 02.10. 06.11. 04.12.
am 1. Freitag im September -> 2026-09-04, 2027-09-03
Mo+Fr wöchentlich -> 07./11./14./18./21./25.09.
ganztägig am 3. -> bleibt der 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fünf Wünsche aus dem Durchtesten:
1. Wiederholungen einstellbar (täglich/wöchentlich/monatlich/jährlich,
alle N, Ende nie/nach Anzahl/an einem Datum). Der eigentliche Aufwand
lag nicht im Formular, sondern im Aufklappen: eine wöchentliche Reihe
erschien sonst als *ein* Eintrag. Der Server klappt sie jetzt im
angefragten Zeitfenster auf (?from=&to=, rrule-go über go-icals
RecurrenceSet), die Ansichten fragen ihren sichtbaren Ausschnitt an.
Geprüft: 5 Termine wöchentlich, nach Umstellung auf 14-tägig korrekt
neu gerechnet, Fenster einer Woche liefert genau einen.
Regeln, die das Formular nicht abbildet (etwa jeder zweite Dienstag
im Monat), werden weiterhin korrekt *angezeigt* -- geprüft, die vier
Termine stimmen -- aber nicht zum Bearbeiten angeboten (409). Ein
halb verstandenes Speichern würde die Regel sonst still beschneiden.
2. Tag wird erst geöffnet, statt sofort einen Termin anzulegen: Klick
auf einen Tag zeigt dessen Termine als Liste, dort wählt man einen
aus oder legt bewusst einen neuen an.
3. Eigene Farbe je Termin (COLOR nach RFC 7986), leer = Kalenderfarbe.
4. Kalenderliste ohne Kästchen: der Farbpunkt selbst ist der Schalter --
gefüllt heißt sichtbar, leerer Ring ausgeblendet.
5. Ganztägig als Schiebeschalter statt Kästchen -- es stellt das
Formular um, das ist keine Nebenangabe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei gemeldete Fehler, alle nachgestellt:
1. Ein vorhandener Termin ließ sich nicht öffnen. Der Klick auf den
Termin reichte an die Tageszelle darunter durch, die sofort 'Neuer
Termin' öffnete und den Bearbeiten-Dialog überschrieb. Fehlendes
stopPropagation.
2. Klick auf den 3. legte den Termin am 2. an. Ein ganztägiger Termin
ist ein *Datum*, kein Zeitpunkt - die Oberfläche rechnete ihn über
new Date(...).toISOString() trotzdem in UTC um, was östlich von UTC
den Vortag ergibt. Nachgestellt und bestätigt: DTSTART landete als
20260902 statt 20260903. Ganztägige Termine laufen jetzt über
dates.ts (allDayISO/eventDay), damit Dialog und Raster dieselbe
Rechnung benutzen. Nebenbefund mitgefixt: DTEND war gleich DTSTART,
was RFC 5545 verbietet (DTEND ist exklusiv) - ein eintägiger Termin
am 3. schreibt jetzt DTEND 20260904, nach außen bleibt die
Schnittstelle beim letzten belegten Tag.
3. 'Fühlt sich starr an': Wochenansicht ist jetzt ein echtes
Stundenraster (Klick trifft die Stunde statt Mitternacht, ganztägige
in eigener Zeile, überlappende Termine nebeneinander, Jetzt-Linie),
mehrtägige Termine stehen in jeder Zelle statt nur am Starttag, neue
Termine schlagen die nächste volle Stunde bzw. 9 Uhr vor,
Kalenderauswahl im Termindialog, Escape schließt, Hover-Rückmeldung,
Kalenderliste auf schmalen Schirmen einklappbar.
Außerdem: ein neu angelegter Kalender blieb unsichtbar, weil er in
keiner Sichtbar-Liste stand - die Liste merkt sich jetzt die
ausgeblendeten statt der sichtbaren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neue Seite CalendarPage (Menüpunkt 'Calendar' in der Seitenleiste,
zwischen Home und Papierkorb) mit Kalenderliste zum Anlegen/Umbenennen/
Löschen/Ein- und Ausblenden, Export/Import als .ics über die
Phase-2-API, und Termine anlegen/ändern/löschen über einen neuen
JSON-Endpunkt.
Backend: neuer events_api.go-Handler unter /api/v1/calendars/{id}/events
(Session-Auth) neben dem bestehenden CalDAV-Protokoll – ein Termin, den
die Web-Oberfläche anlegt, ist ein ganz normales CalDAV-Objekt und
synchronisiert genauso zu DAVx5 wie umgekehrt. Ein per CalDAV mit RRULE
angelegter, wiederkehrender Termin zeigt sich hier nur (recurring:true),
lässt sich aber bewusst nicht über dieses vereinfachte Formular
bearbeiten (409) – das würde die Wiederholungsregel eher beschädigen
als korrekt anpassen.
Gegen den laufenden Container getestet: Kalender/Termin anlegen, per
Web-API ändern und löschen, ein per CalDAV mit RRULE angelegter
Wiederkehrer taucht als recurring:true auf und blockt PATCH mit 409.
TypeScript-Build (tsc + vite build) sauber.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neuer APIHandler unter /api/v1/calendars (Session-Auth, wie der Rest
der API) neben dem bestehenden CalDAV-Handler (App-Passwörter,
/caldav/) – zwei Handler, zwei Auth-Modelle, wie schon bei /dav.
- GET/POST /api/v1/calendars – Liste, Anlegen
- PATCH/DELETE /api/v1/calendars/{id} – Umbenennen, Farbe, Zeitzone,
Löschen (go-webdav kennt PROPPATCH und Kalender-Löschen nicht – dafür
gibt's im CalDAV-Protokoll selbst keinen Weg)
- GET .../export – alle Termine eines Kalenders als eine .ics-Datei,
VTIMEZONE-Blöcke dedupliziert
- POST .../import – .ics mit mehreren VEVENT/VTODO einlesen, jede
Komponente wird ein eigenes Objekt wie bei einem einzelnen CalDAV-PUT
Gegen den laufenden Container getestet (Session-Login, CSRF-Token):
Anlegen, Umbenennen, zwei Termine importieren, sauberer Re-Export,
Löschen, und dass ohne Login 401 statt Daten kommt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neue Spalte cal_calendars.timezone (Default UTC). CalDAV selbst braucht
sie nicht - jeder Termin trägt seine Zeitzone bereits im eigenen .ics,
und die TZID-Auflösung darin läuft schon korrekt über die
System-Zeitzonendatenbank. Gebraucht wird das Feld erst von der
künftigen Web-Oberfläche zum Anlegen neuer Termine (Phase 3) als
Vorgabe, wenn der Nutzer keine eigene Zeitzone wählt - jetzt schon
angelegt, damit dafür keine weitere Migration nötig wird.
Gegen den laufenden Container geprüft: Migration lief durch, Home-Set-
PROPFIND (nutzt die geänderten SELECTs) weiterhin grün.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Manche Clients folgen dem 308-Redirect von /.well-known/caldav mit GET
statt PROPFIND (curl -L reproduziert das exakt), landen dann auf einer
Sammlung (Principal/Home-Set/Kalender) und bekamen dort bisher ein
hartes 404 von GetCalendarObject – ein Client, der so die
Erreichbarkeit prüft, bricht die Einrichtung dann vorzeitig ab, bevor
er je ein PROPFIND schickt. GET/HEAD auf Nicht-.ics-Pfade antworten
jetzt mit einer schlichten 200-Textantwort statt an den CalDAV-Backend
durchgereicht zu werden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
go-webdav leitet GET/HEAD auf jeden Pfad unverändert an
GetCalendarObject weiter, auch auf Principal- oder Home-Set-Pfade (ein
Browser kann nur GET, kein PROPFIND). Ohne Prüfung hielt
parseCalendarPath dann z.B. das Wort 'caldav' aus dem Pfad für eine
Kalender-ID und gab einen internen UUID-Parsing-Fehler nach außen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neues, eigenständiges Modul internal/calendar: implementiert
caldav.Backend (emersion/go-webdav) gegen zwei neue Tabellen
(cal_calendars, cal_objects). Endpunkt /caldav/ mit eigener Basic Auth
(nur App-Passwörter, wie /dav), plus /.well-known/caldav für die
automatische Einrichtung nach RFC 6764.
Gegen echte Container getestet (nicht nur go build): Discovery-Kette
Principal -> Home-Set -> automatisch angelegter Standardkalender,
PUT/GET/DELETE eines Termins, REPORT mit Zeitraumfilter (Treffer und
Nicht-Treffer), If-Match/If-None-Match-Vorbedingungen (412), und dass
ein fremder Nutzer auf einen Kalender-Pfad mit 404 statt 403 abgewiesen
wird.
Zwei Fallen unterwegs gefunden und dokumentiert:
- go-webdav vergleicht von CurrentUserPrincipal/CalendarHomeSetPath
zurückgegebene Pfade direkt gegen r.URL.Path (inklusive Prefix) und
bestimmt den Ressourcentyp allein an der Segmentzahl dahinter – der
Nutzername beim Principal steht deshalb in einem Segment
(principals-max), nicht als Unterpfad.
- chi kennt REPORT/MKCALENDAR nicht von Haus aus (nur PROPFIND & Co.
waren für /dav bereits registriert) und hätte sie sonst mit 405
abgewiesen, bevor der CalDAV-Handler überhaupt lief.
Migration 000021 nur mit CREATE TABLE IF NOT EXISTS, wie in
kalender-plan.md festgelegt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
Gleicher Dateiname erzeugt keine neue Datei, sondern eine neue Fassung -
gleiche ID, gleiche Vorschaubild-Adresse. Die darf einen Tag im Browser
liegen, also zeigte die Kachel nach dem erneuten Hochladen bis zu 24
Stunden das alte Bild. Das erklaert, warum eine Drehung erst nach
mehreren Versuchen "ankam". Die Adresse traegt jetzt ?v=<Fassung>.
Vorschaubilder entstehen im Hintergrund; die Kachel fragt jetzt bis zu
fuenfmal nach, statt bis zum naechsten Neuladen ihr Ersatzsymbol zu
zeigen. Nur bei Bildern juenger als zwei Minuten - sonst fragte jede
dauerhaft fehlgeschlagene Datei bei jedem Oeffnen erneut. Gemessen: 608
ms bis zur Anzeige, ohne Neuladen.
Das Anheben der Kachel beim Ueberfahren ist raus: Steht der Zeiger auf
der Unterkante, rutscht sie unter ihm weg, mouseleave, sie faellt
zurueck, mouseenter - im Kreis, sichtbar als Flackern.
Dazu liest die EXIF-Auswertung jetzt 256 statt 64 KB und gibt bei einem
abgeschnittenen Block nicht mehr auf: Die Drehung steht vorn, das
eingebettete Vorschaubild dahinter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
130 Zeilen fuer einen einmaligen Bedarf - die Haelfte davon eine Karte
im Admin-Bereich, die sonst dauerhaft dort steht. Die EXIF-Behebung
selbst bleibt: Neu hochgeladene Bilder stehen richtig herum.
Bestehende Vorschaubilder werden damit nicht mehr nachgezogen. Wer sie
gerade ruecken will, laedt die Datei erneut hoch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kameras speichern die Bildpunkte fast immer quer und legen die Drehung
als EXIF-Notiz daneben. Browser befolgen sie von selbst - Go nicht.
Deshalb stand dasselbe Foto in der grossen Ansicht aufrecht und lag im
Vorschaubild auf der Seite.
Neues Paket internal/imageutil liest genau ein Feld (0x0112) aus dem
Anfang der Datei und dreht danach. Von Hand statt mit einer Bibliothek:
Der Weg dorthin ist kurz und liegt fest, und gebraucht wird ein Feld.
Gelesen werden nur die ersten 64 KB, damit ein 40-MB-Foto nicht zweimal
im Speicher liegt.
Gedreht wird vor dem Verkleinern - andersherum bekaeme ein hochkantes
Foto den Zuschnitt eines Querformats.
Bestehende Vorschaubilder aendern sich davon nicht. Dafuer neu:
POST /admin/thumbnails/rebuild samt Knopf im Admin-Bereich.
Auch Profilbilder aus OIDC laufen jetzt durch dieselbe Auswertung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Adresse im <script>-Tag stand fest in der index.html, waehrend die CSP
aus HONEYBAR_ORIGIN kam. Wer die Leiste woanders betreibt, bekam genau den
Widerspruch: die CSP erlaubte den eigenen Ursprung, geladen wurde der alte.
Der erste Versuch, das ueber ein Bau-Argument zu loesen, half beim fertigen
Abbild nicht — dort ist der Bau vorbei, und ein Umzug haette einen Neubau
verlangt. Ursprung und Schluessel stehen jetzt in /api/v1/public/config, die
Oberflaeche haengt das Skript selbst ein. Beides speist sich aus derselben
Variablen wie die CSP, sie koennen also nicht auseinanderlaufen, und eine
Aenderung wirkt beim Neustart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Leiste kommt als Skript von der HoneyOS-Domain und holt sich von dort
ihre App-Liste. Beides blockte die CSP bisher (script-src / connect-src
fielen auf 'self' zurueck) – ohne Ausnahme erschien sie gar nicht.
- HONEYBAR_ORIGIN nimmt genau diesen einen Ursprung in script-src und
connect-src auf. Leer heisst: keine Leiste, und die Regel bleibt so eng
wie bisher. Frame-Freigabe erbt sie ausdruecklich nicht.
- SecurityHeaders nimmt die Ausnahmen jetzt als CSP-Struct statt als
variadische frameSrc-Liste – mit drei Richtungen wurde das sonst unklar.
- Das <script>-Tag steht in der index.html und nicht serverseitig
eingespritzt: der Service Worker legt die index.html in seinen Vorrat
(navigateFallback), was zur Bauzeit fehlt, kaeme im Browser nie an.
- defer, damit ein langsames oder abgeschaltetes HoneyOS den Start von
Aether nicht ausbremst.
Tests fuer die neue Regel: eng ohne Ausnahme, beide Richtungen mit,
Pfad wird auf den Ursprung gekuerzt, Adresse ohne Schema faellt raus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Schickt der Anbieter ein `picture`, wird es bei der Anmeldung geholt,
mittig auf 256x256 zugeschnitten und oben rechts angezeigt. Fehlt es
oder scheitert das Holen, bleibt es beim Buchstabenkreis - die
Anmeldung selbst hängt nicht davon ab.
Die Bildadresse kommt aus einem Token, also von außen. Erlaubt ist
deshalb ausschließlich der Ursprung aus OIDC_ISSUER, Weiterleitungen
eingeschlossen; dazu 4 MB und 5 Sekunden als Deckel. Ohne das wäre es
eine Aufforderung an den Server, für einen Fremden beliebige Adressen
im internen Netz anzufragen.
Neu: Migration 000019 (avatar_key/-source/-updated_at), Paket
internal/avatar, Route GET /api/v1/users/{id}/avatar. Der GC durchsucht
"avatars/" mit und kennt die Schlüssel - sonst räumte er die Bilder
nach einem Tag weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Knopf hiess fest "Sign in with SSO / OIDC". Der Anbieter heisst bei
jedem anders; "Anmelden mit PocketID" sagt dem, der davorsteht, mehr.
Vorgabe unveraendert. Der Wert geht nur ueber /public/config, wenn OIDC
ueberhaupt an ist.
Beim Testen dabei selbst einen Fehler gebaut und eingefangen: Ein
Kommentar hinter einem LEEREN Wert wird in einer .env zum Wert.
OIDC_ISSUER= # z. B. https://... startete den OIDC-Aufbau mit dem
Kommentartext als Adresse, und der Start brach ab.
In deploy/.env.example steckten vier weitere solche Zeilen, darunter
OFFICE_PROVIDER= # collabora | onlyoffice - Office waere mit Unsinn als
Anbieter eingeschaltet worden. Alle umgestellt, Warnung an den Dateikopf.
Dazu im README belegt, was beim Ausschalten der Versionierung passiert:
Fassungen bleiben, auch im Papierkorb; erst das Leeren entfernt sie samt
Blobs, und zwar sofort statt erst beim GC.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OIDC_ISSUER=PocketID (der Name statt der Adresse) endete in
unsupported protocol scheme "" – tief aus der Bibliothek, ohne Hinweis
auf die Ursache. Und weil ein Fehlschlag hier den Start abbricht, stand
die ganze Cloud wegen einer Zeile in der .env.
Jetzt wird vorher geprueft und die Zeile beim Namen genannt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Zahl stand doppelt im Quelltext: als const im Backend und noch einmal
im Verlaufs-Dialog, verbunden nur durch den Kommentar "muss zu
maxVersionsKept im Backend passen". Ein Satz ist keine Zusicherung - beim
ersten Aendern haette der Dialog eine andere Zahl genannt als die, die der
Server anwendet.
Jetzt eine .env-Variable (Vorgabe 5), die der Server ueber
/api/v1/public/config mitliefert. Werte unter 1 werden verworfen: Die Zahl
geht als OFFSET in die Aufraeumabfrage, und OFFSET 0 loeschte jede Fassung -
aus "keine Grenze" wuerde "kein Verlauf".
Gemessen: MAX_VERSIONS_KEPT=3, Datei siebenmal ueberschrieben, uebrig
bleiben v6, v5, v4 bei aktueller v7.
Dazu ein README-Abschnitt "Versionen" - Standard aus, warum das so ist,
Kontingent, und dass jedes Strg+S in Office eine Fassung anlegt. Bisher
stand zu alldem nichts im README.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein $ im Wert wird von Docker Compose als Variable gelesen und schneidet
ab: Sicher$Passwort2026 kommt als Sicher an (gemessen). Angelegt wird ein
Konto mit einem Passwort, das niemand kennt; die Anmeldung scheitert mit
'invalid credentials', obwohl alles richtig eingetippt ist. Die Datenbank
laeuft dabei weiter, weil Postgres denselben abgeschnittenen Wert bekommt.
Der Start nennt jetzt die Laenge des Passworts (nicht den Wert) und warnt
unter 12 Zeichen mit dem Hinweis auf $$. Ohne diese Zahl war der Fehler
von aussen nicht zu sehen: Compose meldet zwar 'variable is not set', aber
auf stderr beim up, nicht im Container-Log.
Escape-Tabelle in README und beiden .env.example. Ein Backslash hilft
nicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Verbindungszeichenkette wurde mit fmt.Sprintf zusammengeklebt. Ein
Passwort mit % und @ ergab eine kaputte URL:
parse "postgres://aether:8UoNj%CoWJ7ze@qy@localhost:5432/aether":
invalid URL escape "%Co"
Der Server kam nicht über die Migrationen hinaus. Die key=value-Form hätte
dasselbe Problem mit einem Leerzeichen gehabt, nur stiller.
config.DSN() liefert jetzt eine postgres://-URL, gebaut mit net/url und
net.JoinHostPort (klammert IPv6). pgxpool und golang-migrate nehmen sie
beide unverändert – dsnToURL, splitDSN, indexByte und orDefault in pkg/db
sind ersatzlos entfallen. Test deckt %, @, :, /, Leerzeichen und IPv6 ab.
Dazu: DB_HOST und DB_PORT fehlten in der schlanken .env.example, weshalb
localhost statt postgres gegriffen hat. APP_ENV ergänzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).
Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>