Files
immich-folio/docs/brainstorm-2026-08-05.md
Ralksta 716ae14b0f Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work

Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).

* test: enable route-handler tests and cover admin guards and image content-type

vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.

Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.

Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.

134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.

* feat: serve stale cache entries while Immich is unavailable

v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.

Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.

getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.

STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.

256 tests (up from 244), tsc 0 errors, build succeeds.

* feat: add loading skeletons for dynamic gallery routes

Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.

Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.

* docs: document stale cache window, image token revocation, route-test convention

- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
  unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
  outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
  System & Security — un-publishing an album does not revoke already
  issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
  MISSING sentinel, and a new code-convention note that route-level
  logic (guards, header sanitisation) should get a route-level test
  rather than being extracted into lib/ just to reach the test runner.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00

14 KiB
Raw Blame History

Feature-Brainstorm: Verbreitung & Robustheit

Stand: 2026-08-05 · Basis: v0.9.1 · Fokus: Open-Source-Verbreitung + Betrieb & Robustheit

Dieses Dokument ergänzt ideas.md (Präsentations-Features) und brainstorm.md (Stand Mai 2026). Es wiederholt deren Ideen nicht, sondern nimmt eine andere Perspektive ein: Was hindert andere Fotografen daran, Folio einzusetzen — und was geht im Betrieb kaputt?

Alle Ideen sind gegen den tatsächlichen Code-Stand geprüft. Aufwand: XS (Stunden), S (1 Tag), M (2–5 Tage), L (Wochen).


Kontext: Was seit Mai dazugekommen ist

Aus brainstorm.md bereits erledigt: Map-View (app/map/page.tsx, components/MapView.tsx), Video-Support, Webhook-Invalidierung, Album-Beschreibungen.

Weiterhin offen aus den alten Listen (hier bewusst nicht nochmal ausgeführt): RSS-Feed, Slideshow, Download-Button, Share-Link, Fullscreen, Timeline-View, Immich Smart Search, Photo Stories, PWA, Watermarking, Client-Proofing, EXIF-Analytics.


A. Betrieb & Robustheit

A1. Fehler-, Lade- und 404-Boundaries — XS, hoher Impact

Befund: Im gesamten app/-Verzeichnis existiert keine einzige error.tsx, global-error.tsx, loading.tsx oder not-found.tsx.

Weil alle Seiten dynamic = 'force-dynamic' sind und live gegen Immich laufen, heißt das: Immich neu gestartet, API-Key rotiert, Netzwerk kurz weg → der Besucher bekommt die generische Next.js-Fehlerseite. Ein Tippfehler in der URL → ebenfalls ungestylt. Für ein Produkt, dessen Verkaufsargument „schön" ist, ist das der peinlichste mögliche Zustand.

Umsetzung: app/error.tsx und app/global-error.tsx im Theme-Look mit Retry-Button; app/not-found.tsx; loading.tsx pro Route-Segment mit ThumbHash-Skeletons (lib/thumbhash.ts liefert die Bausteine schon).

Warum zuerst: Billigste Maßnahme mit der größten Wirkung auf den wahrgenommenen Reifegrad.


A2. Stale-while-error — Galerie bleibt online, wenn Immich weg ist — M, hoher Impact

Befund: lib/cache.ts ist ein 200-Einträge-LRU mit harter TTL (CACHE_TTL, Default 300s). Läuft ein Eintrag ab und Immich antwortet nicht, gibt es keinen Fallback — die Seite ist tot.

Idee: Abgelaufene Einträge nicht wegwerfen, sondern als stale markieren. Bei Upstream-Fehler den veralteten Eintrag trotzdem ausliefern, im Hintergrund erneut versuchen, und (optional konfigurierbar) einen dezenten Hinweis einblenden. Das öffentliche Portfolio überlebt damit ein Immich-Update, ein Backup-Fenster oder einen NAS-Reboot.

Warum das zieht: Genau das ist die Angst des Self-Hosters — „meine öffentliche Seite geht offline, weil mein privater Server hustet". Folio als Puffer vor Immich zu positionieren statt als bloßen Proxy ist ein echtes Argument.

Anker: lib/cache.ts, lib/immich.ts (Request-Coalescing ist schon da und passt gut dazu).


A3. Persistenter Bild-Cache auf Platte — M, hoher Impact auf schwacher Hardware

