Files
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

9.4 KiB
Raw Permalink Blame History

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 führt dich in 15 Minuten von null zur laufenden Instanz, inklusive Nginx Proxy Manager. Bedienung? → 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.


Installation

Ausführlich in QUICKSTART.md. Die Kurzfassung:

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.


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):

docker compose stop app
tar czf bricktrack-$(date +%F).tar.gz data .env
docker compose start app

Zurückspielen:

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

./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.

./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

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.

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. Was als Nächstes kommt: docs/ROADMAP.md. Wie lange was gedauert hat: docs/DEVLOG.md.

Lizenz

Privates Projekt, keine Lizenz vergeben.