Files
bricktrack/README.md
T
unknownandClaude Opus 5 e52557b4bf docs: anleitung, changelog 0.3.0 und roadmap zu phase 3
Vier neue Kapitel in der Anleitung (mehrere Sammler, Preise,
Erinnerungen, Dubletten) mit vier automatisch erzeugten Bildern,
Changelog 0.3.0 samt der bewusst weggelassenen BrickLink-Anbindung,
Roadmap abgehakt und um einen offenen Ausblick ergaenzt, Devlog mit
Zeiten und Stolpersteinen.

Dazu zwei Korrekturen aus dem Testlauf:

Der Zeitplan fuer Erinnerungen liess sich nicht aus Next'
instrumentation.ts starten - die Datei wird auch fuer die Edge-Runtime
uebersetzt, und das native SQLite-Modul bricht dort den Build. Er
startet jetzt aus dem Healthcheck heraus, den der Container ohnehin alle
30 Sekunden aufruft, und zusaetzlich aus dem App-Layout; der Aufruf ist
idempotent.

Der Screenshot-Lauf waehlte ploetzlich die falschen Kacheln aus: Der
neue Sammler-Umschalter benutzt ebenfalls aria-pressed. Die
Auswahlkacheln tragen jetzt ein eigenes data-set-card.

Ausserdem raeumen die Testskripte einen Container weg, den ein
abgebrochener Lauf zurueckgelassen hat - sonst blockiert er den Port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 10:11:40 +02:00

