fase 3.4: PWA instalable (manifest e iconos, service worker propio con app shell precacheado y cache versionada por build, API nunca cacheada, fallback offline, aviso de instalacion con guia iOS y banner sin conexion) + branding: perfiles de ticket por instancia (defaults neutros, sin filtrar datos de SKEEN) y Dashboard sin nombre hardcodeado
This commit is contained in:
192
frontend/public/sw.js
Normal file
192
frontend/public/sw.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/* =====================================================================
|
||||
Service Worker del Panel Clínico (SKEEN)
|
||||
---------------------------------------------------------------------
|
||||
Sin dependencias ni Workbox. Estrategia:
|
||||
|
||||
- App shell (HTML, manifest, iconos, JS/CSS con hash): precache en
|
||||
install, versionado por build.
|
||||
- Navegación (HTML): network-first. Un deploy nuevo se ve al primer
|
||||
refresh (nginx sirve index.html con Cache-Control: no-cache) y, si
|
||||
no hay red, se devuelve la última copia del shell o /offline.html.
|
||||
- Assets con hash (/assets/*): cache-first (son inmutables).
|
||||
- Otras GET del mismo origen (logos de marca, svg): stale-while-revalidate.
|
||||
- API (/api/*), peticiones con Authorization y cualquier método distinto
|
||||
de GET: NUNCA se cachean. Se dejan pasar tal cual al navegador.
|
||||
|
||||
La versión de caché y la lista de assets se inyectan en el build
|
||||
(vite.config.ts); si no se inyectan, el SW sigue siendo funcional.
|
||||
===================================================================== */
|
||||
|
||||
var VERSION = '__BUILD_VERSION__';
|
||||
|
||||
// Lista de assets con hash generados por el build (JSON inyectado).
|
||||
var PRECACHE_ASSETS = [];
|
||||
try { PRECACHE_ASSETS = JSON.parse('__PRECACHE_ASSETS__'); } catch (e) { PRECACHE_ASSETS = []; }
|
||||
|
||||
var PRECACHE = 'skeen-precache-' + VERSION;
|
||||
var RUNTIME = 'skeen-runtime-' + VERSION;
|
||||
var CACHE_PREFIX = 'skeen-';
|
||||
|
||||
var PRECACHE_URLS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/offline.html',
|
||||
'/manifest.webmanifest',
|
||||
'/favicon.svg',
|
||||
'/icons/icon.svg',
|
||||
'/icons/icon-32.png',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png',
|
||||
'/icons/icon-192-maskable.png',
|
||||
'/icons/icon-512-maskable.png',
|
||||
'/icons/apple-touch-icon.png',
|
||||
// Brand neutro por defecto: permite pintar el login sin red.
|
||||
'/brand/favicon.svg',
|
||||
'/brand/logo-blanco.svg',
|
||||
'/brand/logo-negro.svg',
|
||||
].concat(PRECACHE_ASSETS);
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// install / activate
|
||||
// --------------------------------------------------------------------
|
||||
self.addEventListener('install', function (event) {
|
||||
event.waitUntil(
|
||||
caches.open(PRECACHE).then(function (cache) {
|
||||
// add() individual: un recurso que falle no aborta toda la instalación.
|
||||
return Promise.all(
|
||||
PRECACHE_URLS.map(function (url) {
|
||||
return cache.add(new Request(url, { cache: 'reload' })).catch(function () { /* ignore */ });
|
||||
})
|
||||
);
|
||||
}).then(function () { return self.skipWaiting(); })
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(
|
||||
caches.keys().then(function (keys) {
|
||||
return Promise.all(
|
||||
keys.map(function (key) {
|
||||
// Borra cachés de builds anteriores (mismo prefijo, otra versión).
|
||||
if (key.indexOf(CACHE_PREFIX) === 0 && key !== PRECACHE && key !== RUNTIME) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
}).then(function () { return self.clients.claim(); })
|
||||
);
|
||||
});
|
||||
|
||||
// Permite forzar la activación desde la página si se desea.
|
||||
self.addEventListener('message', function (event) {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Estrategias
|
||||
// --------------------------------------------------------------------
|
||||
function isSameOrigin(url) {
|
||||
return url.origin === self.location.origin;
|
||||
}
|
||||
|
||||
function isApiRequest(url) {
|
||||
return url.pathname.indexOf('/api/') === 0;
|
||||
}
|
||||
|
||||
function isHashedAsset(url) {
|
||||
return isSameOrigin(url) && url.pathname.indexOf('/assets/') === 0;
|
||||
}
|
||||
|
||||
async function networkFirstNavigation(request) {
|
||||
try {
|
||||
var fresh = await fetch(request);
|
||||
var type = fresh.headers.get('content-type') || '';
|
||||
if (fresh && fresh.ok && type.indexOf('text/html') !== -1) {
|
||||
var cache = await caches.open(PRECACHE);
|
||||
// Se guarda una única copia del shell bajo /index.html.
|
||||
cache.put('/index.html', fresh.clone()).catch(function () { /* ignore */ });
|
||||
}
|
||||
return fresh;
|
||||
} catch (e) {
|
||||
var shell = await caches.open(PRECACHE);
|
||||
var cached = (await shell.match('/index.html')) || (await shell.match('/'));
|
||||
if (cached) return cached;
|
||||
var offline = await shell.match('/offline.html');
|
||||
if (offline) return offline;
|
||||
return new Response('Sin conexión', {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheFirst(request, cacheName) {
|
||||
var cache = await caches.open(cacheName);
|
||||
var hit = await cache.match(request);
|
||||
if (hit) return hit;
|
||||
var res = await fetch(request);
|
||||
if (res && res.ok && res.type === 'basic') {
|
||||
cache.put(request, res.clone()).catch(function () { /* ignore */ });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function staleWhileRevalidate(request, cacheName) {
|
||||
var cache = await caches.open(cacheName);
|
||||
var hit = await cache.match(request);
|
||||
var network = fetch(request).then(function (res) {
|
||||
if (res && res.ok && res.type === 'basic') {
|
||||
cache.put(request, res.clone()).catch(function () { /* ignore */ });
|
||||
}
|
||||
return res;
|
||||
}).catch(function () { return undefined; });
|
||||
if (hit) return hit;
|
||||
var res = await network;
|
||||
if (res) return res;
|
||||
return new Response('', { status: 504, statusText: 'Sin conexión' });
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// fetch
|
||||
// --------------------------------------------------------------------
|
||||
self.addEventListener('fetch', function (event) {
|
||||
var request = event.request;
|
||||
var url;
|
||||
|
||||
try {
|
||||
url = new URL(request.url);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Nunca interceptar lo que no debe cachearse:
|
||||
// - métodos distintos de GET
|
||||
// - peticiones con Authorization (JWT / datos clínicos)
|
||||
// - API del backend (/api/odoo/... y /api/v1/...)
|
||||
// - backend Odoo directo (/web/)
|
||||
if (request.method !== 'GET') return;
|
||||
if (request.headers.get('authorization')) return;
|
||||
if (isApiRequest(url)) return;
|
||||
if (url.pathname.indexOf('/web/') === 0) return;
|
||||
|
||||
// 2) Navegación HTML: network-first con fallback al shell cacheado.
|
||||
if (request.mode === 'navigate') {
|
||||
event.respondWith(networkFirstNavigation(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Assets con hash: cache-first (inmutables).
|
||||
if (isHashedAsset(url)) {
|
||||
event.respondWith(cacheFirst(request, RUNTIME));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Otras GET del mismo origen (logos, svg): SWR.
|
||||
if (isSameOrigin(url)) {
|
||||
event.respondWith(staleWhileRevalidate(request, RUNTIME));
|
||||
return;
|
||||
}
|
||||
|
||||
// 5) Cross-origin (p. ej. Google Fonts): sin interceptar.
|
||||
});
|
||||
Reference in New Issue
Block a user