PageSourceSearch

https://cortex-cashflow.onrender.com/assets/js/cache.js

js cortex-cashflow.onrender.com collected 2026-10-03 11:40:19 UTC 11,936 bytes, 244 lines download raw bytes

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.