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\//,