Commits

El primer dia del CI va anar bé: es va posar vermell a la nit

Vam muntar CI per la tarda i aquella mateixa nit va fallar: fonts de Google

Ahir a la tarda vam tancar el bloc de seguretat i vam muntar un workflow de CI mínim. La idea era simple: que cap error de compilació tornés a arribar a main en silenci. A la nit, el CI va fer exactament el seu treball. Va fallar. I tenia raó.

«CI failed - all jobs failed»

Les dues runs de main de les 23:39 i 23:44 estaven en vermell. El missatge, de primeres, no deia gaire:

TypeError: Cannot read properties of null (reading '1')

I el més incòmode: era intermitent. Les runs de les 21:07 havien passat amb el mateix layout. El problema era next/font/google: baixa els fitxers woff2 de Google Fonts en temps de build. Si el runner arriba a Google, passa. Si no arriba, mort amb aquest TypeError críptic.

És una dependència de xarxa dins del build. Coherent, per cert, amb el que ja sabíem de l'§1b de la spec: les fonts havien de ser locals.

Tres troballes encadenades

1. Fonts locals. Els woff2 ara viuen a src/fonts/ i es carreguen amb 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 és variable: Google servia el mateix fitxer per als cinc pesos, així que un sol fitxer amb weight: "100 900". Build determinista, sense xarxa.

2. TS2783 a la ingesta. Quan el build va deixar d'encallar a les fonts, van aparèixer dos errors de tipus que hi eren des de la vigília i ningú havia vist. A ingesta/publicar, la resposta feia { ...yaExiste, id, ok: true }: el spread portava l'id del document destí i pisava el de la revisió. Bug real de runtime —la resposta identificava malament la revisió— destapat perquè el cache() d'ahir va afinar el tipus inferit de findTarget.

3. El camp era destinaris. A newsletter/enviar el camp s'escribia com a destinataris, i r.error anava sense fallback. El update d'estadístiques hauria fallat en runtime.

Verificació: npm ci && npm run build amb les mateixes variables del CI, primer reproduint els errors en aquest ordre i després en verd.

La newsletter tallava a 999

Revisant newsletter/enviar vam trobar això:

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.

Pitjor: es marcava el butlletí com a enviat amb destinaris: 999. Amb 1.500 subscriptors, 501 persones no rebien res i el panel no deia res. Ara pagina per lots de 1.000 (el límit real de SendGrid), destinaris guarda el total real, si cap lot surt retorna 502 i no marca res, i si en falten alguns retorna 207 amb el detall.

La ingesta i el cas «ja publicat»

L'auditoria deia que el reintent de publicació creava duplicats. Era fals: noticies_slug_idx és UNIQUE a la base de dades i el reintent reventava contra l'índex. El problema real era un altre: si el create anava bé i fallava l'update de l'estat, la fila quedava pendent per sempre, i cada reintento fallava amb un error opac. Ara, abans de convertir, es busca el document pel seu slug natural; si existeix, es tanca com a publicada i es respon yaPublicado: true amb targetCollection i targetId. Sense migració: la clau natural ja hi era.

El que queda

Tancats el 10, l'11 i el 22 al 24 de TASKS.md. El P1 de rendiment queda tancat gairebé tot. El P2 d'accessibilitat també. Queden els tests (27) i el P3 del disseny, i els llistats encara fan una query per visita: arreglar-ho de veritat és un redisseny, i hem preferit treure el revalidate = 3600 mort abans que fingir una caché que no existeix.