Problem: tapping 'Aggiorna Scale Gateway' gave no visible feedback after
the button was pressed — user could not tell if the download was
happening, stuck, or had silently failed.
Changes:
- layout: add horizontal ProgressBar (determinate) + percentage TextView
inside the wizard step-3 status card
- layout: add thin ProgressBar (4 dp) at the bottom of the update banner
(banner changed to vertical orientation to accommodate it)
- startDownloadProgressPoll(downloadId): polls DownloadManager every
500 ms, reads COLUMN_BYTES_DOWNLOADED_SO_FAR and COLUMN_TOTAL_SIZE_BYTES,
updates status card + banner with 'Download: 45% (18.2 MB / 40.5 MB)'
- setInstallUI(): new 'progress' parameter (-2 = hide, -1 = indeterminate,
0-100 = determinate) and 'progressText' for the label under the bar
— both bars updated in sync
- Status transitions now visible:
⏳ Download: 45% [====----] 18.2 MB / 40.5 MB
⏳ Installazione in corso… [~~~~] (indeterminate)
⏳ + 'Conferma nel dialog…'
✅ Installato! → bar hidden, gateway status re-checked after 3 s
❌ + error detail → bar hidden, button re-enabled as '↩ Riprova'
- All error paths (download fail, PackageInstaller exception, installer
failure status) now call ErrorReporter.report() → GitHub Issue created
automatically so failures are tracked without user intervention
- Dismiss button also cancels the progress poll + hides the bar
Problem: tapping 'Aggiorna ora' showed a fleeting 'Download avviato'
Toast and then nothing — no feedback on download progress, installer
state, success or failure.
Solution — setInstallUI() central helper:
- Updates the wizard step-3 status card (icon + title + detail line)
OR the update banner (tvUpdateMessage) depending on which is visible
- Always updates and enables/disables the button that triggered the flow
States shown (status card + button text):
⏳ Scaricamento in corso… (download started)
⏳ Installazione in corso… (download done, PackageInstaller running)
⏳ Installazione in corso… + 'Conferma nel dialog…' (user action needed)
✅ Installato con successo! (onActivityResult RESULT_OK or STATUS_SUCCESS)
→ after 3 s auto-refreshes gateway status + closes banner
❌ Download fallito / Installazione non riuscita
→ button re-enabled as '↩ Riprova'
Strings added (EN default + IT + DE):
install_downloading, install_downloading_detail
install_installing, install_confirm_detail
install_success, install_success_detail
install_error_download, install_error_download_detail
install_perm_detail, install_btn_retry
New job 'auto-merge-to-main' in ci.yml:
- needs: lint-php, lint-js, docker-build, validate-translations
- only runs when github.ref == refs/heads/develop
- uses git merge --no-ff so history is preserved
- push to develop → CI passes → main updated automatically
Wizard step 3 — 'Do you have a Bluetooth smart scale?':
- New question card with two buttons shown first:
✅ Yes → reveal gateway status card + bottom nav buttons
➡️ No → save KEY_HAS_SCALE=false, skip to web view
- KEY_HAS_SCALE pref controls whether the gateway is auto-launched
both after wizard completion and on every subsequent app start
- checkGatewayStatus(): uses string resources (multilingual)
- checkGatewayUpdate(): fetches GitHub release, compares version;
if gateway needs an update shows '📥 Update Scale Gateway' button
that triggers triggerApkDownload() (full PackageInstaller flow)
- onResume step-3 re-check only fires when status card is visible
(i.e. user already answered 'Yes') — handles return from install
Multi-language: strings.xml added for EN (default), IT, DE
strings: wizard_step3_question/yes/no, wizard_gateway_installed/
not_installed/checking/up_to_date/update_available/update_detail,
btn_back/launch/launch_no_scale/download_gateway/update_gateway
Webapp update banner:
- 'Vedi novità' link replaced with 'Aggiorna ora' button
- Clicking 'Aggiorna ora' does a hard page reload (?bust=timestamp)
which forces the browser to fetch the latest files from the server
- GitHub release URL kept as a small secondary 'novità' link
APK install conflict (kiosk + scale gateway):
- STATUS_PENDING_USER_ACTION: changed startActivity → startActivityForResult
(kiosk) / installConfirmLauncher.launch (gateway) so we get notified
if the system installer fails due to signature conflict
- On non-OK result from system installer: show AlertDialog offering to
uninstall, using UNINSTALL_REQUEST / uninstallLauncher
- STATUS_FAILURE_CONFLICT/INCOMPATIBLE: same uninstall flow
- After uninstall completes, install automatically retries with the
saved APK file (pendingInstallFile) — no manual re-download needed
- Gateway: also saves destFile to pendingInstallFile at download time
The function was wrapped as a named function expression (function foo(){})
which scopes the name 'foo' only to the function body, not the outer scope.
_initApp called setTimeout(_checkWebappUpdate, 6000) causing:
ReferenceError: _checkWebappUpdate is not defined
Fix: remove the (...) wrapper so it becomes a regular function declaration
visible to the entire module scope.
Fixes#7
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)
Root causes fixed:
- REQUEST_INSTALL_PACKAGES permission missing from both manifests
- FileProvider not declared in either manifest (FileProvider.getUriForFile() crashed)
- res/xml/file_paths.xml missing (required by FileProvider)
- setDestinationInExternalPublicDir() used public Downloads dir (needs storage
permission + FileProvider can't serve it); replaced with getExternalFilesDir()
which is app-private, needs no permission, and IS accessible by FileProvider
- canRequestPackageInstalls() check returned early with startActivity (fire-and-
forget); user could never retry. Now uses startActivityForResult/installPermLauncher
so the download auto-retries when user returns from the Settings screen
- Added download status check (COLUMN_STATUS == STATUS_SUCCESSFUL) before installing
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.
- submitUseAll() now detects opened packages: if current location has an opened pack, finishes only that; if exactly one opened pack exists elsewhere, uses it automatically; multiple opened packs → disambiguation modal
- quickUse() resets scale baseline on page open so stale weight doesn't immediately trigger auto-fill
- Expired alerts (dashboard + banner) now filter out freezer items within their safety window (level='ok')
- Review banner: conf unit quantity displayed as sub-unit total (e.g. 800g) instead of raw pack count; high-qty threshold evaluated on sub-unit volume to prevent '400 confezioni' nonsense
- Low-stock review banner gains 'È finito tutto' button → new bannerFinishAll() handler
- New _submitUseAllAt() helper and _showUseAllDisambiguation() modal
- New translation keys: toast_opened_finished, disambiguation_hint, disambiguation_all, banner_review_action_finish (it/en/de)
A partially-used fridge entry (e.g. 191 ml of milk) triggered a
'suspiciously low quantity' banner even when sealed packages of the
same product were present in another location (e.g. pantry).
Fix: before pushing a low-qty review alert, group all inventory rows
by product key (barcode, or name+brand fallback). If any sibling entry
for the same product has qty > 0 in a different row, skip the alert.
High-qty and suspicious package-size alerts are unaffected.
- 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
- Convert LOCATIONS labels to use t('locations.*')
- Convert SHOPPING_SECTIONS labels to use t('shopping_sections.*')
- Convert CATEGORY_LABELS to use t('categories.*')
- Convert MEAL_PLAN_TYPES to use t('meal_plan_types.*')
- Convert WEEK_DAYS_SHORT to use t('days.*_short')
- Convert MEAL_TYPES to use t('meal_types.*')
- Convert MEAL_SUB_TYPES to use t('meal_sub.*')
- Convert meal-plan column headers to use translated meal_types
- Replace inline locLabels/LOC_LABELS with translated LOCATIONS object
- Fix shopping action buttons: bring_add_n, bring_add_selected, bring_adding, bring_added_*
- Fix recipe archive empty state
- Fix meal plan reset success toast
- Fix meal plan suggestion hint and screensaver display
- Fix settings save status messages (saved, saved_local, saved_local_error)
- Fix product edit form title
- Fix kiosk session phrases for screensaver counter
- Add cooking.expires_chip translation for expiry date format
- Add meal_plan section (reset_success, suggested_by)
- Add error.select_items for Bring shopping validation
- All strings now properly internationalized for EN/DE languages
- 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