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.