PageSourceSearch

https://cortex-cashflow.onrender.com/sw.js

js cortex-cashflow.onrender.com collected 2026-10-03 11:40:28 UTC 30,040 bytes, 476 lines download raw bytes

1/**
2 * ═══════════════════════════════════════════════════════════════════════════════
3 * CashFlow — Service Worker: CAPA DE DATOS de la caché offline (patrón cache-first)
4 * ═══════════════════════════════════════════════════════════════════════════════
5 *
6 * Servido desde /sw.js (raíz) → scope '/', controla toda la app.
7 *
8 * ARQUITECTURA EN 3 CAPAS (mismo patrón que un Dexie/IndexedDB, adaptado):
9 *   1. Capa de datos (ESTE archivo)  → define "qué tablas existen": los caches
10 *      del navegador (Cache Storage) y la estrategia de lectura/escritura de cada uno.
11 *   2. Capa de acceso (assets/js/cache.js) → el "DTO": sabe hablar con este SW
12 *      (mensajes SINCRONIZAR) y orquesta la precarga de páginas.
13 *   3. Capa de UI (botón Sincronizar del aside) → decide CUÁNDO sincronizar.
14 *
15 * ¿Por qué Cache Storage y no IndexedDB/Dexie? CashFlow es server-rendered
16 * (Handlebars): los datos NO llegan por una API JSON que se pueda guardar en
17 * tablas — llegan ya renderizados dentro del HTML. La "unidad de dato" acá es
18 * la página entera, y el almacén nativo del navegador para respuestas HTTP
19 * completas es Cache Storage (lo maneja el service worker). El patrón lógico
20 * es el mismo: guardar local, leer local cuando la red falta, pisar todo al
21 * sincronizar.
22 *
23 * LAS "TABLAS" (un cache por tipo de dato, versionadas):
24 *   - cf-estaticos-vN → CSS/JS/imágenes/fuentes (propios y de CDNs permitidos).
25 *       Estrategia: cache-first con revalidación en segundo plano
26 *       (stale-while-revalidate). Es la ganancia de EFICIENCIA: estos archivos
27 *       casi nunca cambian y hoy se re-piden en cada navegación.
28 *   - cf-paginas-vN → el HTML renderizado de las pantallas (la "lista").
29 *       Estrategia: NETWORK-FIRST con fallback a caché. Distinto del ticket
30 *       original a propósito: acá el dato es financiero y cambia con cada
31 *       operación del usuario, así que con internet SIEMPRE se muestra lo
32 *       fresco; la caché solo aparece cuando no hay red.
33 *   - cf-pdfs-vN → los PDFs de facturación (el "contenido on demand"):
34 *       se guardan recién cuando el usuario abre ese PDF por primera vez.
35 *       Estrategia: network-first con fallback (el PDF de un detalle cambia
36 *       si cambia su estado, no podemos asumirlo inmutable).
37 *
38 * INVALIDACIÓN: por sincronización (mensaje {tipo:'SINCRONIZAR'} desde
39 * cache.js) → se borran cf-paginas y cf-pdfs y el cliente re-descarga todo.
40 * También se purga automáticamente al navegar a pages-login (= logout/cambio
41 * de usuario): jamás dejamos datos financieros cacheados de una sesión vieja.
42 *
43 * Para invalidar TODO en un deploy (cambió CSS/JS/estructura): subir VERSION.
44 * ═══════════════════════════════════════════════════════════════════════════════
45 */
46'use strict';
47
48// ── Versión global de la caché: subirla invalida todos los caches viejos ──────
49// v5→v10 (2026-08-06): cambios en pages-tienda (imágenes solo por URL, spinner de
50// carga, destello de éxito, stock de variaciones "en stock" por defecto, guardia
51// anti-SKU duplicado, panel responsive con scroll/footer sticky, atributos GLOBALES
52// para swatches de YITH y generación de variaciones robusta [guarda atributos del
53// padre antes de generar + reporta errores]) → invalida el JS/CSS viejo cacheado.
54// v13 (2026-08-06): paneles off-canvas de tienda mobile-fix — safe-area-inset-top
55// en el header (no lo tapa el reloj/notch del iPhone), ancho edge-to-edge sin
56// desborde, X con área táctil grande y cierre con gesto (swipe a la derecha).
57// v14 (2026-08-08): módulo Agenda (CRM + Calendario) — nueva sección en el aside,
58// CSS nav-agenda + variable --mod-agenda, y pantallas pages-agenda-panel/
59// contactos/calendario. Invalida el JS/CSS viejo cacheado (aside, variables.css).
60// v15 (2026-08-08): Facturación → comprobantes de cobro + constancia de recepción
61// (modal en pages-facturacion, uploader de firma en el perfil). Invalida el JS viejo.
62// v16 (2026-08-08): Agenda → Informes (editor Markdown + importar con IA + PDF de
63// marca en página blanca). Nueva pantalla pages-agenda-informes + nav. Invalida caché.
64// v17 (2026-08-08): Agenda → asistente "Jarvis" con IA en el panel (análisis de
65// contactos + agenda). Actualiza pages-agenda-panel. Invalida el JS viejo.
66// v18 (2026-08-08): Agenda → tipos de contacto configurables por el usuario
67// (modal "Tipos", filtros/badges/KPIs dinámicos) + Condición IVA como dropdown
68// con opción "Otro". Actualiza pages-agenda-contactos/panel. Invalida el JS viejo.
69// v19 (2026-08-08): Agenda → página pública de reservas (Calendly). Config en el
70// Panel + página pública /reserva/<slug> (fuera de caché). Invalida el JS del panel.
71// v20 (2026-08-08): Agenda → informes/reuniones en la ficha del contacto, permisos
72// por autor (solo el creador edita/borra), y estadísticas premium en el Panel
73// (gráficos Chart.js). Invalida el JS de contactos/informes/panel.
74// v21 (2026-08-08): Facturación ligada al contacto por contactoId (selector en el
75// form + vincular/desvincular en la ficha) + automatización de facturación mensual
76// (recurrencia por contacto, cron, link público /doc/:token, wa.me, semi-auto
77// "listo para facturar" al pagar). Invalida el JS de contactos/setFacturacion.
78// v22 (2026-08-08): acción "Asociar a contacto" por fila en la tabla de
79// Facturación (PUT /facturacion/:id/contacto + modal). Invalida pages-facturacion.
80// v23 (2026-08-08): panel lateral de acciones (ícono lápiz) que consolida los
81// íconos de cada fila — mobile-first (en mobile el row solo muestra el lápiz).
82// Invalida pages-facturacion.
83// v24 (2026-08-08): el row muestra SOLO el lápiz en TODOS los tamaños; las demás
84// acciones se acceden desde el panel lateral (también en desktop). Invalida CSS.
85// v25 (2026-08-08): vista propia del cliente con gráficos + análisis automático
86// (estable/riesgo) + BCRA cacheado; segmentación en el panel. Invalida contactos/panel.
87// v26 (2026-08-08): FIX routing — /pages-cliente?c=<id> (query, no ruta anidada)
88// para no romper los links relativos del aside; toasts + animaciones sutiles.
89// v27 (2026-08-08): segmentación del panel rediseñada (conversión % + donut +
90// chips/lista prolijos), base para el módulo de Marketing. Invalida panel.
91// v28 (2026-08-08): módulo Tickets (bandeja agnóstica + estadísticas + vinculación
92// Chatwoot open-source; webhook /webhooks/tickets/:token). Nueva pantalla + nav.
93// v29 (2026-08-08): Tickets → widget web PROPIO (/tickets/widget.js, un snippet
94// gratis sin terceros) + tickets asociables a contactos e informes + aparecen en
95// la ficha del cliente. Invalida pages-agenda-tickets/cliente.
96// v30 (2026-08-09): Facturación recurrente → tipo de documento + modo borrador con
97// cola "Pendientes de confirmar" (editar montos, enviar/almacenar) en Facturación
98// y en la ficha del cliente + accesos directos para generar detalle/factura/informe
99// desde el contacto. Invalida pages-facturacion/cliente/agenda-contactos/informes.
100// v32 (2026-08-13): Tickets → adjuntos (subir/descargar/eliminar imágenes y docs) +
101// resolución vía informe (botón "Crear informe de resolución" prellenado desde el
102// ticket) + envío del informe por mail con destinos rápidos (Cliente/Equipo/Otro) y
103// mail de equipo configurable. Invalida pages-agenda-tickets/informes.
104// v33 (2026-08-13): Operaciones y Servicios → filtro de período (semanal/mensual/
105// anual/interanual/histórico) que gobierna el gasto total, el treemap/torta, Top
106// Categorías y el Resumen por Categoría (dinámico), con variación vs período previo
107// en cada categoría (estilo KPIs del index). Helpers de período + createPerio
107dControl
108// en ag-grid-shared.js/css. Invalida pages-operation/services + ag-grid-shared.
109// v35 (2026-08-15): Vista "Resumen" mobile-first (/auth/pages-resumen, misma
110// fuente de datos que index) + bienvenida reducida que deriva según dispositivo +
111// Ritmo de consumo con bandas ajustadas por inflación, meta de ahorro y cuota
112// proyectada. Invalida index, pages-welcome, pages-credit, setFacturacion,
113// setFacturaElectronica, pages-agenda-informes/calendario y el aside.
114// v36 (2026-08-15): Mi Tienda → lector de código de barras (pistola USB + cámara
115// con BarcodeDetector nativo) en el SKU del producto, y pestaña CAJA (punto de
116// venta: escaneo → ticket → QR de pago). Panel admin → nivel "oculto" y módulos
117// base de CashFlow configurables. Splash de marca en la bienvenida.
118// Invalida pages-tienda, pages-admin, pages-welcome, index y el aside.
119// v37 (2026-08-15): Ritmo → "ahorro proyectado" pasa a ser ingreso − objetivo
120// (lo que sobra), no el recorte contra el promedio. Caja → espejo opcional con
121// WooCommerce: la venta de mostrador crea el pedido, descuenta stock y entra a
122// CashFlow por el camino habitual. Invalida index, pages-resumen y pages-tienda.
123// v38 (2026-08-15): Mi Tienda → CAJA DE AUTOPAGO pública (/caja/<token>): el
124// cliente escanea sus productos en una pantalla del local, paga con QR de MP y
125// el webhook cierra la venta (pedido en WooCommerce + stock). Panel: activar la
126// pantalla, link rotable y logo de la tienda. Invalida pages-tienda.
127// v39 (2026-08-15): TICKET de compra (numeración por tienda, PDF 80mm, vista
128// pública por QR y envío por mail) + alta opt-in del cliente al CRM desde el
129// ticket + LIBRO DE OPERACIONES con totales y export CSV. Créditos: capital
130// tomado vs. revaluado. Todo documentado en Ayuda.
131// Invalida pages-tienda, pages-credit y pages-help.
132// v40 (2026-08-18): Ritmo de consumo → la meta de ahorro muestra en plata lo que
133// hace (gasto proyectado del mes y ahorro total sobre el ingreso), la barra
134// explica cada línea con un globo propio (hover y tap) y pinta rayado el tramo
135// de la cuota con el % que ocupa del presupuesto. Invalida index.
136// v41 (2026-08-21): Facturación anti-duplicado → clave de idempotencia en los
137// formularios de emisión (Factura C y detalle de servicios), botón trabado
138// mientras hay un pedido en vuelo y aviso cuando el servidor frena un
139// duplicado. Invalida setFacturaElectronica, setFacturacion y pages-facturacion.
140// v45 (2026-08-21): Informes → editor WYSIWYG (TinyMCE) en lugar del textarea de
141// Markdown; el contenido se sigue guardando en Markdown (puente con turndown).
142// Invalida pages-agenda-informes, que quedó cacheada rota en v44 (se cargaba
143// tinymce.min.js dos veces y el editor no montaba).
144// v46 (2026-08-21): Informes → pegar Markdown crudo en el editor lo convierte a
145// formato real (marked en el paste) en vez de dejar los `##`/`**` literales.
146// v47 (2026-08-21): Fases 0 y 1 del reordenamiento modular. Dos cambios de URL
147// de assets, los dos obligan a invalidar (un cliente con caché v46 tiene las
148// URLs viejas guardadas y, sin subir la versión, las sigue pidiendo y come 404):
149//   1. Prefijo canónico único /assets/* — los alias /js/* y /vendor/* ya no
150//      existen en el server.
151//   2. Mi Tienda pasó a src/modules/tienda/ y sus CSS/JS ahora se sirven en
152//      /m/tienda/* en vez de /assets/{css,js}/pages/pages-tienda*.
153// Ver docs/ARQUITECTURA_MODULAR.md §4.3.
154// v48 (2026-08-21): Fases 2 a 5 — los 12 modulos pasaron a src/modules/*. Sus
155// CSS y JS ahora se sirven en /m/<modulo>/* (antes /assets/{css,js}/pages/*), y
156// se sumo la libreria compartida /assets/js/lib/*. Cambian URLs de assets en
157// TODAS las pantallas de modulo: invalidacion obligatoria.
158// v49-v50 (2026-08-25): Creditos → asistente contable con IA por credito (boton,
159// panel de indicadores y render de markdown en pages-credit) + linea "Prom. 12m
160// ahorrado" en los KPIs Neto Disponible y Remanente del index. Invalida el CSS
161// del modulo creditos y el JS del index.
162// v51 (2026-08-25): pages-login rediseñado con la estetica del splash de
163// bienvenida (fondo de marca, circuitos lime/violeta con trazado en bucle) y
164// baja del video de fondo + particulas. Invalida pages-login.css.
165// v52 (2026-08-25): Creditos → chat de seguimiento sobre el informe del
166// asistente contable (hilo, input y estilos nuevos en pages-credit). Invalida
167// el CSS/JS del modulo creditos.
168// v53 (2026-08-25): Creditos → boton Compartir del informe (CortexIA, WhatsApp,
169// mail, copiar, descargar .md) + pages-chat-ia levanta el informe enviado desde
170// otro modulo. Invalida el CSS/JS de creditos y la vista de CortexIA.
171// v54 (2026-08-25): aside → "Mi Tienda" pasa de Principal a Vinculaciones
172// (encabezando las tiendas vinculadas) y "Resumen" queda solo-mobile (en
173// escritorio duplicaba a "Mis finanzas"). Invalida el CSS del sidebar.
174// v55 (2026-08-26): login/registro/recuperacion → links en lila de marca (el
175// lime no se leia en tema claro) y la tarjeta se fuerza oscura sobre la regla
176// global `.card{background:var(--bg-card)!important}` de layout.css, que en
177// claro la volvia blanca con texto blanco. Invalida los 3 CSS de auth.
178// v56 (2026-08-26): KPIs del index con su detalle propio (posible ahorro del mes
179// / ahorro promedio mensual); libreria compartida assets/js/lib/voz.js (dictado
180// en cualquier input de IA) + su CSS en style.css; carátula del PDF de Informes
181// con el color Fondo del manual de marca; opcion "Guardar en Informes" en el
182// menu Compartir del asistente de creditos. Invalida style.css, el JS de lib y
183// el CSS/JS de creditos.
184// v69 (2026-09-01): selector de modelo de IA — chip con el LLM en uso, sus
185// limites y el cambio de modelo debajo de los inputs de IA (chat y panel
186// flotante), y los topes del plan + el modelo de cada key en el panel
187// "Disponibilidad de IA" del header. Assets nuevos: /assets/js/ai-modelo.js y
188// /assets/css/ai-modelo.css; invalida ai-estado.js, el CSS/JS de cortexia y
189// head/script.
190// v71 (2026-09-04): IA compartida administrable desde el panel Administrador
191// (interruptor + limite por usuario + techo global). El panel "Disponibilidad
192// de IA" y el chip de modelo ahora distinguen "apagada" de "sin cuota" e
193// "ilimitada": invalida ai-estado.js y ai-modelo.js.
194// v74 (2026-09-09): reuniones con recordatorio configurable ("avisame X antes")
195// y agendado desde la ficha del cliente. Assets nuevos: /m/agenda/js/reuniones.js;
196// invalida pages-agenda-calendario y pages-cliente.
197// v75 (2026-09-09): invitados por reunión (varios contactos del CRM + mails
198// sueltos), que viajan al link de Google y al recordatorio. Assets nuevos:
199// /m/agenda/css/reuniones.css; invalida reuniones.js y las dos pantallas.
200// v76 (2026-09-09): segundo camino para agendar — .ics generado en el navegador
201// (lo abre la app de calendario del sistema) junto al botón de Google. Invalida
202// reuniones.js, pages-cliente-reuniones.js y las dos pantallas.
203// v77 (2026-09-12): CRM · Análisis con actividad, registro de horas y demanda
204// por cliente (/m/agenda/css/pages-agenda-panel.css, /m/agenda/js/pages-agenda-
205// panel.js y pages-agenda-actividad.js); módulo Automatizaciones n8n (/m/n8n/*);
206// Ritmo de consumo con ritmo habitual del mes (/assets/js/ritmo-habitual.js,
207// invalida index, pages-resumen y pages-resumen.css).
208// v78 (2026-09-14): Ritmo de consumo → compara lo CARGADO con lo que se suele
209// tener cargado (curva por momento de carga), cierre Bornhuetter-Ferguson y
210// alerta por escenario más favorable, para que cargar tarde o de a tandas no
211// dispare la proyección. Invalida /assets/js/ritmo-habitual.js.
212// v79 (2026-09-16): diagramas Mermaid — /assets/js/lib/diagramas.js (nuevo, global),
213// /m/agenda/js/informe-diagramas.js e informe-preparar.js (nuevos), editor de
214// Informes, y el chat de CortexIA (cortex-chat.js, pages-chat-ia-0/1.js,
215// aianalytics.js). La página intermedia del PDF (/auth/crm/informe/:id/preparar)
216// no se cachea: sin red no tiene sentido y solo redirige.
217// v80 (2026-09-16): Ritmo de consumo → meta de ahorro fijada por MONTO (lápiz:
218// gasto máximo o ahorro, y el % sale solo). Invalida /assets/js/ritmo-habitual.js
219// (metaFijada / anclaEnPesos), index y pages-resumen.
220// v81 (2026-09-17): módulo Automatizaciones (n8n) completo — pantalla /pages-n8n
221// con sus 7 scripts y CSS en /m/n8n/*, entrada nueva en el aside y en el resumen
222// mobile, ficha en Ayuda y límite por plan en el panel Administrador. Además,
223// opt-out del sonido global por request (fetch(url,{sonido:false})) para los
224// refrescos automáticos: invalida /assets/js/sound.js, el aside, index,
225// pages-resumen, pages-resumen.css, pages-help, pages-admin y cache.js.
226// v83 (2026-09-18): Informes con la sintaxis de Obsidian (callouts, ==resaltado==,
227// [[enlaces]], tareas, notas al pie, propiedades) y pantalla rediseñada mobile-first.
228// Nuevos: /m/agenda/js/obsidian-md.js, informe-obsidian.js, pages-agenda-informes.js
229// y /m/agenda/css/pages-agenda-informes.css; invalida pages-agenda-informes y pages-help.
230// v86–v88 (2026-10-02): Informes — barra de TinyMCE sticky por CSS, autoguardado,
231// vista previa del PDF sin guardar y vista gráfica de relaciones con el componente
232// nuevo /assets/js/lib/grafo.js (global, reutilizable).
233// v89 (2026-10-02): diagramas — /assets/js/lib/diagramas.js saca las marcas HTML de
234// formato y ajusta el ancho de las etiquetas a la palabra más larga; la vista previa
235// del PDF dibuja sus diagramas (informe-diagramas / pages-agenda-informes).
236// v90 (2026-10-02): Informes — importar una bóveda/carpeta/.zip/varios .md de una vez
237// (nuevo /m/agenda/js/informe-importar-boveda.js; JSZip lazy desde jsDelivr).
238// v91 (2026-10-02): tablas de Informes — el ancho de columna ajustado en el editor
239// viaja en la fila separadora del Markdown (obsidian-md.js) y lo respeta el PDF.
240var VERSION = 'v92';
241var CACHE_ESTATICOS = 'cf-estaticos-' + VERSION;
242var CACHE_PAGINAS = 'cf-paginas-' + VERSION;
243var CACHE_PDFS = 'cf-pdfs-' + VERSION;
244// Lista completa de caches "nuestros" de esta versión (para limpiar los ajenos).
245var CACHES_VIGENTES = [CACHE_ESTATICOS, CACHE_PAGINAS, CACHE_PDFS];
246
247// ── CDNs de assets que sí cacheamos (jQuery, DataTables, fuentes…) ────────────
248// Todo otro origen externo (PayPal, Gravitec, APIs) pasa directo a la red:
249// son dinámicos o llevan lógica de pago/push que no debe servirse vieja.
250var HOSTS_CDN_PERMITIDOS = [
251  'code.jquery.com',
252  'cdn.datatables.net',
253  'cdn.jsdelivr.net',
254  'cdnjs.cloudflare.com',
255  'fonts.googleapis.com',
256  'fonts.gstatic.com'
257];
258
259// ── Páginas que NUNCA se cachean (auth, admin, errores) ───────────────────────
260// pages-login además dispara la purga total (es el logout de la app).
261var PAGINAS_EXCLUIDAS = [
262  '/auth/pages-login',
263  '/auth/pages-register',
264  '/auth/pages-recovery',
265  '/auth/pages-confirm',
266  '/auth/pages-error-login',
267  '/auth/pages-error-404',
268  '/auth/pages-admin'
269];
270
271// Extensiones que identifican un asset estático (se sirven desde varias rutas
272// raíz por los múltiples express.static de index.js, así que filtramos por
273// extensión y no por prefijo de carpeta).
274var REGEX_ESTATICO = /\.(css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot|map)$/i;
275
276/* ═══════════════════════════════════════════════════════════════════════════
277   CICLO DE VIDA
278   ═══════════════════════════════════════════════════════════════════════════ */
279
280self.addEventListener('install', function () {
281  // Activar de inmediato la versión nueva sin esperar a que se cierren pestañas.
282  self.skipWaiting();
283});
284
285self.addEventListener('activate', function (event) {
286  event.waitUntil(
287    // Migración de versión: borrar cualquier cache nuestro que no sea de la
288    // versión vigente (equivalente a la "migración automática" de esquema).
289    caches.keys().then(function (nombres) {
290      return Promise.all(
291        nombres
292          .filter(function (n) { return n.indexOf('cf-') === 0 && CACHES_VIGENTES.indexOf(n) === -1; })
293          .map(function (n) { return caches.delete(n); })
294      );
295    }).then(function () {
296      // Tomar control de las pestañas abiertas sin esperar una recarga.
297      return self.clients.claim();
298    })
299  );
300});
301
302/* ═══════════════════════════════════════════════════════════════════════════
303   MENSAJES — la capa de acceso (cache.js) habla con el SW por acá
304   ═══════════════════════════════════════════════════════════════════════════ */
305
306self.addEventListener('message', function (event) {
307  var msg = event.data || {};
308
309  // SINCRONIZAR = invalidación total de datos: se pisa lo viejo.
310  // (Los estáticos no se borran: se revalidan solos en segundo plano y borrar
311  //  ~2 MB de vendors para re-bajarlos en cada sync sería puro desperdicio.)
312  if (msg.tipo === 'SINCRONIZAR') {
313    event.waitUntil(
314      purgarDatos().then(function () {
315        // Respondemos por el MessageChannel que abrió cache.js, para que la UI
316        // sepa que la purga terminó antes de arrancar la precarga.
317        if (event.ports && event.ports[0]) event.ports[0].postMessage({ ok: true });
318      })
319    );
320  }
321});
322
323/** Borra los caches de DATOS (páginas + PDFs). Los estáticos quedan. */
324function purgarDatos() {
325  return Promise.all([caches.delete(CACHE_PAGINAS), caches.delete(CACHE_PDFS)]);
326}
327
328/* ═══════════════════════════════════════════════════════════════════════════
329   FETCH — el "router" de estrategias: decide qué tabla y qué estrategia
330   corresponde a cada request
331   ═══════════════════════════════════════════════════════════════════════════ */
332
333self.addEventListener('fetch', function (event) {
334  var req = event.request;
335
336  // Solo GET: los POST/PUT (crear operaciones, emitir facturas…) van SIEMPRE
337  // a la red — nunca hay que responder una escritura desde caché.
338  if (req.method !== 'GET') return;
339
340  var url;
341  try { url = new URL(req.url); } catch (e) { return; }
342
343  // Ignorar esquemas raros (chrome-extension://, etc.).
344  if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
345
346  var esMismoOrigen = (url.origin === self.location.origin);
347
348  // El propio SW y el manifest se dejan al navegador (Chrome maneja su update).
349  if (esMismoOrigen && (url.pathname === '/sw.js')) return;
350
351  // uPrime embebido (/uPrime/*) es otra app con su propio service worker: este
352  // no lo toca (si no, cachearía sus páginas como si fueran de CashFlow).
353  if (esMismoOrigen && /^\/uprime(\/|$)/i.test(url.pathname)) return;
354
355  // ── 1) PDFs de facturación → "contenido on demand" ─────────────────────────
356  // Se cachean recién cuando el usuario los abre (primer GET). Con red se
357  // sirve el fresco (el PDF de un detalle cambia con su estado); sin red, el
358  // último que vio.
359  if (esMismoOrigen && /^\/auth\/facturacion\/.+\/pdf$/.test(url.pathname)) {
360    event.respondWith(redPrimeroConFallback(req, CACHE_PDFS, false));
361    return;
362  }
363
364  // ── 2) Navegaciones (HTML de pantallas) → la "lista" ───────────────────────
365  if (req.mode === 'navigate' && esMismoOrigen) {
366    // Logout / vuelta al login: purgar los datos cacheados de la sesión.
367    // (waitUntil corre en paralelo, no demora la navegación.)
368    if (url.pathname === '/auth/pages-login') {
369      event.waitUntil(purgarDatos());
370      return; // el login siempre va directo a la red, sin caché
371    }
372    // Páginas sensibles o de error: red directa, sin tocar caché.
373    if (PAGINAS_EXCLUIDAS.indexOf(url.pathname) !== -1) return;
374    // Página intermedia de los PDF con diagramas: solo dibuja y redirige.
375    if (/^\/auth\/crm\/informe\/[^/]+\/preparar$/.test(url.pathname)) return;
376    // Webhooks u otras rutas fuera de /auth y raíz no son pantallas de la app.
377    if (url.pathname.indexOf('/auth/') !== 0 && url.pathname !== '/') return;
378
379    event.respondWith(redPrimeroConFallback(req, CACHE_PAGINAS, true));
380    return;
381  }
382
383  // ── 3) Assets estáticos (propios o de CDN permitida) → eficiencia ──────────
384  var esCdnPermitida = HOSTS_CDN_PERMITIDOS.indexOf(url.hostname) !== -1;
385  if ((esMismoOrigen || esCdnPermitida) && REGEX_ESTATICO.test(url.pathname)) {
386    event.respondWith(cachePrimeroRevalidando(req));
387    return;
388  }
389
390  // Todo lo demás (APIs JSON, orígenes externos no listados) pasa directo.
391});
392
393/* ═══════════════════════════════════════════════════════════════════════════
394   ESTRATEGIAS
395   ═══════════════════════════════════════════════════════════════════════════ */
396
397/**
398 * NETWORK-FIRST con fallback a caché (páginas y PDFs).
399 * Con red: se sirve lo fresco y se guarda copia local ("visitar = cachear").
400 * Sin red: se sirve la última copia guardada; si tampoco hay, la pantalla
401 * offline embebida (solo para navegaciones).
402 */
403function redPrimeroConFallback(req, nombreCache, esNavegacion) {
404  return fetch(req)
405    .then(function (respuesta) {
406      // Solo cacheamos respuestas 200 REALES de la página pedida:
407      //  - `redirected` filtra el caso "sesión vencida → redirect al login"
408      //    (si no, guardaríamos el HTML del login bajo la URL de la pantalla).
409      if (respuesta.ok && !respuesta.redirected) {
410        var copia = respuesta.clone();
411        caches.open(nombreCache).then(function (cache) { cache.put(req, copia); });
412      }
413      return respuesta;
414    })
415    .catch(function () {
416      // Falló la red → leer local (el corazón del modo offline).
417      return caches.match(req).then(function (guardada) {
418        if (guardada) return guardada;
419        if (esNavegacion) return paginaOffline();
420        // Un PDF nunca visto y sin red: error de red genuino.
421        return Response.error();
422      });
423    });
424}
425
426/**
427 * CACHE-FIRST con revalidación en segundo plano (stale-while-revalidate).
428 * Para estáticos: responde al instante desde el dispositivo y, en paralelo,
429 * baja la versión nueva para la próxima. Elimina los requests repetidos de
430 * CSS/JS/imágenes en cada navegación.
431 */
432function cachePrimeroRevalidando(req) {
433  return caches.open(CACHE_ESTATICOS).then(function (cache) {
434    return cache.match(req).then(function (guardada) {
435      // cache:'no-cache' = revalidar contra el server salteando la caché HTTP
436      // del navegador (los estáticos se sirven con Cache-Control de 1 día):
437      // si el archivo no cambió es un 304 sin body (baratísimo); si cambió
438      // (deploy nuevo) baja la versión fresca al instante en vez de esperar
439      // a que venza el maxAge.
440      var refresco = fetch(req, { cache: 'no-cache' })
441        .then(function (respuesta) {
442          // Las respuestas de CDN sin CORS llegan "opaque" (status 0): también
443          // sirven para offline, así que las guardamos igual.
444          if (respuesta && (respuesta.ok || respuesta.type === 'opaque')) {
445            cache.put(req, respuesta.clone());
446          }
447          return respuesta;
448        })
449        .catch(function () { return guardada; }); // sin red: no pasa nada, ya respondimos
450      return guardada || refresco;
451    });
452  });
453}
454
455/**
456 * Pantalla mínima para una navegación sin red Y sin copia local.
457 * HTML embebido para no depender de ningún archivo (estamos offline).
458 */
459function paginaOffline() {
460  var html =
461    '<!doctype html><html lang="es"><head><meta charset="utf-8">' +
462    '<meta name="viewport" content="width=device-width,initial-scale=1">' +
463    '<title>Sin conexión — CashFlow</title>' +
464    '<style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;' +
465    'background:#0F1420;color:#E2E8F0;font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:24px}' +
466    '.caja{max-width:340px}h1{font-size:1.35rem;margin:0 0 .5rem}p{color:#94A3B8;font-size:.9rem;line-height:1.55}' +
467    'button{margin-top:1.2rem;background:#A855F7;color:#fff;border:0;border-radius:10px;padding:.7rem 1.6rem;' +
468    'font-size:.95rem;font-weight:600;cursor:pointer}</style></head><body><div class="caja">' +
469    '<div style="font-size:2.4rem">📡</div>' +
470    '<h1>Sin conexión</h1>' +
471    '<p>Esta pantalla todavía no está guardada en tu dispositivo. Las pantallas que visitaste ' +
472    'o sincronizaste antes sí están disponibles offline.</p>' +
473    '<button onclick="location.reload()">Reintentar</button>' +
474    '</div></body></html>';
475  return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
476}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.