release: v0.11.0 (#459)
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>
This commit is contained in:
@@ -30,12 +30,12 @@ unterschiedliche Antworten brauchen:
|
||||
|
||||
**Modus A — die Konfiguration wirft.** `getConfig()` wirft an mehreren Stellen hart:
|
||||
|
||||
| Stelle | Auslöser |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| `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.
|
||||
@@ -85,8 +85,8 @@ Immich-Antwort warten, bevor irgendetwas erscheint.
|
||||
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
|
||||
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
|
||||
@@ -108,12 +108,12 @@ erst mit der Statusunterscheidung werden sie ein Feature.
|
||||
### Befund: das Hindernis
|
||||
|
||||
`components/SetupScreen.tsx` erklärt vier Dateikopien und endet mit
|
||||
*„restart your server"*. Ein Assistent müsste stattdessen selbst schreiben — und
|
||||
_„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
|
||||
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
|
||||
@@ -145,14 +145,14 @@ aufgelöst werden (Zeilen 53–54). Ein Eingriff, eine Stelle.
|
||||
|
||||
Bemerkenswert viel — der Assistent ist eher Verdrahtung als Neubau:
|
||||
|
||||
| Baustein | Wo |
|
||||
|---|---|
|
||||
| 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()` |
|
||||
| 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").
|
||||
@@ -199,12 +199,12 @@ Credentials-Entscheidung (c) sinnvoll anzufangen, weil sie den Rest bestimmt.
|
||||
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 |
|
||||
| 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
|
||||
@@ -230,7 +230,7 @@ zuerst `cache.getStale()` versuchen. Die Rückgabe sollte mitführen, ob die Dat
|
||||
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,
|
||||
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
|
||||
@@ -286,8 +286,8 @@ Der Einstieg ist also billiger, als es aussieht.
|
||||
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.
|
||||
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
|
||||
@@ -313,7 +313,7 @@ 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
|
||||
_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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user