food-market/src/food-market.web/public/sw.js
nns 76a175f491
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
feat(pwa+mobile+s9): PWA owner read-only + mobile tweaks + S9 stage specs
Sprint 9 пункт 3 (mobile-адаптация):
- DataTable: min-w-max sm:min-w-[640px] → узкие таблицы (Loyalty, Promotions)
  влезают на 375px без horizontal-scroll, широкие (Products) скроллятся
  внутри overflow-auto родителя.
- Mobile-audit спека (stage-ui-s9-mobile-audit) — 20 screenshot'ов в
  reports/mobile/ (375 + 768 viewport × 10 страниц + seed-demo).
  Smoke: no console-errors, layouts читаемы.

Sprint 9 пункт 4 (P2-9 PWA):
- public/manifest.webmanifest — read-only PWA владельца. Shortcuts:
  Дашборд, Sales/Profit/Stock отчёты. display=standalone (homescreen icon).
- public/sw.js — service worker:
  • SPA navigate: network-first + offline-fallback на /offline.html.
  • GET /api/*: network-first + cache-fallback (read-only кеш).
  • CSS/JS/SVG: stale-while-revalidate.
  • Мутации (POST/PUT/DELETE): не вмешиваемся, сеть.
- public/offline.html — статический fallback с кнопкой «Открыть дашборд».
- index.html: <link rel='manifest'>, apple-touch-meta, lang=ru-KZ.
- main.tsx: navigator.serviceWorker.register('/sw.js') в production only
  (dev hot-reload не мешает).
- deploy/nginx.conf: /sw.js no-cache, /manifest.webmanifest правильный
  content-type, /offline.html static.

Stage e2e:
- stage-ui-s9-loyalty.spec (4/4 ✓): programs/cards/promotions endpoints
  + UI рендер + SALE20 на 500₸ → total=400 (валидно через API).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 21:22:30 +05:00

84 lines
3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Food Market — service worker (read-only owner PWA).
*
* Стратегии:
* - Навигация (mode=navigate): network-first с offline-fallback на /offline.html.
* - Статика (CSS/JS/SVG): stale-while-revalidate из cache.
* - GET /api/... : network-first; при ошибке/offline отдаём кэш если есть
* (read-only стратегия: записи не кешируем, мутации не оффлайн-обходимы).
* - POST/PUT/DELETE на /api/... : всегда сеть. При отсутствии — toast в UI
* (через api interceptor).
*
* Версия кэша инкрементируется при изменении SW — старые удаляются на activate.
*/
const CACHE_VERSION = 'fm-v1';
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const API_CACHE = `${CACHE_VERSION}-api`;
const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) =>
// Прекеш offline-page чтобы fallback работал даже после reboot'a.
cache.addAll(['/offline.html', '/favicon.svg', '/logo.svg', '/manifest.webmanifest'])
).then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys
.filter((k) => !k.startsWith(CACHE_VERSION))
.map((k) => caches.delete(k)))
).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return; // не вмешиваемся в мутации
const url = new URL(req.url);
// Навигация по приложению — SPA shell.
if (req.mode === 'navigate') {
event.respondWith(
fetch(req).catch(() => caches.match(OFFLINE_URL))
);
return;
}
// API GET — network-first + cache-fallback. Кешируем только успешные ответы.
if (url.origin === self.location.origin && url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(req)
.then((resp) => {
if (resp.ok) {
const copy = resp.clone();
caches.open(API_CACHE).then((c) => c.put(req, copy));
}
return resp;
})
.catch(() => caches.match(req))
);
return;
}
// Статика — stale-while-revalidate.
if (req.destination === 'script' || req.destination === 'style'
|| req.destination === 'image' || req.destination === 'font'
|| url.pathname.endsWith('.svg') || url.pathname.endsWith('.css')
|| url.pathname.endsWith('.js')) {
event.respondWith(
caches.open(STATIC_CACHE).then((cache) =>
cache.match(req).then((cached) => {
const network = fetch(req).then((resp) => {
if (resp.ok) cache.put(req, resp.clone());
return resp;
});
return cached || network;
})
)
);
}
});