Commit Graph
357 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 7cb61d226f Lagerorte: ueberall ein aufklappbarer Baum statt flacher Pfadliste
Die Lagerort-Auswahl schrieb in jeder Zeile den ganzen Pfad aus
("Hedingen → Guido Zimmer → Kallax → Fach Oben Links → Oben"). Bei einem
tiefen Baum ist das eine Wand aus Wiederholungen, und Geschwister stehen
nur zufaellig beieinander.

Jetzt dasselbe Auswahlfeld wie bei den Kategorien: eingerueckter Baum,
Aeste ein- und ausklappbar. Geschlossen steht weiter der volle Pfad da -
"Oben" allein waere nicht eindeutig, genau dafuer war der Pfad gedacht.

CategorySelect konnte das schon und war auf Lagerort-Codes vorbereitet
(seine IDs duerfen Zeichenketten sein). Dazu kam nur LocationSelect als
duenne Huelle: Baumaufbau und die Umrechnung "" <-> null, damit die 16
Aufrufstellen weiter mit dem leeren String fuer "kein Ort" arbeiten.

Zwei Kleinigkeiten dabei: CategorySelect bekommt showRoot, weil bei den
Mindestbestaenden "nichts gewaehlt" kein gueltiger Wert ist - dort heisst
die oberste Zeile "Ueberall". Und LocationSelect sortiert Geschwister
selbst nach Namen; der Server sortiert zeichenweise, dort stuende "Fach
oben Rechts" hinter "Fach Unten Rechts".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-24 17:37:00 +02:00
ScarriffleandClaude Opus 5 290ff9341a Verwendungsstatus: in der App umstellen, filtern und nachsehen
Im Einzelstueck steht jetzt ueber dem Verleih ein Abschnitt "Verwendung"
mit vier Symbolen: verfuegbar, in Verwendung, in Reinigung, in Reparatur.
Ein Tipp bucht sofort, ohne "Speichern" - wie beim Verleih, denn alle
Scan-Wege (QR, NFC, vorrania://i/<UID>) landen genau in dieser Ansicht.
Vier Woerter waeren auf einem iPhone zu breit, vier Symbole passen immer;
darunter steht der Name im Klartext samt "seit wann".

Dazu der Verwendungs-Verlauf mit Zeitstempeln und Konto, und in der
Einzelstueck-Liste ein Filter ueber alle vier Zustaende plus die Zeile
unter dem Namen, solange etwas laeuft. Stuecke ohne Verwendungsstatus
fallen aus dem Filter "Verfuegbar" heraus - ein Schraubenzieher ist nicht
verfuegbar, er fuehrt den Zustand gar nicht.

Der Haken je Kategorie sitzt im Kategorie-Editor und gilt beim Bearbeiten
auch fuer die Unterkategorien.

Zwei Dinge, die beim Pruefen am Simulator auffielen: Die Leiste haette
beim Oeffnen jedes Mal einmal ins Leere gebucht, weil onChange auch beim
Laden feuert - dagegen der Abgleich mit dem zuletzt gemeldeten Stand. Und
SQLite liefert Zeitstempel ohne Zeitzone, woran ISO8601DateFormatter
scheitert; in der App stand deshalb der rohe ISO-Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-20 22:07:27 +02:00
ScarriffleandClaude Opus 5 b9d097fe96 Verwendungsstatus: Weboberflaeche zum Umstellen und eine Spalte dafuer
Auf der Scan-Zielseite /i/<uid> stehen vier Knoepfe - Verfuegbar, In
Verwendung, In Reinigung, In Reparatur -, der aktuelle hervorgehoben. Ein
Griff genuegt, genau dafuer ist die Seite da. Dazu der Verlauf mit
Zeitstempeln, wer wann umgestellt hat.

Dieselbe Bedienung in der Einzelstueck-Tabelle eines Artikels, und in der
Liste aller Einzelstuecke eine Spalte mit Plakette: gelb in Reinigung, rot
in Reparatur. Filterbar nach allen vier Zustaenden, "Verfuegbar"
eingeschlossen - "was liegt bereit" ist eine echte Frage.

Der Haken je Kategorie steht in der Kategorientabelle und im
Anlegen-Formular. Setzt man ihn an einer Oberkategorie, wandert er in die
Unterkategorien; das sieht man dort auch stehen.

Ohne den Haken erscheint nirgends etwas davon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-20 21:57:27 +02:00
ScarriffleandClaude Opus 5 98e2d18142 Verwendungsstatus: Backend fuer "getragen / in Reinigung / in Reparatur"
Einzelstuecke hatten bisher keinen Zustand - der Lagerort sagt, wohin ein
Stueck gehoert, aber nicht, woran es gerade ist. Neue Tabelle item_usages
nach dem Vorbild von item_loans: ein Zeitraum je Zustand, ended_at NULL
heisst "laeuft noch", und ein Teilindex laesst nur einen laufenden zu.

"Verfuegbar" wird bewusst nicht gespeichert - keine offene Zeile IST
verfuegbar, wie bei lots.opened_on und beim Verleih. Damit kostet der
Ruhezustand nichts und die Tabelle fuellt sich nicht mit Rauschen.
Abgeschlossene Zeilen bleiben stehen, sie sind der Verlauf.

Eigener Endpunkt (PUT /items/{id}/verwendung) und ausdruecklich nicht
PATCH /items/{id}: Das ist Admins vorbehalten, und die App schickt dort
immer alle Felder mitsamt null - jedes Speichern wuerde den Status
stillschweigend zuruecksetzen. Umstellen darf deshalb jeder angemeldete
Benutzer, wie Verleihen.

Der Zustand aendert den Bestand nicht und schreibt keine Bewegung: ein
getragenes Hemd ist weiterhin im Haushalt, anders als ein verliehenes.

Sichtbar nur bei Kategorien mit usage_tracking. Ein Haken an einer
Oberkategorie wandert in den Unterbaum, weil die Garderobe als
Kategoriebaum ja schon existiert. Die Markierung fahrt in beiden
Sicherungswegen mit; der Verlauf nicht, weil Einzelstuecke dort
ueberhaupt nicht vorkommen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-20 21:47:02 +02:00
ScarriffleandClaude Opus 5 8c752e3d1f App: NFC-Fehler kurz statt "Stack Error", Knopf weg nach Erfolg
Apples localizedDescription kam unveraendert durch und lieferte beim
Beschreiben von Tags wiederholt "Stack Error" - ein Text, der niemandem
sagt, was zu tun ist. Jetzt uebersetzt NFCDienst.kurz die CoreNFC-Codes
in kurze Meldungen ("Tag zu frueh entfernt.", "NFC gerade belegt."); ein
unbekannter Code behaelt seine Nummer, damit der fehlende Fall beim
naechsten Auftreten feststeht.

Die Meldung steht jetzt auch in Apples Systemblatt: im Fehlerfall wurde
die Sitzung wortlos beendet, deshalb las man sie erst danach in der App.

Nach einem erfolgreichen Schreibvorgang verschwinden Knopf und
Schreibschutz-Schalter - sie standen vorher weiter da und waren aktiv,
ein weiterer Tipp hat das Tag ein zweites Mal beschrieben. Stattdessen
steht dort jetzt "Fertig".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-20 21:41:35 +02:00
ScarriffleandClaude Opus 5 04f8c1fccd App: "Einzelstuecke anlegen" schloss sich beim ersten Mal sofort wieder
Artikel oeffnen, "Einzelstuecke anlegen" tippen - das Blatt ging kurz auf und
sofort wieder zu. Erst der zweite Versuch hielt. Danach blieb es stabil, bis
man den Artikel neu oeffnete.

Das Blatt hing an der Section, nicht am Formular. Genau davor warnt
ProductDetailView:368 seit laengerem im eigenen Kommentar - die Mengen-Sheets
wurden deshalb schon auf die Form-Ebene gelegt. Fuer die Einzelstuecke wurde
das nie nachgezogen.

Jetzt gibt ProductItemsSection das Blatt nach oben ab: Die Bindung gehoert
ProductDetailView, gezeigt wird es neben den Mengen-Sheets. Weil damit der
Rueckruf in den Abschnitt wegfaellt, bekommt er einen Nachlade-Zaehler - die
Elternansicht zaehlt ihn nach dem Anlegen hoch, und die Liste frischt auf.

Im Simulator vorher reproduziert und nachher geprueft: Erster Tipp nach dem
Oeffnen laesst das Blatt stehen, und ein angelegtes Stueck erscheint sofort
in der Liste ("Einzelstuecke (1)").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-17 19:30:48 +02:00
ScarriffleandClaude Opus 5 9f5d0a2310 Rezepte: Listenbreite ziehbar, lange Namen brechen um
Die Rezeptliste hing auf hoechstens 320px fest, und .btn ist nowrap - ein
Name wie "Cheesecake mit Streuseln und Fruchteinlage" lief damit einfach aus
der Spalte heraus und war abgeschnitten.

Jetzt sitzt zwischen Liste und Detail ein Griff zum Ziehen, Bedienung wie der
Spaltengriff der Tabellen (Zeiger-Ereignisse, col-resize). Die Breite wird
gemerkt und zwischen 180 und 640 Pixel gehalten: darunter ist die Liste
unlesbar, darueber bleibt fuer die Zutaten nichts mehr uebrig.

Dazu eine kleine Klasse .btn.umbruch, die das nowrap aufhebt - ein Knopf,
dessen Beschriftung umbrechen darf, ist auch anderswo brauchbar.

In der laufenden Oberflaeche nachgemessen: Ziehen um 120px ergibt genau 120px
mehr und landet im Speicher, die Grenzen klemmen bei 180 und 640, und der
lange Name braucht mit der Klasse zwei Zeilen (Text 372px bei 372px Spalte)
statt ueber den Rand zu laufen (434px).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 16:48:43 +02:00
ScarriffleandClaude Opus 5 8de222072c Exportbedingungen: Vergleichsart je Feldart statt nacktem Textfeld
Bisher tippte man den Wert immer von Hand ein - auch bei einem Haken, wo man
wissen musste, dass intern "true" steht, und bei einer Auswahl, wo ein
Vertipper stillschweigend nichts fand. Das ist der unangenehmste Fehlschlag:
Die CSV kommt einfach leer heraus, ohne Hinweis warum.

Jetzt bringt jede Bedingung die passende Eingabe mit:

  Ja/Nein    ein Haken
  Auswahl    "ist gleich" / "ist nicht gleich" plus das Dropdown der Optionen
  Zahl       "ist groesser =" / "ist =" / "ist kleiner =" plus Zahlenfeld
  Text       wie bisher freie Eingabe, leer = ohne Wert

Damit die Oberflaeche das kann, traegt /etiketten/spalten jetzt Art und
Auswahlwerte je Feld mit - vorher gab es nur Schluessel und Titel.

Der Filter bekommt dafuer ein op (=, !=, >=, <=), Vorgabe "=", damit
bestehende Vorlagen unveraendert weiterlaufen. Groesser/kleiner vergleicht
als Zahl und nimmt Komma wie Punkt; Zeilen ohne Wert treffen dabei NIE -
"leer" ist nicht kleiner als drei, es ist unvergleichbar. Eine unbekannte
Vergleichsart faellt auf "=" zurueck statt den Export zu zerreissen.

Die Sicherung traegt das op mit, wie schon das Feld selbst.

Sieben neue Tests, 547 gruen. Diesmal zusaetzlich in der laufenden Oberflaeche
durchgespielt: Alle vier Feldarten zeigen die richtige Eingabe, der Filter
wird gespeichert, und die CSV enthaelt bei "Laenge >= 5" genau die Artikel
mit 5 und 12 - nicht den mit 2 und nicht den ohne Wert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 16:18:48 +02:00
ScarriffleandClaude Opus 5 dd57d3356a Eigene Felder: Spalten in der Artikelliste und Export nach Feldwerten
Eigene Felder waren bisher nur im Artikelformular zu sehen. Zwei Luecken
daraus: In den Listen tauchten sie gar nicht auf, und es gab keinen Weg,
gezielt nach einem Feldwert zu exportieren.

SPALTEN. Je eigenem Feld eine Spalte in der Artikelliste, Schluessel
feld:<id> wie im Etikettenexport (die ID und nicht key, der ist nur je
Kategorie eindeutig). Reine Oberflaechenarbeit: GET /products liefert die
Werte laengst mit, Products.jsx warf sie nur weg. Filtern, Sortieren,
Ein-/Ausblenden und das Merken erbt die Spalte von DataTable.

Ja/Nein erscheint als Haken, liefert zum Filtern aber "Ja"/"Nein" - und
"Nein" auch ohne gespeicherten Wert: Wer den Haken wegnimmt, loescht die
Zeile, "nein" und "nie gesetzt" sind in den Daten dasselbe. Zahlen sortieren
als Zahl, sonst stuende "10" vor "9".

DataTable lernt dafuer defaultHidden: Alle Feldspalten starten verborgen und
sind erst auf Wunsch da - sonst hinge die Liste je nach Haushalt voller
Spalten, die fuer die meisten Zeilen gar nicht gelten.

Dabei einen vorhandenen Fehler gefunden: urlState las die Sortierung mit
split(":") und nahm das erste Stueck als Spalte. Bei "feld:12:asc" kam
dadurch "feld" heraus, und die Sortierung fiel beim Neuladen still weg. Jetzt
am LETZTEN Doppelpunkt getrennt.

EXPORT. Die Etikettenvorlagen bekommen einen Wertefilter: feldfilter in der
Konfiguration, mehrere Bedingungen UND-verknuepft, genau verglichen ohne
Ruecksicht auf Gross-/Kleinschreibung, leerer Wert heisst "ohne Wert".
Gefiltert wird in Python ueber die ohnehin geladenen Feldwerte - auf value
liegt kein Index, und "fehlende Zeile zaehlt als leer" braeuchte in SQL je
Bedingung ein NOT EXISTS.

Der Filter gilt auch fuer Einzelstuecke; der Wert haengt am Artikel des
Stuecks. Ein geloeschtes Feld laesst seine Bedingung wegfallen, genau wie
eine Spalte zu einem geloeschten Feld still verschwindet.

Die Sicherung schreibt den Filter wie die Spalten auf Kategorie::Label um -
ohne das zeigte er nach dem Zurueckspielen auf ein fremdes Feld, und der
Export gaebe mehr Zeilen aus als das Original. Dafuer ist _konfiguration aus
dem Router als konfiguration_schreiben in den Dienst gezogen: Sonst haetten
DREI Stellen denselben JSON-Text gebaut.

Zehn neue Tests, 540 gruen. Keine Schemaaenderung - konfiguration ist freies
JSON, alte Vorlagen lesen sich als leerer Filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 16:02:53 +02:00
ScarriffleandClaude Opus 5 83f7134459 App: Folgeblatt nach dem Anlegen ging beim ersten Mal sofort wieder zu
Zwei sheet-Modifikatoren hingen am selben View: einzelstueckeNachAnlegen und
mengeNachAnlegen, beide in ea7a42a nebeneinandergesetzt. Zwei Praesentationen
am selben View vertragen sich in SwiftUI nicht zuverlaessig - eine davon wird
verschluckt, und genau deshalb hielt das Blatt erst im zweiten Anlauf.

Jetzt ein einziger Modifikator, der innen verzweigt: Einzelstuecke oder Menge
je Lagerort, je nach Verwaltungsart. Statt zweier Zustaende nur noch einer.

In der laufenden App geprueft, beide Zweige einzeln: Gegenstand mit
Einzelstuecken angelegt -> "Einzelstuecke anlegen" bleibt beim ERSTEN Mal
offen; Gegenstand mit Menge je Lagerort angelegt -> "Menge hinzufuegen"
ebenso.

Dabei nebenbei bestaetigt: Ein Gegenstand ohne Kategorie bekommt vom Server
tracking="food" und damit zu Recht gar keinen Folgeschritt - die
Verwaltungsart allein macht noch keinen Gegenstand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 06:35:37 +02:00
ScarriffleandClaude Opus 5 1e8c4b2797 App: sichtbarer Knopf "Anlegen & NFC-Tag beschreiben" im Anlege-Formular
Gewuenscht war ein Knopf, den man sieht. Gebaut war ein Schritt, der erst
NACH dem Antippen von "Anlegen" kam - im Formular also nichts zu sehen. Und
er kam nicht einmal dann: await onDone() laedt die Elternansicht neu, dabei
wird das Anlege-Blatt abgeraeumt, und jeder Zustand, den man danach setzt,
laeuft ins Leere. Deshalb war von dem Schritt nie etwas zu sehen.

Jetzt steht "Anlegen & NFC-Tag beschreiben" neben "Anlegen", mit Wellensymbol
und nur bei Anzahl 1 (ein Tag gehoert zu genau einem Stueck; bei mehr sagt
ein Hinweis, warum er fehlt). Frueher kann der Knopf nicht stehen: Die UID
vergibt der Server erst beim Anlegen, vorher gibt es nichts zu schreiben.

Das Neuladen holt jetzt "Fertig" im Tag-Schritt nach, statt vorher zu laufen.

Diesmal in der laufenden App geprueft, nicht nur uebersetzt: Backend lokal
auf SQLite, Testartikel angelegt, Ablauf im Simulator durchgespielt. Der
Knopf erscheint, legt das Stueck an und fuehrt zum Tag-Schritt mit der
richtigen UID und vorrania://i/<UID> als Nutzlast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 21:51:19 +02:00
ScarriffleandClaude Opus 5 75899714b3 App: Listendarstellung der Auswahl zurueckgenommen - sie warf aus dem Formular
Beim Anlegen eines Artikels fuehrte die Kategoriewahl zurueck in die
Artikelliste: Das Formular steht in einem Blatt, und die geschobene Auswahl
riss beim Zurueckspringen den ganzen Navigationsstapel mit - das Blatt also
gleich dazu. Der Artikel liess sich so nicht mehr anlegen.

Damit ist .pickerStyle(.navigationLink) an allen acht Stellen wieder
draussen. Die Auswahl ist wieder ein Menue und rueckt mit fuehrenden
Leerzeichen ein, wie vor d7002e2.

Der Umbruch langer Namen bleibt damit ungeloest: Faellt "Schublade Links
Unten" auf eine zweite Zeile, faengt die wieder ganz links an. Die Grenze
steht jetzt als Kommentar an Baumzeile.label, damit der naechste Versuch
nicht wieder in dieselbe Falle laeuft. Funktionierendes Anlegen wiegt
schwerer als saubere Einrueckung.

Der mittige Loeschdialog aus demselben Commit bleibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 21:37:11 +02:00
ScarriffleandClaude Opus 5 38489a5217 App: der Tag-Schritt beim Anlegen erschien nie - jetzt geschoben statt aufgelegt
Nach dem Anlegen eines einzelnen Stuecks sollte das Tag-Blatt aufgehen. Es
kam nie. Ein zweites Blatt aus einem Blatt heraus ist hier gleich dreifach
riskant: ItemAddSheet traegt schon ein fullScreenCover (Lagerort scannen) -
zwei Praesentations-Modifikatoren am selben View vertragen sich in SwiftUI
nicht zuverlaessig - und das Formular wird selbst aus einer Section heraus
gezeigt. Vor beidem warnen ProductDetailView:349 und ItemViews:775 bereits
aus eigener Erfahrung.

Der Tag-Schritt wird deshalb in den ohnehin vorhandenen NavigationStack
geschoben. Ein Schub kennt keine dieser Fallen und ist fuer einen Schritt in
einem Ablauf ohnehin die passendere Form.

NFCSchreibenView bekommt dafuer ein optionales onFertig. Ohne Angabe
schliesst sie sich selbst wie bisher - der Aufruf aus der Stueck-Bearbeitung
bleibt unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 21:27:16 +02:00
ScarriffleandClaude Opus 5 d7002e2252 App: Einrueckung haelt auch bei Umbruch, Loeschen fragt mittig
Eingerueckt wurde mit fuehrenden Leerzeichen. Die verschieben aber nur die
ERSTE Zeile: Bricht ein langer Name um ("Schublade Links Unten"), faengt die
Fortsetzung wieder ganz links an und die Hierarchie ist weg.

In einem aufklappenden Menue laesst sich das nicht loesen - dort liefert man
nur Text ab, das System ignoriert jedes Layout (auch lineLimit). Die
Lagerort- und Kategorieauswahl erscheint deshalb jetzt als Liste
(.pickerStyle(.navigationLink)). Dort ist jede Zeile eine echte Ansicht, ein
Innenabstand rueckt den ganzen Block ein - auch die zweite Zeile. Das liest
sich bei vielen Lagerorten ohnehin besser als ein langes Aufklappmenue.

Zwei enge Zeilen bleiben Menues: Mindestbestand je Ort bei Artikel und
Gruppe, wo direkt daneben das Mengenfeld steht. Dort wuerde eine
Listenauswahl die Zeile sprengen; sie behalten die Leerzeichen-Einrueckung.

Der Loeschdialog war ein confirmationDialog und fuhr als Blase von unten
herein. Jetzt ein alert: steht mittig und will gelesen werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 21:12:29 +02:00
ScarriffleandClaude Opus 5 ea7a42ac22 App: Tag beim Anlegen, Menge beim Anlegen, Loeschen und NFC beim Ein-/Auslagern
Vier Bruchstellen im taeglichen Gebrauch, alle an derselben Wurzel: Nach dem
Anlegen endete der Weg, und was danach kam, musste man sich selbst suchen.

NFC beim Anlegen: createItems liefert die angelegten Stuecke samt UID laengst
zurueck, ItemAddSheet warf sie nur weg. Bei genau EINEM Stueck geht es jetzt
direkt zum Tag-Blatt. Bei mehreren bleibt es wie bisher - die App koennte
nicht wissen, welches der fuenf Stuecke gerade am Tag haengt. Ohne NFC im
Geraet (iPad) ebenso unveraendert.

Menge beim Anlegen: Ein Gegenstand, der nur als Menge je Lagerort gezaehlt
wird, landete nach dem Anlegen im Nichts. Das Blatt dafuer gab es schon
(ObjectAddSheet), nur fuehrte kein Weg dorthin. Jetzt derselbe Uebergang wie
bei Einzelstuecken, an beiden Einstiegen. Es holt sich die Lagerorte selbst -
ObjectAddSheet nimmt sie als Konstante und laedt sie nicht nach.

Loeschen: Der Knopf sitzt unten in der Artikelansicht, nur fuer Admins - die
Route verlangt das, ohne Riegel gaebe es nur ein wortkarges 403. Lehnt der
Server ab (noch Einzelstuecke oder Bestand), bleibt die Ansicht stehen und
seine Begruendung steht unveraendert in der Fehlerzeile.

NFC beim Ein- und Auslagern: Ein Tag benennt immer genau ein Einzelstueck,
deshalb fuehrt er zur Stueck-Ansicht - beim Auslagern ist "Entfernen (mit
Grund)" genau die passende Aktion. Bewusst NICHT ueber resolve() abgekuerzt:
das faellt bei unbekannter UID auf lookup(barcode:) durch und boete an, einen
Artikel mit einer URL als Barcode anzulegen. Beim Nachschlagen gab es NFC
schon, nur unscheinbar unter der Kamerakarte - Knopf abgesetzt und in der
Bildunterschrift erwaehnt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 20:55:37 +02:00
ScarriffleandClaude Opus 5 501d80a9b8 Lagerort- und Kategorieauswahl zeigen den Baum statt einer Mischung
Die Auswahl war nicht flach, weil die Beschriftung gefehlt haette - der volle
Pfad stand laengst drin. Sie kam vom Server nach dem BLATTnamen sortiert, und
so standen "Garage -> Fach A" und "Keller -> Fach A" irgendwo zwischen allem
anderen. Geschwister und Kinder fanden nie zusammen.

