chore: add Claude Code config and brainstorm notes
This commit is contained in:
138
.claude/skills/run-immich-folio/SKILL.md
Normal file
138
.claude/skills/run-immich-folio/SKILL.md
Normal 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.
|
||||
89
.claude/skills/run-immich-folio/driver.mjs
Normal file
89
.claude/skills/run-immich-folio/driver.mjs
Normal 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
119
docs/brainstorm.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user