Repository navigation
La web se explica a quien no sabe biología, y deja de ofrecer lo que no funciona - #996
Merged
Merged
Conversation
Tres defectos del rail, los tres visibles para un visitante anonimo de
protea.ngrok.app:
1. /instrument/graph estaba listado DOS VECES en el grupo "results",
misma etiqueta y mismo icono, con dos hints distintos. El rail
renderizaba dos filas de aspecto identico apuntando a una sola ruta.
Se queda la primera posicion y el hint que NO afirma recuentos: el
descartado decia "ten nodes" y "nine panels", numeros que nada aqui
verifica.
2. /maintenance se ofrecia a todo el mundo, y el edge lo contesta con un
403 de texto plano ("Forbidden: this page requires role 'operator'",
middleware.ts). Un visitante que lo seguia aterrizaba en esa pagina
desnuda. NavItem gana un campo minRole que refleja la tabla PROTECTED
del middleware, y un filtro central descarta los enlaces que el rol
del visitante no puede abrir, mas los grupos que queden vacios.
El edge sigue siendo la puerta de verdad; esto solo deja de anunciar
un destino que la puerta va a cerrar. El comentario en el campo dice
que las dos listas hay que mantenerlas a la par.
3. El CTA de annotate apuntaba a `/<locale>#annotate-form` cuando el
visitante estaba en la portada, y ese ancla NO existe alli:
id="annotate-form" vive solo en la pagina de annotate. O sea que la
llamada a la accion de la primera pantalla no llevaba a ningun sitio.
Ahora va a /<locale>/annotate en todos los casos.
El filtro usa useRole, que devuelve "viewer" en SSR y antes de la
hidratacion, asi que el HTML del servidor es el rail anonimo y una fila
restringida no parpadea antes de leerse la cookie.
Los tests: cuatro fallan contra el codigo anterior (duplicado, grafo una
sola vez, maintenance oculto, CTA) y dos pasan en ambos estados a
proposito, porque son guardas contra el arreglo excesivo: que maintenance
SI se ofrezca a un operador, y que las paginas publicas del instrumento
sigan visibles para un anonimo. Comprobado quitando el cambio y
volviendolo a poner.
Un detalle del andamiaje que merece quedar escrito: el componente monta
el rail dos veces, escritorio y cajon movil, y el cajon se queda en el
DOM oculto por transform. Lleva aria-hidden cuando esta cerrado, asi que
en el arbol de accesibilidad hay exactamente uno y por eso una
comprobacion de duplicados sobre todo el rail significa algo.
No tocado: el error preexistente de react-hooks/set-state-in-effect en
Sidebar.tsx:295, que es de otro cambio.
Lo que veia un visitante de protea.ngrok.app esta semana, comprobado
contra la API viva el 2026-10-07 sin cabeceras, como un anonimo:
POST /v1/annotate -> 409
{"detail":"No annotation sets available. Load GO annotations first."}
y ese cuerpo se le renderizaba literal, porque lib/api.ts hace
`throw new Error(await res.text())` y AnnotateForm lo pinta. O sea que
quien evaluaba el proyecto leia una instruccion para el operador de la
base de datos. La base viva tiene cero filas en annotation_set, go_term,
protein_go_annotation y embedding_config, asi que no es un fallo
transitorio: no hay nada contra lo que predecir.
Tres cambios.
1. lib/campaign.ts, nuevo. Constantes fechadas y a mano, a proposito: la
pagina NO debe preguntarle a la API cuantos datos hay, porque ahora
mismo toda cuenta que pueda contestar es cero o esta en movimiento, y
una cifra que cambia bajo el lector es peor que una que dice cuando se
tomo. LIVE_ANNOTATION_AVAILABLE lleva escrito que se invierte en el
mismo cambio que pueda ensenar un PredictionSet CON FILAS, no un job
en verde: con el pool de referencia vacio la operacion corta por lo
sano, no crea conjunto, y el formulario reintenta ocho veces y deja al
visitante en una pagina vacia.
2. El formulario, en pausa, se explica en el idioma del lector y NO toca
la red. Antes sondeaba /jobs/gpu-availability cada 30 s para alimentar
un banner de cola que desde el panel en pausa no se puede alcanzar.
3. La portada abre en lenguaje llano. La frase de la tesis era el h1, asi
que lo primero que leia un visitante era "a taxonomy of orthogonal
evidence, combined by a calibrated fusion". Es la frase correcta para
el argumento y la equivocada para una entrada: conserva todas sus
palabras una seccion mas abajo, como h2.
La unica cifra de resultado que la portada afirma es CAFA 6, puesto 19
de 2.186 equipos: externa, fechada, de un tercero y comprobable por
cualquiera. El tablero LAFA ("first in seven of nine cells") NO sube a
la cabecera: viene de la campana que se borro el 2026-09-14 y, aparte
del borrado, 594 de sus 1.296 resultados se habian evaluado pasada la
marca de holdout v227. Se queda donde estaba, dentro del argumento,
que ahora lleva encima un aviso fechado que lo enmarca.
No se edita una palabra de la prosa de la tesis. Afirma LAFA en
presente en varios sitios y era cierto cuando se escribio, asi que lo
honesto es fechar el registro entero, no reescribir las frases del
investigador.
Las cifras del bloque de bienvenida estan contadas, no redondeadas a
ojo: 136 operaciones sobre 117 rutas leidas del OpenAPI vivo, y 4.495
funciones test_ en 293 ficheros contadas con AST. "Mas de 140 endpoints"
circulaba y es falso.
Tests. Los cuatro flujos e2e del formulario vivo se saltan desde la misma
constante que lee el componente, asi que el cambio que devuelva el
predictor los devuelve con el; y hay uno nuevo del estado en pausa que
comprueba, entre otras cosas, que el texto del 409 NO aparece. En
unitarios, cuatro del panel (incluido que no se hace ninguna peticion) y
seis de paridad de catalogos.
Esa paridad encontro de paso una clave que faltaba en chino,
functionalAnnotation.resultsTab.tableHeaders.k, anadida aqui. Una clave
ausente no rompe el build: next-intl renderiza la ruta de la clave, asi
que la pagina ensena "home.annotatePausedTitle" a quien lea ese idioma.
Comprobado que el test lo detecta quitandola y volviendola a poner.
Lint: 27 errores antes en AnnotateForm.tsx y 27 despues, todos
no-explicit-any preexistentes. Los `as any` de las claves nuevas no
hacian falta y no estan.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #996 +/- ##
===========================================
- Coverage 87.85% 87.79% -0.06%
===========================================
Files 306 328 +22
Lines 23414 25287 +1873
===========================================
+ Hits 20570 22201 +1631
- Misses 2844 3086 +242
🚀 New features to boost your workflow:
|
Tres correcciones a mi propio cambio anterior, las tres de Francisco
mirando la pagina.
1. EL BENCHMARK SE QUEDABA CARGANDO PARA SIEMPRE. Era un error de
verdad, no una falta de datos. Con el corpus vacio el endpoint
contesta perfectamente {"rows":[],"stages":[],...}, pero la pagina
resolvia su `stage` de matrix.stages, asi que stage se quedaba en null
y la puerta `if (!embeddings || !matrix || stage === null)` retenia la
rama de carga indefinidamente. Lo curioso es que la pagina YA tenia
estado vacio de verdad mas abajo (emptyKind, hasData); nunca llegaba.
Una herramienta sin datos no es una herramienta cargando, y no es una
herramienta rota, pero detras de un esqueleto las tres se ven igual.
Ahora hay una rama explicita, ANTES de la de carga, que dice que esta
construido y esperando.
2. ESCONDER EL FORMULARIO ESCONDIA LA HERRAMIENTA. En el cambio anterior
sustitui el formulario de annotate por un aviso, y eso borraba de la
vista algo que esta programado y funciona. Ahora el formulario SE VE,
deshabilitado, con el aviso encima. Reutiliza el camino que el
componente ya tenia para "cola saturada" en vez de inventar otro, y
las dos causas conservan banners distintos porque dicen cosas
distintas.
3. LA PORTADA SE VEIA MAL, Y ERA MIO. Puse `bg-[var(--muted)]` como
fondo, y --muted es #57534E: un color de TEXTO de la familia stone, no
una superficie. Encima el texto iba en --muted-foreground, que NO
EXISTE como token. Era gris oscuro sobre gris oscuro. Y los botones
rellenos de azul eran ajenos a una pagina editorial en serif que no
usa ni uno.
Rehecho en el lenguaje de la pagina: familia stone, cuerpo en serif, y
el enlace subrayado que usa en todas partes.
Tambien hacia que la tesis pareciera retenida en vez de publicada. El
aviso ahora dice que el argumento se publica entero y contra que se
midio, en vez de sugerir que esta bloqueado.
ENLACES EXTERNOS, los cinco comprobados con una peticion real antes de
escribirlos, porque un enlace muerto en una pagina que alguien esta
evaluando es peor que ningun enlace:
kaggle.com/competitions/cafa-6-protein-function-prediction 200
github.com/frapercan/cafaeval-protea 200
cafaeval-protea.readthedocs.io 200
github.com/claradepaolis/CAFA-evaluator-PK 200
biofunctionprediction.org/cafa 200
La portada enlaza la competicion, nuestro despliegue del evaluador
oficial y el upstream del que parte, que es lo que hace que las cifras
las pueda regenerar un tercero. La validacion externa se explica como es:
el metodo se envia a LAFA como contenedor en ejecucion, no como un
fichero de predicciones.
NO HAY ENLACE A LAFA, deliberadamente. No encontre ninguna URL suya en el
repositorio y no me la invento. Si existe, la anado.
Test de regresion del benchmark: comprueba que con la respuesta vacia
EXACTA de la API viva la pagina dice que esta esperando y deja de
declararse aria-busy. Los tres fallan contra el codigo que tenia el
error. Y no se puede comprobar con curl: next-intl inyecta el catalogo de
mensajes entero en el HTML, asi que toda cadena aparece en el documento
se haya renderizado o no. Caí en eso antes de darme cuenta.
El test del formulario cambia de contrato con el codigo: ahora afirma que
el textarea ESTA y esta deshabilitado, y que todos los botones de envio
lo estan, en vez de que el formulario no exista.
Tres defectos visibles en cuanto estuvo en pantalla, mas el enlace que faltaba. 1. LA MEDIDA ERA INCONSISTENTE. El h1 y la linea divisoria ocupaban el contenedor (max-w-3xl) mientras cada parrafo cortaba en max-w-prose, asi que el cuerpo se veia mas estrecho que todo lo que lo rodeaba y la columna bailaba. Una sola medida ahora, la del contenedor. 2. EL RESULTADO ESTABA ENTERRADO. "19 de 2.186 equipos en CAFA 6" iba de tercer parrafo, con menos peso visual que el recuadro de la campana que tenia debajo. Es la afirmacion mas fuerte y mas comprobable de la pagina: externa, fechada, puntuada por otros y verificable en un clic. Ahora encabeza, en su propio bloque con etiqueta, y la nota de la campana baja al final y se vuelve mas tenue. 3. LAS CIFRAS DE INGENIERIA ESTABAN DENTRO DE LA PROSA, que es donde no se pueden escanear. Son una fila de tres ahora: API REST, tests de backend, datos de anotacion leidos. Todas contadas, no estimadas, y PLATFORM lleva escrito como se contaron, porque son justo las cifras que un entrevistador comprueba. Y el enlace que faltaba: LAFA es https://functionbench.net/ (titulo comprobado: "CAFA Forever"). En el cambio anterior deje escrito que no lo ponia porque no encontraba la URL en el repositorio y no me la inventaba; Francisco la dio. Con ella son cuatro enlaces externos en la cabecera, los cuatro con una peticion real detras antes de escribirlos: el banco al que se envia el contenedor, la competicion, nuestro despliegue del evaluador oficial y el upstream del que parte.
Dos cosas que Francisco vio en la pagina desplegada. 1. "LA THESIS ESTA BLOQUEADA". Lo estaba: GET /thesis.pdf contestaba 404 en los tres sitios (API, frontend y la URL publica) con "thesis PDF not available", mientras la portada enlazaba a ella. Y el enlace lo habia puesto yo en la cabecera nueva sin probarlo, justo despues de escribir que un enlace muerto es peor que ninguno. La ruta estaba bien. Lo que faltaba era el fichero: el repositorio thesis/ tiene las fuentes LaTeX pero su unico PDF era un project_document, que es otro documento y no lo publico como si fuera la tesis. Construida con `make all` (212 paginas, titulo "PROTEA: PROtein funcTional Embedding-based Annotation") y colocada en static/thesis.pdf, que es la segunda ruta que mira el resolvedor y se lee en cada peticion, asi que la ruta quedo arreglada sin reiniciar la API. Ahora: 200 application/pdf, 1.114.070 bytes, por protea.ngrok.app. Anotado en el runbook de despliegue, incluido que es un artefacto de build: si cambian las fuentes nadie lo reconstruye, y un git clean se lo lleva. 2. EL TABLERO SELLADO ERA LO PRIMERO Y SE LEIA DESCONECTADO. El formulario de annotate vuelve a la portada, justo despues de la cabecera. No es una idea nueva: el CTA del menu apuntaba a `/<locale>#annotate-form` y ese ancla ya no existia alli, que es el fosil de este bloque habiendo vivido en la portada antes. Es lo que arregle esta manana sin saber que arreglaba el sintoma y no la causa. Y se lee mejor: quien no sabe lo que es una anotacion de proteina aprende mas de un formulario que de una tabla de cifras retiradas. El formulario se pinta deshabilitado, con su propia explicacion. Orden de la portada ahora: resultado externo, cifras de la plataforma, la herramienta, el argumento fechado, y el tablero mas abajo.
SUPPORT ESTABA ROTO DE LA PEOR MANERA: MINTIENDO.
POST /support exigia ROLE_VIEWER, y PROTEA_AUTHN_REQUIRED vale True por
defecto, asi que todo visitante anonimo que pulsaba "apoyar" recibia
401 {"detail": "Missing API key or bearer token"}
Pero SupportButton.handleSubmit no miraba res.ok. Leia el cuerpo, hacia
setSubmitted(true) pasara lo que pasara, y le daba las gracias. O sea que
el visitante veia "gracias", el contador no se movia y no se guardaba
nada. Un boton que miente sobre haber escrito es peor que uno que se
niega.
Tres arreglos:
1. POST /support pasa a require_anon_quota, el mismo patron que
/annotate: abierto a anonimos bajo cuota diaria por hash de IP. Es
coherente con que GET /support sea publico y con que el boton viva en
una pagina publica. Un pulgar arriba no es una escritura privilegiada:
crea una fila con un comentario ya acotado por el validador, y nada lo
lee de vuelta como instruccion. La cuota es el control que sustituye
al rol.
2. El boton comprueba res.ok y, si falla, lo dice en el idioma del lector
en vez de dar las gracias.
3. La pagina de apoyo enseñaba, con cero apoyos, un pulgar arriba de 60px
sobre un "0" gigante y debajo "0 con comentarios · 0 anonimos". Una
pagina cuya razon de ser es la prueba social, probando lo contrario.
A cero, invita.
De paso, su error de carga era un "Could not load support data." en
rojo, en ingles y sin traducir, para cualquier idioma.
Verificado contra la API viva: POST anonimo devuelve 201. La fila que
creo esa comprobacion la he borrado, porque un apoyo inventado en una
pagina publica es una afirmacion falsa a los visitantes.
SPHINX DABA 404 EN LOS TRES SITIOS, igual que la tesis y por la misma
razon: la ruta existia, la documentacion no estaba construida. Y el menu
se la ofrecia a los anonimos, asi que era el tercer enlace muerto.
Construida (30 MB en docs/build/html, que esta gitignored). Aqui hay una
trampa que conviene dejar escrita: las dependencias de Sphinx NO estan en
el .venv del proyecto, y ese .venv es el que usan la API y el worker
vivos, asi que instalarlas ahi podia moverles jinja2 o pygments por
debajo. En vez de eso, un venv aparte enlazado al del proyecto con un
.pth: autodoc importa protea, pip escribe solo en la capa de arriba, y el
entorno vivo queda intacto. Comprobado despues: jinja2 y pygments sin
tocar, sphinx ausente del venv vivo.
El montaje de /sphinx se decide al crear la app, asi que hubo que
reiniciar la API. Se hizo en la ventana limpia: sin ningun job QUEUED ni
RUNNING, con el driver dentro de una descarga de ~1 h en la que no habla
con la API.
EL TABLERO SE REPETIA. "first in seven of nine" se leia tres veces: la
cursiva del pie, la frase de cabecera y el capitulo cero. La cursiva
estaba pegada a la frase que repetia, asi que se queda con la mitad que
no era repeticion, la que admite las dos celdas que no se ganan. Las dos
restantes viven en secciones distintas. No se toca la prosa de la tesis.
… rotas
Ocho agentes recorrieron las 14 paginas que el menu ofrece a un anonimo,
con la base vacia, fetcheando en vivo y leyendo el codigo. 79 hallazgos:
1 bloqueante, 22 graves. El bloqueante era Sphinx, arreglado mientras
corrian. De los graves, dos clases compartian una sola raiz.
1. "ApiError: {json}" EN PANTALLA, OCHO VECES. lib/api.ts aplicaba sus
dos mensajes escritos para personas solo a las LECTURAS; una escritura
caia al cuerpo crudo de la respuesta. Asi que un visitante que pulsaba
"Launch Job" en /instrument/proteins?tab=insert leia, literal y
durante tres segundos y medio:
ApiError: {"type":"/problems/unauthorized","title":"Unauthorized",
"status":401,"detail":"Missing API key or bearer token",
"instance":"/api-proxy/jobs"}
Dos arreglos en la raiz: los mensajes de 401 y 403 se aplican tambien
a las escrituras, con su propia redaccion ("no se ha enviado" en vez
de "se retuvo el dato"), y un problemDetail() saca la frase `detail`
del documento RFC 9457 en lugar de enseñar el documento. Y un
errorText() sustituye a String(err) en los 24 sitios que lo pintaban,
porque String sobre un Error antepone el nombre de la clase.
2. LOS RECIBOS CITABAN ONCE FICHEROS Y NINGUNO EXISTE. La funcion
caracteristica de los capitulos es tirar del marcador y ver "el recibo
y la operacion que regenera el numero". Los once artefactos que nombra
son de la campana borrada. En una pagina cuyo lema es que una
afirmacion que no puedes regenerar no es una afirmacion, es el detalle
mas dañino que podia estar mal.
El tipo Receipt ya tenia el campo para esto, con su comentario
exacto: "True when the plan names this receipt but it was absent from
the storage snapshot". Dos recibos lo usaban y cuatro no. Ahora los
seis avisan. No se toca la prosa.
3. LA MIGA DE PAN LLEVABA AL 404 PELADO DE NEXT. /en/pillar/1 pintaba
"Home > pillar > 1" con el "pillar" enlazado, y /en/pillar no es una
ruta: app/[locale]/pillar solo tiene un hijo [n]. El visitante
aterrizaba en el 404 interno de Next, sin barra lateral, sin idioma y
sin vuelta atras. Los segmentos que solo son espacio de nombres dejan
de ser enlaces.
4. LA PAGINA DE PROTEINAS DECIA "NO PROTEINS FOUND" CON 745.421 EN LA
BASE. Su flag de carga empezaba en false y la peticion solo arranca en
un efecto, asi que el SSR y el pintado previo a la hidratacion tomaban
la rama vacia: "0 proteins" y, dos veces, "No proteins found. Use the
Insert Proteins tab to import from UniProt." La hermana de
/instrument/annotations ya lo hacia bien, empezando sus flags en true.
Al verificar el arreglo volvi a caer en la trampa de medicion de hoy:
el texto seguia apareciendo en el HTML. Separando los <script> se ve
que en el DOM real estaba a cero y lo que quedaba era el catalogo de
mensajes que next-intl inyecta entero.
El test de contrato de lib/api afirmaba el comportamiento viejo (que un
401 en POST llevase el cuerpo crudo). Actualizado a lo que ahora
garantiza, mas dos guardas nuevas: que el documento problem+json NO se
filtre, y que errorText no anteponga el nombre de la clase.
Lint: 115 errores antes y 115 despues, todos preexistentes.
… solo Hallazgos de una auditoria externa con navegador de verdad. Estos cuatro son comprobables desde aqui, y los cuatro los introduje yo. 1. UNA CIFRA FALSA, EN LA PAGINA CUYO ARGUMENTO ES QUE SUS CIFRAS SE PUEDEN COMPROBAR. El texto decia que la campana "has read 75 releases ... 802.1 GB of files". 75 y 802,1 GB son el tamano de la SERIE; leidas van 60. Lo escribi yo esta manana, el mismo dia que me he pasado corrigiendo exageraciones ajenas. CAMPAIGN_STATUS separa ahora releasesRead de goaReleases, y las tres cadenas que lo dicen (portada, aviso de annotate y vacio del benchmark) pasan a "X de Y", en los cinco idiomas. 2. CADA VISITA A LA PORTADA SE DESCARGABA LA TESIS ENTERA. Mi QuietLink mandaba por next/link todo lo que no fuera externo, y next/link PREFETCHEA. /thesis.pdf no es una ruta de Next, lo sirve la API, asi que cada visita se traia 1.114.070 bytes de fondo. Los destinos con extension van ahora por un <a> normal. 3. LA IMAGEN DE VISTA PREVIA RESPONDIA 308. openGraph.images no estaba declarado, asi que Next la derivaba de app/[locale]/opengraph-image.tsx, y con trailingSlash: true esa URL contesta 308 antes de contestar 200. Los crawlers de vista previa no siempre siguen una redireccion en la imagen, y LinkedIn no podia generar la tarjeta. El PNG se captura una vez a public/og/ y se declara absoluto. Declararlo no bastaba: la convencion de fichero de Next gana, asi que el generador dinamico se retira (sigue en el historial, en 12b3c55, que es de donde se capturo el PNG). Ahora: 200 image/png al primer intento, cero redirecciones. 4. robots.txt emitia "Sitemap: /sitemap.xml". Tiene que ser absoluta; Next no la resuelve contra metadataBase. LO QUE NO SE PUEDE ARREGLAR AQUI, y queda medido para no volver a intentarlo: toda respuesta de protea.ngrok.app lleva X-Robots-Tag: noindex, nofollow. No es del codigo (una peticion directa al puerto 3000 no la trae) ni de la config local de ngrok. Probado con una politica de trafico que anadia una cabecera de prueba y quitaba esa: x-protea-policy: aplicada <- mi cabecera llego x-robots-tag: noindex, nofollow <- sobrevivio al remove-headers La politica se ejecuta; la cabecera se inyecta despues, en el borde de ngrok. Es comportamiento de la plataforma en *.ngrok.app y bloquea las vistas previas de LinkedIn. Solo lo arregla un dominio propio u otro alojamiento. La maquinaria que escribi para intentarlo la he retirado, para no dejar nada inerte.
La auditoria externa, que recorrio el sitio en los cinco idiomas, señalo que escribi "hasta que termine esa corrida" en la portada en castellano. En Espana "corrida" tiene un sentido vulgar, y la plaza a la que opta Francisco es en Barcelona. Ahora dice "hasta que termine la reconstruccion". Con ella, otros calcos del ingles que venian de la misma mano o de antes: "banco LAFA" (falso amigo de benchmark), "tablero LAFA" por tabla cerrada, y tres cadenas viejas sin tildes: proteinas, automaticamente y un "obtiene" donde iba un imperativo.
Cuatro cosas de la auditoria externa, con la decision de Francisco sobre LAFA: se muestra, y se dice que el evaluador corre en local y que ahi es donde se validan los resultados. 1. "being recomputed" SE LEIA EN INGLES EN LOS CUATRO IDIOMAS. El valor retirado se interpolaba con un literal ingles escrito a pelo en page.tsx, dentro de una frase traducida: "el resultado sellado es being recomputed". Ahora es una clave, traducida en los cinco. 2. CUATRO RECUENTOS DE PROTEINAS EN EL MISMO MINUTO (745.421, 715.602, 713.318 y 754673). No se contradecian: eran cantidades distintas sin etiquetar. /v1/proteins/stats las separa, total = canonicas + isoformas, y esta pagina citaba una instantanea escrita a mano del total que ya se habia quedado vieja. La portada lo lee ahora en vivo con dos segundos de limite y la constante fechada como respaldo, y dice QUE cuenta: "entradas de proteina, canonicas mas isoformas". Es ademas lo unico vivo de verdad en la pagina, asi que es la cifra que mejor demuestra que hay algo funcionando detras. 3. EL NOMBRE NO ESTABA EN NINGUNA PARTE, mientras el aviso legal decia que el servicio se opera "personalmente" sin decir por quien. Es lo primero que busca quien evalua. Sacado del frontmatter de la tesis y del autor de git, no inventado, y enlazado al perfil. Mas un enlace al repositorio, que tampoco estaba en la portada. NO pongo el correo: publicar una direccion personal es decision suya, no de este fichero. Tampoco LinkedIn, que no tengo. 4. LA FRASE DE LAFA dice ahora exactamente que: el metodo se valida frente al benchmark enviando un contenedor en ejecucion; el evaluador oficial lo ejecutamos en nuestra propia maquina y ahi es donde se validan los resultados antes de afirmar nada; y es un fork con la puntuacion sin tocar. Antes decia "el evaluador oficial que desplegamos aqui" y en la misma frase "el evaluador del que parte", lo que la auditoria leyo, con razon, como una contradiccion.
… entrada 1. LA FIRMA, corregida por el propio Francisco: no "doctoral researcher" sino estudiante de doctorado, y la adscripcion es doble, Universidad de Sevilla y el Centro Andaluz de Biologia del Desarrollo (CSIC). Si se pone, se pone entera. Con enlace al ORCID ademas del de GitHub. El ORCID no lo escribi hasta tenerlo: el unico con esa forma en los repositorios era el marcador 0000-0000-0000-0000, y un ORCID equivocado en una pagina publica apunta a otro investigador. Lo dio el, y el registro publico dice "Francisco M. Perez-Canales". 2. EL MURO LEGAL TAPABA EL h1 EN LOS PRIMEROS DIEZ SEGUNDOS, y en movil se abria a mitad de texto. Existe para recoger el consentimiento antes de enviar una secuencia, y con el predictor en pausa nadie puede enviar ninguna: hoy no protege nada. Atado a la misma constante que el formulario, asi que vuelve con el. El texto NO queda inalcanzable, que seria peor: se abre con ?policy=1 y la portada lleva un enlace discreto "Terms and privacy". 3. EL MENU PASA DE 22 ENTRADAS A LAS QUE TIENEN ALGO QUE ENSENAR. Diez superficies se ocultan mientras el corpus este vacio: embeddings, anotaciones, query sets, scoring, reranker, datasets, feature registry, grafo, benchmark y evaluacion. No se borran ni dejan de funcionar; cuelgan de DATA_SURFACES_HAVE_DATA y vuelven con ella. Se quedan las que tienen algo: proteinas (754.862 filas), jobs (la campana en vivo) y stack (la arquitectura, que es a lo que viene un lector tecnico). Mas una entrada nueva al codigo, que no necesita datos y no estaba en ningun sitio. Dos de los tests del menu fallaron con este cambio, y era su trabajo: eran las guardas que escribi contra esconder de mas. Reescritos al contrato nuevo, y uno de ellos afirma ahora exactamente eso, que las superficies con datos NO desaparecen con la bandera. 4. Y una incoherencia que cree yo al ocultarlas: la portada seguia mandando a benchmark y al grafo desde sus llamadas a la accion. Ahora van al hub del instrumento y a la cola de trabajos, que es lo unico vivo de verdad.
A Francisco la anterior le parecia artificial, y lo era: fondo azul marino con "PROTEA" en degradado morado y rosa, que no se parece a nada de este sitio. La nueva usa los tokens de la propia pagina. fondo #FAFAF9 --background texto #0C0A09 --foreground antetitulo #1E40AF --primary, en versales con tracking filete #E7E5E4 --border, el mismo separador de la portada secundaria #57534E --muted Con el logo de verdad, /protea-mark.png, y el titular en Newsreader 400, que es la serif que la portada usa en su h1. Debajo, la unica cifra comprobable y la URL: "19th of 2,186 teams in CAFA 6 · protea.ngrok.app". Nada de LAFA ni de cifras retiradas, que en una miniatura no se pueden matizar. COMO SE HIZO, porque importa para la proxima. Se propuso capturarla con Playwright, pero en esta maquina no hay navegadores instalados y bajarlos son ~400 MB. No hacia falta: next/og renderiza sin navegador. Se reconstruyo el generador con el diseno nuevo, leyendo el logo y las dos tipografias de disco en runtime nodejs, se capturo su salida UNA vez, y el generador se retiro otra vez. Se retira a proposito: la convencion de fichero de Next gana sobre metadata.openGraph.images, y con trailingSlash: true esa ruta contesta 308 antes que 200. Esa redireccion es justo lo que impedia a LinkedIn dibujar la tarjeta. Lo que se publica es el PNG. Los TTF se bajaron a un temporal y no quedan en el arbol, asi que el build sigue sin tocar la red. Verificado como LinkedInBot: 200 image/png, 1200x630, 48.170 bytes, cero redirecciones al primer intento, y twitter:image apunta al mismo fichero.
En una pantalla de 1830 px el contenedor max-w-3xl dejaba unos 370 px de hueco muerto a cada lado de una columna de 768. Francisco lo vio en cuanto estuvo desplegada. Ensanchar el texto habria sido el arreglo equivocado: unos 65 caracteres es la medida que se lee bien, y el h1 y el argumento la conservan. Lo que compra el espacio es un sitio DONDE PONER cosas. Asi que la cabecera pasa a dos columnas desde xl: a la izquierda la entrada en prosa, y a la derecha un rail con las tres cifras contadas, los cinco enlaces de accion y la nota de la campana. Eso resuelve de paso otra cosa que señalaba la auditoria externa: esas tres cifras y los enlaces estaban por debajo del pliegue. En movil y tableta no cambia nada, se apila como antes. Y cada seccion por debajo de la cabecera queda anclada a max-w-3xl, para que la tesis, el capitulo cero, el tablero y los pilares conserven su medida de lectura en vez de estirarse a lo ancho del contenedor nuevo.
Segunda pasada del auditor externo. Lo primero que trae es un destrozo mio: al sacar el literal ingles "being recomputed" a una clave traducida, lo meti en una plantilla que tiene un hueco para un NUMERO, no para un verbo. Las cuatro paginas traducidas quedaron asi: es "el resultado sellado es recalculandose" de "lautet das versiegelte Ergebnis wird neu berechnet bei f_micro_w" pt "o resultado selado e sendo recalculado" zh "封存结果为 正在重新计算(f_micro_w)" El estado retenido necesitaba su propia frase, no un hueco. Ahora son dos mensajes y cada uno esta escrito para ser gramatical en su idioma. Lo demas de esta tanda, tambien mio: - "frente al tabla cerrada de LAFA". Lo dejo un reemplazo a ciegas que hice ayer cambiando "tablero" por "tabla cerrada" sin mirar el articulo. - CINCO RECUENTOS DE PROTEINAS donde deberia haber uno. La portada ya lo lee en vivo, pero el 745.421 estatico seguia en el aviso de annotate y en el vacio del benchmark. En vez de poner una peticion en cada sitio, esos mensajes dejan de citar el recuento: la cifra viva vive donde se lee viva, y los demas hablan de releases y GB, que no se mueven. - "Annotation data read 802.1 GB" en la fila de cifras. Otra vez lo mismo: 802,1 GB es la serie entera, no lo leido. La etiqueta pasa a "Annotation series". - "It placed 19 of 2,186" -> "19th". Y en castellano, "Quedo en el puesto 19 de 2.186". - UNA CONTRADICCION QUE METI AYER en el parrafo de LAFA: decia "validated externally" y en la frase siguiente "which is where these results are validated", hablando de nuestra propia maquina. Ahora separa las dos cosas, que es lo que Francisco describio: el metodo se ENVIA fuera, como contenedor en ejecucion; y antes de afirmar nada aqui, reproducimos la puntuacion nosotros con el evaluador desplegado en esta maquina.
He estado desplegando a produccion sin mirar CI. Cinco comprobaciones en rojo, y las cuatro causas son mias. 1. UN BARRIDO DE SEGURIDAD CAZO EL CAMBIO DE /support. tests/test_endpoint_gating_sweep.py mantiene dos listas explicitas, _VIEWER_PATHS y _ANON_OPEN_PATHS, y esta construido para que abrir una escritura a anonimos tenga que ser un acto deliberado y escrito, no un efecto colateral. Funciono exactamente asi. /support se mueve de una lista a la otra con el motivo al lado: GET /support ya era publico, el boton vive en una pagina publica, la escritura crea una fila con un comentario ya acotado por su validador, nada la lee de vuelta como instruccion, y la cuota diaria por hash de IP es el control que sustituye al rol, igual que en /annotate. Y se le añade su afirmacion positiva: un test nuevo que comprueba que un anonimo NO es rechazado en la puerta. La ruta no deja de estar vigilada, cambia lo que se vigila. 2. docs/openapi.json no reflejaba el cambio de guard. Regenerado. 3. check_openapi_refs fallaba por una ruta fantasma que escribi yo: cite "GET /thesis.pdf" en el runbook, y esa puerta exige que toda ruta citada exista en el esquema. /thesis.pdf es un fichero montado, no una operacion. Reformulado. 4. UN FALLO LATENTE QUE DESTAPE AL CONSTRUIR LA TESIS. test_thesis_pdf_url_reflects_filesystem apuntaba la variable de entorno a un fichero inexistente y esperaba None. Pero por contrato el resolvedor NO devuelve None ahi: cae al siguiente candidato, <root>/static/thesis.pdf. El test pasaba solo porque nadie habia construido nunca la tesis; colocarla lo rompio. Ahora el caso ausente se le pregunta al resolvedor con una raiz que el test controla, que es lo que de verdad afirma el contrato. Local: ruff y mypy limpios en el ambito de CI, las nueve puertas de lint en verde y la suite entera con codigo de salida 0.
…hidratacion 1. CINCO BOTONES "LAUNCH JOB" QUE UN VISITANTE PODIA PULSAR. Las pestañas de insercion de proteinas y de carga de anotaciones se renderizaban enteras y prerrellenadas, con el boton azul vivo. POST /v1/jobs exige rol de operador, asi que el visitante recibia el documento de error del servidor por haber pulsado algo que la pagina le ofrecia. Los cinco quedan deshabilitados para quien no sea operador, con una frase al lado que dice por que. El formulario SIGUE A LA VISTA, que es la misma decision que con annotate: esconderlo esconderia una parte de la plataforma que existe y funciona. Lo restringido es el envio, no la existencia. El hook devuelve la respuesta anonima antes de la hidratacion, asi que el boton no parpadea habilitado para quien no puede usarlo. 2. EL BENCHMARK NO TENIA h1 EN NINGUNA DE LAS DOS RAMAS QUE UN VISITANTE VE HOY. El encabezado existia, pero por debajo de los dos retornos tempranos, asi que la pagina de carga y la de "esperando datos" no tenian encabezado de primer nivel. Las tres ramas lo comparten ahora, y de paso el titulo deja de ser una cadena inglesa a pelo. 3. REACT #418 EN DATASETS, Y LA CAUSA EXPLICA ALGO MAS. `toLocaleString()` sin locale, y `toLocaleString([])`, toman el del ENTORNO. Node resuelve aqui a es-ES / Europe/Madrid y el navegador del visitante resuelve al suyo, asi que el servidor pintaba "754.862" y un navegador en ingles pintaba "754,862". React se niega a reconciliar eso, y es ademas por lo que el separador de miles no coincidia entre paginas. Los dos ayudantes de datasets pasan a formateadores fijos, en en-GB y UTC. En UTC a proposito: una marca de tiempo en una pagina de operaciones vale mas si todos los lectores ven el mismo instante, y ahora lo dice. OJO, esto NO esta arreglado en el resto: quedan 71 llamadas a `toLocaleString()` sin argumentos y 12 con `[]`. Mismo defecto, misma causa. No barro los 71 sitios en el mismo cambio a estas horas y con la campana corriendo.
La auditoria externa conto nueve bloques sin traducir en cada uno de los cuatro idiomas. No son todos la misma cosa, y mezclarlos habria sido un error. LO QUE SI ES CROMO, y pasa a traducirse: - "carried" y "frontier" dentro de las nueve celdas. Son los VEREDICTOS de la tabla: un lector que no los entiende no entiende la figura entera. Ademas, en corchetes y en monoespaciado, "[frontier]" se lee como un marcador sin renderizar. - La explicacion al pie, que decia de donde vienen las posiciones y por que no hay cifras detras. Era prosa inglesa incrustada en el componente. LO QUE NO TRADUZCO, y digo por que: la tesis, el capitulo cero y las cuatro tarjetas de pilares son 66 cadenas largas en lib/book.ts, y eso es el argumento cientifico de Francisco. Traducirlo yo cambiaria afirmaciones medidas en cuatro idiomas a la vez, y una afirmacion cientifica mal traducida es peor que una en ingles. Asi que se dice en voz alta: en los cuatro idiomas no ingleses, la seccion del argumento lleva ahora una linea que avisa de que lo que sigue se publica en el idioma en que se escribio. Quien entra en /es y se topa con un muro de ingles merece saber por que, en vez de concluir que la traduccion esta rota. Si Francisco quiere la prosa traducida, es decision suya y se hace aparte, con el tiempo que merece.
A 390 px la columna CC queda en x=475, fuera de pantalla. El contenedor ya tenia overflow-x-auto, asi que se podia desplazar, pero no habia ninguna señal de que hubiera algo mas a la derecha: la auditoria externa lo leyo como un tablero de dos columnas en vez de tres. Una linea de pista, visible solo por debajo de sm, que es donde el desbordamiento ocurre de verdad. Traducida en los cinco idiomas, como el resto del cromo del tablero.
Dos de las tres pegas de RGPD que levanto la auditoria externa, y que se pueden arreglar sin inventar nada. 1. EL RESPONSABLE NO ESTABA IDENTIFICADO. El aviso decia que el servicio se opera "personalmente e independientemente" sin decir por quien, lo que es justo lo que un aviso de este tipo tiene que decir. Ahora lo nombra. El dato ya es publico en la propia portada, asi que no se publica nada nuevo. 2. NO SE MENCIONABA NGROK. Todo el trafico llega al servidor a traves de ngrok, un proveedor de Estados Unidos, que por tanto ve los metadatos de la conexion. Es un hecho comprobable con solo mirar el dominio, y callarlo en el apartado de privacidad era una omision. LA TERCERA NO LA TOCO, y es la peor: las solicitudes de acceso, supresion y rectificacion siguen yendo a una incidencia PUBLICA de GitHub, lo que obliga a quien ejerce un derecho a publicar su peticion y a tener cuenta en GitHub. Cambiarla sin tener una direccion privada donde apuntarla la dejaria peor, no mejor: un aviso de proteccion de datos sin via de contacto no sirve de nada. Hace falta que Francisco decida que direccion se publica. Yo no publico su correo por mi cuenta.
"critical user flows" fallaba, y era el unico rojo que quedaba:
> 214 | await expect(page.getByRole("textbox")).toHaveCount(0);
Expected: 0
9 x locator resolved to 1 element
Esa linea la escribi cuando el estado en pausa SUSTITUIA al formulario.
Francisco dijo despues que el formulario tenia que verse, porque
esconderlo escondia una herramienta que esta programada y funciona. Lo
cambie, actualice el test unitario... y me deje el e2e.
Ahora afirma lo que de verdad garantiza la pagina: la caja de secuencia
esta, y esta deshabilitada.
No lo detecte en local porque en esta maquina no hay navegadores de
Playwright instalados y bajarlos son unos 400 MB, cosa que dije al
cambiar el spec. CI es quien corre esa suite, y ha hecho exactamente su
trabajo.
El e2e de landing seguia en rojo, y esta vez sin excusa: el primer
auditor externo me listo estas tres aserciones esta mañana, antes de que
yo tocara nada, y no las arregle.
- el h1 tenia que ser /Protein function is predictable/. Lo cambie a la
frase llana y movi la tesis a un h2.
- la cursiva del tablero tenia que decir "the two we are not, we
explain". La reescribi para que dejara de repetir el puesto que la
frase de al lado ya dice.
- "Open the instrument" tenia que aterrizar en /benchmark. Lo cambie al
hub, justamente porque el benchmark esta oculto del menu mientras no
haya datos y mandar ahi la llamada principal contradecia la
navegacion.
Los tres alineados con lo que la pagina hace hoy. Y el primero gana una
asercion que antes no existia: que la tesis SIGUE publicada, como h2. Sin
ella, el argumento podria desaparecer de la portada sin que ningun test
se quejara, que es exactamente el riesgo de haberlo movido.
Un test nuevo para lo que se añadio hoy y no vigilaba nadie: que el
nombre y el ORCID estan en la portada. Es lo primero que busca quien
evalua el proyecto y hasta hoy no estaba en ningun sitio.
Playwright se nego a elegir entre ellos ("strict mode violation") y al
contarlos resulto que el problema no era el test:
/en/instrument/ el del rail, que anadi yo hoy
/en/instrument/jobs/ el del aviso de annotate, que cambie hoy
/en/instrument/benchmark/ el del pie, que ya estaba
Tres enlaces que leen "Open the instrument" y llevan a tres destinos. El
primer auditor externo ya lo habia señalado esta mañana; yo lo empeore
añadiendo el tercero.
Ahora los dos que comparten etiqueta comparten destino, el hub, que es
ademas lo coherente con que el benchmark este oculto del menu mientras no
haya datos. Y el de annotate, que apunta a la cola de trabajos, deja de
mentir sobre a donde va: dice "Watch the campaign run", en los cinco
idiomas.
Repetir la llamada a la accion arriba y abajo es normal en una portada.
Que digan lo mismo y lleven a sitios distintos no lo es.
El test fija ahora las dos cosas, el numero Y el destino de cada uno, en
vez de esconder la duplicacion detras de un .first() sin decirlo.
github-actions Bot
pushed a commit
that referenced
this pull request
Oct 8, 2026
…997) ## Por qué El consumer confirmaba el mensaje **antes** de ejecutar el job, para esquivar el `consumer_timeout` del broker. Eso tenía dos efectos medidos durante la fase 2 de la campaña GOA: 1. **El segundo nodo se quedaba sin trabajo.** Con el ack inmediato el canal queda con 0 sin confirmar, así que RabbitMQ asigna el siguiente mensaje al mismo consumidor, que lo deja en su buffer local durante la hora que dura la carga en curso. El sobremesa no veía nada que tomar. 2. **La conexión se moría en cada carga.** El `consumer_timeout` no era el temporizador que mandaba: lo era el heartbeat AMQP. `handle_job` bloquea el bucle de E/S de pika, pika deja de enviar heartbeats, y el broker cierra a los 2x600 s. Medido en el log del worker: 58 `StreamLostError` y 75 avisos de `missed heartbeats` en el log del broker, uno por carga, a los ~20-26 min. ## Qué cambia El job pasa a un hilo propio y el hilo de E/S se queda bombeando la conexión cada 5 s mientras dura (`_run_job_while_pumping`). El ack se mueve al `finally`, detrás del job, de modo que el mensaje sigue sin confirmar mientras se ejecuta y el prefetch de 1 reserva el nodo. Las dos mitades son inseparables: mover el ack sin bombear deja que el broker cierre la conexión igual, y bombear sin mover el ack no reparte trabajo. Un fallo del worker sigue confirmando el mensaje, sin reencolar: eso no cambia, y los tests que lo fijaban siguen ahí con el nombre corregido (`test_worker_failure_still_acks`). Se añaden dos guardas de orden: `test_the_ack_waits_for_the_job` y `test_a_failing_job_is_still_acked_last`. ## Medido en producción Desplegado en el portátil el 2026-10-08 a las 15:49. En las 35 min siguientes y en la carga completa de la release 168 (63,6 min): - `StreamLostError` nuevos: **0** (antes: uno por carga) - `missed heartbeats` nuevos: **0** (antes: uno por carga) - `messages_ready=0, messages_unacknowledged=1` — el nodo retiene su mensaje durante toda la carga, que es el efecto buscado - el log muestra `Job finished` y `Job acked` en el mismo segundo, y ya no `Job acked` junto al `Dispatching` La 168 tardó 63,6 min contra los 62,7 de la 166 en solitario: correr dos nodos no le cuesta nada al que ya iba. ## Lo otro que lleva El commit de la deuda de formato (`toLocaleString` en 83 sitios, la guarda `noAmbientLocale` y `scripts/deploy-frontend.sh`) se quedó fuera de #996 porque entró después del merge. Va aquí sin cambios.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lo que veía un visitante
Comprobado contra la API viva el 2026-10-07, sin cabeceras, como un anónimo:
Y ese cuerpo se le renderizaba literal, porque
lib/api.ts:1002-1007hacethrow new Error(await res.text())yAnnotateForm.tsx:171-174lo pinta. Quien evaluaba el proyecto leía una instrucción para el operador de la base de datos.No es transitorio: la base viva tiene cero filas en
annotation_set,go_term,protein_go_annotationyembedding_config. No hay nada contra lo que predecir.Cambios
1.
lib/campaign.ts(nuevo). Constantes fechadas y a mano. La página no debe preguntarle a la API cuántos datos hay: ahora mismo toda cuenta que pueda contestar es cero o está en movimiento, y una cifra que cambia bajo el lector es peor que una que dice cuándo se tomó.LIVE_ANNOTATION_AVAILABLElleva escrito que se invierte en el mismo cambio que pueda enseñar unPredictionSetcon filas, no un job en verde: con el pool de referencia vacío la operación corta por lo sano (predict_go_terms/_coordinator.py:169-172), no crea conjunto, y el formulario reintenta ocho veces y deja al visitante en una página vacía.2. El formulario, en pausa, se explica y no toca la red. Antes sondeaba
/jobs/gpu-availabilitycada 30 s para alimentar un banner de cola que desde el panel en pausa no se puede alcanzar.3. La portada abre en lenguaje llano. La frase de la tesis era el
h1, así que lo primero que leía un visitante era «a taxonomy of orthogonal evidence, combined by a calibrated fusion». Conserva todas sus palabras una sección más abajo, comoh2.La única cifra de resultado que la portada afirma es CAFA 6, puesto 19 de 2.186: externa, fechada, de un tercero, comprobable. El tablero LAFA no sube a la cabecera — viene de la campaña borrada el 2026-09-14 y, aparte del borrado, 594 de sus 1.296 resultados se evaluaron pasada la marca de holdout v227. Se queda dentro del argumento, que ahora lleva encima un aviso fechado.
No se edita una palabra de la prosa de la tesis. Afirma LAFA en presente y era cierto cuando se escribió: lo honesto es fechar el registro, no reescribir las frases del investigador.
4. El menú deja de ofrecer destinos que no existen o no abren.
/instrument/graph/maintenanceminRole: "operator", oculto al anónimo/<locale>#annotate-form, y ese ancla no existe en la portada/<locale>/annotateNavItemganaminRole, que refleja la tablaPROTECTEDdel middleware. El edge sigue siendo la puerta; esto sólo deja de anunciar un destino que la puerta va a cerrar.Cifras, contadas
No redondeadas a ojo: 136 operaciones sobre 117 rutas leídas del OpenAPI vivo, y 4.495 funciones
test_en 293 ficheros contadas con AST. «Más de 140 endpoints» circulaba y es falso.Tests
Los cuatro flujos e2e del formulario vivo se saltan desde la misma constante que lee el componente, así que el cambio que devuelva el predictor los devuelve con él, y hay uno nuevo del estado en pausa que comprueba que el texto del 409 no aparece.
En unitarios: 6 del menú, 4 del panel (incluido que no se hace ninguna petición) y 6 de paridad de catálogos. Comprobado que fallan contra el código anterior: 4 de los 6 del menú fallan, y los 2 que pasan en ambos estados son guardas deliberadas contra el arreglo excesivo (que
/maintenancesí se ofrezca a un operador, y que las páginas públicas sigan visibles).La paridad encontró de paso una clave ausente en chino,
functionalAnnotation.resultsTab.tableHeaders.k, añadida aquí. Una clave que falta no rompe el build: next-intl renderiza la ruta de la clave, así que la página enseñahome.annotatePausedTitlea quien lea ese idioma.Lint: 27 errores en
AnnotateForm.tsxantes y 27 después, todosno-explicit-anypreexistentes.Desplegado y verificado
Ya está en producción. El despliegue de esta máquina borra y recrea
.nextbajo el proceso que sirve, y el fallo no es un 500 honesto sino un 200 con HTML rancio y chunks a 404 que pasa los health checks (scripts/deploy-check.sh:4-11, el incidente del 2026-08-20). Así que: copia de respaldo del build, build, reinicio explícito y verificación.