# Aether – Test-Checkliste Manuelle Test-Checkliste für die fertigen Phasen. Abhaken beim Durchtesten. - ⚠️ = bekannte Lücke, muss *nicht* getestet werden - 🔎 = von Claude per `curl`/API gegen ein Wegwerf-Konto geprüft und grün. Bewusst **nicht** abgehakt: eine API-Prüfung ersetzt nicht den Test in der Oberfläche bzw. mit einem echten Client. Beim Durchgehen selbst abhaken. --- ## Kalender: Selbsttest per Skript Die Kalender-Schnittstelle hat einen automatischen Selbsttest – schneller und gründlicher als Klicken, und er prüft genau die Stellen, an denen schon einmal etwas schiefging: python3 scripts/kalender-selbsttest.py 36 Prüfungen: Ganztags-Datum (der 3. muss der 3. bleiben – der Fehler, bei dem ein Termin östlich von UTC einen Tag zurücksprang), mehrtägige Termine, Terminfarbe in beiden Schreibweisen, alle Wiederholungsmuster (wöchentlich an bestimmten Tagen, monatlich/jährlich am festen Tag bzw. am n-ten Wochentag), Bearbeiten einer Reihe, einen einzelnen Termin aus einer Reihe streichen (EXDATE), Umhängen in einen anderen Kalender (samt ctag auf **beiden** Seiten, sonst gleicht ein CalDAV-Client nur eine davon ab), und dass das Zeitfenster wirklich begrenzt. **Legt Termine an und löscht am Ende alle Termine im ersten Kalender des Kontos**; für den Umzugstest entsteht kurzzeitig ein zweiter Kalender „T zweitkalender", der danach wieder verschwindet. Also nur gegen eine Entwicklungsinstanz laufen lassen. ## CalDAV: Selbsttest per Skript Der Test oben prüft die JSON-Schnittstelle – also den Weg der Web-Oberfläche. Dieser hier geht den Weg eines echten Clients (DAVx5, Thunderbird, Apple Kalender). Beide führen auf dieselben Daten, aber über völlig verschiedenen Code: Ein grüner Lauf des einen sagt nichts über den anderen. python3 scripts/caldav-selbsttest.py 45 Prüfungen: Anmeldung (falsches Passwort und **Kontopasswort** werden abgewiesen – für CalDAV gelten nur App-Passwörter), Auffinden über `.well-known/caldav` samt der Umleitung, der Clients mit GET folgen, Kalenderliste mit Anzeigename, Komponenten, `getctag` und `calendar-color`, PUT/GET/DELETE eines Termins, `If-Match` (falsches ETag muss 412 geben), `calendar-query` mit Zeitfenster und `calendar-multiget`, beide Richtungen zwischen Web und CalDAV (Ganztagstermin als reines Datum mit exklusivem DTEND, Terminfarbe als CSS3-Name, RRULE, EXDATE), das Umhängen aus Client-Sicht (Datei verschwindet in der Quelle, taucht im Ziel auf, UID bleibt) und dass ein fremder Benutzerpfad 404 gibt statt eines leeren 207. Legt sich ein eigenes App-Passwort und zwei eigene Kalender an und räumt beides am Ende weg; vorhandene Kalender werden nur gelesen. ### Zwei Eigenschaften kommen nicht aus der Bibliothek `go-webdav` v0.7.0 beantwortet in PROPFIND einen festen Satz Eigenschaften und bietet keinen Haken, eigene dazuzulegen. `getctag` (Calendarserver) und `calendar-color` (Apple) ergänzt Aether deshalb selbst (`backend/internal/calendar/propfind.go`): Beide werden aus der Anfrage genommen, bevor go-webdav sie zu sehen bekommt, und der Antwort als eigener `propstat`-Block wieder angehängt. Ohne das Herausnehmen stünde dieselbe Eigenschaft zweimal in der Antwort – einmal mit 404, einmal mit 200. Weil das eigener Code ist und nicht Bibliothek, prüft das Skript ihn besonders: dass beide mit 200 kommen, die Farbe in Apple-Schreibweise (`#RRGGBBAA`), dass das ctag sich bei jeder Änderung **ändert** (ein festgefrorenes ctag ist schlimmer als gar keins – der Client fragt einmal und hält den Kalender danach für unverändert), dass eine Anfrage nach *nur* diesen Eigenschaften funktioniert (sonst bliebe für go-webdav ein leeres `` übrig, das es abweist), und dass ohne Nachfrage auch nichts davon in der Antwort steht. Dazu eine Ungenauigkeit ohne Folgen: Ein PUT, das einen vorhandenen Termin überschreibt, antwortet mit `201 Created` statt `204 No Content`. Das kommt aus go-webdav; Clients prüfen auf 2xx. ## Notfall: Migration hängt fest ("Dirty database version") Betrifft jede künftige Migration, nicht nur eine bestimmte. Nachgestellt und bestätigt (siehe kalender-plan.md, Abschnitt „Migrationen"): Scheitert eine Migration, bleibt `schema_migrations.dirty` auf `true` stehen, und **jeder** weitere Start von Aether bricht ab – die ganze Cloud, nicht nur der neue Teil. Kein Datenverlust dabei: Die Migration lief in einer Transaktion und wurde zurückgerollt, bevor der Fehler kam. `dirty=true` ist nur eine Sperre, keine Aussage über beschädigte Daten. Beheben: docker exec aether-db psql -U aether -d aether \ -c "UPDATE schema_migrations SET dirty=false;" Danach Aether neu starten. Lief die Migrationsdatei nur teilweise (z. B. zwei `CREATE TABLE` in einer Datei, das erste gelang), vorher prüfen, was bereits existiert, bevor man einen erneuten Lauf zulässt – sonst scheitert der zweite Versuch am selben Fehler wie der erste. --- ## Phase 3 – Admin, Gruppen, Theming, Profil ### Admin-Panel: Zugriff & Sicherheit - [X] Admin-Link erscheint **nur** für Admin-User in der Sidebar - [X] Normaler User sieht den Admin-Link **nicht** - [X] Normaler User, der `/api/v1/admin/users` direkt aufruft, bekommt `403` (serverseitiger Check) - [X] Nach Logout ist kein Admin-Endpunkt erreichbar (`401`) ### Admin: Benutzerverwaltung - [x] Benutzerliste zeigt alle User mit verwendetem Speicher + Quota + Rolle - [x] Neuen User anlegen (Username, Anzeigename, Passwort, Quota, Admin-Flag) - [x] Anlegen mit existierendem Username → Fehler „bereits vergeben" (`409`) - [x] User bearbeiten (Anzeigename, Quota, Admin-Flag ändern) - [x] **Selbst-Schutz:** eigenen Admin-Status kann man **nicht** entfernen (Fehlermeldung) - [x] *Selbst-Schutz:** eigenes Konto kann man **nicht** löschen - [x] Passwort-Reset generiert neues Passwort und zeigt es **einmalig** an - [x] Neuer User kann sich mit dem zurückgesetzten Passwort einloggen - [ ] User löschen entfernt den User (und seine Dateien) 🔎 ### Admin: Gruppen - [x] Gruppen-Tab lädt ohne schwarzen Bildschirm (war der Bug – jetzt gefixt) - [x] Gruppe anlegen (Name + Beschreibung) - [x] Gruppe aufklappen zeigt Mitglieder - [x] Mitglied über Dropdown hinzufügen → Zähler steigt - [x] Mitglied entfernen → Zähler sinkt - [x] Gruppe mit doppeltem Namen → Fehler (`409`) - [x] Gruppe löschen (auch mit Mitgliedern – Cascade) - [ ] ⚠️ *Gruppen haben noch keine Funktion (keine Gruppenordner) – Phase 4.5/C, offen* ### Admin: Theming - [x] Cloud-Name ändern → erscheint nach Reload überall (Login-Seite, Sidebar) - [x] Primärfarbe ändern → Buttons/Akzente übernehmen die Farbe sofort - [x] Akzentfarbe ändern → Vorschau aktualisiert live - [x] Nach Reload sind die Farben persistent - [ ] ⚠️ *Logo-Upload noch nicht vorhanden – Phase 4.5/D, offen* ### Profil-Seite (jeder User) - [x] Avatar/Name unten in der Sidebar öffnet die Profil-Seite - [x] Speicheranzeige (Balken) stimmt mit tatsächlicher Nutzung - [ ] Anzeigename ändern → wird gespeichert (✓-Bestätigung) 🔎 - [x] Passwort ändern: falsches aktuelles Passwort → Fehler - [x] Passwort ändern: neue Passwörter stimmen nicht überein → Fehler - [x] Passwort ändern: < 8 Zeichen → Fehler - [x] Passwort erfolgreich ändern → Logout → Login mit neuem Passwort klappt - [x] Persönliche Farben ändern → werden angewendet - [x] Logout-Button im Profil-Bereich funktioniert (ohne Profil zu öffnen) --- ## Phase 4 – WebDAV & OIDC ### WebDAV – Verbindung Verbinden mit `https:///dav/` oder lokal direkt mit `http://127.0.0.1:8090/dav/`. **Achtung Port:** die App lauscht *im Container* auf 8080, Compose bildet das auf dem Host aber auf **8090** ab (`127.0.0.1:8090->8080`). Vom Host aus gilt immer 8090 – und nur vom Host, die Bindung ist auf localhost. Anmeldung mit dem Benutzernamen und einem **App-Passwort** (Profil → App passwords). Das Kontopasswort wird hier **abgelehnt** – es ist zugleich der Admin-Zugang und hat auf Geräten nichts verloren. Clients: KDE/Dolphin, macOS Finder, Windows Explorer, `cadaver`, `rclone`, Cyberduck. > **Hintergrund zu Dolphin/KIO:** KIO schickt Zugangsdaten nie vorab, sondern > immer erst auf den `401` hin – und beginnt dabei sofort mit dem Rumpf, ohne > `Expect: 100-continue` abzuwarten. Große Uploads scheiterten deshalb mit > „unerwarteter Fehler 0", während kleine Dateien durchliefen. Behoben am > 22.08.2026: Der Server liest den Rumpf einer unauthentifizierten Anfrage weg, > bevor er die Anmeldeaufforderung schickt, damit die Verbindung stehen bleibt > und KIO die `401` überhaupt zu sehen bekommt. > Alle Punkte hier wurden am 19.08.2026 per `curl` gegen ein Wegwerf-Konto > geprüft und waren grün (🔎). Was das **nicht** abdeckt: ein echter Client > (Finder, Explorer, Cyberduck, rclone) verlangt andere PROPFIND-Properties und > nutzt Locking – deshalb stehen die Haken offen. - [x] Login mit Username + **App-Passwort** → Verbindung steht 🔎 - [x] Login mit falschem Passwort → abgelehnt (`401`) 🔎 - [x] Login mit dem **Kontopasswort** → abgelehnt (`401`), auch wenn es stimmt 🔎 - [x] Ordnerstruktur entspricht der in der Web-App (gleiche Ordner sichtbar) 🔎 ### WebDAV – Lesen - [x] Ordner auflisten (PROPFIND) zeigt Dateien + Unterordner 🔎 - [x] Datei herunterladen → Inhalt korrekt 🔎 - [x] Datei, die in der Web-App hochgeladen wurde, ist via WebDAV sichtbar 🔎 ### WebDAV – Schreiben - [x] Datei hochladen (PUT) → erscheint danach in der Web-App 🔎 - [x] Neuen Ordner anlegen (MKCOL) → erscheint in der Web-App 🔎 - [x] Datei umbenennen (MOVE im selben Ordner) 🔎 - [x] Datei in anderen Ordner verschieben (MOVE) 🔎 - [x] Ordner umbenennen 🔎 - [x] Datei löschen (DELETE) → landet im Papierkorb der Web-App (nicht hart gelöscht!) 🔎 - [x] Bestehende Datei überschreiben → bei **aktivierter** Versionierung landet die alte Fassung in der Historie; bei deaktivierter wird ersetzt (so gewollt, Versionierung ist pro Datei opt-in) 🔎 - [ ] Checksumme/Integrität: hochgeladene Datei wieder runterladen → identisch 🔎 - [x] **Große Datei (100 MB+) per Dolphin hochladen** – war der Fehler „unerwarteter Fehler 0" 🔎 - [ ] Quota wird bei WebDAV-Upload geprüft → `507 Insufficient Storage` 🔎 - [ ] Abgebrochener Upload (Kontingent voll oder Verbindung weg) hinterlässt **nichts** – keine abgeschnittene Datei, kein verwaistes Blob 🔎 - [ ] Rate-Limit/Lockout am WebDAV-Login (10 Fehlversuche / 15 min → `429`) 🔎 - [ ] App-Passwort ist der **einzige** WebDAV-Zugang, gilt nicht fürs Web-Login, und ist nach dem Widerruf sofort tot 🔎 ### App-Passwörter (Profil → App passwords) - [ ] App-Passwort anlegen → Klartext erscheint **einmalig**, „Copy" kopiert es - [ ] Damit einen WebDAV-Client verbinden (Username = normaler Benutzername) - [ ] Liste zeigt „Last used" nach der ersten Nutzung - [ ] Widerrufen → Client kommt sofort nicht mehr rein - [ ] Kontopasswort im WebDAV-Client → abgelehnt; der Anmeldedialog nennt den Grund im Realm-Text 🔎 - [ ] Kontopasswort funktioniert weiterhin fürs **Web-Login** 🔎 - [ ] App-Passwort funktioniert **nicht** fürs Web-Login 🔎 ### Audit-Log (Admin → Activity) - [ ] Tab zeigt Einträge, neueste zuerst 🔎 - [ ] Fehlgeschlagene Anmeldung erscheint rot als „Sign-in failed" 🔎 - [ ] Anmeldung, Passwortwechsel, User-/Gruppen-Änderungen, Freigaben und Löschungen tauchen auf 🔎 - [ ] Filter-Dropdown schränkt auf eine Aktion ein - [ ] Blättern funktioniert (ab 50 Einträgen) - [ ] Gelöschtes Konto: Eintrag bleibt lesbar, Name mit „(deleted account)" - [ ] Passwort-Reset steht im Protokoll – **ohne** das Passwort selbst - [ ] Normaler User bekommt auf `/api/v1/admin/audit` ein `403` ### Speicher-Bereinigung (Admin → Activity → „Run now") - [ ] „Run now" meldet „Nothing to clean up", solange alles in Ordnung ist 🔎 - [ ] Verwaiste Datei älter als 24 h wird entfernt, Ergebnis wird angezeigt 🔎 - [ ] Frisch angelegte verwaiste Datei bleibt liegen (Schonfrist) 🔎 - [ ] Echte Dateien bleiben nach dem Lauf unangetastet 🔎 - [ ] Der Lauf erscheint anschließend selbst im Audit-Log --- ## Phase 4.5/B – Interne Freigaben ### Freigeben (Rechtsklick → „Share with people…") - [ ] Ordner an eine **Person** freigeben, Recht „Can view" - [ ] Empfänger sieht ihn unter **Shared with me**, mit „View only" - [ ] Empfänger kann hineinsehen und herunterladen - [ ] Empfänger kann **nicht** umbenennen, löschen, hochladen (Meldung statt Fehler) - [ ] Recht auf „Can edit" ändern → wird ersetzt, **kein** zweiter Eintrag - [ ] Empfänger kann jetzt umbenennen und hochladen - [ ] Einzelne **Datei** freigeben → Empfänger sieht nur sie, nicht den Ordner drumherum - [ ] An eine **Gruppe** freigeben → alle Mitglieder sehen es, mit Hinweis „via " - [ ] Mitglied aus der Gruppe entfernen → Zugriff sofort weg - [ ] „Remove" im Dialog → Zugriff sofort weg ### Grenzen (das Wichtige) - [ ] Empfänger kann die Freigabe **nicht weitergeben** (nur der Eigentümer teilt) - [ ] Dateien bleiben beim Eigentümer: Speicherbalken des **Eigentümers** wächst, wenn der Empfänger etwas hochlädt – nicht der des Empfängers - [ ] Unterordner, den ein Empfänger anlegt, gehört dem **Eigentümer** - [ ] Empfänger kann eine geteilte Datei **nicht** in seinen eigenen Ordner verschieben - [ ] Ohne Freigabe liefert ein direkter Aufruf mit bekannter ID `404` (nicht `403` – sonst verriete die Antwort, dass die ID existiert) - [ ] Freigaben tauchen im Audit-Log als `share.grant` / `share.revoke` auf > Über WebDAV erscheinen Freigaben unter `/dav/Shared///` > – siehe eigener Abschnitt weiter unten. --- --- ## Phase 4.5/C – Gruppenordner ### Anlegen (Admin → Groups) - [ ] Gruppe wählen, Namen eingeben, „Create" → Ordner erscheint in der Liste - [ ] Zweiter Ordner für dieselbe Gruppe → Fehler „already has a folder" (`409`) 🔎 - [ ] Übersicht zeigt Gruppe, Mitgliederzahl und belegten Platz ### Beim Nutzer - [ ] **Kein** Mitglied: Ordner ist nicht sichtbar 🔎 - [ ] Mitglied hinzufügen → Ordner erscheint in dessen Wurzel, beschriftet als „Group folder" 🔎 - [ ] Zwei Mitglieder arbeiten darin: hochladen, herunterladen, umbenennen, Unterordner anlegen 🔎 - [ ] Mitglied entfernen → Ordner verschwindet sofort 🔎 ### Der Ordner selbst ist geschützt - [ ] Rechtsklick auf den Gruppenordner: **kein** Löschen, Umbenennen, Verschieben, Teilen – nur Öffnen und Favorit - [ ] Er lässt sich nicht per Drag & Drop verschieben - [ ] Direkter API-Aufruf (`DELETE /folders/{id}`) → `403` 🔎 - [ ] **Innerhalb** des Ordners ist alles normal löschbar 🔎 ### Speicher und Papierkorb - [ ] Dateien liegen unter `groups//home/…`, nicht bei einem Nutzer 🔎 - [ ] Kontingent zählt gegen die **Gruppe**, bei keinem Mitglied 🔎 - [ ] Gruppenkontingent überschritten → `413` 🔎 - [ ] Gelöschte Gruppendatei erscheint im Papierkorb **jedes Mitglieds** und lässt sich wiederherstellen 🔎 - [ ] Admin löscht den Gruppenordner → Daten und Verzeichnis restlos weg 🔎 ### Geteiltes und Gruppenordner über WebDAV Der Aufbau bildet die Weboberfläche nach: Gruppenordner liegen **in der Wurzel** neben den eigenen, Personen-Freigaben gebündelt unter `Shared/`. - [ ] Gruppenordner erscheint in der WebDAV-Wurzel, gleiche Liste wie in der Weboberfläche 🔎 - [ ] Hineinnavigieren, hochladen, Unterordner anlegen 🔎 - [ ] **Namensgleichheit:** eigener Ordner `Projekt` + Gruppenordner `Projekt` → eigener behält den Namen, Gruppenordner heißt `Projekt ()`, beide erreichbar, Schreibvorgänge landen jeweils richtig 🔎 - [ ] Nach Austritt aus der Gruppe verschwindet er aus der Wurzel 🔎 - [ ] `Shared` erscheint nur, wenn eine **Person** etwas geteilt hat 🔎 - [ ] `/Shared/` listet die Personen, `/Shared//` deren Freigaben 🔎 - [ ] Datei im geteilten Ordner lesen und schreiben (bei „Can edit") 🔎 - [ ] Einzeln geteilte Datei erscheint als **Datei**, nicht als Ordner 🔎 - [ ] Nur-Lesen: Schreiben und Löschen → `403` (nicht `404`/`405`) 🔎 - [ ] In `/Shared/` selbst lässt sich nichts anlegen oder löschen (`403`) 🔎 - [ ] Verschieben über Eigentumsgrenzen → `403` 🔎 - [ ] Nicht Vorhandenes bleibt `404` – verrät nichts 🔎 - [ ] Entzug der Freigabe / Austritt aus der Gruppe → sofort `404` 🔎 - [ ] Bens Upload landet im Bereich des **Eigentümers** und zählt gegen dessen Kontingent 🔎 - [ ] Prüfsumme über den geliehenen Pfad: Upload → WebDAV-GET → Web-Download, alles identisch 🔎 - [ ] Mit einem echten Client (Dolphin) durchgehen ### OIDC (nur falls konfiguriert) OIDC ist optional. Test nur, wenn `OIDC_ISSUER` etc. in `.env` gesetzt sind. **Wichtig:** `OIDC_REDIRECT_URL` muss auf `/api/v1/auth/oidc/callback` zeigen und exakt so beim IdP registriert sein. - [ ] Ohne OIDC-Config: **kein** SSO-Button auf der Login-Seite (Standardfall) - [ ] Mit OIDC-Config: SSO-Button erscheint - [ ] Klick auf SSO → Weiterleitung zum IdP - [ ] Nach erfolgreichem IdP-Login → zurück in Aether, eingeloggt - [ ] Beim ersten OIDC-Login wird automatisch ein User angelegt (aus `preferred_username`) - [ ] Zweiter OIDC-Login desselben Accounts → gleicher User (kein Duplikat) - [ ] Username-Kollision → automatischer Suffix (`name2`, `name3`, …) - [ ] Manipulierter/abgebrochener Callback → Redirect mit Fehler, kein Login - [ ] CSRF/State: Login mit falschem `state`-Cookie → abgelehnt --- ## Phase 4.5/D – Logo & App-Symbol ### Admin → Theming: Logo - [x] Logo hochladen → erscheint sofort in der Vorschau, Seitenleiste und auf der Anmeldeseite (ohne Speichern-Klick) 🔎 - [x] Logo austauschen → neues Bild sofort sichtbar, altes ist von der Platte verschwunden 🔎 - [ ] „Remove" → Standard-Wolke ist zurück 🔎 - [ ] SVG, Textdatei, getarnte EXE → abgelehnt (`415`) 🔎 - [ ] Datei über 2 MB → `413` mit klarer Meldung 🔎 - [ ] Normaler Nutzer kann weder hochladen noch löschen (`403`) 🔎 - [ ] Logo ist **ohne** Anmeldung abrufbar (die Anmeldeseite braucht es) 🔎 ### Admin → Theming: App-Symbol Ein Upload für beides – Tab-Symbol und installierte App. Der Server rechnet 64, 192, 512 und die maskable-Fassung daraus. - [ ] Quadratisches PNG (am besten 512×512) hochladen → Symbol im Tab wechselt ohne Neuladen 🔎 - [ ] Anderer, schon offener Tab zieht beim nächsten Laden nach - [ ] Austauschen → neues Symbol sofort da, altes Verzeichnis von der Platte verschwunden 🔎 - [ ] „Remove" → Standard-Symbol ist zurück, Verzeichnis gelöscht 🔎 - [ ] Vorlage kleiner als 192×192 → `422` mit klarer Meldung 🔎 - [ ] Vorlage mit 256×256 → es entsteht **kein** 512er, das Manifest nennt ihn auch nicht (nie hochskalieren) 🔎 - [ ] Nicht quadratisches Bild → wird gerahmt, **nicht** beschnitten 🔎 - [ ] SVG und ICO → abgelehnt (`415`); Datei über 2 MB → `413` 🔎 - [ ] Ohne Anmeldung abrufbar (der Browser fragt das Tab-Symbol vor dem Anmelden ab) 🔎 - [ ] Speicher-Bereinigung („Run now") löscht den Symbol-Satz **nicht** 🔎 ### Manifest (`/manifest.webmanifest`) - [ ] Direkt aufrufen → nennt Cloud-Namen und Primärfarbe aus dem Admin-Bereich, nicht die fest eingebauten Werte 🔎 - [ ] Ohne eigenes Symbol fällt es auf die mitgelieferten Bilder zurück 🔎 - [ ] Auf dem Handy „Zum Startbildschirm hinzufügen" → Name und Symbol stimmen - [ ] **Bereits installierte App:** Symbol wechselt erst nach ein, zwei Tagen – Chrome fragt das Manifest von selbst nach, aber nicht sofort. Sicher sofort nur nach Deinstallieren und neu Installieren. --- ## Vor und Zurück im Verlauf - [ ] In mehrere Ordner nacheinander wechseln → **Zurück** (Browser-Taste, Seitentaste der Maus oder Wischgeste) geht die Reihenfolge rückwärts, **Vor** wieder nach vorn - [ ] In der Werkzeugleiste stehen **keine** eigenen Vor/Zurück-Pfeile mehr – das macht allein der Browser - [ ] Über die Seitenleiste die Ansicht wechseln (Papierkorb, Freigaben, Admin, Profil) → Zurück führt zur vorigen Ansicht - [ ] Aus einer anderen Ansicht zurück zu „Files" → der zuletzt geöffnete Ordner ist wieder da, nicht die Wurzel - [ ] Über die Brotkrumen nach oben springen → zählt als eigener Schritt, Zurück führt wieder nach unten - [ ] Auf einen Favoriten in der Seitenleiste klicken → eigener Schritt im Verlauf - [ ] Zweimal denselben Ordner öffnen, in dem man schon steht → erzeugt **keinen** zweiten Schritt (Zurück springt nicht ins Leere) - [ ] Seite neu laden (`F5`) → man landet wieder im selben Ordner - [ ] Adresse zeigt die Ansicht (`/admin`, `/trash`); direkt eingegeben lädt sie auch. Der Ordner steht **nicht** in der Adresse – ein Link auf einen tiefen Ordner ist also nicht teilbar (bewusst, siehe Projektplan) --- ## Suche Feld in der Werkzeugleiste, rechts neben den Brotkrumen. Sucht in **Namen** von Dateien und Ordnern – und seit dem 23.08.2026 zusätzlich im **Inhalt** von PDFs und Bildern (siehe „Volltext über den Inhalt" weiter unten). - [ ] Ab zwei Zeichen erscheinen Treffer, ein Zeichen tut nichts - [ ] Unter jedem Treffer steht der Ordner, in dem er liegt - [ ] Ordner-Treffer anklicken → Ordner wird geöffnet - [ ] Datei-Treffer anklicken → springt **nur** in den Ordner. Es öffnet sich **keine** Vorschau 🔎 - [ ] Die gesuchte Datei ist dort kurz umrandet und wird in den sichtbaren Bereich geholt; die Markierung verschwindet nach knapp drei Sekunden von selbst 🔎 - [ ] Geöffnet wird weiterhin per **Doppelklick** auf die Datei - [ ] Das gilt in Kachel- **und** Listenansicht - [ ] Pfeiltasten wählen aus, Enter öffnet, Esc schließt - [ ] Schnell tippen → keine veralteten Treffer (jede Anfrage bricht die vorige ab) - [ ] Papierkorb bleibt außen vor: gelöschte Datei erscheint nicht 🔎 ### Volltext über den Inhalt (OCR) Der Punkt, für den ein Rechnungsarchiv überhaupt gebaut wird: Ein Beleg namens `scan_0042.pdf` muss über das zu finden sein, was **drin** steht. Wichtig zum Verständnis beim Prüfen: Die Erkennung läuft **im Hintergrund**. Direkt nach dem Hochladen findet die Suche den Inhalt noch nicht – bei einem PDF mit Textebene nach etwa einer Sekunde, bei einem Scan nach rund einer Minute je Seite. Der Stand steht in `GET /api/v1/admin/ocr` (noch ohne Oberfläche) und im Log (`docker compose logs app | grep component=ocr`). - [ ] PDF **mit** Textebene hochladen → nach ein paar Sekunden findet ein Wort aus dem Text die Datei; im Log steht `source=layer` 🔎 - [ ] Eingescannte oder abfotografierte Rechnung hochladen → nach etwa einer Minute je Seite findet ein Wort daraus die Datei; im Log `source=ocr` 🔎 - [ ] Unter dem Treffer steht die **Fundstelle** mit hervorgehobenem Suchwort, und der Name allein hätte nicht gepasst 🔎 - [ ] Die Fundstelle läuft über **zwei Zeilen** und bricht am Ende der zweiten mit „…" ab – nicht mitten in der ersten 🔎 - [ ] Links daneben eine dünne Linie in derselben Farbe wie das hervorgehobene Wort; sie trennt das Zitat vom Ordnernamen darüber 🔎 - [ ] Ein Dokument mit `