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>
This commit is contained in:
336
docs/brainstorm-2026-08-05.md
Normal file
336
docs/brainstorm-2026-08-05.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# Feature-Brainstorm: Verbreitung & Robustheit
|
||||
|
||||
_Stand: 2026-08-05 · Basis: v0.9.1 · Fokus: Open-Source-Verbreitung + Betrieb & Robustheit_
|
||||
|
||||
Dieses Dokument ergänzt [`ideas.md`](ideas.md) (Präsentations-Features) und
|
||||
[`brainstorm.md`](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):**
|
||||
|
||||
5. B1 — Setup-Assistent im Browser · _M_
|
||||
6. A2 — Stale-while-error · _M_
|
||||
|
||||
**Danach:**
|
||||
|
||||
7. A5 — Config-Doctor · _S_
|
||||
8. B2 — Demo-Modus · _S–M_
|
||||
9. B3 — Deploy-Templates · _S_
|
||||
10. A3 — Persistenter Bild-Cache · _M_
|
||||
11. 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.
|
||||
364
docs/deep-dive-2026-08-05.md
Normal file
364
docs/deep-dive-2026-08-05.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# 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
|
||||
`<html>` und `<body>` vollständig ersetzt.
|
||||
|
||||
Daraus folgt: `global-error.tsx` muss **komplett eigenständig** sein — eigene
|
||||
`<html>`/`<body>`, 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<T>(key)`, das auch abgelaufene Daten herausgibt. Die Speichergrenze
|
||||
regelt weiterhin die LRU-Eviction. Ein hartes Limit (`STALE_MAX_AGE`, Default 24 h)
|
||||
verhindert, dass ein dauerhaft totes Immich wochenalte Daten ausliefert.
|
||||
|
||||
**2. `lib/immich.ts`: Fallback statt Leere.**
|
||||
In `getAlbums()`, `getAlbum()` und `getAssetInfo()` bei `null` aus `request()`
|
||||
zuerst `cache.getStale()` versuchen. Die Rückgabe sollte mitführen, ob die Daten
|
||||
frisch oder stale sind — sonst kann die Seite es nicht anzeigen.
|
||||
|
||||
**3. Konsequenz ziehen: nicht `notFound()`, wenn der Upstream tot ist.**
|
||||
Gibt es weder frische noch stale Daten *und* ist Immich nachweislich unerreichbar,
|
||||
dann werfen statt 404. Das ist derselbe Eingriff wie in Thema 1 — die beiden
|
||||
Punkte sind eigentlich ein Feature von zwei Seiten. Den Ping nicht neu bauen:
|
||||
`app/api/health/route.ts` hat bereits einen 10-Sekunden-gecachten, gegen
|
||||
Thundering-Herd abgesicherten `immich.ping()`; den Mechanismus in ein kleines
|
||||
Modul ziehen und beidseitig nutzen.
|
||||
|
||||
### Trade-offs, die benannt gehören
|
||||
|
||||
- **Stale kann falsch sein.** Ein in Immich gelöschtes Foto bliebe bis zu
|
||||
`STALE_MAX_AGE` sichtbar. Für ein kuratiertes Portfolio ist das Risiko gering,
|
||||
aber es ist eine bewusste Entscheidung, keine Nebenwirkung.
|
||||
- **Passwörter sind nicht betroffen.** Sie stammen aus `gallery.yaml`, nicht aus
|
||||
Immich — eine Passwortänderung greift also sofort, unabhängig vom Stale-Cache.
|
||||
Das ist der beruhigende Teil.
|
||||
- **Nur im Prozessspeicher.** Nach einem Container-Neustart ist der Cache leer,
|
||||
ein gleichzeitig totes Immich hilft dann nicht. Der persistente Bild-Cache
|
||||
(A3 im Brainstorm) ist die natürliche Fortsetzung — zusammen überleben die
|
||||
beiden auch einen Neustart.
|
||||
|
||||
### Aufwand
|
||||
|
||||
**M.** Die Cache-Änderung selbst ist klein; die Sorgfalt steckt in der
|
||||
404/503-Politik und darin, den Stale-Zustand ehrlich anzuzeigen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tests für die API-Routen
|
||||
|
||||
### Befund: strukturell ausgeschlossen
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
include: ['lib/__tests__/**/*.test.ts'],
|
||||
```
|
||||
|
||||
Die Routen sind nicht vergessen worden — sie sind per Konfiguration außen vor.
|
||||
Das ist eine Einzeilenänderung.
|
||||
|
||||
Und die Infrastruktur passt bereits:
|
||||
|
||||
- `environment: 'node'` — Route-Handler sind gewöhnliche async-Funktionen, die
|
||||
ein `NextRequest` nehmen. `new NextRequest('http://localhost/api/image/xyz')`
|
||||
genügt, es braucht keinen laufenden Server.
|
||||
- `vitest.setup.ts` setzt die Pflicht-Env inklusive `AUTH_SECRET`.
|
||||
- `lib/__tests__/immich.test.ts` zeigt das Muster: `vi.mock('../config')` plus
|
||||
`global.fetch = vi.fn()`. Exakt das, was Routentests brauchen.
|
||||
|
||||
Der Einstieg ist also billiger, als es aussieht.
|
||||
|
||||
### Was zu testen sich lohnt — Invarianten, keine Zeilen
|
||||
|
||||
**Allowlist.** `getAlbum()` weist nicht gelistete IDs ab (`lib/immich.ts:374`).
|
||||
Ein Album aus `gallery.yaml` entfernen → Route muss 404 liefern.
|
||||
|
||||
**Token-Grenze.** `/api/image/[id]` mit roher UUID statt Token → 400. Mit einem
|
||||
Token, das unter einem *anderen* `AUTH_SECRET` erzeugt wurde → 400. `tokens.ts`
|
||||
ist unit-getestet; hier geht es darum, dass die *Route* das durchsetzt.
|
||||
|
||||
**Die Lücke, die dokumentiert gehört.** Der Bild-Proxy dekodiert den Token und
|
||||
streamt (`app/api/image/[id]/route.ts:78`) — er prüft **nicht**, ob das Asset noch
|
||||
zu einem freigegebenen Album gehört. Da Tokens deterministisch sind (bewusst, für
|
||||
Browser-Caching, siehe `CLAUDE.md`), folgt daraus: **Ein Album aus `gallery.yaml`
|
||||
zu entfernen widerruft bereits ausgelieferte Bild-URLs nicht.** Sie funktionieren
|
||||
weiter, solange `AUTH_SECRET` gleich bleibt.
|
||||
|
||||
Das ist ein legitimer Trade-off, aber ein überraschender. Er verdient einen Test,
|
||||
der das Verhalten festschreibt, und eine Zeile im README — sonst nimmt jemand an,
|
||||
„nicht mehr veröffentlicht" hieße „nicht mehr erreichbar".
|
||||
|
||||
**Content-Type-Sanitisierung.** Zeilen 84–93 der Bild-Route stammen aus einem
|
||||
echten Stored-XSS-Fix (`c2fa8e7`). `image/svg+xml` und `text/xml` müssen zu
|
||||
`application/octet-stream` werden. Klassischer Regressionskandidat.
|
||||
|
||||
**Rate-Limit-Namespaces.** Die Buckets sind pro Endpunkt getrennt (`image:`,
|
||||
`map:`, `auth:`). Test: `image:` ausschöpfen darf `/api/auth` nicht auf 429 setzen.
|
||||
|
||||
**Admin-Guards — der wertvollste Einzeltest.** Alle sieben `/api/admin/*`-Routen
|
||||
müssen ohne gültiges Session-Cookie ablehnen, ebenso mit einem unter falschem
|
||||
Secret signierten Token. Das ist genau die Fläche von Commit `37f7f62`.
|
||||
|
||||
Entscheidend ist die Bauform: **tabellengetrieben über die Liste der
|
||||
Admin-Routen**, nicht sieben Einzeltests. Dann schlägt die Suite auch bei einer
|
||||
*künftig hinzugefügten* Route ohne Guard an — sie testet dann die Regel statt der
|
||||
sieben bekannten Fälle. Das ist der Unterschied zwischen einer Suite, die alte
|
||||
Bugs festhält, und einer, die neue verhindert.
|
||||
|
||||
**Passwort-Gate.** `/api/auth` setzt das Cookie nur bei korrektem Passwort;
|
||||
Vergleich zeitkonstant; die drei Pfade `scrypt:` / Klartext / abgelehntes bcrypt.
|
||||
|
||||
**304/ETag.** Bei `If-None-Match` kein Body.
|
||||
|
||||
### Aufwand
|
||||
|
||||
**M** für die volle Abdeckung. Aber der Einstieg ist klein: `vitest.config.ts`
|
||||
erweitern, dann Admin-Guard-Tabelle und Content-Type-Regression zuerst — das ist
|
||||
etwa ein halber Tag und deckt bereits beide Stellen ab, an denen es in der
|
||||
Vergangenheit real geknallt hat.
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge und Zusammenhänge
|
||||
|
||||
Die vier Themen sind nicht unabhängig:
|
||||
|
||||
```
|
||||
Thema 1 (Boundaries) ─┐
|
||||
├─→ gemeinsame 404-vs-503-Politik
|
||||
Thema 3 (Stale) ─┘
|
||||
|
||||
Thema 2 (Assistent) ──→ hängt an der Credentials-Entscheidung (c)
|
||||
|
||||
Thema 4 (Tests) ──→ sichert 1–3 ab, sollte parallel mitlaufen
|
||||
```
|
||||
|
||||
**Phase 1 — etwa ein Tag, sofort sichtbar**
|
||||
`global-error.tsx`, `error.tsx`, `not-found.tsx`, `loading.tsx`;
|
||||
`vitest.config.ts` erweitern; Admin-Guard-Tabellentest.
|
||||
|
||||
**Phase 2 — die Betriebs-Härtung**
|
||||
Stale-Cache plus 404/503-Politik. Ein Vorhaben, nicht zwei. Danach die
|
||||
Content-Type- und Allowlist-Tests nachziehen.
|
||||
|
||||
**Phase 3 — der Verbreitungs-Hebel**
|
||||
Setup-Assistent, beginnend mit dem Hybrid-Fallback für Credentials und der
|
||||
Entscheidung, `ADMIN_PASSWORD` vorauszusetzen.
|
||||
|
||||
### Der eine Satz dahinter
|
||||
|
||||
Phase 1 und 2 sorgen dafür, dass die Seite eines fremden Fotografen auch dann
|
||||
noch etwas Vernünftiges tut, wenn dessen Immich hustet — und Phase 3 sorgt dafür,
|
||||
dass dieser Fotograf überhaupt erst so weit kommt.
|
||||
@@ -196,17 +196,17 @@ Your bio text here. Supports full Markdown.
|
||||
|
||||
- **Album UUIDs**: In Immich, go to Albums → click an album → the UUID is in the URL bar
|
||||
- **Asset UUIDs**: Click any photo → the UUID is in the URL bar
|
||||
|
||||
|
||||
## System & Security
|
||||
|
||||
|
||||
Advanced system settings are configured via environment variables in your `.env` or `.env.local` file.
|
||||
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
|
||||
To protect against brute-force attacks and resource exhaustion, Immich Folio includes an in-memory rate limiter.
|
||||
|
||||
|
||||
- `RATE_LIMIT_RPM`: Maximum requests per minute per IP (default: `1500` for general API, `120` for map data).
|
||||
|
||||
|
||||
### Trusted Proxies
|
||||
|
||||
If you run Immich Folio behind a reverse proxy (nginx, Traefik, Caddy, Cloudflare), tell it how many proxies sit in front. Without this, the rate limiter reads a client-supplied header and an attacker can pick their own bucket — defeating brute-force protection on the password endpoints.
|
||||
@@ -237,8 +237,14 @@ location / {
|
||||
}
|
||||
```
|
||||
|
||||
`$proxy_add_x_forwarded_for` is the important one: it *appends* the real peer address rather than overwriting the header.
|
||||
`$proxy_add_x_forwarded_for` is the important one: it _appends_ the real peer address rather than overwriting the header.
|
||||
|
||||
#### Migrating from `TRUSTED_PROXIES`
|
||||
|
||||
`TRUSTED_PROXIES` has been removed. It matched proxy IPs against the socket peer address, which a self-hosted Next.js server never exposes — so the check never passed. Setting it silently put **every visitor into one shared rate-limit bucket**, letting a single client trip the limit for the entire site. If it is still set, the app assumes `TRUSTED_PROXY_HOPS=1` and logs a warning; replace it with an explicit value.
|
||||
|
||||
### Image Token Revocation
|
||||
|
||||
Asset tokens are deterministic — the same photo always yields the same token, which is what lets browsers cache images for a year. The image proxy validates the token but does not re-check album membership on every request.
|
||||
|
||||
Removing an album from `gallery.yaml` hides it from the site, but any image URL already handed out (bookmarked, embedded, cached by a browser) keeps working. To invalidate previously issued links, rotate `AUTH_SECRET` — this also signs out every password-protected gallery and admin session.
|
||||
|
||||
1154
docs/superpowers/plans/2026-08-05-resilience-boundaries.md
Normal file
1154
docs/superpowers/plans/2026-08-05-resilience-boundaries.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user