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:
dadaloop82
2026-05-03 17:46:42 +00:00
parent f9718fee6d
commit 58e69625bd
6 changed files with 118 additions and 30 deletions
+39 -14
View File
@@ -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) ? ` <strong>${latestTag}</strong>` : '';
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>` +
`<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>`;
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(() => {