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>
422 lines
14 KiB
JavaScript
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();
|
|
}
|