- 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
style.css:
- Connected dot: white fill + green border/glow (was green-on-green, invisible)
app.js:
- _kioskReconfigureScale(): show #kiosk-needs-update-notice + toast when
kiosk APK is too old and reconfigureScale() method is missing
- _scaleUpdateStatus(): show/hide #scale-live-diag panel, update device + battery
- _scaleOnMessage weight: update #scale-diag-weight in real time
- _scaleOnMessage status: update #scale-diag-proto with BLE protocol
index.html:
- #kiosk-needs-update-notice: amber warning + download link inside kiosk panel
- #scale-live-diag: device name, battery, live weight readout, reconnect info
- CSS cache bump ?v=20260506d
style.css:
- Scale connected dot: bright #4ade80 fill + white border + double shadow
so it pops on the dark green header (was white on green = invisible)
index.html:
- Scale settings tab: add kiosk panel with 'Riconfigura bilancia BLE'
button (hidden in browser, shown in kiosk mode)
- Wrap gateway download section and WebSocket URL section with IDs
so JS can hide them in kiosk mode
- CSS cache bust ?v=20260506b
app.js:
- syncSettingsFromDB: in kiosk mode hide scale gateway download section,
WebSocket URL section and test button; show kiosk BLE panel instead;
auto-set URL to ws://localhost:8765
- Add _kioskReconfigureScale() helper that calls _kioskBridge.reconfigureScale()
KioskActivity.kt:
- Add reconfigureScale() @JavascriptInterface: stops GatewayService,
clears saved scale device prefs, launches SetupActivity at step 4
- Import GatewayService
- CSS: header-title-wrap uses position:absolute with left:0/right:0
+ justify-content:center so title is truly centered regardless of
button count on each side (kiosk has 2 left / 3 right)
- header-actions gets margin-left:auto to push right; both sides z-index:1
- Bump version 1.7.2 → 1.7.3 so already-open kiosk tabs will see
the update badge once the new version is deployed to the server
- CSS cache bust: ?v=20260506a
style.css was still served with ?v=20260421a so all CSS changes since
April 21 (scale indicator redesign, banner close button fix, etc.) were
invisible in the kiosk WebView — it used the cached old file.
Bumping manifest.json version 1.7.1→1.7.2 causes the auto-update
detector (_checkWebappUpdate) to fire on the kiosk: it compares the
loaded page version (1.7.1) with the server version (1.7.2), detects
a change, and shows the '⬆️ Aggiorna' badge so the user can reload.
webapp:
- Scale indicator: replace plain green dot with ⚖️ emoji + colored
status badge (green/amber/grey/red); icon fades out when disconnected;
tap shows a toast with device name + battery level
- Logo images: crop excess transparent padding from logo.png and
logo_icon.png so content fills the frame at small display sizes
- style.css: reworked .scale-status-indicator CSS for emoji+badge
kiosk:
- SetupActivity: use device's real LAN IP for scale_gateway_url
(was hardcoded 127.0.0.1 — only worked if server and kiosk run on
the same machine); added getDeviceLanIp() helper (prefers wlan/eth)
- activity_setup.xml: reduce welcome step padding/margins so step 1
fits on screen without scrolling; text sizes slightly reduced
- activity_setup.xml: fix feature bullet 'Bilancia Bluetooth via
Gateway app' → 'Bilancia BLE integrata (nessuna app esterna)'
- strings.xml (en + it): rewrite all wizard_gateway_* strings to
reflect integrated BLE service instead of external gateway APK
- ic_logo.png: regenerated at all densities from cropped source
- assets/img/logo/logo.png: trimmed full logo (icon + text, transparent bg)
- assets/img/logo/logo_icon.png: icon-only crop (no text, for header)
- drawable-*/ic_logo.png: multi-density PNGs for Android splash (mdpi→xxxhdpi)
- activity_kiosk.xml: replace ic_launcher_foreground with ic_logo at 260dp,
remove redundant 'EverShelf' text row (already in logo image)
- index.html: add logo_icon.png in header title, logo.png in preloader
- style.css: add .app-preloader-logo and .header-logo-icon rules
- Add _showDestructiveConfirm() helper: shows modal with 5-second
auto-confirm countdown bar; user can confirm early or cancel
- throwAll(): now shows confirmation before discarding all stock
- submitUseAll(): same confirmation before marking all as used
- undoTransactionEntry(): replace native confirm() with modal
- Rename core logic to _doSubmitUseAll() / _doUndoTransaction()
- btn-log-undo: more visible (red tint + larger font) so user can
easily undo accidental operations from the history log
- Bump app.js version to v=20260505a
- Replace checkbox-label with toggle-row pattern matching other toggles
- Add missing <span class="toggle-slider"></span> inside toggle-switch
- Add data-i18n attributes to card title and hint
- Add screensaver.card_title / card_hint translations in all 3 locales
feat(demo): full demo mode implementation
- _applyDemoModeUI(): set _geminiAvailable=true + call _updateGeminiButtonState()
- api(): no-op all bring_add/bring_remove/bring_set_spec calls in demo mode
- api(): return in-memory shoppingItems for bring_list in demo mode
- loadShoppingList(): show placeholder list in demo mode, skip all Bring! calls
fix(shopping): graceful Bring! missing credentials handling
- Show friendly message with link to settings instead of raw PHP error
- Add shopping.bring_not_configured i18n key in IT/EN/DE
Bump app.js cache buster to v=20260504b
Toggle appears in the Language settings tab, below the language selector.
Default: disabled. When disabled, initInactivityWatcher() exits early so
the screensaver never activates. i18n added for it/en/de.
- Title always left-aligned (was centered via 3-col flex trick)
- In kiosk mode: exit/refresh buttons appear left of title via header-left
- All action buttons unified as .header-btn (42×42px, consistent style)
- Scan button: 48×48px + pulse animation to stand out from others
- Gemini button: no longer misuses header-scan-btn class; own indigo tint
- Scale status: same 42×42px .header-btn shape with colored .scale-dot
inside instead of a tiny 22px standalone circle
- Update notification: uses #header-update-badge beside the title instead
of replacing title innerHTML (title never disappears anymore)
- Fixed _scaleUpdateStatus() to preserve header-btn class on className reset
Header layout:
- Redesign header-content as 3-column flex (left / center / right)
- Add #header-left div: dedicated slot for kiosk buttons (empty by default)
- header-title: flex-shrink auto, no more position:absolute centering hack
- header-actions: flex:1 1 0 + justify-content:flex-end (right)
- header-left: flex:1 1 0 (left) — equal width balances the title visually
Kiosk exit/refresh buttons:
- _injectKioskOverlay() now appends to #header-left instead of
insertBefore(firstChild) — buttons appear on LEFT, not mixed with center
DB migration:
- Add shopping_name TEXT DEFAULT '' to CREATE TABLE products schema
- Add ALTER TABLE migration in migrateDB() for existing databases
- Avoids repeated ALTER TABLE in seed code on every request
Demo mode UI:
- _applyDemoModeUI(): hides ⚙️ settings nav button in demo mode
- Suppresses first-run setup wizard when _demoMode === true
- Shows a small DEMO badge in header-left
- Called from both syncSettingsFromDB() and _initApp()
1. Remove raw API key from get_settings response
- getServerSettings() no longer returns gemini_key in plain text
- Only gemini_key_set (boolean) and settings_token_set (boolean)
- JS updated to only check gemini_key_set (removes stale gemini_key fallback)
2. Protect save_settings with SETTINGS_TOKEN
- If SETTINGS_TOKEN is set in .env, all save_settings calls must
include matching X-Settings-Token header (uses hash_equals)
- Empty token = no protection (backwards-compatible default)
- Settings UI (Security tab) has a token input field
- Wrong/missing token returns HTTP 403 with error 'unauthorized'
- JS shows '🔒 Token non valido o mancante' on 403
3. DEMO_MODE native blocking in PHP
- DEMO_MODE=false added to .env (default off)
- When DEMO_MODE=true, all write actions return HTTP 403 before routing
- Blocked: save_settings, product_save/delete/merge, inventory_add/use/update/remove,
dismiss_anomaly, bring_add/remove/sync
- demo_mode flag exposed via get_settings so JS can adapt UI
Header title centering:
- .header-content: remove max-width:600px, use position:relative + justify-content:center
- .header-title: position:absolute; left:50%; transform:translateX(-50%)
so the title is always at the exact center of the header regardless of
screen width or how many action buttons are on the right
- Added max-width:calc(100% - 200px) to prevent overlap with action buttons
on narrow screens
Spesa skeleton preloader:
- index.html: add stat-loading class to stat-spesa (was missing, other 3 had it)
- app.js showPage('dashboard'): add 'spesa' to the skeleton init array
- app.js loadShoppingCount(): remove stat-loading class after data loads
(like loadDashboard() does for the other 3 locations)
APK install conflict:
- Replace ACTION_VIEW-based install with PackageInstaller.Session API (API 21+)
- PackageInstaller gives us the actual install status via BroadcastReceiver:
STATUS_PENDING_USER_ACTION → launch system confirmation dialog automatically
STATUS_SUCCESS → success toast
STATUS_FAILURE_CONFLICT/INCOMPATIBLE → show AlertDialog offering to
uninstall the old version (ACTION_DELETE) so user can re-download and install
- FileProvider no longer needed for install (still kept for other uses)
- Kiosk: derive target package from filename (gateway vs kiosk self-update)
Dashboard 0-flash:
- Replace hardcoded 0 in HTML stat-value spans with ... placeholder
- Add .stat-loading CSS class: shimmer skeleton animation (gradient sweep)
- showPage(dashboard): set ... + stat-loading before API call
- loadDashboard: remove stat-loading class and set real count after data arrives
- 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)
- 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
- 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
- Retry loop extended to 10s (50×200ms) for slow Android WebViews
- Show 'Nessuna voce disponibile' after timeout instead of infinite loader
- Show 'Voce non supportata dal browser' if speechSynthesis missing
- Reset to loading state on each settings open (fixes stale empty select)
- Add refresh button ↺ to force-reload voices manually
- 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
- 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!
- index.html: replace broken byte with 📋 emoji in Storico nav button
- submitRecipeUse: pass 'Ricetta: <title>' as notes to inventory_use API
so every ingredient use is linked to the recipe in the DB
- loadLog: render recipe note as small italic line 🍳 below log-detail row
- style.css: add .log-recipe-note style (0.75rem, muted, italic)
- 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
- Green-themed banner between settings panels and Save button
- Direct download link to kiosk-latest release APK
- Auto-hidden when running inside Android WebView (kiosk mode)
- i18n: translations added for it/en/de
- Fix progress-bar restarting continuously when weight is stable:
add _cancelScaleTimersOnly() that stops timers/animations without
_cancelScaleTimersOnly() so the same value resumes counting when
stability returns instead of always restarting the 5-s wait.
Add 'else if' branch in _scaleAutoFillUse / _scaleAutoFillRecipeUse
to restart stability wait after brief instability for the same value.
- Show red blinking warning in scale-live-box when weight < 10 g:
adds scale-low-weight CSS class with pulsing border/shadow animation,
the label shows '< 10 g · inserisci manualmente' instead of the
stability progress bar. No auto-confirm fires below 10 g.
- Gateway Android app: scale auto-reconnect now retries indefinitely.
isAutoReconnecting flag keeps the scan→wait→scan cycle running until
the scale is found again; onScanStopped schedules a new scan after
10 s whenever autoReconnect is active and scale is still offline.
Two root causes:
1. _useNormalUnit was stale ('pz') for conf products because it's only
updated in normal mode — fix: resolve effective unit from
_useConfMode.packageUnit when in sub mode
2. Food scales in liquid mode send unit='ml' directly — was falling
through to raw value, skipping density; fix: detect scaleAlreadyMl
flag to use ml directly for ml target, or apply density for g target
Also: add scale pre-fill call after switchUseUnit('sub') in conf mode
- _scaleDensityForProduct(): returns g/ml density based on product name/category
olio oliva 0.91, girasole 0.92, spirits 0.94, aceto/panna 1.01, latte 1.03,
yogurt 1.05, succo 1.04, miele/sciroppo 1.40, default 1.00 (water)
- _scaleAutoFillUse(): normalises all scale units to grams first, then converts
to target unit; when unit=ml applies density; never touches pz/conf
- _scaleAutoFillUse(): converts and fills use-quantity on stable readings
- _scaleLatestWeight pre-fills when use page opens (after unit is known)
- oninput on use-quantity pauses auto-fill when user types manually
- showUseForm() resets paused flag so next opening resumes auto-fill
- Small 'live' hint text shown while auto-filling, hidden when overridden