Neu ist baum() in Baum.swift: bringt eine flache Eltern-Kind-Liste in
Baumreihenfolge und rueckt je Ebene ein. Waisen gelten als oberste Ebene,
damit nichts lautlos aus der Auswahl faellt.

Derselbe Lauf lag bisher dreimal im Projekt (CategoryPicker, TreeMasterView,
LocationEditor). Alle drei zeigen jetzt auf den einen - unterm Strich
verschwinden mehr Zeilen als dazukommen. Zwei der Kopien waren auch nicht
gleichwertig: LocationEditor.pfad hatte den Zyklusschutz von path(in:) nicht
und waere bei einer ringfoermigen Elternkette endlos gelaufen.

Umgestellt sind neun Auswahlfelder. Die Lagerortzeile beim Kassenzettel zeigte
nur den nackten Namen und bekommt den vollen Pfad - dort ist kein Platz zum
Einruecken.

MARKETING_VERSION stand auf 1.2 in der project.pbxproj, aber auf 1.1 in der
project.yml, aus der sie erzeugt wird. Die Quelle nachgezogen, sonst haette
das naechste "xcodegen generate" die Anhebung zurueckgedreht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 20:11:25 +02:00
ScarriffleandClaude Opus 5 264d3886f3 Artikelsuche findet die Marke - und Loeschen fragt erst nach dem Bestand
Die Suche verglich bisher einen einzigen Textschnipsel gegen den Namen. Wer
"Vogel Jack and Jones" eintippte, bekam nichts: Dieser Ausdruck steht in
keinem Artikelnamen, und die Marke wurde ueberhaupt nie durchsucht. Jetzt
wird der Suchtext in Woerter zerlegt, jedes muss in Name oder Marke
vorkommen, die Reihenfolge ist egal.

