365 lines
16 KiB
Markdown
365 lines
16 KiB
Markdown
|
|
# 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.
|