diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cbb832..0b8a7ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,53 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unreleased] +## [0.3.0] - 2026-09-07 + +Phase 3: mehrere Kinder, Wertentwicklung, Erinnerungen und ein Schutz vor +doppelten Katalogeintraegen. + +### Hinzugefuegt + +**Mehrere Sammler** +- Der Katalog bleibt gemeinsam, Sammlung und Wunschlisten gehoeren je einem + Sammler. "Hat er das schon?" beantwortet damit die Frage fuer *ihn*: ein Set, + das nur die Schwester besitzt, faellt fuer den Bruder unter "fehlt noch". +- Umschalter in Katalog und Sammlung, Farbmarkierung je Sammler, Zuordnung im + Sammlungseintrag und je Wunschliste. +- Solange es nur einen Sammler gibt, ist davon nichts zu sehen - die App + bleibt genauso einfach wie vorher. + +**Preisbeobachtung** +- Notierte Preise je Set mit Betrag, Zustand, Datum, Quelle und Notiz. +- Wertentwicklung gegen die UVP, in der Sammlung als Kachel "Aktueller Wert". + +**Erinnerungen** +- E-Mail an den Besitzer, wenn der Termin einer Wunschliste naeher rueckt und + noch Wuensche offen sind - standardmaessig 14 Tage vorher, hoechstens einmal + pro Woche je Liste. +- Verwaltungsseite mit Verbindungstest, Testnachricht und sofortiger Pruefung. +- Ohne SMTP-Konfiguration bleibt die Funktion vollstaendig aus. + +**Dublettenpruefung** +- Vor dem Anlegen eines Sets warnt die App vor gleicher Setnummer beim + gleichen Hersteller, sehr aehnlichem Namen und gleicher Nummer bei einem + anderen Hersteller - mit Link zum Verdaechtigen und der Moeglichkeit, + trotzdem anzulegen. + +### Geaendert + +- Die Migration legt einen Sammler "Sammlung" an und ordnet ihm alle + vorhandenen Sammlungseintraege und Wunschlisten zu. Umbenennen unter + Verwaltung -> Sammler. +- Der Smoke-Test laeuft gegen das Docker-Image statt gegen den Dev-Server. + +### Nicht umgesetzt + +- Der BrickLink-Preisfuehrer war im Plan als optional vermerkt und wurde + bewusst weggelassen: OAuth1 mit einer pro Nutzer registrierten Anwendung ist + viel Apparat fuer eine Zahl, die sich in zehn Sekunden eintippen laesst. Das + Feld "Wo" nimmt jede Quelle auf, die Datenstruktur bleibt offen. + ## [0.2.0] - 2026-09-07 Phase 2: die Handgriffe, die den Alltag schneller machen. @@ -147,6 +194,7 @@ Erste lauffaehige Fassung: Phase 1 vollstaendig, im Docker-Stack verifiziert. - Unter Windows scheitert `pnpm build` an fehlenden Symlink-Rechten. Der Docker-Build unter Linux ist davon nicht betroffen. -[Unreleased]: https://git.wilde-server.de/florian.wilde/bricktrack/compare/v0.2.0...HEAD +[Unreleased]: https://git.wilde-server.de/florian.wilde/bricktrack/compare/v0.3.0...HEAD +[0.3.0]: https://git.wilde-server.de/florian.wilde/bricktrack/releases/tag/v0.3.0 [0.2.0]: https://git.wilde-server.de/florian.wilde/bricktrack/releases/tag/v0.2.0 [0.1.0]: https://git.wilde-server.de/florian.wilde/bricktrack/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index 142e680..9071836 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,10 @@ Bild-URLs, die der Nutzer selbst hinterlegt hat. | `src/lib/rebrickable.ts` | Lego-Import, ausschliesslich serverseitig | | `src/lib/set-number.ts`, `src/lib/ean.ts` | Setnummern und Barcodes (rein, testbar) | | `src/lib/uploads.ts` | Datei-Uploads, Magic Bytes, Thumbnails | +| `src/lib/similarity.ts` | Namensvergleich fuer die Dublettenpruefung (rein) | +| `src/lib/prices.ts` | Wertentwicklung aus notierten Preisen (rein) | +| `src/lib/reminders.ts` | Regeln, wann eine Erinnerung faellig ist (rein) | +| `src/lib/reminder-job.ts`, `src/lib/mailer.ts` | Versand und Zeitplan | | `src/lib/export.ts` | Export und Import des gesamten Bestands | | `src/lib/money.ts`, `src/lib/format.ts` | Cent-Umrechnung, dd.MM.yyyy | @@ -114,8 +118,16 @@ Diese Bedingung steht an genau einer Stelle: `src/lib/ownership.ts`. ### Besitz +Mehrere **Sammler** teilen sich den Katalog, nicht den Besitz: `collector` +haelt Name und Farbe, `collection_item.collector_id` und +`wishlist.collector_id` ordnen zu. Solange nur ein Sammler existiert, blendet +die Oberflaeche das Thema vollstaendig aus - Umschalter, Abfragen und Badges +erscheinen erst ab dem zweiten. + | Tabelle | Zweck | Wichtige Felder | |---|---|---| +| `collector` | ein Kind | `name`, `color`, `birthday`, `sort_order` | +| `price_point` | beobachteter Preis je Set | `price_cents`, `condition`, `observed_at`, `source` | | `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` | @@ -190,7 +202,7 @@ Session. - **Phase 2 (Komfort)** - Rebrickable-Import, Barcode-Scanner, Fehlteile, Einladungen, Export/Import, PWA. *Fertig in 0.2.0.* - **Phase 3 (Nice to have)** - mehrere Sammler, Preisbeobachtung, - E-Mail-Erinnerungen, Dublettenpruefung. + E-Mail-Erinnerungen, Dublettenpruefung. *Fertig in 0.3.0.* Details und Stand: `docs/ROADMAP.md`. Zeitaufwaende: `docs/DEVLOG.md`. diff --git a/README.md b/README.md index 91bc6a7..fbc6cff 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ du per Link mit Familie und Freunden teilst. Die reservieren dort ihr Geschenk | **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 @@ -81,7 +84,9 @@ Alle Werte kommen aus `.env` (Vorlage: `.env.example`). | `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 | – | Ab Version 0.2.0: Lego-Import. Ohne Key läuft alles manuell. | +| `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 @@ -208,6 +213,10 @@ 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 diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md index 71a73c8..b5304a8 100644 --- a/docs/ANLEITUNG.md +++ b/docs/ANLEITUNG.md @@ -27,7 +27,11 @@ Kinderzimmer, nicht für den Schreibtisch. 13. [Noch ein Konto für die Familie](#13-einladungen) 14. [Daten sichern, umziehen, auswerten](#14-daten) 15. [Auf den Startbildschirm legen](#15-startbildschirm) -16. [Antworten auf die zwei wichtigen Fragen](#16-die-zwei-fragen) +16. [Zwei Kinder, ein Katalog](#16-mehrere-sammler) +17. [Was ein Set heute wert ist](#17-preise) +18. [Erinnerung vor dem Termin](#18-erinnerungen) +19. [Schutz vor doppelten Einträgen](#19-dubletten) +20. [Antworten auf die zwei wichtigen Fragen](#20-die-zwei-fragen) --- @@ -419,7 +423,115 @@ den Zwischenspeicher, damit auf einem geteilten Gerät nichts lesbar bleibt. --- -## 16. Die zwei Fragen +## 16. Mehrere Sammler + +![Sammler verwalten](bilder/21-sammler.png) + +Kommt ein zweites Kind dazu, wird aus „hat er das schon?" die Frage „hat +**er** das schon?". Genau dafür gibt es Sammler. + +Der **Katalog bleibt gemeinsam**: Ein Set existiert einmal, unabhängig davon, +wer es besitzt. **Sammlung und Wunschlisten** gehören dagegen je einem +Sammler. Ein Set, das nur die Schwester hat, fällt für den Bruder wieder unter +„fehlt noch" – und darf auf seine Wunschliste. + +> **Solange es nur einen Sammler gibt, siehst du davon nichts.** Kein +> Umschalter, keine Abfrage, keine Farben. Die App bleibt genauso einfach wie +> vorher. Erst der zweite Sammler schaltet all das frei – und dann willst du +> es auch. + +Anlegen unter **Verwaltung → Sammler**. Jeder bekommt einen Namen und eine +Farbe zur Unterscheidung. + +![Nach Sammler filtern](bilder/22-sammler-filter.png) + +Danach steht in Katalog und Sammlung oben ein Umschalter. „Alle zusammen" +zeigt den Haushalt als Ganzes, ein Name zeigt genau dessen Sicht – inklusive +der Frage, was **diesem** Kind noch fehlt. + +Beim Hinzufügen zur Sammlung fragt die App jetzt, wer das Set bekommt. Und +beim Anlegen einer Wunschliste, für wen sie ist: Das bestimmt, wessen Sammlung +als „hat er schon" zählt und wann ein Wunsch automatisch als erfüllt gilt. + +> Die Umstellung einer bestehenden Instanz macht nichts kaputt: Beim Update +> entsteht ein Sammler namens „Sammlung", dem alles Vorhandene gehört. +> Umbenennen unter **Verwaltung → Sammler**, fertig. + +--- + +## 17. Preise + +![Preis und Wertentwicklung](bilder/23-preise.png) + +Auf jeder Set-Seite gibt es **„Preis notieren"**: Betrag, Zustand (neu oder +gebraucht), wann und wo gesehen. Daraus wird eine Reihe – und damit eine +Antwort auf die eigentlich interessante Frage: Was hat das Set seit dem Kauf +gemacht? + +- **Ursprünglich** ist die UVP des Sets, sonst der älteste notierte Preis. +- **Aktuell** ist der neueste notierte Preis. +- **Entwicklung** ist die Differenz in Prozent. + +In der Sammlung wird die Kachel „Wert nach UVP" automatisch zu **„Aktueller +Wert"**, sobald irgendwo ein Preis notiert ist. + +> Es wird **kein** Preisdienst abgefragt. Was hier steht, hast du selbst +> eingetragen – das ist ehrlicher als eine Zahl, deren Herkunft niemand kennt, +> und es geht nichts nach draußen. + +--- + +## 18. Erinnerungen + +![Erinnerungen](bilder/24-erinnerungen.png) + +Wenn ein Wunschlisten-Termin näher rückt und **noch Wünsche offen sind**, +schickt Bricktrack dir eine E-Mail – standardmäßig 14 Tage vorher, höchstens +einmal pro Woche je Liste. Ist alles reserviert, bleibt es still: eine +Erinnerung ohne Handlungsbedarf ist nur Lärm. + +Dafür braucht es einen SMTP-Zugang in der `.env`: + +```bash +SMTP_HOST=mail.example.de +SMTP_PORT=587 +SMTP_USER=bricktrack@example.de +SMTP_PASSWORD=… +SMTP_FROM=Bricktrack + +# Optional, Standard ist 14: +REMINDER_DAYS_BEFORE=14 +``` + +Unter **Verwaltung → Erinnerungen** kannst du die Verbindung prüfen, dir eine +Testnachricht schicken und die Prüfung sofort auslösen, statt auf die nächste +Stunde zu warten. Dort steht auch, welche Listen einen Termin haben und wie +viel dort noch offen ist. + +Ohne SMTP-Zugang ist die Funktion vollständig aus – alles andere funktioniert +unverändert. + +--- + +## 19. Dubletten + +Beim Anlegen eines neuen Sets schaut Bricktrack nach, ob es das schon gibt: + +- **gleiche Setnummer beim gleichen Hersteller** – dann ist es dasselbe Set, +- **sehr ähnlicher Name** – meist ein Tippfehler oder eine andere Schreibweise, +- **gleiche Nummer bei einem anderen Hersteller** – meist der falsche + Hersteller im Formular. + +Passt etwas, erscheint ein Hinweis mit Link zu den Verdächtigen. Von dort +kommst du direkt zum vorhandenen Eintrag – oder legst mit „Trotzdem anlegen" +weiter an, wenn es wirklich ein anderes Set ist. + +Die Erkennung ist bewusst zurückhaltend eingestellt: Tippfehler schlagen an, +zwei echte Sets desselben Themas nicht. + +--- + +## 20. Die zwei Fragen Dafür ist die ganze App gebaut: @@ -428,7 +540,8 @@ Dafür ist die ganze App gebaut: **„Welche Sets hat er noch nicht?"** → Katalog, Filter „Fehlt noch". Von dort in zwei Handgriffen auf eine -Wunschliste und per Link zu Oma. +Wunschliste und per Link zu Oma. Bei mehreren Kindern beantwortet der +Umschalter oben die Frage für jedes einzeln. Im Laden vor dem Regal: Scanner öffnen, Strichcode einlesen. Steht ein grüner Haken dran, habt ihr es schon. diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 60d4e5c..e9924d5 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -45,6 +45,20 @@ Commit-Spalte verweist auf den Stand am Ende des Schritts. **Summe Phase 2: 0:55** +## Phase 3 – Nice to have (v0.3.0) + +### 07.09.2026 + +| Von | Bis | Dauer | Schritt | Ergebnis | Commit | +|---|---|---|---|---|---| +| 08:35 | 09:00 | 0:25 | Mehrere Sammler | Neue Tabelle, Migration mit Nachziehen der Bestandsdaten, Besitzlogik um eine Dimension erweitert, Umschalter in Katalog und Sammlung, Zuordnung in Sammlungseintrag und Wunschliste | `c23b754` | +| 09:00 | 09:13 | 0:13 | Dublettenprüfung | Ähnlichkeitsvergleich mit konservativer Schwelle, Warndialog mit Links, E2E-Test | `87131b8` | +| 09:13 | 09:17 | 0:04 | Preisbeobachtung | Preisreihe je Set, Wertentwicklung gegen die UVP, Kachel „Aktueller Wert" | `91fb66b` | +| 09:17 | 09:22 | 0:05 | Erinnerungen | SMTP optional, stündliche Prüfung, Sperrfrist je Liste, Verwaltungsseite mit Testfunktionen | `0d08df0` | +| 09:22 | 09:50 | 0:28 | Verifikation und Dokumentation | Docker-Build, Smoke-Test, Screenshots, Changelog 0.3.0, Anleitung | folgt | + +**Summe Phase 3: 1:15** + --- ## Notizen und Entscheidungen @@ -120,6 +134,21 @@ Commit-Spalte verweist auf den Stand am Ende des Schritts. Konfliktregeln für jede Tabelle und wäre im Ergebnis nicht vorhersagbar. Der Import ersetzt deshalb – dafür mit getipptem `ERSETZEN` als Bremse. +### Phase 3 + +- **Migration mit Bestandsdaten**: Die generierte SQL-Datei wurde von Hand um + drei Anweisungen ergänzt, die einen Sammler anlegen und alles Vorhandene + zuordnen. Ohne das wären nach dem Update alle Sammlungseinträge ohne + Besitzer gewesen. +- **Instrumentation und native Module**: Der Zeitplan für Erinnerungen liess + sich nicht aus Next' `instrumentation.ts` starten – die Datei wird auch für + die Edge-Runtime übersetzt, und das native SQLite-Modul bricht dort den + Build. Der Zeitplan startet jetzt aus dem Healthcheck heraus, den der + Container ohnehin alle 30 Sekunden aufruft. +- **BrickLink bewusst weggelassen**: OAuth1 mit einer pro Nutzer registrierten + Anwendung ist viel Apparat für eine Zahl, die man in zehn Sekunden eintippt. + Im Plan war der Punkt als optional vermerkt. + ### Beim Testen gefunden und behoben - Eine spontan aus dem Katalog angelegte Wunschliste war **nicht teilbar**: @@ -130,3 +159,8 @@ Commit-Spalte verweist auf den Stand am Ende des Schritts. `required` des Eingabefeldes. - In der Mehrfachauswahl verdeckte die Aktionsleiste die letzte Kachelreihe; das Raster bekommt im Auswahlmodus zusätzliches Polster nach unten. +- Die Sperrfrist der Erinnerungen rechnete mit umgedrehtem Vorzeichen und + hätte jede Erinnerung dauerhaft unterdrückt. Der Test dafür war zuerst da. +- Der Screenshot-Lauf wählte plötzlich die falschen Kacheln aus: Der neue + Sammler-Umschalter benutzt ebenfalls `aria-pressed`. Die Auswahlkacheln + tragen jetzt ein eigenes `data-set-card`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index fe01b00..fad9430 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -46,12 +46,25 @@ Laeuft vollstaendig im Docker-Stack. Sammlung zusaetzlich als CSV - [x] PWA: Manifest, Service Worker, Katalog offline lesbar -## Phase 3 - Nice to have (Ziel: v0.3.0) +## Phase 3 - Nice to have (v0.3.0, abgeschlossen am 07.09.2026) -- [ ] Mehrere Sammler mit eigener Sammlung und eigenen Wunschlisten, +- [x] 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 +- [x] Preisbeobachtung und Wertentwicklung (manuell gepflegt). + Der BrickLink-Preisfuehrer war als optional vermerkt und wurde + bewusst weggelassen: er verlangt OAuth1 mit einer pro Nutzer + registrierten Anwendung - viel Apparat fuer eine Zahl, die sich in + zehn Sekunden eintippen laesst. Die Datenstruktur ist offen dafuer. +- [x] E-Mail-Erinnerung (SMTP), wenn ein Wunschlisten-Termin naeher rueckt und Eintraege unreserviert sind -- [ ] Dublettenpruefung beim Anlegen von Sets +- [x] Dublettenpruefung beim Anlegen von Sets + +## Danach + +Phase 1 bis 3 sind umgesetzt. Was noch offen ist, steht hier - ohne Termin +und ohne Anspruch auf Vollstaendigkeit: + +- [ ] BrickLink-Preisfuehrer anbinden, falls der Aufwand sich doch lohnt +- [ ] Mehrere Fotos je Sammlungseintrag (bisher nur je Set) +- [ ] Anleitungen im PDF-Viewer durchsuchen +- [ ] Statistik ueber die Zeit: was kam wann dazu diff --git a/docs/bilder/03-verwaltung-hersteller.png b/docs/bilder/03-verwaltung-hersteller.png index e4db9b5..6ada860 100644 Binary files a/docs/bilder/03-verwaltung-hersteller.png and b/docs/bilder/03-verwaltung-hersteller.png differ diff --git a/docs/bilder/04-katalog.png b/docs/bilder/04-katalog.png index 77f9643..980c770 100644 Binary files a/docs/bilder/04-katalog.png and b/docs/bilder/04-katalog.png differ diff --git a/docs/bilder/05-fehlende-sets.png b/docs/bilder/05-fehlende-sets.png index 2a1bd7f..155fa39 100644 Binary files a/docs/bilder/05-fehlende-sets.png and b/docs/bilder/05-fehlende-sets.png differ diff --git a/docs/bilder/06-mehrfachauswahl.png b/docs/bilder/06-mehrfachauswahl.png index eb08bc7..c3ef473 100644 Binary files a/docs/bilder/06-mehrfachauswahl.png and b/docs/bilder/06-mehrfachauswahl.png differ diff --git a/docs/bilder/07-wunschliste-dialog.png b/docs/bilder/07-wunschliste-dialog.png index 3527da4..974026b 100644 Binary files a/docs/bilder/07-wunschliste-dialog.png and b/docs/bilder/07-wunschliste-dialog.png differ diff --git a/docs/bilder/08-set-detail.png b/docs/bilder/08-set-detail.png index dade78c..ad0f8c3 100644 Binary files a/docs/bilder/08-set-detail.png and b/docs/bilder/08-set-detail.png differ diff --git a/docs/bilder/09-sammlung.png b/docs/bilder/09-sammlung.png index 57ff976..87a8485 100644 Binary files a/docs/bilder/09-sammlung.png and b/docs/bilder/09-sammlung.png differ diff --git a/docs/bilder/10-wunschlisten.png b/docs/bilder/10-wunschlisten.png index b0b69fe..cb6e1c1 100644 Binary files a/docs/bilder/10-wunschlisten.png and b/docs/bilder/10-wunschlisten.png differ diff --git a/docs/bilder/11-wunschliste-bearbeiten.png b/docs/bilder/11-wunschliste-bearbeiten.png index becf7c2..779a267 100644 Binary files a/docs/bilder/11-wunschliste-bearbeiten.png and b/docs/bilder/11-wunschliste-bearbeiten.png differ diff --git a/docs/bilder/13-reservieren.png b/docs/bilder/13-reservieren.png index 5b08544..080d9b0 100644 Binary files a/docs/bilder/13-reservieren.png and b/docs/bilder/13-reservieren.png differ diff --git a/docs/bilder/14-reserviert.png b/docs/bilder/14-reserviert.png index e9ec42c..8141b64 100644 Binary files a/docs/bilder/14-reserviert.png and b/docs/bilder/14-reserviert.png differ diff --git a/docs/bilder/15-wunschliste-reserviert.png b/docs/bilder/15-wunschliste-reserviert.png index f406a05..e19f81e 100644 Binary files a/docs/bilder/15-wunschliste-reserviert.png and b/docs/bilder/15-wunschliste-reserviert.png differ diff --git a/docs/bilder/18-fehlteile.png b/docs/bilder/18-fehlteile.png index a857431..8a0597e 100644 Binary files a/docs/bilder/18-fehlteile.png and b/docs/bilder/18-fehlteile.png differ diff --git a/docs/bilder/19-einladungen.png b/docs/bilder/19-einladungen.png index fb4e304..5413fca 100644 Binary files a/docs/bilder/19-einladungen.png and b/docs/bilder/19-einladungen.png differ diff --git a/docs/bilder/20-daten.png b/docs/bilder/20-daten.png index 6d51743..494b872 100644 Binary files a/docs/bilder/20-daten.png and b/docs/bilder/20-daten.png differ diff --git a/docs/bilder/21-sammler.png b/docs/bilder/21-sammler.png new file mode 100644 index 0000000..55db779 Binary files /dev/null and b/docs/bilder/21-sammler.png differ diff --git a/docs/bilder/22-sammler-filter.png b/docs/bilder/22-sammler-filter.png new file mode 100644 index 0000000..3d906b6 Binary files /dev/null and b/docs/bilder/22-sammler-filter.png differ diff --git a/docs/bilder/23-preise.png b/docs/bilder/23-preise.png new file mode 100644 index 0000000..b221209 Binary files /dev/null and b/docs/bilder/23-preise.png differ diff --git a/docs/bilder/24-erinnerungen.png b/docs/bilder/24-erinnerungen.png new file mode 100644 index 0000000..5145391 Binary files /dev/null and b/docs/bilder/24-erinnerungen.png differ diff --git a/e2e/screenshots.spec.ts b/e2e/screenshots.spec.ts index 695c6af..c0b784e 100644 --- a/e2e/screenshots.spec.ts +++ b/e2e/screenshots.spec.ts @@ -66,7 +66,8 @@ test('Screenshots fuer die Anleitung', async ({ page, browser }) => { await page.getByRole('button', { name: 'Auswählen' }).click(); await expect(page.getByText('Tippe die Sets an')).toBeVisible(); - const cards = page.locator('button[aria-pressed]'); + // data-set-card, not aria-pressed: the collector switcher uses that too. + const cards = page.locator('button[data-set-card]'); await cards.nth(0).click(); await cards.nth(1).click(); await expect(page.getByText('2 ausgewählt')).toBeVisible(); @@ -160,4 +161,33 @@ test('Screenshots fuer die Anleitung', async ({ page, browser }) => { await visit(page, '/verwaltung/daten'); await expect(page.getByText('Alles als ZIP')).toBeVisible(); await shoot(page, '20-daten'); + + // --- 16. collectors ------------------------------------------------------- + await visit(page, '/verwaltung/sammler'); + await expect(page.getByText('Der Katalog ist gemeinsam')).toBeVisible(); + await shoot(page, '21-sammler'); + + // The switcher only exists because the seed created two collectors. + await visit(page, '/katalog?besitz=fehlt'); + await expect(page.getByRole('button', { name: /Max/ })).toBeVisible(); + await page.getByRole('button', { name: /Max/ }).click(); + await expect(page.getByText('Sets, die Max noch nicht hat')).toBeVisible(); + await shoot(page, '22-sammler-filter'); + + // --- 17. prices ----------------------------------------------------------- + await visit(page, '/katalog?q=60398'); + await page.getByText('Familienhaus und Elektroauto').first().click(); + await expect(page.getByText('Preis und Wertentwicklung')).toBeVisible(); + await page.getByRole('button', { name: 'Preis notieren' }).click(); + // Role, not label: the label text carries the required marker. + await page.getByRole('textbox', { name: 'Preis', exact: true }).fill('79,99'); + await page.getByLabel('Wo').fill('Amazon'); + await page.getByRole('button', { name: 'Speichern' }).click(); + await expect(page.getByText('Amazon')).toBeVisible(); + await shoot(page, '23-preise'); + + // --- 18. reminders -------------------------------------------------------- + await visit(page, '/verwaltung/erinnerungen'); + await expect(page.getByText('Erinnerungen').first()).toBeVisible(); + await shoot(page, '24-erinnerungen'); }); diff --git a/scripts/e2e-prepare.mjs b/scripts/e2e-prepare.mjs index a0aff46..db802e0 100644 --- a/scripts/e2e-prepare.mjs +++ b/scripts/e2e-prepare.mjs @@ -17,6 +17,13 @@ fs.mkdirSync(path.join(dataDir, 'db'), { recursive: true }); fs.mkdirSync(path.join(dataDir, 'files'), { recursive: true }); console.log(`[e2e] ${dataDir} zurueckgesetzt.`); +// A container left behind by an aborted run would hold the port. +try { + execFileSync('docker', ['rm', '-f', 'bricktrack-e2e'], { stdio: 'ignore' }); +} catch { + // Nothing to remove - the normal case. +} + console.log('[e2e] Image wird gebaut (bricktrack:e2e) …'); execFileSync('docker', ['build', '-t', 'bricktrack:e2e', '.'], { stdio: 'inherit' }); console.log('[e2e] Image ist bereit.'); diff --git a/scripts/screenshots-prepare.mjs b/scripts/screenshots-prepare.mjs index 69ef5ef..05224ca 100644 --- a/scripts/screenshots-prepare.mjs +++ b/scripts/screenshots-prepare.mjs @@ -16,6 +16,13 @@ fs.mkdirSync(path.join(dataDir, 'db'), { recursive: true }); fs.mkdirSync(path.join(dataDir, 'files'), { recursive: true }); console.log(`[screenshots] ${dataDir} zurueckgesetzt.`); +// A container left behind by an aborted run would hold the port. +try { + execFileSync('docker', ['rm', '-f', 'bricktrack-shots'], { stdio: 'ignore' }); +} catch { + // Nothing to remove - the normal case. +} + console.log('[screenshots] Image wird gebaut (bricktrack:shots) …'); execFileSync('docker', ['build', '-t', 'bricktrack:shots', '.'], { stdio: 'inherit' }); console.log('[screenshots] Image ist bereit.'); diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 3fe9e1b..69963e7 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,8 +1,13 @@ import { AppHeader } from '@/components/layout/app-header'; import { BottomNav } from '@/components/layout/bottom-nav'; +import { ensureReminderScheduler } from '@/lib/reminder-job'; import { requireUser } from '@/lib/session'; export default async function AppLayout({ children }: { children: React.ReactNode }) { + // Second entry point besides the healthcheck, in case someone turns that + // off. The call is idempotent. + ensureReminderScheduler(); + const user = await requireUser(); return ( diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index c774a02..84d744e 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,6 +1,7 @@ import { sql } from 'drizzle-orm'; import { NextResponse } from 'next/server'; import { db } from '@/db'; +import { ensureReminderScheduler } from '@/lib/reminder-job'; export const dynamic = 'force-dynamic'; @@ -9,6 +10,10 @@ export const dynamic = 'force-dynamic'; * file is readable - a healthy Node process on a broken volume is not healthy. */ export function GET() { + // The container polls this every 30s, which makes it the earliest reliable + // moment to have the reminder scheduler running. Repeat calls are free. + ensureReminderScheduler(); + try { db.get(sql`select 1`); return NextResponse.json({ status: 'ok', time: new Date().toISOString() }); diff --git a/src/components/katalog/catalog-grid.tsx b/src/components/katalog/catalog-grid.tsx index 9bbfbb6..c24b85c 100644 --- a/src/components/katalog/catalog-grid.tsx +++ b/src/components/katalog/catalog-grid.tsx @@ -208,6 +208,7 @@ export function CatalogGrid({