Befund: streamAsset() proxied jedes Bild bei jedem Kaltstart erneut von Immich. Der LRU-Cache hält nur Metadaten, keine Bytes.

Idee: Optionales Cache-Volume (CACHE_DIR), in dem proxied Thumbnails und Previews pro Größen-Tier landen. Die Asset-Tokens sind bereits deterministisch (lib/tokens.ts: gleiche UUID → gleicher Token), also ist der Cache-Key geschenkt. Mit LRU-Eviction und konfigurierbarem Größenlimit.

Warum: Die typische Zielgruppe hostet auf Synology, RasPi oder einem alten NUC. Dort ist wiederholtes Proxying der Flaschenhals — und es hält gleichzeitig den Immich-Server unter Last.


A4. ISR statt force-dynamic, getrieben vom vorhandenen Webhook — M

Befund: Alle Seiten sind force-dynamic. Gleichzeitig existiert bereits POST /api/webhook mit HMAC-Signatur und gezielter Cache-Invalidierung.

Idee: Seiten auf Next.js-Cache-Tags umstellen (albums, album-<id>) und den Webhook revalidateTag() aufrufen lassen statt nur den internen LRU zu leeren. Ergebnis: statisch schnelle Auslieferung, trotzdem sofort aktuell, sobald sich in Immich etwas ändert.

Achtung: Passwortgeschützte Subpages/Alben müssen dynamisch bleiben — die Cookie-Prüfung in lib/auth.ts darf nicht wegcachen. Das ist die eigentliche Arbeit an dieser Idee, nicht das Umstellen selbst.

Stand in ideas.md schon als Einzeiler; hier mit dem Webhook als Auslöser konkretisiert.


A5. Config-Doctor — S, hoher Impact auf Support-Aufwand

Befund: GET /api/admin/status existiert bereits als Ansatzpunkt. Es gibt aber keine Stelle, die typische Fehlkonfigurationen aktiv meldet.

Idee: Ein Diagnose-Panel im Admin (plus npm run doctor fürs Terminal), das prüft:

  • AUTH_SECRET gesetzt und ausreichend lang?
  • TRUSTED_PROXY_HOPS plausibel? — tatsächlich eingehende X-Forwarded-For-Kette messen und mit dem konfigurierten Wert vergleichen. Ein falscher Wert legt das Rate-Limiting still oder macht es spoofbar; heute merkt das niemand.
  • Existieren alle Album-IDs aus gallery.yaml noch in Immich?
  • Reichen die Permissions des API-Keys?
  • Liegen Passwörter im Klartext statt als scrypt:-Hash? (lib/auth.ts warnt bislang nur in die Logs, wo es keiner liest.)
  • Ist content/ beschreibbar? (war real ein Bug — siehe Commit-History)

Warum: Fast jedes „geht bei mir nicht"-Issue eines Self-Hosting-Projekts ist eine dieser Zeilen. Der Doctor beantwortet sie, bevor das Issue aufgemacht wird.


A6. AUTH_SECRET automatisch bereitstellen — XS

Befund: lib/secret.ts wirft in Production hart, wenn AUTH_SECRET fehlt. Korrekt gedacht (nicht auf eine ratbare Konstante zurückfallen), aber es bedeutet: docker run ohne gesetztes Secret → App startet nicht, mit einem Fehler, den Einsteiger nicht sofort einordnen.

Idee: Fehlt die Env-Variable, beim ersten Start ein Zufalls-Secret erzeugen und nach content/.secret (Mode 0600) persistieren. Weiterhin warnen, aber nicht blockieren. Env-Variable behält Vorrang.

Trade-off, der dokumentiert gehört: Das Secret liegt dann im Content-Volume statt in der Umgebung. Für ein Self-Hosted-Portfolio ist das der richtige Kompromiss — das Volume enthält ohnehin die Galerie-Passwörter.


A7. Strukturierte Logs + optionale Metriken — S bis M

Heute: console.warn-Prosa. Vorschlag: JSON-Logzeilen hinter LOG_LEVEL, plus optionales GET /api/metrics im Prometheus-Textformat mit Cache-Hit-Rate, Immich-Latenz-Histogramm, Rate-Limit-Treffern und Bytes durch den Bild-Proxy.

