chore: add Claude Code config and brainstorm notes

This commit is contained in:
ralksta
2026-05-29 17:05:18 +02:00
parent 5dffadbe3c
commit e723358514
3 changed files with 346 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
---
name: run-immich-folio
description: Build, run, and drive immich-folio. Use when asked to start the app, run its dev server, take a screenshot of the portfolio, interact with the running gallery, or test immich-folio.
---
Immich Folio is a Next.js 16 (App Router) photography portfolio that proxies images from an Immich server. Drive it by starting the Next.js dev server and running `.claude/skills/run-immich-folio/driver.mjs` from the repo root — the script uses Playwright's bundled Chromium to navigate the gallery, verify the health endpoint, and capture screenshots.
All paths below are relative to the repo root (`immich-folio/`).
## Prerequisites
Node 20+ and npm are sufficient — no extra system packages needed.
Playwright's Chromium is bundled in `node_modules` but must be installed once:
```bash
npm install
node_modules/.bin/playwright install chromium
```
## Setup
Copy the example files if `content/gallery.yaml` or `.env.local` don't exist yet:
```bash
cp .env.local.example .env.local
# edit .env.local: set IMMICH_API_URL and IMMICH_API_KEY
cp content/gallery.yaml.example content/gallery.yaml
# edit gallery.yaml: paste Immich album UUIDs
```
Required env vars (`.env.local`):
```bash
IMMICH_API_URL=http://your-immich:2283 # base URL of your Immich instance
IMMICH_API_KEY=your-api-key-here # from Immich → Admin → API Keys
AUTH_SECRET=a-long-random-secret # required in production; auto-generated in dev
```
## Run (agent path)
Start the dev server in the background, then run the driver:
```bash
npm run dev &> /tmp/immich-folio-dev.log &
echo $! > /tmp/immich-folio.pid
# Wait until the health endpoint answers
for i in {1..30}; do
curl -sf http://localhost:3001/api/health >/dev/null && echo "Ready" && break
sleep 1
done
```
> **Port note:** If port 3000 is taken, Next.js auto-assigns the next free port (3001, 3002, …). Check the log: `grep "Local:" /tmp/immich-folio-dev.log`
Run the driver (from repo root):
```bash
node .claude/skills/run-immich-folio/driver.mjs http://localhost:3001
```
Expected output:
```
[1/4] Homepage...
title: Ralfos Portfolio
→ /tmp/shots/01-homepage.png
[2/4] Subpage: /deutschland
→ /tmp/shots/02-subpage.png
[3/4] Album: /deutschland/an-der-ostsee
→ /tmp/shots/03-album-detail.png
[4/4] API health...
{"status":"ok","immich":"connected","uptime":...}
✓ All checks passed. Screenshots in /tmp/shots/
```
Screenshots land in `/tmp/shots/`. Pass a different URL as the first argument to target any running instance:
```bash
node .claude/skills/run-immich-folio/driver.mjs http://localhost:3002
```
Stop the dev server:
```bash
kill $(cat /tmp/immich-folio.pid)
# or: pkill -f "next dev"
```
## Run (human path)
```bash
npm run dev # → http://localhost:3001 (or next free port). Ctrl-C to stop.
```
## Test
```bash
# Unit tests (Vitest, 66 tests across 7 suites — runs in ~250ms)
npm run test:unit
# Type-check
npx tsc --noEmit
# E2E (Playwright, starts its own dev server automatically)
npm run test:e2e
```
---
## Gotchas
- **Port auto-selection** — if 3000 is occupied, Next.js picks 3001 silently. The driver defaults to 3001; override with the first argument if needed. Check `grep "Local:" /tmp/immich-folio-dev.log` to find the actual port.
- **Use `page.goto()` not `.click()` for album navigation** — Next.js RSC streaming causes `.click()` + `waitForLoadState('networkidle')` to capture the previous page's DOM before the new route has streamed in. Navigate by href directly.
- **`eval()` console error in dev** — React emits `eval() is not supported in this environment` in dev mode due to the CSP middleware injecting a nonce. This is dev-only noise; production is unaffected. The driver filters it out.
- **`⚠ The "middleware" file convention is deprecated`** — Next.js 16 warns about `middleware.ts` being renamed to `proxy.ts`. Harmless; the app still works.
- **Immich must be reachable** — all pages are `dynamic = 'force-dynamic'` (no SSG). If `IMMICH_API_URL` is unreachable the health endpoint returns `{"immich":"error"}` and album pages return 404. The driver will fail at step 4.
- **`content/gallery.yaml` must have at least one album or subpage** — `getConfig()` throws at startup if the file is empty or has no albums. If you only have the example file, fill in real UUIDs first.
## Troubleshooting
- **`Cannot find package '@playwright/test'`**: Run the driver from the repo root (`immich-folio/`), not from `/tmp` or another directory. `@playwright/test` is in `node_modules/` here.
- **Health returns 404 (returns HTML from a different app)**: Port 3000 is another app. Check the actual port: `grep "Local:" /tmp/immich-folio-dev.log` and pass it to the driver.
- **`EADDRINUSE` on restart**: The previous dev server is still running. `pkill -f "next dev"` before relaunching.
- **`gallery.yaml must define at least one album or subpage`**: Your `content/gallery.yaml` is empty or only has comments. Add at least one real Immich album UUID under `albums:` or `subpages:`.
- **Security warning about `AUTH_SECRET`**: Set `AUTH_SECRET` in `.env.local`. In development the app generates a temporary one per session (cookies break on restart). In production it hard-fails if unset.

