- 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.
291 lines
11 KiB
React
291 lines
11 KiB
React
/**
|
|
* 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;
|
|
}
|
|
|