Commit Graph

65 Commits

Author SHA1 Message Date
Scarriffle
72256f4fe9 Backend: globale Einzelstueck-Liste (GET /items) + Bild-Version an Produkten
Neuer Endpoint GET /items liefert alle Einzelstuecke ueber alle Produkte, angereichert mit Produkt-, Kategorie-, Lagerort- und Shop-Angaben (Beziehungen vorgeladen). ItemOut bekommt category_id/category_name. ProductOut bekommt image_version (Epoch der letzten Bildaenderung, None ohne Bild) - identisch zum ETag der Bild-Route, damit Clients Bilder cachen und nur bei Aenderung neu laden. Grundlage fuer die neue Einzelstueck-Liste (Web+iOS) und den iOS-Bild-Cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:37:17 +02:00
Scarriffle
c8c1686395 Amazon-Kaufdatum erkennen; Preis-Vorschlaege beim Anlegen; Lagerort-Dropdowns mit Pfad
1) Beleganalyse erkennt Datumsangaben mit Monatsnamen (DE+EN, z.B. Amazon 'Bestelldatum 29 April 2026'), nicht nur 29.04.2026 - so wird das Kaufdatum solcher Rechnungen gefunden. 2) Beim direkten Anlegen eines Einzelstuecks mit Beleg werden jetzt alle erkannten Preise als Vorschlag angeboten (Dropdown), statt stillschweigend nur den wahrscheinlichsten zu nehmen. 3) Alle Lagerort-Dropdowns zeigen den vollen Pfad (Hedingen -> Keller -> Schublade 1) statt nur den Namen, damit gleichnamige Orte unterscheidbar sind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:24:08 +02:00
Scarriffle
21ab0a825a Nachschlagen: Lagerort-QR zeigt alle Artikel an diesem Ort
Neuer Endpunkt GET /location/{code} liefert alle Artikel an einem Lagerort inkl. der Unterorte (Lot-Bestaende und Einzelstuecke), sortiert nach Name mit Menge in Artikeleinheiten. In der App loest der /l/-QR beim Nachschlagen jetzt diese Liste auf; ein Tipp auf einen Eintrag oeffnet den Artikel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:10:29 +02:00
Scarriffle
d518b4aa23 Lagerorte per 10-Zeichen-Code statt fortlaufender ID; iOS-Einlagern ohne Kamerazwang
Lagerort-IDs sind jetzt ein zufaelliger 10-Zeichen-Code (wie die Einzelstueck-UIDs) statt einer fortlaufenden Zahl - so kollidiert die Stammdaten-Sicherung zwischen zwei Instanzen praktisch nie mehr, und der Code ist zugleich der Inhalt des QR /l/<code>. Alle Fremdschluessel (lots, movements, items, Mindestbestaende, parent_id) ziehen mit; die Umstellung laeuft einmalig und transaktional beim Serverstart (_migrate_locations_to_code) und rollt bei Fehlern komplett zurueck. Vor dem Deploy ein DB-Backup machen.

iOS-Einlagern oeffnet nicht mehr sofort die Kamera, sondern ein Formular mit Artikelsuche; die Kamera kommt erst per Button. Im Formular laesst sich der Lagerort zusaetzlich per /l/-QR scannen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:15:25 +02:00
Scarriffle
6e48cd0a7a Stammdaten-Export/Import (JSON) + Lagerort-Eltern bleibt beim Anlegen
- Backend: services/master_data.py exportiert/importiert Kategorien (inkl.
  eigener Felder), Lagerorte, Einheiten und Gebinde als JSON – MIT IDs, damit
  gedruckte QR-Codes und Verweise nach dem Wiederherstellen passen. Import-Modus
  skip (Vorhandenes lassen) oder overwrite (per ID aktualisieren); Namens-
  Konflikte werden uebersprungen, nicht abgebrochen. Postgres-Sequenzen werden
  danach angehoben. Endpunkte GET /export/master-data, POST /import/master-data.
  5 neue Tests, Suite 162 gruen.
- Web: neue Karte "Stammdaten sichern & wiederherstellen (JSON)" in Import/Export.
- Web: beim Lagerort-Anlegen bleibt der uebergeordnete Ort in der Auswahl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 11:17:14 +02:00
Scarriffle
625b6ee263 Lagerorte: Baum-Picker beim Anlegen; Unter-Ort-Bestand zählt zum Bedarf
- Web: der übergeordnete Lagerort wird beim Anlegen jetzt über den
  aufklappbaren Baum (CategorySelect) gewählt statt über das flache Dropdown.
- Backend: Mindestbestand je Lagerort wird jetzt gegen den Bestand INKL. aller
  Unter-Lagerorte geprueft. Ein Bedarf auf „Hedingen" gilt also als gedeckt,
  wenn der Vorrat in „Hedingen -> Keller" liegt. Helfer
  location_subtree_stock_base; 2 neue Tests, Suite 157 gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:50:31 +02:00
Scarriffle
4a950acd85 Backend: Zeilenpreise als Kaufpreis-Kandidaten anbieten
Bei einer Rechnung mit mehreren Artikeln soll man den Zeilenpreis des passenden
Produkts wählen können. Die Kandidatenliste enthaelt daher wieder alle
geldartigen Betraege (Zeilen- und Summenbetraege), nur ohne MwSt-Satz und
Nicht-Geld-Zahlen; der Auto-Tipp bleibt die Gesamtsumme und steht vorne.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:45:08 +02:00
Scarriffle
88b9bdcbea Backend: Beleg-Analyse präziser (Preis, Kaufdatum, Garantieende)
Anhand echter Galaxus-Rechnungen nachgeschärft:
- Preis: MwSt-Satz ("8.10 %") wird als Prozentangabe ignoriert; von den
  Betraegen nahe einem Summen-Stichwort gewinnt der GRÖSSTE (Endbetrag inkl.,
  steht rechts) statt des naechstgelegenen (bisher der Exkl.-Betrag).
- Kaufdatum: eindeutige Stichworte (Rechnungs-/Bestell-/Kaufdatum) haben Vorrang;
  "Fälligkeitsdatum"/Zahlungsziel werden nicht mehr als Kaufdatum gewertet.
- Garantieende: ein ausdruecklich genanntes Enddatum im Garantie-Umfeld
  (z.B. "… - 06.10.2027") hat Vorrang vor dem gerechneten. 4 neue Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:40:18 +02:00
Scarriffle
bbf10b36c6 Backend: Beleg-Analyse um Kaufdatum + Shop; Analyse-Endpoint
- guess_acquired_on (Kaufdatum) und guess_shop (bekannter Shop erkannt -> id;
  sonst Kandidatenname zum Anlegen) ergaenzt.
- Upload liefert zusaetzlich suggested_acquired_on / shop_id / shop_name.
- Neuer POST /items/analyze-document: analysiert einen Beleg OHNE zu speichern
  (zum Vorbefuellen beim Anlegen). Gemeinsamer Helfer _doc_suggestions.
- 6 neue Tests; Suite 150 gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 09:30:35 +02:00
Scarriffle
67bc2dd87d Backend: Preiserkennung präziser (nicht mehr jede Zahl)
Nur noch Beträge, die nach Preis aussehen: direkt hinter einem Summen-Stichwort
oder mit Währung (CHF/EUR/€/…) in der Nähe. Ohne solchen Hinweis kein Vorschlag.
Datumsangaben (31.12.2027) werden per Lookaround nicht mehr als Betrag (31.12)
missgelesen. Kandidatenliste enthaelt entsprechend nur plausible Preise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 07:06:12 +02:00
Scarriffle
d0d854be5a Backend: Mindestbestand je Lagerort für Produkte und Gruppen
Zusaetzlich zum globalen Mindestbestand: je Produkt und je Gruppe laesst sich pro
Lagerort ein Mindestbestand (in Artikeleinheiten) hinterlegen.
- Neue Tabellen product_location_min_stock / group_location_min_stock.
- PUT /products/{id}/location-min-stock und /groups/{id}/location-min-stock
  ersetzen die Eintraege; Produkt-/Gruppen-Ausgabe liefert sie mit.
