fix: preloader + update notification robustness
- 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)
This commit is contained in:
+5
-1
@@ -5720,7 +5720,11 @@ function _isLatestVersion(string $clientVersion): bool {
|
|||||||
if ($clientVersion === '') return true; // unknown → allow (don't suppress)
|
if ($clientVersion === '') return true; // unknown → allow (don't suppress)
|
||||||
$latest = _latestReleaseTag();
|
$latest = _latestReleaseTag();
|
||||||
if ($latest === '') return true; // no release yet → allow
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -68,6 +68,42 @@ body {
|
|||||||
box-shadow: var(--shadow);
|
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 {
|
.header-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+39
-14
@@ -79,19 +79,28 @@ function reportError(payload) {
|
|||||||
// Checks the latest GitHub release once per session and shows a text banner
|
// Checks the latest GitHub release once per session and shows a text banner
|
||||||
// if the running webapp version is outdated.
|
// if the running webapp version is outdated.
|
||||||
(function _checkWebappUpdate() {
|
(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 now = Date.now();
|
||||||
const lastCheck = parseInt(sessionStorage.getItem(STORAGE_KEY) || '0', 10);
|
const lastCheck = parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10);
|
||||||
if (now - lastCheck < 3 * 60 * 60 * 1000) return; // once per 3 h per tab
|
if (now - lastCheck < TTL_MS) return;
|
||||||
sessionStorage.setItem(STORAGE_KEY, String(now));
|
localStorage.setItem(STORAGE_KEY, String(now));
|
||||||
|
|
||||||
fetch('api/index.php?action=check_update', { method: 'GET' })
|
fetch('api/index.php?action=check_update', { method: 'GET' })
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data || !data.latest_tag) return;
|
if (!data) return;
|
||||||
const latest = data.latest_tag.replace(/^v/, '');
|
// Release date-based comparison: show banner only if the release is
|
||||||
const current = (document.querySelector('.header-version')?.textContent?.trim() || '').replace(/^v/, '');
|
// newer than the last one the user acknowledged.
|
||||||
if (!current || !latest || current === latest) return;
|
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
|
// Show a dismissible banner at the top of the page
|
||||||
if (document.getElementById('_evershelf_update_banner')) return;
|
if (document.getElementById('_evershelf_update_banner')) return;
|
||||||
@@ -105,18 +114,23 @@ function reportError(payload) {
|
|||||||
'border-bottom:2px solid #fbbf24',
|
'border-bottom:2px solid #fbbf24',
|
||||||
'box-shadow:0 2px 8px rgba(0,0,0,.4)',
|
'box-shadow:0 2px 8px rgba(0,0,0,.4)',
|
||||||
].join(';');
|
].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) ? ` <strong>${latestTag}</strong>` : '';
|
||||||
banner.innerHTML =
|
banner.innerHTML =
|
||||||
`<span>⬆️ <strong>EverShelf ${latest}</strong> disponibile (stai usando ${current}). ` +
|
`<span>⬆️ Nuovo aggiornamento EverShelf${versionText} disponibile. ` +
|
||||||
`<a href="${releaseUrl}" target="_blank" rel="noopener" style="color:#93c5fd;text-decoration:underline">Vedi novità</a></span>` +
|
`<a href="${releaseUrl}" target="_blank" rel="noopener" style="color:#93c5fd;text-decoration:underline">Vedi novità</a></span>` +
|
||||||
`<button onclick="document.getElementById('_evershelf_update_banner').remove()" ` +
|
`<button id="_evershelf_banner_close" ` +
|
||||||
`style="background:none;border:none;color:#94a3b8;font-size:18px;cursor:pointer;padding:0 4px">✕</button>`;
|
`style="background:none;border:none;color:#94a3b8;font-size:18px;cursor:pointer;padding:0 4px">✕</button>`;
|
||||||
document.body.prepend(banner);
|
document.body.prepend(banner);
|
||||||
// Auto-dismiss after 20 s
|
document.getElementById('_evershelf_banner_close').onclick = () => {
|
||||||
setTimeout(() => banner.remove(), 20000);
|
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(() => {});
|
.catch(() => {});
|
||||||
})();
|
});
|
||||||
|
|
||||||
// ── Global uncaught error handler ────────────────────────────────────────────
|
// ── Global uncaught error handler ────────────────────────────────────────────
|
||||||
window.addEventListener('error', function(e) {
|
window.addEventListener('error', function(e) {
|
||||||
@@ -11705,6 +11719,17 @@ async function _initApp() {
|
|||||||
scaleInit(); // connect to smart scale gateway if configured
|
scaleInit(); // connect to smart scale gateway if configured
|
||||||
_injectKioskOverlay(); // kiosk X / refresh buttons (only when running inside Android WebView)
|
_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 ───────────────────────────────────────────────
|
// ── Background intervals ───────────────────────────────────────────────
|
||||||
// 1) Ogni 5 min: ricarica la pagina corrente (scadenze, inventario, ecc.)
|
// 1) Ogni 5 min: ricarica la pagina corrente (scadenze, inventario, ecc.)
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
|||||||
@@ -699,21 +699,16 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// Normalise: strip leading 'v' for comparison
|
// Normalise: strip leading 'v' for comparison
|
||||||
val norm = { v: String -> v.trimStart('v') }
|
val norm = { v: String -> v.trimStart('v') }
|
||||||
|
// If tag is not semver-like (e.g. "latest") we can't compare — treat as "needs update"
|
||||||
val kioskNeedsUpdate = latestTag.isNotEmpty() && currentKiosk.isNotEmpty() &&
|
val isSemver = latestTag.trimStart('v').matches(Regex("\\d+\\.\\d+.*"))
|
||||||
norm(latestTag) != norm(currentKiosk)
|
|
||||||
val gatewayNeedsUpdate = currentGateway != null && latestTag.isNotEmpty() &&
|
|
||||||
norm(latestTag) != norm(currentGateway)
|
|
||||||
|
|
||||||
if (!kioskNeedsUpdate && !gatewayNeedsUpdate) return@Thread
|
|
||||||
|
|
||||||
// Find APK download URLs in release assets
|
// Find APK download URLs in release assets
|
||||||
val assets = json.optJSONArray("assets")
|
val assets = json.optJSONArray("assets")
|
||||||
var kioskApkUrl = KIOSK_DOWNLOAD_URL
|
var kioskApkUrl = "" // only set if the release actually contains the APK
|
||||||
var gatewayApkUrl = GATEWAY_DOWNLOAD_URL
|
var gatewayApkUrl = ""
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
for (i in 0 until assets.length()) {
|
for (i in 0 until assets.length()) {
|
||||||
val a = assets.getJSONObject(i)
|
val a = assets.getJSONObject(i)
|
||||||
val name = a.optString("name", "").lowercase()
|
val name = a.optString("name", "").lowercase()
|
||||||
val url = a.optString("browser_download_url", "")
|
val url = a.optString("browser_download_url", "")
|
||||||
if (name.contains("kiosk") && url.isNotEmpty()) kioskApkUrl = 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)
|
// Build message and choose primary download (kiosk takes precedence)
|
||||||
val lines = mutableListOf<String>()
|
val lines = mutableListOf<String>()
|
||||||
var primaryApkUrl = ""
|
var primaryApkUrl = ""
|
||||||
if (kioskNeedsUpdate) {
|
if (kioskNeedsUpdate) {
|
||||||
lines += "🔄 Kiosk $currentKiosk → $latestTag"
|
val label = if (isSemver) "$currentKiosk → $latestTag" else latestTag
|
||||||
|
lines += "🔄 Kiosk $label"
|
||||||
primaryApkUrl = kioskApkUrl
|
primaryApkUrl = kioskApkUrl
|
||||||
}
|
}
|
||||||
if (gatewayNeedsUpdate) {
|
if (gatewayNeedsUpdate) {
|
||||||
lines += "🔄 Scale Gateway $currentGateway → $latestTag"
|
val label = if (isSemver) "$currentGateway → $latestTag" else latestTag
|
||||||
|
lines += "🔄 Scale Gateway $label"
|
||||||
if (primaryApkUrl.isEmpty()) primaryApkUrl = gatewayApkUrl
|
if (primaryApkUrl.isEmpty()) primaryApkUrl = gatewayApkUrl
|
||||||
}
|
}
|
||||||
val message = lines.joinToString(" • ")
|
val message = lines.joinToString(" • ")
|
||||||
|
|||||||
+9
-3
@@ -427,10 +427,10 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener
|
|||||||
val latestTag = json.optString("tag_name", "").ifEmpty { return@Thread }
|
val latestTag = json.optString("tag_name", "").ifEmpty { return@Thread }
|
||||||
val current = try { packageManager.getPackageInfo(packageName, 0).versionName ?: "" } catch (_: Exception) { "" }
|
val current = try { packageManager.getPackageInfo(packageName, 0).versionName ?: "" } catch (_: Exception) { "" }
|
||||||
val norm = { v: String -> v.trimStart('v') }
|
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
|
// Find scale-gateway APK in release assets
|
||||||
var apkUrl = APK_DOWNLOAD_URL
|
var apkUrl = ""
|
||||||
val assets = json.optJSONArray("assets")
|
val assets = json.optJSONArray("assets")
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
for (i in 0 until assets.length()) {
|
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) }
|
runOnUiThread { showNativeUpdateBanner(msg, apkUrl) }
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}.start()
|
}.start()
|
||||||
|
|||||||
@@ -50,6 +50,14 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<!-- ===== APP PRELOADER (hidden by JS once _initApp completes) ===== -->
|
||||||
|
<div id="app-preloader" aria-hidden="true">
|
||||||
|
<div class="app-preloader-inner">
|
||||||
|
<div class="app-preloader-spinner"></div>
|
||||||
|
<span class="app-preloader-label">🏠 EverShelf</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Top Header -->
|
<!-- Top Header -->
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user