README:
- Added prominent live demo banner at the top with links to
https://evershelfproject.dadaloop.it/demo and project website
Wiki (docs/wiki/):
- Home.md — overview, table of contents, what's new
- Installation.md — Docker, Apache, Nginx, HTTPS, cron, backup
- Configuration.md — full .env reference, settings UI, rate limits
- Features.md — complete feature documentation
- API-Reference.md — all REST endpoints with params/responses
- Android-Kiosk.md — setup wizard, permissions, troubleshooting
- Scale-Gateway.md — BLE protocol, setup, troubleshooting
- Translations.md — how to add/edit language files
- Contributing.md — dev workflow, branch strategy, CI, code style
- FAQ.md — common issues and solutions
- 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
- installApk(): add runtime canRequestPackageInstalls() check with user dialog
(permission may have been revoked or not active even if declared in manifest)
- STATUS_FAILURE else branch: show AlertDialog with full diagnostics on screen
(status code, message, APK size, Android version, device model) so the
problem is visible even when the server error report fails to deliver
- Dialog has Riprova button (retries installWithPackageInstaller immediately)
and Salta button (skips to gateway status check)
- INSTALL_PERM_REQUEST onActivityResult: resume from installApk() if a local
APK file is already present, not just from triggerApkDownload()
- ErrorReporter context enriched with device model string
Two bugs caused the gateway APK install to always fail with status=1:
1. setAppPackageName() removed from SessionParams
This optional call forces the installer to verify the package name
against the APK's manifest. On some OEM/Android versions this
comparison fails even when the name is correct, returning the generic
STATUS_FAILURE (1) with no EXTRA_STATUS_MESSAGE. Removing it lets
the installer proceed without the extra check.
2. BroadcastReceiver was unregistered on STATUS_PENDING_USER_ACTION
On Android 11+ the final install result (STATUS_SUCCESS/STATUS_FAILURE)
arrives as a SECOND broadcast AFTER the user confirms the dialog.
The receiver was being unregistered immediately on the first broadcast
(PENDING_USER_ACTION), so the final result was never received.
Fix: only unregister on terminal statuses (SUCCESS, FAILURE, ABORTED).
Additional improvements:
- STATUS_FAILURE_ABORTED (-1) handled explicitly: resets UI without
showing an error (user just pressed back on the confirmation dialog)
- session.abandon() called on exception instead of letting .use{} close
- ErrorReporter now includes apk_kb and android API level in context
- onActivityResult(INSTALL_CONFIRM_REQUEST) no longer sets success/failure
UI (the BroadcastReceiver is responsible for the final result)
SetupActivity was sending type 'install-failure' (hyphen) but the PHP
version-guard bypass list only checked for 'install_failure' (underscore).
Result: if the kiosk was not on the latest released version the error was
silently discarded and no GitHub issue was created.
Fix:
- SetupActivity: change type to 'install_failure' (underscore, consistent
with KioskActivity which already used the correct name)
- api/index.php: add 'install-failure' (hyphen) to the bypass list as
defensive fallback so old APK builds already in the field are covered too
ErrorReporter:
- Init ErrorReporter at Setup onCreate using any previously saved URL
(before the fix, init() was only called at step 3, so install errors
happening in step 4 were silently dropped)
- Add ErrorReporter.reportMessage() call in the STATUS_FAILURE else branch
of installWithPackageInstaller (was showing error UI but sending nothing)
Gateway pre-configuration:
- finishSetup() now detects has_scale=true + gateway installed
- If so, POSTs scale_enabled=true + scale_gateway_url=ws://127.0.0.1:8765
to the EverShelf server's save_settings API endpoint
- This means the webapp works with the scale out-of-the-box after setup
without the user having to go into web Settings and configure it manually
Permissions step:
- Add btnGrantPerms as class field (was only inline findViewById)
- Extract onPermissionsGranted() helper: transforms button to green '✅ Permessi
concessi — Continua →' and sets click listener to showStep(3)
instead of calling onPermissionsGranted() which advances to step 3 (Server)
- Remove the 600ms auto-advance; user controls when to proceed
- Add setup_perms_granted_next string in EN/IT/DE
Network discovery — wrong subnet fix:
- Skip virtual/VPN/cellular interfaces: tun, ppp, rmnet, pdp, ccmni, dummy, sit,
gre, v4-, v6-, p2p, ham, nordlynx prefixes
- Also skip intf.isVirtual interfaces
- Sort: wlan*/eth* interfaces first (highest priority), others after
- Show detected subnet(s) in UI immediately before scan starts
Problems fixed:
- f.get() sequential collection blocked on timed-out futures in submission order
→ replaced with ExecutorCompletionService: results collected as soon as ready
- WifiManager.getConnectionInfo() deprecated on Android 10+, could return IP=0
→ replaced with NetworkInterface.getNetworkInterfaces() for subnet detection
- No real-time feedback: status stuck on 'Scanning…' throughout
→ UI updated every ~120ms showing current IP:port (n/total)
- TCP socket pre-check (600ms) before HTTP probe: filters unreachable hosts instantly
→ reduces scan time from minutes to seconds on typical /24 networks
- executor.shutdownNow() cancels remaining tasks the moment server is found
- 60-thread pool instead of 40 for faster parallel scanning
- Remove unused WifiManager import
- 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
- SetupActivity: new Step 0 — language picker (IT/EN/DE) with large buttons,
hardcoded trilingual title so it's always readable; saves 'kiosk_language' pref,
calls recreate() via onSaveInstanceState to reload the Activity in chosen locale
- SetupActivity: new Step 5 — screensaver toggle (before Done), saves 'screensaver_enabled'
- All existing steps shifted: Welcome→1, Permissions→2, Server→3, Scale→4, Done→6
- Progress dots updated to 5 dots (steps 1-5)
- attachBaseContext override in SetupActivity, KioskActivity, SettingsActivity to
apply the saved locale to all Activities via SetupActivity.applyLocale()
- buildSummary now shows language, screensaver setting, and scale status
- New string resources: setup_screensaver_*, summary_lang, summary_scale_skip,
summary_screensaver_on/off in IT, EN, DE
Screensaver toggle init was incorrectly placed inside _populateLanguageSelector()
where 's' (getSettings()) is not in scope. Moved to loadSettingsUI() alongside
the other preference checkboxes where 's = getSettings()' is already defined.
- cleanupObsoleteBringItems now protects items the user explicitly added
from the suggestions panel via a '_userPinnedBring' localStorage set
(30-day TTL, cleared on force-sync)
- cleanup now protects ALL smart-predicted items (any urgency), not only
critical/high — if the algorithm still flags it, it should stay in list
- autoAddCriticalItems: bypass purchased-blocklist for depleted items
(current_qty=0) so products that ran out are always re-added to Bring
- forceSyncBring also clears _userPinnedBring for a full reset
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
screen.orientation.lock() returns a Promise; the old try/catch only
caught synchronous errors, leaving the rejection unhandled and triggering
the auto-reporter (issue #8). Added .catch(()=>{}) to suppress it.
Also fixed CI: add sleep+retry around gh release create to avoid 502
race condition after delete.
Closes#8
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
Feature 1: AI product storage/shelf-life hint
- New API: gemini_product_hint → {location, expiry_days, reason}
- After opening the add form, Gemini suggests optimal storage and expiry
- Shown inline next to expiry estimate as a subtle AI badge with tooltip
- Also updates location buttons if AI suggests a different location
- Cached permanently in food_facts_cache.json (per name+lang)
Feature 2: AI-enriched shopping suggestions
- New API: gemini_shopping_enrich → adds tip field to each suggestion
- After bring_suggest renders, Gemini adds practical buying/storing tips
- Tips shown inline under each suggestion item in indigo italic text
- Cached per item list + lang in food_facts_cache.json
Feature 3: AI anomaly explanation
- New API: gemini_anomaly_explain → plain-language explanation
- '🤖 Spiega' button added to anomaly banners (when Gemini available)
- Explains in 2-3 conversational sentences why the discrepancy likely happened
- Replaces technical banner detail text with friendly explanation
- No caching (anomaly context is always specific)
Added _geminiAvailable global flag (false by default):
- Set in _initApp() from serverSettings.gemini_key_set after app loads
- Updated in syncSettingsFromDB() so it stays current if key is added later
Added _requireGemini() helper:
- Returns true if Gemini key is configured → proceed normally
- Returns false + shows a warning toast if key is missing → abort
Added _updateGeminiButtonState():
- Adds .header-btn-no-ai CSS class to Gemini button when key is missing:
greyed out, slight grayscale filter, amber dot badge in corner
- Updates button tooltip to explain what to do
- Removes class/restores normal appearance when key is present
All 6 AI entry points now call _requireGemini() as first line:
captureForAI() — AI product identification from scan page
captureForAIFormFill() — AI product fill in manual add form
scanExpiryWithAI() — AI expiry date reader
openRecipeDialog() — recipe generation dialog
generateRecipe() — recipe generation (direct call path)
quickRecipeSuggestion() — quick expiring-products recipe (→ chat)
showPage('chat') — Gemini chat page
Previously: user would click the button, camera would open, API call
would fail, and only THEN see an error message deep in the flow.
Now: blocked immediately at the entry point with a clear toast.
Instead of a fixed banner that covers the top of the page, the update
notification now replaces only the header title area (the centered title):
- .header-title content is swapped in-place with an animated pill:
⬆️ v1.x.x [Aggiorna] ✕
- Pulsing animation (header-update-pulse) draws attention without being
intrusive; camera and Gemini buttons stay exactly where they are
- [Aggiorna] button does window.location.reload()
- [✕] dismisses: for a release update stores publishedAt so it won't reappear;
for a server deploy simply restores title (reappears next 5-min check)
- Auto-restores after 60 s without marking as seen
- Removed the old fixed position:fixed banner entirely
CSS header fixes:
- .header-content: justify-content:flex-end so .header-actions (camera, Gemini)
naturally stays at the right edge as a flex child
- .header-title: removed overflow:hidden and text-overflow:ellipsis that were
clipping the version number; title stays absolutely centered
- Cleaned up unused max-width:none and margin:0 from previous broken attempt
Real-time webapp update detection:
- Added module-level _loadedVersion captured at page load (version in HTML header)
- _checkWebappUpdate() now has two checks:
1. webapp_version from server vs _loadedVersion: if different, the server was
updated since this page was loaded → show '🔄 Nuova versione disponibile' banner
2. GitHub latest release vs _loadedVersion (existing behaviour)
Different banner messages: deploy-changed shows simple reload prompt;
release-newer shows version + changelog link (same as before)
- TTL reduced from 6h to 5 min so updates are detected quickly
- _checkWebappUpdate() now also fires on visibilitychange so the user sees
the banner as soon as they return to the tab after a deploy
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)
bringAddItems() used $input and $items without ever decoding the request
body. $items was undefined (null) so the foreach never ran, every call
returned added=0 skipped=0 regardless of what was sent.
Added:
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$items = $input['items'] ?? [];
Also added the missing $auth guard (consistent with all other Bring functions).
The try expression had a spurious 'true' result in one branch which
made Kotlin infer the type as Any? instead of ByteArray?.
Simplified to a single try block with explicit type annotation
ByteArray? to eliminate the ambiguity.
Bug 1 — Uninstall loop (kiosk lock task blocks system uninstall UI):
startActivityForResult(ACTION_DELETE) was called while lock task was
active. The system uninstall activity is not in the lock task whitelist
so it either silently fails or creates an unresolvable loop.
Fix: call disableKioskLock() immediately before every ACTION_DELETE
intent (3 call sites). Call enableKioskLock() at the start of
onActivityResult(UNINSTALL_REQUEST) before retrying install.
Added 600 ms delay after uninstall so PackageManager finishes cleanup.
Bug 2 — Step 2 only checks HTTP connectivity, not PHP API:
testConnection() was checking the root URL only. A generic web server
could pass while the EverShelf PHP API was absent.
Fix: after HTTP 200-399 on the root URL, do a second GET to
/api/?action=check_update and check the response body contains
'latest_tag'|'webapp_version'|'ok'. Shows:
✅ Server EverShelf trovato e API attiva!
⚠ Server raggiungibile ma API PHP non trovata (codice N)
Bug 3 — STATUS_FAILURE=1 even after uninstall (invalid APK file):
GitHub DownloadManager follows redirects; if the release asset does
not exist yet, GitHub returns a 404 HTML page but DownloadManager
still reports STATUS_SUCCESSFUL. PackageInstaller then tries to parse
HTML as an APK and returns STATUS_FAILURE=1.
Fix: validate APK magic bytes (0x504B = 'PK') before calling
installWithPackageInstaller. If invalid: show error, delete corrupt
file, send ErrorReporter event, re-enable retry button.
Also renamed install error string to install_error_install (separate
from install_error_download) for clarity.
Bug 4 — ErrorReporter.serverBaseUrl empty during wizard install:
ErrorReporter.init() is called in onCreate() with the saved URL.
On first setup the URL is typed in step 2 and saved to prefs, but
ErrorReporter still has serverBaseUrl='' for the rest of that session.
Any install error in step 3 silently failed to POST.
Fix: call ErrorReporter.init(this, url) in btnStep2Next immediately
after prefs.edit().putString(KEY_URL, url) so step 3 has a live URL.
Server check (wizard step 3):
- New horizontal card above the scale question always shows server status
as soon as step 3 is entered: ⏳ checking → ✅ reachable / ⚠️ not reachable
- Pings GET $serverUrl/api/?action=check_update (5 s timeout)
- If reachable: 'Error reporting active — failures sent to GitHub Issues'
- If not reachable: 'Check the URL in step 2' warning
- checkServerReachability() called every time goToStep(3) runs
- Strings added in EN / IT / DE
Signature-conflict fallback (else branch in installWithPackageInstaller):
- When PackageInstaller returns a generic STATUS_FAILURE and the target
package is already installed, a signature conflict is the most likely
cause (CONFLICT/INCOMPATIBLE are caught separately earlier)
- New AlertDialog: 'Disinstalla e riprova' → startActivityForResult
ACTION_DELETE → UNINSTALL_REQUEST → auto-retries install on return
- Only shown when all else has already failed
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.
Root cause of 'stuck on downloading' bug (Android 13+):
DownloadManager.ACTION_DOWNLOAD_COMPLETE is sent by the system process,
which is external to our app. Registering the receiver with
RECEIVER_NOT_EXPORTED silently drops the broadcast — the BroadcastReceiver
never fires, the install never starts, and the UI stays frozen at
whatever progress percentage the poller last saw.
Fix: use RECEIVER_EXPORTED for the DownloadManager completion receiver in
both kiosk and scale-gateway apps.
The PackageInstaller result receiver (internal PendingIntent broadcast,
same package) correctly keeps RECEIVER_NOT_EXPORTED — that one is
intentionally app-private.
ErrorReporter.report() takes a Throwable as first argument.
The three new calls added in fe633c9 incorrectly passed 'this'
(Context) instead, causing compileDebugKotlin to fail.
Replace with ErrorReporter.reportMessage(type, message) which
is the correct overload for non-exception error events.
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