Estrategia de retrieval diario — marvelousdb (Fly)

Contexto: marvelous ( superhero DB en Fly, app marvelousdb ) tiene errores diarios en el scraping de datos faltantes. Diagnóstico y plan del 2026-09-06.

DIAGNÓSTICO REAL (verificado 2026-09-06 con cron.log + sqlite en servidor)

18 “paso fallo” acumulados. La corrida de hoy (09:00 UTC, no el wake de 14:00 — el loop sleep-86400 deriva a través de stop/start, confirmado):

PasoResultadoEstado real
Browser Run imágenes0 cached, 0 sin imagen, 968 en índiceAGOTADO
Comic Vine DCoffset 436,600 / ~436,700, 0 insertados, 44,270 DC en BDAGOTADO (escaneo total hecho)
DeepL18 traducidos, quedan 39,825 (422/40k)ÚNICO backlog real — pero 39,825 × ~800 chars ≈ 32M chars; free tier 500k/mes → ~20/día ≈ 5 años. Inconvergente por diseño.
Wikipedia23/23 categorías “ya hecho”, 0 personajesAGOTADO
SerpAPI imágenes0 sin imagenAGOTADO

Conclusión: NO son errores de red ni de cupo pisado. Son 4 de 5 fuentes sin backlog que corren igual cada día y fallan al detectar “nada que hacer” + el paso que sí produce (DeepL) no puede converger con el cupo gratuito.

ESTRATEGIA CORREGIDA (reemplaza el plan original de esta sección hacia abajo)

1. Estados de fuente en el ledger (cron-state.json)

Cada paso: active | exhausted | failing. Reglas de transición:

  • Carrera completa con 0 items elegibles → exhausted (exit 0, se OMITE los días siguientes).
  • Sweep periódico (semanal/mensual) reactiva temporalmente para detectar additions (personajes nuevos en Comic Vine, imágenes recién reportadas desde el sitio).
  • Efecto inmediato: el log diario pasa de 5 pasos ruidosos a 1, errores a cero.

2. Retirar el bulk-scan de Comic Vine

El offset ya recorrió los ~436k. No borrar el progress, pero el paso se marca exhausted. El retrieval futuro de DC es quirúrgico: search-by-name por personaje reportado desde el sitio (misma API, 1 request por caso), no escaneo masivo.

3. DeepL: resolver el backlog con Ollama local (local-first)

El servidor deja de traducir. Script nuevo data/translate-ollama.js corre en el System76 (modelo tipo gemma/nllm para ES→EN→DE de intros de lore), produce el mismo formato de lore-translations.json, se sube al volumen vía fly ssh sftp put o endpoint interno. 32M chars en local = costo cero, sin cupo, convergible. El worker del servidor queda SOLO para sweeps de fuentes freshly-agotadas.

4. Scheduling e higiene (vigente del plan original)

  • Quitar while true; sleep 86400 del entrypoint; correr una vez por boot con gate de fecha UTC en el ledger.
  • Exit codes: 0 ok · 3 sin-cupo (esperado) · 1 error real.
  • Eliminar ruido --env-file-if-exists=.env en el contenedor (allá todo es secrets).

Orden de implementación

  1. Ledger + estados + gate HECHO 2026-09-07: data/cron-imports.js reescrito (STEPS con empty(out), gate por fecha UTC, sweep a los 7d, --force/--sweep/--dry-run) y entrypoint.sh sin loop. Probado: 13/13 casos de regex contra cron.log real + escenarios B–E de gate/sweep/fallo.
  2. translate-ollama.js local + sftp push → prompt listo en ~/nef/decisions/[CREDENCIAL].md.
  3. needs_work/backoff por personaje (tabla sqlite) → solo si el sitio empieza a reportar casos imposibles; no construir antes de tiempo.

Falta: fly deploy para activar (o esperar el siguiente merge + deploy normal).


Addendum 2026-09-07 (tarde) — higiene de stubs + retrieval de un comando

