d7002e22525c325f1885590cf034df731885246a
344
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
7372590171 |
Zubehoer: anklickbar, mit Lagerort, und mehr Platz
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>
|
||
|
|
a82405b92a |
Zubehoer-Liste: langer Name schiebt den Loesen-Knopf nicht mehr um
.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> |
||
|
|
8a56b13036 |
Einzelstueck-Details: "Gekauft bei" steht in einer Reihe mit den Nachbarn
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>
|
||
|
|
1553e36b78 |
Artikelformular: Typ und Verwaltung als Auswahlliste
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>
|
||
|
|
a3fd9819c4 |
Rueckgabe sichtbar machen, Zubehoerteile am Einzelstueck
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>
|
||
|
|
ca4e9a2264 |
Einzelstueck anlegen: im Verlauf sichtbar und vom Artikel aus erreichbar
Zwei Luecken, die zusammen den Eindruck machten, ein angelegtes Einzelstueck
sei verschwunden.
Der Verlauf kannte nur das Verschwinden. remove_item schreibt seit jeher eine
Bewegung, create_items nicht - Abgaenge von Einzelstuecken tauchten also auf,
Zugaenge nie, im Verlauf wie im Dashboard. Jetzt schreibt das Anlegen je Stueck
eine in-Bewegung mit UID (oder der eigenen Notiz, falls angegeben), Lagerort und
Benutzer, spiegelbildlich zum Entfernen. Der Bestand bleibt unberuehrt: er
zaehlt bei Einzelstuecken die Zeilen in items und rechnet nicht aus Bewegungen.
Und der Weg dorthin fehlte. ProductFormView liefert den angelegten Artikel, aber
zwei der drei Aufrufer warfen ihn weg ("{ _ in showNew = false }") - nur der
Einlagern-Weg nutzte ihn. "Einzelstuecke anlegen" steckt ausschliesslich in der
Artikelansicht, die man vom Scan-Tab und aus den Listen nach dem Anlegen gar
nicht erreicht. Man musste den frischen Artikel von Hand wiederfinden.
Bei Gegenstaenden mit eigener UID je Stueck oeffnet sich jetzt direkt das
Anlege-Formular. Gesetzt wird das im onDismiss des Artikelformulars: beides im
selben Zug zu setzen verschluckt SwiftUI, weil das zweite Sheet erscheinen soll,
waehrend das erste noch verschwindet.
Der Parameter von create_items heisst dadurch user statt _; die drei Testaufrufe
sind mitgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
c924dffad6 |
Einzelstuecke: Zeile zeigt nur noch, was man im Alltag braucht
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> |
||
|
|
b2dda78a7b |
Rueckgabe-Erinnerung: ein Meldetag ist eine Meldung
Erinnert an faellige Rueckgaben verliehener Einzelstuecke, auf dem Weg der Ablauf-Meldungen: lokale Benachrichtigung, geplant aus GET /loans?due_in=. Je Server einstellbar - "X vorher", am Rueckgabetag, und taeglich weiter, solange etwas ueberfaellig ist. Ein Verleih ohne Rueckgabedatum ist nie faellig und kommt nicht vor. Zwei Aenderungen am bestehenden Plan waren dafuer noetig, und beide haben denselben Grund: Der Plan muss allein stehen. Der BGAppRefreshTask ist keine Garantie - iOS fuehrt ihn nur aus, wenn es will -, also darf die Erinnerung nicht davon abhaengen, dass die App geoeffnet wird. 1. Alles, was an einem Tag auf einem Server anliegt, wird EINE Meldung. Vorher waren es zwei Arten (bald ablaufend, abgelaufen) mit je eigener Kennung; mit dem Verleih waeren es vier geworden. iOS erlaubt 64 geplante Mitteilungen je App - ein Platz je Tag statt je Art ist die Voraussetzung dafuer, dass der Horizont ueberhaupt hineinpasst. Ein vergessener Verleih hat sonst mit taeglicher Meldung den ganzen Monat belegt und alle MHD-Meldungen verdraengt, weil die weiter in der Zukunft liegen und bei der Kappung zuerst wegfallen. 2. Horizont 30 statt 60 Tage. Er ist damit die Zusage, wie lange die App ungeoeffnet bleiben darf, ohne dass Erinnerungen ausfallen. Ein laengerer Horizont haette nur spaetere Tage eingeplant und dafuer die naechsten verdraengt. Preis: Eine MHD-Regel mit mehr als 30 Tagen Vorlauf greift erst, wenn das Datum in dieses Fenster rueckt. Der LoanPlanner ist wie der ExpiryPlanner netzfrei und ohne iOS-Typen gebaut, damit sich die Entscheidungen ohne Geraet nachrechnen lassen. Gespeicherte Konfigurationen werden mit decodeIfPresent gelesen - eine bestehende Einstellung bleibt nach dem Update gueltig. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aa72d8342a |
App: Einzelstuecke verleihen - alle Scan-Wege enden schon dort
Verleih-Abschnitt im Einzelstueck: an wen, seit wann, bis wann, dazu Herausgeben bzw. Zuruecknehmen und der Verleih-Verlauf. Pflicht ist nur der Name; Grund und Rueckgabedatum sind abschaltbar, weil "kurz mal mitgenommen" nicht an einem Datum scheitern soll. Der Abschnitt sitzt bewusst oben in ItemEditView, und zwar nicht nur der Wichtigkeit wegen: QR ueber Nachschlagen, eingenaehtes NFC-Tag und ein angetipptes Tag (vorrania://i/<UID>) landen alle in genau dieser Ansicht. Damit ist "scannen und verleihen" ueberall moeglich, ohne einen eigenen Weg dafuer zu bauen. In der Einzelstueck-Liste steht der Verleih unter dem Stueck - ueberfaellig in Rot - und das Filter-Menue kann auf Verliehen, Ueberfaellig oder Im Haus eingrenzen. Wo etwas ist, war bisher immer der Lagerort; ist es verliehen, ist der Entleiher die Antwort, und der Lagerort steht daneben. days_overdue rechnet der Server (GET /loans, ItemOut.loan), damit App und Web nicht getrennt rechnen und nicht auseinanderlaufen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1332dd86fa |
Web: Einzelstuecke verleihen, auch direkt nach dem Scannen
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> |
||
|
|
467ef3c804 |
Einzelstuecke verleihen: eigener Vorgang statt Lagerort-Wert
Der Lagerort bleibt beim Verleihen stehen - er sagt weiter, wohin das Stueck nach der Rueckgabe zurueckgehoert. Ein Lagerort-Wert "verliehen" haette diese Information vernichtet. Stattdessen eine eigene Tabelle item_loans mit Entleiher und Grund als Freitext, Verleihdatum und einem optionalen Rueckgabedatum: "kurz mal mitgenommen" soll nicht an einem Datumszwang scheitern. Zurueckgegebene Vorgaenge bleiben stehen, damit die Tabelle gleichzeitig die Verleih-Historie je Stueck ist. Ein Teilindex auf die offenen Vorgaenge haelt fest, dass ein Stueck nicht zweimal gleichzeitig verliehen sein kann - wie bei den "Ueberall"-Mindestbestaenden, weil Postgres NULLs in UNIQUE als verschieden zaehlt. Verliehen heisst nicht im Bestand: Ein Stueck, das nicht im Haus ist, zaehlt nirgends mehr mit - weder am Lagerort noch im Gesamtbestand. Entschieden wird das an einer Stelle (services/loans.py::nicht_verliehen), die alle fuenf Zaehlungen benutzen. Sonst behauptete "Was liegt hier?" weiterhin, die Kamera liege im Regal. Folge: Faellt ein verliehenes Stueck unter den Mindestbestand, erscheint der Artikel auf der Einkaufsliste. Verleihen und Zuruecknehmen darf jeder angemeldete Benutzer - es ist eine Buchung wie Ein- und Auslagern, keine Stammdatenaenderung - und wird mit dem Konto protokolliert. Nur das Loeschen eines Vorgangs bleibt Admins vorbehalten, weil es Historie vernichtet. Bewusst keine Movement-Bewegung je Verleih: Der Verlauf rechnet den Bestand rueckwaerts aus vorzeichenbehafteten Bewegungen, ein Abgang ohne Gegenbuchung wuerde diese Kurve verfaelschen. GET /loans?due_in= liefert die faelligen Verleihe und ist damit schon die Datenquelle fuer die spaetere Rueckgabe-Erinnerung der App. Oberflaeche in Web und App fehlt noch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ba84e4ea01 |
Ort zuordnen: Karte zeigt, was man da in der Hand haelt
Bisher stand dort nur "Einzelstück R9D55SDC". Die UID sagt aber nichts darueber, welches Stueck das ist - gerade bei Kleidung, wo das NFC-Tag eingenaeht und unsichtbar ist. Unter dem Sucher lag dafuer die halbe Seite ungenutzt. Die Karte zeigt jetzt Vorschaubild, Artikelname, Marke und die Notiz, darunter als beschriftete Zeilen die Einzelstuecknummer und den aktuellen Lagerort. Der Ort ist beim Zuordnen die nuetzlichste Angabe ueberhaupt: man sieht sofort, ob das Stueck schon irgendwo liegt. Der Block war mit 50 Zeilen zu gross fuer den ohnehin langen body und steht jetzt als EinzelstueckKarte fuer sich. ProductThumb bekommt dafuer einen Groessenparameter (Vorgabe 34 wie bisher, also keine Aenderung an den Listen) und bei grossen Bildern einen passenderen Eckenradius und ein groesseres Ersatzsymbol. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a220823882 |
Versionsnummern kommen aus project.yml statt dreifach auseinanderzulaufen
Die Info.plist hatte CFBundleShortVersionString und CFBundleVersion fest auf
"1.0" und "1" stehen. Damit blieb jede Aenderung an MARKETING_VERSION wirkungslos:
Xcode baute weiter 1.0, und da 1.0 bereits veroeffentlicht ist, lehnte App Store
Connect den Upload ab ("Invalid Pre-Release Train").
Beide Werte ziehen jetzt ueber $(MARKETING_VERSION) und
$(CURRENT_PROJECT_VERSION) aus den Build-Einstellungen - so, wie es die Datei
fuer EXECUTABLE_NAME, PRODUCT_NAME und den Bundle-Identifier schon tat. Gepflegt
werden sie in project.yml, die ohnehin die Quelle des Projekts ist; ein in Xcode
angeklicktes 1.1 hatte der naechste "xcodegen generate" sonst wieder verworfen.
Auf 1.1 (2) gesetzt und gegengeprueft: das gebaute Bundle meldet 1.1 / 2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|