View File

@@ -0,0 +1,89 @@
/**
* Immich Folio smoke driver.
*
* Usage (run from repo root):
* node .claude/skills/run-immich-folio/driver.mjs [BASE_URL]
*
* BASE_URL defaults to http://localhost:3001.
* Screenshots land in /tmp/shots/.
*
* Exits 0 on success, 1 on any failure.
*/
import { chromium } from '@playwright/test';
import { mkdirSync } from 'fs';
const BASE = process.argv[2] ?? process.env.BASE_URL ?? 'http://localhost:3001';
const SHOTS = '/tmp/shots';
mkdirSync(SHOTS, { recursive: true });
let browser;
try {
browser = await chromium.launch({ args: ['--no-sandbox'] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const consoleErrors = [];
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
// ── 1. Homepage ──────────────────────────────────────────────
console.log('[1/4] Homepage...');
await page.goto(`${BASE}/`);
await page.waitForLoadState('networkidle');
const title = await page.title();
if (!title) throw new Error('Homepage has no title');
console.log(` title: ${title}`);
await page.screenshot({ path: `${SHOTS}/01-homepage.png` });
console.log(` → ${SHOTS}/01-homepage.png`);
// ── 2. First nav subpage ─────────────────────────────────────
// Grab the first subpage link from the header nav (skip "Home" and "About")
const firstSubpageHref = await page.$eval(
'.header__nav a:not([href="/"]):not([href="/about"]):not([href="/map"])',
el => el.getAttribute('href'),
);
if (!firstSubpageHref) throw new Error('No subpage nav link found');
console.log(`[2/4] Subpage: ${firstSubpageHref}`);
await page.goto(`${BASE}${firstSubpageHref}`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: `${SHOTS}/02-subpage.png` });
console.log(` → ${SHOTS}/02-subpage.png`);
// ── 3. Album detail ──────────────────────────────────────────
// Album cards on a subpage grid use class "subpage-grid__item".
// Navigate by href (not click) — click + networkidle can race with RSC streaming.
const albumCard = page.locator('a.subpage-grid__item').first();
const albumHref = await albumCard.getAttribute('href').catch(() => null);
if (albumHref) {
console.log(`[3/4] Album: ${albumHref}`);
await page.goto(`${BASE}${albumHref}`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: `${SHOTS}/03-album-detail.png` });
console.log(` → ${SHOTS}/03-album-detail.png`);
} else {
// Subpage had only one album — we're already on the album detail
console.log('[3/4] Already on album detail (single-album subpage)');
await page.screenshot({ path: `${SHOTS}/03-album-detail.png` });
console.log(` → ${SHOTS}/03-album-detail.png`);
}
// ── 4. Health endpoint ───────────────────────────────────────
console.log('[4/4] API health...');
const health = await page.evaluate(async (base) => {
const r = await fetch(`${base}/api/health`);
return r.json();
}, BASE);
console.log(` ${JSON.stringify(health)}`);
if (health.status !== 'ok') throw new Error(`Health check failed: ${JSON.stringify(health)}`);
// ── Summary ───────────────────────────────────────────────────
const appErrors = consoleErrors.filter(e => !e.includes('eval() is not supported'));
if (appErrors.length > 0) {
console.warn(`\n⚠ Console errors (${appErrors.length}):`);
appErrors.forEach(e => console.warn(' ', e));
}
console.log(`\n✓ All checks passed. Screenshots in ${SHOTS}/`);
} finally {
await browser?.close();
}

119
docs/brainstorm.md Normal file
View File

