- Shopping list: each item now shows 'Hai già Xg in dispensa' for same-family inventory stock
- Lazy-loads inventory once per shopping page visit (_getShoppingInventoryCache)
- Matches by first significant token (same logic as related-stock on action page)
- Green hint below item badges, dark-mode aware (.shopping-pantry-hint)
- Barcode lookup: added Open Products Facts + Open Beauty Facts as step 3;
Gemini AI (_barcodeLookupGemini) as final step 4 fallback
- Added stockForName PHP endpoint (stock_for_name action) for future use
- Restored missing function signatures for _offFetchProduct() and saveProduct()
that were accidentally lost when stockForName was added in a previous session
- Translation: added shopping.pantry_hint in it/en/de
- Restore the if($bringEnabled) block that was accidentally removed in fa0442e
- Check is skipped entirely when SHOPPING_MODE != bring or credentials not set
- Missing token file = first launch, auto-created on next shopping open → ok:true
- Warning shown only if token file exists but access_token field is missing (corrupt)
- Expired tokens are OK (refreshed automatically)
Fixes spurious 'Token Bring!' warning on installs without Bring! configured
- PHP (all 3 recipe endpoints): conf products with weight/volume package_unit
(e.g., 300g/conf) now keep qty_number in sub-units (grams/ml) instead of
converting to fractional conf. Post-normalisation block converts any AI-
returned fractional conf values to grams automatically.
- JS submitRecipeUse: vacuum state now read from actual inventory item at the
used location (_recipeUseContext.items), not from recipe ingredient data.
- JS showRecipeMoveModal: now uses _prefMoveLocCache preference system —
after 2 consistent choices the modal is skipped automatically. 'Stay' button
records the choice. Added _recipeMoveCancelStay() helper.
- JS confirmRecipeMove: records move choice via _recordMoveLocChoice();
accepts optional forcedVacuum param for preference-triggered auto-moves.
Closes#108 (duplicate of #107 — data dir permissions)
- New shopping_list SQLite table (migration in migrateDB)
- shoppingGetList/Add/Remove — delegates to Bring! or internal DB
based on SHOPPING_MODE env var (default: internal)
- isShoppingBringMode() guard: requires mode=bring + BRING credentials
- bringQuickSyncProduct updated to support both modes
- All bring_* JS calls replaced with shopping_* (bring_migrate_names kept)
- New settings tab 'Lista spesa' (tab-bring) with:
- Enable/disable shopping list toggle
- Provider radio: internal vs Bring!
- Bring! sub-section (shown only when mode=bring)
- AI smart suggestions toggle
- Forecast toggle
- Auto-add threshold (qty slider)
- Price estimation section
- _applyShoppingSettingsUI, onShoppingEnabledChange, onShoppingModeChange
- SHOPPING_* env vars documented in .env.example
- cron_smart_shopping respects SHOPPING_MODE and SHOPPING_SMART_SUGGESTIONS
- Translations: 12 new keys in all 5 languages (it/en/de/fr/es)
- DB busy_timeout=5000ms + WAL pragma in getDB() (fixes#95)
- Switch redirect URI from server IP to http://localhost (works everywhere)
- Add manual code exchange flow: user copies URL from browser, pastes in app
- New PHP action gdrive_oauth_exchange to exchange auth code for refresh token
- Fix null bug in gdrive_oauth_exchange (was read before initialization)
- Add #gdrive-code-section UI with input + submit button in index.html
- Update _gdriveAuthorize() to show code section and store redirect_uri
- Add _gdriveSubmitCode() JS function for manual code submission
- Update setup wizard and backup tab to show http://localhost as redirect URI
- Add 5 new translation keys (gdrive_redirect_uri_hint, gdrive_code_title,
gdrive_code_hint, gdrive_code_submit, gdrive_code_empty) in all 5 languages
- Update gdrive_oauth_steps in all translations to reflect new flow
- Document Google Drive OAuth setup in README.md
- Dark mode: comprehensive fix for 30+ components with hardcoded light colors
- 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)
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.
- 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