version updates
This commit is contained in:
@@ -40,6 +40,11 @@ window.App = window.App || {};
|
||||
// or failing status endpoint. Now fetch fresh status in the background and
|
||||
// reconcile the UI with whatever comes back.
|
||||
App.storage.refreshServerStatusInBackground();
|
||||
|
||||
// Watch for frontend deploys and seamlessly reload/hot-swap changed assets.
|
||||
if (App.version && App.version.start) {
|
||||
App.version.start();
|
||||
}
|
||||
}
|
||||
|
||||
initApp();
|
||||
|
||||
138
frontend/js/version.js
Normal file
138
frontend/js/version.js
Normal file
@@ -0,0 +1,138 @@
|
||||
window.App = window.App || {};
|
||||
App.version = App.version || {};
|
||||
|
||||
(function() {
|
||||
const VERSION_URL = '/api/version';
|
||||
const POLL_INTERVAL_MS = 60000;
|
||||
|
||||
// Baseline manifest captured on startup: { version, files: { rel: hash } }.
|
||||
let baseline = null;
|
||||
let timer = null;
|
||||
// Set once a JS/HTML change is detected; the page reloads at a safe moment.
|
||||
let reloadPending = false;
|
||||
let checking = false;
|
||||
|
||||
async function fetchVersion() {
|
||||
const resp = await fetch(VERSION_URL, { cache: 'no-store' });
|
||||
if (!resp.ok) throw new Error('version fetch failed: ' + resp.status);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// Swap a stylesheet <link> in place using a cache-busted href so updated CSS
|
||||
// applies instantly. The old link is removed only after the new one loads to
|
||||
// avoid a flash of unstyled content.
|
||||
function hotReloadCss(relPath, hash) {
|
||||
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
|
||||
const match = links.find((l) => {
|
||||
const href = (l.getAttribute('href') || '').split('?')[0];
|
||||
return href.endsWith(relPath) || href.endsWith('/' + relPath);
|
||||
});
|
||||
if (!match) return false;
|
||||
const base = (match.getAttribute('href') || '').split('?')[0];
|
||||
const fresh = match.cloneNode(false);
|
||||
fresh.setAttribute('href', base + '?v=' + hash);
|
||||
fresh.addEventListener('load', () => { if (match.parentNode) match.remove(); });
|
||||
fresh.addEventListener('error', () => { if (fresh.parentNode) fresh.remove(); });
|
||||
match.parentNode.insertBefore(fresh, match.nextSibling);
|
||||
return true;
|
||||
}
|
||||
|
||||
function diffFiles(oldFiles, newFiles) {
|
||||
const changed = [];
|
||||
const keys = new Set([
|
||||
...Object.keys(oldFiles || {}),
|
||||
...Object.keys(newFiles || {})
|
||||
]);
|
||||
keys.forEach((k) => {
|
||||
if ((oldFiles || {})[k] !== (newFiles || {})[k]) changed.push(k);
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
// A reload is "safe" when the user isn't mid-playback: no open video modal,
|
||||
// no active reels feed, and no playing <video>. App state survives a reload
|
||||
// because it is restored from localStorage on boot.
|
||||
function isSafeToReload() {
|
||||
if (App.state && App.state.feedOpen) return false;
|
||||
const modal = document.getElementById('video-modal');
|
||||
if (modal && modal.style.display && modal.style.display !== 'none') return false;
|
||||
const player = document.getElementById('player');
|
||||
if (player && !player.paused && !player.ended) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function showUpdateBanner() {
|
||||
const banner = document.getElementById('update-banner');
|
||||
if (!banner) return;
|
||||
banner.classList.add('show');
|
||||
const btn = document.getElementById('update-banner-btn');
|
||||
if (btn) btn.onclick = () => window.location.reload();
|
||||
}
|
||||
|
||||
function tryReloadWhenSafe() {
|
||||
if (!reloadPending) return;
|
||||
if (isSafeToReload()) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
showUpdateBanner();
|
||||
}
|
||||
}
|
||||
|
||||
function apply(latest) {
|
||||
const changed = diffFiles(baseline.files, latest.files);
|
||||
if (!changed.length) return;
|
||||
|
||||
let needsReload = false;
|
||||
changed.forEach((file) => {
|
||||
if (file.endsWith('.css') && hotReloadCss(file, latest.files[file])) {
|
||||
return; // hot-swapped without reload
|
||||
}
|
||||
// JS and HTML can't be safely live-patched; they require a reload.
|
||||
needsReload = true;
|
||||
});
|
||||
|
||||
// Adopt the new manifest so we don't re-trigger on the same change.
|
||||
baseline = latest;
|
||||
|
||||
if (needsReload) {
|
||||
reloadPending = true;
|
||||
tryReloadWhenSafe();
|
||||
}
|
||||
}
|
||||
|
||||
async function check() {
|
||||
if (checking || !baseline) return;
|
||||
checking = true;
|
||||
try {
|
||||
const latest = await fetchVersion();
|
||||
apply(latest);
|
||||
} catch (e) {
|
||||
// Network blips are non-fatal; we retry on the next tick.
|
||||
} finally {
|
||||
checking = false;
|
||||
}
|
||||
}
|
||||
|
||||
App.version.start = async function() {
|
||||
try {
|
||||
baseline = await fetchVersion();
|
||||
} catch (e) {
|
||||
return; // endpoint unavailable; skip version checking entirely
|
||||
}
|
||||
timer = setInterval(check, POLL_INTERVAL_MS);
|
||||
// Check promptly when the user returns to the tab so updates land while
|
||||
// they were away, and retry a pending reload once playback stops.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
tryReloadWhenSafe();
|
||||
check();
|
||||
}
|
||||
});
|
||||
// Re-attempt a deferred reload whenever a video finishes/pauses.
|
||||
const player = document.getElementById('player');
|
||||
if (player) {
|
||||
player.addEventListener('pause', tryReloadWhenSafe);
|
||||
player.addEventListener('ended', tryReloadWhenSafe);
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user