feat: implement comprehensive PWA force update system
Some checks are pending
CodeQL Analysis / Analyze CodeQL (push) Waiting to run
Deploy Application / deploy (push) Waiting to run
Mirror to Codeberg / mirror (push) Waiting to run
Mirror to PrivacyGuides / mirror (push) Waiting to run

- Add UpdateManager and UpdateChecker for automatic version detection
- Add post-build script for meta.json generation and version injection
- Enhance Service Worker with version-aware caching
- Add .htaccess configuration for proper cache control

This ensures all users receive the latest version after deployment
without manual cache clearing.
This commit is contained in:
lockbitchat
2025-12-29 10:51:07 -04:00
parent 1b6431a36b
commit 91c292a6cf
20 changed files with 1606 additions and 74 deletions

View File

@@ -1924,7 +1924,7 @@
}
}
handleMessage(' SecureBit.chat Enhanced Security Edition v4.7.53 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
handleMessage(' SecureBit.chat Enhanced Security Edition v4.7.55 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
const handleBeforeUnload = (event) => {
if (event.type === 'beforeunload' && !isTabSwitching) {
@@ -3747,9 +3747,25 @@
]);
};
// UpdateChecker компонент для автоматической проверки обновлений
const UpdateCheckerWrapper = ({ children }) => {
// Проверяем доступность UpdateChecker
if (typeof window !== 'undefined' && window.UpdateChecker) {
return React.createElement(window.UpdateChecker, {
debug: false
}, children);
}
// Fallback если UpdateChecker не загружен
return children;
};
function initializeApp() {
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
// Оборачиваем приложение в UpdateChecker для автоматической проверки обновлений
const AppWithUpdateChecker = React.createElement(UpdateCheckerWrapper, null,
React.createElement(EnhancedSecureP2PChat)
);
ReactDOM.render(AppWithUpdateChecker, document.getElementById('root'));
} else {
console.error('Модули не загружены:', {
hasCrypto: !!window.EnhancedSecureCryptoUtils,
@@ -3776,5 +3792,20 @@
}
};
// Render Enhanced Application
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
// Render Enhanced Application with UpdateChecker
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
const UpdateCheckerWrapper = ({ children }) => {
if (typeof window !== 'undefined' && window.UpdateChecker) {
return React.createElement(window.UpdateChecker, {
debug: false
}, children);
}
return children;
};
const AppWithUpdateChecker = React.createElement(UpdateCheckerWrapper, null,
React.createElement(EnhancedSecureP2PChat)
);
ReactDOM.render(AppWithUpdateChecker, document.getElementById('root'));
} else {
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
}

View File

@@ -0,0 +1,290 @@
/**
* UpdateChecker - React component for automatic update checking
*
* Wraps the application and automatically detects new versions,
* showing a modal window with update progress
*/
const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
const [updateState, setUpdateState] = React.useState({
hasUpdate: false,
isUpdating: false,
progress: 0,
currentVersion: null,
newVersion: null,
showModal: false
});
const updateManagerRef = React.useRef(null);
// Initialize UpdateManager
React.useEffect(() => {
// Check that UpdateManager is available
if (typeof window === 'undefined' || !window.UpdateManager) {
console.error('❌ UpdateManager not found. Make sure updateManager.js is loaded.');
return;
}
// Create UpdateManager instance
updateManagerRef.current = new window.UpdateManager({
versionUrl: '/meta.json',
checkInterval: 60000, // 1 minute
checkOnLoad: true,
debug: debug,
onUpdateAvailable: (updateInfo) => {
setUpdateState(prev => ({
...prev,
hasUpdate: true,
currentVersion: updateInfo.currentVersion,
newVersion: updateInfo.newVersion,
showModal: true
}));
// Call external callback if available
if (onUpdateAvailable) {
onUpdateAvailable(updateInfo);
}
},
onError: (error) => {
if (debug) {
console.warn('Update check error (non-critical):', error);
}
}
});
// Cleanup on unmount
return () => {
if (updateManagerRef.current) {
updateManagerRef.current.destroy();
}
};
}, [onUpdateAvailable, debug]);
// Force update handler
const handleForceUpdate = async () => {
if (!updateManagerRef.current || updateState.isUpdating) {
return;
}
setUpdateState(prev => ({
...prev,
isUpdating: true,
progress: 0
}));
try {
// Simulate update progress
const progressSteps = [
{ progress: 10, message: 'Saving data...' },
{ progress: 30, message: 'Clearing Service Worker caches...' },
{ progress: 50, message: 'Unregistering Service Workers...' },
{ progress: 70, message: 'Clearing browser cache...' },
{ progress: 90, message: 'Updating version...' },
{ progress: 100, message: 'Reloading application...' }
];
for (const step of progressSteps) {
await new Promise(resolve => setTimeout(resolve, 300));
setUpdateState(prev => ({
...prev,
progress: step.progress
}));
}
// Start force update
await updateManagerRef.current.forceUpdate();
} catch (error) {
console.error('❌ Update failed:', error);
setUpdateState(prev => ({
...prev,
isUpdating: false,
progress: 0
}));
// Show error to user
alert('Update error. Please refresh the page manually (Ctrl+F5 or Cmd+Shift+R)');
}
};
// Close modal (not recommended, but leaving the option)
const handleCloseModal = () => {
// Warn user
if (window.confirm('New version available. Update is recommended for security and stability. Continue without update?')) {
setUpdateState(prev => ({
...prev,
showModal: false
}));
}
};
// Format version for display
const formatVersion = (version) => {
if (!version) return 'N/A';
// If version is timestamp, format as date
if (/^\d+$/.test(version)) {
const date = new Date(parseInt(version));
return date.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
return version;
};
return React.createElement(React.Fragment, null, [
// Main application content
children,
// Update modal window
updateState.showModal && React.createElement('div', {
key: 'update-modal',
className: 'fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm',
style: {
animation: 'fadeIn 0.3s ease-in-out'
}
}, [
React.createElement('div', {
key: 'modal-content',
className: 'bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 border border-gray-200 dark:border-gray-700',
style: {
animation: 'slideUp 0.3s ease-out'
}
}, [
// Header
React.createElement('div', {
key: 'header',
className: 'text-center mb-6'
}, [
React.createElement('div', {
key: 'icon',
className: 'w-16 h-16 mx-auto mb-4 bg-blue-500/10 rounded-full flex items-center justify-center'
}, [
React.createElement('i', {
key: 'icon-fa',
className: 'fas fa-sync-alt text-blue-500 text-2xl animate-spin'
})
]),
React.createElement('h2', {
key: 'title',
className: 'text-2xl font-bold text-gray-900 dark:text-white mb-2'
}, 'Update Available'),
React.createElement('p', {
key: 'subtitle',
className: 'text-gray-600 dark:text-gray-300 text-sm'
}, 'A new version of the application has been detected')
]),
// Version information
React.createElement('div', {
key: 'version-info',
className: 'bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-6 space-y-2'
}, [
React.createElement('div', {
key: 'current',
className: 'flex justify-between items-center'
}, [
React.createElement('span', {
key: 'current-label',
className: 'text-sm text-gray-600 dark:text-gray-400'
}, 'Current version:'),
React.createElement('span', {
key: 'current-value',
className: 'text-sm font-mono text-gray-900 dark:text-white'
}, formatVersion(updateState.currentVersion))
]),
React.createElement('div', {
key: 'new',
className: 'flex justify-between items-center'
}, [
React.createElement('span', {
key: 'new-label',
className: 'text-sm text-gray-600 dark:text-gray-400'
}, 'New version:'),
React.createElement('span', {
key: 'new-value',
className: 'text-sm font-mono text-blue-600 dark:text-blue-400 font-semibold'
}, formatVersion(updateState.newVersion))
])
]),
// Update progress
updateState.isUpdating && React.createElement('div', {
key: 'progress',
className: 'mb-6'
}, [
React.createElement('div', {
key: 'progress-bar',
className: 'w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 mb-2'
}, [
React.createElement('div', {
key: 'progress-fill',
className: 'bg-blue-500 h-2.5 rounded-full transition-all duration-300',
style: {
width: `${updateState.progress}%`
}
})
]),
React.createElement('p', {
key: 'progress-text',
className: 'text-center text-sm text-gray-600 dark:text-gray-400'
}, `${updateState.progress}%`)
]),
// Action buttons
!updateState.isUpdating && React.createElement('div', {
key: 'actions',
className: 'flex gap-3'
}, [
React.createElement('button', {
key: 'update-btn',
onClick: handleForceUpdate,
className: 'flex-1 bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2',
disabled: updateState.isUpdating
}, [
React.createElement('i', {
key: 'update-icon',
className: 'fas fa-download'
}),
React.createElement('span', {
key: 'update-text'
}, 'Update Now')
]),
React.createElement('button', {
key: 'close-btn',
onClick: handleCloseModal,
className: 'px-4 py-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-200',
disabled: updateState.isUpdating
}, [
React.createElement('i', {
key: 'close-icon',
className: 'fas fa-times'
})
])
]),
// Update indicator
updateState.isUpdating && React.createElement('div', {
key: 'updating',
className: 'text-center'
}, [
React.createElement('p', {
key: 'updating-text',
className: 'text-sm text-gray-600 dark:text-gray-400'
}, 'Update in progress...')
])
])
])
]);
};
// Export for use
if (typeof module !== 'undefined' && module.exports) {
module.exports = UpdateChecker;
} else {
window.UpdateChecker = UpdateChecker;
}

View File

@@ -539,7 +539,7 @@ const EnhancedMinimalHeader = ({
React.createElement('p', {
key: 'subtitle',
className: 'text-xs sm:text-sm text-muted hidden sm:block'
}, 'End-to-end freedom v4.7.53')
}, 'End-to-end freedom v4.7.55')
])
]),

