/** * Tractatus Version Manager * - Registers service worker * - Checks for updates every hour * - Shows update notifications * - Manages PWA install prompts */ class VersionManager { constructor() { this.serviceWorker = null; this.deferredInstallPrompt = null; this.updateCheckInterval = null; this.currentVersion = null; this.init(); } async init() { // Only run in browsers that support service workers if (!('serviceWorker' in navigator)) { console.log('[VersionManager] Service workers not supported'); return; } try { // Register service worker await this.registerServiceWorker(); // Check for updates immediately await this.checkForUpdates(); // Check for updates every hour this.updateCheckInterval = setInterval(() => { this.checkForUpdates(); }, 3600000); // 1 hour // Listen for PWA install prompt this.setupInstallPrompt(); // Listen for service worker messages navigator.serviceWorker.addEventListener('message', (event) => { if (event.data.type === 'UPDATE_AVAILABLE') { this.showUpdateNotification(event.data); } }); } catch (error) { console.error('[VersionManager] Initialization failed:', error); } } async registerServiceWorker() { try { // Cache-bust service worker to force update: v0.1.5 nuclear reset const registration = await navigator.serviceWorker.register('/service-worker.js?v=0.1.6'); this.serviceWorker = registration; console.log('[VersionManager] Service worker registered'); // Check for updates when service worker updates registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // New service worker available this.showUpdateNotification({ updateAvailable: true, currentVersion: this.currentVersion, serverVersion: 'latest' }); } }); }); } catch (error) { console.error('[VersionManager] Service worker registration failed:', error); } } async checkForUpdates() { try { const response = await fetch('/version.json', { cache: 'no-store' }); const versionInfo = await response.json(); // Get current version from localStorage or default const storedVersion = localStorage.getItem('tractatus_version') || '0.0.0'; this.currentVersion = storedVersion; if (storedVersion !== versionInfo.version) { console.log('[VersionManager] Update available:', versionInfo.version); this.showUpdateNotification({ updateAvailable: true, currentVersion: storedVersion, serverVersion: versionInfo.version, changelog: versionInfo.changelog, forceUpdate: versionInfo.forceUpdate }); } } catch (error) { console.error('[VersionManager] Version check failed:', error); } } showUpdateNotification(versionInfo) { // Don't show if notification already visible if (document.getElementById('tractatus-update-notification')) { return; } const notification = document.createElement('div'); notification.id = 'tractatus-update-notification'; notification.className = 'fixed bottom-0 left-0 right-0 bg-blue-600 text-white px-4 py-3 shadow-lg z-50 transform transition-transform duration-300 translate-y-full'; notification.innerHTML = `

Update Available

A new version of Tractatus Framework is available ${versionInfo.serverVersion ? `(v${versionInfo.serverVersion})` : ''}

${versionInfo.changelog ? `
What's new?
    ${versionInfo.changelog.map(item => `
  • ${item}
  • `).join('')}
` : ''}
${versionInfo.forceUpdate ? ` ` : ` `}
`; document.body.appendChild(notification); // Add event listeners (CSP compliant) const updateNowBtn = document.getElementById('update-now-btn'); const updateLaterBtn = document.getElementById('update-later-btn'); const updateReloadBtn = document.getElementById('update-reload-btn'); if (updateNowBtn) { updateNowBtn.addEventListener('click', () => this.applyUpdate()); } if (updateLaterBtn) { updateLaterBtn.addEventListener('click', () => this.dismissUpdate()); } if (updateReloadBtn) { updateReloadBtn.addEventListener('click', () => this.applyUpdate()); } // Animate in setTimeout(() => { notification.classList.remove('translate-y-full'); }, 100); // Auto-reload for forced updates after 10 seconds if (versionInfo.forceUpdate) { setTimeout(() => { this.applyUpdate(); }, 10000); } } applyUpdate() { // Store new version fetch('/version.json', { cache: 'no-store' }) .then(response => response.json()) .then(versionInfo => { localStorage.setItem('tractatus_version', versionInfo.version); // Tell service worker to skip waiting if (this.serviceWorker) { this.serviceWorker.waiting?.postMessage({ type: 'SKIP_WAITING' }); } // Reload page window.location.reload(); }); } dismissUpdate() { const notification = document.getElementById('tractatus-update-notification'); if (notification) { notification.classList.add('translate-y-full'); setTimeout(() => { notification.remove(); }, 300); } } setupInstallPrompt() { // Listen for beforeinstallprompt event window.addEventListener('beforeinstallprompt', (e) => { // Prevent Chrome 67 and earlier from automatically showing the prompt e.preventDefault(); // Stash the event so it can be triggered later this.deferredInstallPrompt = e; // Check if user has dismissed install prompt before const dismissed = sessionStorage.getItem('install_prompt_dismissed'); if (!dismissed) { // Show install prompt after 30 seconds setTimeout(() => { this.showInstallPrompt(); }, 30000); } }); // Detect if app was installed window.addEventListener('appinstalled', () => { console.log('[VersionManager] PWA installed'); this.deferredInstallPrompt = null; // Hide install prompt if visible const prompt = document.getElementById('tractatus-install-prompt'); if (prompt) { prompt.remove(); } }); } showInstallPrompt() { if (!this.deferredInstallPrompt) { return; } // Don't show if already installed or on iOS Safari (handles differently) if (window.matchMedia('(display-mode: standalone)').matches) { return; } const prompt = document.createElement('div'); prompt.id = 'tractatus-install-prompt'; prompt.className = 'fixed bottom-0 left-0 right-0 bg-gradient-to-r from-purple-600 to-blue-600 text-white px-4 py-3 shadow-lg z-50 transform transition-transform duration-300 translate-y-full'; prompt.innerHTML = `

Install Tractatus App

Add to your home screen for quick access and offline support

`; document.body.appendChild(prompt); // Add event listeners (CSP compliant) const dismissBtn = document.getElementById('dismiss-install-btn'); const installBtn = document.getElementById('install-app-btn'); if (dismissBtn) { dismissBtn.addEventListener('click', () => this.dismissInstallPrompt()); } if (installBtn) { installBtn.addEventListener('click', () => this.installApp()); } // Animate in setTimeout(() => { prompt.classList.remove('translate-y-full'); }, 100); } async installApp() { if (!this.deferredInstallPrompt) { // Show helpful feedback if installation isn't available this.showInstallUnavailableMessage(); return; } // Show the install prompt this.deferredInstallPrompt.prompt(); // Wait for the user to respond to the prompt const { outcome } = await this.deferredInstallPrompt.userChoice; console.log(`[VersionManager] User response: ${outcome}`); // Clear the deferredInstallPrompt this.deferredInstallPrompt = null; // Hide the prompt this.dismissInstallPrompt(); } showInstallUnavailableMessage() { // Check if app is already installed const isInstalled = window.matchMedia('(display-mode: standalone)').matches; // Don't show message if it already exists if (document.getElementById('tractatus-install-unavailable')) { return; } const message = document.createElement('div'); message.id = 'tractatus-install-unavailable'; message.className = 'fixed bottom-0 left-0 right-0 bg-gray-800 text-white px-4 py-3 shadow-lg z-50 transform transition-transform duration-300 translate-y-full'; if (isInstalled) { message.innerHTML = `

Already Installed

Tractatus is already installed on your device. You're using it right now!

`; } else { message.innerHTML = `

Installation Not Available

Your browser doesn't currently support app installation. Try using Chrome, Edge, or Safari on a supported device.

`; } document.body.appendChild(message); // Add event listener for dismiss button const dismissBtn = document.getElementById('dismiss-unavailable-btn'); if (dismissBtn) { dismissBtn.addEventListener('click', () => { message.classList.add('translate-y-full'); setTimeout(() => { message.remove(); }, 300); }); } // Animate in setTimeout(() => { message.classList.remove('translate-y-full'); }, 100); // Auto-dismiss after 8 seconds setTimeout(() => { if (message.parentElement) { message.classList.add('translate-y-full'); setTimeout(() => { message.remove(); }, 300); } }, 8000); } dismissInstallPrompt() { const prompt = document.getElementById('tractatus-install-prompt'); if (prompt) { prompt.classList.add('translate-y-full'); setTimeout(() => { prompt.remove(); }, 300); } // Remember dismissal for this session sessionStorage.setItem('install_prompt_dismissed', 'true'); } } // Initialize version manager on page load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { window.versionManager = new VersionManager(); }); } else { window.versionManager = new VersionManager(); }