/** * Client-side cache management utility * Handles cache busting and version checking for the application */ class ClientCacheManager { constructor() { this.currentVersion = null; this.versionCheckInterval = null; this.storageKey = 'app-version'; this.init(); } /** * Initialize cache manager */ init() { this.getCurrentVersion(); this.startVersionChecking(); this.setupBeforeUnload(); } /** * Get current app version from meta tag or API */ async getCurrentVersion() { try { // First try to get version from meta tag const metaVersion = document.querySelector('meta[name="app-version"]'); if (metaVersion) { this.currentVersion = metaVersion.getAttribute('content'); this.storeVersion(this.currentVersion); return this.currentVersion; } // Fallback to API call const response = await fetch('/api/version'); if (response.ok) { const data = await response.json(); this.currentVersion = data.version; this.storeVersion(this.currentVersion); return this.currentVersion; } } catch (error) { console.warn('Could not retrieve app version:', error); } return null; } /** * Store version in localStorage * @param {string} version - Version to store */ storeVersion(version) { try { localStorage.setItem(this.storageKey, version); } catch (error) { // Ignore localStorage errors } } /** * Get stored version from localStorage * @returns {string|null} Stored version */ getStoredVersion() { try { return localStorage.getItem(this.storageKey); } catch (error) { return null; } } /** * Check if app version has changed * @returns {boolean} True if version changed */ async hasVersionChanged() { const storedVersion = this.getStoredVersion(); const currentVersion = await this.getCurrentVersion(); return storedVersion && currentVersion && storedVersion !== currentVersion; } /** * Force reload the page with cache busting */ forceReload() { // Clear cache-related storage try { localStorage.removeItem(this.storageKey); sessionStorage.clear(); } catch (error) { // Ignore errors } // Force reload with cache busting const url = new URL(window.location); url.searchParams.set('_cb', Date.now()); window.location.replace(url.toString()); } /** * Start periodic version checking */ startVersionChecking() { // Check every 60 seconds (reduced from 30 to ease rate limiting) this.versionCheckInterval = setInterval(async () => { try { if (await this.hasVersionChanged()) { this.handleVersionChange(); } } catch (error) { console.warn('Version check failed:', error); // If we get a rate limit error, slow down checks if (error.message?.includes('429') || error.message?.includes('Too Many Requests')) { console.log('Slowing down version checks due to rate limiting'); clearInterval(this.versionCheckInterval); // Restart with a longer interval (2 minutes) this.versionCheckInterval = setInterval(async () => { try { if (await this.hasVersionChanged()) { this.handleVersionChange(); } } catch (error) { console.warn('Version check failed:', error); } }, 120000); // 2 minutes } } }, 60000); // 1 minute } /** * Stop version checking */ stopVersionChecking() { if (this.versionCheckInterval) { clearInterval(this.versionCheckInterval); this.versionCheckInterval = null; } } /** * Handle version change detection */ handleVersionChange() { // Show update notification this.showUpdateNotification(); } /** * Show update notification to user */ showUpdateNotification() { // Remove existing notification const existingNotification = document.querySelector('.update-notification'); if (existingNotification) { existingNotification.remove(); } // Create notification element const notification = document.createElement('div'); notification.className = 'update-notification'; notification.innerHTML = `