540
src/utils/updateManager.js Normal file
View File

@@ -0,0 +1,540 @@
/**
* UpdateManager - Comprehensive PWA update management system
*
* Automatically detects new application versions and forcefully
* updates all cache levels: Service Worker, browser cache, localStorage
*
* @class UpdateManager
*/
class UpdateManager {
constructor(options = {}) {
this.options = {
// URL for version check (meta.json)
versionUrl: options.versionUrl || '/meta.json',
// Update check interval (ms)
checkInterval: options.checkInterval || 60000, // 1 minute
// Local storage key for version
versionKey: options.versionKey || 'app_version',
// Keys for preserving critical data before cleanup
preserveKeys: options.preserveKeys || [
'auth_token',
'user_settings',
'encryption_keys',
'peer_connections'
],
// Callback on update detection
onUpdateAvailable: options.onUpdateAvailable || null,
// Callback on error
onError: options.onError || null,
// Logging
debug: options.debug || false,
// Force check on load
checkOnLoad: options.checkOnLoad !== false,
// Request timeout
requestTimeout: options.requestTimeout || 10000
};
this.currentVersion = null;
this.serverVersion = null;
this.checkIntervalId = null;
this.isUpdating = false;
this.updatePromise = null;
// Initialization
this.init();
}
/**
* Initialize update manager
*/
async init() {
try {
// Load current version from localStorage
this.currentVersion = this.getLocalVersion();
if (this.options.debug) {
console.log('🔄 UpdateManager initialized', {
currentVersion: this.currentVersion,
versionUrl: this.options.versionUrl
});
}
// Check version on load
if (this.options.checkOnLoad) {
await this.checkForUpdates();
}
// Start periodic check
this.startPeriodicCheck();
// Listen to Service Worker events
this.setupServiceWorkerListeners();
} catch (error) {
this.handleError('Init failed', error);
}
}
/**
* Get local version from localStorage
*/
getLocalVersion() {
try {
return localStorage.getItem(this.options.versionKey) || null;
} catch (error) {
this.handleError('Failed to get local version', error);
return null;
}
}
/**
* Save version to localStorage
*/
setLocalVersion(version) {
try {
localStorage.setItem(this.options.versionKey, version);
this.currentVersion = version;
if (this.options.debug) {
console.log('✅ Version saved:', version);
}
} catch (error) {
this.handleError('Failed to save version', error);
}
}
/**
* Check for updates on server
*/
async checkForUpdates() {
// Prevent parallel checks
if (this.updatePromise) {
return this.updatePromise;
}
this.updatePromise = this._performCheck();
const result = await this.updatePromise;
this.updatePromise = null;
return result;
}
/**
* Perform version check
*/
async _performCheck() {
try {
if (this.options.debug) {
console.log('🔍 Checking for updates...');
}
// Request meta.json with cache-busting
const response = await this.fetchWithTimeout(
`${this.options.versionUrl}?t=${Date.now()}`,
{
method: 'GET',
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const meta = await response.json();
this.serverVersion = meta.version || meta.buildVersion || null;
if (!this.serverVersion) {
throw new Error('Version not found in meta.json');
}
if (this.options.debug) {
console.log('📦 Server version:', this.serverVersion, 'Local:', this.currentVersion);
}
// Compare versions
if (this.currentVersion === null) {
// First load - save version
this.setLocalVersion(this.serverVersion);
return { hasUpdate: false, version: this.serverVersion };
}
if (this.currentVersion !== this.serverVersion) {
// New version detected
if (this.options.debug) {
console.log('🆕 New version detected!', {
current: this.currentVersion,
new: this.serverVersion
});
}
// Call callback
if (this.options.onUpdateAvailable) {
this.options.onUpdateAvailable({
currentVersion: this.currentVersion,
newVersion: this.serverVersion,
updateManager: this
});
}
return {
hasUpdate: true,
currentVersion: this.currentVersion,
newVersion: this.serverVersion
};
}
return { hasUpdate: false, version: this.serverVersion };
} catch (error) {
// Graceful degradation - if meta.json is unavailable, continue working
if (this.options.debug) {
console.warn('⚠️ Update check failed (non-critical):', error.message);
}
if (this.options.onError) {
this.options.onError(error);
}
return { hasUpdate: false, error: error.message };
}
}
/**
* Fetch with timeout
*/
async fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.options.requestTimeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
/**
* Force application update
* Clears all cache levels and reloads the page
*/
async forceUpdate() {
if (this.isUpdating) {
if (this.options.debug) {
console.log('⏳ Update already in progress...');
}
return;
}
this.isUpdating = true;
try {
if (this.options.debug) {
console.log('🚀 Starting force update...');
}
// Step 1: Preserve critical data
const preservedData = this.preserveCriticalData();
// Step 2: Clear Service Worker caches
await this.clearServiceWorkerCaches();
// Step 3: Unregister Service Workers
await this.unregisterServiceWorkers();
// Step 4: Clear browser cache (localStorage, sessionStorage)
this.clearBrowserCaches();
// Step 5: Update version
if (this.serverVersion) {
this.setLocalVersion(this.serverVersion);
}
// Step 6: Restore critical data
this.restoreCriticalData(preservedData);
// Step 7: Force reload with cache-busting
if (this.options.debug) {
console.log('🔄 Reloading page with new version...');
}
// Small delay to complete operations
await new Promise(resolve => setTimeout(resolve, 500));
// Reload with full cache bypass
window.location.href = `${window.location.pathname}?v=${Date.now()}&_update=true`;
} catch (error) {
this.handleError('Force update failed', error);
this.isUpdating = false;
throw error;
}
}
/**
* Preserve critical data before cleanup
*/
preserveCriticalData() {
const data = {};
this.options.preserveKeys.forEach(key => {
try {
const value = localStorage.getItem(key);
if (value !== null) {
data[key] = value;
}
} catch (error) {
if (this.options.debug) {
console.warn(`⚠️ Failed to preserve ${key}:`, error);
}
}
});
if (this.options.debug) {
console.log('💾 Preserved critical data:', Object.keys(data));
}
return data;
}
/**
* Restore critical data after cleanup
*/
restoreCriticalData(data) {
Object.entries(data).forEach(([key, value]) => {
try {
localStorage.setItem(key, value);
} catch (error) {
if (this.options.debug) {
console.warn(`⚠️ Failed to restore ${key}:`, error);
}
}
});
if (this.options.debug) {
console.log('✅ Restored critical data');
}
}
/**
* Clear all Service Worker caches
*/
async clearServiceWorkerCaches() {
try {
if ('caches' in window) {
const cacheNames = await caches.keys();
if (this.options.debug) {
console.log('🗑️ Clearing Service Worker caches:', cacheNames);
}
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
// Send message to Service Worker for cleanup
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'CACHE_CLEAR'
});
}
if (this.options.debug) {
console.log('✅ Service Worker caches cleared');
}
}
} catch (error) {
this.handleError('Failed to clear SW caches', error);
}
}
/**
* Unregister all Service Workers
*/
async unregisterServiceWorkers() {
try {
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
if (this.options.debug) {
console.log('🔌 Unregistering Service Workers:', registrations.length);
}
await Promise.all(
registrations.map(registration => {
// Send skipWaiting command before unregistering
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
if (registration.installing) {
registration.installing.postMessage({ type: 'SKIP_WAITING' });
}
return registration.unregister();
})
);
if (this.options.debug) {
console.log('✅ Service Workers unregistered');
}
}
} catch (error) {
this.handleError('Failed to unregister SW', error);
}
}
/**
* Clear browser caches (localStorage, sessionStorage)
*/
clearBrowserCaches() {
try {
// Clear sessionStorage
sessionStorage.clear();
// Clear localStorage (except critical data that is already preserved)
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && !this.options.preserveKeys.includes(key) && key !== this.options.versionKey) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => {
try {
localStorage.removeItem(key);
} catch (error) {
if (this.options.debug) {
console.warn(`⚠️ Failed to remove ${key}:`, error);
}
}
});
if (this.options.debug) {
console.log('✅ Browser caches cleared');
}
} catch (error) {
this.handleError('Failed to clear browser caches', error);
}
}
/**
* Start periodic update check
*/
startPeriodicCheck() {
if (this.checkIntervalId) {
clearInterval(this.checkIntervalId);
}
this.checkIntervalId = setInterval(() => {
this.checkForUpdates();
}, this.options.checkInterval);
if (this.options.debug) {
console.log(`⏰ Periodic check started (${this.options.checkInterval}ms)`);
}
}
/**
* Stop periodic check
*/
stopPeriodicCheck() {
if (this.checkIntervalId) {
clearInterval(this.checkIntervalId);
this.checkIntervalId = null;
if (this.options.debug) {
console.log('⏹️ Periodic check stopped');
}
}
}
/**
* Setup Service Worker event listeners
*/
setupServiceWorkerListeners() {
if ('serviceWorker' in navigator) {
// Listen to Service Worker updates
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (this.options.debug) {
console.log('🔄 Service Worker controller changed');
}
// Check for updates after controller change
setTimeout(() => {
this.checkForUpdates();
}, 1000);
});
// Listen to messages from Service Worker
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SW_ACTIVATED') {
if (this.options.debug) {
console.log('✅ Service Worker activated');
}
// Check for updates after activation
setTimeout(() => {
this.checkForUpdates();
}, 1000);
}
});
}
}
/**
* Handle errors
*/
handleError(message, error) {
const errorMessage = `${message}: ${error.message || error}`;
if (this.options.debug) {
console.error('❌ UpdateManager error:', errorMessage, error);
}
if (this.options.onError) {
this.options.onError(new Error(errorMessage));
}
}
/**
* Destroy manager (cleanup)
*/
destroy() {
this.stopPeriodicCheck();
this.updatePromise = null;
if (this.options.debug) {
console.log('🗑️ UpdateManager destroyed');
}
}
}
// Export for use in modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = UpdateManager;
} else {
window.UpdateManager = UpdateManager;
}