IMPLEMENTADO y desplegado-en-volumen:

  • data/lib/populated.js: regla única → personaje “visible” si bio ≥ 40 chars O imagen real (image_not_available y blank.png de Comic Vine NO cuentan).
  • Columna characters.populated: la calculan build-sqlite, comicvine y wikipedia al insertar; data/refresh-populated.js = migración idempotente + recomputo (incluye imágenes del cache local) como paso [0] del batch diario.
  • api.js: /api/characters (browse/search/random), picker de nombres y random de /api/battle filtran populated = 1; blank.png rinde no-image.svg. Los stubs siguen accesibles por id directo y visibles en coverage — no se borran nada.
  • Comic Vine ya no inserta stubs (skip si ni bio ni imagen real).
  • Backfill ejecutado en servidor: 54,201 visibles / 1,714 ocultos. Jack Irons ([ID-TEL]) confirmado populated=0.
  • Un comando: npm run retrieve (batch local), retrieve:sweep, retrieve:server (POST /api/admin/retrieve con Bearer RETRIEVE_TOKEN — corre dentro de la máquina con sus secrets y el ledger; ignora gate), y retrieve:status (ledger + tail del cron.log por HTTP). Pendiente: fly secrets set RETRIEVE_TOKEN=<openssl rand -hex 16> y reflejarlo en .env.

CRÍTICO: sin fly deploy, el contenedor viejo (loop sleep-86400 + código anterior) volverá a correr ruidoso ~al despertar de mañana 14:00 UTC. El deploy debe ocurrir antes, o la máquina quedará una última jornada ruidosa.

Incidente post-deploy 2026-09-06 ~17:30 (resuelto)

El boot del código nuevo reveló un bug LATENTE: CACHE_DIR de cache-images.js resolvía por __dirname/../public/img/cache (no migrado al volumen en df3dd85f) y la imagen hornea ese directorio REAL con archivos viejos → el guard if [ ! -e ] del entrypoint nunca enlazaba el symlink → los archivos cacheados escribían en storage efímero (perdidos en cada deploy) y el prune de boot barrió 909/968 entradas del índice al no ver sus archivos.

  • Reparación en caliente: repair-cache.js (rescató 121 archivos del dir público, reconstruyó el índice = 1,369 entradas desde los 1,380 archivos del volumen) + symlink forzado. Ningún archivo se perdió.
  • Código: CACHE_DIR = dirname(DB_PATH)/img-cache + entrypoint reemplaza directorios stale por el symlink ([ ! -L ]) + web.js now forwards the Authorization header (el trigger externo devolvía 401 tras el proxy).
  • Commits de la jornada: d785831c, 4d75231d (ledger/gate), fc870b77 (higiene + trigger), 111b8e1b (fix volumen/proxy). Desplegado y verificado: batch de arranque silencioso (18 DeepL, todo lo demás EXHAUSTED), imágenes 200 desde el volumen, stubs fuera de búsqueda.

Addendum 2026-09-07 — fuentes para el gap “personajes vacíos”

Síntoma: /character/[ID-TEL] (Jack Irons) vacío. Investigado en vivo:

  • DC sin bio: 7,469 / 44,270. El bulk-search de Comic Vine guarda documentos finos; probé el endpoint de detalle (/character/4005-XXXXXX/?field=site_detail) con Jack Irons + 8 muestras: site_detail = 0 chars en todos. Son stubs de Comic Vine (0 aparcciones reales; el filtro min-appearances no los atrapó).
  • Conclusión: para la mayoría el dato NO existe en Comic Vine — buscar más en la misma fuente es tirar cupo.

Fuentes evaluadas (verificadas 2026-09-07)

FuenteEstadoVeredicto
dc.fandom.com API MediaWiki (/api.php, sin llave)PROBADA: search responde, miles de páginas de personajes DC con bio real, incluso menoresMejor opción para las 7.5k sin bio; reusar el patrón fandomPass de cache-images.js, ahora para lore
Comic Vine bulk publisher:Marvel ComicsMismo pipeline probado del scan DCCorrige desbalance: 774 Marvel vs 44,270 DC en BD. Budget de una semana, roster nuevo sin fuentes nuevas
Wikipedia Category:DC Comics characters180 páginas directas + 32 subcategoríasSolo famosos; pipeline import-wikipedia ya sirve para esto. Marginal
League of Comic Geekssin API oficialDescartado (scraping frágil/ToS)
Grand Comics Databasetiene apariciones, no biosNo ataca este gap

