Desarrollo web

El día que producción no perdonó: dashboard, editor y estadísticas

Migración del dashboard a React, documentación de arquitectura y tres errores encadenados en producción: el 502 del proxy, el editor que no montaba y las estadísticas inaccesibles.

El día que producción no perdonó: dashboard, editor y estadísticas

El dashboard, pantalla a pantalla

Hoy he cerrado la migración del dashboard a React siguiendo la decisión que dejé anotada por la mañana: pantalla a pantalla, sin reescritura global y sin tocar la web pública.

Primero el listado de artículos: tenía más de doscientas líneas de JavaScript imperativo que seleccionaban nodos por ID y reconstruían filas a mano. Ahora es una única isla de React con los mismos estilos de siempre. Después los formularios de cuenta, luego el resumen —unas 450 líneas más de script que movían KPIs, gráficos y borradores— y por último el editor. En el editor, Milkdown (1,1 MB) va ahora en un import() dinámico: la página baja un chunk de 7 KB y la librería solo se descarga cuando se monta el editor.

Antes de desplegar pasé las cinco islas por las reglas de rendimiento de Vercel. Casi todo ya cumplía; el arreglo real fue uno: los formateadores de fechas se creaban en cada render y ahora viven a nivel de módulo.

La documentación que faltaba

He escrito docs/architecture.md: los tres contenedores y sus redes, el flujo de Alembic con comandos reales, recetas de backup con pg_dump y la política de índices de pgvector. Cada dato contrastado contra las fuentes. No he creado el índice HNSW: con el volumen actual no lo justifica, y el documento recoge el disparador para cuando toque.

Producción: tres fallos encadenados

El despliegue acabó con los tests en verde y todas las rutas respondiendo 200. Y entonces llegó producción.

Primero, 502 en /api: nginx resuelve el proxy_pass una sola vez al cargar la configuración y cachea la IP. Al recrear el contenedor de la API cambió de IP y el proxy siguió apuntando a la vieja. Un nginx -s reload lo arregla al momento; la solución definitiva es usar una variable en la location, que se resuelve en cada petición.

Segundo, el editor no montaba: el build no emitía un CSS que el manifest sí referenciaba (bug conocido de rolldown-vite con imports dinámicos de CSS) y el precargador de Vite, al fallar ese fichero, descartaba el import() entero. Por eso los temas del editor van ahora como import estático y solo el JavaScript de Milkdown queda diferido.

Tercero, las estadísticas daban 500: el router del servidor filtra la salida hacia el rango de Cloudflare donde resolvía analytics.escandell.cat. He fijado el dominio a una IP alcanzable con extra_hosts en el compose y el flujo completo vuelve a funcionar. Techo conocido: si Cloudflare reasigna la IP, el pin habrá que actualizarlo.

La lección del día: un 502 en producción no siempre es del código que acabas de desplegar. Aquí hubo tres capas encadenadas y solo una era nueva.

Cuando el agente se pasa

Mi agente de código, buscando la causa de las estadísticas, inventó dos salidas sin consultar y una apuntaba a un contenedor de otro proyecto del servidor. Lo he corregido y hemos dejado la regla clara: si la causa es ambiental, parar, explicar y preguntar antes de improvisar.

Fase 8: design system de verdad

Tailwind v4 integrado solo en el administrador — la web pública no recibe ni una línea. Tokens de color en un único bloque, cuatro primitivas (botón, input, panel y badge de estado) y una página de Estilos dentro de Ajustes para verlo todo vivo. Primer aprendizaje del sistema: el CSS antiguo resetea todos los márgenes, así que en componentes nuevos el espaciado tipográfico se declara siempre explícito.

Fase 9, el primer consumidor

La página de Artículos ya usa las primitivas: chips y botones migrados. Mejora silenciosa: eliminar ahora se muestra en rojo destructivo; antes era un botón idéntico a los demás. Filtros y buscador esperan a tener un segundo consumidor.