- Complete i18n audit: 25+ new translation keys (en/it/de) — vacuum toast,
TTS voices, timer steps, product notes, error prefixes, form placeholders,
barcode hints, recipe/cooking ingredient labels, unit variants
- pz/conf unit labels now use t('units.pz') / t('units.conf') throughout
- Splash screen: minimum 3-second display (_splashStart recorded at parse
time, fade delayed by remaining ms if app loads faster)
- Quantity decimal precision: qtyNum in recipe/cooking buttons and conf
fallback display capped to 1 decimal (was showing 7+ from raw AI output)
- Recipe/cooking buttons: removed Italian fallback strings from t() calls
- README: translated remaining Italian phrases; added demo.gif to Screenshots
- CHANGELOG: updated 1.7.15 entry with all session changes
- assets/img/demo.gif: EverShelf.gif processed at 2x speed (~36s)
When migrateDB() upgraded the transactions table to add the 'waste'
CHECK constraint, the new table was created WITHOUT the 'undone' column.
The migration then tried to build idx_transactions_pid_type_undone, which
references 'undone' → PDOException SQLSTATE[HY000].
Fix:
- Add undone INTEGER DEFAULT 0 to the migration CREATE TABLE
- Replace 'INSERT INTO transactions SELECT * FROM transactions_old'
with explicit column list (transactions_old may predate undone column)
Fixes: #56
Both PHP and JS rules for opened confettura/marmellata in
section G (fridge condiments) were returning 60 days — too short.
An opened jar of jam lasts ~6 months in the fridge.
Also: update README roadmap with comprehensive, grouped view
matching the internal memory roadmap (high/medium/low/completed).
Fixes: database.php line ~412, app.js line ~1707
Query was missing AND i.quantity > 0, so thrown-away items (qty=0)
with a past expiry_date kept appearing in the expired list.
Also cleaned up the orphan row for Aglio in the DB.
Two changes:
1. Skip prediction when expected_qty=0 — model says 'should be finished'
but user simply restocked or consumed less. Not actionable.
2. Raise 'more than expected' threshold to 400% (was 30%).
Having more than expected almost always means a restock the model
doesn't know about yet — only truly extreme cases (>4x) are flagged.
'Less than expected' stays at 30% (still actionable: unregistered use).
Root cause: baseline was 'restockQty' (only the new items added) but
actualQty = pre-existing stock + new items → always looked like 'more than expected'.
New approach: baseline = current_qty + consumed_since_restock.
This correctly reflects the true starting point regardless of pre-existing stock,
eliminating all false positives after shopping trips.
When a user manually edits quantity (e.g. after restocking), the diff
is recorded as 'in' or 'out' transaction with note '[Correzione manuale]'.
This prevents the anomaly detector from flagging manual edits as phantom
or missing consumption.
When usage splits a row into 'whole sealed packages' + 'opened fraction',
the sealed row was updated without clearing opened_at — if it had been
opened previously, the stale flag would persist and wrongly show
'aperto da N giorni' on intact packages.
Now all 3 split paths (conf early-split, conf post-split, g/ml/l split)
explicitly set opened_at = NULL on the sealed row.
Fresh milk is explicitly matched by 'latte fresco/intero/parzial/scremato' (3 days).
Generic 'Latte' without qualifier is almost always UHT in Italian households — 7 days.
When a Docker named volume is first mounted at /var/www/html/data,
the directory may be owned by root (the volume is created empty before
the image's chown step applies). This caused PDO::__construct to throw:
SQLSTATE[HY000][14] unable to open database file
Fix: _ensureDataDir() checks/creates the directory and attempts chmod
before every getDB() call. On subsequent calls the is_dir+is_writable
checks are O(1) stat calls with no overhead.
Fixes#34 (also closes#28#29#30#31#32#33)
- Category badge on every inventory item (icon + label); 'altro' items
refined asynchronously via new guess_category Gemini endpoint
(data/category_ai_cache.json) — no AI call when key not configured
- Category search: inventory search now matches by macro-category key
and translated label (e.g. 'biscotti' finds all cookie items)
- Brand fast-path in guessCategoryFromName (Oreo, Barilla, Lavazza…)
- Fix: duplicate banner alerts — _bannerLoading guard + _queuedItemIds Set
- Fix: mapToLocalCategory with en:dairies (dairi stem added)
- Fix: mapToLocalCategory no longer blocks on 'altro' — falls back to
guessCategoryFromName(productName) before returning 'altro'
- Fix: 'Tonno all'olio' was resolving to condimenti — moved tonno\b
check before olio\b in conserve regex block
- AI guards: _refineCategoryBadgesAsync and fetchAllPrices now check
_geminiAvailable (JS); getShoppingPrice returns no_api_key (PHP)
when GEMINI_API_KEY is not set — all AI functions are now explicit
- Recipe use modal: reset _scaleLastConfirmedGrams al peso attuale prima
di aprire il modale, così la tara ha tempo; soglia ridotta 10→5g
- PHP useFromInventory: prima di auto-aggiungere a Bring! un prodotto esaurito,
controlla se la famiglia shopping_name ha scorte da altri prodotti (es.
'Sale marino iodato' esaurito ma 3kg di altri sali in dispensa → non aggiunge)
JS, così il cron bringCleanupObsolete può auto-rimuovere
- Rimosso manualmente 'Sale' da Bring! (aggiunto senza marker dalla vecchia logica)
- Smart shopping: aggiungi family-coverage check per prodotti 'quasi finiti'.
Se il shopping_name family ha scorte da altri prodotti (es. Burro conf)
con unità diff (g/ml vs conf), l'alert 'sta finendo' viene soppresso.
- Corretto bug traduzioni: sezione 'action' duplicata in de/en/it.json
causava JSONDecodeError in CI/CD (line 944 column 2).
- DB: allineamento inventario burro — rimossi 30g residui (usati),
pulito opened_at da pacco nuovo Burro conf (comprato 2026-05-08).
- Bottone 'Apri la ricetta': il transfer btn si trasforma direttamente in
'📖 Apri la ricetta' dopo il successo (invece di aggiungere un elemento DOM separato)
- meal null: chatToRecipe e recipe_from_ingredient non auto-categorizzano il pasto;
renderRecipe mostra il tag meal solo se presente
- Nuovo endpoint recipe_from_ingredient: genera una ricetta con l'ingrediente
selezionato come protagonista, stessa pipeline di chatToRecipe (Gemini + fuzzy-match)
- Bottone '👨🍳 Crea una ricetta con questo' nel pannello azione degli alimenti
(span-2 sotto la griglia 2x2), apre overlay Ricette in loading state
Fixes parse_error on complex recipes (JSON was truncated at 2048 tokens).
After successful transfer, shows 'Apri la ricetta' button inline in chat
alongside the '✅ Aggiunta alle Ricette!' button.
Closes#27
- Sostituisce 'Usa ingredienti' inline con 'Trasferisci a Ricette'
- Nuovo endpoint chat_to_recipe: Gemini restituisce JSON completo
(title, meal, servings, ingredients, steps, nutrition_note),
PHP arricchisce tutti gli ingredienti con product_id/location
via fuzzy-match identico a generateRecipe
- La ricetta viene salvata in archivio e si apre nell'overlay Ricette
con tutti i pulsanti Usa, modalità cottura, salvataggio intatto
- Rimossi: chatExtractIngredients, _buildChatIngredientPanelHTML,
_chatRecipeTitle, chat_extract_recipe, chat-recipe-panel CSS
- Nuovo endpoint chat_extract_recipe: Gemini estrae solo nomi+quantità
con prompt minimo (nessun inventario nel prompt → niente troncamento),
poi PHP fuzzy-match contro l'inventario completo identico a generateRecipe
- Frontend: _looksLikeRecipe() rileva risposte chat con ricetta;
bottone '🥄 Usa ingredienti' appare sotto la bubble, chiama chatExtractIngredients()
che mostra pannello inline con pulsanti '📦 Usa' per ogni ingrediente in dispensa
- useRecipeIngredient() riusato 1:1 con fallback _chatRecipeTitle per le note
- Stili CSS: btn-chat-use-recipe, chat-recipe-panel, chat-recipe-panel-container
- Chiavi i18n: use_ingredients_btn, recipe_ingredients_from_pantry (it/en/de)
- Move system prompt to systemInstruction API field instead of injecting
it as a fake user/model turn, saving the full turn's token count from
the context window used for generation
- Increase maxOutputTokens from 1500 to 4096 so full recipes (with
ingredients + instructions) can complete without being cut off
- Increase API timeout from 60 to 90 seconds for longer responses
finish_reason changes from MAX_TOKENS → STOP, reply goes from 265 to 2108 chars
- Add 'macchina del pane' to multiFunction list and capabilityMap with
bread-specific instructions (ingredient order: liquids → flour → salt →
sugar → yeast on top; programs: Base, Integrale, Francese, Rapido, Dolce)
- Fix compact appliances prompt: when multiple specialized appliances exist,
list each with capabilities instead of forcing 'PREFERISCI Cookeo' (which
caused Gemini to ignore the user's explicit bread machine request)
- Add chat rule #10: when user asks for a specific appliance recipe, always
provide instructions tailored to that device only
- Remove recentlyExhausted bypass from shopping_name family suppression:
products recently exhausted (<14d) were incorrectly flagged as critical
even when the same family had ample stock (Yogurt 2002g, Affettato 1022g,
Pane 400g). recentlyExhausted now only bypasses loose token-based coverage.
- Add prewarmShelfLifeCache() in cron: pre-warms opened shelf life via
Gemini AI (max 5 items/cycle) so the UI never blocks on first load.
PHP api/index.php:
- DB connection failure (500) now calls _phpErrorReport()
- Main router catch-all (500) now calls _phpErrorReport()
- undoTransaction DB error (500) now calls _phpErrorReport()
PHP api/cron_smart_shopping.php:
- cron Throwable catch now calls _phpErrorReport() before exit(1)
(fires even in CRON_MODE since _phpErrorReport() has its own guard)
Scale Gateway GatewayWebSocketServer.kt:
- onError() now calls ErrorReporter.report(ex, ...) in addition to Log.e
Combined with previous kiosk commit, every error path in the entire
EverShelf stack now sends an automatic GitHub Issue.
- AI prompt: always return a best-guess price (never null/price_not_found)
for unrecognised items returns generic package estimate with '~' prefix
- Cache key bumped to v3 to invalidate old null-returning cache entries
- JS: manually-added items (no smart match, no spec) default to qty=1/conf
instead of qty=1/pz so _calcEstimatedTotal treats them as a single pack
- Price badge: shows '~€X.XX' prefix when source_note starts with '~'
so user knows the price is a rough estimate
- app.js and style.css versioned to 20260507a so browsers load new code
- get_shopping_price / get_all_shopping_prices moved to dedicated 'price'
rate-limit bucket (60 req/min) separate from general (120 req/min)
to avoid false 429s during sequential per-item price loading
- Add get_shopping_price / get_all_shopping_prices API endpoints
- AI (Gemini) estimates retail price per natural unit (pack, piece, bunch)
instead of always per-kg — avoids absurd totals like €1609
- _calcEstimatedTotal: proper g/ml→package conversion using defQty + regex
on unit_label; only 'kg'/'l' labels trigger weight/volume math
- Cache key bumped to v2 to invalidate old per-kg cached entries
- Suggested quantity cap lowered from 20 to 10 conf/pz
- Unit mismatch guard: if totalUsed >> buyCount*5 for unit=conf, use
purchase frequency instead of raw consumption rate
- JS _buildPricePayload: use smartShoppingItems for qty/unit (not Bring! spec)
- JS _cachedPrices: persist in sessionStorage (survives navigation);
validated by _qty/_unit metadata so stale totals auto-invalidate
- Price display redesigned: right-side column per row (price-col-main +
price-col-unit) instead of small inline badge
- fetchAllPrices: buttons disabled immediately before guard check;
running total uses only current shoppingItems (not Object.values cache)
- Background refresh: always silent (removed 90s interaction condition)
- visibilitychange: sets _bgCall=true for shopping before refreshCurrentPage
- .gitignore: add runtime data files (bring_migrate_ts, shopping_price_cache,
anomaly_dismissed, opened_shelf_cache, shopping_name_cache)
- Remove bring_catalog.json and bring_migrate_ts.json from tracking
- app.js: move initInactivityWatcher() inside syncSettingsFromDB().then()
so it reads screensaver_enabled after server sync, not stale localStorage
- app.js: skip gemini_key/bring_password in save_settings POST when empty
to avoid overwriting server .env with blank values
- api/index.php: add screensaver_enabled to getServerSettings() + saveSettings()