/** * 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 = `
🔄 A new version is available!
Please clear your browser cache and cookies, then refresh this page.
Instructions: On most browsers, press Ctrl + Shift + R (Windows) or Cmd + Shift + R (Mac) to force refresh.
For best results, consider deleting browsing data via your browser settings.
`; // Add styles notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #4CAF50; color: white; padding: 15px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; animation: slideIn 0.3s ease-out; `; // Add animation styles const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .update-notification-content { display: flex; align-items: center; gap: 10px; } .update-button { background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; } .update-button:hover { background: rgba(255,255,255,0.3); } .update-dismiss { background: none; border: none; color: white; cursor: pointer; font-size: 18px; padding: 0; margin-left: 5px; } .update-message { font-size: 14px; } `; document.head.appendChild(style); document.body.appendChild(notification); // Auto-dismiss after 10 seconds setTimeout(() => { if (notification && notification.parentNode) { notification.remove(); } }, 10000); } /** * Setup beforeunload handler to check version on page refresh */ setupBeforeUnload() { window.addEventListener('beforeunload', async () => { // Quick version check before unload try { if (await this.hasVersionChanged()) { // Clear cached version to force fresh load this.storeVersion(null); } } catch (error) { // Ignore errors during unload } }); } /** * Manual cache clear function */ clearCache() { try { // Clear localStorage localStorage.clear(); // Clear sessionStorage sessionStorage.clear(); // Clear service worker cache if available if ('serviceWorker' in navigator && 'caches' in window) { caches.keys().then(names => { names.forEach(name => { caches.delete(name); }); }); } console.log('Cache cleared successfully'); return true; } catch (error) { console.error('Failed to clear cache:', error); return false; } } /** * Get debug information * @returns {object} Debug info */ getDebugInfo() { return { currentVersion: this.currentVersion, storedVersion: this.getStoredVersion(), versionCheckActive: !!this.versionCheckInterval, timestamp: new Date().toISOString() }; } } // Initialize cache manager when DOM is ready let cacheManager; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { cacheManager = new ClientCacheManager(); }); } else { cacheManager = new ClientCacheManager(); } // Make cache manager globally available for debugging window.cacheManager = cacheManager; // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = ClientCacheManager; }