1/** 2 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3 * CashFlow â cache.js: CAPA DE ACCESO de la caché offline (el "DTO") 4 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5 * Se carga en TODAS las páginas (via partials/script.hbs), después de pwa.js 6 * (que es quien registra el service worker). 7 * 8 * Traduce entre "la app" y "el almacén" (el service worker /sw.js): 9 * - sincronizar(): pide al SW purgar los datos viejos y re-descarga (precarga) 10 * las pantallas principales para que queden disponibles offline. Es el 11 * punto único de invalidación: lo nuevo PISA lo viejo. 12 * - Auto-sync: al cargar cualquier página, si nunca se sincronizó o pasaron 13 * más de 24 h, dispara una sincronización silenciosa (misma regla que la 14 * cadena de syncs de la app de referencia: forzado || 24h || vacÃo). 15 * - Estado de conexión: mantiene la clase `cf-offline` en <body> y actualiza 16 * el botón "Sincronizar" del aside (deshabilitado sin red). 17 * 18 * La capa de UI (el botón del aside) NUNCA habla con el SW directo: siempre 19 * pasa por window.CashFlowCache. Si mañana cambia el almacén (p. ej. a 20 * IndexedDB), se toca solo esta capa y sw.js. 21 * 22 * API pública: window.CashFlowCache = { sincronizar, ultimaSync, esOffline } 23 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 24 */ 25(function () { 26 'use strict'; 27 28 // ââ Configuración ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 29 var CLAVE_ULTIMA_SYNC = 'cf_ultima_sync'; // timestamp (ms) en localStorage 30 var INTERVALO_AUTO_MS = 24 * 60 * 60 * 1000; // auto-sync: cada 24 h 31 32 // Pantallas que la sincronización deja precargadas para uso offline. 33 // Son las del aside + ayuda; el resto se va cacheando "on demand" al visitarla 34 // (y los PDFs de facturación, al abrirlos por primera vez). 35 var PAGINAS_PRECARGA = [ 36 '/auth/index', 37 '/auth/pages-resumen', 38 '/auth/pages-operation', 39 '/auth/pages-services', 40 '/auth/pages-agenda', 41 '/auth/pages-saving', 42 '/auth/pages-facturacion', 43 '/auth/pages-credit', 44 '/auth/pages-debts', 45 '/auth/pages-inversiones', 46 '/auth/pages-situacion-crediticia', 47 '/auth/pages-tienda', 48 '/auth/pages-n8n', 49 '/auth/pages-help' 50 ]; 51 52 var sincronizando = false; // candado anti doble-click 53 54 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 55 Comunicación con el service worker 56 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 57 58 /** 59 * Pide al SW que purgue los caches de datos (páginas + PDFs) y espera su "ok". 60 * Usa un MessageChannel para que la respuesta llegue a ESTA pestaña. 61 * Si el SW no contesta en 3 s (o no hay SW), resolvemos igual: la precarga 62 * posterior usa cache:'reload', asà que lo viejo se pisa de todas formas. 63 */ 64 function purgarEnSW() { 65 return new Promise(function (resolver) { 66 var sw = navigator.serviceWorker && navigator.serviceWorker.controller; 67 if (!sw) return resolver(false); 68 69 var canal = new MessageChannel(); 70 var timeout = setTimeout(function () { resolver(false); }, 3000); 71 canal.port1.onmessage = function () { 72 clearTimeout(timeout); 73 resolver(true); 74 }; 75 sw.postMessage({ tipo: 'SINCRONIZAR' }, [canal.port2]); 76 }); 77 } 78 79 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 80 Sincronización 81 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 82 83 /** 84 * Sincroniza la caché offline: purga los datos viejos y re-descarga las 85 * pantallas principales (el SW las va guardando al pasar). 86 * @param {Object} [opts] 87 * @param {boolean} [opts.silenciosa] true en el auto-sync: sin animación de botón. 88 * @returns {Promise<{ok:number, total:number}|null>} null si no se pudo arrancar. 89 */ 90 function sincronizar(opts) { 91 opts = opts || {}; 92 if (sincronizando) return Promise.resolve(null); 93 if (!navigator.onLine) { 94 if (!opts.silenciosa) marcarBoton('offline'); 95 return Promise.resolve(null); 96 } 97 98 sincronizando = true; 99 if (!opts.silenciosa) marcarBoton('girando'); 100 101 return purgarEnSW() 102 .then(function () { 103 // Re-descarga secuencial (no saturamos el server con 11 requests a la vez). 104 // cache:'reload' salta la caché HTTP del navegador â baja SIEMPRE del API, 105 // y el SW guarda cada respuesta fresca (esto es el "pisar lo viejo"). 106 var ok = 0; 107 var cadena = Promise.resolve(); 108 PAGINAS_PRECARGA.forEach(function (ruta) { 109 cadena = cadena.then(function () { 110 return fetch(ruta, { credentials: 'same-origin', cache: 'reload' }) 111 .then(function (r) { if (r.ok) ok++; }) 112 .catch(function () { /* una página caÃda no corta la cadena */ }); 113 }); 114 }); 115 return cadena.then(function () { return ok; }); 116 }) 117 .then(function (ok) { 118 // Registrar el momento de la sync para el "hace X" del botón y el auto-sync. 119 try { localStorage.setItem(CLAVE_ULTIMA_SYNC, String(Date.now())); } catch (e) { } 120 sincronizando = false;
121 if (!opts.silenciosa) marcarBoton('listo'); 122 refrescarMeta(); 123 // Evento para quien quiera enterarse (futuras pantallas, toasts, etc.). 124 try { 125 document.dispatchEvent(new CustomEvent('cf:sync', { detail: { ok: ok, total: PAGINAS_PRECARGA.length } })); 126 } catch (e) { } 127 return { ok: ok, total: PAGINAS_PRECARGA.length }; 128 }) 129 .catch(function () { 130 sincronizando = false; 131 if (!opts.silenciosa) marcarBoton('error'); 132 return null; 133 }); 134 } 135 136 /** Timestamp (ms) de la última sincronización, o null si nunca corrió. */ 137 function ultimaSync() { 138 try { 139 var v = parseInt(localStorage.getItem(CLAVE_ULTIMA_SYNC), 10); 140 return isNaN(v) ? null : v; 141 } catch (e) { return null; } 142 } 143 144 /** "hace 5 min", "hace 3 h", "hace 2 d" â para el pie del botón. */ 145 function humanizarDesde(ts) { 146 if (!ts) return 'nunca'; 147 var seg = Math.max(0, Math.floor((Date.now() - ts) / 1000)); 148 if (seg < 60) return 'recién'; 149 var min = Math.floor(seg / 60); 150 if (min < 60) return 'hace ' + min + ' min'; 151 var hs = Math.floor(min / 60); 152 if (hs < 24) return 'hace ' + hs + ' h'; 153 return 'hace ' + Math.floor(hs / 24) + ' d'; 154 } 155 156 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 157 UI â botón [data-cf-sync] del aside y estado de conexión 158 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 159 160 function boton() { return document.querySelector('[data-cf-sync]'); } 161 162 /** Actualiza el texto chiquito "Ãltima: hace X" del botón. */ 163 function refrescarMeta() { 164 var meta = document.querySelector('[data-cf-sync-meta]'); 165 if (meta) meta.textContent = navigator.onLine 166 ? 'Ãltima: ' + humanizarDesde(ultimaSync()) 167 : 'Sin conexión â modo offline'; 168 } 169 170 /** 171 * Estados visuales del botón: 'girando' (sincronizando), 'listo' (éxito, 172 * check verde 2 s), 'error' / 'offline' (aviso breve). Todo por clases CSS 173 * definidas en aside.hbs. 174 */ 175 function marcarBoton(estado) { 176 var b = boton(); 177 if (!b) return; 178 var icono = b.querySelector('i'); 179 b.classList.remove('cf-sync-girando', 'cf-sync-listo', 'cf-sync-error'); 180 if (icono) icono.className = 'bi bi-arrow-repeat'; 181 182 if (estado === 'girando') { 183 b.classList.add('cf-sync-girando'); 184 b.disabled = true; 185 } else if (estado === 'listo') { 186 b.disabled = false; 187 b.classList.add('cf-sync-listo'); 188 if (icono) icono.className = 'bi bi-check-circle'; 189 setTimeout(function () { marcarBoton('normal'); }, 2000); 190 } else if (estado === 'error' || estado === 'offline') { 191 b.disabled = false; 192 b.classList.add('cf-sync-error'); 193 if (icono) icono.className = 'bi bi-wifi-off'; 194 setTimeout(function () { marcarBoton('normal'); }, 2500); 195 } else { 196 // 'normal' 197 b.disabled = !navigator.onLine; 198 } 199 refrescarMeta(); 200 } 201 202 /** Refleja el estado de red en <body> (clase cf-offline) y en el botón. */ 203 function actualizarEstadoRed() { 204 document.body && document.body.classList.toggle('cf-offline', !navigator.onLine); 205 marcarBoton('normal'); 206 } 207 208 window.addEventListener('online', actualizarEstadoRed); 209 window.addEventListener('offline', actualizarEstadoRed); 210 211 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 212 Arranque 213 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 214 215 document.addEventListener('DOMContentLoaded', function () { 216 actualizarEstadoRed(); 217 218 // Click en el botón Sincronizar del aside. 219 var b = boton(); 220 if (b) b.addEventListener('click', function () { sincronizar(); }); 221 222 // El botón solo existe en páginas logueadas (el aside): usamos su presencia 223 // como señal de sesión activa para el auto-sync. 224 if (!b || !navigator.onLine) return; 225 226 // Auto-sync: nunca sincronizado (caché "vacÃa") o pasaron más de 24 h. 227 var ts = ultimaSync(); 228 if (!ts || (Date.now() - ts) > INTERVALO_AUTO_MS) { 229 // Silenciosa, demorada para no competir con la carga de la página y con 230 // JITTER aleatorio (4-30 s): si cientos de usuarios abren la app a la 231 // misma hora (o tras un deploy), sus precargas de ~11 páginas no le
232 // pegan al server todas en el mismo segundo sino repartidas. 233 var espera = 4000 + Math.floor(Math.random() * 26000); 234 setTimeout(function () { sincronizar({ silenciosa: true }); }, espera); 235 } 236 }); 237 238 // ââ API pública âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 239 window.CashFlowCache = { 240 sincronizar: sincronizar, 241 ultimaSync: ultimaSync, 242 esOffline: function () { return !navigator.onLine; } 243 }; 244})();
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.