Files
manga-reader/app/public/sw.js
T
ninoandClaude Opus 5.5 d3e196a2f3 Librería con listas, recomendaciones, descarga anticipada, app instalable con lectura offline y seguridad para exponerlo a internet
- Historial pasa a Librería: pestañas Leyendo, Siguiendo, Terminados, En el dispositivo y listas propias (Favoritos, Pendientes, Abandonados por defecto)
- Recomendaciones en el inicio según tus géneros más leídos (populares de MangaDex)
- Descarga anticipada de los N capítulos siguientes (opcionalmente en HD) al pasar la mitad de uno
- PWA: manifest, iconos y service worker; capítulos guardados en el dispositivo para leer sin conexión, progreso offline sincronizado después
- Seguridad: baneo de IP tras 6 intentos fallidos (configurable), IP real detrás de proxies de confianza, CSP y cabeceras, cookie Secure/HSTS en HTTPS, login sin diferencias de tiempo, registro cerrado por defecto, contraseñas de 8+, protección SSRF en el proxy de imágenes, errores 500 genéricos
- Ajustes: sección Seguridad con IPs bloqueadas, intentos recientes y la IP vista por el servidor
- LeerCapitulo: reintentos ante caídas (522/5xx), copia anterior si sigue caído y mensaje claro

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 07:52:13 -06:00

49 lines
2.3 KiB
JavaScript

// Service worker: la app abre sin conexión (shell en caché) y los capítulos guardados "en este
// dispositivo" (caché OFFLINE, la llena app.js) se leen sin red.
const SHELL = 'shell-v1'
const OFFLINE = 'offline-v1'
const SHELL_FILES = ['/', '/app.js', '/style.css', '/icon.svg', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png']
self.addEventListener('install', e => {
e.waitUntil(caches.open(SHELL).then(c => c.addAll(SHELL_FILES)).then(() => self.skipWaiting()))
})
self.addEventListener('activate', e => {
e.waitUntil(caches.keys()
.then(keys => Promise.all(keys.filter(k => k !== SHELL && k !== OFFLINE).map(k => caches.delete(k))))
.then(() => self.clients.claim()))
})
// Red primero (así siempre se ve la última versión) y la caché si no hay conexión.
async function networkFirst(req, cacheName, { store = true, ignoreSearch = false, fallback } = {}) {
try {
const res = await fetch(req)
if (store && res.ok) {
const c = await caches.open(cacheName)
if (store === true || (await c.match(req, { ignoreSearch }))) c.put(req, res.clone())
}
return res
} catch (err) {
const hit = await caches.match(req, { ignoreSearch, cacheName }) || (fallback && await caches.match(fallback))
if (hit) return hit
throw err
}
}
self.addEventListener('fetch', e => {
const req = e.request
if (req.method !== 'GET') return
const url = new URL(req.url)
if (url.origin !== location.origin) return
const p = url.pathname
if (req.mode === 'navigate') return e.respondWith(networkFirst(req, SHELL, { fallback: '/' }))
if (SHELL_FILES.includes(p) || p === '/sw.js') return e.respondWith(networkFirst(req, SHELL))
if (p === '/api/me') return e.respondWith(networkFirst(req, SHELL))
// Ficha y capítulo: solo se guardan si el usuario los guardó (store: 'existing' = refrescar si ya estaban).
if (p.startsWith('/api/manga/')) return e.respondWith(networkFirst(req, OFFLINE, { store: 'existing', ignoreSearch: true }))
if (p.startsWith('/api/chapter/')) return e.respondWith(networkFirst(req, OFFLINE, { store: false, ignoreSearch: true }))
// Imágenes: si están guardadas, sin red (más rápido y sin gastar datos).
if (p.startsWith('/api/local/') || p.startsWith('/api/img') || p.startsWith('/api/local-cover/')) {
e.respondWith(caches.open(OFFLINE).then(c => c.match(req)).then(hit => hit || fetch(req)))
}
})