Registrierung des ersten Nutzers macht ihn zum Administrator, danach ist die Registrierung geschlossen (Einladungen folgen in Phase 2). Login per Passwort oder Passkey inklusive Conditional UI, Verwaltung der Passkeys unter "Konto" mit Umbenennen und Loeschen. Rate-Limiting liegt in der Datenbank und ueberlebt Neustarts; Session-Cookies sind httpOnly, sameSite=lax und im Produktivbetrieb secure. Die Client-IP wird hinter dem Reverse Proxy aus x-forwarded-for gelesen. Dazu die mobile-first Grundstruktur: Bottom-Navigation, Header mit Konto-Menue, Healthcheck unter /api/health und ein Dashboard mit den vier Einstiegspunkten. Bedienelemente sind auf 44px Touch-Groesse angehoben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
167 lines
7.1 KiB
Markdown
167 lines
7.1 KiB
Markdown
# 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`.
|
|
- Alle Datenbankspalten in `snake_case`. Auch in den Auth-Tabellen -
|
|
der Drizzle-Adapter von better-auth loest Modelle ueber die
|
|
TypeScript-Property-Namen auf, nicht ueber die Spaltennamen.
|
|
- 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.
|