Warum es sich lohnt: Die Self-Hosting-Community betreibt Grafana. Ein Projekt, das ein fertiges Dashboard-JSON mitliefert, wirkt sofort erwachsen — und es ist wenig Arbeit, weil die Zählpunkte (lib/cache.ts, lib/immich.ts, lib/rate-limit.ts) schon zentralisiert sind.


A8. Redis-Backend, aber nur optional — M, niedrige Priorität

Rate-Limiter und Cache sind ausdrücklich In-Memory und skalieren nicht über mehrere Instanzen. Ein optionales Backend hinter REDIS_URL würde das lösen.

Ehrliche Einschätzung: Die Zielgruppe fährt einen Container. Das ist Dokumentation eines bekannten Limits, kein dringendes Feature. Nur angehen, wenn konkret danach gefragt wird.


B. Open-Source-Verbreitung

B1. Setup-Assistent im Browser statt Anleitung — M, größter Adoption-Hebel

Befund: components/SetupScreen.tsx erklärt dem Nutzer, welche Dateien er per Hand kopieren und ausfüllen soll. Gleichzeitig kann das Admin-Panel bereits YAML atomar schreiben (lib/admin/yaml-service.ts) und alle Immich-Alben durchsuchen (/api/admin/albums) — die Bausteine für einen echten Assistenten liegen also schon da.

Idee: Aus dem Setup-Screen einen geführten Ablauf machen:

  1. Immich-URL + API-Key eingeben → Verbindung sofort testen, Permissions prüfen
  2. Aus den gefundenen Shared Albums auswählen (visuelle Auswahl, keine UUIDs)
  3. Theme-Preset aus Vorschaukarten wählen (die Screenshots existieren in docs/screenshots/)
  4. Titel/Untertitel eintragen → fertig, Galerie steht

Schreibt gallery.yaml, settings.yaml und die Credentials. Absicherung: nur erreichbar, solange needsSetup gilt — danach dicht.

Warum das die wichtigste Einzelmaßnahme ist: Der Unterschied zwischen „funktioniert nach fünf Minuten" und „ich muss erst UUIDs aus der Immich-API kopieren" entscheidet, ob jemand dabei bleibt. Immich selbst hat diesen Onboarding-Standard gesetzt; Folio wird daran gemessen.


B2. Demo-Modus ohne Immich-Instanz — S bis M

Befund: Ohne laufenden Immich lässt sich Folio überhaupt nicht ansehen. Auch die E2E-Tests (e2e/) hängen deshalb an einer echten Instanz.

Idee: DEMO_MODE=true schaltet lib/immich.ts auf einen Fixture-Adapter mit mitgelieferten, frei lizenzierten Bildern um. Drei Effekte auf einen Schlag:

  • Eine öffentliche Live-Demo, die man im README verlinken kann
  • E2E-Tests laufen deterministisch in CI, ohne Immich
  • Interessenten können docker run machen und sofort etwas sehen

Warum: Bei Self-Hosted-Software ist die Live-Demo der Konversionspunkt. Screenshots im README reichen nicht — Leute wollen durchklicken.


B3. Deploy-Templates für die Kanäle, wo die Zielgruppe sucht — S

Docker-Compose ist vorhanden. Was fehlt, sind die Verzeichnisse, in denen Immich-Nutzer tatsächlich stöbern: unRAID Community Applications, CasaOS- und Umbrel-App-Store, Portainer-Stack, Coolify-/Dokploy-Template.

Wenig Code, überwiegend Metadaten und PRs an fremde Repos. Aber es ist der Unterschied zwischen „muss man kennen" und „findet man".


B4. Internationalisierung (DE/EN) — M

Steht in der alten Liste, hier aber mit anderer Begründung: nicht als Besucher-Komfort, sondern als Verbreitungs-Voraussetzung. Das Projekt hat deutsche Wurzeln (app/impressum/page.tsx, das legal:-Feld ist auf § 18 MStV gemünzt), die Oberfläche ist Englisch, und deutsche Nutzer stolpern über den Mix. Umgekehrt schreckt ein „Impressum" im Footer internationale Nutzer ab, solange nicht klar ist, dass es abschaltbar ist.

Reihenfolge: Erst die Besucher-Oberfläche, dann das Admin-Panel. Ohne Framework — ein einfacher Wörterbuch-Lookup gegen settings.yaml: locale genügt und passt zur Vanilla-CSS-/Kein-Framework-Linie des Projekts.


