Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not, so this is a clean sync. ## Headline changes since v0.10.0 - **Journal & Journal Studio** — photo essays at `/journal`, split-screen block editor with live preview, drafts, per-entry passwords, cover images - **First-run setup wizard at `/install`** — configure a fresh deployment from the browser; credentials are verified against Immich before anything is written, and the wizard is gated by a one-time token - **About page editor and toggle** in the admin panel - **Per-album photo order** independent of Immich, with a drag & drop editor - **Custom favicon upload** - **Experimental portfolio features** — justified grid, `cover` hero, unlisted subpages, per-album grid overrides, cover focal points, external nav links - **Security** — asset tokens length-capped before decoding (#423), journal author markdown escaped rather than filtered, journal file paths contained, admin session key derived with scrypt (#432), Next.js 16.3.0 for CVE-2025-13465 - **Fixed** — client proofing honours its own configuration again (#454); journal photo rendering (#432) Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15). ## Verified `npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run build` succeeds · lint 0 errors · `prettier --check` clean · local `docker build` of the release tree. ## After merge Tag `v0.11.0` on `main` and publish the release — that triggers `docker-publish.yml`, which pushes the image and moves `latest`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
337 lines
14 KiB
Markdown
337 lines
14 KiB
Markdown
# 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.
|