diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6346bc2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +Alle nennenswerten Aenderungen an Bricktrack werden hier festgehalten. + +Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), +die Versionierung [Semantic Versioning](https://semver.org/lang/de/). + +## [Unreleased] + +### Hinzugefuegt + +- Projekt-Scaffold: Next.js 15 (App Router, TypeScript strict), + Tailwind CSS 4, shadcn/ui, pnpm. +- Vollstaendiges Datenmodell in Drizzle ORM mit initialer Migration: + Katalog, Besitz, Wunschlisten und die Auth-Tabellen von better-auth. +- SQLite-Anbindung mit WAL-Modus, aktivierten Fremdschluesseln und + Migrations-Runner (`pnpm db:migrate`). +- Projektdokumentation: CLAUDE.md, Roadmap, Changelog, Devlog. + +[Unreleased]: https://git.wilde-server.de/florian.wilde/bricktrack/compare/main...HEAD diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e48bf7d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,166 @@ +# Bricktrack + +Self-hosted Webanwendung zur Verwaltung einer Spielzeug-Set-Sammlung +(Lego, Playmobil, Cobi, BlueBrixx, Mega Bloks und beliebige weitere +Hersteller). Sie beantwortet zwei Fragen sofort: **Welche Sets hat er?** +und **Welche Sets hat er noch nicht?** Aus den fehlenden Sets entstehen +teilbare Wunschlisten. + +## Stack + +| Bereich | Wahl | Anmerkung | +|---|---|---| +| Framework | Next.js 15, App Router, Server Actions | `output: 'standalone'` | +| Sprache | TypeScript strict | | +| UI | Tailwind CSS 4, shadcn/ui (radix-nova), lucide | mobile first | +| Datenbank | SQLite via better-sqlite3, WAL | Datei unter `DATA_DIR/db` | +| ORM | Drizzle ORM + drizzle-kit | Migrationen in `drizzle/` | +| Auth | better-auth + `@better-auth/passkey` | E-Mail/Passwort und WebAuthn | +| Validierung | zod (serverseitig, immer) | | +| Bilder | sharp (nur serverseitig, Thumbnails) | | +| Tests | Vitest (Unit), Playwright (E2E, Screenshots) | | +| Paketmanager | pnpm 10 | | + +Bewusst **kein** Prisma (Standalone-Build im Container), **keine** +Cloud-Dienste, **keine** externen Requests aus dem Browser ausser zu +Bild-URLs, die der Nutzer selbst hinterlegt hat. + +## Konventionen + +- **UI-Texte auf Deutsch**, **Code und Kommentare auf Englisch**. +- Datum `dd.MM.yyyy`, Waehrung EUR, Zeitzone `Europe/Berlin`. +- **Geldbetraege liegen als Integer in Cent** in der Datenbank + (`retail_price_cents`, `acquired_price_cents`). SQLite hat keinen + Decimal-Typ; Float-Summen waeren ungenau. Umrechnung ausschliesslich + in `src/lib/money.ts`. +- Datenbankspalten in `snake_case` - **ausser** in den Auth-Tabellen, + die `camelCase` verwenden, weil das exakt der Ausgabe von + `@better-auth/cli generate` entspricht. +- IDs sind Text-UUIDs (`crypto.randomUUID()`), Zeitstempel sind + `integer(mode: 'timestamp')`. +- Routen sind deutsch (`/katalog`, `/sammlung`, `/wunschlisten`), + API-Routen englisch (`/api/health`, `/api/files`). +- Commits: Conventional Commits, Betreff auf Deutsch, Imperativ, + erste Zeile <= 72 Zeichen. + +## Befehle + +```bash +pnpm dev # Entwicklungsserver auf http://localhost:3000 +pnpm build # Produktionsbuild (standalone) +pnpm start # Produktionsserver +pnpm lint # ESLint +pnpm typecheck # tsc --noEmit +pnpm db:generate # SQL-Migration aus dem Schema erzeugen +pnpm db:migrate # Migrationen anwenden (auch im Container-Start) +pnpm db:seed # Beispieldaten einspielen +pnpm db:studio # Drizzle Studio +pnpm test # Vitest +pnpm test:e2e # Playwright +pnpm screenshots # Screenshots fuer docs/ANLEITUNG.md erzeugen +``` + +## Datenmodell + +Kernregel: Ein **`set`** ist ein *Katalogeintrag* - es existiert, +unabhaengig davon ob wir es besitzen. Ein **`collection_item`** +verknuepft ein Set mit dem *tatsaechlichen Besitz*. + +> **"Hat er nicht"** = Sets im Katalog, zu denen kein `collection_item` +> mit einem Status ungleich `sold` existiert. + +Diese Bedingung steht an genau einer Stelle: `src/lib/ownership.ts`. + +### Katalog + +| Tabelle | Zweck | Wichtige Felder | +|---|---|---| +| `manufacturer` | Hersteller | `name`, `slug` (unique), `website` | +| `theme` | Thema/Serie, hierarchisch | `manufacturer_id`, `parent_theme_id`, unique je (Hersteller, Name) | +| `set` | Katalogeintrag | unique `(manufacturer_id, set_number)`, `year`, `piece_count`, `minifig_count`, `ean`, `image_url` oder `image_path`, `retail_price_cents`, `external_ids` (JSON) | +| `tag`, `set_tag` | freie Verschlagwortung (n:m) | | +| `instruction` | Anleitung | `kind` = `pdf_upload` (dann `file_path`) oder `external_url` (dann `url`), per CHECK erzwungen | + +### Besitz + +| Tabelle | Zweck | Wichtige Felder | +|---|---|---| +| `collection_item` | ein besessenes Exemplar | `status` = `owned` / `built` / `disassembled` / `lent_out` / `sold`, `completeness`, `condition`, `location_id`, `acquired_*`, `gifted_by` | +| `location` | Aufbewahrungsort, hierarchisch | `parent_location_id` | +| `missing_part` | Fehlteile je Exemplar | `part_number`, `color_name`, `quantity`, `resolved` | +| `photo` | eigene Fotos | gehoert entweder zu `set_id` **oder** `collection_item_id` (CHECK) | + +### Wunschlisten + +| Tabelle | Zweck | Wichtige Felder | +|---|---|---| +| `wishlist` | eine Liste | `share_token` (32 Zufallsbytes, unique), `is_public`, `surprise_mode`, `occasion`, `event_date`, `note` | +| `wishlist_item` | ein Wunsch | unique `(wishlist_id, set_id)`, `priority` 1-3 (CHECK), `sort_order`, `reserved_by_name`, `reserved_at`, `reservation_token_hash`, `fulfilled_at` | + +Der Unique-Index auf `(wishlist_id, set_id)` macht das Ueberspringen von +Dubletten bei der Mehrfachauswahl zu einer **Datenbankgarantie**, nicht +nur zu einer UI-Regel: Der Bulk-Insert laeuft mit +`onConflictDoNothing()` und meldet die uebersprungenen Sets zurueck. + +`fulfilled_at` wird gesetzt, sobald das Set in der Sammlung auftaucht. + +### Auth + +`user` (mit `role` = `admin` | `member`), `session`, `account`, +`verification`, `passkey`, `rateLimit` kommen von better-auth. +`invite` ist selbst gebaut: Der Klartext-Token steht nur im Link, in der +Datenbank liegt sein SHA-256-Hash. Gleiches gilt fuer +`wishlist_item.reservation_token_hash`. Der `wishlist.share_token` +bleibt im Klartext, weil er im Dashboard erneut angezeigt werden muss. + +## Verzeichnisse zur Laufzeit + +Ein einziges `./data` reicht fuer ein vollstaendiges Backup. + +``` +DATA_DIR/ + db/bricktrack.db SQLite (+ -wal, -shm) + files/ + images/sets//... Set-Bilder (oeffentlich lesbar) + photos/... eigene Fotos (nur mit Session) + instructions//... Anleitungs-PDFs (nur mit Session) +``` + +Set-**Bilder** sind ohne Login abrufbar, weil die oeffentliche +Wunschliste sie braucht. Fotos und Anleitungen erfordern zwingend eine +Session. + +## Sicherheitsregeln + +1. Jede Eingabe wird **serverseitig** mit zod validiert. Clientseitige + Validierung ist Komfort, nie Schutz. +2. Uploads: nur PDF, JPEG, PNG, WebP. Pruefung ueber MIME **und** + Magic Bytes. Groessenlimit aus `MAX_UPLOAD_MB`. Dateinamen werden + **immer** serverseitig erzeugt, nie vom Client uebernommen. +3. Oeffentliche Wunschlisten-Routen (`/w/[shareToken]`) nutzen + ausschliesslich den Share-Token als Zugriffsschluessel und geben + nichts preis ausser den Wunschlisten-Daten - keine Nutzernamen, + keine Sammlungsdaten, keine IDs anderer Objekte. +4. Externe API-Aufrufe (Rebrickable, ab Phase 2) laufen ausschliesslich + serverseitig. +5. Session-Cookies: `httpOnly`, `secure`, `sameSite=lax`. + Rate-Limiting auf den Login-Endpunkten. + +## Phasenplan + +- **Phase 1 (MVP)** - Auth mit Passkeys, Stammdaten, Katalog, Sammlung, + Wunschlisten inkl. oeffentlicher Ansicht, Docker-Stack. +- **Phase 2 (Komfort)** - Rebrickable-Import, Barcode-Scanner, Fehlteile, + Einladungen, Export/Import, PWA. +- **Phase 3 (Nice to have)** - mehrere Sammler, Preisbeobachtung, + E-Mail-Erinnerungen, Dublettenpruefung. + +Details und Stand: `docs/ROADMAP.md`. Zeitaufwaende: `docs/DEVLOG.md`. + +## Arbeitsweise + +- Nie direkt auf `main` entwickeln; pro Phase ein Branch + (`feat/phase-1`), Merge erst nach manuellem Test. +- Nach jedem abgeschlossenen Teilschritt committen. +- `pnpm lint` und `pnpm typecheck` muessen sauber sein, bevor eine Phase + als fertig gilt. diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md new file mode 100644 index 0000000..f296602 --- /dev/null +++ b/docs/DEVLOG.md @@ -0,0 +1,32 @@ +# Devlog + +Zeiterfassung und Nachvollziehbarkeit der Entwicklung. Eine Zeile je +abgeschlossenem Arbeitsschritt, Zeiten in `HH:mm` (Europe/Berlin). + +## Phase 1 - MVP + +### 2026-09-06 + +| Von | Bis | Dauer | Schritt | Ergebnis | Commit | +|---|---|---|---|---|---| +| 22:06 | 22:12 | 0:06 | Planung | Phase-1-Plan und Drizzle-Schema zur Freigabe vorgelegt; 5 Entscheidungsfragen geklaert (Cent statt Float, Migrator statt drizzle-kit im Image, Bilder oeffentlich / PDFs geschuetzt, Token gehasht, Branch pro Phase) | - | +| 22:12 | 22:20 | 0:08 | Scaffold | pnpm via npm global installiert (corepack scheiterte an Rechten in `C:\Program Files\nodejs`), Next 15.5.25 + Tailwind 4 + shadcn/ui (radix-nova) mit 20 Komponenten | `0de49a9` | +| 22:20 | 22:26 | 0:06 | Datenmodell | 19 Tabellen in Drizzle, CHECK-Constraints fuer `instruction`, `photo` und `priority`, Unique-Index `(wishlist_id, set_id)`, Migration erzeugt und angewendet, WAL verifiziert | `06268bc` | +| 22:26 | 22:34 | 0:08 | Dokumentation | CLAUDE.md (Stack, Konventionen, Datenmodell, Sicherheitsregeln), Roadmap, Changelog, Devlog | `folgt` | + +**Summe Phase 1 bisher: 0:28** + +## Notizen und Entscheidungen + +- **pnpm**: `corepack enable` scheitert auf diesem Rechner an fehlenden + Schreibrechten in `C:\Program Files\nodejs`. Stattdessen + `npm install -g pnpm@10`; pnpm liegt unter + `%APPDATA%\npm\pnpm.ps1` und ist im PATH. +- **shadcn-CLI**: Die aktuelle Version (4.21) arbeitet mit Presets + (`-p nova`) und Base-Libraries (`-b radix`) statt der frueheren + Style-Auswahl. Sie zieht `cn` und `radix-ui` als Pakete; `clsx` und + `tailwind-merge` werden dadurch nicht mehr direkt gebraucht. +- **better-auth 1.7.3**: Das Passkey-Plugin liegt nicht mehr unter + `better-auth/plugins/passkey`, sondern im eigenen Paket + `@better-auth/passkey` (Exports `.` und `./client`). Das erwartete + Schema deckt sich exakt mit der Tabelle `passkey`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..3ca7b74 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,57 @@ +# Roadmap + +Legende: `[ ]` offen · `[~]` in Arbeit · `[x]` fertig + +## Phase 1 - MVP (Ziel: v0.1.0) + +Alles laeuft vollstaendig im Docker-Stack. + +- [x] Projekt-Scaffold (Next 15, Tailwind 4, shadcn/ui, pnpm) +- [x] Datenmodell in Drizzle, initiale Migration +- [x] Projektdokumentation (CLAUDE.md, Roadmap, Changelog, Devlog) +- [ ] Auth: Registrierung des ersten Users (wird Admin), Login mit + Passwort, Passkey anlegen und damit einloggen (Conditional UI), + Passkey-Verwaltung unter "Konto", Rate-Limit auf Login +- [ ] Mobile-first Layout-Shell mit Bottom-Navigation +- [ ] Stammdaten: Hersteller, Themen (mit Unterthemen), Standorte +- [ ] Set-Katalog: Kachelliste, Suche, Filter (Hersteller, Thema, Jahr, + Besitzstatus), Anlegen und Bearbeiten +- [ ] Upload-Pipeline (PDF/JPEG/PNG/WebP, Magic-Byte-Pruefung, + serverseitige Dateinamen, Thumbnails) +- [ ] Set-Detailseite: Metadaten, Bild, Anleitungen mit pdf.js-Viewer, + Fotos, Sammlungseintraege +- [ ] Sammlung: Uebersicht, Filter, Statistik-Kacheln +- [ ] Fehlende Sets (`/katalog?besitz=fehlt`) mit Mehrfachauswahl und + Bulk-Aktion "Zur Wunschliste hinzufuegen" +- [ ] Wunschlisten: Verwaltung, Prioritaet, Sortierung, Teilen, + Ueberraschungsmodus, automatisches "erfuellt" +- [ ] Oeffentliche Wunschliste `/w/[shareToken]` mit Reservierung ohne + Anmeldung und Storno-Link +- [ ] Seed-Skript (3 Hersteller, 10 Sets, 4 in Sammlung, 1 Wunschliste) +- [ ] Unit-Tests (Besitzstatus, Bulk-Add inkl. Dubletten, Reservierung, + Token-Erzeugung) und Playwright-Smoke-Test +- [ ] Docker: Multi-Stage-Build, Compose-Stack, Healthcheck, Entrypoint +- [ ] Dokumentation: README, QUICKSTART, bebilderte Anleitung, + update.sh, Nginx-Proxy-Manager-Konfiguration + +## Phase 2 - Komfort (Ziel: v0.2.0) + +- [ ] Rebrickable-Import: Setnummer eingeben, Metadaten und Bild + uebernehmen, Bild lokal cachen. Ohne API-Key bleibt alles manuell. +- [ ] Barcode-Scanner (EAN) per Handykamera, Suche nach EAN, bei + Unbekanntem direkt ins Formular "Neues Set" +- [ ] Fehlteile je Sammlungseintrag pflegen +- [ ] Einladungslinks durch den Admin +- [ ] Export/Import: gesamte Datenbank als JSON, Dateien als ZIP, + Sammlung zusaetzlich als CSV +- [ ] PWA: Manifest, Service Worker, Katalog offline lesbar + +## Phase 3 - Nice to have (Ziel: v0.3.0) + +- [ ] Mehrere Sammler mit eigener Sammlung und eigenen Wunschlisten, + gemeinsamer Katalog +- [ ] Preisbeobachtung und Wertentwicklung (manuell gepflegt, + optional BrickLink-Preisfuehrer) +- [ ] E-Mail-Erinnerung (SMTP), wenn ein Wunschlisten-Termin naeher + rueckt und Eintraege unreserviert sind +- [ ] Dublettenpruefung beim Anlegen von Sets