Diagnóstico y fix: Surtidito envió reportes viejos (18 sep 2026)

Fecha del incidente: 18 de septiembre de 2026, 08:51 UTC
Reporte enviado: Edición #13 del 16 de septiembre (2 días viejo)
Diagnóstico: 2026-09-22
Máquina: Atlantic (System76, local-first)


Qué pasó

El sistema de newsletter Surtidito envió por correo la edición del 16 de septiembre el día 18 de septiembre (2 días después), cuando debería haber enviado contenido del 18 de septiembre.


Causa raíz

Pipeline del boletín (flujo normal)

03:00 UTC → cron local: prensa_noche.py
              ↓ genera candidatas/prensa-YYYY-MM-DD.json
              
03:00 UTC → cron Vercel: /api/proponer
              ↓ lee candidatas + feed
              ↓ genera terna (3 propuestas LLM)
              ↓ guarda en Supabase
              ↓ envía email al editor

 [manual] → editor aprueba 1 de las 3 propuestas
              ↓ marca en Supabase: {"estado": "aprobada", "aprobada": N}

08:00 UTC → cron Vercel: /api/cron
              ↓ resuelve qué edición enviar:
              ├─ propuesta aprobada? → envía esa
              ├─ propuesta sin aprobar + hora >= 07:00? → envía la mejor de la terna
              └─ SIN propuesta del día? → **FALLBACK: último JSON en reporte/**

Falla específica (17-18 septiembre)

17 de septiembre, 03:00 UTC:

$ tail /tmp/prensa-noche.log
  x Página 12/portada: feed ilegible (<urlopen error [Errno 8] nodename nor servname provided, or not known>)
  x Página 12/el-pais: feed ilegible (<urlopen error [Errno 8] nodename nor servname provided, or not known>)
  ...
  x Rompeviento TV/youtube: feed ilegible (<urlopen error [Errno 8] nodename nor servname provided, or not known>)
LISTO: candidatas/prensa-2026-09-17.json (0 notas, 10 fallas)

18 de septiembre, 03:00 UTC: Mismo error (DNS caído)

18 de septiembre, 08:00 UTC:

  • /api/cron no encontró propuesta del día
  • Ejecutó fallback → buscó último reporte/????-??-??.json
  • Encontró reporte/2026-09-16.json (2 días viejo)
  • Lo envió a todos los suscriptores

Error de DNS: Temporal. La máquina Atlantic perdió conectividad o DNS durante la ventana de las 03:00 UTC ambos días.


Fix aplicado

1. Alerta crítica en prensa_noche.py

Archivo: scripts/prensa_noche.py
Cambio: Si TODAS las fuentes fallan (0 notas), envía email de alerta al editor.

def _enviar_alerta(asunto, mensaje):
    """Envía alerta por correo si RESEND_API_KEY está configurado."""
    ...
 
def main():
    ...
    if len(notas) == 0 and len(fallas) > 0:
        asunto = f"[SURTIDO CRÍTICO] prensa_noche.py falló completamente ({hoy})"
        mensaje = (...)
        _enviar_alerta(asunto, mensaje)

Beneficio: Notificación inmediata cuando el cron falla por DNS/conectividad.


2. Failsafe en _resolver_edicion() (Vercel)

Archivo: asgi.py
Cambio: NO enviar reportes viejos (>2 días). Si la última edición tiene más de 2 días de antigüedad, detener el envío y reportar el motivo.

def _resolver_edicion():
    ...
    fecha_edicion = datetime.fromisoformat(e.get("fecha", "1970-01-01")).date()
    dias_antiguedad = (hoy - fecha_edicion).days
    if dias_antiguedad > 2:
        return None, (f"última edición demasiado vieja ({e.get('fecha')}, hace {dias_antiguedad} días). "
                     "Revisar pipeline: prensa_noche.py + proponer.py")
    ...

Beneficio: Previene envío de contenido desactualizado. El sistema falla visiblemente (no envía nada) en lugar de silenciosamente (envía contenido viejo).


Verificación

Test del fix (alerta)

$ cd ~//nef/surtido
$ .venv/bin/python scripts/prensa_noche.py --out /tmp/test-prensa
# → Si falla completamente, envía email a EDITOR_EMAIL

Test del fix (failsafe)

# Simular edición vieja (>2 días)
$ rm reporte/2026-09-2*.json  # eliminar ediciones recientes
$ curl -H "Authorization: Bearer $CRON_SECRET" https://www.surtido.xyz/api/cron
# → Debe retornar: {"envio": "última edición demasiado vieja (2026-09-16, hace N días)..."}

Recomendaciones adicionales

1. Monitoreo de crons

Configurar healthcheck externo que verifique:

  • candidatas/prensa-YYYY-MM-DD.json existe y tiene notas.length > 0 (cada día a las 03:30 UTC)
  • propuesta_edicion en Supabase se actualiza diariamente (cada día a las 03:30 UTC)

2. DNS local confiable

Opciones para evitar fallos de DNS:

  • Systemd-resolved (si usas systemd): configurar fallback a [IP] / [IP]
  • Wireguard/Tailscale: Si Atlantic está en una red local, configurar VPN para resolver DNS externo
  • Cron retry: Modificar cron para reintentar en caso de fallo:
    0 3 * * * cd ~//surtido && .venv/bin/python scripts/prensa_noche.py >> /tmp/prensa.log 2>&1 || (sleep 300 && .venv/bin/python scripts/prensa_noche.py >> /tmp/prensa.log 2>&1)

3. RSSHub local

El log muestra que RSSHub (localhost:1200) no está corriendo. Considerar:

  • Levantar RSSHub en Docker/systemd para feeds de X (Twitter)
  • O eliminar dependencia de RSSHub si no es crítico (el sistema ya usa RSS oficial de YouTube)

Estado actual (22 sep 2026)

  • Fix aplicado: ✅ Ambos cambios committeados
  • Cron configurado: ✅ prensa_noche.py a las 03:00 UTC (Atlantic)
  • DNS funcional: ✅ (verificado manualmente el 22 sep)
  • Última edición generada: 16 de septiembre (pendiente generar 17-22 sept si se requiere contenido retroactivo)

Comandos útiles

# Ver log del cron nocturno
$ tail -50 /tmp/prensa-noche.log
 
# Generar candidatas manualmente (hoy)
$ .venv/bin/python scripts/prensa_noche.py
 
# Generar feed + propuesta manual
$ .venv/bin/python proponer.py --solo
 
# Ver últimos envíos de Resend
$ .venv/bin/python mailing.py list --limit 10
 
# Ver estado de la propuesta en Supabase
$ .venv/bin/python -c "from supabase import leer_artefacto; import json; print(json.dumps(leer_artefacto('propuesta_edicion'), indent=2))"

Autor: Claude (OpenCode)
Sesión: 2026-09-22
Perfil: Nef (bias: scope down, ship smallest real version, instrument before migrating)