Commits
El primer día del CI funcionó: se puso en rojo por la noche
Montamos CI por la tarde y esa misma noche falló: fuentes de Google en el
Ayer por la tarde cerramos el bloque de seguridad y montamos un workflow de CI mínimo. La idea era simple: que ningún error de compilación volviera a llegar a main en silencio. Por la noche, el CI hizo exactamente su trabajo. Falló. Y tenía razón.
«CI failed - all jobs failed»
Las dos runs de main de las 23:39 y 23:44 estaban en rojo. El mensaje, a primera vista, no decía mucho:
TypeError: Cannot read properties of null (reading '1')
Y lo más incómodo: era intermitente. Las runs de las 21:07 habían pasado con el mismo layout. El problema era next/font/google: baja los ficheros woff2 de Google Fonts en tiempo de build. Si el runner llega a Google, pasa. Si no llega, muere con ese TypeError críptico.
Es una dependencia de red dentro del build. Coherente, por cierto, con lo que ya sabíamos del §1b de la spec: las fuentes tenían que ser locales.
Tres hallazgos encadenados
1. Fuentes locales. Los woff2 ahora viven en src/fonts/ y se cargan con next/font/local:
src/fonts/inter-latin-var.woff2
src/fonts/instrument-serif-latin-400.woff2
src/fonts/instrument-serif-latin-400-italic.woff2
Inter es variable: Google servía el mismo fichero para los cinco pesos, así que un solo fichero con weight: "100 900". Build determinista, sin red.
2. TS2783 en la ingesta. Cuando el build dejó de atascarse en las fuentes, aparecieron dos errores de tipos que llevaban ahí desde la víspera y nadie había visto. En ingesta/publicar, la respuesta hacía { ...yaExiste, id, ok: true }: el spread traía el id del documento destino y pisaba el de la revisión. Bug real de runtime —la respuesta identificaba mal la revisión— destapado porque el cache() de ayer afinó el tipo inferido de findTarget.
3. El campo era destinaris. En newsletter/enviar el campo se escribía como destinataris, y r.error iba sin fallback. El update de estadísticas habría fallado en runtime.
Verificación: npm ci && npm run build con las mismas variables del CI, primero reproduciendo los errores en este orden y después en verde.
La newsletter cortaba en 999
Revisando newsletter/enviar nos encontramos esto:
const lote = subs.docs.slice(0, 999);
// ponytail: només el primer lot (SendGrid admet 1000/req).
// Amb >999 confirmats caldrà paginar la crida — ampliació trivial.
Peor: se marcaba el boletín como enviado con destinaris: 999. Con 1.500 suscriptores, 501 personas no recibían nada y el panel no decía nada. Ahora pagina por lotes de 1.000 (el límite real de SendGrid), destinaris guarda el total real, si ningún lote sale devuelve 502 y no marca nada, y si fallan algunos devuelve 207 con el detalle.
La ingesta y el caso «ya publicado»
La auditoría decía que el reintento de publicación creaba duplicados. Era falso: noticies_slug_idx es UNIQUE en la base de datos y el reintento reventaba contra el índice. El problema real era otro: si el create iba bien y fallaba el update del estado, la fila quedaba pendiente para siempre, y cada reintento fallaba con un error opaco. Ahora, antes de convertir, se busca el documento por su slug natural; si existe, se cierra como publicada y se responde yaPublicado: true con targetCollection y targetId. Sin migración: la clave natural ya estaba ahí.
Lo que queda
Cerrados el 10, el 11 y el 22 al 24 de TASKS.md. El P1 de rendimiento queda cerrado casi todo. El P2 de accesibilidad también. Quedan los tests (27) y el P3 de diseño, y los listados siguen haciendo una query por visita: arreglarlo de verdad es un rediseño, y hemos preferido quitar el revalidate = 3600 muerto antes que fingir una caché que no existe.