diff --git a/worklenz-frontend/src/layouts/admin-center-layout.tsx b/worklenz-frontend/src/layouts/admin-center-layout.tsx
index 2ce73c63..4addfd3c 100644
--- a/worklenz-frontend/src/layouts/admin-center-layout.tsx
+++ b/worklenz-frontend/src/layouts/admin-center-layout.tsx
@@ -1,10 +1,9 @@
import { Flex, Typography } from 'antd';
-import React, { useEffect } from 'react';
+import React from 'react';
import { Outlet } from 'react-router-dom';
import { useMediaQuery } from 'react-responsive';
import AdminCenterSidebar from '@/pages/admin-center/sidebar/sidebar';
import { useTranslation } from 'react-i18next';
-import { verifyAuthentication } from '@/features/auth/authSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
const AdminCenterLayout: React.FC = () => {
@@ -13,9 +12,7 @@ const AdminCenterLayout: React.FC = () => {
const isMarginAvailable = useMediaQuery({ query: '(min-width: 1000px)' });
const { t } = useTranslation('admin-center/sidebar');
- useEffect(() => {
- void dispatch(verifyAuthentication());
- }, [dispatch]);
+
return (
{
+ registerSW({
+ onSuccess: (registration) => {
+ console.log('SW registered successfully');
+ },
+ onUpdate: (registration) => {
+ // Show update notification to user
+ },
+ onOfflineReady: () => {
+ console.log('App ready for offline use');
+ }
+ });
+}, []);
+```
+
+### Using the Hook
+
+```tsx
+import { useServiceWorker } from '../utils/serviceWorkerRegistration';
+
+const MyComponent = () => {
+ const { isOffline, swManager, clearCache, forceUpdate } = useServiceWorker();
+
+ return (
+
+
Status: {isOffline ? 'Offline' : 'Online'}
+
Clear Cache
+
Update App
+
+ );
+};
+```
+
+### Status Component
+
+```tsx
+import ServiceWorkerStatus from '../components/service-worker-status/ServiceWorkerStatus';
+
+// Minimal offline indicator
+
+
+// Full status with controls
+
+```
+
+## Configuration
+
+### Cacheable Resources
+
+Edit the patterns in `sw.js`:
+
+```javascript
+// API endpoints that can be cached
+const CACHEABLE_API_PATTERNS = [
+ /\/api\/project-categories/,
+ /\/api\/task-statuses/,
+ // Add more patterns...
+];
+
+// Resources that should never be cached
+const NEVER_CACHE_PATTERNS = [
+ /\/api\/auth\/login/,
+ /\/socket\.io/,
+ // Add more patterns...
+];
+```
+
+### Cache Names
+
+Update version to force cache refresh:
+
+```javascript
+const CACHE_VERSION = 'v1.0.1'; // Increment when deploying
+```
+
+## Development
+
+### Testing Offline
+
+1. Open DevTools → Application → Service Workers
+2. Check "Offline" to simulate offline mode
+3. Verify app still functions
+
+### Debugging
+
+```javascript
+// Check service worker status
+navigator.serviceWorker.ready.then(registration => {
+ console.log('SW ready:', registration);
+});
+
+// Check cache contents
+caches.keys().then(names => {
+ console.log('Cache names:', names);
+});
+```
+
+### Cache Management
+
+```javascript
+// Clear all caches
+caches.keys().then(names =>
+ Promise.all(names.map(name => caches.delete(name)))
+);
+
+// Clear specific cache
+caches.delete('worklenz-api-v1.0.0');
+```
+
+## Best Practices
+
+### 1. Cache Strategy Selection
+
+- **Static Assets**: Cache First (fast loading)
+- **API Data**: Network First (fresh data)
+- **User Content**: Network Only (always fresh)
+- **App Shell**: Cache First (instant loading)
+
+### 2. Cache Invalidation
+
+- Increment `CACHE_VERSION` when deploying
+- Use versioned URLs for assets
+- Set appropriate cache headers
+
+### 3. Offline UX
+
+- Show offline indicators
+- Queue actions for later sync
+- Provide meaningful offline messages
+- Cache critical user data
+
+### 4. Performance
+
+- Cache only necessary resources
+- Set cache size limits
+- Clean up old caches regularly
+- Monitor cache usage
+
+## Monitoring
+
+### Storage Usage
+
+```javascript
+// Check storage quota
+navigator.storage.estimate().then(estimate => {
+ console.log('Used:', estimate.usage);
+ console.log('Quota:', estimate.quota);
+});
+```
+
+### Cache Hit Rate
+
+Monitor in DevTools → Network:
+- Look for "from ServiceWorker" requests
+- Check cache effectiveness
+
+## Troubleshooting
+
+### Common Issues
+
+1. **SW not updating**
+ - Hard refresh (Ctrl+Shift+R)
+ - Clear browser cache
+ - Check CACHE_VERSION
+
+2. **Resources not caching**
+ - Verify URL patterns
+ - Check NEVER_CACHE_PATTERNS
+ - Ensure HTTPS in production
+
+3. **Offline features not working**
+ - Verify SW registration
+ - Check browser support
+ - Test cache strategies
+
+### Reset Service Worker
+
+```javascript
+// Unregister and reload
+navigator.serviceWorker.getRegistrations().then(registrations => {
+ registrations.forEach(registration => registration.unregister());
+ window.location.reload();
+});
+```
+
+## Browser Support
+
+- ✅ Chrome 45+
+- ✅ Firefox 44+
+- ✅ Safari 11.1+
+- ✅ Edge 17+
+- ❌ Internet Explorer
+
+## Future Enhancements
+
+1. **Background Sync**
+ - Queue offline actions
+ - Sync when online
+
+2. **Push Notifications**
+ - Task assignments
+ - Project updates
+ - Deadline reminders
+
+3. **Advanced Caching**
+ - Intelligent prefetching
+ - ML-based cache eviction
+ - Compression
+
+4. **Offline Analytics**
+ - Track offline usage
+ - Cache hit rates
+ - Performance metrics
+
+---
+
+*Last updated: January 2025*
\ No newline at end of file
diff --git a/worklenz-frontend/src/utils/serviceWorkerRegistration.ts b/worklenz-frontend/src/utils/serviceWorkerRegistration.ts
new file mode 100644
index 00000000..1f70f785
--- /dev/null
+++ b/worklenz-frontend/src/utils/serviceWorkerRegistration.ts
@@ -0,0 +1,273 @@
+// Service Worker Registration Utility
+// Handles registration, updates, and error handling
+
+import React from 'react';
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ window.location.hostname === '[::1]' ||
+ window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
+);
+
+type Config = {
+ onSuccess?: (registration: ServiceWorkerRegistration) => void;
+ onUpdate?: (registration: ServiceWorkerRegistration) => void;
+ onOfflineReady?: () => void;
+ onError?: (error: Error) => void;
+};
+
+export function registerSW(config?: Config) {
+ if ('serviceWorker' in navigator) {
+ // Only register in production or when explicitly testing
+ const swUrl = '/sw.js';
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://bit.ly/CRA-PWA'
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ } else {
+ console.log('Service workers are not supported in this browser.');
+ }
+}
+
+function registerValidSW(swUrl: string, config?: Config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then(registration => {
+ console.log('Service Worker registered successfully:', registration);
+
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+ );
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+
+ if (config && config.onOfflineReady) {
+ config.onOfflineReady();
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch(error => {
+ console.error('Error during service worker registration:', error);
+ if (config && config.onError) {
+ config.onError(error);
+ }
+ });
+}
+
+function checkValidServiceWorker(swUrl: string, config?: Config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { 'Service-Worker': 'script' },
+ })
+ .then(response => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type');
+ if (
+ response.status === 404 ||
+ (contentType != null && contentType.indexOf('javascript') === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log(
+ 'No internet connection found. App is running in offline mode.'
+ );
+ });
+}
+
+export function unregisterSW() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready
+ .then(registration => {
+ registration.unregister();
+ console.log('Service Worker unregistered successfully');
+ })
+ .catch(error => {
+ console.error('Error during service worker unregistration:', error);
+ });
+ }
+}
+
+// Utility to communicate with service worker
+export class ServiceWorkerManager {
+ private registration: ServiceWorkerRegistration | null = null;
+
+ constructor(registration?: ServiceWorkerRegistration) {
+ this.registration = registration || null;
+ }
+
+ // Send message to service worker
+ async sendMessage(type: string, payload?: any): Promise
{
+ if (!this.registration || !this.registration.active) {
+ throw new Error('Service Worker not available');
+ }
+
+ return new Promise((resolve, reject) => {
+ const messageChannel = new MessageChannel();
+
+ messageChannel.port1.onmessage = (event) => {
+ if (event.data.error) {
+ reject(event.data.error);
+ } else {
+ resolve(event.data);
+ }
+ };
+
+ this.registration!.active!.postMessage(
+ { type, payload },
+ [messageChannel.port2]
+ );
+
+ // Timeout after 5 seconds
+ setTimeout(() => {
+ reject(new Error('Service Worker message timeout'));
+ }, 5000);
+ });
+ }
+
+ // Get service worker version
+ async getVersion(): Promise {
+ try {
+ const response = await this.sendMessage('GET_VERSION');
+ return response.version;
+ } catch (error) {
+ console.error('Failed to get service worker version:', error);
+ return 'unknown';
+ }
+ }
+
+ // Clear all caches
+ async clearCache(): Promise {
+ try {
+ await this.sendMessage('CLEAR_CACHE');
+ return true;
+ } catch (error) {
+ console.error('Failed to clear cache:', error);
+ return false;
+ }
+ }
+
+ // Force update service worker
+ async forceUpdate(): Promise {
+ if (!this.registration) return;
+
+ try {
+ await this.registration.update();
+ await this.sendMessage('SKIP_WAITING');
+ window.location.reload();
+ } catch (error) {
+ console.error('Failed to force update service worker:', error);
+ throw error;
+ }
+ }
+
+ // Check if app is running offline
+ isOffline(): boolean {
+ return !navigator.onLine;
+ }
+
+ // Get cache storage estimate
+ async getCacheSize(): Promise {
+ if ('storage' in navigator && 'estimate' in navigator.storage) {
+ try {
+ return await navigator.storage.estimate();
+ } catch (error) {
+ console.error('Failed to get storage estimate:', error);
+ }
+ }
+ return null;
+ }
+}
+
+// Hook to use service worker in React components
+export function useServiceWorker() {
+ const [isOffline, setIsOffline] = React.useState(!navigator.onLine);
+ const [swManager, setSWManager] = React.useState(null);
+
+ React.useEffect(() => {
+ const handleOnline = () => setIsOffline(false);
+ const handleOffline = () => setIsOffline(true);
+
+ window.addEventListener('online', handleOnline);
+ window.addEventListener('offline', handleOffline);
+
+ // Register service worker
+ registerSW({
+ onSuccess: (registration) => {
+ setSWManager(new ServiceWorkerManager(registration));
+ },
+ onUpdate: (registration) => {
+ // You could show a toast here asking user to refresh
+ console.log('New version available');
+ setSWManager(new ServiceWorkerManager(registration));
+ },
+ onOfflineReady: () => {
+ console.log('App ready for offline use');
+ }
+ });
+
+ return () => {
+ window.removeEventListener('online', handleOnline);
+ window.removeEventListener('offline', handleOffline);
+ };
+ }, []);
+
+ return {
+ isOffline,
+ swManager,
+ clearCache: () => swManager?.clearCache(),
+ forceUpdate: () => swManager?.forceUpdate(),
+ getVersion: () => swManager?.getVersion(),
+ };
+}
\ No newline at end of file
diff --git a/worklenz-frontend/vite.config.ts b/worklenz-frontend/vite.config.ts
index d0ba2dac..6bca483d 100644
--- a/worklenz-frontend/vite.config.ts
+++ b/worklenz-frontend/vite.config.ts
@@ -138,5 +138,8 @@ export default defineConfig(({ command, mode }) => {
define: {
__DEV__: !isProduction,
},
+
+ // **Public Directory** - sw.js will be automatically copied from public/ to build/
+ publicDir: 'public',
};
});
\ No newline at end of file