- api/index.php: auto-delete legacy dispensa.db when evershelf.db exists
and dispensa.db is empty (<1KB); vacuum-sealed items only show as
expired after VACUUM_EXPIRY_EXTENSION_DAYS (default 30) past printed
date; add dbCleanup() function; add recipe/tx/vacuum params to
getServerSettings + saveSettings intMap; add 'db_cleanup' action
- api/cron_smart_shopping.php: run dbCleanup() each cron cycle
- app.js: add zerowaste_tips_enabled + screensaver_timeout + retention
days to saveSettings POST payload (were missing, causing reset on sync);
asset version bumped to v=20260518b
- .env: added ZEROWASTE_TIPS_ENABLED, RECIPE_RETENTION_DAYS=7,
TRANSACTION_RETENTION_DAYS=7, VACUUM_EXPIRY_EXTENSION_DAYS=30
Backend (api/index.php):
- callGemini() now extracts usageMetadata (tokens_in/tokens_out) from response
- _recordAiUsage() persists monthly token data to data/ai_usage.json
- callGeminiWithFallback() accepts $usageAction param; all 15 call sites labeled
- gemini_usage endpoint: returns token stats, cost estimate, log info, DB size
- smartShopping(): rolling 90-day EWMA (70% last-30d / 30% days-31-90)
with fallback to all-time rate when <14 days of history
Frontend (index.html + app.js):
- New Info tab (ℹ️) in Settings with Gemini usage and System cards
- _loadInfoTab() / _renderInfoTab(): loads on click, auto-refreshes every 30s
- switchSettingsTab() stops auto-refresh when leaving Info tab
Translations (it/en/de): settings.info.* keys
- Entry log (INFO/DEBUG) added to all public API functions
- EverLog::warn/error added before every uncovered http_response_code(4xx/5xx)
- Total EverLog calls: 117 across all request paths, error paths, and AI flows
- Only pure helper functions excluded (no I/O, no side-effects)
- api/logger.php: EverLog static class con 4 livelli (DEBUG/INFO/WARN/ERROR)
- Rotazione oraria/giornaliera configurabile via LOG_ROTATE_HOURS
- Max file configurabile via LOG_MAX_FILES (default 14)
- Request ID unico per tracciare ogni chiamata API
- EverLog::query(), aiCall(), aiResponse(), cache(), slowOp(), exception()
- Endpoint get_logs per inspection remota (protetto da SETTINGS_TOKEN)
- LoggingPDO + LoggingPDOStatement: auto-log di OGNI query SQLite
- api/database.php: getDB() restituisce LoggingPDO (drop-in, retrocompat.)
- api/index.php: EverLog integrato in ~82 punti
- Entry log in ogni funzione API
- callGemini/callGeminiWithFallback: timing AI + aiCall/aiResponse
- Rate limiter, unknown action, errori globali, DB connect fail
Livello default: INFO (query DB a DEBUG, solo se LOG_LEVEL=DEBUG)
Banner prodotti aperti:
- Rimosse le opzioni 'Usa comunque' e 'Ignora' (non hanno senso
se il prodotto è solo aperto — rimangono solo 'L\''ho finito!',
'L\''ho buttato', 'Correggi data')
- Per prodotti scaduti non aperti il comportamento rimane invariato
Preloader startup check:
- Sostituito il mini-label monospace con una ruota 3D (stile cooking wheel)
- Testo grande, colorato: VERDE=ok, ARANCIONE=warning, ROSSO=errore
- Il check precedente sale in cima (rotateX tilt, dimmed) mentre il
nuovo entra dal basso con animazione 3D
- setProgress() ora guida la ruota; slowAnim() aggiorna solo la barra
Defaults / non-bloccante:
- Gemini API key non impostata → ok:true 'non configurata' (verde)
- Bring! token non ancora generato → ok:true (verde, auto-generato al 1° accesso)
- La configurazione mancante mostra ✅ informativo, non ⚠️ warning
Step 5 del wizard ora mostra 4 toggle (pre-compilati se già configurati):
- Salvaschermo orologio (screensaver_enabled)
- Prezzi lista spesa (price_enabled)
- Piano pasti (meal_plan_enabled)
- Suggerimenti zero-waste durante cottura (zerowaste_tips_enabled)
Solo i toggle NON ancora impostati in prefs partono da false (fresh install).
Tutti e 4 vengono salvati in SharedPreferences e inviati al server via
save_settings al completamento del wizard.
PHP/JS: zerowaste_tips_enabled aggiunto come impostazione server-side
(ZEROWASTE_TIPS_ENABLED in .env), sincronizzata nel WebView via
_applySyncedSettings() al caricamento.
- health_check: use evershelf.db (not dispensa.db); auto-migrate if needed
- removed dispensa.db (legacy, obsolete)
- backups check: verify files exist (not dir writability, cron writes as root)
- bring_token: read data/bring_token.json (not env var)
- warning popup: 5s countdown bar with label+hint per warning, auto-closes
- error popup: blocking panel with title + hint per critical failure
- db_legacy check: warns if old dispensa.db still present
- 32 total checks (added db_legacy, tts_url, scale_gateway)
- hint messages on every check explaining cause and fix
- translations: added check_db_legacy, check_tts, check_scale,
critical_error_intro, error_network_detail in it/en/de
- Replace banner checklist with real-time progress bar + per-check label
Bar fills smoothly (0→100%) as each check runs; label shows current check.
On success: bar stays green briefly then fades. On warnings: amber badges
shown for 2.2s. On critical error: bar turns red + error block + Retry.
- Extend health_check to 29 comprehensive checks:
PHP 8.0+ version, 4 critical extensions (pdo_sqlite/curl/json/mbstring),
4 optional extensions (openssl/fileinfo/zip/intl), PHP memory/timeout/upload,
data/ writable, rate_limits/ dir, backups/ dir, actual file-write test,
free disk space, SQLite connect, required tables, PRAGMA quick_check integrity,
WAL mode, DB file size, inventory row count, .env file, Gemini AI key,
Bring! credentials + token, cURL SSL version, internet reachability (Gemini API)
- Fresh-install detection: if dispensa.db not found + data/ writable → OK (auto-create)
- Translations: startup.* expanded to 28 keys in IT, EN, DE, FR, ES
- CSS: new .preloader-progress-wrap, .preloader-bar-track, .preloader-bar,
.preloader-check-label, .preloader-warn-badge; removed old .preloader-checks
- Version: v1.7.21, assets v=20260520b
- Add ?action=health_check PHP endpoint (early-exit, before rate-limiter)
Checks: PHP version, required extensions, data/ writability, SQLite DB
connection + table integrity, .env file, Gemini AI key, Bring! token
- Display animated checklist in splash screen with per-item icons
(ok/warn/error); critical failures block app launch with clear error
message and Retry button; optional warnings shown but don't block
- New JS: _runStartupCheck(), _startupRetry(); called first in _initApp()
- New HTML elements in #app-preloader: #preloader-checks, #preloader-error-msg,
#preloader-retry-btn (hidden until startup check completes)
- New CSS: .preloader-checks, .preloader-check-row, .preloader-error-msg,
.preloader-retry-btn with state colors (ok=green, warn=amber, error=red)
- Translations: startup.* keys (10 per language) in IT, EN, DE, FR, ES
- Asset version bump: v=20260520a
- TTS: tts_engine, tts_rate, tts_pitch, tts_auth_header_name, tts_auth_header_value,
tts_extra_fields now stored in .env and synced across devices via get_settings/save_settings
- meal_plan: persisted to SQLite app_settings table on every edit (selectMealPlanType,
resetMealPlan) and restored on startup via syncSettingsFromDB — all devices stay in sync
- tts_voice: also synced to SQLite for best-effort cross-device restore
- saveSettings() sends meal_plan + tts_voice to app_settings_save after env write
- Remove deprecated SPESA_PROVIDER and SPESA_AI_PROMPT from .env
- .env.example: full rewrite documenting all 30+ keys in labelled sections
(AI, Shopping, TTS, Preferences, Appliances, Scale, Meal Plan, Screensaver, Prices,
Security, Developer)
- 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