@@ -0,0 +1,119 @@
# Feature Brainstorm
_Stand: 2026-05-29 — generiert aus Projekt-Review + ideas.md_
---
## ✅ Bereits implementiert
| Feature | Datum | Notizen |
|---|---|---|
| **Album-Beschreibungen** | 2026-05-29 | `description:` in `gallery.yaml` (pro Album), eigenes `<p>` unter dem Titel in `AlbumDetailView` |
| **Webhook Cache-Invalidierung** | 2026-05-29 | `POST /api/webhook`, HMAC-SHA256, `WEBHOOK_SECRET` in `.env`, gezielte oder vollständige Cache-Invalidierung |
| **Video-Support** | 2026-05-29 | Proxy `GET /api/video/[token]` mit Range-Support, Play-Button im Grid, `<video controls>` in Lightbox |
---
## ⚡ Quick Wins (offen)
| Idee | Effort | Warum interessant |
|---|---|---|
| **Sequentielle Album-Navigation** | XS | „← Vorheriges / Nächstes Album →" am Ende einer Detailseite |
| **Alt-Text aus Immich-Description** | XS | Immich-Assets haben ein `description`-Feld → als `alt`-Text → bessere Accessibility & SEO |
| **RSS/Atom Feed** | S | `/feed.xml` mit neuen Alben — Follower können „neue Arbeit" abonnieren |
| **Globaler Passwortschutz** | S | Eine einzige `site_password` für die gesamte Site (jetzt: nur per Subpage/Album) |
| **Privacy-Analytics-Integration** | S | Plausible / Umami / GoatCounter via Script-Tag in `settings.yaml` konfigurieren (kein Tracking-Framework nötig) |
---
## 📅 Timeline-View
Chronologische Ansicht aller Fotos quer durch alle Alben — geordnet nach Aufnahmedatum (EXIF).
- Routing: `/timeline` oder als alternative Subpage-Layout-Option
- Gruppierung nach Monat/Jahr als visuelle Trennlinie
- Nutzt bereits vorhandene EXIF-Daten
- Guter Einstieg für Visitor, die sich treiben lassen wollen
---
## 🔍 Suche (Immich Smart Search)
Immich hat eine eigene Suche (inkl. semantischer KI-Suche). Folio könnte das exponieren:
- Suchfeld → `POST /api/search` → proxied an Immich `/api/search/smart`
- Ergebnisse nur aus Albums die im `gallery.yaml` erlaubt sind (Allowlist!)
- Simple Text-Suche oder Semantic Search togglebar
- Echter Differentiator gegenüber anderen Portfolio-Tools
---
## 🖼️ Client-Delivery-Portal
Kombination aus bestehenden Features (Passwortschutz + Download):
- Password-geschützte Alben + Download-Button im Lightbox (schon in ideas.md)
- **Neu:** Auswahl-Modus — Client kann Fotos markieren/favorisieren
- „Ich habe meine Auswahl getroffen" → E-Mail-Notification an Fotografen
- Alles ohne Datenbank: Auswahl als signiertes JSON-Cookie oder URL-Share-Link
_Konkretisiert die „Client proofing"-Idee aus `ideas.md`._
---
## 📡 PWA / Offline-Support
- `manifest.json` + Service Worker
- Letzte besuchte Galerie wird gecacht (für schlechte Verbindung)
- „Installierbar" auf iOS/Android
- Besonders interessant für Fotografen die das Folio auf Messen/Events zeigen
---
## 🗺️ Map-View (Vollausbau)
Die API `/api/map` existiert schon — der Frontend-Teil fehlt noch.
- Interaktive Karte (MapLibre + freie Tiles, z.B. OpenFreeMap)
- Clustered Dots → Klick öffnet Album-Vorschau
- Als eigene `/map`-Seite oder als optionaler Hero-Typ
- Filter nach Zeitraum
_Basis-Idee schon in `ideas.md`, hier konkretisiert._
---
## 📊 EXIF-Analytics (konkretisiert)
Schon in `ideas.md` — hier ein konkreter Ansatz:
- Eigene Seite `/stats` oder Einbettung in `about`
- Charts: Focal-Length-Distribution, Aperture vs. ISO-Heatmap, „meist genutzte Kamera/Objektiv"
- Datenquelle: aggregierte EXIF aller zugänglichen Alben
- Lightweight Charts (z.B. Chart.js oder reine SVG-Balken ohne Dependency)
---
## 🔗 Prioritäten-Übersicht
```
✅ Erledigt:
→ Video-Support (2026-05-29)
→ Webhook Cache-Invalidierung (2026-05-29)
→ Album-Beschreibungen (2026-05-29)
🔥 Als nächstes:
→ RSS Feed (0 Dependencies, großer Community-Nutzen)
→ Sequentielle Album-Navigation (XS Effort)
→ Map-View Vollausbau (Backend ist schon da!)
🚀 Mittelfristig:
→ Client-Delivery-Portal (echter Business-Case)
→ Privacy Analytics (einfache Integration)
→ Globaler Passwortschutz
💡 Längerfristige Differenziator:
→ Immich Smart Search (KI-Suche als Alleinstellungsmerkmal)
→ Photo Stories (schon in ideas.md, gut ausgearbeitet)
→ PWA / Offline-Support
```