tractatus/public/js/version-manager.js
TheFlow 06049bc2bf fix: add cache-busting to service worker registration
CRITICAL FIX: Forces browsers to fetch new service worker

- Add ?v=0.1.4 query parameter to service worker registration
- Browsers will now immediately fetch updated SW file
- Ensures Phase 2 content visible to ALL users immediately
- Fixes persistent caching issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:03:41 +13:00

422 lines
14 KiB
JavaScript

/**
* 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.4 Phase 2 launch
const registration = await navigator.serviceWorker.register('/service-worker.js?v=0.1.4');
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 = `
<div class="max-w-7xl mx-auto flex items-center justify-between flex-wrap gap-4">
<div class="flex items-start flex-1">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-semibold">Update Available</p>
<p class="text-sm text-blue-100">
A new version of Tractatus Framework is available
${versionInfo.serverVersion ? `(v${versionInfo.serverVersion})` : ''}
</p>
${versionInfo.changelog ? `
<details class="mt-2 text-sm">
<summary class="cursor-pointer text-blue-100 hover:text-white">What's new?</summary>
<ul class="mt-2 ml-4 list-disc text-blue-100">
${versionInfo.changelog.map(item => `<li>${item}</li>`).join('')}
</ul>
</details>
` : ''}
</div>
</div>
<div class="flex gap-3">
${versionInfo.forceUpdate ? `
<button id="update-now-btn" class="bg-white text-blue-600 px-6 py-2 rounded-lg font-semibold hover:bg-blue-50 transition">
Update Now
</button>
` : `
<button id="update-later-btn" class="text-white hover:text-blue-100 transition px-3">
Later
</button>
<button id="update-reload-btn" class="bg-white text-blue-600 px-6 py-2 rounded-lg font-semibold hover:bg-blue-50 transition">
Reload
</button>
`}
</div>
</div>
`;
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 = `
<div class="max-w-7xl mx-auto flex items-center justify-between flex-wrap gap-4">
<div class="flex items-start flex-1">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/>
</svg>
<div>
<p class="font-semibold">Install Tractatus App</p>
<p class="text-sm text-purple-100">
Add to your home screen for quick access and offline support
</p>
</div>
</div>
<div class="flex gap-3">
<button id="dismiss-install-btn" class="text-white hover:text-purple-100 transition px-3">
Not Now
</button>
<button id="install-app-btn" class="bg-white text-purple-600 px-6 py-2 rounded-lg font-semibold hover:bg-purple-50 transition">
Install
</button>
</div>
</div>
`;
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 = `
<div class="max-w-7xl mx-auto flex items-center justify-between flex-wrap gap-4">
<div class="flex items-start flex-1">
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-semibold">Already Installed</p>
<p class="text-sm text-gray-300">
Tractatus is already installed on your device. You're using it right now!
</p>
</div>
</div>
<button id="dismiss-unavailable-btn" class="text-white hover:text-gray-300 transition px-3">
Okay
</button>
</div>
`;
} else {
message.innerHTML = `
<div class="max-w-7xl mx-auto flex items-center justify-between flex-wrap gap-4">
<div class="flex items-start flex-1">
<svg class="w-6 h-6 mr-3 flex-shrink-0 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-semibold">Installation Not Available</p>
<p class="text-sm text-gray-300">
Your browser doesn't currently support app installation. Try using Chrome, Edge, or Safari on a supported device.
</p>
</div>
</div>
<button id="dismiss-unavailable-btn" class="text-white hover:text-gray-300 transition px-3">
Okay
</button>
</div>
`;
}
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();
}