Gefiltert wird in Python und nicht per ILIKE, weil normalisieren() Umlaute
aufloest ("Muehle" findet "Mühle") - auf der Spalte faende ein ILIKE den
gespeicherten Wert nicht wieder. Dieselbe Form nutzt recipes.zuordnung_ziele
schon laenger. Alle Artikellisten haengen an dieser einen Route, die
Verbesserung wirkt also auch bei Gegenstaenden und beim Ein-/Auslagern.

Die Artikelzeile in der App zeigt nun die Marke, sonst waere ein Treffer
darauf nicht zu erklaeren.

Dazu die zweite Haelfte: delete_product hatte keinerlei Schutz und riss
Chargen, Einzelstuecke, Barcodes, Bilder und den ganzen Bewegungsverlauf per
Kaskade mit. Jetzt lehnt der Server mit 409 ab, solange Einzelstuecke oder
Chargen dranhaengen. Bewegungen allein halten bewusst nicht auf - sonst waere
jeder je benutzte Artikel fuer immer unloeschbar. Der Bestaetigungstext im
Web versprach den Verlust des Bestands und stimmte damit nicht mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 20:06:07 +02:00
ScarriffleandClaude Opus 5 94f8ed4db9 Etikettenvorlage: der Umlaut aus macOS sprengte den Download
macOS legt Dateinamen zerlegt ab - das "ae" in "Lagerplaetze.lbx" ist dort
nicht ein Zeichen, sondern "a" plus ein kombinierender Trema-Akzent. Genau
diese Form landet beim Hochladen in der Datenbank. Starlette schreibt
Header aber als latin-1, und fuer den Akzent gibt es dort nichts - die
Route starb mit UnicodeEncodeError und antwortete 500, obwohl der Name am
Bildschirm voellig harmlos aussah.

Der Header wird jetzt nach RFC 6266 zweigleisig gebaut: filename= traegt
eine ASCII-Fassung als Rueckfall, filename*= den echten Namen
prozentkodiert. Browser bevorzugen filename* und zeigen den Umlaut richtig.

Derselbe Riegel liegt auf dem CSV-Download, wo der frei getippte
Vorlagenname in den Header geht - gleicher Defekt, nur noch nicht
aufgefallen.

Fuer die Download-Route gab es bisher keinen Test; zwei kommen dazu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 14:47:52 +02:00
ScarriffleandClaude Opus 5 b3256457d2 App: Chargenansicht zeigt auch Einlagerdatum und Verlauf
Nachtrag zur Detailansicht von gerade eben: Zwei Angaben fehlten, die
man an einer Charge sucht. "Seit wann liegt das hier?" - das
Einlagerdatum kam im Swift-Modell gar nicht erst an, obwohl /lots/rows
es liefert. Und "was ist damit passiert?" - dafuer gibt es jetzt den
Weg in den Verlauf des Artikels. Eine einzelne Charge hat im Backend
keinen eigenen Verlauf, die Bewegungen haengen am Artikel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 09:10:29 +02:00
ScarriffleandClaude Opus 5 ce05e5f4f6 App: eine Charge laesst sich jetzt oeffnen und ansehen
Die neue Chargenliste reagierte nur aufs Wischen. Das sieht man nicht,
und man kam an die Angaben der Charge ueberhaupt nicht heran - Menge,
Fristen und Lagerort standen zwar gekuerzt in der Zeile, aber mehr gab
es nicht.

Antippen oeffnet jetzt die Charge: Artikel und Marke, Menge (bei einer
offenen Packung auch die Menge beim Oeffnen), Zustand, Lagerort, MHD,
Anbruchtag und Verbrauchsdatum. Darunter beide Handlungen als sichtbare
Knoepfe - damit haengt "Anbrechen" nicht mehr allein an einer Geste, die
niemand findet. Das Wischen bleibt als Abkuerzung.

Das Verbrauchen-Formular wird aus der Detailansicht BLAETTERND geoeffnet,
nicht als zweites Fenster. Ein Sheet im Sheet aufzuziehen und beide
gleich wieder zu schliessen ist in SwiftUI eine zuverlaessige Quelle
fuer haengende Ansichten; so bleibt es eine Ebene.

Nicht kompiliert (kein Xcode hier): geprueft sind Klammer- und
Anfuehrungsbalance, dass keine Code-Zeile ein unpaariges
Anfuehrungszeichen traegt, und dass vom entfernten Zustand nichts
stehengeblieben ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:57:59 +02:00
ScarriffleandClaude Opus 5 c860c82912 App: Chargenliste, Anbrechen und Teilverbrauch
In der App gab es beides nicht. Chargen waren ueberhaupt nur innerhalb
eines Artikels zu sehen, und dort nur zum Korrigieren - eine
Chargenliste wie im Web fehlte, und damit jeder Weg, eine Packung
anzubrechen oder eine Teilmenge zu verbrauchen. Das Backend kann das
seit zwei Wochen.

Neu im Tab "Listen": der Eintrag "Chargen" mit allen Chargen ueber alle
Artikel, durchsuchbar, mit Schalter "Nur angebrochene". Je Zeile stehen
Menge, MHD bzw. Verbrauchsdatum, Lagerort und ein Abzeichen
"angebrochen"; bei einer offenen Packung die Restmenge in Basiseinheiten
("980 g von 1.000 g"), denn "0,98 Packungen" sagt niemandem etwas.

Nach links wischen gibt die zwei Buchungen:

"Anbrechen" mit Rueckfrage - eine Packung wird abgespalten und bekommt
ihr Verbrauchsdatum, der Rest bleibt zu und behaelt sein MHD.

"Verbrauchen" oeffnet ein Fenster fuer die VERBRAUCHTE Menge (nicht den
Rest - das ist die Zahl, die man kennt). Die Einheitenauswahl holt die
Hausmasse der Kategorie dazu, "2 EL" ist damit buchbar und landet bei
Mehl als 20 g. War die Charge noch zu, bricht die Buchung sie an; der
Haken dafuer ist abwaehlbar.

Loeschen fehlt in der Liste mit Absicht: Eine Charge verschwindet durchs
Verbrauchen, und das Korrigieren sitzt beim Artikel.

Dort zeigt die Chargenzeile jetzt auch "angebrochen · verbrauchen bis
..." - zwei Zeilen mit gleicher Menge waren sonst nicht zu
unterscheiden.

Alles in bestehende Dateien gelegt, keine neue Quelldatei: Das
mitgelieferte Xcode-Projekt listet die Dateien einzeln, eine neue waere
ohne "xcodegen generate" stillschweigend nicht mitgebaut worden.

Gegen den laufenden Server geprueft, genau mit den Aufrufen, die die App
macht: /lots/rows liefert alle Felder, die das Swift-Modell liest;
{"quantity":null} an /lots/{id}/open spaltet 1000 von 2000 g ab und
setzt consume_by; 2 Essloeffel an /lots/{id}/consume buchen 20 g ab und
die Antwort traegt opened und lot.

Nicht kompiliert: kein Xcode auf dieser Maschine. Geprueft sind Klammer-
und Anfuehrungsbalance (und dabei EIN echter Fehler gefunden: ein
deutsches Anfuehrungszeichen mit geradem Schlusszeichen haette eine
Zeichenkette vorzeitig beendet), dass formatAmount global erreichbar
ist, und dass die benutzten Hilfsfunktionen so heissen. Der Build in
Xcode bleibt die Probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 07:40:23 +02:00
ScarriffleandClaude Opus 5 f04d80a330 App: die selbst draufgesetzten Posten fehlten in der Einkaufsliste
Die Einkaufsliste besteht aus zwei Haelften: gerechnete Bedarfe (Artikel
und Gruppen unter ihrem Mindestbestand, Bedarfe je Lagerort) und das,
was jemand selbst draufgesetzt hat - eigene Eintraege und die aus einem
Rezept uebernommenen Zutaten.

Die App kannte nur die erste Haelfte. Sie fragte /shopping-list,
/shopping-list/groups und /shopping-list/by-location ab - drei Routen,
die ausschliesslich rechnen. Die gespeicherten Eintraege liegen in
/shopping-list/entries und kommen gebuendelt ueber /shopping-list/all;
danach hat die App nie gefragt. Ergebnis: Man setzt im Web etwas auf die
Liste, nimmt das Telefon mit in den Laden, und es ist nicht da.

Jetzt holt die App die gebuendelte Route - ein Aufruf statt drei - und
zeigt die eigenen Posten in einem eigenen Abschnitt "Selbst
draufgesetzt", mit Herkunft ("fuer Pfannkuchen") und Menge. Antippen
hakt ab: Der Posten verschwindet sofort, geht das Speichern schief,
kommt er zurueck - lieber ein Ruecksprung als ein falsches Haekchen.

Dieselbe Blindstelle hatte die Dashboard-Karte der App; sie meldete
"alle Mindestbestaende sind gedeckt", waehrend Posten auf der Liste
standen. Auch sie nimmt jetzt die gebuendelte Route.

Dazu ein Hinweis im Docstring von GET /shopping-list: Diese Route ist
ein Drittel der Liste, wer "die Liste" will, nimmt /all. Die Aufteilung
hat in zwei Tagen zweimal jemanden glauben lassen, es sei nichts da -
beim zweiten Mal war es die App selbst.

Gegen den laufenden Server geprueft: Mit einem Eintrag "Backpapier"
liefert /shopping-list weiterhin [] und /shopping-list/all den Eintrag
samt art, name, menge_text, source, recipe_name und done - genau die
Felder, die das neue Swift-Modell liest.

