From 58e69625bd3425295f9e66a551246426133d5a4e Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Sun, 3 May 2026 17:46:42 +0000 Subject: [PATCH] fix: preloader + update notification robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- api/index.php | 6 ++- assets/css/style.css | 36 +++++++++++++ assets/js/app.js | 53 ++++++++++++++----- .../dadaloop/evershelf/kiosk/KioskActivity.kt | 33 +++++++----- .../evershelf/scalegate/MainActivity.kt | 12 +++-- index.html | 8 +++ 6 files changed, 118 insertions(+), 30 deletions(-) diff --git a/api/index.php b/api/index.php index 9832c69..735eb85 100644 --- a/api/index.php +++ b/api/index.php @@ -5720,7 +5720,11 @@ function _isLatestVersion(string $clientVersion): bool { if ($clientVersion === '') return true; // unknown → allow (don't suppress) $latest = _latestReleaseTag(); if ($latest === '') return true; // no release yet → allow - return ltrim($clientVersion, 'v') === ltrim($latest, 'v'); + $latestNorm = ltrim($latest, 'v'); + // If tag is not semver-like (e.g. "latest", "rolling") we can't compare + // meaningfully, so don't suppress error reporting. + if (!preg_match('/^\d+\.\d+/', $latestNorm)) return true; + return ltrim($clientVersion, 'v') === $latestNorm; } /** diff --git a/assets/css/style.css b/assets/css/style.css index b20faa2..b8959e3 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -68,6 +68,42 @@ body { box-shadow: var(--shadow); } +/* ===== PRELOADER ===== */ +#app-preloader { + position: fixed; + inset: 0; + background: var(--bg-dark, #0f172a); + z-index: 200000; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.35s ease; +} +#app-preloader.fade-out { + opacity: 0; + pointer-events: none; +} +.app-preloader-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} +.app-preloader-spinner { + width: 48px; + height: 48px; + border: 4px solid rgba(255,255,255,0.15); + border-top-color: #4ade80; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +.app-preloader-label { + color: rgba(255,255,255,0.75); + font-size: 1.2rem; + font-weight: 600; + letter-spacing: 0.5px; +} + .header-content { display: flex; align-items: center; diff --git a/assets/js/app.js b/assets/js/app.js index 3ea6f85..7ee0e07 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -79,19 +79,28 @@ function reportError(payload) { // Checks the latest GitHub release once per session and shows a text banner // if the running webapp version is outdated. (function _checkWebappUpdate() { - const STORAGE_KEY = '_evershelf_update_checked'; + const STORAGE_KEY = '_evershelf_update_checked_at'; // last-checked timestamp + const SEEN_KEY = '_evershelf_update_seen_ts'; // published_at of last-dismissed release + const TTL_MS = 6 * 60 * 60 * 1000; // re-check every 6 h (localStorage) const now = Date.now(); - const lastCheck = parseInt(sessionStorage.getItem(STORAGE_KEY) || '0', 10); - if (now - lastCheck < 3 * 60 * 60 * 1000) return; // once per 3 h per tab - sessionStorage.setItem(STORAGE_KEY, String(now)); + const lastCheck = parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10); + if (now - lastCheck < TTL_MS) return; + localStorage.setItem(STORAGE_KEY, String(now)); fetch('api/index.php?action=check_update', { method: 'GET' }) .then(r => r.ok ? r.json() : null) .then(data => { - if (!data || !data.latest_tag) return; - const latest = data.latest_tag.replace(/^v/, ''); - const current = (document.querySelector('.header-version')?.textContent?.trim() || '').replace(/^v/, ''); - if (!current || !latest || current === latest) return; + if (!data) return; + // Release date-based comparison: show banner only if the release is + // newer than the last one the user acknowledged. + const publishedAt = data.published_at || ''; + const seenTs = localStorage.getItem(SEEN_KEY) || ''; + if (!publishedAt || publishedAt === seenTs) return; + + const latestTag = (data.latest_tag || '').replace(/^v/, ''); + const current = (document.querySelector('.header-version')?.textContent?.trim() || '').replace(/^v/, ''); + // If tag looks like a proper semver and they match → no update needed + if (/^\d+\.\d+/.test(latestTag) && current && current === latestTag) return; // Show a dismissible banner at the top of the page if (document.getElementById('_evershelf_update_banner')) return; @@ -105,18 +114,23 @@ function reportError(payload) { 'border-bottom:2px solid #fbbf24', 'box-shadow:0 2px 8px rgba(0,0,0,.4)', ].join(';'); - const releaseUrl = data.html_url || `https://github.com/dadaloop82/EverShelf/releases/latest`; + const releaseUrl = data.html_url || 'https://github.com/dadaloop82/EverShelf/releases/latest'; + const versionText = /^\d+\.\d+/.test(latestTag) ? ` ${latestTag}` : ''; banner.innerHTML = - `⬆️ EverShelf ${latest} disponibile (stai usando ${current}). ` + + `⬆️ Nuovo aggiornamento EverShelf${versionText} disponibile. ` + `Vedi novità` + - ``; document.body.prepend(banner); - // Auto-dismiss after 20 s - setTimeout(() => banner.remove(), 20000); + document.getElementById('_evershelf_banner_close').onclick = () => { + localStorage.setItem(SEEN_KEY, publishedAt); // mark as seen + banner.remove(); + }; + // Auto-dismiss after 30 s (without marking as seen, so it reappears next visit) + setTimeout(() => banner.remove(), 30000); }) .catch(() => {}); -})(); +}); // ── Global uncaught error handler ──────────────────────────────────────────── window.addEventListener('error', function(e) { @@ -11705,6 +11719,17 @@ async function _initApp() { scaleInit(); // connect to smart scale gateway if configured _injectKioskOverlay(); // kiosk X / refresh buttons (only when running inside Android WebView) + // Hide preloader once the dashboard is rendered + const preloader = document.getElementById('app-preloader'); + if (preloader) { + preloader.classList.add('fade-out'); + setTimeout(() => preloader.remove(), 380); + } + + // Defer update check: fire 6 s after app is ready so it doesn't compete + // with initial API calls and the PHP worker isn't blocked during startup. + setTimeout(_checkWebappUpdate, 6000); + // ── Background intervals ─────────────────────────────────────────────── // 1) Ogni 5 min: ricarica la pagina corrente (scadenze, inventario, ecc.) setInterval(() => { diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt index 0ff8853..f9ebd98 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt @@ -699,21 +699,16 @@ class KioskActivity : AppCompatActivity() { // Normalise: strip leading 'v' for comparison val norm = { v: String -> v.trimStart('v') } - - val kioskNeedsUpdate = latestTag.isNotEmpty() && currentKiosk.isNotEmpty() && - norm(latestTag) != norm(currentKiosk) - val gatewayNeedsUpdate = currentGateway != null && latestTag.isNotEmpty() && - norm(latestTag) != norm(currentGateway) - - if (!kioskNeedsUpdate && !gatewayNeedsUpdate) return@Thread + // If tag is not semver-like (e.g. "latest") we can't compare — treat as "needs update" + val isSemver = latestTag.trimStart('v').matches(Regex("\\d+\\.\\d+.*")) // Find APK download URLs in release assets val assets = json.optJSONArray("assets") - var kioskApkUrl = KIOSK_DOWNLOAD_URL - var gatewayApkUrl = GATEWAY_DOWNLOAD_URL + var kioskApkUrl = "" // only set if the release actually contains the APK + var gatewayApkUrl = "" if (assets != null) { for (i in 0 until assets.length()) { - val a = assets.getJSONObject(i) + val a = assets.getJSONObject(i) val name = a.optString("name", "").lowercase() val url = a.optString("browser_download_url", "") if (name.contains("kiosk") && url.isNotEmpty()) kioskApkUrl = url @@ -721,15 +716,29 @@ class KioskActivity : AppCompatActivity() { } } + // Kiosk needs update: APK is in release AND (non-semver tag OR version mismatch) + val kioskHasApk = kioskApkUrl.isNotEmpty() + val kioskNeedsUpdate = kioskHasApk && currentKiosk.isNotEmpty() && + (!isSemver || norm(latestTag) != norm(currentKiosk)) + + // Gateway needs update: installed AND APK in release AND (non-semver OR mismatch) + val gatewayHasApk = gatewayApkUrl.isNotEmpty() + val gatewayNeedsUpdate = currentGateway != null && gatewayHasApk && + (!isSemver || norm(latestTag) != norm(currentGateway)) + + if (!kioskNeedsUpdate && !gatewayNeedsUpdate) return@Thread + // Build message and choose primary download (kiosk takes precedence) val lines = mutableListOf() var primaryApkUrl = "" if (kioskNeedsUpdate) { - lines += "🔄 Kiosk $currentKiosk → $latestTag" + val label = if (isSemver) "$currentKiosk → $latestTag" else latestTag + lines += "🔄 Kiosk $label" primaryApkUrl = kioskApkUrl } if (gatewayNeedsUpdate) { - lines += "🔄 Scale Gateway $currentGateway → $latestTag" + val label = if (isSemver) "$currentGateway → $latestTag" else latestTag + lines += "🔄 Scale Gateway $label" if (primaryApkUrl.isEmpty()) primaryApkUrl = gatewayApkUrl } val message = lines.joinToString(" • ") diff --git a/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt b/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt index bf6e4bb..fe242db 100644 --- a/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt +++ b/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt @@ -427,10 +427,10 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener val latestTag = json.optString("tag_name", "").ifEmpty { return@Thread } val current = try { packageManager.getPackageInfo(packageName, 0).versionName ?: "" } catch (_: Exception) { "" } val norm = { v: String -> v.trimStart('v') } - if (norm(latestTag) == norm(current)) return@Thread // already up to date + val isSemver = latestTag.trimStart('v').matches(Regex("\\d+\\.\\d+.*")) // Find scale-gateway APK in release assets - var apkUrl = APK_DOWNLOAD_URL + var apkUrl = "" val assets = json.optJSONArray("assets") if (assets != null) { for (i in 0 until assets.length()) { @@ -442,7 +442,13 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener } } } - val msg = "⬆️ Scale Gateway $current → $latestTag" + // Only show banner if the release actually contains our APK + if (apkUrl.isEmpty()) return@Thread + // If semver tag matches current version → already up to date + if (isSemver && norm(latestTag) == norm(current)) return@Thread + + val label = if (isSemver) "$current → $latestTag" else latestTag + val msg = "⬆️ Scale Gateway $label" runOnUiThread { showNativeUpdateBanner(msg, apkUrl) } } catch (_: Exception) {} }.start() diff --git a/index.html b/index.html index b2e81af..f0d0209 100644 --- a/index.html +++ b/index.html @@ -50,6 +50,14 @@ + + +