# Deep Dive: die vier priorisierten Themen _Stand: 2026-08-05 · Basis: v0.9.1 · Vertiefung zu [`brainstorm-2026-08-05.md`](brainstorm-2026-08-05.md)_ Vertieft: (1) Fehler-Boundaries, (2) Setup-Assistent, (3) Stale-while-error, (4) API-Routen-Tests. **Vorab drei Korrekturen** an meiner ersten Einschätzung, die sich beim Lesen des Codes ergeben haben: - Ein unerreichbarer Immich führt **nicht** zu einem Absturz. `request()` fängt den Fehler und gibt `null` zurück — die Seite wird still leer und liefert HTTP 404 für real existierende Alben. Das ist ein anderes (und heimtückischeres) Problem als ein Crash. - Ein **`app/error.tsx` würde den wichtigsten Fehlerfall gar nicht abfangen**, weil `app/layout.tsx:53` selbst `getConfig()` aufruft. Nötig ist `global-error.tsx`. - Der Setup-Assistent hat ein architektonisches Hindernis, das man erst beim Bauen bemerkt: Credentials kommen aus **Env-Variablen**, nicht aus YAML — und `lib/env.ts` parst genau einmal beim Prozessstart. --- ## 1. Fehler-, Lade- und 404-Boundaries ### Befund Kein `error.tsx`, `global-error.tsx`, `not-found.tsx` oder `loading.tsx` im gesamten `app/`-Baum. Es gibt **zwei völlig verschiedene Fehlermodi**, die unterschiedliche Antworten brauchen: **Modus A — die Konfiguration wirft.** `getConfig()` wirft an mehreren Stellen hart: | Stelle | Auslöser | |---|---| | `lib/config/index.ts:136` | `gallery.yaml` ohne Album und ohne Subpage | | `lib/config/index.ts:144` | Subpage ohne `name` | | `lib/config/index.ts:170` | Subpage ohne Alben/Sections | | `lib/secret.ts:20` | `AUTH_SECRET` fehlt in Production | Nicht in dieser Liste: `validateUuid()` (`lib/config/parser.ts`) wirft **nicht** — es warnt und liefert bewusst eine Dummy-UUID zurück, damit der Build nicht bricht. Eine kaputte Album-ID führt also zu einem stillschweigend fehlenden Album, nicht zu einem Fehler. Das ist ein Fall für den Config-Doctor (A5), nicht für die Boundaries. **Modus B — Immich ist nicht erreichbar.** Hier wirft nichts: `request()` fängt alles ab (`lib/immich.ts:108–111`) und liefert `null`. Folge: - `getAlbums()` → `[]` → Startseite zeigt **stillschweigend keine Alben** - `getAlbum()` → `null` → `notFound()` (`app/[...path]/page.tsx:233`) → **HTTP 404 für eine URL, die es wirklich gibt** - Map, EXIF, OG-Images: leer ### Der entscheidende technische Punkt `app/error.tsx` wird **innerhalb** des Root-Layouts gerendert. Da das Layout selbst `getConfig()` aufruft (`app/layout.tsx:53`), wirft bei Modus A das Layout — und ein `error.tsx` kommt nie zum Zug. Nur `app/global-error.tsx` greift, weil es `` und `
` vollständig ersetzt. Daraus folgt: `global-error.tsx` muss **komplett eigenständig** sein — eigene ``/``, Inline-Styles, kein `getConfig()`, kein Theme-Zugriff. Genau wie der heutige `SetupScreen`, der aus demselben Grund schon inline gestylt ist. ### Entwurf **`app/global-error.tsx`** — der Konfigurations-Notausgang. Sollte nicht „Etwas ist schiefgelaufen" sagen, sondern die Datei nennen, in der das Problem steckt. Die Fehlermeldungen aus `getConfig()` sind bereits präzise formuliert (`Subpage "Reisen" must have albums or sections`) — sie sind die halbe Miete, sie kommen nur nirgends an. In Production nur Dateiname + Meldung zeigen, keine Stacktraces. **`app/error.tsx`** — für Render-Fehler unterhalb des Layouts, im Theme-Look, mit `reset()`-Button. **`app/not-found.tsx`** — gestaltete 404 statt Next.js-Default. **`loading.tsx` pro Segment** — Skeletons aus ThumbHash (`lib/thumbhash.ts` ist da). Wirkt besonders, weil alle Seiten `force-dynamic` sind und heute komplett auf die Immich-Antwort warten, bevor irgendetwas erscheint. ### Der Teil, der wirklich zählt: 404 vs. 503 Heute kann die App nicht unterscheiden zwischen „dieses Album existiert nicht" und „ich kann Immich gerade nicht fragen". Beides endet in `notFound()`. Das ist nicht nur kosmetisch: Ein 404 sagt Suchmaschinen *„diese URL löschen"*, ein 503 sagt *„später nochmal versuchen"*. Ein Immich-Update während eines Google-Crawls kann so die Sichtbarkeit des Portfolios kosten. **Vorschlag:** Bevor `app/[...path]/page.tsx` `notFound()` aufruft, den Upstream-Status prüfen (`immich.ping()` ist über `/api/health` bereits mit 10-Sekunden-Cache versehen — wiederverwenden, nicht neu bauen). Ist Immich unerreichbar, stattdessen werfen → Error-Boundary → 5xx. Das greift direkt in Thema 3 über. ### Aufwand **XS** für die drei Boundary-Dateien. **S**, wenn die 404/503-Unterscheidung mitkommt. Ich würde beides zusammen machen — die Boundaries allein sind Kosmetik, erst mit der Statusunterscheidung werden sie ein Feature. --- ## 2. Setup-Assistent im Browser ### Befund: das Hindernis `components/SetupScreen.tsx` erklärt vier Dateikopien und endet mit *„restart your server"*. Ein Assistent müsste stattdessen selbst schreiben — und stößt sofort auf ein Problem: ```ts // lib/env.ts, letzte Zeile export const env = parseEnv(); // läuft genau einmal beim Modul-Load ``` `IMMICH_API_URL` und `IMMICH_API_KEY` kommen aus der Umgebung. Ein Browser-Formular kann eine Umgebungsvariable eines laufenden Prozesses nicht ändern. YAML dagegen ist bereits heiß nachladbar — `loadYaml()` prüft die mtime, und `getConfig()` verzichtet bewusst auf einen In-Memory-Cache (Kommentar in `lib/config/index.ts:49–52`). Die Credentials liegen also ausgerechnet in der einzigen Schicht, die einen Neustart erzwingt. ### Drei Wege **(a) Assistent schreibt `.env.local`.** Erfordert `docker restart`. Zerstört das Versprechen „in fünf Minuten fertig", und in vielen Compose-Setups liegt `.env.local` nicht einmal in einem gemounteten Volume — die Datei wäre nach dem Neustart weg. **(b) Credentials nach `content/` verlagern**, z. B. `content/connection.yaml` (Mode 0600), gelesen über denselben mtime-Loader. Kein Neustart, Assistent läuft durch. Bricht aber bestehende Deployments, wenn Env nicht mehr gilt. **(c) Hybrid — Empfehlung.** Env gewinnt, wenn gesetzt; sonst Fallback auf `content/connection.yaml`. Bestehende Installationen ändern sich um nichts, neue Nutzer bekommen den Assistenten. `lib/env.ts` bleibt der alleinige Env-Parser; der Fallback gehört nach `lib/config/index.ts`, wo `apiUrl`/`apiKey` ohnehin schon aufgelöst werden (Zeilen 53–54). Ein Eingriff, eine Stelle. ### Was schon existiert Bemerkenswert viel — der Assistent ist eher Verdrahtung als Neubau: | Baustein | Wo | |---|---| | `/admin` bleibt während des Setups erreichbar | `app/layout.tsx:68–79`, `isAdminPath()`, Commit `8f0bb94` | | Atomares YAML-Schreiben mit Backups | `lib/admin/yaml-service.ts` | | Alle Shared Albums listen (ohne Allowlist) | `GET /api/admin/albums` | | Visuelle Album-/Asset-Auswahl | `AlbumPicker.tsx`, `AssetPicker.tsx` | | Theme-Vorschaubilder | `docs/screenshots/theme-*.png` | | Cache nach dem Speichern invalidieren | `invalidateConfigCache()` | Dass `/admin` während `needsSetup` offen bleibt, wurde in `#392` bewusst gebaut — mit genau dieser Begründung („it is the tool that completes the setup"). ### Ablauf 1. **Verbinden** — URL + API-Key eingeben, sofort gegen `/server/ping` testen, Permissions prüfen, Immich-Version prüfen (Folio braucht ≥ 3.0, siehe die Warnung in `lib/immich.ts:400`). Fehler hier sofort und verständlich melden — das ist die Stelle, an der die meisten scheitern. 2. **Alben wählen** — visuell aus den Shared Albums, keine UUIDs. 3. **Look wählen** — Preset-Karten mit den vorhandenen Screenshots. 4. **Identität** — Titel, Untertitel, Footer. Schreibt `connection.yaml`, `gallery.yaml`, `settings.yaml`; ruft `invalidateConfigCache()`; leitet auf die fertige Galerie. ### Die Sicherheitsfrage, die vorher beantwortet sein muss Während `needsSetup` gilt, ist die Instanz frisch — möglicherweise ohne gesetztes `ADMIN_PASSWORD`. Ein ungeschützter Assistent auf einer öffentlichen URL bedeutet: Wer den Container zuerst findet, besitzt ihn. Zwei gängige Muster: einmaliges Setup-Token ins Container-Log schreiben (wie es diverse Self-Hosted-Tools machen), oder `ADMIN_PASSWORD` zur Voraussetzung machen. **Empfehlung: `ADMIN_PASSWORD` voraussetzen.** Das Sicherheitsmodell bleibt exakt wie heute (`lib/admin/auth.ts`, HMAC-Session-Cookie), es gibt keinen zweiten, schwächeren Pfad — und aus Nutzersicht ist es trotzdem ein Fortschritt: **eine** Env-Variable statt dreien. Angesichts von Commit `37f7f62` („fix pre-auth bypasses") ist zusätzliche Vorsicht in genau diesem Bereich angebracht. ### Aufwand **M.** Der größte Einzelhebel für Verbreitung — aber erst nach der Credentials-Entscheidung (c) sinnvoll anzufangen, weil sie den Rest bestimmt. --- ## 3. Stale-while-error ### Befund: es ist kein Absturz, es ist Stille Meine ursprüngliche Formulierung war ungenau. `request()` wirft nicht, es gibt `null` zurück. Wenn Immich weg ist, passiert Folgendes: | Aufruf | Ergebnis | Für den Besucher | |---|---|---| | `getAlbums()` | `[]` | Startseite ohne Alben | | `getAlbum(id)` | `null` → `notFound()` | **HTTP 404 auf echter URL** | | `getAssetInfo()` | `null` | Kein EXIF, keine Map-Punkte | | `/api/image/[id]` | 404 pro Bild | Kaputte Bildplätze | Immerhin sauber gelöst: das leere Ergebnis wird **nicht** gecacht (`lib/immich.ts:214`, `if (!all) return []` steht vor `cache.set`) — es brennt sich also nichts ein. Das eigentliche Problem ist der 404. Eine Portfolio-Seite, die während eines Immich-Neustarts gecrawlt wird, meldet Suchmaschinen die Löschung ihrer eigenen Alben. Das ist der Grund, warum ich das über ISR stellen würde: Bei ISR geht es um Millisekunden, hier geht es um Auffindbarkeit. ### Entwurf — drei kleine Eingriffe **1. `lib/cache.ts`: abgelaufene Einträge behalten.** Heute löscht `get()` beim Ablauf (Zeilen 23–26). Stattdessen zwei Zeitstempel pro Eintrag — `staleAt` (bisheriges TTL) und `hardExpiresAt` — und ein neues `getStale