Nicht kompiliert: kein Xcode auf dieser Maschine. Geprueft sind
Klammer- und Anfuehrungsbalance, dass die drei alten Routen niemand mehr
aufruft, und dass die benutzten Hilfsfunktionen (makeRequest, jsonBody,
send) wirklich so heissen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 07:24:58 +02:00
ScarriffleandClaude Opus 5 892c52e49a App: /expiring kann jetzt ohne MHD kommen - das hat sie nicht ausgehalten
Beim Zusammenstellen der Testliste fuer den naechsten iOS-Build
aufgefallen, bevor es jemandem begegnet ist: Seit den angebrochenen
Packungen liefert /expiring auch Chargen OHNE aufgedrucktes MHD - eine
geoeffnete Packung hat nur ein Verbrauchsdatum, und genau die gehoert in
diese Liste. `best_before` ist dort seither null.

Im Swift-Modell stand `let bestBefore: String`, nicht optional. Eine
einzige solche Charge haette das Entschluesseln der GANZEN Antwort
scheitern lassen: keine Ablaufliste, keine Ablaufkarte - und keine
Erinnerungen, denn der Planer laedt dieselbe Antwort.

Gegen den laufenden Server geprueft: Eine angebrochene Packung ohne MHD
kommt als {"best_before": null, "consume_by": "2026-10-05",
"warn_date": "2026-10-05", "warn_source": "consume_by"}.

Mitgenommen, weil es ohnehin an derselben Stelle sitzt: Das Modell kennt
jetzt opened_on, consume_by, warn_date und warn_source. Die Erinnerungen
haengen am Warndatum statt am MHD - eine angebrochene Packung ohne
aufgedrucktes Datum braucht sie am dringendsten und bekam bisher keine.
Liste und Dashboard-Karte schreiben "verbrauchen bis 05.10." statt
"MHD ohne MHD", denn das sind zwei verschiedene Aussagen.

Nicht kompiliert: Auf dieser Maschine gibt es kein Xcode. Geprueft sind
Klammerbalance, Anfuehrungszeichen, dass item.bestBefore nirgends mehr
nicht-optional benutzt wird, dass ExpiringItem nirgends von Hand erzeugt
wird (nur entschluesselt), und der Abgleich jedes Feldnamens gegen eine
echte Serverantwort. Der Build in Xcode bleibt die Probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 21:13:43 +02:00
ScarriffleandClaude Opus 5 25e7f23c6c Tabellenfilter und Sortierung in die Adresszeile
Filter, Sortierung und ausgeblendete Spalten lagen nur im Browserspeicher.
Ein Neuladen ueberstanden sie damit schon, aber verschicken liess sich ein
gefilterter Stand nicht, und der Adresszeile sah man nicht an, was man
gerade ansieht.

Das Format kommt ohne Maskierung aus, weil Mehrfachauswahlen als
WIEDERHOLTER Parameter stehen statt als Liste mit eigenem Trennzeichen:

  ?f.product=milch&v.art=Lebensmittel&v.art=Gegenstand&sort=menge:desc&aus=created

Sonst zerbraeche ein Filterwert wie "Klamotten -> kurze Hosen" oder
"(ohne)" an der erstbesten Kommastelle. Der Sonderfall "keiner der Werte
gewaehlt" bekommt ein eigenes vn.<spalte>=1 - ein leerer Wert ist belegt,
es gibt echte Leerwerte ("(Leere)").

Die Vorrangregel: Der Browserspeicher ist die persoenliche Vorgabe, die
URL schlaegt sie. Geschrieben wird der Speicher aber NUR von
Bedienhandlungen. Wer einen fremden gefilterten Link oeffnet, soll seine
eigenen Einstellungen nicht verlieren, bloss weil er geschaut hat.

Dabei ist mein erster Versuch durchgefallen, und zwar sichtbar: Ich
wollte den ersten Speicherlauf ueberspringen. React fuehrt Effekte im
Entwicklungsmodus doppelt aus - der zweite Lauf schrieb dann doch, und
die Probe zeigte die fremden Filter im eigenen Speicher stehen. Jetzt
haengt ein Merker an den Settern statt am Mounten, und Spaltenbreiten und
-reihenfolge werden davon ausgenommen: Die entstehen immer durch Ziehen
und sind reines Layout.

Fremde Parameter derselben Seite bleiben stehen - auf der
Kategorienseite stehen kat und panel in derselben Adresse. Deshalb
bekommt setSearchParams eine Funktion und keinen fertigen Wert, sonst
raeumte ein veralteter Abschluss sie weg.

Von Hand durchgeklickt: Filter, Sortierung und ausgeblendete Spalte
stehen in der URL; eine blanke Adresse fuellt sich aus der gemerkten
Vorgabe; ein fremder Link gewinnt in der Ansicht und laesst den eigenen
Speicher unangetastet; eine eigene Aenderung wird gemerkt; "Filter
loeschen" raeumt beides; sieben Tastendruecke im Filterfeld erzeugen
NULL Verlaufseintraege; ein Tabellenfilter laesst kat/panel stehen;
/auth/callback wird weiterhin nicht vom Auffangmuster geschluckt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 16:19:55 +02:00
ScarriffleandClaude Opus 5 18481bbca0 Seitenzustand in die Adresszeile
Neu web/src/urlState.js mit useUrlWert/useUrlZahl/useUrlWerte. Zwei
Regeln stecken darin, beide damit die Adresszeile brauchbar bleibt:
geschrieben wird immer mit replace - sonst legt jeder Tastendruck in
einem Suchfeld einen Verlaufseintrag an und die Zurueck-Taste braeuchte
zwanzig Versuche, um die Seite zu verlassen -, und Vorgabewerte fliegen
aus der URL, sonst sammelt sich "?typ=&q=&kat=" an.

