/**
* 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 {
const registration = await navigator.serviceWorker.register('/service-worker.js');
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();
}