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 @@
+
+
+