useUrlWerte gibt es, weil zwei getrennte Schreibvorgaenge im selben
Durchlauf einander ueberschreiben: Beide bekommen denselben
Ausgangsstand. Wo ein Klick zwei Dinge aendert ("Kategorie 12, Karte
Masse"), muss es ein Aufruf sein.

Damit wandern in die URL:

- Rezepte: Das Rezept steht jetzt im PFAD (/rezepte/<slug>), weil es
  benennt, was man ansieht - so wie /products/:id und /i/:uid. Portionen
  und Suchbegriff als Parameter. "Pfannkuchen fuer 12" ist damit ein
  verschickbarer Link.
- Kategorien: Segment-Umschalter und die geoeffnete Seitenkarte
  (?typ=food&kat=6&panel=masse). Gespeichert wird die Kennung, nicht das
  Objekt - die Kategorien kommen erst vom Server.
- Gruppen: die geoeffnete Seitenkarte.
- Artikel: der Segment-Umschalter.
- Ein- und Auslagern: der gewaehlte Artikel als Kennung, nachgeladen beim
  Aufruf. Eine halbfertige Buchung ueberlebt damit ein Neuladen.

Die Portionszahl landet NUR in der URL, wenn jemand sie selbst gestellt
hat. Die Vorbelegung aus dem Rezept bleibt draussen - sonst traegt jedes
geoeffnete Rezept ein ?portionen mit sich herum, das gar keine
Entscheidung ist.

Dazu im Backend: Ein unbekanntes Rezept ist jetzt 404 statt 502 und sagt
"Dieses Rezept gibt es in Mealie nicht (mehr)" statt "Mealie kennt
/api/recipes/x nicht". Das wurde noetig, weil ein Rezept seit diesem
Commit verlinkbar ist und ein alter Link irgendwann ins Leere zeigt -
eine gewoehnliche Auskunft, kein Gateway-Fehler.

Von Hand durchgeklickt: Rezept oeffnen und Portionen auf 12 ->
/rezepte/pfannkuchen?portionen=12, Neuladen bringt 250 g -> 750 g zurueck;
unbekannter Slug zeigt den klaren Hinweis und laesst die Liste stehen;
Kategorie-Karte und Segment ueberstehen F5, Schliessen raeumt die URL;
/checkin?artikel=1 laedt den Artikel nach, ?artikel=999 raeumt die
Adresse und zeigt die Suchmaske. 1 neuer Test, Suite bei 518.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 16:13:37 +02:00
ScarriffleandClaude Opus 5 a1b10d9ec3 Startseite heisst /home, Bearbeiten steht in der URL
Die Adresszeile sagte nicht, wo man ist. Die Startseite lag auf "/",
waehrend jede andere Seite einen sprechenden Pfad hat, und der
Bearbeitungsmodus des Dashboards stand nirgends: Neuladen warf einen
heraus, der Stand liess sich nicht verschicken, die Zurueck-Taste tat
nichts.

Jetzt /home und /home/edit, fuer weitere Dashboards /d/:id und
/d/:id/edit. "/" und jeder unbekannte Pfad leiten sichtbar auf /home um.

Der Kern der Arbeit steckte in Dashboard.jsx: Der Modus war nicht als
eigenes Flag gefuehrt, sondern aus `entwurf !== null` abgeleitet - eine
Variable, die zugleich Modus und ungespeicherte Aenderung war. Der Modus
kommt jetzt als Prop aus der Route, `entwurf` ist nur noch das, was sein
Name sagt. Weil der Entwurf allein im Speicher lebt, baut ihn ein Effekt
aus dem gespeicherten Layout auf, sobald dieses geladen ist - sonst
koennte man /home/edit nicht direkt aufrufen oder neu laden.

Dabei eine Falle vermieden: `aktives` ist nach jedem setDashboards ein
neues Objekt. Ohne Merker, zu welchem Dashboard der Entwurf gehoert,
haette jedes Neuladen der Liste waehrend des Bearbeitens die
ungespeicherte Anordnung verworfen.

"Anordnen" navigiert mit push, damit die Zurueck-Taste den Modus
verlaesst; "Speichern" und "Abbrechen" mit replace, damit sie nicht
hineinfuehrt. /home/edit bei verbindlicher Admin-Vorgabe faellt auf die
Ansicht zurueck, statt eine Leiste ohne Speichern-Knopf zu zeigen.

Dazu zwei Dinge, die sonst stillschweigend gebrochen waeren:

- Der ErrorBoundary-Schluessel hing am ganzen Pfad. /home/edit haette als
  andere Seite gegolten, den Baum neu eingehaengt und beim Umschalten
  Dashboards, Artikel und Lagerorte erneut geladen. Jetzt nur das erste
  Pfadsegment - im Mitschnitt steht beim Umschalten kein einziger Aufruf.
- Die Seitenleisten-Links trugen `end` (exakter Match), weil "/" sonst
  ueberall als Treffer galt. Bei /home ist das nicht noetig, und ohne
  `end` bleibt der Eintrag auch unter /home/edit markiert.

Von Hand durchgeklickt: "/" und "/gibtsnicht" landen auf /home;
Anordnen -> /home/edit, Neuladen bleibt im Modus mit sieben Karten als
Entwurf, Zurueck verlaesst ihn, nach Abbrechen fuehrt Zurueck nicht
wieder hinein; zweites Dashboard -> /d/1/edit; bei erzwungener Vorgabe
faellt /home/edit auf /home zurueck. Backend-Suite unveraendert bei 517.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 16:04:23 +02:00
ScarriffleandClaude Opus 5 98db3339a2 Vier Anzeigefehler aus dem Gebrauch
"NaN Glaeser" auf der Karte "Artikel im Blick". Die Karte reichte einen
ARTIKEL an articlePrimary() weiter, das fuer CHARGEN geschrieben ist und
die Menge unter dem Namen `quantity` sucht. Ein Artikel nennt sie
`stock` - also undefined / 190 = NaN. Neu bestandPrimary() und
bestandSecondary() in units.js als die eine Stelle, an der zwischen den
beiden Namen uebersetzt wird; sie delegieren an die vorhandenen
Formatierer, damit es nicht zwei Fassungen gibt. Steckte seit 8959814
drin.

Dazu gibt fmt() fuer nicht-endliche Zahlen jetzt einen Strich aus statt
"NaN". Dem Benutzer "NaN" hinzuschreiben ist nie richtig - der eigentliche
Fehler wird trotzdem an der Quelle behoben, nicht hier.

Die Spalte "Verbrauch" schnitt ab. Zwei beschriftete Knoepfe brauchen
rund 230 px, die Spalte hatte 190, und .datatable ist table-layout:fixed
mit overflow:hidden. Weil die Zelle rechtsbuendig ist, fiel der ANFANG
weg - sichtbar stand dort "rbrauchen". Jetzt 240 px, dazu flex-wrap in
der Zelle, damit die Knoepfe umbrechen statt abzuschneiden, wenn jemand
die Spalte von Hand schmaler zieht.

Die LETZTE Spalte liess sich nicht verbreitern. Ihr Anfasser sitzt exakt
am rechten Rand des Rollbereichs (gemessen: 0 px Luft), und nach rechts
ist dort Schluss - der Zeiger kann nicht weiter, also kam nie ein
positiver Abstand zum Startpunkt zustande. Der Resize rechnet jetzt mit
der BEWEGUNG seit dem letzten Schritt statt mit dem Abstand zum Start,
und in der Randzone waechst die Spalte von selbst weiter, solange die
Taste haelt; ein Taktgeber treibt das an, denn ein stehender Zeiger
meldet keine Bewegung. Die Tabelle rollt dabei mit, damit der Anfasser
unter dem Zeiger bleibt.

"3 Glas" statt "3 Glaeser" in der Chargenliste, waehrend die Uebersicht
"3 Glaeser" schrieb. Die Mehrzahlformen liegen in einer Modulvariablen,
weil auch Nicht-Komponenten sie brauchen - eine Aenderung daran loest
aber kein Neuzeichnen aus. Wer seine Liste zeichnete, bevor
/package-types zurueck war, blieb bei der Einzahl. Ein Zaehler im
Einstellungs-Kontext zieht die Formatierer jetzt nach.

Von Hand geprueft: Karte zeigt "3 Glaeser / 570 g"; beide Knoepfe stehen
vollstaendig in der Spalte; die letzte Spalte waechst beim Halten am
Rand von 240 auf ueber 470 und bleibt beim Loslassen stehen, waehrend
eine mittlere Spalte weiterhin exakt der Ziehstrecke folgt (170 -> 230);
Chargenliste schreibt "3 Glaeser".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 15:39:44 +02:00
ScarriffleandClaude Opus 5 4972feef09 Artikelauswahl im Dashboard: Marke dazu, Suche davor
Die Auswahl zeigte nur den Namen. In einem Vorrat mit mehreren
Herstellern standen damit zwei Zeilen "Pesto Rosso" untereinander, ohne
jeden Unterschied - man konnte nur raten und hinterher nachsehen, ob die
richtige Karte entstanden ist. Dazu war es eine blanke Liste ueber den
ganzen Katalog, hunderte Zeilen lang.

Jetzt steht in jeder Zeile "Name - Marke - Gebindegroesse", und darueber
ein Suchfeld, das wortweise filtert ("pesto mutti" findet eins von
vier). Der bereits gewaehlte Artikel bleibt in der Liste, auch wenn die
Suche ihn ausblendet - sonst zeigte das Feld nichts an und ein Klick
daneben haette die Wahl verworfen.

Der Kartentitel traegt die Marke jetzt ebenfalls, sonst hiessen zwei
Karten weiterhin gleich. Die Gebindegroesse bleibt dort weg, sie waere
im Titel zu lang; dafuer gibt es den Schalter an artikelLabel().

Die Beschriftung liegt in units.js, weil sie ueberall gebraucht wird, wo
Artikel zur Auswahl stehen - zwei Fassungen davon liefen sonst
auseinander.

Von Hand geprueft: zwei gleichnamige Pesto angelegt, Auswahl
unterscheidet sie, Suche kuerzt auf einen, Titel liest "Pesto Rosso -
Mutti". Suite bei 517.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 15:23:48 +02:00
ScarriffleandClaude Opus 5 2e610677aa Dashboard kennt die Einkaufsliste jetzt ganz
Der Knopf im Rezept schrieb die fehlenden Zutaten weg, aber auf der
Uebersicht blieb davon nichts zu sehen: Die Karte "Einkaufsliste" holte
die drei GERECHNETEN Quellen einzeln und wusste von den gespeicherten
Eintraegen nichts. Wer die Uebersicht als Zettel benutzt, hat sie
schlicht nicht gesehen.

Die Karte holt jetzt alles mit einem Aufruf und zeigt oben den
Abschnitt "Selbst draufgesetzt" - eigene Eintraege und die aus einem
Rezept uebernommenen, mit Herkunft und Menge. Abgehakte verschwinden
dort, die Karte ist zum Einkaufen da und nicht zum Nachlesen.

Das Abhaken war in der Karte reiner Sitzungszustand und damit nach dem
Neuladen weg. Es benutzt jetzt dieselben Schluessel und dieselben Routen
wie die volle Seite: Der Haken wandert sofort, speichert im
Hintergrund, und springt zurueck, wenn das schiefgeht.

Dazu die Kennzahl "Nachzukaufen": Sie zaehlte nur die gerechneten
Zeilen und meldete "alles erreicht", waehrend auf der Liste fuenf Posten
standen. Offene eigene Eintraege zaehlen jetzt mit, abgehakte nicht -
die sind gekauft.

Von Hand geprueft: Zwei Eintraege angelegt, Kennzahl springt auf 2, die
Karte zeigt beide samt "2 Packungen", und ein Haken aus der Karte steht
anschliessend in der Datenbank. 1 neuer Test, Suite bei 517.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 15:20:05 +02:00
ScarriffleandClaude Opus 5 b903de3841 Masse einer Kategorie: nur zeigen, was etwas bedeutet
Jedes angelegte Mass bekam eine Zeile, und jede Zeile drei gestapelte
Reihen (Name, Menge, Art) plus Muelleimer. Bei sechs Massen sah das aus
wie ein Formular voller Pflichtangaben - und keine davon ist eine.

Jetzt drei Sorten:

- Hier eingetragen: bearbeitbare Zeile mit Muelleimer.
- Von oben geerbt: eine Zeile zum Lesen ("Essloeffel (EL) 10 g geerbt")
  mit Knopf "abweichen", der sie bei Bedarf zur Eingabezeile macht.
- Alles andere: steckt in der Auswahl "Mass hinzufuegen" und stoert
  nicht. Hinzufuegen klappt eine leere Zeile auf; das Kreuz daneben
  klappt sie wieder zu, ohne etwas zu speichern.

Dazu das Zeilenlayout auf EINE Zeile gebracht: Name, Menge, Art, Aktion
nebeneinander, Spalten duerfen schrumpfen, Einheiten als Kurzform
(g/ml/Stk) - in der schmalen Seitenkarte ist das lesbarer, der Name
steht ja daneben.

Von Hand geprueft: Eine Kategorie mit zwei eingetragenen Massen zeigt
zwei Zeilen statt sechs, die Unterkategorie drei geerbte Lesezeilen und
kein einziges Eingabefeld. "Tasse" ueber die Auswahl hinzugefuegt, 200
ml eingetragen, gespeichert. Zeilenhoehe 29 bzw. 40 Pixel, kein
Umbruch bei 270 Pixel Kartenbreite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 15:09:53 +02:00
ScarriffleandClaude Opus 5 7ce07daa97 Globale Oeffnungsfrist in die Einstellungen
Die letzte Station der Vererbungskette hatte keine Oberflaeche: Artikel >
Kategorie (naechster Vorfahr) > global. Die Kategorien konnten seit dem
letzten Commit abweichen, aber der Wert, von dem alles ausgeht, liess
sich nur per API setzen.

Er steht jetzt in der Karte "Ablaufwarnung", direkt unter der
Vorwarnzeit - beides sind Fristen, die man im selben Moment einstellt.
Mindestens 1 Tag, denn er ist Pflicht: Waere er leer, bekaeme eine
angebrochene Packung gar kein "zu verbrauchen bis".

Von Hand geprueft: 3 auf 4 Tage geaendert und gespeichert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 14:39:20 +02:00
ScarriffleandClaude Opus 5 f61c76034c Hausmasse und Oeffnungsfrist bekommen eine Oberflaeche
Die Endpunkte standen seit dem Backend-Commit, aber eintragen liess sich
nichts - die Mengen gingen nur per API rein. Damit war "2 EL Mehl" im
Verbrauchen-Dialog zwar vorgesehen, aber fuer niemanden erreichbar.

Zwei Stellen dafuer:

Stammdaten > Masse verwaltet nur die NAMEN ("Essloeffel", "Tasse"). Die
Seite ist absichtlich kurz, denn die Mengen gehoeren nicht an das Mass:
Ein Essloeffel Mehl sind etwa 10 g, einer Zucker etwa 12 g, und eine
Tasse Milch ist Milliliter. Ein globaler Wert waere bei jedem zweiten
Artikel falsch.

Kategorien > "Masse & Frist" ist deshalb der Ort, an dem die Arbeit
passiert. Beides steckt in einem Bedienfeld, weil es dieselbe Mechanik
ist: Der Wert gilt fuer die Kategorie und alles darunter, und der
naechste Vorfahr mit einem Eintrag gewinnt. Leere Felder erben - und
zeigen als Platzhalter, WAS sie erben ("erbt 5 Tage (Kategorie: Mehl &
Zucker)", "10 g" mit Abzeichen "geerbt"). Ohne das muesste man den
Kategorienbaum im Kopf verfolgen, um eine leere Eingabe zu verstehen.

Dafuer zwei kleine Ergaenzungen am Backend: CategoryOut nennt die
geerbte Frist samt Herkunft, und /categories/{id}/measures nimmt
?vererbt=true, um neben den eigenen auch die von oben geerbten Mengen zu
liefern. Die Bearbeitungsansicht ohne Flagge bleibt wie sie war.

Von Hand durchgeklickt - und dabei einen echten Fehler gefunden: Der
API-Aufruf im Browser hat das vererbt-Flag verschluckt, weil die
Funktion nur ein Argument annahm. Die Unterkategorie zeigte dadurch
leere Felder, obwohl der Server die geerbten Werte lieferte. Jetzt
stehen dort "10 g" und "3 g" mit der Quelle im Tooltip.

4 neue Tests, Suite bei 516.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 14:37:53 +02:00
ScarriffleandClaude Opus 5 795e233a1b Einkaufsliste bekommt ein Gedaechtnis - und den Knopf aus dem Rezept
Die Einkaufsliste war vollstaendig GERECHNET: Artikel unter ihrem
Mindestbestand, Gruppen unter ihrem, Bedarfe je Lagerort. Das bleibt, es
ist die Staerke der Liste. Was fehlte, war ein Ort fuer "Milch gehoert
drauf, weil ich das sage" - und damit auch fuer "diese Zutaten fehlen mir
fuer das Rezept". Der Knopf konnte nirgends hinschreiben.

Zwei Tabellen dafuer, und die Trennung ist der Kern:

shopping_entries haelt, was jemand bewusst draufgesetzt hat. Genau eines
von Artikel, Gruppe oder Freitext - der Freitext ist unverzichtbar, denn
eine Rezeptzutat ohne Artikel ("Ras el Hanout") kommt nur so auf den
Zettel, und gerade die unbekannte Zutat ist die, die man vergisst.

shopping_checks haelt nur den HAKEN an einem gerechneten Posten, nicht
den Posten. Die gerechneten Vorschlaege duerfen keine Zeile bekommen: Sie
aendern sich mit jedem Einlagern, eine materialisierte Kopie waere sofort
falsch. Abhaken ging deshalb bisher nur im Browser und war nach dem
Neuladen weg - mitten im Supermarkt.

Aus einem Rezept uebernommen wird die FEHLMENGE, nicht die Rezeptmenge:
Wer 250 g braucht und 100 g hat, kauft 150 g. Gerechnet wird frisch mit
der Portionszahl, die auf dem Schirm steht. Je Rezept und Ziel gibt es
hoechstens einen offenen Eintrag - ein zweiter Druck nach dem Hochdrehen
der Portionen rechnet die Menge neu, statt die Liste zu verdoppeln. Ueber
verschiedene Rezepte hinweg bleiben es getrennte Zeilen, denn derselbe
Artikel kann zweimal fehlen. Ein abgehakter Eintrag blockiert nichts -
abgehakt heisst gekauft.

Mengen formatiert dieselbe Funktion wie beim Rezeptabgleich, damit die
Liste eine Sprache spricht: "2 Packungen" statt "1500 ml", aufgerundet,
weil man keine halbe Packung kauft.

In der Oberfläche eine eigene Karte "Selbst draufgesetzt" mit Eingabefeld,
Herkunftsangabe ("fuer Pfannkuchen") und "Erledigte entfernen", das beide
Quellen zusammen aufraeumt - fuer den Nutzer ist es eine Liste. Das
Abhaken wandert sofort und speichert im Hintergrund; schlaegt es fehl,
springt der Haken zurueck. Die Seite holt jetzt alles mit einem Aufruf
statt mit drei.

Von Hand geprueft gegen eine Mealie-Attrappe: Pfannkuchen fuer 48
Portionen ergibt 2 Packungen Mehl, 2 Packungen Milch, 3 Packungen Eier
und "Ras el Hanout" als Notiz; derselbe Knopf bei 96 Portionen
aktualisiert diese vier statt acht daraus zu machen; ein Haken
ueberlebt das Neuladen. 17 neue Tests, Suite bei 512.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 14:30:00 +02:00
ScarriffleandClaude Opus 5 f02e9a3840 Rezeptseite: Portionen drehen laesst die Seite stehen
Bisher setzte jede Aenderung an den Portionen das Rezept auf null - die
ganze rechte Karte verschwand und zeigte "Links ein Rezept waehlen",
bevor sie neu aufbaute. Bei einem Druck auf den Zahlenpfeil ist das ein
Flackern, bei getippten Zahlen dreimal hintereinander.

Jetzt bleibt stehen, was sich nicht aendert: Rezeptname, Portionenfeld
und die Zutatenliste. Nur die drei Wertespalten (Gebraucht, Im Haus,
Zustand) sind waehrend des Rechnens unklar und zeigen einen schimmernden
Platzhalter; das Ergebnis blendet sich danach ein (.wert-ein, 0.28s, mit
Ruecksicht auf prefers-reduced-motion). Das Kopf-Abzeichen sagt in der
Zeit "wird gerechnet..." statt eine veraltete Zahl, und die roten/gelben
Zeilenfarben setzen aus - sie gelten fuer die alten Mengen.

Dazu zwei Dinge, die ohne das Standenbleiben nicht aufgefallen waeren:

- Das Zahlenfeld reagiert sofort, die Anfrage wartet 280 ms. Wer von 4
  auf 12 tippt, bekam sonst erst ein Ergebnis fuer 1.
- Antworten koennen in falscher Reihenfolge eintreffen. Eine laufende
  Nummer je Anfrage verwirft alles, was nicht die neueste ist - sonst
  gewinnt beim schnellen Klicken zufaellig die langsamere Antwort.

Ausserdem: Eine Knopfreihe direkt unter einer Angabenliste klebte am
letzten Wert (Mealie-Karte in den Einstellungen). .kv hat bewusst keinen
Aussenabstand und .field-inline nur einen nach unten - den fehlenden
setzt jetzt eine Geschwisterregel, damit es ueberall gilt, wo dieses
Paar vorkommt.

Von Hand geprueft gegen eine Mealie-Attrappe: Pfannkuchen von 4 auf 12
Portionen - waehrend des Ladens 15 Platzhalter und die Zutatennamen
stehen, danach 250 g -> 750 g und 2 EL -> 6 EL (60 g), keine leere Karte
dazwischen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 14:20:33 +02:00
ScarriffleandClaude Opus 5 53a8b9e0b6 Anbrechen und Teilverbrauch bekommen Knoepfe
Das Backend konnte das seit dem letzten Commit, aber ohne Oberflaeche kam
niemand dran. Jetzt traegt jede Charge zwei Knoepfe:

"Anbrechen" spaltet eine Packung ab, gibt ihr ein "zu verbrauchen bis" und
laesst den Rest ungeoeffnet mit seinem eigenen MHD - mit Rueckfrage, weil
sich die abgespaltene Packung danach anders verhaelt.

"Verbrauchen" nimmt die VERBRAUCHTE Menge ("100 ml genommen", "2 von 6
Broetchen"), nicht den Rest - das ist die Zahl, die man kennt. War die
Charge noch zu, wird sie dabei angebrochen; der Haken dafuer ist
abwaehlbar, falls man nur eine Buchung nachtraegt. Die Einheitenauswahl
holt die Hausmasse der Kategorie dazu, deshalb laesst sich "2 EL" buchen
und landet bei Mehl als 20 g im Bestand.

Die Liste zeigt den Zustand jetzt auch: Spalte "Zustand" (angebrochen),
Spalte "Verbrauchen bis", und bei einer offenen Packung die Restmenge in
Basiseinheiten statt im Gebinde - "980 g von 1.000 g" sagt etwas,
"0,98 Packungen" nicht.

Gewarnt wird nach dem FRUEHEREN von MHD und Verbrauchsdatum (units.js::
faelligAm, Spiegelbild von Lot.faellig_am): Eine angebrochene Milch ist
nicht mehr bis zum aufgedruckten Datum gut. Dafuer ein eigenes Farb-Token
--open in ruhigem Blaugrau - angebrochen ist ein Zustand, keine Warnung.

Dazu ein Fehler, der die ganze Anzeige wertlos gemacht haette: In
list_all_lots fehlten opened_on, consume_by und opened_quantity. Die
Liste haette eine offene Packung wie jede andere Charge gezeigt, samt
falscher Ablaufwarnung. Test dagegen.

Von Hand durchgeklickt: 4x1l Milch anbrechen ergibt 1 l offen mit
Verbrauchsdatum 5.10. und 3 l zu mit MHD 15.11.; "2 EL" aus dem Mehl
ergibt 980 g offen und 1 Packung zu. Suite bei 495.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 14:17:20 +02:00
ScarriffleandClaude Opus 5 fe6327c913 Rezept-Zuordnung: ein Eigenschaftswort frisst keinen Artikelnamen mehr
Gefunden bei der Probe, ob ein Embedding-Modell die Zuordnung besser
hinbekommt: Die Sperrliste der Eigenschaftswoerter lief als eine einzige
Liste mit Praefix-Vergleich. "Frischkaese" fing mit "frisch" an, galt
damit als blosse Eigenschaft und fiel aus dem Vergleich - die Zutat
"Kaese" fand den Frischkaese deshalb NIE. Ebenso "Biomilch" (bio),
"Rohrzucker" (roh), "Kleingebaeck" (klein), "Ganzkornbrot" (ganz),
"Grobgriess" (grob).

Unsichtbar, weil kein falscher Treffer entstand, sondern ein richtiger
ausblieb - die Zutat stand einfach als "nicht zugeordnet" da.

Jetzt zwei Toepfe statt einem. Stammformen wie "getrock" oder "vegan"
vergleichen weiter ueber den Wortanfang, weil Deutsch beugt und diese
Stuecke keine Lebensmittelnamen anfangen. Kurze Alltagswoerter wie
"frisch", "bio", "roh", "klein" zaehlen nur noch als Eigenschaft, wenn
hoechstens eine Beugungsendung dranhaengt: "frische" ja, "frischkase"
nein.

Gegenprobe im Test: "frische Hefe" findet die "Frischen Kraeuter"
weiterhin nicht - die Sperre ist praeziser geworden, nicht schwaecher.

7 neue Tests, Suite bei 494.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 14:01:56 +02:00
ScarriffleandClaude Opus 5 8072e7f566 Zutaten von Hand zuordnen - und Vorrania merkt es sich
Der unscharfe Abgleich muss raten und wird das immer tun: "Lime Curd" hat
mit "Zitronencreme" kein Wort gemeinsam, und manchmal liegt er daneben.
Bisher war das eine Sackgasse - die Zutat blieb unzugeordnet, bei jedem
Aufruf neu.

Jetzt traegt jede Zutatenzeile einen Knopf "Zuordnen". Entschieden wird
gegen Artikel UND Gruppen, gemerkt unter dem normalisierten Namen des
Lebensmittels: "Schlagsahne", "schlagsahne" und "SCHLAGSAHNE " treffen
dieselbe Zeile. Ab dann gilt die Entscheidung fuer jedes Rezept, in dem
diese Zutat vorkommt - eine Zutat wird hoechstens einmal zugeordnet.

Schluessel ist der Name, nicht die Mealie-ID: IDs sind UUIDs, die beim
Neuanlegen eines Lebensmittels wechseln, und nur ueber den Namen greift
die Zuordnung auch fuer Zutaten ohne verknuepftes Lebensmittel ("2 Eier").

Im Auswahldialog stehen oben die Vorschlaege, die der Abgleich selbst
gefunden hat - meist ist der Richtige darunter, nur nicht an erster
Stelle -, darunter die freie Suche ueber alles. Die Basiseinheit steht
dabei, damit nicht wieder Milliliter auf ein Gramm-Produkt wandern.

Beim Raten bleiben Gegenstaende aussen vor; von Hand darf alles gewaehlt
werden ausser Einzelstuecken. Backpapier ist kein Lebensmittel und steht
trotzdem in Rezepten, waehrend ein Einzelstueck nie eine Zutat ist.

Zeigt das Ziel ins Leere, weil der Artikel geloescht wurde, faellt die
Zeile beim Laden still heraus: Auf SQLite greift kein Fremdschluessel,
und ein geloeschter Artikel darf kein Rezept mehr sprengen.

Die Zuordnungs-Routen stehen bewusst vor /recipes/{slug}, sonst liest
FastAPI "zuordnungen" als Rezept-Slug und fragt Mealie danach. Ein Test
wacht ueber die Reihenfolge, weil ein Umsortieren der Datei den Fehler
sonst still einbauen wuerde.

16 neue Tests, Suite bei 487.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 13:40:23 +02:00
ScarriffleandClaude Opus 5 2a56cca3c8 Rezept-Zuordnung: die Einheit verraet das falsche Produkt
"200 ml Schlagsahne" landete auf "Kraeuter Sahne Haehnchen" - beide teilen
das Wort "Sahne", der Rest ist Zufall. Die Einheit haette es verraten: Das
Haehnchen wird in Gramm gefuehrt, das Rezept sagt Milliliter. Wo sich die
Rezept-Einheit gar nicht umrechnen laesst, wird deshalb nicht mehr
zugeordnet, sondern offen gelassen - lieber "nichts Passendes gefunden"
samt Begruendung als eine falsche Behauptung.

Entscheidend ist, WARUM die Umrechnung scheitert, und nur einer der Gruende
sagt etwas ueber den Artikel:

- Bekannte Einheit falscher Art ohne Zweiteinheit -> spricht gegen den
  Artikel. Milliliter an einem Gramm-Produkt ist ein anderes Produkt.
- Unbekanntes Wort oder nicht hinterlegtes Hausmass ("1 Tasse Linsen") ->
  sagt gar nichts. Die Linsen koennen voellig richtig sein, es fehlt bloss
  die Massangabe, und die laesst sich nachtragen.
- Gar keine Einheit ("3 Eier") -> passt immer.

Die erste Fassung hat das nicht unterschieden und damit "1 Tasse Linsen"
zu "nicht zugeordnet" gemacht, obwohl der Artikel stimmte - die beiden
bestehenden Tests haben das sofort gezeigt.

Bei mehreren Kandidaten gewinnt jetzt der mit passender Einheit, auch gegen
eine hoehere Namensaehnlichkeit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 13:25:41 +02:00
ScarriffleandClaude Opus 5 41d262cb2d Rezept-Zuordnung: "vegan" ist eine Eigenschaft, kein Lebensmittel
Der Wortanker nahm jedes geteilte Wort ab vier Zeichen - also auch
"vegan". Damit passte jedes vegane Produkt auf jede vegane Zutat: "vegane
Butter" landete auf "veganes Muehlen Knusper Bleu", "veganer griechischer
Joghurt" auf "Vegane Muehlen Nuggets" und "veganer Lime Curd" auf "Vegane
Muehlen-Schnitzel auf Basis von Soja". Geteilt wurde dort nur die
Eigenschaft, nicht die Sache.

Beschreibende Woerter begruenden deshalb keinen Treffer mehr - vegan,
vegetarisch, bio, frisch, geschmolzen, ungesalzen, geschaelt, geschnitten,
Zimmertemperatur, oder, optional und so weiter, dazu reine Zahlen. Als
Wortanfaenge aufgefuehrt, weil Deutsch beugt ("vegan", "vegane",
"veganer", "veganes"). Sie sperren aber nicht: "vegane Butter" findet
weiterhin eine "Vegane Butter", nur eben ueber das Wort "Butter".

Wichtig war, die Liste auf BEIDEN Seiten anzuwenden. Sonst steckt "vegan"
weiter im Produktnamen und der Teilstring-Vergleich greift von dort aus.

Gegenprobe mit dem echten Katalog: Von zehn Zutaten bleiben die drei
richtigen Treffer (Mehl, Vollmilch, Aepfel), die sieben erfundenen sind
weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 13:22:38 +02:00
ScarriffleandClaude Opus 5 4e34accee3 Rezept-Zuordnung: Gegenstaende raus, Wortanker rein, Kategorie zuerst
Die Zuordnung war unbrauchbar: "Mehl Typ 405" landete auf "Muehlen
Geschnetzeltes", "Eier" auf "Reis", "Zucker" auf "Joystick fuer WII",
"Apfel" auf "Snack Collection Platten 4 (Waffeln)" und "Salz" auf "Shirt
Casual Fit". Drei Fehler steckten darin.

1. Gegenstaende standen ueberhaupt zur Auswahl. Der Kassenzettel-Abgleich
   filtert sie seit immer heraus, mein Rezeptabgleich nicht - also konnten
   ein Joystick, ein Shirt und eine Waffelplatte als Zutat gelten. Jetzt
   zaehlen nur Lebensmittel, und Einzelstuecke bleiben ebenfalls draussen.

2. Entschieden hat eine Buchstaben-Aehnlichkeit. Die haelt "Eier" und
   "Reis" fuer verwandt, weil sich Zeichen ueberlappen. Neu muss ein ganzes
   Wort geteilt sein: "Mehl" in "Weissmehl Typ 550" ja, "Mehl" in "Muehlen
   Geschnetzeltes" nein. Umlaute und Mehrzahl werden dabei zusammengezogen,
   damit "Apfel" auch "Aepfel" findet.

3. Gesucht wurde im ganzen Katalog. Jetzt zuerst dort, wo es hingehoert:
   Teilt der Zutatenname ein Wort mit einer Kategorie, gilt nur deren
   Teilbaum - "Mehl Typ 405" sucht in "Mehl & Zucker", nicht im Fleisch.
   Erst wenn darin nichts passt, zaehlt der Rest.

Die Punktzahl ist damit keine Sperre mehr, sondern nur noch die
Reihenfolge. Als Sperre war sie untauglich in beide Richtungen: "Vollmilch
Zimmertemperatur" gegen "H Vollmilch" kommt auf etwa 40 Punkte, weil das
lange Beiwort den Durchschnitt zieht - offensichtlich dasselbe Produkt.
"Mehl" gegen "Muehlen Geschnetzeltes" kam dagegen ueber 60.

Folge, die so gewollt ist: Wo nichts passt, steht jetzt "nicht zugeordnet"
und gar kein Vorschlag mehr. Ein Vorschlag, den niemand nachvollziehen
kann, ist schlimmer als keiner - er verleitet zum Durchklicken. Die
dauerhafte Loesung fuer echte Luecken ist die gemerkte Zuordnung
("Mealie-Lebensmittel X ist bei uns Y"), die noch aussteht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 13:18:27 +02:00
ScarriffleandClaude Opus 5 2247c8cf69 Rezepte aus Mealie gegen den Bestand pruefen
Neue Seite "Rezepte" (nur sichtbar, wenn Mealie eingerichtet ist): links
die Rezeptliste mit Suche, rechts das gewaehlte Rezept mit allen Zutaten
und der einen Frage, die zaehlt - kann ich das heute kochen, oder muss ich
vorher einkaufen? Je Zutat steht der Zustand: vorhanden, zu wenig, fehlt,
Menge unklar oder nicht zugeordnet; Fehlendes ist rot.

Die Portionenzahl laesst sich hochdrehen. Vorrania rechnet die Mengen dann
hoch, bevor es sie mit dem Bestand vergleicht - ein Rezept fuer 4, das man
fuer 8 kocht, braucht die doppelte Menge, und genau danach soll der
Abgleich gehen. Nennt das Rezept keine Portionszahl, wird nicht skaliert
und die Seite sagt das auch.

Drei Dinge entscheidet der Abgleich nacheinander (services/rezepte.py,
bewusst ohne HTTP, damit sich alles mit erfundenen Zutaten pruefen laesst):

1. Welcher Artikel ist gemeint? Unscharf ueber den Namen, gegen Artikel
   UND Gruppen - "Milch" ist markenuebergreifend gemeint, und genau dafuer
   gibt es Gruppen. Die Schwelle liegt hoeher als beim Kassenzettel: Dort
   sieht ein Mensch die Vorschlaege durch, hier darf nicht stillschweigend
   das Falsche als vorhanden gelten. Unter der Schwelle bleibt es bei
   "nicht zugeordnet" samt Kandidatenliste.
2. Wie viel ist das in Gramm? Ueber die Einheiten und die Hausmasse der
   Kategorie - "2 EL Zucker" wird zu 24 g. Fehlt das Mass fuer diese
   Kategorie, meldet die Zutat genau das zurueck (Massname und
   Kategorie-ID), damit daraus ein "Mass jetzt anlegen" werden kann.
3. Reicht der Bestand? Bei Gruppen ueber dieselbe gewichtete Summe wie in
   der Einkaufsliste, damit Untergruppen und Zweiteinheiten gleich zaehlen.

Die Fuzzy-Heuristik lag in routers/products.py und ist nach
services/matching.py gezogen: Der Rezeptabgleich stellt dieselbe Frage, und
zwei Kopien waeren mit der Zeit auseinandergelaufen. Der Kassenzettel-Scan
nutzt sie unveraendert weiter, test_receipt_match.py bleibt gruen.

Dazu zwei Dinge aus deiner Rueckmeldung zur Einstellungs-Karte:

- Der verbundene Zustand bleibt jetzt stehen, statt nur als Meldung
  aufzublitzen: Adresse, Version und "Dauertoken hinterlegt" samt den
  Knoepfen Pruefen / Zugang aendern / Trennen. Das Formular erscheint nur,
  solange nichts eingerichtet ist - ein Dauerzustand gehoert nicht in ein
  Eingabefeld, sonst sieht man nie, ob es geklappt hat.
- Leere Felder melden sich auf Deutsch ("Bitte das Passwort eingeben.")
  statt mit der rohen Schema-Meldung "String should have at least 1
  character", die niemandem sagt, welches Feld gemeint ist.

Der Knopf "Fehlende zur Einkaufsliste" fehlt noch: Vorranias Einkaufsliste
wird heute vollstaendig aus Mindestbestaenden gerechnet und hat keinen Ort
fuer einen eigenen Eintrag. Das ist der naechste Schritt; die Seite sagt es
auch so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 13:10:04 +02:00
ScarriffleandClaude Opus 5 7d8a54a3fc Mealie anbinden: Passwort einmal, danach nur noch ein Dauertoken
Unter Einstellungen gibt es jetzt eine Karte "Mealie (Rezepte)": Adresse,
Benutzername, Passwort. Vorrania meldet sich damit EINMAL an
(POST /api/auth/token), laesst sich von Mealie ueber
POST /api/users/api-tokens ein Dauertoken ausstellen und verwirft das
Passwort sofort - es steht in keiner Zeile der Datenbank, in keiner
Antwort und in keinem Fehlertext. Wer sein Passwort gar nicht erst
eintippen will, haekelt "Ich gebe lieber selbst ein Token ein" an.

Das Token liegt NICHT in den Einstellungen: GET /settings liefert jedem
angemeldeten Nutzer alle Zeilen im Klartext. Eine Ausschlussliste dort
waere beim naechsten Geheimnis vergessen, deshalb eine eigene Tabelle ohne
jeden Leseweg - setzen, loeschen, und die Auskunft "ist gesetzt". Was
keinen Ausgang hat, kann nicht durchrutschen. Ein Test prueft genau das.

Der Dienst folgt services/oidc.py, nicht off.py: Bei Open Food Facts ist
"kein Treffer" der Normalfall und wird still zu None, hier ist "Mealie
antwortet nicht" ein Fehler, den der Nutzer sehen muss. Die Kennungen
trennen auch, was verschieden ist - ein falsches Passwort (400) ist etwas
anderes als ein weggetretener Server (502).

Mealie 1.x legt die Haushalts-Routen unter /api/groups/, 2.x unter
/api/households/. Erkannt wird das zur Laufzeit mit einer Anfrage und
dann eine Stunde gepuffert; ueber die Versionsnummer waere es unzuverlaessig,
die melden Installationen uneinheitlich.

Rezeptliste und -abgleich kommen als Naechstes; dies ist der Teil, mit dem
sich der Server ueberhaupt erst verbinden laesst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 12:54:43 +02:00
ScarriffleandClaude Opus 5 37ab1ba47d Seitenleiste: "Listen" ist ein Eintrag wie jeder andere
Die Gruppe sah aus wie eine Abschnittsueberschrift - klein, versal,
gedaempft - und stand damit optisch neben der Leiste statt darin. Jetzt
traegt sie die Klasse .nav-link wie alle anderen Eintraege und misst sich
genauso; nur das Verhalten ist anders, sie klappt auf statt zu navigieren.

In der CSS bleiben deshalb nur die Eigenheiten, die ein <button> braucht.
Alles Sichtbare kommt von .nav-link, damit die Hoehe nicht auseinanderlaeuft,
sobald dort jemand etwas aendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 12:49:37 +02:00
ScarriffleandClaude Opus 5 c66bd0b09e Chargen: mehrere auf einmal ausbuchen, teilweise abbuchen oder loeschen
Die Mehrfachauswahl gab es schon, sie konnte aber nur den Lagerort setzen.
Jetzt kommen drei Aktionen dazu: komplett ausbuchen, von jeder Charge
dieselbe Teilmenge abbuchen, und loeschen als Korrektur.

Anders als bulk-location kein Sammel-UPDATE: Je Charge muss eine Bewegung
entstehen, sonst fehlte der Vorgang im Verlauf und in der
Verbrauchsstatistik. Schlaegt eine Charge fehl, weil sie zu wenig enthaelt,
wird alles zurueckgerollt - ein halb gebuchter Stand waere schlimmer als
gar keiner, dieselbe Haltung wie beim Stapel-Einlagern.

Die gemeinsame Teilmenge gibt es nur, wenn alle ausgewaehlten Chargen zum
selben Artikel gehoeren. "1 Packung" bedeutet bei Milch und Broetchen
Verschiedenes; der Server lehnt es ab, und die Oberflaeche sperrt das Feld
gleich mit einem Hinweis, statt den Fehler erst nach dem Klick zu zeigen.

Loeschen schreibt wie das einzelne Loeschen eine Korrektur-Bewegung
(adjust, negative Menge) statt einer Entnahme - es ist eine Fehleingabe,
kein Verbrauch, und darf die Verbrauchsstatistik nicht verfaelschen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 12:41:23 +02:00
ScarriffleandClaude Opus 5 a182037153 Seitenleiste: die vier Bestandslisten klappen zusammen
Lebensmittel, Gegenstaende, Einzelstuecke und Chargen stehen jetzt unter
einer Gruppe "Listen". Sie ist der laengste Block der Leiste und wird im
Alltag selten gebraucht - wer einlagert, nimmt die Knoepfe darueber.
Vorgabe ist zugeklappt, der Zustand bleibt im Browser (wie die
Spalteneinstellungen der Tabellen).

Steht man auf einer dieser Seiten, klappt die Gruppe auf, ohne den
gemerkten Zustand zu ueberschreiben - sonst saehe man nicht, wo man ist.

Die Eintraege bleiben dabei im Dokument und werden nur per Klasse
verborgen: Auf schmalen Bildschirmen wird die Leiste waagerecht, dort gibt
es keine Abschnitte und kein Platzproblem, also blendet die CSS sie wieder
ein und die Gruppe selbst aus. Waeren sie ausgehaengt, waeren sie dort weg.

Die Mobil-Regeln brauchen `.sidebar` davor: Ein Media-Query erhoeht die
Spezifitaet nicht, und die Grundregeln stehen weiter unten in der Datei -
ohne das gewinnt die spaetere Regel und die Ausnahme greift nie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 12:36:23 +02:00
ScarriffleandClaude Opus 5 afacf01f11 FEFO: angebrochene Packung immer zuerst
Ich hatte das nach dem frueheren wirksamen Datum sortiert, sodass eine
ungeoeffnete Packung mit knappem MHD vor einer offenen drankam. Das ist
falsch herum: Das MHD einer geschlossenen Packung ist konservativ und wird
real meist ueberlebt, eine offene verdirbt dagegen tatsaechlich. Und wer
die zweite aufmacht, waehrend die erste offen steht, hat am Ende zwei
angebrochene.

Angebrochenes kommt also immer zuerst; innerhalb beider Gruppen gilt
weiter FEFO ueber das naehere von MHD und Verbrauchsdatum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 12:33:02 +02:00
ScarriffleandClaude Opus 5 fa5a2bc4fa Angebrochene Packungen, Teilverbrauch und Hausmasse (Backend)
Eine Teilmenge auszulagern ging rechnerisch schon, aber es blieb keine Spur
davon: Eine Charge mit 900 von urspruenglich 1000 ml war von einer frisch
eingelagerten 900-ml-Charge nicht zu unterscheiden, und eine geoeffnete
Milch galt weiter bis zum aufgedruckten MHD.

Anbrechen spaltet jetzt eine Packung ab. Aus 4 x 1 l wird eine offene
Charge mit 1 l und eine ungeoeffnete mit 3 l - sonst truegen die
restlichen 3 l das kurze Verbrauchsdatum mit, obwohl sie noch zu sind. Der
Gesamtbestand aendert sich dabei nicht, es ist eine Umbuchung wie das
Aufteilen und schreibt wie dieses keine Bewegung.

Verbrauchen geht in einem Schritt: Wer "100 ml verbraucht" eintraegt,
bricht die Charge dabei an (abschaltbar ueber auto_open). Gerechnet wird
vor dem Anbrechen - scheitert die Einheit, soll keine Packung grundlos
offen stehen.

MHD und "zu verbrauchen bis" stehen nebeneinander und bleiben beide
sichtbar; gewarnt wird nach dem FRUEHEREN (Lot.faellig_am). Dafuer musste
in /expiring die Vorbedingung "best_before IS NOT NULL" weg, sonst fehlte
ausgerechnet die angebrochene Packung ohne aufgedrucktes MHD. ExpiringItem
traegt beide Daten plus warn_date und warn_source, und best_before ist dort
jetzt optional.

Die Frist kommt aus drei Ebenen: Artikel schlaegt Kategorie, Kategorie
schlaegt ueber ihre Oberkategorien hinweg den globalen Wert. Der Helfer
dafuer (services/vererbung.py) liest die Kategoriekette von
services/fields.py rueckwaerts - dort werden Felder ALLER Vorfahren
gesammelt, hier gewinnt der naechste. Dieselbe Mechanik traegt die
Hausmasse.

Hausmasse sind der zweite Teil: "2 EL Mehl" liess sich bisher nicht
buchen, weil eine globale Einheit "Essloeffel" bei Mehl und Zucker
zwangslaeufig gleich schwer waere. Der Name ist deshalb global, die Menge
haengt an Kategorie und Artikel. to_base loest sie nur mit hausmasse=True
auf - beim Auslagern und Verbrauchen ja, beim Einlagern und bei
Mindestbestaenden ausdruecklich nicht, denn ein Loeffel ist ein
Schaetzwert. Verwaltete Einheiten werden zuerst geprueft, damit ein Mass
namens "Liter" die echte Einheit nicht verdeckt.

Zwei Stellen waeren sonst still kaputtgegangen:

- Die Konsolidierung haette die offene Packung beim naechsten Einlagern am
  selben Ort einkassiert und ihr Verbrauchsdatum verschluckt. Angebrochene
  Chargen werden deshalb nie zusammengefasst - in consolidate_lot_group
  und im Start-Aufraeumer, beide bisher ohne jeden Test. Das Netz dafuer
  ist jetzt da (test_konsolidierung.py).
- FEFO sortiert nach dem wirksamen Datum statt nur nach dem MHD. Eine
  offene Packung kommt damit meist von selbst zuerst, aber eine
  ungeoeffnete, die morgen ablaeuft, geht vor - Verderb schlaegt
  Bequemlichkeit. Erst bei gleichem Datum wird das Angebrochene vorgezogen.

Dashboard bekommt "angebrochen" als fuenften Zustand, mit Vorrang
abgelaufen > bald > angebrochen > in Ordnung: Was offen ist und morgen
ablaeuft, gehoert in die Warnung, nicht in den ruhigeren Zustand.

Oberflaechen in Web und App folgen in eigenen Commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 11:25:36 +02:00
ScarriffleandClaude Opus 5 254737086e Sitzungsdauer einstellbar, bis hin zu unbegrenzt
Die Anmeldung galt fest 7 Tage - und die Frist lief ab dem *Anmelden*, nicht
ab der letzten Nutzung. Wer die App taeglich benutzte, flog trotzdem
punktgenau nach einer Woche hinaus.

Beides ist jetzt geregelt:

- Einstellungen -> Anmeldung: 7 Tage bis 1 Jahr oder unbegrenzt. Gilt fuer
  Website und App gemeinsam, weil beide dasselbe Token bekommen - auch nach
  Single Sign-on.
- POST /auth/refresh schiebt die Frist beim Start nach vorn. Damit zaehlt die
  eingestellte Dauer ab dem letzten Besuch, und eine geaenderte Einstellung
  greift ohne Neuanmeldung.

"Unbegrenzt" heisst ein Token ganz ohne exp. Das ist bewusst folgenreich: Es
gibt keine Sperrliste, eine erteilte Anmeldung laesst sich nicht einzeln
zurueckziehen. Die Oberflaeche sagt das dazu und nennt den einzigen Ausweg
(JWT_SECRET aendern).

/auth/refresh nimmt bewusst nur JWTs an, keine langlebigen API-Token: aus
einem jederzeit widerrufbaren Zugang (Home Assistant) waere sonst ein
unwiderruflicher geworden.

Der Rueckfallwert aus der Umgebung steigt von 7 auf 30 Tage - er gilt nur noch,
solange nichts eingestellt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 23:15:46 +02:00
ScarriffleandClaude Opus 5 2b9b98dd14 Etiketten-Vorlagen: die ganze Zeile klappt auf und zu
Aufklappen ging nur ueber ein kleines Stift-Symbol am rechten Rand - und dass
derselbe Knopf auch wieder schliesst, sah man ihm nicht an.

Jetzt ist die Zeile selbst der Schalter, mit einem Pfeil links, der den Zustand
zeigt (wie in der Einzelstueck-Tabelle). Der Stift faellt damit weg; die
Knoepfe rechts - CSV, .lbx, Loeschen - bleiben eigenstaendig und loesen das
Auf- und Zuklappen nicht mit aus.

Dafuer eine kleine Klasse .zeilen-schalter: .link-btn waere naheliegend gewesen,
faerbt aber alles im Akzent - Name und graues Beiwerk sollen ihre Farben
behalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:30:03 +02:00
ScarriffleandClaude Opus 5 c741df5963 Etiketten-Vorlagen: gespeicherte Exporte mit frei waehlbaren Spalten
Der Etikettenexport konnte genau eine Sache, und zwar unveraenderlich: sechs
feste Spalten ueber Einzelstuecke. Was fehlte, war genau das, was ein Etikett
brauchbar macht - die eigenen Felder. Eine Kabellaenge oder die Kapazitaet einer
Powerbank steht laengst am Artikel, kam aber in keiner CSV vor. Und jede
Zusammenstellung war beim naechsten Mal wieder weg: Kategorien im React-State,
Trennzeichen im localStorage, Spalten hartkodiert.

Jetzt gibt es benannte Vorlagen mit eigener Spaltenwahl und Reihenfolge, fuer
Einzelstuecke, Artikel und Lagerorte. Eine Tabelle nach dem Vorbild von
DashboardLayout: benannt, Nutzlast als JSON-Text. Ohne user_id - eine Vorlage
gehoert allen, nicht einem Konto.

Spaltenschluessel fuer eigene Felder ist die ID, nicht der key: FieldDefinition.key
ist nur JE KATEGORIE eindeutig, zwei Kategorien duerfen beide "kapazitaet" fuehren.

Die CSV entsteht jetzt im Server statt im Browser. Mit waehlbaren Spalten,
eigenen Feldern und drei Bezugsarten gehoert das an eine Stelle - und zwar
dorthin, wo es sich pruefen laesst. Formel-Injektionsschutz, BOM und CRLF sind
aus der Browser-Fassung mitgenommen. Der Grund, aus dem der QR-Inhalt bisher im
Browser entstand ("nur die Oberflaeche kennt die oeffentliche Adresse"), gilt
allein fuer Lagerorte; dafuer reicht sie ihre Herkunft als Parameter durch.

Die beiden alten Abschnitte weichen den Vorlagen. Beim ersten Start entstehen
zwei mitgelieferte, die genau die bisherigen Spalten fuehren - wer nichts
einstellt, bekommt dasselbe Ergebnis wie vorher.

Dazu laesst sich eine P-touch-Datei hinterlegen. Geprueft wird ueber Endung und
ZIP-Inhalt (label.xml), nicht ueber den Content-Type: Fuer .lbx meldet der
Browser meist application/octet-stream. Eine .lbx merkt sich den Pfad zu ihrer
Datenquelle - auf einem fremden Rechner muss die CSV in P-touch einmal neu
verknuepft werden. Das laesst sich von hier aus nicht loesen; die Datei
dabeizuhaben erspart trotzdem das Suchen.

Sicherung auf Version 4: Vorlagen kommen mit, Kategorien als Pfad und eigene
Felder als Label, weil IDs zwischen zwei Instanzen nicht dieselben sind. Die
.lbx als Base64 - das erste Binaerformat in dieser Sicherung, vertretbar bei
wenigen Kilobyte und einer Vorlage, die ohne ihre Datei nur eine halbe waere.

23 neue Tests (343 gesamt), dazu im Browser durchgespielt: Kabel-Kategorie mit
eigenem Feld, Spalte hinzugefuegt, CSV mit "Länge (m)" und dem Wert, .lbx
hoch- und wieder heruntergeladen (Pruefsumme identisch), PNG und Nicht-ZIP
sauber abgelehnt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:07:49 +02:00