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>
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||

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

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

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

|
||||
|
||||
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 <bricktrack@example.de>
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 185 KiB |
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -208,6 +208,7 @@ export function CatalogGrid({
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
data-set-card
|
||||
disabled={owned}
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => toggle(item.id)}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Runs once when the server starts.
|
||||
*
|
||||
* Next calls this before handling the first request, which is the one place a
|
||||
* long-lived background task can be started from.
|
||||
*/
|
||||
export async function register() {
|
||||
// Only in the Node runtime - the edge runtime has no timers that survive,
|
||||
// and the dev server would start a second scheduler on every reload.
|
||||
if (process.env.NEXT_RUNTIME !== 'nodejs') return;
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
|
||||
const { startReminderScheduler } = await import('@/lib/reminder-job');
|
||||
startReminderScheduler();
|
||||
}
|
||||
@@ -107,6 +107,21 @@ export async function runReminderJob(now = new Date()): Promise<ReminderRun> {
|
||||
|
||||
const HOUR_MS = 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Started lazily from the first server render or healthcheck rather than from
|
||||
* Next's instrumentation hook: that hook is compiled for the edge runtime too,
|
||||
* and pulling the native SQLite driver into that bundle breaks the build.
|
||||
* Cached on globalThis so the dev server's module reloading cannot stack up
|
||||
* several schedulers.
|
||||
*/
|
||||
const globalForScheduler = globalThis as unknown as { bricktrackReminders?: boolean };
|
||||
|
||||
export function ensureReminderScheduler(): void {
|
||||
if (globalForScheduler.bricktrackReminders) return;
|
||||
globalForScheduler.bricktrackReminders = true;
|
||||
startReminderScheduler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the hourly check.
|
||||
*
|
||||
|
||||