diff --git a/static/Screenshot 2026-07-29 at 04-00-56 Партнеры.png b/static/Screenshot 2026-07-29 at 04-00-56 Партнеры.png new file mode 100644 index 0000000..1d40936 Binary files /dev/null and b/static/Screenshot 2026-07-29 at 04-00-56 Партнеры.png differ diff --git a/static/photo_2026-07-29_02-45-09.jpg b/static/photo_2026-07-29_02-45-09.jpg new file mode 100644 index 0000000..3807e69 Binary files /dev/null and b/static/photo_2026-07-29_02-45-09.jpg differ diff --git a/static/photo_2026-07-29_02-45-17.jpg b/static/photo_2026-07-29_02-45-17.jpg new file mode 100644 index 0000000..04e7bd5 Binary files /dev/null and b/static/photo_2026-07-29_02-45-17.jpg differ diff --git a/static/photo_2026-07-29_02-45-22.jpg b/static/photo_2026-07-29_02-45-22.jpg new file mode 100644 index 0000000..b0b5e82 Binary files /dev/null and b/static/photo_2026-07-29_02-45-22.jpg differ diff --git a/static/service-worker.js b/static/service-worker.js index 1395521..7c4bb9a 100644 --- a/static/service-worker.js +++ b/static/service-worker.js @@ -1,177 +1,200 @@ +importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js'); + +workbox.setConfig({ debug: false }); + const CACHE_NAME = 'app-cache-v2.1.4'; +const MIN_VALID_SIZE = 500; const ASSETS_TO_CACHE = [ - '/pwa2', - '/icon-192x192.png', - '/icon-512x512.png', - '/offline.html', - '/static/Inter-Bold.woff2', - '/static/Inter-SemiBold.woff2', - '/static/Inter-Regular.woff2', - '/static/painter.svg', - '/static/event1.jpg', - '/static/event2.jpg', - '/static/event3.jpg', - '/static/event4.jpg', - '/static/event5.jpg', - '/static/event6.jpg', - '/static/event7.webp', + '/pwa2', + '/icon-192x192.png', + '/icon-512x512.png', + '/offline.html', + '/static/Inter-Bold.woff2', + '/static/Inter-SemiBold.woff2', + '/static/Inter-Regular.woff2', + '/static/painter.svg', + '/static/event1.jpg', + '/static/event2.jpg', + '/static/event3.jpg', + '/static/event4.jpg', + '/static/event5.jpg', + '/static/event6.jpg', + '/static/event7.webp', + '/partner', ]; -const MIN_VALID_SIZE = 500; const NOCACHE_PATHS = [ '/api', '/form', +]; + +const VALIDATE_ON_MODIFY_PATHS = [ + '/', + '/pwa', + '/paintings', '/profile', '/competitions', '/competition', '/exhibitions', '/exhibition', '/login', - '/registration' -]; - -const VALIDATE_ON_MODIFY_PATHS = [ + '/registration', '/artists', + '/my-paints', '/partner', '/news', - '/', - '/pwa', - '/paintings', - ]; -self.addEventListener('install', (e) => { - e.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - console.log('Caching static assets...'); - return cache.addAll(ASSETS_TO_CACHE); - }) - .then(() => { - console.log('All assets cached successfully.'); - return self.skipWaiting(); - }) - .catch((error) => { - console.error('Failed to cache assets during install:', error); - }) - ); +// ——— Lifecycle ——— +self.skipWaiting(); +workbox.core.clientsClaim(); + +// Clean old caches + notify clients +self.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => { + console.log(`Deleting old cache: ${key}`); + return caches.delete(key); + }) + ); + + const clients = await self.clients.matchAll(); + clients.forEach((client) => client.postMessage({ type: 'SW_UPDATED' })); + console.log('Service Worker activated.'); + })() + ); }); -self.addEventListener('activate', (e) => { - e.waitUntil( - caches.keys().then((keys) => - Promise.all( - keys.map((key) => { - if (key !== CACHE_NAME) { - console.log(`Deleting old cache: ${key}`); - return caches.delete(key); - } - }) - ) - ) - .then(() => { - console.log('Service Worker activated.'); - return self.clients.claim(); - }) - .then(() => { - return self.clients.matchAll().then((clients) => { - clients.forEach((client) => - client.postMessage({ type: 'SW_UPDATED' }) - ); - }); - }) - ); -}); -self.addEventListener('fetch', (e) => { - if (e.request.method !== 'GET') return; +// ——— Precaching ——— +workbox.precaching.precacheAndRoute( + ASSETS_TO_CACHE.map((url) => ({ url, revision: null })), + { + cacheName: CACHE_NAME, + } +); - const url = new URL(e.request.url); +// ——— Runtime routes ——— - if (NOCACHE_PATHS.some(path => - url.pathname === path || url.pathname.startsWith(path + '/') - )) { - return; - } +// 1. Never cache these paths (NetworkOnly) +workbox.routing.registerRoute( + ({ url }) => + NOCACHE_PATHS.some( + (path) => url.pathname === path || url.pathname.startsWith(path + '/') + ), + new workbox.strategies.NetworkOnly() +); - e.respondWith( - (async () => { - const cache = await caches.open(CACHE_NAME); - const cachedResponse = await caches.match(e.request); +// 2. Paths that should revalidate on Last-Modified / Content-Length +workbox.routing.registerRoute( + ({ url }) => + VALIDATE_ON_MODIFY_PATHS.some( + (path) => url.pathname === path || url.pathname.startsWith(path + '/') + ), + new workbox.strategies.NetworkFirst({ + cacheName: CACHE_NAME, + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({ + statuses: [0, 200], + }), + { + // Custom plugin: only update cache when content actually changed + // or when the cached entry is suspiciously small + cacheWillUpdate: async ({ request, response, event }) => { + if (!response || response.status !== 200) return null; - const shouldValidate = VALIDATE_ON_MODIFY_PATHS.some(path => - url.pathname === path || url.pathname.startsWith(path + '/') + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request); + + if (!cached) return response; + + const contentLength = parseInt( + cached.headers.get('Content-Length') || '0', + 10 + ); + const isTooSmall = contentLength > 0 && contentLength < MIN_VALID_SIZE; + + if (isTooSmall) { + console.warn( + `Cached file ${request.url} was too small (${contentLength} bytes). Forcing update.` ); + return response; + } - let isCachedTooSmall = false; - if (cachedResponse) { - const contentLength = parseInt(cachedResponse.headers.get('Content-Length') || '0', 10); - isCachedTooSmall = contentLength > 0 && contentLength < MIN_VALID_SIZE; - } + const [cachedModified, netModified] = await Promise.all([ + cached.headers.get('Last-Modified'), + response.headers.get('Last-Modified'), + ]); + const [cachedLen, netLen] = await Promise.all([ + cached.headers.get('Content-Length'), + response.headers.get('Content-Length'), + ]); - if (cachedResponse && !shouldValidate && !isCachedTooSmall) { - return cachedResponse; - } + if (cachedModified === netModified && cachedLen === netLen) { + console.log( + `Content unchanged for ${new URL(request.url).pathname}, keeping cached version` + ); + return null; // do not update cache + } - try { - const networkResponse = await fetch(e.request); + console.log(`Caching/updating: ${new URL(request.url).pathname}`); + return response; + }, + }, + ], + }) +); - if (networkResponse && networkResponse.status === 200) { - const responseClone = networkResponse.clone(); +// 3. Everything else → CacheFirst (with size safeguard) +workbox.routing.registerRoute( + ({ request }) => request.method === 'GET', + new workbox.strategies.CacheFirst({ + cacheName: CACHE_NAME, + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({ + statuses: [0, 200], + }), + { + // Force update when cached response is too small + cachedResponseWillBeUsed: async ({ + cacheName, + request, + cachedResponse, + }) => { + if (!cachedResponse) return null; - let shouldUpdateCache = true; + const contentLength = parseInt( + cachedResponse.headers.get('Content-Length') || '0', + 10 + ); + if (contentLength > 0 && contentLength < MIN_VALID_SIZE) { + console.warn( + `Cached file ${request.url} was too small (${contentLength} bytes). Ignoring cache.` + ); + return null; // force network + } + return cachedResponse; + }, + }, + ], + }) +); - if (shouldValidate && cachedResponse) { - const [cachedModified, netModified] = await Promise.all([ - cachedResponse.headers.get('Last-Modified'), - networkResponse.headers.get('Last-Modified') - ]); +// ——— Offline fallback for navigation requests ——— +workbox.routing.setCatchHandler(async ({ event }) => { + if (event.request.mode === 'navigate') { + const offline = await caches.match('/offline.html'); + if (offline) return offline; - const [cachedLen, netLen] = await Promise.all([ - cachedResponse.headers.get('Content-Length'), - networkResponse.headers.get('Content-Length') - ]); + const root = await caches.match('/'); + if (root) return root; + } - if (cachedModified === netModified && cachedLen === netLen) { - shouldUpdateCache = false; - console.log(`Content unchanged for ${url.pathname}, keeping cached version`); - } - } - - if (isCachedTooSmall) { - console.warn(`Cached file ${url.pathname} was too small (${cachedResponse.headers.get('Content-Length')} bytes). Forcing update.`); - shouldUpdateCache = true; - } - - if (shouldUpdateCache) { - console.log(`Caching/updating: ${url.pathname}`); - await cache.put(e.request, responseClone); - } - - return networkResponse; - } - - if (cachedResponse) return cachedResponse; - - throw new Error('Network response was not ok'); - - } catch (err) { - console.error('Fetch failed:', err); - - if (e.request.mode === 'navigate') { - const offlinePage = await caches.match('/offline.html'); - if (offlinePage) return offlinePage; - - const rootPage = await caches.match('/'); - if (rootPage) return rootPage; - } - - if (cachedResponse) { - console.warn(`Returning cached response despite small size due to offline state: ${url.pathname}`); - return cachedResponse; - } - - throw err; - } - })() - ); + // For non-navigation requests just return whatever we have (or fail) + return Response.error(); }); \ No newline at end of file diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 0000000..7c4bb9a --- /dev/null +++ b/static/sw.js @@ -0,0 +1,200 @@ +importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js'); + +workbox.setConfig({ debug: false }); + +const CACHE_NAME = 'app-cache-v2.1.4'; +const MIN_VALID_SIZE = 500; + +const ASSETS_TO_CACHE = [ + '/pwa2', + '/icon-192x192.png', + '/icon-512x512.png', + '/offline.html', + '/static/Inter-Bold.woff2', + '/static/Inter-SemiBold.woff2', + '/static/Inter-Regular.woff2', + '/static/painter.svg', + '/static/event1.jpg', + '/static/event2.jpg', + '/static/event3.jpg', + '/static/event4.jpg', + '/static/event5.jpg', + '/static/event6.jpg', + '/static/event7.webp', + '/partner', +]; + +const NOCACHE_PATHS = [ + '/api', + '/form', +]; + +const VALIDATE_ON_MODIFY_PATHS = [ + '/', + '/pwa', + '/paintings', + '/profile', + '/competitions', + '/competition', + '/exhibitions', + '/exhibition', + '/login', + '/registration', + '/artists', + '/my-paints', + '/partner', + '/news', +]; + +// ——— Lifecycle ——— +self.skipWaiting(); +workbox.core.clientsClaim(); + +// Clean old caches + notify clients +self.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => { + console.log(`Deleting old cache: ${key}`); + return caches.delete(key); + }) + ); + + const clients = await self.clients.matchAll(); + clients.forEach((client) => client.postMessage({ type: 'SW_UPDATED' })); + console.log('Service Worker activated.'); + })() + ); +}); + +// ——— Precaching ——— +workbox.precaching.precacheAndRoute( + ASSETS_TO_CACHE.map((url) => ({ url, revision: null })), + { + cacheName: CACHE_NAME, + } +); + +// ——— Runtime routes ——— + +// 1. Never cache these paths (NetworkOnly) +workbox.routing.registerRoute( + ({ url }) => + NOCACHE_PATHS.some( + (path) => url.pathname === path || url.pathname.startsWith(path + '/') + ), + new workbox.strategies.NetworkOnly() +); + +// 2. Paths that should revalidate on Last-Modified / Content-Length +workbox.routing.registerRoute( + ({ url }) => + VALIDATE_ON_MODIFY_PATHS.some( + (path) => url.pathname === path || url.pathname.startsWith(path + '/') + ), + new workbox.strategies.NetworkFirst({ + cacheName: CACHE_NAME, + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({ + statuses: [0, 200], + }), + { + // Custom plugin: only update cache when content actually changed + // or when the cached entry is suspiciously small + cacheWillUpdate: async ({ request, response, event }) => { + if (!response || response.status !== 200) return null; + + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request); + + if (!cached) return response; + + const contentLength = parseInt( + cached.headers.get('Content-Length') || '0', + 10 + ); + const isTooSmall = contentLength > 0 && contentLength < MIN_VALID_SIZE; + + if (isTooSmall) { + console.warn( + `Cached file ${request.url} was too small (${contentLength} bytes). Forcing update.` + ); + return response; + } + + const [cachedModified, netModified] = await Promise.all([ + cached.headers.get('Last-Modified'), + response.headers.get('Last-Modified'), + ]); + const [cachedLen, netLen] = await Promise.all([ + cached.headers.get('Content-Length'), + response.headers.get('Content-Length'), + ]); + + if (cachedModified === netModified && cachedLen === netLen) { + console.log( + `Content unchanged for ${new URL(request.url).pathname}, keeping cached version` + ); + return null; // do not update cache + } + + console.log(`Caching/updating: ${new URL(request.url).pathname}`); + return response; + }, + }, + ], + }) +); + +// 3. Everything else → CacheFirst (with size safeguard) +workbox.routing.registerRoute( + ({ request }) => request.method === 'GET', + new workbox.strategies.CacheFirst({ + cacheName: CACHE_NAME, + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({ + statuses: [0, 200], + }), + { + // Force update when cached response is too small + cachedResponseWillBeUsed: async ({ + cacheName, + request, + cachedResponse, + }) => { + if (!cachedResponse) return null; + + const contentLength = parseInt( + cachedResponse.headers.get('Content-Length') || '0', + 10 + ); + if (contentLength > 0 && contentLength < MIN_VALID_SIZE) { + console.warn( + `Cached file ${request.url} was too small (${contentLength} bytes). Ignoring cache.` + ); + return null; // force network + } + return cachedResponse; + }, + }, + ], + }) +); + +// ——— Offline fallback for navigation requests ——— +workbox.routing.setCatchHandler(async ({ event }) => { + if (event.request.mode === 'navigate') { + const offline = await caches.match('/offline.html'); + if (offline) return offline; + + const root = await caches.match('/'); + if (root) return root; + } + + // For non-navigation requests just return whatever we have (or fail) + return Response.error(); +}); \ No newline at end of file