docs: claude.md, roadmap, changelog und devlog anlegen
CLAUDE.md dokumentiert Stack, Konventionen, das vollstaendige Datenmodell und die Sicherheitsregeln. Roadmap haelt den Stand der drei Phasen fest, das Devlog die Zeiten je Arbeitsschritt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -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/<setId>/... Set-Bilder (oeffentlich lesbar)
|
||||||
|
photos/... eigene Fotos (nur mit Session)
|
||||||
|
instructions/<setId>/... 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.
|
||||||
@@ -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`.
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user