245 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bricktrack
Self-hosted Verwaltung für die Spielzeug-Set-Sammlung deines Kindes – Lego,
Playmobil, Cobi, BlueBrixx, Mega Bloks und jeden weiteren Hersteller, den du
selbst anlegst.
Die App beantwortet zwei Fragen sofort:
- **Welche Sets hat er?** → die Sammlung, mit Standort, Zustand und Statistik.
- **Welche Sets hat er noch nicht?** → alles im Katalog ohne Sammlungseintrag.
Aus den fehlenden Sets entsteht in wenigen Handgriffen eine Wunschliste, die
du per Link mit Familie und Freunden teilst. Die reservieren dort ihr Geschenk
– **ohne sich zu registrieren**, und ohne dass jemand doppelt kauft.
> **Neu hier?** → [QUICKSTART.md](QUICKSTART.md) führt dich in 15 Minuten von
> null zur laufenden Instanz, inklusive Nginx Proxy Manager.
> **Bedienung?** → [docs/ANLEITUNG.md](docs/ANLEITUNG.md), bebildert.
---
## Was drin ist
| | |
|---|---|
| **Katalog** | Sets aller Hersteller, Suche nach Nummer, Name oder EAN, Filter nach Hersteller, Thema, Jahr und Besitzstatus |
| **Sammlung** | Was tatsächlich da ist: Status, Vollständigkeit, Zustand, Standort, Kaufpreis, „geschenkt von" |
| **Anleitungen** | PDFs hochladen und im Browser blättern (pdf.js, ohne externe Dienste) oder auf die Hersteller-Seite verlinken |
| **Fotos** | Eigene Bilder je Set – direkt aus der Handykamera |
| **Wunschlisten** | Mehrfachauswahl aus den fehlenden Sets, Priorität, Reihenfolge, Termin |
| **Teilen** | Öffentlicher Link ohne Anmeldung, Reservierung mit Namen, Storno-Link, Überraschungsmodus |
| **Mehrere Kinder** | Gemeinsamer Katalog, getrennte Sammlungen und Wunschlisten – sichtbar erst ab dem zweiten Sammler |
| **Preise** | Notierte Preise je Set, Wertentwicklung gegen die UVP |
| **Erinnerungen** | E-Mail, wenn ein Termin näher rückt und noch Wünsche offen sind (SMTP optional) |
| **Anmeldung** | E-Mail und Passwort oder Passkey (WebAuthn), mehrere Geräte je Konto |
Mobil zuerst gebaut: Die App wird im Laden und im Kinderzimmer benutzt, nicht
am Schreibtisch.
## Technik
Next.js 15 (App Router, Server Actions, TypeScript strict) · Tailwind CSS 4 ·
shadcn/ui · SQLite über Drizzle ORM und better-sqlite3 · better-auth mit
Passkey-Plugin · alles in **einem** Docker-Container.
Keine Cloud, keine externen Dienste, keine Telemetrie. Der Browser lädt nichts
von fremden Servern außer den Bild-URLs, die du selbst hinterlegt hast.
Details zu Stack, Konventionen und Datenmodell: [CLAUDE.md](CLAUDE.md).
---
## Installation
Ausführlich in [QUICKSTART.md](QUICKSTART.md). Die Kurzfassung:
```bash
git clone https://git.wilde-server.de/florian.wilde/bricktrack.git
cd bricktrack
cp .env.example .env
# BASE_URL und BETTER_AUTH_SECRET setzen:
# openssl rand -base64 48
mkdir -p ./data
sudo chown -R 1000:1000 ./data # der Container laeuft als UID 1000
docker compose up -d --build
```
Danach `https://deine-domain` aufrufen. **Der erste registrierte Nutzer wird
Administrator**, danach ist die Registrierung geschlossen.
### Konfiguration
Alle Werte kommen aus `.env` (Vorlage: `.env.example`).
| Variable | Pflicht | Standard | Bedeutung |
|---|---|---|---|
| `BASE_URL` | ja | `http://localhost:3000` | Öffentliche Adresse. Bestimmt die Passkey-Domain (RP-ID). |
| `BETTER_AUTH_SECRET` | ja | – | Signiergeheimnis, mindestens 32 Zeichen. |
| `DATA_DIR` | nein | `/data` | Datenverzeichnis im Container. Nicht ändern. |
| `MAX_UPLOAD_MB` | nein | `25` | Größenlimit je Datei. Muss zum Proxy passen. |
| `TZ` | nein | `Europe/Berlin` | Zeitzone des Containers. |
| `BIND_ADDRESS` | nein | `127.0.0.1` | Auf welcher Host-Adresse der Container lauscht. `0.0.0.0`, wenn der Proxy auf einem anderen Host läuft. |
| `HOST_PORT` | nein | `3000` | Port auf dem Host, falls 3000 belegt ist. |
| `REBRICKABLE_API_KEY` | nein | – | Lego-Import. Ohne Key läuft alles manuell. |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM` | nein | – | Mailversand für Wunschlisten-Erinnerungen. Ohne `SMTP_HOST` und `SMTP_FROM` ist die Funktion aus. |
| `REMINDER_DAYS_BEFORE` | nein | `14` | Wie viele Tage vor dem Termin erinnert wird. |
### Reverse Proxy
TLS wird außerhalb terminiert, der Container spricht nur HTTP auf Port 3000.
Drei Dinge sind wichtig:
1. **HTTPS mit Force-SSL** – ohne das keine Passkeys.
2. **`client_max_body_size`** mindestens so groß wie `MAX_UPLOAD_MB`, sonst
scheitern große Anleitungs-PDFs mit „413".
3. **`X-Forwarded-For`** durchreichen, damit das Rate-Limit auf dem
Login-Endpunkt die echte Client-IP sieht.
Die vollständige Nginx-Proxy-Manager-Konfiguration steht in
[QUICKSTART.md, Schritt 5](QUICKSTART.md#5-reverse-proxy-einrichten-nginx-proxy-manager).
---
## Backup und Restore
**Ein Verzeichnis reicht.** `./data` enthält die Datenbank *und* alle
hochgeladenen Dateien:
```
data/
db/bricktrack.db SQLite (dazu -wal und -shm)
files/images/sets/… Set-Bilder
files/photos/… eigene Fotos
files/instructions/… Anleitungs-PDFs
```
**Sichern** (Container kurz anhalten, damit die Datenbank nicht mitten im
Schreiben eingepackt wird):
```bash
docker compose stop app
tar czf bricktrack-$(date +%F).tar.gz data .env
docker compose start app
```
**Zurückspielen:**
```bash
docker compose down
rm -rf data
tar xzf bricktrack-2026-09-06.tar.gz
sudo chown -R 1000:1000 ./data
docker compose up -d
```
Migrationen laufen bei jedem Start automatisch – ein altes Backup wird beim
Zurückspielen also auf den aktuellen Schemastand gehoben.
## Aktualisieren
```bash
./update.sh
```
Das Skript sichert `./data` und `.env`, holt den neuen Stand, baut das Image,
startet neu und wartet auf den Healthcheck. Wird der Container nicht gesund,
rollt es Code **und** Container automatisch auf den vorherigen Commit zurück
und sagt dir, wo die Sicherung liegt.
```bash
./update.sh --no-pull # nur neu bauen, ohne git pull
./update.sh --no-backup # ohne Sicherung (nicht empfohlen)
```
Die letzten zehn Sicherungen bleiben unter `./backups` liegen.
---
## Entwicklung
```bash
corepack enable pnpm # pnpm 10
pnpm install
cp .env.example .env # DATA_DIR=./data, BASE_URL=http://localhost:3000
pnpm db:migrate # Datenbank anlegen
pnpm db:seed # Beispieldaten: 3 Hersteller, 10 Sets, 1 Wunschliste
pnpm dev # http://localhost:3000
```
| Befehl | Wirkung |
|---|---|
| `pnpm dev` | Entwicklungsserver |
| `pnpm build` | Produktionsbuild (standalone) |
| `pnpm lint` / `pnpm typecheck` | ESLint / TypeScript – beide müssen sauber sein |
| `pnpm test` | Unit-Tests (Vitest) |
| `pnpm test:e2e` | Playwright-Smoke-Test gegen das Docker-Image (eigenes `./data-e2e`) |
| `pnpm db:generate` | SQL-Migration aus dem Drizzle-Schema erzeugen |
| `pnpm db:migrate` | Migrationen anwenden |
| `pnpm db:studio` | Drizzle Studio |
| `pnpm screenshots` | Bilder für `docs/ANLEITUNG.md` neu erzeugen |
`pnpm test:e2e` und `pnpm screenshots` bauen jeweils das Docker-Image und
fahren es hoch. Sie brauchen also einen laufenden Docker-Daemon, testen dafür
aber genau das Artefakt, das später auch deployt wird – der Dev-Server hat
unter der Last eines ganzen Durchlaufs reproduzierbar Chunks verloren.
> **Unter Windows** schlägt `pnpm build` mit `EPERM … symlink` fehl: der
> Standalone-Output legt Symlinks an, wofür Windows den Entwicklermodus oder
> administrative Rechte braucht. Der Docker-Build unter Linux ist davon nicht
> betroffen. Zum Prüfen lokal: `docker build -t bricktrack:test .`
Die Passkey-Anmeldung funktioniert lokal auch über `http://localhost` –
WebAuthn behandelt localhost als sicheren Kontext. Auf jeder anderen Adresse
braucht es HTTPS.
---
## Datenmodell in einem Absatz
Ein **`set`** ist ein Katalogeintrag: Das Set existiert in der Welt,
unabhängig davon, ob wir es besitzen. Ein **`collection_item`** verknüpft ein
Set mit einem tatsächlich vorhandenen Exemplar – dasselbe Set zweimal im Regal
sind zwei Einträge. Daraus folgt die zentrale Regel:
> „Hat er nicht" = Sets im Katalog, zu denen kein `collection_item` mit einem
> Status ungleich `sold` existiert.
Ein verkauftes Exemplar zählt also nicht mehr als Besitz – das Set taucht
wieder unter „fehlt noch" auf und darf zurück auf eine Wunschliste. Diese
Regel steht an genau einer Stelle im Code: `src/lib/ownership.ts`.
Gibt es mehrere **Sammler**, bekommt die Frage ein „für wen": Der Katalog ist
gemeinsam, der Besitz nicht. Ein Set, das nur das Geschwisterkind hat, fehlt
dem anderen weiterhin.
Das vollständige Modell inklusive aller Tabellen: [CLAUDE.md](CLAUDE.md#datenmodell).
## Sicherheit
- Jede Eingabe wird serverseitig mit zod validiert.
- Uploads sind auf PDF, JPEG, PNG und WebP begrenzt – geprüft an den **Magic
Bytes**, nicht am gemeldeten MIME-Typ. Dateinamen erzeugt immer der Server.
- Die öffentliche Wunschliste nutzt ausschließlich den Share-Token als
Schlüssel und gibt nichts außer den Wunschlisten-Daten preis. Sie ist
`noindex`.
- Storno- und Einladungs-Token liegen nur als SHA-256-Hash in der Datenbank.
- Session-Cookies sind `httpOnly`, `sameSite=lax` und im Betrieb `secure`.
Auf den Login-Endpunkten liegt ein Rate-Limit (5 Versuche pro Minute).
- Set-Bilder sind ohne Anmeldung abrufbar – die geteilte Wunschliste braucht
sie. Anleitungs-PDFs und eigene Fotos **nie**.
## Stand und Pläne
Aktuelle Version und Änderungen: [CHANGELOG.md](CHANGELOG.md).
Was als Nächstes kommt: [docs/ROADMAP.md](docs/ROADMAP.md).
Wie lange was gedauert hat: [docs/DEVLOG.md](docs/DEVLOG.md).
## Lizenz
Privates Projekt, keine Lizenz vergeben.