diff --git a/frontend/nginx.conf b/frontend/nginx.conf index c267175..545df84 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -10,12 +10,45 @@ server { gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + # Hashed build assets — safe to cache forever + location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } + # Other static files (icons, fonts at root etc.) — short cache + location ~* \.(png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800"; + } + + # Never cache entry points — must always revalidate so clients + # can detect new frontend versions and service worker updates + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires 0; + } + + location = /sw.js { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires 0; + } + + location = /registerSW.js { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires 0; + } + + location = /manifest.webmanifest { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires 0; + } + + location = /manifest.json { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires 0; + } + # Proxy API requests to backend location /api/ { proxy_pass http://backend:8080; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 859b7d7..9d7f9e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { DeliveryForm } from './components/delivery/DeliveryForm'; import { LoginForm } from './components/auth/LoginForm'; import { ToastContainer } from './components/ui/Toast'; import { Button } from './components/ui/Button'; +import { UpdatePrompt } from './components/ui/UpdatePrompt'; import { useDeliveryStore } from './stores/deliveryStore'; import { useAuthStore } from './stores/authStore'; @@ -89,6 +90,7 @@ function App() { if (!isAuthenticated) { return ( <> + @@ -97,6 +99,7 @@ function App() { return (
+
diff --git a/frontend/src/components/ui/UpdatePrompt.tsx b/frontend/src/components/ui/UpdatePrompt.tsx new file mode 100644 index 0000000..fac22c4 --- /dev/null +++ b/frontend/src/components/ui/UpdatePrompt.tsx @@ -0,0 +1,55 @@ +import { useRegisterSW } from 'virtual:pwa-register/react'; +import { RefreshCw } from 'lucide-react'; +import { Button } from './Button'; + +// Check for SW updates every hour and on tab focus/visibility change +const UPDATE_INTERVAL_MS = 60 * 60 * 1000; + +export function UpdatePrompt() { + const { + needRefresh: [needRefresh], + updateServiceWorker, + } = useRegisterSW({ + onRegisteredSW(_swUrl, registration) { + if (!registration) return; + + const checkForUpdate = async () => { + if (registration.installing || !navigator) return; + if ('connection' in navigator && !navigator.onLine) return; + try { + await registration.update(); + } catch { + // network error — ignore, will retry + } + }; + + setInterval(checkForUpdate, UPDATE_INTERVAL_MS); + + const onVisible = () => { + if (document.visibilityState === 'visible') checkForUpdate(); + }; + document.addEventListener('visibilitychange', onVisible); + window.addEventListener('focus', checkForUpdate); + }, + }); + + if (!needRefresh) return null; + + return ( +
+
+
+ + Доступна новая версия приложения +
+ +
+
+ ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 09a06e4..bef5202 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,18 +3,6 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' -if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js', { scope: '/' }) - .then((registration) => { - console.log('SW registered:', registration) - }) - .catch((error) => { - console.log('SW registration failed:', error) - }) - }) -} - createRoot(document.getElementById('root')!).render( diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index af516fc..9e7e886 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -5,7 +5,7 @@ "useDefineForClassFields": true, "lib": ["ES2023", "DOM", "DOM.Iterable"], "module": "ESNext", - "types": ["vite/client"], + "types": ["vite/client", "vite-plugin-pwa/client", "vite-plugin-pwa/react"], "skipLibCheck": true, /* Bundler mode */ diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a205fe8..1b04995 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,10 +9,11 @@ export default defineConfig({ react(), tailwindcss(), VitePWA({ - registerType: 'autoUpdate', + registerType: 'prompt', manifest: false, // manifest.json from public workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'], + cleanupOutdatedCaches: true, runtimeCaching: [ { urlPattern: /^https:\/\/.*\/api\//,