Some checks are pending
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>
84 lines
3 KiB
JavaScript
84 lines
3 KiB
JavaScript
/* 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;
|
||
})
|
||
)
|
||
);
|
||
}
|
||
});
|