Equipo — The Good App

Mail 12 — Continuación del Mail 11 (noticias sindicadas automáticas)

Hola equipo,

Este correo continúa la línea del Mail 11 (plan-frontend/maileleven.html). En este ciclo implementamos la importación diaria de noticias positivas desde portales de prensa de Estados Unidos: texto e imagen principal, con enlace y atribución al origen, visibles en la app como cualquier noticia publicada por usuarios.

Resumen de cambios (API + Admin + Flutter)

  • API (good-app): modelo NewsPortal; campos en News (isSyndicated, sourceUrl, sourceAttribution, etc.); módulo syndication con RSS (rss-parser), scraping por portal (cheerio + JSON de selectores) e imágenes recortadas en servidor (sharp: cabecera 534×300, tarjeta 374×374, mismas medidas que la app sin CDN).
  • Endpoints: POST /api/v1/syndication/run-if-needed (JWT, dispara importación si hoy aún no corrió) y GET /api/v1/syndication/status.
  • Admin (admin-goodapp): menú Portales de noticias (CRUD + JSON de scraping editable); menú Noticias importadas (editar / eliminar / crear manual); pestaña Sindicación en Configuración (activar, permitir sin imagen, ver última ejecución, resetear bandera diaria).
  • Flutter (flutter-good): al entrar en la app (shell), llamada en segundo plano a run-if-needed; en detalle de noticia, bloque Leer en la fuente con URL externa si la noticia es importada.
  • Seed / migración: 4 portales US por defecto (Good News Network, Sunny Skyz, InspireMore, The Optimist Daily), categoría positive-news, usuario sistema [email protected].

Cómo funciona la importación diaria

  • Disparo: el primer usuario que abre la app en el día (con sesión válida) ejecuta la importación una sola vez; los siguientes reciben ran: false sin volver a jalar.
  • Límite: hasta 10 noticias por portal habilitado por ejecución (maxItemsPerRun, editable en admin por portal).
  • Bandera diaria: clave news_syndication_last_run_date en Setting (fecha UTC YYYY-MM-DD). En admin: botón para borrarla y forzar un nuevo pull el mismo día.
  • Concurrencia: lock transaccional (SELECT … FOR UPDATE) para que varios usuarios entrando a la vez no dupliquen el trabajo.
  • Deduplicación: no se reimporta la misma sourceUrl ni el mismo guid RSS por portal.
  • Tono positivo: portales curados + lista blockedTitleKeywords en el JSON de cada portal (p. ej. murder, shooting).

Configuración de scraping por portal (admin)

Cada portal guarda un JSON en scrapeConfig (editable en el panel). Esquema unificado:

  • mode: rss_and_scrape (recomendado) o rss_only.
  • article.contentSelector, excerptSelector, imageSelector (+ atributos para meta og:*) — equivalente a indicar en admin dónde está el cuerpo, el resumen y la imagen en el HTML del artículo.
  • filters.blockedTitleKeywords: palabras que descartan el ítem si aparecen en título o excerpt.

Los 4 portales precargados ya traen un JSON de ejemplo; se puede ajustar RSS URL y selectores sin tocar código.

Experiencia en la app

  • Las noticias importadas aparecen en Home / Near como published, con las mismas imágenes base64 (cabecera + tarjeta).
  • Al abrir el detalle, el usuario ve el texto completo importado y, al final, el enlace Leer en {fuente} que abre el artículo original en el portal.
  • Campos API nuevos en el JSON de noticia: isSyndicated, sourceUrl, sourceAttribution.

Despliegue en staging / producción

  1. API: npx prisma migrate deploy (migración 20260515120000_news_syndication) y npm run prisma:seed (portales + settings + usuario sistema).
  2. Admin: php artisan migrate (tablas/columnas espejo si aplica).
  3. Probar: en admin → Configuración → Sindicación → Resetear bandera diaria; abrir la app con un usuario logueado; revisar noticias en feed y menú Noticias importadas.
  4. Revisar stats: tras la corrida, news_syndication_last_run_stats guarda importados, omitidos y errores por portal.

Archivo: good-app/plan-frontend/mailtwelve.html. Enlace en plan-frontend/index.html.

Saludos,

Correo interno — avance técnico (Mail 12)