B5. Update-Hinweis im Admin-Dashboard — XS bis S

Version aus package.json gegen die GitHub-Releases-API prüfen (gecacht, täglich, abschaltbar) und im Admin-Dashboard dezent anzeigen. Standard bei Self-Hosted-Tools; Nutzer laufen sonst monatelang auf alten Versionen — insbesondere problematisch, weil es bereits Security-Releases gab (37f7f62).


B6. Themes teilbar machen — S

Export/Import von settings.yaml als „Theme-Paket" plus ein themes/community/-Verzeichnis im Repo, in das Nutzer per PR ihre Presets beisteuern.

Warum: Es schafft einen Beitragsweg für Leute, die kein TypeScript schreiben. Fotografen mit Gestaltungsanspruch sind genau die Zielgruppe — und jeder eingereichte Look ist zugleich Werbematerial.


B7. Tests für die API-Routen — M

lib/__tests__/ deckt die Bibliotheken gut ab (Tokens, Auth, Rate-Limit, Config), e2e/ deckt fünf Seiten ab. Die Routen selbst — Bild-Proxy, Auth-Endpoint, die sieben Admin-Routen — haben keine direkten Tests.

Das ist ausgerechnet die Schicht, an der die Sicherheitsgarantien des Projekts hängen: Allowlist-Durchsetzung, Token-Decoding, Rate-Limit-Namespaces, Admin-Session-Prüfung. Nach 37f7f62 („fix pre-auth bypasses") sollte hier eine Regressions-Suite liegen. Sie ist außerdem das, was Contributors Sicherheit gibt, überhaupt PRs zu stellen.


B8. Kleinigkeit: Projektname in package.json — XS

"name": "immich-lightbox" bei einem Projekt namens Immich Folio. Fällt jedem Contributor beim ersten npm install auf.


C. Ein größerer Differenziator

C1. Mehrere Galerien aus einer Instanz — L

Heute: ein Container, ein Portfolio. Denkbar: mehrere Sites aus einer Instanz, je nach Host-Header, mit eigener gallery.yaml, eigenem Theme und eigener Domain — bei geteiltem Immich-Backend und geteiltem Bild-Cache.

Zielgruppen: Fotografen-Kollektive; Leute, die für Kunden hosten; jemand, der öffentliches Portfolio und private Familiengalerie trennen will.

Ehrliche Einschätzung: Deutlich mehr Aufwand als alles andere hier, weil Config-Singleton (lib/config/index.ts), Cache-Keys und Auth-Cookie-Namen alle mandantenfähig werden müssten. Nur sinnvoll, wenn tatsächlich Nachfrage entsteht — aber es wäre etwas, das kein vergleichbares Tool bietet. Als Beobachtungsposten notieren, nicht als Plan.


Empfohlene Reihenfolge

Erst (kleiner Aufwand, sofort sichtbar):

  1. A1 — Fehler-/Lade-/404-Boundaries · XS
  2. A6 — AUTH_SECRET automatisch bereitstellen · XS
  3. B8 — Projektname korrigieren · XS
  4. B5 — Update-Hinweis im Admin · XS–S

Dann (die zwei Hebel, die den Unterschied machen):

  1. B1 — Setup-Assistent im Browser · M
  2. A2 — Stale-while-error · M

Danach:

  1. A5 — Config-Doctor · S
  2. B2 — Demo-Modus · S–M
  3. B3 — Deploy-Templates · S
  4. A3 — Persistenter Bild-Cache · M
  5. B7 — API-Routen-Tests · M

Später / nach Bedarf: A4 (ISR), A7 (Logs & Metriken), B4 (i18n), B6 (Theme-Sharing), A8 (Redis), C1 (Multi-Site).


Roter Faden

Zwei Sätze, die die Auswahl zusammenhalten:

Verbreitung entscheidet sich in den ersten fünf Minuten — deshalb Setup-Assistent (B1) und Demo-Modus (B2) vor allem anderen.

Robustheit heißt bei Folio nicht Uptime der eigenen App, sondern Unabhängigkeit von der Verfügbarkeit des privaten Immich — deshalb ist Stale-while-error (A2) das strategisch wichtigste Backend-Feature, nicht ISR.