- Neue Einkaufsliste GET /shopping-list/by-location: Bedarfe je Ort (Produkte +
  Gruppen), Bestand-am-Ort gegen Mindestbestand-am-Ort.
- Helfer location_stock_base (Bestand je Ort). 4 neue Tests, Suite 144 gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 06:58:08 +02:00
Scarriffle
2bc871f229 Preis-Vorschlag: Auswahl aus mehreren erkannten Beträgen
Findet die Beleg-Analyse mehrere Beträge, liefert der Upload jetzt eine
Kandidatenliste (bester Tipp zuerst). Web und iOS bieten dann ein Dropdown zur
Auswahl des richtigen Kaufpreises, statt nur den automatischen Tipp. 1 neuer
Test; Backend-Suite gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 06:39:03 +02:00
Scarriffle
ba65e48d47 Backend: Kaufpreis aus Beleg-PDF schätzen
guess_price_cents zieht den Kaufpreis aus dem Belegtext: bevorzugt den Betrag
nach einem Summen-Stichwort (Gesamtbetrag/Total/…), sonst den groessten Betrag.
Versteht Tausendertrenner (1'299.00) und deutsches Format (1.299,00). Der
Upload liefert den Vorschlag als suggested_price_cents mit. 5 neue Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 20:57:30 +02:00
Scarriffle
61270e6d00 Backend: Kaufpreis + Belege an Einzelstücken, PDF-Garantie-Vorschlag
- Item: price_cents + currency (Kaufpreis rappen-/centgenau). Migration ergaenzt.
- Neue Tabelle item_documents (mehrere Belege je Stueck, PDF oder Bild); Blob
  wird verzoegert geladen, damit Item-Listen leicht bleiben.
- Endpunkte: Upload (POST), Ansehen/Download (GET), Loeschen (DELETE) je Stueck.
  Dateiname beim Download gehaertet (keine Header-Injektion).
- services/warranty.py: schaetzt aus PDF-Text lokal ein Garantieende
  (Zeitraum + Kaufdatum, oder Datum nahe Garantie-Stichwort); pypdf ergaenzt.
  Der Upload liefert den Vorschlag zurueck. 10 neue Tests, Suite 136 gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 20:49:08 +02:00
Scarriffle
b49546b297 Lagerorte umhängen (Backend + Web)
Bestehende Lagerorte lassen sich jetzt nachträglich einem anderen Elternort
zuordnen oder auf die oberste Ebene holen.
- Backend: LocationUpdate um parent_id (Name jetzt optional); update_location
  haengt um, mit Schutz gegen Ringe (weder auf sich selbst noch auf einen
  eigenen Unterort) und Existenzpruefung des Ziels. 6 Tests.
- Web: Lagerorte-Seite bekommt je Ort Bearbeiten (Name + Elternort ueber den
  CategorySelect-Baum, eigene Unterorte ausgeschlossen); api.updateLocation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 15:58:54 +02:00
Scarriffle
a70a99b387 Dashboard zaehlt Einzelstuecke mit; Baum-Dropdown scrollbar
Auswertungen (by_category, expiry_split, stats) iterierten nur Lots. Gegenstaende
mit Menge-je-Lagerort liegen als Lots und tauchten auf – Einzelstuecke liegen
aber als Items und fehlten dadurch komplett (Dashboard zeigte z.B. nur die
Powerbank, nicht Kamera/Kabel). Neuer Helfer _bestand_beitraege deckt beide
Speicherformen ab: jedes Item zaehlt als ein Stueck ohne MHD. Regressionstest
ergaenzt; volle Suite gruen (120).

CategorySelect: Scrollen IM aufklappbaren Menue schloss es, weil jeder Scroll als
Seiten-Scroll gewertet wurde. Jetzt schliesst nur ein Scroll ausserhalb des
Menues; die lange Liste laesst sich scrollen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 12:09:25 +02:00
Scarriffle
2ebdf94b16 Web: QR-Etiketten-Export (CSV für P-touch & Co.)
Auf "Import / Export" neue Karte: Kategorien auswaehlen (inkl. Unterkategorien)
und je Einzelstueck eine CSV-Zeile herunterladen - Spalten QR-Inhalt (…/i/<UID>),
UID, Produkt, Marke, Kategorie, Lagerort. In der Etiketten-Software als Datenbank
verknuepfen, dann entsteht QR + Text automatisch - kein einzelnes Bild-Download
und Zuordnen mehr.

Backend: GET /export/labels?category_ids=... liefert die Zeilen (JSON); den
QR-Inhalt setzt das Web mit window.location.origin dazu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 09:22:03 +02:00
Scarriffle
7f7129c87a iOS: Einzelstücke (UID/QR) + Scan öffnet das Stück; Seed ohne Modell-Kaufdatum
iOS-Parität zum Web: Item-Modell + API, Einzelstueck-Liste in der Produkt-
Detailansicht (anlegen, je-Stueck Lagerort/Kaufdatum/Garantie/Bezugsquelle/Notiz
aendern, entfernen mit Grund, QR je Stueck via CoreImage). Anlegen-Formular hat
den Schalter "Einzelstuecke". Der Scanner erkennt Einzelstueck-QR (…/i/<UID>) und
oeffnet direkt das Stueck.

Seed: "Kaufdatum"/"Garantie bis" nicht mehr als Modell-Felder auf Elektronik -
diese Angaben gehoeren je Einzelstueck ans Item, nicht ans Modell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 07:48:25 +02:00
Scarriffle
5ce4411d1e Backend: Einzelstücke (Items) mit UID je Gegenstand
Gegenstands-Produkte koennen als Einzelstuecke gefuehrt werden (Product.individual):
jedes physische Stueck ist ein Item mit eigener kurzer UID (fuer QR), eigenem
Lagerort, Kaufdatum, Garantie, Bezugsquelle und Notiz. So laesst sich dasselbe
Modell mehrfach getrennt fuehren (Powerbank 2024 + 2025).

Neu: models.Item + Product.individual (+Migration), Schemas, services/items.py
(UID-Erzeugung, Anreicherung), routers/items.py (CRUD, /items/by-uid/{uid} zur
QR-Aufloesung, /items/{id}/remove mit Grund als Bewegung). current_stock zaehlt
bei Einzelstueck-Produkten die Items. Tests + HTTP-Smoke gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:26:15 +02:00
Scarriffle
5c8b46a0d5 Foto-Upload: 413 beheben und Kamera stabilisieren
- nginx client_max_body_size auf 12m (Standard 1 MB loeste beim Bild-Upload
  einen 413 aus, bevor das Backend den Upload sah).
- Backend-Bildlimit auf 8 MB angehoben, bleibt unter dem nginx-Limit, damit
  Uebergroesse eine klare 400-Meldung statt eines nackten 413 ergibt.
- Web: Foto vor dem Upload clientseitig verkleinern (max 1600px, JPEG) -
  schneller, kleiner in der DB, zuverlaessig unter dem Limit.
- iOS: Kamera ueber fullScreenCover statt sheet und Schliessen deterministisch
  ueber ein Binding - behebt das "Kamera geht direkt wieder zu"-Flackern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:11:59 +02:00
Scarriffle
044f63446f Inline-Anlage (Kategorien/Felder), Artikelfoto und CSV-Spalte "art"
- Web: Kategorien/Unterkategorien und eigene Felder direkt im Artikelformular
  anlegen (ohne Umweg über die Kategorien-Seite).
- Artikelfoto per Kamera oder Galerie hochladen – Backend-Endpunkt
  (PUT/DELETE /products/{id}/image), Web (Foto machen / Galerie) und iOS
  (Kamera + PhotosPicker, inline in der Produktansicht angezeigt).
- CSV-Import: neue Spalte "art" (Gegenstand/Lebensmittel) steuert den Modus
  automatisch angelegter Kategorien; Export schreibt sie mit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:18:36 +02:00
Scarriffle
5b952524d7 Gegenstands-Verwaltung (Non-Food) neben Lebensmitteln
Die Kategorie bestimmt die Verwaltungsart (food/object). Gegenstaende:
Menge je Lagerort statt Chargen/MHD, Umlagern (ohne Grund) und Entfernen
mit Pflicht-Grund samt Entnahme-Statistik. Beliebig viele eigene Felder
je Kategorie (vererbt an Unterkategorien), "gekauft bei" ueber eine
verwaltbare Shop-Liste und ein Produktlink. Barcode-Lookup zusaetzlich
ueber Open Products Facts.

Der Lebensmittel-Teil (Chargen/MHD/FEFO) bleibt unveraendert. Umgesetzt in
Backend (FastAPI, +Tests gruen), Web (React) und iOS (SwiftUI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:33:56 +02:00
Scarriffle
f62289f975 Mehrere Dashboards und dieselbe Karte mehrfach
Zwei Grenzen sassen tief im Datenmodell. Erstens war "i" im gespeicherten
Layout zugleich die Kartenart; da react-grid-layout eindeutige Kennungen
verlangt, ging jede Art genau einmal - zwei Artikel gleichzeitig beobachten war
damit unmoeglich. Zweitens war dashboard_layouts.user_id unique, also genau ein
Dashboard je Benutzer.

Jetzt ist "i" die Kennung dieser einen Karte und "type" ihre Art, dazu kommt
"props" fuer das, was nur diese Karte angeht (etwa welcher Artikel). Aeltere
Anordnungen haben kein "type"; fuer sie gilt die alte Kennung als Art, sodass
gespeicherte Startseiten unveraendert weiterlaufen.

Die Tabelle bekommt Name und Reihenfolge, das unique faellt. Sein Name haengt
davon ab, wie die Tabelle entstanden ist, deshalb wird er in pg_constraint
nachgeschlagen statt geraten.

Eine Feinheit beim Anlegen: Wer noch auf der Admin-Vorgabe sitzt und sein
erstes eigenes Dashboard anlegt, wuerde die Vorgabe schlagartig verlieren -
sobald eigene Zeilen da sind, zaehlen nur noch die. Deshalb wird sie vorher
uebernommen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:11:49 +02:00
Scarriffle
794a81f58b Lagerorte und Einheiten umbenennen
Beide kannte die API bisher nur als Anlegen und Loeschen. Wer sich vertippt
hatte, musste den Eintrag wegwerfen und neu anlegen - und verlor dabei genau
das, was daran haengt: Chargen zeigen auf die Lagerort-ID, Produkte und Gruppen
auf die Einheiten-ID. Ein Tippfehler kostete also Zuordnungen.

Neu ist je ein PATCH nach dem Vorbild der Gebinde, samt Pruefung auf doppelte
Namen ohne Ruecksicht auf Gross- und Kleinschreibung. Anders als beim Loeschen
duerfen auch eingebaute Einheiten umbenannt werden - auch das wie bei den
Gebinden. Art und Faktor einer Einheit bleiben dagegen fest: Sie stecken in
bereits umgerechneten Bestaenden, eine Aenderung wuerde die still verfaelschen.

Die Tests halten fest, worum es eigentlich geht - dass die Verweise das
Umbenennen ueberleben.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 18:23:51 +02:00
Scarriffle
5171a5ecab Backendstart repariert: 204-Route mit future-annotations
Derselbe Fehler wie damals beim Dashboard. In package_types.py stand
"from __future__ import annotations"; dadurch wird "-> None" zu einer
Zeichenkette, die FastAPI zu NoneType aufloest und als Antwortmodell wertet -
zusammen mit 204 bricht der Aufbau der Anwendung ab, und der Container kommt
gar nicht erst hoch.

Der Import ist raus, mit Begruendung im Quelltext. Die uebrigen Router mit
diesem Import (branding, dashboard, transfer) sind geprueft: Sie geben in
204-Routen Response zurueck.

Damit es nicht ein drittes Mal erst am Container auffaellt, importiert ein
Test jetzt app.main. Genau dabei loest der Fehler aus - der Test deckt damit
auch jede kuenftige Route ab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:28:12 +02:00
Scarriffle
fd9045226b Gebinde verwalten - mit Einzahl und Mehrzahl
Bisher war die Auswahl eine fest im Frontend verdrahtete Liste und es gab nur
eine Form: ueberall stand "3 Glas".

Neu unter Verwaltung > Gebinde: anlegen, umbenennen, loeschen, jeweils mit
Einzahl und Mehrzahl. Die eingebauten Gebinde lassen sich in der Schreibweise
aendern, aber nicht loeschen; ein Gebinde, das ein Artikel verwendet, ebenfalls
nicht.

Der Artikel speichert weiterhin nur die Einzahl als Text - so bleiben
vorhandene Artikel, Sicherungen und CSV-Dateien gueltig, und eine unbekannte
Bezeichnung faellt schlicht auf die Einzahl zurueck. Deshalb zieht ein
Umbenennen die Artikel mit; sonst zeigten sie auf eine Bezeichnung, die es
nicht mehr gibt.

Die Mehrzahl greift jetzt in Artikelliste, Artikelseite (Chargen und Gebinde-
Auswahl), Auslagern und in allen Ablauf- und Einkaufslisten samt Startseite.
Einheiten wie Gramm oder Liter bleiben unveraendert - die haben im Deutschen
keine Mehrzahl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:24:42 +02:00
Scarriffle
0ba968985f Bild uebernehmen: sichtbare Rueckmeldung und Nachholen bei fehlender Kopie
Zwei Ursachen dafuer, dass der Knopf folgenlos wirkte.

Sichtbar: Uebernehmen schreibt nur ins Formular, das Artikelbild wechselt erst
beim Speichern - es gab aber nichts, woran man das erkannt haette. Die Spalte
"Bisher" zeigt jetzt sofort das uebernommene Bild mit Kennzeichnung, der Knopf
heisst danach "Uebernommen", und eine Meldung erklaert den naechsten Schritt.

Tatsaechlich folgenlos war der haeufigste Fall: Stimmte die Bildadresse
laengst und nur der Abruf war damals fehlgeschlagen, aenderte sich beim
Speichern nichts - und die Bedingung fragte genau nach einer Aenderung. Sie
richtet sich jetzt danach, woher die vorhandene Kopie stammt. Fehlt sie, wird
geholt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:03:28 +02:00
Scarriffle
8766e35e91 Artikeldaten von Open Food Facts vergleichen und einzeln uebernehmen
Neuer Knopf unter "Nachschlagen" auf der Artikelseite. Er fragt OFF erneut und
stellt die Antwort den eigenen Daten gegenueber: Name, Marke, Packungsgroesse,
Kategorie und Bild, jeweils mit eigenem Uebernehmen-Knopf und einem
"Alle uebernehmen". Uebernommen wird nur ins Formular - gespeichert erst mit
"Speichern", damit man bis zuletzt bei den eigenen Daten bleiben kann.

Dafuer noetig: GET /products/{id}/off. /lookup taugt hier nicht, weil es bei
einem bekannten Barcode den eigenen Artikel meldet und OFF gar nicht erst
fragt. Der neue Endpunkt probiert auch die zusaetzlichen EAN-Codes durch - oft
ist nur einer davon bei OFF hinterlegt.

Damit ist das Bild unabhaengig davon nachholbar, ob ein Backup eine
Bildadresse enthielt.

Nebenbei: Der Nachschlagen-Knopf uebergab die Gruppenliste, wo die
Einheitenliste erwartet wird - die Einheit eines Vorschlags wurde deshalb nie
richtig zugeordnet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:00:30 +02:00
Scarriffle
2fa274eae8 Backup-Import: Bildadresse ging weiterhin verloren
Der vorige Anlauf war wirkungslos. _import_json baut aus jedem JSON-Eintrag eine
eigene Zeile fuer _get_or_create_product - und image_url stand in dieser Zeile
nicht drin. Die Abfrage lief also immer ins Leere.

Zwei Tests gehen jetzt durch _import_json statt nur durch
_get_or_create_product, weil die Adresse genau zwischen den beiden herausfiel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:52:04 +02:00
Scarriffle
3763f2f114 Artikelbild: nur noch an einer Stelle, hochkant, und beim Import beruecksichtigt
Das alte Bild in der Chargen-Karte entfaellt - seit dem neuen Bild neben den
Kennfeldern stand es doppelt auf der Seite.

Der Rahmen ist jetzt hochkant (3:4) statt quadratisch. Lebensmittelverpackungen
sind fast immer hoeher als breit; im Quadrat blieben links und rechts leere
Streifen.

Beim Import wurde image_url komplett verworfen, obwohl das JSON-Backup sie
enthaelt - eingelesene Artikel blieben deshalb dauerhaft ohne Bild. Die Adresse
wird jetzt uebernommen (bei bestehenden Artikeln nur ergaenzend, nie
ueberschreibend); die lokale Kopie holt der Bildabruf beim ersten Ansehen.
Bewusst nicht waehrend des Imports: Ein Backup mit 200 Artikeln wuerde sonst
200 Downloads in einer Anfrage abarbeiten.

Ausserdem holen jetzt auch Artikelliste, Ein- und Auslagern ihr Vorschaubild
aus der eigenen Datenbank statt direkt von Open Food Facts. Einzige Ausnahme
bleibt der OFF-Vorschlag beim Scannen: Dort existiert der Artikel noch nicht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:48:20 +02:00
Scarriffle
c155116b4e Artikelbilder lokal speichern und auf der Artikelseite anzeigen
Open Food Facts liefert nur eine Bildadresse. Direkt dorthin zu verlinken hiesse:
Das Bild verschwindet, wenn OFF es austauscht, die Installation braucht Internet,
und jeder Seitenaufruf verraet OFF, welche Artikel jemand ansieht.

Das Bild wird daher einmal geholt und in einer eigenen Tabelle product_images
abgelegt - eigene Tabelle, damit Artikellisten die Blobs nicht mitziehen.
Geholt wird beim Anlegen, bei geaenderter Bildadresse und beim ersten Abruf
(so bekommen auch bestehende Artikel ihre Kopie, ohne Wanderung ueber alle
Datensaetze).

GET /products/{id}/image verlangt eine Anmeldung - aus den Bildern liesse sich
sonst ohne Konto ablesen, was im Vorrat liegt. Da ein <img src> keinen
Authorization-Header schickt, laedt die Oberflaeche das Bild ueber fetch und
zeigt es als Objekt-URL.

Angezeigt wird es rechts neben Barcode, Name und Marke. Fehlt ein Bild, rendert
die Komponente nichts und die Felder nehmen die volle Breite ein.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:35:10 +02:00
Scarriffle
dc1750d23a Umbenennung in Vorrania vollstaendig durchziehen
Auf ausdruecklichen Wunsch jetzt auch die internen Bezeichner, weil die
Installation noch keine echten Daten enthaelt: POSTGRES_USER/PASSWORD/DB, der
Volume-Name, die Standard-DATABASE_URL, der localStorage-Schluessel und die
iOS-Zeichenketten inklusive Keychain-Konto.

Das setzt eine leere Datenbank voraus: Der neue Volume-Name legt eine neue,
leere Datenbank an. Die alte bleibt als verwaistes Volume liegen und muss von
Hand entfernt werden.

Die Warnung in docker-compose.yml bleibt stehen, damit eine spaetere
Umbenennung nicht versehentlich mit Daten im Volume passiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:13:11 +02:00
Scarriffle
2423e74cae Umbenennung in Vorrania
Sichtbarer Name ueberall: Kopfzeichen der Oberflaeche, Seitentitel, Favicon,
API-Titel, User-Agent zu Open Food Facts, Dateinamen der Sicherungen, README,
Roadmap, Installer- und Update-Skript, npm-Paketname.

Bewusst NICHT umbenannt, jeweils mit Begruendung im Code:
- POSTGRES_USER/DB und der Volume-Name bleiben "project_good". Ein neuer
  Volume-Name legt eine leere Datenbank an, ein neuer Benutzername scheitert am
  bestehenden Volume - vorhandene Bestaende waeren verloren.
- Der localStorage-Schluessel bleibt, sonst waeren alle Browser einmal
  ausgesperrt.
- ios/ bleibt unangetastet (wird getrennt bearbeitet).

Die Klon-Adresse im README zeigt jetzt auf .../vorrania.git; das Repo in Gitea
muss dafuer umbenannt werden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:05:32 +02:00
Scarriffle
26f7ed21d8 Ein- und Auslagerungen als Wasserfall- bzw. Brueckendiagramm
Bisher waren es zwei Linien mit der Anzahl der Vorgaenge. Gemeint war eine
Bruecke: die Bestandslinie, und schwebende gruene/rote Balken, die jeden ihrer
Spruenge erklaeren.

Damit Balken und Linie ueberhaupt zusammen in ein Bild duerfen, zaehlen die
Balken jetzt Mengen in Artikeleinheiten statt Vorgaengen - beides liegt so auf
derselben Achse. Zwei Skalen in einem Diagramm bleiben ausgeschlossen.

Neuer Endpunkt GET /dashboard/flow liefert je Abschnitt Anfangsbestand, Zugang
und Abgang aus einer Abfrage. Getrennt abgefragt koennten die Abschnitte von
timeline und activity um eine Schrittweite auseinanderliegen - dann stuende ein
Balken neben dem Sprung, den er erklaert.

/dashboard/activity bleibt fuer externe Zugriffe erhalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:45:59 +02:00
Scarriffle
d6a717e39c Ablaufkarten trennen: "Bald ablaufend" zeigt kein Abgelaufenes mehr
Eine Karte mit dieser Ueberschrift, in der bereits Ueberfaelliges steht,
ist irrefuehrend. KarteAblauf bekommt daher einen Modus soon/expired/alle
statt des bisherigen Schalters "nurAbgelaufen".

Neu ist die Karte "Ablauf-Uebersicht" (alle), die beides zusammen zeigt.
Sie steht auch in der eingebauten Vorgabe, damit dort ohne Zutun weiterhin
Ueberfaelliges sichtbar ist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:24:30 +02:00
Scarriffle
1fd1e89872 Zeitraum im Kartenkopf; Verlauf feiner als ein Punkt pro Tag
Zeitraum-Auswahl:
Sie stand ueber dem Diagramm und nahm ihm Hoehe weg. Jetzt sitzt sie klein im
Kartenkopf neben dem Titel - die ganze Kartenhoehe gehoert dem Diagramm. Die
Wahl wird in der Anordnung mitgespeichert, bleibt also nach dem Neuladen
erhalten; ausserhalb des Bearbeitungsmodus geschieht das still im Hintergrund.

Feinere Aufloesung:
Ein Punkt je Tag verbarg, WANN ein- und ausgelagert wurde. Die Abtastrate
richtet sich jetzt nach dem Zeitraum:
  bis  2 Tage  -> stuendlich
  bis 14 Tage  -> alle 6 Stunden
  bis 120 Tage -> taeglich
  darueber     -> woechentlich
So bleibt die Punktzahl immer zwischen etwa 25 und 105 - fein genug zum
Erkennen, grob genug zum Zeichnen. Ein Test haelt diese Spanne fest.

Die Zeitachse traegt entsprechend Uhrzeit statt Datum, wenn stuendlich
abgetastet wird. Punkte heissen jetzt "at" (Zeitpunkt) statt "date".
Zeitstempel aus SQLite kommen ohne Zeitzone zurueck und werden vereinheitlicht,
sonst schluege die Differenzbildung fehl.

Auswahl erweitert: 24 Stunden, 2 Tage, 7, 30, 90 Tage, 6 Monate, 1 Jahr.

Geprueft: "npm run build" laeuft durch. pytest weiterhin nicht ausfuehrbar -
kein Python auf diesem Rechner; die Tests wurden an die neue Aufloesung
angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:36:33 +02:00
Scarriffle
97a6e94ea1 Kurzmeldungen als Overlay, Liniendiagramm fuellt die Karte, Zeitraum ab 1 Tag
Meldungen:
"Gespeichert", "Anordnung gespeichert" und die uebrigen Erfolgsmeldungen standen
als Leiste mitten in der Seite - beim Speichern sprang dadurch das ganze Layout
ein Stueck nach unten. Sie erscheinen jetzt als Overlay am unteren Rand und
verschwinden nach fuenf Sekunden von selbst; ein Kreuz blendet sie sofort aus.
Umgestellt in Uebersicht, Einstellungen, Gefahrenbereich, Benutzer,
Produktformular sowie Ein- und Auslagern.

Bewusst NICHT umgestellt: Fehlermeldungen. Ein Fehler, der nach fuenf Sekunden
verschwindet, ist genau dann weg, wenn man ihn noch braucht - der bleibt an
seiner Stelle stehen.

Rollbalken beim Liniendiagramm:
Den hatte ich mir mit der Zeitraum-Auswahl selbst eingebaut. Das Diagramm hatte
eine feste Hoehe von 200 px; zusammen mit der Auswahl darueber passte das nicht
mehr in die Karte. Jetzt wird auch die Hoehe gemessen, das Diagramm fuellt den
verbleibenden Platz.

Zeitraum "1 Tag":
Ein einzelner Tag ergibt genau einen Messpunkt - eine Linie aus einem Punkt
waere unsinnig. Das Diagramm zeigt in dem Fall die blanke Zahl. Die
Backend-Begrenzung laesst jetzt einen Tag zu.

Geprueft: "npm run build" laeuft durch; kein Verweis mehr auf die entfernten
Meldungszustaende.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:23:53 +02:00
Scarriffle
aa58914374 Kartengroessen korrigiert, Vorschau im Menue, Zeitraum-Auswahl bei Verlaeufen
Zu kleine Karten (Konstruktionsfehler):
Bei Schnellzugriff, Status und den Kennzahlen war die Mindesthoehe gleich der
Standardhoehe - und beide zu klein. Bei 96 px Zeilenhoehe blieben nach Kartenkopf
und Innenabstand nur 23 px fuer den Inhalt, eine Kennzahl braucht allein rund
45 px. Deshalb erschien schon in der Grundeinstellung ein Rollbalken, und
kleiner ging es nicht.
- Zeilenhoehe von 96 auf 40 px: die Hoehe laesst sich jetzt fein abstufen.
- Jede Karte hat eine Mindesthoehe, in die ihr Inhalt nachweislich passt
  (Rechnung steht als Kommentar am Verzeichnis).
- Gespeicherte Anordnungen aus der alten Fassung werden beim Laden auf die
  Mindestgroesse angehoben, statt zu flach zu bleiben.

Vorschau im Menue "Karte hinzufuegen":
Der Name allein verriet nicht, was eine Karte zeigt. Jede Karte hat jetzt eine
kleine Skizze (Tabelle, Ring, Balken, Linie, Kennzahl) und eine Beschreibung.
Bewusst gezeichnete Skizzen statt echter Vorschau - sonst muesste das Menue fuer
jede Karte Daten laden.

Zeitraum bei den Verlaufskarten:
Bestandsverlauf, Artikelverlauf und Ein-/Auslagerungen haben eine Auswahl
7 Tage bis 1 Jahr.

Ringdiagramme:
Der Ring klebte oben in der Karte, darunter blieb viel Leerraum. Er fuellt jetzt
die Karte und sitzt mittig.

Geprueft: "npm run build" laeuft durch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:09:05 +02:00
Scarriffle
4246e0d98f Fix Startabbruch: 204-Route in dashboard.py brach den Backend-Start ab
AssertionError beim Import: "Status code 204 must not have a response body".

Ursache ist eine Wechselwirkung, die nur in dieser Datei auftritt: dashboard.py
nutzt "from __future__ import annotations". Dadurch ist die Rueckgabeangabe
"-> None" eine Zeichenkette, die FastAPI zu NoneType aufloest und als
Antwortmodell wertet. Zusammen mit status_code=204 - das keinen Rumpf haben darf -
schlaegt die Pruefung beim Registrieren der Route fehl und das Backend startet
gar nicht. Andere Router im Projekt haben dieses __future__-Import nicht, deshalb
funktioniert dasselbe Muster dort.

Die Route folgt jetzt dem bereits bewaehrten Muster aus branding.py: Der
Statuscode steht am Response, nicht im Dekorator. Der Grund steht als Kommentar
an der Funktion, damit die Falle nicht erneut zuschnappt.

Geprueft: Alle uebrigen Routen der Datei haben ein explizites response_model im
Dekorator und sind davon nicht betroffen. Ein Start des Backends konnte ich hier
nicht pruefen - auf diesem Rechner ist weder Python noch Docker vorhanden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 09:58:01 +02:00
Scarriffle
84ce596446 Modulare Startseite: Kartenraster, eigene Anordnung je Benutzer, Auswertungen
Die Uebersicht war fest verdrahtet und fuer alle gleich. Jetzt besteht sie aus
Karten in einem 12-Spalten-Raster, die sich ziehen, in der Groesse aendern und
einrasten lassen (react-grid-layout). "Anordnen" schaltet das frei, gespeichert
wird ausdruecklich - Probieren bleibt folgenlos.

Anordnung:
- Neue Tabelle dashboard_layouts; user_id = NULL ist die Admin-Vorgabe.
- Jeder Benutzer hat seine eigene Anordnung. Admins speichern eine Vorgabe fuer
  alle und koennen sie ueber die Einstellung dashboard_enforced verbindlich
  machen; dann lehnt das Backend das Speichern eigener Anordnungen ab.
- "Auf Vorgabe zuruecksetzen" verwirft die persoenliche Anordnung.

15 Karten (Verzeichnis web/src/dashboard/cards.jsx, neue Karte = ein Eintrag):
Schnellzugriff, Status, Artikelzahl, Artikeleinheiten, Bald ablaufend,
Abgelaufen, Einkaufsliste, letzte Bewegungen sowie sechs Auswertungen
(Ablauf-Ring, Kategorien-Ring, Kategorien nach Zustand, Bestandsverlauf,
Verlauf eines Artikels, Ein-/Auslagerungen).

Diagramme als eigene SVG-Komponenten statt Diagramm-Paket:
- Zustandsfarben (ok/bald/abgelaufen) sind Statusangaben aus den Tokens und
  stehen nie ohne Beschriftung; Kategorien nutzen eine gepruefte Farbreihe in
  fester Ordnung, ab sieben Kategorien wird zu "Andere" gebuendelt.
- Eine Werteachse, duenne Marken, zurueckhaltendes Gitter, Fadenkreuz mit
  Kurzinfo. Hell und Dunkel haben eigene Farbstufen.

Mengen durchgehend in Artikeleinheiten (Glaeser, Packungen, Stueck): Gramm und
Stueck lassen sich nicht addieren, Gebinde schon. Der bisher dreifach
vorhandene Helfer liegt jetzt einmal in services/conversion.py::article_unit.

Der Verlauf wird rueckwaerts vom heutigen Bestand aus den Bewegungen
rekonstruiert. Bekannte Ungenauigkeit, im Code und in der Roadmap vermerkt:
Die Umrechnung nutzt die heutige Packungsgroesse.

Geprueft: "npm run build" laeuft durch. Die neuen pytest-Tests
(backend/tests/test_dashboard.py) konnten hier nicht laufen - auf diesem
Rechner ist kein Python installiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 09:48:16 +02:00
Scarriffle
4448718d8d Logo-Upload sichtbar machen; empfohlene Abmessungen angeben
Fehler: Ein hochgeladenes Logo blieb unsichtbar, auch nach dem Neuladen.
Die Oberflaeche prueft per Anfrage, ob ein Bild hinterlegt ist, und nutzte dafuer
HEAD. FastAPI registriert fuer eine GET-Route aber - anders als Starlette - kein
HEAD; die Anfrage lief in ein 405, und jedes hinterlegte Bild galt als "nicht
vorhanden". Der Upload selbst war die ganze Zeit in Ordnung.

- Neuer Endpunkt GET /branding liefert nur den Status ({"logo": true, ...}),
  ohne Bilddaten zu uebertragen. Die Oberflaeche fragt jetzt diesen ab.
- Die Bild-Route beantwortet zusaetzlich HEAD, damit sie sich erwartungsgemaess
  verhaelt.

Ausserdem:
- Die empfohlenen Abmessungen stehen jetzt sichtbar ueber der Vorschau
  (Logo etwa 400x72 px, Favicon 64x64 px, jeweils hoechstens 512 KB).
- Nach dem Hochladen wird die tatsaechliche Bildgroesse angezeigt, damit man
  sie mit der Empfehlung vergleichen kann.

Geprueft: "npm run build" laeuft durch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 08:55:17 +02:00
Scarriffle
8f549636a8 Marke statt Produktname am EAN-Code, Layout bricht um statt abzuschneiden
Am Gruppen-Code stand der Produktname. Innerhalb einer Gruppe hilft der aber
nicht weiter: In der Gruppe "Zucker" heisst jeder Artikel irgendwie "Zucker",
in "Mehl" irgendwie "…mehl". Unterscheiden laesst sich das nur ueber die Marke.
Der Endpunkt liefert jetzt beides; angezeigt wird die Marke, faellt bei
fehlender Marke auf den Namen zurueck und zeigt den vollen Namen im Tooltip.
Nebenbei ist die Marke meist deutlich kuerzer als der Name.

Abgeschnittene Tabelle: Die Spalte "verwalten" wurde gekappt. Ursache war nicht
die Breite an sich, sondern dass Grid-Kinder standardmaessig nicht unter ihre
Inhaltsbreite schrumpfen (min-width: auto). Dadurch lief die Tabelle aus ihrer
Karte heraus und wurde beschnitten, statt dass das vorhandene overflow-x
gegriffen haette. Mit min-width: 0 an den Grid-Kindern scrollt sie jetzt.
Zusaetzlich hat die Tabelle eine Mindestbreite von 640px - lieber waagerecht
scrollen als Spalten so weit quetschen, bis Text verschwindet.

Umbruch statt Quetschen: Die feste Umbruchbreite von 820px passte nicht zur
tatsaechlichen Lage - entscheidend ist, wie viel Platz die Spalten wirklich
brauchen, nicht wie breit der Bildschirm ist. Jetzt auto-fit mit Mindestbreite
je Spalte: Zwei Spalten, solange beide genug Platz haben, sonst automatisch
untereinander. Seiten mit inhaltsreichem Seitenblock (EAN-Codes) verlangen mehr
und stapeln frueher.

Durchgerechnet: Bei 1920 voller Breite zwei Spalten zu je 668px, Tabelle passt.
Bei halber Breite (960) eine Spalte zu 676px, Tabelle passt weiterhin. Erst bei
sehr schmalen Fenstern (unter etwa 900px Gesamtbreite) scrollt die Tabelle
waagerecht. In keinem Fall wird noch etwas abgeschnitten.

Getestet: 58 pytest-Tests gruen, Web-Build laeuft durch. Gegen die laufende API
geprueft, dass Marke und Name am Code mitkommen und die fehlende Marke sauber
auf den Namen zurueckfaellt. Die Breiten sind rechnerisch geprueft, die
Darstellung im Browser habe ich nicht selbst angesehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:53:29 +02:00
Scarriffle
5293810529 Gruppen-Codes: Dublette behoben und Altbestand nachgezogen
Zwei Fehler in der EAN-Liste einer Gruppe, beide gemeldet und nachgestellt.

Derselbe Code stand zweimal in der Liste: einmal schreibgeschuetzt mit der
Kennzeichnung "Artikel", einmal darunter mit Notizfeld. Grund war, dass
_group_to_out die Codes der Artikel unabhaengig von den Gruppen-Codes
zusammengestellt hat - seit die Zuordnung automatisch einen Gruppen-Code
anlegt, trifft beides auf denselben Code zu. Die schreibgeschuetzte Zeile stand
oben, deshalb war das Notizfeld darunter leicht zu uebersehen.

Jetzt gibt es eine Zeile je Code. Der Gruppen-Code fuehrt den Artikel mit, ueber
den er dazugehoert (neues Feld product_name in BarcodeOut), zeigt weiterhin die
Kennzeichnung "Artikel" - und hat trotzdem ein Notizfeld. Der Muelleimer
entfaellt bei diesen Codes, denn sie kaemen beim naechsten Speichern des
Artikels sofort zurueck; dafuer muss der Artikel die Gruppe wechseln.

Zweitens fehlte fuer bestehende Daten der Code ganz. Die automatische Pflege
greift nur beim Anlegen und Aendern eines Artikels; Zuordnungen, die es vorher
schon gab, hatten nie einen Gruppen-Code bekommen. In der Verwaltung stand der
Code deshalb ausschliesslich als schreibgeschuetzte Artikel-Zeile - genau die
Stelle, an der sich keine Notiz hinterlegen liess. Neu holt backfill() das beim
Start nach: fuer jeden Artikel mit Gruppe und Barcode wird der Gruppen-Code
angelegt, sofern er fehlt. Gefahrlos wiederholbar.

Getestet: 58 pytest-Tests gruen, einer neu (Backfill legt den fehlenden Code an
und beim zweiten Lauf nichts doppelt). Der gemeldete Fall wurde vorher gegen die
laufende API nachgestellt - Altbestand ohne Gruppen-Code und ein doppelt
gelisteter Code nach einer Neuanlage - und danach als behoben bestaetigt: eine
Zeile je Code, mit Artikelnamen und Notizfeld. Web-Build laeuft durch.
Die Oberflaeche habe ich nicht selbst bedient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:41:43 +02:00
Scarriffle
dfbc62398d Eigener Rueckfrage-Dialog statt Browsermeldung, Zeitstempel im Export, Notizen an Gruppen-Codes
Rueckfragen liefen bisher ueber window.confirm. Das reisst aus der Oberflaeche,
zeigt den Hostnamen, laesst sich nicht gestalten und die Knoepfe heissen immer
"OK/Abbrechen". Neu ist ein eigener Dialog (src/confirm.jsx) als Provider mit
dem Hook useConfirm. Der Aufruf bleibt so einfach wie vorher, weil er eine
Zusage zurueckgibt:

    if (!(await confirm({ title: "…?", message: "…" }))) return;

Damit sind alle neun Stellen umgestellt: Gruppen, Kategorien, Lagerorte,
Einheiten, Benutzer, API-Tokens, Produkt und Charge loeschen sowie das Ersetzen
beim Import. Jede Rueckfrage hat jetzt einen sprechenden Titel, einen Satz zu
den Folgen und einen benannten Knopf ("Loeschen", "Widerrufen", "Ersetzen")
statt eines nichtssagenden OK. Unwiderrufliche Schritte sind rot.
Kuenftig gilt: keine Browserdialoge mehr.

Export: Dateinamen tragen jetzt Datum und Uhrzeit
(bestand_2026-07-22_2130.csv, project-good-backup_2026-07-22_2130.json). Ohne
Zeitstempel hiessen mehrere Ausleitungen alle gleich und der Browser haengte
(1), (2) an - dann war nicht mehr erkennbar, welche die aktuelle ist. Der
Zeitstempel entsteht an beiden Enden gleich: im Content-Disposition-Kopf des
Backends und im Dateinamen, den der Browser setzt. Im JSON steht zusaetzlich
die Ortszeit neben dem bereits vorhandenen UTC-Zeitpunkt. Die CSV bleibt
inhaltlich unveraendert - eine zusaetzliche Spalte oder Kopfzeile wuerde die
Datei in Excel nur stoeren.

Notizen an Gruppen-Codes: Codes, die beim Zuordnen eines Artikels automatisch
entstehen, hatten bisher keine Moeglichkeit, eine Notiz zu bekommen - die liess
sich nur beim Anlegen von Hand mitgeben. Neu PATCH /groups/{id}/barcodes/{code}
und ein Notizfeld in der Liste, das beim Verlassen speichert. Leeren entfernt
die Notiz.

Getestet: 57 pytest-Tests unveraendert gruen, Web-Build laeuft durch. Gegen die
laufende API geprueft: Zeitstempel in beiden Content-Disposition-Koepfen und im
JSON-Inhalt; Notiz an einem automatisch angelegten Code setzen, aendern und
leeren, unbekannter Code antwortet mit 404. Die Dialoge selbst habe ich nicht
im Browser angeklickt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:31:45 +02:00
Scarriffle
48cc2646e8 Favicon aus dem Seitenleisten-Zeichen und Gefahrenbereich in den Einstellungen
Favicon: Bisher diente der Barcode-Glyph des App-Icons als Standard. Jetzt
zeigt der Browser-Tab dasselbe Zeichen wie die Seitenleiste - das "box"-Icon
aus components/Icon.jsx auf der Akzentflaeche. Die Farbe folgt der Oberflaeche:
im dunklen Modus der hellere Akzent, damit das Symbol auf beiden Untergruenden
sitzt. Ein eigenes Favicon laesst sich weiterhin hochladen und hat Vorrang.

Gefahrenbereich (Einstellungen, nur Admin) mit drei Stufen, damit man nicht
mehr wegwirft als noetig:

1. Alle Bestaende loeschen - Chargen und Bewegungsverlauf. Der Katalog bleibt
   vollstaendig erhalten, der Bestand steht ueberall auf null.
2. Alle Artikel loeschen - zusaetzlich der Artikelkatalog samt seiner
   EAN-Codes. Gruppen, Kategorien, Lagerorte und Einheiten bleiben. Die Codes,
   die an Gruppen haengen, bleiben ebenfalls.
3. Alles zuruecksetzen - zusaetzlich Gruppen, Kategorien, Lagerorte, Einheiten.

Benutzer, API-Tokens, Einstellungen und das eigene Logo bleiben in allen drei
Stufen erhalten. Anders liesse man sich aussperren und muesste externe Zugriffe
wie Home Assistant neu einrichten. Nach Stufe 3 werden die eingebauten
Einheiten und die Kategorie-Startliste sofort neu angelegt - ohne Einheiten
liesse sich nichts mehr einlagern, die Installation waere unbrauchbar.

Absicherung: Jede Aktion nennt vorher mit Zahlen, was genau verschwindet
("Betrifft 43 Chargen und 12 Bewegungen"), und laeuft erst, wenn das Wort
LOESCHEN eingetippt wurde. Ein Fehlklick kann nichts anrichten. Der Abschnitt
ist rot abgesetzt und weist auf das JSON-Backup hin.

Getestet: 57 pytest-Tests gruen, 3 davon neu (Bestaende loeschen laesst den
Katalog stehen, Artikel loeschen laesst Gruppen, Lagerorte und Einheiten sowie
den Gruppen-Code stehen, Startkategorien kommen zurueck). Gegen die laufende
API alle drei Stufen nacheinander durchgespielt und die Zahlen nach jedem
Schritt geprueft; Benutzer bleiben, Einheiten sind danach wieder da, ohne
Adminrechte antwortet der Endpunkt mit 401. Das Favicon wurde gerendert und
mit dem Zeichen der Seitenleiste verglichen. Web-Build laeuft durch.
Die Oberflaeche habe ich nicht selbst bedient - insbesondere habe ich keine
der Loeschaktionen ueber die Web-Oberflaeche ausgeloest.

Nebenbei die deutschen Anfuehrungszeichen im ganzen Projekt vereinheitlicht
(schliessendes Zeichen typografisch statt gerade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:21:52 +02:00
Scarriffle
158196f67b Gruppen und Kategorien getrennt, EAN-Codes an der Gruppe automatisch gefuehrt
Die Umbenennung von Gruppen in "Kategorien" war falsch: Es sind zwei
verschiedene Dinge. Sie ist zurueckgenommen, Kategorien kommen als eigene Ebene
dazu.

GRUPPE zaehlt Bestaende mehrerer Marken zusammen - Mehl von Rewe, Aldi und
Migros ergeben "5 kg Mehl". Dafuer Mindestbestand mit Einheit und EAN-Codes.
KATEGORIE ordnet allein die Artikelliste ("zeig mir alle Suesswaren"), ist
verschachtelbar wie ein Lagerort und hat weder Bestand noch EAN-Codes. Ein
Artikel kann beides, eines oder keines haben.

Die Vermischung war aelter als die Umbenennung: guessGroup in offUtils.js hat
aus der Open-Food-Facts-KATEGORIE eine GRUPPE geraten. Das ist entfernt. Die
OFF-Einordnung steuert jetzt die Kategorie, wo sie hingehoert; eine Gruppe
entsteht nur ueber einen hinterlegten Gruppen-Code oder bewusste Auswahl.

EAN-Codes an der Gruppe: Das war kein Anzeigefehler. Beim Anlegen eines Artikels
wurde ausschliesslich group_id gesetzt - ein Gruppen-Code entstand nie, die
Liste war tatsaechlich leer. Die Meldung "bereits vergeben" kam daher, dass der
Code am Artikel hing. Jetzt pflegt services/group_codes.py den Code mit: beim
Zuordnen kommt er hinzu, beim Gruppenwechsel wandert er mit, beim Entfernen der
Gruppe oder Loeschen des Artikels verschwindet er. Beim Scannen aendert sich
nichts an der Reihenfolge - der Artikel wird weiterhin zuerst gefunden; der
Gruppen-Eintrag ist Beleg in der Verwaltung und Rueckfall. Traegt man denselben
Code von Hand nach, ist das kein Fehler mehr, sondern die Auskunft, dass er ueber
den Artikel bereits dort steht.

Kategorien im Backend: neue Tabelle mit parent_id (Muster von Location),
products.category_id per ADD COLUMN IF NOT EXISTS nachgezogen, deutsche
zweistufige Startliste analog zu den eingebauten Einheiten. Die Startliste wird
nur angelegt, wenn ueberhaupt noch keine Kategorie existiert - wer sie bewusst
leerraeumt, findet sie nicht wieder. Beim Setzen einer Oberkategorie wird
geprueft, dass keine Kategorie sich selbst oder einem eigenen Nachfahren
untergeordnet wird; sonst entstuende ein Ring und jede Baumdarstellung liefe
endlos. Der Produktfilter schliesst Unterkategorien ein, category_id=0 liefert
die Artikel ohne Kategorie. Export und Import fuehren die Kategorie als Pfad
("Suesswaren & Snacks > Schokolade") in einer Spalte, damit die CSV in Excel
bedienbar bleibt.

Web: neue Seite Kategorien mit Baumdarstellung, Filter ueber der Produktliste,
getrennte Auswahlfelder im Produktformular mit je einer Zeile Erklaerung, und
beim Einlagern laesst sich eine Gruppe samt Einheit und Mindestbestand direkt
anlegen, ohne den Vorgang zu verlassen.

iOS: Kategorie-Filter ueber der Produktliste, Unterkategorien eingerueckt. Die
Artikelzeile nennt jetzt das Gebinde und warnt, wenn der Mindestbestand
unterschritten ist (rot) oder weniger als ein Viertel Luft bleibt (orange).
Getrennte Auswahlfelder fuer Kategorie und Gruppe, Gruppe direkt anlegbar.

Ausserdem die Eingabefelder in der App: .textFieldStyle(.roundedBorder) zeichnet
in der dunklen Darstellung einen fast schwarzen Kasten. Ersetzt durch eine
Systemfuellung, die sich Hell und Dunkel anpasst und zurueckhaltend bleibt.

Getestet: 54 pytest-Tests gruen, 14 davon neu (Nachfahren-Sammler, OFF-Zuordnung
inklusive Vorrang der Unterkategorie, und die komplette Codepflege an der
Gruppe). Gegen die laufende API geprueft: Startliste ohne Dubletten, Filter auf
Ober- und Unterkategorie, Ringschutz, Loeschen einer Kategorie laesst Artikel
und Unterkategorien bestehen, Export/Import-Rundlauf mit Kategoriepfad, und der
Durchlauf aus der Meldung - Artikel mit Gruppe anlegen, Code steht danach in der
Gruppe. Web-Build und iOS-Geraetebuild fehler- und warnungsfrei.
Die Oberflaechen habe ich nicht selbst bedient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:59:14 +02:00
Scarriffle
8dcc17b22f EAN-Codes einer Kategorie nachvollziehbar gemacht
Eine Kategorie mit Artikeln zeigte "0 EANs", und der Versuch, den Code eines
eigenen Artikels einzutragen, scheiterte mit "Dieser Code ist bereits vergeben".
Beides war fachlich richtig, aber nirgends erklaert.

Hintergrund: Die Code-Liste einer Kategorie ist eine Vorratsliste fuer Artikel,
die es noch nicht gibt. Sie greift nur, wenn beim Einlagern ein unbekannter Code
gescannt wird - dann landet der neu angelegte Artikel in dieser Kategorie
(routers/products.lookup). Haengt ein Code bereits an einem Artikel, findet die
Suche immer zuerst den Artikel; ein gleichlautender Kategorie-Eintrag koennte
nie wirken. Die Spalte zaehlte bisher ausschliesslich diese Vorratscodes, nie
die Codes der enthaltenen Artikel - daher die irritierende 0.

Die Kategorie liefert jetzt zusaetzlich die Codes ihrer Artikel mit
(product_barcodes, rein informativ). Beide Herkuenfte stehen in einer Liste:
Artikel-Codes sind als solche gekennzeichnet, nennen den Artikel und lassen
sich hier nicht loeschen, weil sie am Artikel haengen. Bewusst ohne Dublette in
der Datenbank - ein zweiter Datensatz koennte nie greifen und beim Loeschen des
Artikels verwaisen. Die Spalte zaehlt beide Herkuenfte.

Die Fehlermeldung sagt jetzt, wem ein Code gehoert: beim Artikel mit Namen und
dem Hinweis, dass Artikel-Codes hier nicht eingetragen werden muessen; bei einer
anderen Kategorie mit deren Namen.

Signal beim Einlagern: Wird ein Code erkannt, der einer Kategorie zugeordnet
ist, steht jetzt deutlich sichtbar "Wird automatisch der Kategorie X zugeordnet"
samt Begruendung - sowohl im Web als auch in der App, und in beiden Faellen
(Open-Food-Facts-Treffer und voellig unbekannter Code). Nach dem Anlegen meldet
die Web-Oberflaeche zurueck, welche Kategorie es geworden ist. Vorher stand die
Zuordnung nur als Nebensatz in grauer Kleinschrift.

Getestet: Gegen die laufende API geprueft, dass eine Kategorie die Codes ihrer
Artikel meldet (Haupt-Barcode und zusaetzliche Alias-Codes), dass das Eintragen
eines Artikel-Codes und eines fremden Kategorie-Codes mit der jeweils richtigen
Begruendung abgewiesen wird und dass ein echter Vorratscode weiterhin angelegt
werden kann. iOS-Geraetebuild und Web-Build fehlerfrei, 40 pytest-Tests gruen.

Dabei zwei Uebersetzungsfehler durch deutsche Anfuehrungszeichen gefunden: Das
schliessende Zeichen war ein gerades ", das den String vorzeitig beendete.
Korrigiert und die uebrigen Vorkommen im Projekt gleich mit vereinheitlicht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 19:52:34 +02:00
Scarriffle
f682993b62 Eigenes Logo und Favicon je Installation
Beides ist optional und wird ueber Einstellungen -> Darstellung gesetzt. Ohne
eigenes Bild bleibt alles wie bisher.

Ablage bewusst in der Datenbank (Tabelle branding_assets) und nicht im
Dateisystem: So liegt das Bild automatisch im Backup, und das Deployment
braucht kein zusaetzliches Volume. Es geht um wenige Kilobyte; der Upload ist
auf 512 KB und auf Bildformate begrenzt, die ein Browser auch wirklich
darstellt. Das Abrufen ist ohne Anmeldung moeglich, weil der Browser das
Favicon schon vor dem Login holt - hochladen und entfernen duerfen nur
Administratoren.

Das Logo ersetzt in der Seitenleiste und auf der Anmeldeseite Zeichen und
Schriftzug zusammen. Damit ein zu grosses oder sehr breites Bild das Layout
nicht auseinanderziehen kann, ist die Hoehe per CSS gedeckelt und die Breite
auf den Container begrenzt; object-fit haelt das Seitenverhaeltnis. Faellt das
Laden fehl, erscheint wieder das eingebaute Zeichen.

Als Standard-Favicon dient derselbe Barcode-Glyph wie im App-Icon, damit Web
und iOS-App zusammenpassen.

Getestet: Die Endpunkte sind gegen die laufende API geprueft - hochladen,
abrufen ohne Anmeldung, Abweisen von falschem Dateityp (400), zu grosser Datei
(400), unbekannter Bildart (404) und fehlenden Rechten (401), entfernen und der
Rueckfall auf 404 danach. Web-Build laeuft durch, 40 pytest-Tests gruen. Die
Darstellung im Browser habe ich nicht selbst angesehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 18:02:11 +02:00
Scarriffle
eed7c9e1de MHD wahlweise nur mit Monat und Jahr (Backend)
Auf vielen Verpackungen steht nur "09/2026". Bisher liess sich ausschliesslich
ein Tagesdatum erfassen, was zu erfundener Genauigkeit fuehrte.

Entwurf: best_before bleibt ein echtes DATE, damit FEFO, die Ablauf-Abfragen
und alle bestehenden Sortierungen unveraendert weiterlaufen. Eine Monatsangabe
wird auf den Monatsletzten gelegt - die uebliche Lesart bei Lebensmitteln, und
die sichere Richtung, weil nicht zu frueh aussortiert wird. Zusaetzlich merkt
sich jede Charge in best_before_precision, wie genau die Angabe war; davon
haengt allein die Anzeige ab. Am Produkt steht in date_precision, welche
Genauigkeit dort ueblich ist (Voreinstellung der Eingabe, z.B. Konserven).

Bewusst als VARCHAR statt als DB-Enum gespeichert: So laesst sich die Spalte
auf bestehenden Tabellen per ADD COLUMN IF NOT EXISTS nachziehen, ohne vorher
einen neuen Postgres-Typ anzulegen. Beide Spalten haben ein Server-Default
'day', damit vorhandene Chargen unveraendert gueltig bleiben.

Export/Import bleiben verlustfrei: Das JSON-Backup fuehrt beide Felder mit. Die
CSV bekommt bewusst keine neue Spalte, damit die Datei in Excel unveraendert
bedienbar bleibt - stattdessen wird eine Monatsangabe als "09/2026" geschrieben
und beim Import an der Schreibweise wieder erkannt ("09/2026", "2026-09",
"09.2026"); tagesgenaue Formate werden weiterhin gelesen.

Beim Korrigieren einer Charge werden MHD und Genauigkeit gemeinsam ausgewertet,
sonst bliebe ein auf Monat umgestellter Wert auf dem alten Tag stehen.

Getestet: 40 pytest-Tests gruen (17 neue zu Monatsletztem, Schaltjahr,
Einlagern, Export-Schreibweise und Import-Erkennung). Zusaetzlich ein
Durchlauf ueber die echte API: Produkt mit Monatsvorgabe anlegen und aendern,
Sammel-Einlagern mit gemischten Genauigkeiten, Chargen wieder auslesen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:48:11 +02:00
Scarriffle
faa9d6a25d Langlebige API-Tokens fuer externe Zugriffe (Home Assistant)
Die Einkaufsliste war ueber /shopping-list und /shopping-list/groups schon
abrufbar, aber nur mit einem Login-JWT, das nach 7 Tagen ablaeuft - fuer HA
unpraktisch.

- Neue Tabelle api_tokens (Name, SHA-256-Hash, Ersteller, zuletzt genutzt).
  Der Klartext wird nur einmal beim Anlegen zurueckgegeben.
- get_current_user akzeptiert jetzt JWT ODER API-Token im Authorization-Header;
  bei Nutzung wird last_used_at fortgeschrieben.
- Endpunkte /api-tokens (Admin): auflisten, erzeugen, widerrufen.
- Einstellungsseite: Tokens verwalten inkl. einmaliger Anzeige und einem
  aufklappbaren Konfigurationsbeispiel fuer Home Assistant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:36:17 +02:00