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>
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>
"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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Die Liste war eine Sackgasse - man sah einen Namen und eine UID, kam aber
nirgends hin. Jetzt fuehrt der Name zum Artikel und die UID zu genau diesem
Stueck; beide Fragen kommen vor ("was ist das fuer ein Netzteil?" gegen "wo ist
dieses eine?").
Dazu der Lagerort je Teil. ItemKurz trug ihn schon, angezeigt wurde er nie -
dabei ist das bei Zubehoer die haeufigste Frage ueberhaupt.
Ab zwei Teilen nimmt der Block zwei Rasterspalten. In einer einzigen Spalte
(minmax 240px) brach schon ein mittellanger Name um.
Und ein Fehler von mir: Die "Gehoert zu"-Zeile war ein rohes <a href> und lud
damit die ganze Seite neu, statt im Router zu navigieren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
.clean-list li bricht bewusst um (flex-wrap), damit lange Inhalte nicht aus der
Karte laufen. In der Zubehoer-Liste traf das den Loesen-Knopf: Bei einem Namen
wie "Shooter Grip fuer WII Controller" landete das x allein in einer zweiten
Zeile unter dem Namen.
Jetzt waechst der Textblock (flex:1, min-width:0) und alles rechts davon bleibt
stehen. Der Name bricht innerhalb seines Blocks um, das x bleibt rechts und
mittig. overflow-wrap: anywhere fuer den Fall, dass ein einzelnes Wort breiter
ist als die Spalte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Im aufgeklappten Bereich stehen die Beschriftungen ueber den Feldern. Beim
Auswahlfeld "Gekauft bei" klebte sie stattdessen links daneben, waehrend
"Gekauft am" und "Garantie bis" darueber standen - die Zeile sah dadurch
verrutscht aus.
Ursache war nicht das Markup, das ist identisch zu den Nachbarn, sondern
td select { width: auto } (styles.css). Die Regel ist fuer die kompakte
Tabellenzeile richtig, im Detailbereich schrumpft sie das Feld aber auf
Inhaltsbreite, sodass es neben seine Beschriftung passt.
Nur direkte Kinder eines label: Waehrung und Preisvorschlag sitzen in eigenen
Zeilen-Behaeltern und bleiben schmal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Umschalter mit zusammen fuenf Knoepfen beanspruchten zwei volle Zeilen im
Formular - der dreifache ("Menge je Lagerort / Einzelstuecke /
Verbrauchsgegenstand") brauchte dafuer sogar eine eigene Umbruch-Regel. Als
Auswahllisten stehen sie kompakt untereinander und sehen aus wie die
Kategorie-Auswahl direkt dazwischen.
Am Verhalten aendert sich nichts: changeModus und setObjMode bekommen denselben
Wert, nur aus onChange statt onClick.
.seg-wrap wird dadurch nirgends mehr gebraucht und faellt weg. .segmented und
.seg-label bleiben - die nutzen Mindestbestaende, Produkte und Kategorien
weiterhin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RUECKGABE
Der Knopf war da, nur nicht zu finden: ein 16-px-Symbol in einer Spalte ohne
Ueberschrift, 52 px breit, ganz rechts in einer horizontal scrollenden Tabelle.
Beschriftet war er allein ueber title= - ein Tooltip, den auf einem Touchgeraet
nie jemand sieht.
Jetzt traegt er Text ("Zuruecknehmen"), die Spalte heisst "Verleih" und ist
breit genug, und beide Knoepfe haben ein aria-label. Dazu eine neue Seite
"Verliehen" in der Seitenleiste: alles Draussene an einem Ort, Rueckgabe direkt
aus der Liste. Bisher gab es diese Uebersicht nur als zwei Dashboard-Karten, und
die stehen nicht im Standard-Layout - wer sie nicht selbst hinzufuegte, hatte
gar keine.
In der App fuehrte der einzige Weg ueber das Bearbeiten-Formular des Stuecks;
jetzt geht es per Wischgeste und Kontextmenue direkt aus der Liste. Der
Verleih-Filter dort sass hinter einem nackten Symbol und ist beschriftet.
Zurueckgenommen wird nur noch nach Rueckfrage, in Web und App.
ZUBEHOER
Ein Netzteil gehoert zu einem Laptop: items.parent_item_id, genau ein Geraet je
Teil, beliebig viele Teile je Geraet. Eine Spalte genuegt dafuer, es braucht
keine Verknuepfungstabelle. ON DELETE SET NULL, damit das Netzteil den Laptop
ueberlebt statt mitgeloescht zu werden - und weil SQLite Fremdschluessel nicht
erzwingt, loest der Router die Zuordnung beim Loeschen zusaetzlich selbst.
Ringe (A traegt B, B traegt A) weist die Zuordnung mit 409 ab; die Elternkette
wird dafuer hochgelaufen, wie beim Kategoriebaum.
Die Beziehung laeuft ueber eigene Endpunkte und bewusst NICHT ueber ItemUpdate:
dessen Schema sendet immer alle Felder mit, auch leere, damit sich Felder
loeschen lassen. Genau so gingen schon einmal Kaufpreis und Waehrung verloren,
als ein Aufrufer ein Feld vergass.
Beim Verleihen fragt die Oberflaeche, welche Teile mitgehen (vorausgewaehlt:
alles, was im Haus ist), beim Zuruecknehmen kommen sie zusammen zurueck. Ohne
das Spiegelbild kaeme das Geraet zurueck und sein Netzteil bliebe ewig
"verliehen", weil niemand mehr danach sucht. Ungefragt nimmt der Server nie
etwas mit.
In der App sind zubehoer und gehoert_zu optional dekodiert: Die App spricht mit
mehreren Serverprofilen, und ein aelterer Server schickt die Felder nicht - ein
Vorgabewert reicht dafuer nicht, Swift nimmt ihn beim Dekodieren nicht her.
12 neue Tests (320 gesamt). Der Umlauf ist ausserdem gegen ein lokales Backend
im Browser durchgespielt: verleihen mit Zubehoer, Rueckfrage, beide zusammen
zurueck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Tabelle trug neun Spalten, davon sechs als dauernd offene
Eingabefelder - bei einem einzigen Stueck schon eine Wand aus
Bedienelementen. Jetzt stehen in der Zeile QR, UID, Lagerort, "Verliehen
an" und die Aktionen; Kaufdatum, Garantie, Shop, Notiz, Kaufpreis, Belege
und der Verleih-Verlauf klappen je Stueck auf. Aenderbar bleibt alles,
nur nicht alles gleichzeitig sichtbar.
Dazu zwei Fehler von mir aus dem Verleih-Commit:
- Die drei Aktionssymbole standen uebereinander statt nebeneinander. Die
Aktionszelle benutzte .field-inline, und das bricht in Tabellen bewusst
um (styles.css) - mit dem vierten Knopf reichte der Platz nicht mehr.
Jetzt .cell-row, das umbricht nicht.
- "ueberfaellig (vor 1 Woche)" las sich als Zeitpunkt, gemeint ist eine
Dauer. Jetzt "7 Tage ueberfaellig".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verleihen und Zuruecknehmen in der Einzelstueck-Tabelle eines Artikels und
in der Liste aller Einzelstuecke, dort als Spalten "Verliehen an" und
"Zurueck bis" (filter- und sortierbar). Ueberfaellig ist rot, demnaechst
faellig gelb - dieselben Zustandsfarben wie beim MHD.
Die Scan-Zielseite /i/<uid> leitete vorher nur zum Artikel weiter. Fuer
"scannen und verleihen" braucht die Aktion aber genau dort ihren Platz:
Die Seite zeigt jetzt, was man in der Hand haelt - Stueck, Artikel,
Lagerort, Verleihzustand - und bietet Verleihen bzw. Zuruecknehmen an.
Gegenstueck zur Lagerort-Seite /l/<code>, die es fuers Einlagern genauso
macht.
Anzeige und Aktionen liegen einmal in components/Verleih.jsx, weil alle
drei Stellen dasselbe brauchen. Der Knopf haengt nicht an isAdmin:
Verleihen ist eine Buchung wie Ein- und Auslagern.
Dazu zwei Dashboard-Karten: alles Verliehene und, getrennt, nur das
Ueberfaellige. Stuecke ohne Rueckgabedatum sind nie faellig und tauchen in
der zweiten Karte nicht auf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Sitzungs-Token benannte den Benutzer ueber seinen Namen (sub = username).
Damit war der Name die Kennung der Sitzung - ein Umbenennen haette jede laufende
Anmeldung dieses Benutzers ungueltig gemacht, und deshalb war es gar nicht erst
vorgesehen. Beim Anmeldedienst geaenderte Namen liessen sich in Vorrania
folglich nirgends nachziehen.
Neue Token tragen die ID (sub = "<id>", zusaetzlich uid), und deps.py loest die
Sitzung darueber auf. Alles andere haengt ohnehin schon an der ID: Bewegungen,
Dashboards, API-Tokens. Der Name ist damit reine Anzeige.
Benutzer lassen sich jetzt unter "Benutzer" umbenennen - auch sich selbst, ohne
aus der Sitzung zu fliegen. Der Anmeldeweg selbst sucht weiterhin ueber den
Namen; das ist das Feld im Formular und soll so bleiben.
Bestandsschutz: Token ohne uid werden weiter ueber den Namen aufgeloest, damit
beim Update niemand abgemeldet wird. Der Zweig darf nach Ablauf der
Token-Lebensdauer weg. Das zusaetzliche uid unterscheidet alt von neu eindeutig -
ein Benutzer koennte schlicht "42" heissen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wer sich zum ersten Mal ueber den Anmeldedienst anmeldet und hier noch kein
Konto hat, bekam bisher wortlos ein neues - mit Rolle "Benutzer" und ohne
Verlauf. Wer schon ein Konto hatte, wollte aber fast immer genau das verknuepfen
und stand dann mit zwei Konten da.
Jetzt zeigt der Rueckweg eine Entscheidungsseite: bestehendes Konto verknuepfen
oder ein neues anlegen. Das Verknuepfen verlangt einmalig Benutzername und
Passwort des bestehenden Kontos. Genau dieser Nachweis ist der Grund, warum der
Anmeldeweg selbst nie fragen darf, welches Konto gemeint ist - ohne ihn koennte
sich jeder mit einem beliebigen Konto beim Anmeldedienst an ein fremdes
Vorrania-Konto haengen.
Die geprueften Angaben liegen dafuer kurzlebig und einmalig in
oidc_pending_links; vom Code steht dort nur der SHA-256-Hash, wie bei den
API-Tokens. Ein falsch getipptes Passwort verbraucht den Vorgang nicht, und
unbekannter Benutzername und falsches Passwort antworten gleich - sonst liessen
sich vorhandene Konten abzaehlen.
AUTO_CREATE=false blendet den Knopf zum Anlegen aus und laesst sich auch ueber
den Dialog nicht umgehen. Der App-Weg bleibt unveraendert, weil er keinen
Zwischenschritt anzeigen kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg:
OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration
aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher.
Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania
sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die
Rollenpruefung und die langlebigen API-Tokens unveraendert.
Backend
- Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein
zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung.
- Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE)
enden beide im selben Token-Schema.
- state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem
Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit
allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt.
Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf
bleibt mit der db-Fixture pruefbar.
- Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die
BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein
gleichlautender preferred_username uebernimmt nie ein bestehendes Konto.
- users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password
faengt das ab, sonst antwortete der Login mit 500 statt 401.
Verknuepfen bestehender Konten
- Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen
und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es
braucht also weder eine hinterlegte Adresse noch Admin-Rechte.
- Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token
nicht mitschicken kann.
- Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen.
iOS
- AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem
eingebetteten WebView, und ohne Client-Secret.
- offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und
bekaeme sonst einen ueberfluessigen Zustimmungsdialog.
- Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App
still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung.
Deployment
- docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit
fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht
erfordern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warum eine angeklickte Artikelseite nur zur Haelfte lud: Die Bild-Route wird je
Tabellenzeile einmal aufgerufen, und ein Browser haelt je Host nur rund sechs
Verbindungen offen. Was diese Route aufhaelt, legt die ganze Oberflaeche lahm.
Sie hielt gleich dreifach auf:
1. Fehlte die lokale Kopie, holte ``images.ensure`` das Bild MITTEN in der
Anfrage von der fremden Adresse - bis zu 15 Sekunden, je Zeile, und bei
jedem Aufruf erneut, weil ein Fehlschlag nirgends vermerkt wurde. Artikel
aus einem Backup-Import haben genau diese Ausgangslage. Gemessen: 2,01 s
je Aufruf, dreimal hintereinander. Jetzt: 0,01 s, das Nachholen laeuft nach
der Antwort im Hintergrund und ein toter Link wird eine Stunde gesperrt.
2. Ein Vorschaubild ist 34x34 Pixel gross - ausgeliefert wurde das Original.
Gemessen: 2530 KB je Briefmarke, bei 40 Zeilen 99 MB fuer eine Liste. Neu
erzeugt die Bild-Route (Pillow) eine Vorschau und legt sie daneben ab;
``?thumb=1`` liefert sie aus. Gemessen: 0,6 KB.
3. If-None-Match wurde ignoriert. Nach den 5 Minuten Cache-Frist lud der
Browser jedes Bild komplett neu - das erklaert, warum es "alle 20 Minuten"
wieder losging. Jetzt 304 ohne Daten.
Dazu im Web: laufende Bildabrufe werden beim Seitenwechsel abgebrochen (sonst
steht die neue Seite hinter Bildern Schlange, die niemand mehr sieht), und wo
``image_version`` bekannt ist, entfaellt die Anfrage fuer bildlose Artikel ganz.
Und der Verbindungsvorrat der Datenbank: 5 (+10) gegen 40 Arbeits-Threads von
FastAPI. Ab der 16. gleichzeitigen Anfrage wartete eine Route stillschweigend
30 Sekunden - von aussen ein haengender Server. Jetzt 10 (+20) mit 10 Sekunden
Frist: lieber ein Fehler als eine halbe Minute Stille.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Gruppen-Spalte fehlte im Spalten-Menue - aus zwei Gruenden, und der zweite
ist der ernstere.
1) order wurde EINMAL beim Mounten berechnet: gespeicherte Reihenfolge plus
alles, was zu dem Zeitpunkt in columns stand. Aendert eine Seite ihre Spalten
danach - die Artikelliste zeigt "Verwaltung" nur bei Gegenstaenden -, fehlt die
neue Spalte in order und faellt damit aus allOrdered heraus. Sie ist dann nicht
bloss aus dem Menue verschwunden, sondern aus der TABELLE. Ein useEffect zieht
fehlende Schluessel jetzt nach; angehaengt, nicht einsortiert, damit eine selbst
gewaehlte Reihenfolge nicht umspringt.
2) Die Liste im Menue war fest auf 240px begrenzt. Bei zehn Spalten stand der
letzte Eintrag ausserhalb, und weil macOS die Bildlaufleiste ausblendet, sah es
aus, als gaebe es ihn nicht. Die Hoehe richtet sich jetzt nach dem freien Platz
bis zum unteren Bildrand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein- und Auslagern sind ein Paar und duerfen sich nur in EINEM Merkmal
unterscheiden - der Pfeilrichtung. Tatsaechlich sprang auch die Linie: beim
Einlagern lag sie unten und der Pfeil zeigte auf sie zu (richtig), beim
Auslagern lag sie oben und der Pfeil zeigte ebenfalls auf sie zu. Das las sich
wie "hinein, nur nach oben" statt wie "heraus".
Jetzt liegt die Linie bei beiden unten - sie ist das Lager - und nur der Pfeil
dreht sich: hinein beim Einlagern, weg beim Auslagern. Das ist auch die
Konvention der Vorlage (Feather download/upload), an der sich die uebrigen
Icons orientieren.
Wirkt ueberall, wo das Icon steht: Navigation, Ein-/Auslagern-Knoepfe,
Schnellbuchung, Umlagern, Export und die Verlaufsansicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die beiden Kisten unterschieden sich nur in einem kleinen Pfeil. Bei 18 px war
das nicht zu trennen - erkannt hat man sie allein an der Farbe, und die war
gerade wegen der Wertung (gruen rein, orange raus) rausgeflogen. Ohne Farbe
blieb nichts uebrig, woran man sie auseinanderhaelt.
Jetzt die Strich-Icons checkin/checkout/edit: grosser Pfeil auf eine Linie zu
bzw. von ihr weg, Stift fuer die Korrektur. Dieselben Zeichen wie auf den
Ein-/Auslagern-Knoepfen, also schon vertraut, und auch klein eindeutig - die
Richtung liegt im Zeichen statt in der Farbe.
Auf der Verlaufsseite steht die Spalte jetzt an erster Stelle; sie sagt in
einem Blick worum es geht und braucht als Zeichen kaum Breite.
Der gefuellte Icon-Zweig in Icon.jsx wird damit nicht mehr gebraucht und ist
raus. Die SVG-Quellen bleiben unter brand/icons/ liegen - das README sagt jetzt
ausdruecklich, dass sie ungenutzt sind, warum, und wie man sie zurueckholt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gruen fuer Einlagern und Orange fuer Auslagern las sich wie gut und schlecht -
dabei ist Auslagern schlicht der Normalfall, kein Missstand. Die Farbe kam noch
aus der Badge-Zeit, wo sie neben dem Wort stand; als alleiniges Signal wertet
sie.
Alle drei Arten tragen jetzt dieselbe neutrale Kachel. Unterschieden wird ueber
die Pfeilrichtung im Icon, und die Bedeutung steht weiterhin in title und
aria-label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die drei SVGs lagen in der Repo-Wurzel und hiessen noch wie beim Download
("open-box (1).svg" – inklusive Leerzeichen und Klammer). Durch mein git add -A
im letzten Commit sind sie dort auch gelandet.
Jetzt liegen sie in brand/icons/ und heissen wie die Icons im Code:
einlagern.svg, auslagern.svg, korrektur.svg. Ein README daneben haelt fest,
wofuer jede da ist - die beiden Kisten unterscheiden sich nur in der
Pfeilrichtung, wer sie verwechselt dreht Ein- und Auslagern um, ohne dass etwas
kaputtgeht - und wie man die Pfaddaten nach einer Aenderung neu uebernimmt.
Zur Laufzeit laedt niemand diese Dateien: die Pfade stehen inline in Icon.jsx,
damit die Icons ueber currentColor die Textfarbe annehmen. Die Originale tragen
festes Schwarz.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Eingelagert"/"Ausgelagert"/"Korrektur" standen als Textbadge in einer eigenen
Spalte - in der schmalen Ansicht als dritte beschriftete Zeile je Eintrag. Jetzt
eine getoente Kachel mit Icon: Kiste mit Pfeil hinein, Kiste mit Pfeil heraus,
Stapel mit Stift (aus open-box (1).svg, open-box.svg, edit-server.svg). Die
Farbe traegt dieselbe Bedeutung wie vorher das Badge - gruen rein, orange raus,
neutral Korrektur; Titel und aria-label behalten das Wort.
Die Dashboard-Karte bekommt dabei das Layout aus der Skizze: Kachel links,
Zeitpunkt ueber Produkt, ohne Tabelle. Die Verlaufs-SEITE bleibt eine Tabelle -
dort stehen noch Menge und Benutzer -, dort weicht nur das Wort der Kachel.
Die Icon-Komponente kann jetzt auch GEFUELLTE Grafiken mit eigenem viewBox; die
vorhandenen sind Strichzeichnungen (fill:none, stroke:currentColor) und
vertragen sich damit nicht. Die Pfaddaten sind per Skript aus den SVGs
uebernommen, nicht abgetippt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Im ganzen Frontend gab es keine einzige Error Boundary. In React 18 haengt eine
Ausnahme beim Rendern damit den KOMPLETTEN Baum aus: die Seite wird schlagartig
weiss und nur ein Neuladen hilft. Genau das gemeldete Bild - "keine Karten",
"gar nichts", "muss F5 druecken". Schlimmer noch: man sah nicht, WAS passiert
ist, und konnte es folglich auch nicht melden.
Zwei Ebenen: eine aeussere in main.jsx als letzte Instanz (faengt auch Fehler in
den Providern) und eine je Seite in Protected, damit bei einem Absturz
wenigstens die Navigation bedienbar bleibt. Die innere haengt am Pfad, sonst
haenge man nach einem Fehler dauerhaft in der Fehleranzeige fest.
Angezeigt wird die Meldung selbst, nicht nur ein "Ups", plus die technischen
Angaben zum Aufklappen. Damit ist der naechste Vorfall in einer Minute
eingeordnet statt in einer Stunde.
Das erklaert die Haeufigkeit, die meine vorherige Erklaerung (abgelaufene
Anmeldung) nicht hergab: ein Token gilt sieben Tage, nicht zwanzig Minuten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Defekte, die zusammen das gemeldete Muster ergeben.
1) Es gab keine zentrale Behandlung von HTTP 401. AuthProvider prueft die
Anmeldung EINMAL beim Start; laeuft das Token danach ab, bleibt der Zustand
"angemeldet" stehen, waehrend jede Abfrage scheitert. Seiten mit
.catch(() => {}) (18 Stellen) zeigen dann schlicht nichts, andere eine
englische Servermeldung - genau das "mal gar nichts, mal die Haelfte".
Jetzt raeumt der API-Client bei 401 die Sitzung auf und der Login sagt, dass
sie abgelaufen ist. Beim Anmelden selbst greift das nicht, dort heisst 401
"Passwort falsch".
2) Der Start hatte ein blankes `catch`, das das Token bei JEDEM Fehler loeschte -
auch bei 502 oder weggebrochenem Netz. Wer waehrend eines Server-Neustarts F5
drueckte, war deshalb abgemeldet, obwohl die Sitzung voellig in Ordnung war.
Das duerfte der haeufigere Ausloeser gewesen sein, weil in letzter Zeit oft neu
deployt wurde. Jetzt wird nur bei 401/403 abgemeldet; bei allem anderen bleibt
die Anmeldung liegen und es erscheint "Server nicht erreichbar" mit einem
Knopf zum erneuten Versuchen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Eingabefelder auf der Mindestbestaende-Seite standen je Zeile woanders. Die
Zeile war ein .field-inline, und dort waechst sowohl der Name als auch das
Eingabefeld (.field-inline > input { flex: 1 1 auto }) - beide teilten sich den
Restplatz, also verschob ein laengerer Name das Feld nach rechts und machte es
schmaler. Jetzt ist die Zeile ein Raster mit festen Spalten (Name, Menge,
Einheit, Bestand, Ort, Loeschen); ohne Adminrechte bleiben die letzten beiden
Zellen leer, damit die Zeilen trotzdem fluchten. Schmale Fenster stellen den
Namen ueber die volle Breite.
Die "Details"-Spalte liess sich nicht verschieben: sie war fixed und hatte
zusaetzlich einen leeren Kopf - es gab also nichts zu greifen. Sie ist jetzt
frei und traegt einen sichtbaren Titel. Dasselbe fuer die gleichartigen
Link-Spalten in der Einzelstueck- und der Chargenliste ("Artikel",
"Aufteilen"). Sortieren bleibt dort aus, weil toggleSort ohne sortValue
ohnehin aussteigt. Bild- und Auswahlspalten bleiben fest - die haben keinen
sinnvollen Titel und gehoeren an den Anfang.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Gruppen-Dropdowns waren flache, alphabetische Listen - man sah nicht, dass
"Grillwurst" unter "Wurst" haengt. Ein gemeinsamer Helfer (gruppenOptionen)
rollt den Graphen zum Baum aus und rueckt ein; eine Gruppe mit mehreren
Obergruppen erscheint unter jeder, denn genau das ist ihr Sinn, und ausgewaehlt
wird ohnehin dieselbe ID.
Eingerueckt wird mit GESCHUETZTEN Leerzeichen - normale fasst der Browser in
einem <option> zusammen, die Einrueckung waere sonst wirkungslos.
Betroffen: Gruppen-Auswahl im Artikelformular (beide Zweige), beim Einlagern,
die Ziel-Auswahl der Mindestbestaende und die Obergruppen-Mehrfachauswahl (die
beim Suchen bewusst abflacht, weil eine Einrueckung ohne die Eltern in die Irre
fuehrt). In der App dasselbe in beiden Gruppen-Pickern.
Die Gruppen-Spalte der Artikelliste zeigt jetzt den Weg von oben statt eines
Etiketts - "Wurst → Grillwurst", mit gedaempften Vorfahren wie bei der
Kategorie. Gefiltert wird ueber ALLE Wege: wer auf "Wurst" filtert, sieht auch
die Artikel aus den Untergruppen. Dafuer gibt es gruppenInfoMap als Gegenstueck
zu categoryInfoMap, das mit mehreren Wegen je Gruppe umgehen kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Seite hatte die Tabelle links und eine feste rechte Spalte (24% der
Breite). Seit die Tabelle Obergruppen, Bestand mit Einheit und die
Gebinde-Erlaeuterung traegt, war die rechte Spalte teurer als sie half - und sie
stand auch dann da, wenn gar keine Gruppe ausgewaehlt war.
Jetzt bekommt die Tabelle die volle Breite. "Neue Gruppe" steht darunter, die
Karten zur ausgewaehlten Gruppe (EAN-Codes, Mindestbestand) erscheinen beim
"verwalten" ebenfalls darunter, nebeneinander. Kein Springen des Layouts beim
Auswaehlen, weil die Tabelle ihre Breite nie aendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Anzeigefehler: die Gruppenliste klebte immer min_stock_unit_name hinter den
Bestand - auch wenn die Gruppe laengst im Gruppen-Gebinde zaehlt. Aus 790 g bei
einem 190-g-Glas wurde so "4,16 Gramm" statt "4,16 Glaeser". Gerechnet war
richtig, beschriftet falsch.
Ursache war doppelte Logik: MinStock.jsx hatte grpPkgMode/grpFactor/grpUnit
lokal, Groups.jsx gar nicht. Die drei stehen jetzt in units.js und werden von
beiden benutzt. Die Einheiten-Spalte sagt ausserdem, wenn eine Gruppe im
Gebinde zaehlt ("zaehlt in Glaeser a 190 g") und bietet den Weg zurueck an.
Gruppenliste als Baum: eine Gruppe mit zwei Obergruppen erscheint unter beiden -
das ist die ehrliche Darstellung eines Graphen. Jede Zeile traegt ihren PFAD als
Schluessel, damit die vorhandene Baumlogik der DataTable unveraendert zurecht
kommt; die kennt nur einen Elternteil je Zeile, und ein Pfad hat genau einen.
Beim Sortieren/Filtern flacht sie wie gewohnt ab.
"1 Untergruppen" heisst jetzt "1 Untergruppe" - an allen vier Stellen ueber
einen gemeinsamen Helfer (anzahlWort), Web und App.
Lebensmittelliste bekommt eine Spalte "Gruppe" mit Filter. Dafuer liefert
ProductOut jetzt group_name, symmetrisch zu category_name, mit joinedload gegen
N+1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Im Grillwurst-Fall stand dieselbe Zahl zweimal im Formular, mit zwei
Bedeutungen und in identischer Aufmachung: "Packungsgroesse 3" (1 Packung = 3
Stueck) und "Zweiteinheit 3 Stk ≙ 250 g" (3 Stueck wiegen 250 g). Dazu sass der
Zweiteinheit-Block zwischen Gebinde/MHD und Mindestbestand/Gruppe, hiess nach
einem Fachbegriff und benutzte ≙ statt eines Satzes.
Jetzt steht alles zur Einheit unter einer Ueberschrift und liest sich als Satz:
"Gezaehlt wird in Stueck", "Gebinde Packung", "1 Packung sind 3 Stueck". Das
Gebinde steht dafuer vor der Packungsgroesse - die Zeile darunter nennt es beim
Namen.
Die Umrechnung heisst nach dem, was sie liefert (Gewicht / Volumen / Stueckzahl)
und beginnt mit einer einzigen Auswahl; erst danach erscheint der Satz "3 Stk
wiegen 250 g" mit passendem Verb und Ein-/Mehrzahl. Die linke Zahl wird beim
Waehlen der Zielart EINMALIG mit der Packungsgroesse vorbelegt - zu tippen
bleibt nur die 250. Bewusst keine dauerhafte Kopplung: wer die Packung spaeter
auf 4 aendert, hat dieselbe Wurst, und ein mitgezogener Wert wuerde
stillschweigend behaupten, sie sei leichter geworden.
MHD-Angabe wandert aus dem Einheiten-Block zur Gruppen-Zeile; sie stand nur
zufaellig neben dem Gebinde.
Reine Oberflaeche - Datenmodell, API und Backend-Tests unveraendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Im Artikel-Formular eine Zeile "3 [Stk] ≙ 250 [Gramm]" mit ausgerechneter
Kontrolle darunter ("1 Stk ≈ 83,33 g"). Links steht die Basiseinheit des
Artikels fest, rechts stehen nur die anderen Arten zur Wahl - eine Bruecke auf
die eigene Art haette nichts umzurechnen. Nur vollstaendig ausgefuellt wird sie
gespeichert; Gegenstaende ohne Charge tragen keine.
buildUnitOptions laesst bei hinterlegter Bruecke auch die Einheiten der Gegenart
zu - damit erben Ein- und Auslagern die Faehigkeit ohne eigene Logik. Beim
Auslagern steht der Bestand jetzt in beiden Lesarten ("6 Stueck · 500 g").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mindestbestaende-Seite ist jetzt nach Lagerort gegliedert statt nach Artikel -
"Ueberall (egal wo)" zuerst, danach die Lagerorte. Es gibt keine automatische
Leerzeile je Artikel mehr; angelegt wird ueber "+ hinzufuegen" im jeweiligen
Abschnitt, dann steht der Ort schon fest und es fehlen nur Ziel und Menge.
Das Zeilenmodell faellt damit von vier Faellen auf zwei, und weil der Server
einheitlich Basiseinheiten liefert, bleibt genau eine Umrechnung uebrig statt
der drei gegenlaeufigen von vorher.
Gruppen: neue Spalte "Obergruppen" mit Mehrfachauswahl (GroupParentSelect).
Bewusst kein Baum - CategorySelect und die tree-Prop der DataTable koennen nur
einen Elternteil und wuerden eine Gruppe mit zwei Obergruppen doppelt anzeigen.
Die eigene Gruppe und ihre Untergruppen sind in der Auswahl gesperrt, damit die
Ringregel sichtbar wird, bevor der Server sie ablehnt. Produktzahl zeigt
transitiv und direkt ("12 (3 direkt)"), dazu Badges fuer Untergruppen und eine
Warnung, wenn eine Gruppe mit Untergruppen keine Einheit hat - sonst summiert
sie Gramm und Stueck zu einer sinnlosen Zahl.
Einkaufsliste: "Gesamt" heisst "Ueberall", Gruppen zeigen ihre mitgezaehlten
Untergruppen, und eine Fussnote benennt die eine Stelle, die sich nicht
verrechnen laesst (zwei Obergruppen mit gemeinsamer Untergruppe).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gruppen mit verwalteter Einheit lassen sich in der Spalte 'Einheit' auf ein
Gruppen-Gebinde umschalten. Über 'Gebinde festlegen…' definiert man einmalig
'1 Packung ≈ X g/ml' (Richtwert, da Produkte unterschiedlich große Packungen
haben können). Danach werden Mindestbestand UND Bestand der Gruppe in
Packungen angezeigt/erfasst; das Backend rechnet vorhandene Werte beim
Umschalten um.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Produkte mit Packungsgröße lassen sich auf der Mindestbestandsseite nun
wahlweise in ihrer Gebinde-Einheit (Glas/Dose) statt nur in g/ml erfassen
- Auswahl in der Spalte 'Einheit'. Mindestbestand UND Bestand werden dann
in Packungen angezeigt; der Hinzufügen-Dialog hat den Umschalter ebenfalls.
Gespeicherte Werte bleiben physisch gleich (nur Anzeige/Erfassung wechselt),
das Backend-Flag min_stock_in_packages steuert die Umrechnung.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Statt 'fehlt 500 Gramm' jetzt die Produktgroesse als Leitangabe:
'fehlt 3 Glaeser (500 g)'. Auf ganze Gebinde aufgerundet, weil man nur
ganze Glaeser/Dosen kauft; Basiseinheit als kleiner Hinweis.
Backend: neues ShoppingNeed-Objekt an jeder Einkaufslisten-Zeile (Produkte
+ Gruppen, gesamt + je Ort) - damit die Angabe auch direkt ueber die API
kommt ('x Glaeser (y g)'), inkl. Pluralisierung aus der Gebinde-Tabelle.
Gruppen leiten das Gebinde ab, wenn alle passenden Produkte dasselbe haben,
sonst bleibt es bei der Basiseinheit. Bestehende Felder unveraendert.
Frontend: gemeinsame Komponente ShoppingNeedText fuer Karte und volle Seite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>