Prioridad recomendada

  1. Higiene primero (costo 0): no indexar en FTS ni ofrecer en VS-random a personajes sin bio Y sin imagen real (descartar URLs blank.png de CV como imagen). Eso elimina el síntoma de página vacía ya.
  2. fandomPass para DC lore (fuente nueva, gratis, ilimitada-ish).
  3. Scan Marvel en Comic Vine si se quiere roster Marvel grande.

Nota operativa: fly ssh console NO hereda secrets; para pruebas en vivo, root + env del proceso de /proc. fly sftp put <local> <remoto> (orden inverso a get) y no sobreescribe: rm remoto primero.

Arquitectura actual (relevante)

  • entrypoint.sh: al boot → sleep 60 → cron-imports.js → loop sleep 86400.
  • .github/workflows/wake.yaml (14:00 UTC) / sleep.yaml (05:00 UTC): la máquina vives ~15h diarias; el wake es de facto el disparador del batch.
  • data/cron-imports.js: corre 5 pasos secuenciales con presupuesto fijo de 90% del cupo por corrida (Browser Run, Comic Vine, DeepL, Wikipedia, SerpAPI).
  • Importadores ya son resumables: comicvine-progress.json, wikipedia-progress.json, lore-translations.json, img-cache.json + missing-images.json.

Causa raíz de los “errores diarios”

  1. Redeploy/restart intra-día → el batch corre 2+ veces el mismo día → la segunda corrida se come contra 429s reales (cupo ya gastado).
  2. Los importadores salen con exit 1 cuando agotan cupo normalmente (import-comicvine.js:123, cache-images.js:390 “commons 429”, DeepL 429). Un “terminé por hoy” se registra como fallo.
  3. El sleep 86400 dentro del contenedor deriva con cada wake y muere con el stop — nunca dispara una segunda vez; solo genera falsas expectativas de scheduling.

Estrategia (orden de implementación)

1. Ledger de cupo por día UTC — data/cron-state.json

{ "runs": { "2026-09-06": { "comicvine": 180, "deeplChars": 14400, "browserRunSec": 540, "serpapi": 7, "completed": true } } }
  • cron-imports.js lee el día UTC, calcula restante = presupuesto_diario - usado, y pasa --limit restante a cada paso. Si restante ⇐ 0, paso se omite (log claro).
  • Cada importador reporta cuánto consumió (o el wrapper lo estima por páginas/límites).
  • Efecto: N corridas el mismo día siguen gastando 90% total, nunca más.

2. Convenio de exit codes

  • 0 = completado · 3 = “sin cupo / continúa mañana” (esperado) · 1 = error real.
  • En cron-imports.js, exit 3 → línea informativa, no ! paso fallo.

3. Scheduler = wake diario, no sleep interno

  • Borrar el while true; sleep 86400 del entrypoint.
  • El batch corre en boot, una sola vez por día UTC (gate por cron-state.json:runs[fecha].completed).
  • El wake de las 14:00 UTC es el cron. Los redeploys ya no causan daño (gate + ledger).

4. Cola de reintentos por personaje (convergencia)

  • Tabla sqlite needs_work(character_id, field, source, attempts, last_error, next_retry_at).
  • Poblada por el coverage check actual (los missing* queries).
  • El batch consume solo filas next_retry_at <= now(), ordenadas por attempts asc.
  • Falla → attempts++, next_retry_at = now + backoff (1d, 3d, 7d); attempts > 4 → status='hopeless' y aparece en el dashboard de coverage para atención manual.
  • Efecto: casos imposibles dejan de quemar cupo diario; el retrieval converge.

Qué tocar

  • entrypoint.sh — quitar loop, correr una vez con gate.
  • data/cron-imports.js — ledger + exit-code mapping + passing restantes.
  • data/import-comicvine.js, import-deepl.js, cache-images.js, import-wikipedia.js — respetar --limit restante y exit 3 al agotar cupo.
  • data/build-sqlite.js / nuevo migration — tabla needs_work.

Verificación

  • node data/cron-imports.js --dry-run dos veces el mismo día: la segunda debe mostrar “restante = 0” y no spawning.
  • Simular restart: fly restart intra-día → log sin 429s nuevos.
  • Regresar tail -n 50 /app/data/cron.log vía fly ssh console para confirmar la causa raíz antes de implementar (requiere fly auth login local, que no estaba disponible en la sesión del diagnóstico).