Commits

La publicidad se importa: Advanced Ads hacia Payload

Lunes corto y resuelto: el importador de Advanced Ads lleva zonas, campañas

El lunes corto

Tres commits, todos de migración de publicidad (F6c): el importador de Advanced Ads (la publicidad del WordPress viejo) ya lleva las campañas hacia Payload, con el contacto del anunciante incluido cuando existe. El camino hasta aquí: el primer esbozo del migrador y la conexión con el WordPress de producción.

El contacto que no siempre está

El campo de contacto del anunciante es opcional: si el WordPress no lo tiene, la campaña se crea igual. Si fuera obligatorio, cada campaña sin contacto habría fallado.

pymysql no da texto

La corrección que cortaba la creación de campañas: iso() esperaba texto ('2027-01-29 23:01:00'), pero pymysql (la librería que habla con MySQL, la base de datos) devuelve datetime (el tipo fecha-hora) al leer post_date. Un replace sobre un datetime no existe, y la importación reventaba. Ahora acepta las dos formas, y las comprobaciones propias tienen el assert (una verificación que falla si no se cumple):

assert iso(_dt(2027, 1, 29, 23, 1)) == "2027-01-29T23:01:00"   # pymysql
assert iso("2027-01-29 23:01:00") == "2027-01-29T23:01:00"     # text

Comprobarlo contra el Payload en vivo

Y para cerrar, el script de verificación post-import: lee las campañas y banners reales del Payload y los enfrenta con el origen WordPress. Migrar sin comprobar es migrar con los dedos cruzados.

Qué queda

F6c, cerrado. La publicidad del WordPress viejo ya tiene camino hacia Payload: zonas, campañas, banners y contacto de anunciante.

La noche: suscripciones, boletines y el salto a producción

El lunes no se acababa con la publicidad. Entre las 19h y las 23h cayó la fase 7 entera y la primera piedra de la 8.

Doble opt-in, no negociable

El formulario de suscripción ya no es un stub. Ahora hay una colección de suscriptores con estado (pendent, confirmat, baixa), un token único que solo puede leer el admin y la IP truncada por GDPR: IPv4 a /24, IPv6 a /64. El POST /api/subscriure mantiene el contrato del stub del layout: respuesta 200 siempre, para que no se puedan sondear direcciones. Y la baja no borra el registro: queda como auditoría.

Boletines: composer, envío y archivo

El composer de /admin/newsletter, envío por SendGrid y el archivo público en /butlletins (F7b). Con su lote de migración:

DO $$ BEGIN
  CREATE TYPE "public"."enum_butlletins_estat" AS ENUM('esborrany', 'programat', 'enviat');
EXCEPTION WHEN duplicate_object THEN null; END $$;

CREATE TYPE IF NOT EXISTS no existe en Postgres: el patrón DO-block del repo es la forma. Y dos clásicos: comentarios JS dentro del template SQL que se iban como texto literal (ahora --), y un Set de relaciones compartido entre el array y la tabla raíz en butlletins_rels, mismo patrón que ya vimos con plans_laia_rels.

Detalle de frontend que costó su hora: el archivo de boletines con prerender de ISR intentaba hablar con la base de datos durante el build. force-dynamic y fuera. Y un test que mentía: el del sitemap daba falso negativo por SIGPIPE con pipefail.

El servidor de producción, por escrito

F8a arranca: docker-compose.prod.yml con Caddy al TLS (80/443), la app solo escuchando en 127.0.0.1:3000, MinIO sin puertos públicos y los secretos vía PANXING_SECRETS_DIR. El deploy-prod.sh hace pull → build (con tag para poder hacer rollback) → up → migrate → smoke. Y export-to-prod.sh automatiza el runbook 09a: cuando haya VPS, ejecutarlo dos veces y mirar el smoke.

El remate final, muy del día a día: el guard de usuarios de MinIO hacía mc admin user list | awk | grep, y la imagen latest ha dejado de traer awk ni grep. Ahora:

if mc admin user info local "$S3_ACCESS_KEY_ID" >/dev/null 2>&1; then

F7 cerrada. F8 arrancada.