Bug 1 — Root cause of PackageInstaller STATUS_FAILURE=1:
The dest file is always named 'evershelf-update.apk'. installApk()
was trying to detect 'gateway' in the filename — always false.
So setAppPackageName() was always passed 'it.dadaloop.evershelf.kiosk'
even when installing the gateway APK (package scalegate).
PackageInstaller rejects the mismatch with STATUS_FAILURE=1.
Fix: save apkUrl into pendingApkDownloadUrl at the TOP of
triggerApkDownload() (not only in the permission branch), then derive
targetPkg from the URL (which does contain 'gateway'/'scale') instead
of the filename.
Bug 2 — Install errors not reaching GitHub Issues:
PHP reportError() has a version guard: if the client version is not
the latest release, it silently skips GitHub issue creation.
A device that is FAILING TO INSTALL an update is by definition on an
old version, so every install error was silently dropped.
Fix: bypass the version guard for types install_download_failed,
install_failure, install_packager_exception.
- Add full-screen CSS preloader to webapp (fades out when _initApp completes)
- Defer _checkWebappUpdate() to 6s after app init so it does not compete
with startup API calls (fixes perceived slowness on first load)
- Switch update-check throttle from sessionStorage to localStorage (6h TTL);
use release published_at instead of version string for comparison, so the
banner correctly appears when a new release is published regardless of whether
the tag is a semver or the rolling "latest" tag
- PHP _isLatestVersion(): return true (do not suppress error reports) when
tag_name is non-semver (e.g. "latest") — was incorrectly blocking ALL reports
- Kiosk checkForUpdates(): show banner only when the release asset actually
contains an APK for the component; handle non-semver tag by treating it
as always-update (prevents silent no-op with rolling "latest" tag)
- Scale gateway checkForUpdates(): same non-semver fix; apkUrl now defaults
to empty and bails out if no matching APK found in assets (prevents 404 install)
in both PHP (api/index.php) and Scale Gateway (ErrorReporter.kt)
- Add _isLatestVersion() / _latestReleaseTag() / _appVersion() helpers in PHP;
skip GitHub issue creation if caller is not on the latest released version
- Add checkUpdate() PHP endpoint (GET api/?action=check_update, no auth required)
- Webapp (app.js): fetch check_update on load, show dismissible amber top-banner
when a newer GitHub release is available; auto-dismiss after 20 s
- Kiosk (KioskActivity.kt + activity_kiosk.xml): replace old JS bottom-banner with
native Android top-banner; real APK download via DownloadManager + PackageInstaller
- Scale Gateway (MainActivity.kt + activity_main.xml): same native top-banner
with checkForUpdates() / showNativeUpdateBanner() / triggerApkDownload() / installApk()
- PHP (api/index.php): hardcode GH_ISSUE_TOKEN/GH_REPO constants at top of
file (before exception handler runs); fix $fp_ variable interpolation bug;
global set_exception_handler + register_shutdown_function; reportError()
endpoint (POST ?action=report_error) with rate limiting, local log, dedup
via fingerprint search on GitHub Issues API
- Kiosk (ErrorReporter.kt): add crash persistence – saves crash payload to
SharedPreferences before network POST, clears on success, retries as
'uncaught-exception-survived' on next launch via sendPendingCrash() in init()
- Scale Gateway: new ErrorReporter.kt – calls GitHub Issues API directly
(no relay needed, token hardcoded, scoped Issues R+W only); crash
persistence via SharedPreferences; MainActivity.kt hooked at onCreate,
startGatewayServer catch, onError (BLE errors)
Tested end-to-end: issues #3-#6 created and closed during QA.
- Replace simple bar chart with full Anti-Waste Report Card
- Grade system (A+ to D) based on user's waste rate
- Dual comparison bars: user waste rate vs national average (IT/DE/US)
- Estimated monthly savings in money, meals saved, CO2 avoided
- 3-month trend mini chart with colour-coded bars
- Backend: getStats() now returns 3×30d buckets (used_30d, used_prev_30d, used_prev_60d, etc.)
- Real-world benchmarks: IT 22%/5.4kg/mo (REDUCE), DE 20%/6.5kg/mo (Eurostat), US 30%/9.2kg/mo (USDA)
- All labels fully i18n: 18 new antiwaste.* keys in it/en/de translation files
- Section is fully JS-rendered; HTML now just an empty container
- Banner: detect expired opened-products via effective shelf-life (opened_at +
estimateOpenedExpiryDays), not just raw expiry_date — fixes Fagioli/Panna case
- Banner: expired items show safety tip inline; danger-level items (fridge dairy,
meat, fish) get red banner + 'L'ho buttato' as primary button, 'Usa comunque'
demoted to grey; safety-ok/warning items keep original button order
- Banner: anomaly dismiss button now shows current inventory qty ('La quantità è
giusta (2 pz)') so the action is unambiguous
- AI: add callGeminiWithFallback() helper — tries gemini-2.5-flash first (separate
quota), falls back to gemini-2.0-flash; applied to all endpoints (expiry, chat,
identify, recipe non-streaming, shopping name classifier)
- AI: show friendly 'Quota AI esaurita' message instead of raw Gemini error string
- Cooking TTS: fix auto-speak broken since 'auto-speak removed' comment — each step
is now read automatically on navigate and on first step when entering cooking mode
- Cooking TTS: remove incorrect s.tts_enabled gate — _cookingTTS toggle is the only
gate; browser Web Speech API used by default without requiring Settings config
- Cooking TTS: timer fires '10 secondi rimanenti' warning at T-10s
- Cooking TTS: announce recipe completion ('Buon appetito!') on last step confirm
- i18n: add timer_warning_tts, recipe_done_tts, error.ai_quota keys (IT/EN/DE)
- CSS: add banner-expired-danger, banner-safety-* styles for unsafe expired items
Two bugs in the migration function:
1. DELETE endpoint does not exist in Bring! API — must use PUT with
'remove' param (same as the remove-from-list flow elsewhere in the code)
2. Items were added using the Italian shopping_name as the 'purchase' field
instead of the German catalog key via italianToBring(shoppingName).
This created Italian/German duplicates (e.g. both 'Affettato' and
'Aufschnitt' in the list at the same time).
Also add a pre-add duplicate check so existing catalog-key items are not
double-added when the old specific item is removed.
Manual cleanup run: removed 25 stale/duplicate items, added 8 correct
German-key items, ran migration (1 more migrated). List is now clean.
- bringGetList() now runs bringMigrateNamesInternal() silently after
returning the response, max once per 10 minutes (bring_migrate_ts.json flag)
- Refactored migration into bringMigrateNamesInternal() reusable function
- Removed manual migrate button from UI (not needed, it's automatic now)
- One-shot migration already executed via curl: 16 items updated in Bring!
- New API action bring_migrate_names: reads current Bring! list, matches
items against products DB, replaces specific names with shopping_name
(e.g. 'Mortadella IGP' → 'Affettato' with spec 'Mortadella IGP · Brand')
- New button in Bring! settings: 'Generalizza nomi lista Bring!'
with live status feedback (migrated / skipped / errors count)
- Auto-refreshes shopping list view after migration
Root cause: after scale auto-confirm fires submitUse(), the old code called
_cancelScaleAutoConfirm(false) which reset _scaleLastConfirmedGrams to null.
This allowed the scale (still showing the same reading) to start a new 10-second
stability+confirm cycle and trigger a second identical deduction.
JS fix:
- submitUse() now calls _cancelScaleTimersOnly() instead of
_cancelScaleAutoConfirm(false), preserving _scaleLastConfirmedGrams so the
same weight is rejected until the product is removed from the plate.
- _scaleStabilityVal reset to null so a genuinely new weight starts fresh.
- Duplicate API response (result.duplicate) silently ignored in the UI.
PHP fix (server-side safety net):
- useFromInventory() rejects a second 'out' transaction for the same product
within 12 seconds with { success: false, duplicate: true }.
This catches any client-side edge cases regardless of scale timing.
- inventory_use API now returns product_shopping_name in response
- showLowStockBringPrompt: uses generic shopping name (e.g. Affettato) as
Bring! item name, specific product name + brand as specification field
- addLowStockToBring: reads from window._lowStockName instead of arg
- Auto-add on depletion JS fallback: same generic-name pattern
- Deduplication check now tries both shoppingName and raw name
1. shopping_name compound-phrase map (computeShoppingName)
Add phraseMap checked against the full product name BEFORE the single-token
keyword loop. Prevents 'pane grattugiato' → 'Pane', 'panna da cucina' → 'Panna', etc.
Key new phrases:
- pane/pan grattugiato → Pangrattato
- panna da cucina / panna cucina / panna chef → Panna da cucina
- fette biscottate → Fette biscottate
- aceto balsamico / glassa balsamico → Aceto balsamico
- latte condensato/evaporato/vegetale/di soia/mandorla/avena/riso/cocco → specific
- prosciutto cotto → Prosciutto cotto
- farina di riso/mais/integrale → specific
- pasta fresca, zucchero di canna, acqua minerale/frizzante/gassata, brodo, …
Also added single-token safety-net entries: 'grattugiato'/'grattato'/'pangrattato'
→ 'Pangrattato', 'biscottate' → 'Fette biscottate'.
2. DB migration (sqlite3 UPDATE)
Re-classified 10 products that had wrong shopping_name:
Pane grattugiato → Pangrattato
Panna da cucina (×4) → Panna da cucina
Fette biscottate (×2) → Fette biscottate
Aceto balsamico (×3) → Aceto balsamico
Cleared 2 stale Gemini cache entries.
3. showLowStockBringPrompt (app.js)
When totalRemaining <= 0 (product fully depleted), skip the modal entirely.
The backend already auto-adds to Bring! on depletion; the JS only asks as a
fallback if that failed (fire-and-forget async, never blocks the UI).
The afterCallback (e.g. move-remainder modal, navigate to dashboard) is called
immediately without user interaction.
- Extended keyword map: 100+ entries covering bread (bauletto->Pane),
cheese (bel->Formaggio, casatella->Formaggio), wine (vesoletto/trebbiano->Vino),
foreign brand names (kaffee->Caffe, risofrolle->Cracker, zuppalatte->Biscotti,
inchusa->Birra, apfelsaft->Succo, kartoffelpüree->Purè, ciobar->Cioccolata calda,
ovomaltine->Bevande), desserts (gelato->Gelato), herbs (camomilla->Camomilla),
liquors (sambuca->Liquore), sugar variants (zuccheri->Zucchero), foreign words
(jus/zumo/arome->Italian equivalents)
- Add _geminiClassifyProduct(): calls Gemini 2.0 Flash for ambiguous product names,
with persistent cache in data/shopping_name_cache.json (never re-queries same product)
- computeShoppingName() now calls Gemini when keyword map and Bring! catalog both fail
and the product name is multi-token or has a brand/category hint
- saveProduct() auto-computes shopping_name on every create/update (already in prev commit)
- DB migration: all 210 products re-classified with new rules
- shopping list: 38->33 groups (Formaggio +4v, Affettato +5v, Biscotti +1v, Pane +1v...)
- Final SQL fixes for edge cases: Gelato, Camomilla, brand name single tokens
- Add shopping_name column to products table
- Add computeShoppingName() PHP auto-assign function:
* Curated keyword map: all salumi/cold cuts → 'Affettato'
* Bring! catalog back-translation: 'Latte di Montagna' → 'Latte'
* Fallback: first significant token capitalized
- Migrate all 210 existing products with auto-computed shopping_name
- saveProduct() auto-computes shopping_name on every create/update
- smartShopping() groups items by shopping_name: most urgent item is
representative, others listed as variants (e.g. 'Affettato' shows
Mortadella, Speck, Nduja, Salame, Prosciutto, Schinkenspeck as one row)
- _productOnBring() also checks shopping_name for Bring! match detection
- addToInventory auto-remove: uses shopping_name-based Bring! key
- useFromInventory auto-add: sends shopping_name to Bring! (not raw name),
specific product name goes into specification field
- Frontend renderSmartItem: shows shopping_name as title, specific
product name(s) in italic subtitle line below
- _syncOnBringFlags: matches on both name and shopping_name
getFinishedItems now:
- Computes total_in - total_out for every qty=0 row
- If balance <= unit threshold (e.g. <20g, <0.1 conf): product was
legitimately used up → silently DELETE, no banner shown
- Only if balance > threshold (unexpected zero): return to frontend
so banner asks user to verify
Banner detail now shows the expected residual qty so user understands
why the alert fired.
- useFromInventory: replace DELETE with UPDATE qty=0 when stock hits 0
(both normal path and use-all-locations path)
- listInventory: add WHERE quantity > 0 so qty=0 rows are invisible in
the regular inventory list
- New API actions: inventory_finished_items (query) and
inventory_confirm_finished (delete after user confirms)
- Banner: new 'finished' type (priority 600, above anomalies)
Shows: '{name} — è finito?' with two buttons
'Sì, è finito' → permanently deletes the qty=0 row
'No, ne ho ancora' → navigates to add-inventory form
- i18n: banner_finished_* and toast.product_finished_confirmed (it/en/de)
- DB migration: restored 75 auto-deleted products (last 30 days) as
qty=0 inventory rows so they appear in the banner queue
- Scadenze: rimuove 'in scadenza' dal banner (solo prodotti già scaduti)
- Consumo anomalo: spiega la media giornaliera, giorni dall'ultimo rifornimento
e direzione della discrepanza (più/meno del previsto) con contesto
- Quantità sospetta: messaggio specifico per caso (bassa/alta/conf insolita)
- Anomalia inventario: linguaggio naturale invece di jargon tecnico
phantom='hai più scorte del previsto', missing='hai meno scorte del previsto'
- API prediction: aggiunge days_since_restock, direction, tx_count
- confirmBannerPrediction: toast con info su ricalcolo previsioni
- Add generateRecipeStream() endpoint with real-time SSE status events
- Frontend generateRecipe() uses ReadableStream for live step updates
- Fix gemini-2.5-flash thinking model: disable thinkingBudget, raise maxOutputTokens to 4096
- Passo 2 is pure PHP heuristic (zero extra AI calls)
- Retry logic with live countdown on 429, fallback chain: 2.5-flash → 2.0-flash
- Pass all ingredients when meal plan is active (no limits)
- Add recipe-loading-msg element with CSS transition
- Aggiunto helper callGemini($url, $payload, $timeout):
* Fino a 4 tentativi su 429 / 503
* Legge Retry-After header dalla risposta HTTP di Google
* Legge retryDelay dal corpo JSON di errore (es. '10s', '30s')
* Backoff default: 2s, 4s, 6s (sovrascitto da Google se specificato)
- geminiReadExpiry(), geminiChat(), geminiIdentifyProduct(): rimosso curl
diretto senza retry, ora usano callGemini()
- generateRecipe(): rimosso vecchio loop manuale (3 tentativi, 2s/4s fissi),
ora usa callGemini() che rispetta i delay suggeriti da Google
- In caso di 429 finale restituisce il messaggio di errore da Google (non generico)
- New API endpoint 'inventory_anomalies': detects items where stored qty
differs from tx history by >20% AND >50 units (phantom qty or missing qty)
- New API endpoint 'dismiss_anomaly': persists dismissal in anomaly_dismissed.json
- Banner system: new 'anomaly' type shown in dashboard alert banner
with 'Correggi' (opens edit) and 'Ok, ignora' (dismisses) buttons
- CSS: banner-anomaly style (orange gradient)
- Fix: lo zucchero azzerato (175g fantasma rimossi), aggiunto a Bring!
- Cache smart_shopping: 10min → 3min (urgenza fresca)
- backgroundBringSync: ogni 10min → ogni 5min + setInterval continuo
- backgroundBringSync: aggiunge anche 'high' (non solo 'critical') a Bring
- autoSyncUrgencySpecs: aggiorna spec anche se il livello di urgenza sale/scende
- Risultato: Latte/prodotti urgenti compaiono su Bring in max ~5min
Prompt ricette:
- Lista ingredienti compatta: skip staples, no brand, flag brevi (🔴3gg)
- Cap gruppo 4 a 40 items, gruppo 6 a 20
- Regole condensate da 10 verbose a 6 concise
- Testi condizionali (varietà, regen, opzioni) abbreviati
- Aggiunto detail errore API Gemini nel toast
Bilancia:
- Ignora oscillazioni sub-grammo (jitter 0.5g)
- Confronto integer-gram prima di dichiarare instabile
- italianToBring(): pass-2 whole-word fallback now ignores generic qualifiers
(dolce, light, classico, originale, etc.)
- when multiple single-word matches exist, choose the longest/specific token
instead of first catalog iteration hit
- prevents wrong mappings like 'Pancetta Dolce' being interpreted via generic
adjective rather than the core product token
- PHP: predictive urgency block now scales by imminence:
round(days_left) <= 3 → high, <= 7 → medium, <= 14 → low
(was flat 'low' for any days_left <= 14)
- PHP: also upgrades existing 'low' urgency to 'high' when
imminent depletion detected (round(days_left) <= 3, isFrequent)
- JS: autoAddCriticalItems now also adds:
- high urgency items with pct_left < 20% (nearly empty)
- high urgency items with days_left <= 3 (imminent)
- any item with days_left <= 2 and uses_per_month >= 5
Result: Latte di Montagna (27.8x/mo, 3 days left) now appears
on shopping list before running out, as do Lenticchie/Riso
Basmati at 1% stock and Sandwich at 1 day left.
- Kiosk: replace header-inject overlay with position:fixed div appended to <html>
so buttons appear regardless of SPA init timing
- Kiosk: bump versionCode 3→4, versionName 1.2.0→1.3.0
- Kiosk: add explicit signingConfigs block (debug keystore) to avoid signature
mismatch on updates; update banner now shows uninstall instruction + 12s timeout
- Web: v1.4.0 → v1.5.0
- Preferred use-location: remember last N location choices per product; after 3+
consistent picks auto-select and collapse location picker (with 'cambia' link)
- Scale: call updateScaleReadButtons() on every status change so live-box and
read button appear instantly on reconnect without manual refresh
- Smart shopping cache: invalidate JSON cache file on every inventory_add and
inventory_use so next shopping-page load always sees current stock
- isLowStock: conf threshold changed <= 1 → < 1 (1 full pack is not low stock)
- italianToBring: replace substring matching with whole-word matching (min 4 chars)
to prevent 'gin' matching 'original', 'rum' matching 'crumble', etc.
Philadelphia original was silently mapped to Gin and skipped as duplicate
- Storico: add undo support (transaction_undo endpoint, undone column, JS undo btn)
- LOG → Storico rename in UI, nav, translations
- Bring! sync: urgency-aware purchased blocklist TTL (critical 30m, high 90m, others 4h)
- forceSyncBring() button to clear all guards and re-sync from scratch
- Scale live-box: position:fixed CSS class, 1.6rem/800 value, direct ml display
- Recipe use modal: scale live-box with 10s stability + 5s auto-confirm countdown
- Recipe use modal: show recipe quantity as highlighted row in Usa popup
- api/scale_discover.php: async TCP scan of whole /24 subnet on port 8765,
confirms with WebSocket handshake, returns found ws:// URLs in ~1.5s
- index.html: '🔍 Auto' button next to gateway URL field
- app.js: discoverScaleGateway() — calls relay, fills URL field and
auto-saves settings + reconnects on success
- showMoveAfterUseModal: add '🫙 Torna sotto vuoto' checkbox (default=previous state)
only shown if item was vacuum sealed; confirmMoveAfterUse passes vacuum_sealed to API
- showRecipeMoveModal: same vacuum checkbox with wasVacuum default passed from ingredient
- confirmRecipeMove: passes vacuum_sealed to inventory_update
- PHP API: add vacuum_sealed to recipe ingredient enrichment
- renderCookingStep: remove step-text word filter; show ALL unused from_pantry
ingredients at every cooking step (AI uses pronouns like 'tagliarla' instead of
repeating the ingredient name, causing them to be invisible)
- Add evershelf-scale-gateway/ Android app (Kotlin):
- BLE scanning and GATT connection to smart scales
- Supports BT SIG Weight Scale (0x181D), Body Composition (0x181B), and generic heuristic parser
- WebSocket server on port 8765 (local LAN)
- Real-time weight broadcasting to EverShelf browser client
- Add scale status indicator in header (green/orange/grey dot)
- Add Settings tab for scale configuration (URL, enable toggle, test, APK download link)
- Add 'Read from scale' button in Add/Use forms when unit is g or ml
- Add scale WebSocket client logic in app.js with auto-reconnect
- Fix recipe suggestion: expiry-prioritized ingredients now only injected into
AI prompt when user explicitly selects 'Priorità Scadenze' or 'Zero Sprechi'
- Update README with smart scale section and website link
- Update all translations (it, en, de) with scale strings