feat: HA sensor enrichment, inventory edit guard, recipe ingredient fix, bread shelf-life

- HA sensor: expiring_list now includes full product details (location, brand,
  category, days_remaining, opened_at, vacuum_sealed, default_quantity, etc.)
- HA sensor: new expired_list attribute with full product details per expired item
- HA sensor: new low_stock_list attribute (items with quantity ≤ 1, full details)
- HA sensor: new sensor=product endpoint (?action=ha_sensor&sensor=product)
  with optional filters: &id=, &name=, &location=
- HA cron webhook: expiry alert items now carry full product details
- Inventory edit: confirm dialog when quantity exceeds unit-specific threshold
  (prevents data loss from unit-confusion typos, e.g. 183 conf instead of 0.183)
- Recipe AI: explicit rule against ingredient form substitution
  (fresh tomatoes ≠ passata, fresh milk ≠ UHT ≠ cream, etc.)
- Shelf-life: opened bread rules (piadina 2d, bauletto/pancarrè 4d, pane 3d)
- docs/wiki: HA page updated with new schema, examples, product endpoint

Closes #125
This commit is contained in:
dadaloop82
2026-05-29 05:40:25 +00:00
parent bc39361246
commit 1637cc1020
11 changed files with 476 additions and 56 deletions
+5 -2
View File
@@ -1086,11 +1086,13 @@
"offline_ai_disabled": "Offline nicht verfügbar",
"offline_cache_ready": "Offline — {n} Produkte im Cache"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?",
"kiosk_exit": "Kioskmodus verlassen?",
"cancel": "Abbrechen",
"proceed": "Bestätigen"
"proceed": "Bestätigen",
"discard_one": "1 Stück wegwerfen"
},
"location": {
"dispensa": "Vorratskammer",
@@ -1102,7 +1104,8 @@
"unknown_hint": "Produktname und Informationen eingeben",
"label_name": "🏷️ Produktname",
"choose_location_title": "Welchen Ort?",
"choose_location_hint": "Wähle den zu bearbeitenden Ort:"
"choose_location_hint": "Wähle den zu bearbeitenden Ort:",
"confirm_large_qty": "Du setzt die Menge auf {qty} {unit}. Das scheint ungewöhnlich hoch zu sein. Bestätigen?"
},
"screensaver": {
"recipe_btn": "Rezepte",
+5 -2
View File
@@ -1086,11 +1086,13 @@
"offline_ai_disabled": "Not available offline",
"offline_cache_ready": "Offline — {n} items cached"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "Do you really want to remove this product from inventory?",
"kiosk_exit": "Exit kiosk mode?",
"cancel": "Cancel",
"proceed": "Confirm"
"proceed": "Confirm",
"discard_one": "Discard 1 piece"
},
"location": {
"dispensa": "Pantry",
@@ -1102,7 +1104,8 @@
"unknown_hint": "Enter the product name and information",
"label_name": "🏷️ Product name",
"choose_location_title": "Which location?",
"choose_location_hint": "Choose the location to edit:"
"choose_location_hint": "Choose the location to edit:",
"confirm_large_qty": "You are setting the quantity to {qty} {unit}. This seems unusually high. Confirm?"
},
"screensaver": {
"recipe_btn": "Recipes",
+5 -2
View File
@@ -1037,11 +1037,13 @@
"offline_ai_disabled": "No disponible sin conexión",
"offline_cache_ready": "Offline — {n} productos en caché"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "¿Realmente quieres eliminar este producto del inventario?",
"kiosk_exit": "¿Salir del modo kiosco?",
"cancel": "Cancelar",
"proceed": "Confirmar"
"proceed": "Confirmar",
"discard_one": "Tirar 1 unidad"
},
"location": {
"dispensa": "Despensa",
@@ -1053,7 +1055,8 @@
"unknown_hint": "Introduce el nombre del producto y la información",
"label_name": "🏷️ Nombre del producto",
"choose_location_title": "¿Qué ubicación?",
"choose_location_hint": "Elige la ubicación a editar:"
"choose_location_hint": "Elige la ubicación a editar:",
"confirm_large_qty": "Estás configurando la cantidad a {qty} {unit}. Esto parece inusualmente alto. ¿Confirmar?"
},
"screensaver": {
"recipe_btn": "Recetas",
+5 -2
View File
@@ -1037,11 +1037,13 @@
"offline_ai_disabled": "Indisponible hors ligne",
"offline_cache_ready": "Offline — {n} produits en cache"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "Voulez-vous vraiment supprimer ce produit de l'inventaire ?",
"kiosk_exit": "Quitter le mode kiosque ?",
"cancel": "Annuler",
"proceed": "Confirmer"
"proceed": "Confirmer",
"discard_one": "Jeter 1 pièce"
},
"location": {
"dispensa": "Garde-manger",
@@ -1053,7 +1055,8 @@
"unknown_hint": "Entrez le nom du produit et les informations",
"label_name": "🏷️ Nom du produit",
"choose_location_title": "Quel emplacement ?",
"choose_location_hint": "Choisissez l'emplacement à modifier :"
"choose_location_hint": "Choisissez l'emplacement à modifier :",
"confirm_large_qty": "Vous définissez la quantité à {qty} {unit}. Cela semble inhabituellement élevé. Confirmer ?"
},
"screensaver": {
"recipe_btn": "Recettes",
+4 -2
View File
@@ -1090,7 +1090,8 @@
"remove_item": "Vuoi davvero rimuovere questo prodotto dall'inventario?",
"kiosk_exit": "Uscire dalla modalità kiosk?",
"cancel": "Annulla",
"proceed": "Conferma"
"proceed": "Conferma",
"discard_one": "Butta 1 pezzo"
},
"location": {
"dispensa": "Dispensa",
@@ -1102,7 +1103,8 @@
"unknown_hint": "Inserisci il nome e le informazioni del prodotto",
"label_name": "🏷️ Nome prodotto",
"choose_location_title": "Quale modifica?",
"choose_location_hint": "Scegli la posizione da modificare:"
"choose_location_hint": "Scegli la posizione da modificare:",
"confirm_large_qty": "Stai impostando la quantità a {qty} {unit}. Questo sembra un valore insolitamente alto. Confermare?"
},
"screensaver": {
"recipe_btn": "Ricette",