epidemiologia¶
Propósito y diseño¶
epidemiologia es la aplicación de APS Conecta Gestión 34 que muestra dentro de la intranet lo que publican el MINSAL, el ISP y la OPS/OMS, y enlaza lo que no se puede mostrar. Persiste solo lo que publican sus fuentes —los metadatos de cada fuente y una copia del documento de cada informe— y no toca datos de pacientes. Es un proxy de lectura sobre una caché y dos tablas propias: la caché es el camino rápido y las tablas, la copia durable. La guía de uso es Epidemiología.
La unidad del diseño es la fuente (CONTEXT.md): lo que la aplicación lee, interpreta y persiste, con su propia frescura y su propio estado degradado. Son siete, en el orden de Sources::ALL:
id |
Sector en la interfaz |
Grano del host |
Regla de almacenamiento |
Intérprete |
|---|---|---|---|---|
|
Alertas vigentes |
|
vigente: el conjunto se reemplaza entero |
|
|
Departamento de Epidemiología |
|
vigente |
|
|
Circulación de Virus Respiratorios |
|
serie por (año, semana), con el documento guardado |
|
|
Medicamentos (ANAMED) |
|
flujo: se conserva todo lo leído |
|
|
Farmacovigilancia |
|
flujo |
|
|
Vigilancia de cosméticos |
|
flujo |
|
|
Alertas y actualizaciones |
|
flujo |
|
Las decisiones que sostienen esa forma están en los ADR del repositorio (docs/adr/):
Un solo pipeline para las seis fuentes de alertas (
docs/adr/0017-six-alert-sectors-one-pipeline.md, D1–D3). Cada fuente de alertas es una instancia deAlertRepositoryconfigurada por unFuentede solo lectura y construida por la fábricaAlertRepositories: el contenedor de APS Conecta Gestión registra un servicio por nombre de clase, así que seis instancias configuradas de una clase son una fábrica, no seis registros. El informe conserva su propia clase,IspReportRepository, porque una serie de documentos semanales guardados es una regla que el pipeline no tiene.Las fuentes persisten y el cliente se identifica (
docs/adr/0015-the-fuentes-persist-and-the-fetcher-introduces-itself.md). Cada solicitud lleva el User-AgentAPS-Conecta-Epidemiologia/1.0, respeta el freno del host y acepta una negativa como respuesta.Un contrato OCS de una sola ruta, con clave por fuente (
docs/adr/0006-one-ocs-route-keyed-by-source.md). El tablero y EPIVIGILA no son fuentes: son enlaces, sin nada que leer, guardar en caché ni degradar, y quedan fuera de la carga útil.Sin catálogo
l10n/es.json(docs/adr/0011-no-l10n-file-and-no-seam-without-an-adapter.md). Las cadenas de la interfaz ya están en español en el código fuente, y el catálogo sería un mapa de identidad.Sin rasterizar PDF (
docs/adr/0002-no-pdf-rasterisation.md). La imagen oficial del servidor trae la extensiónimagickpero no Ghostscript, así que el informe se muestra como una tarjeta por semana epidemiológica.
La aplicación se integra con la plataforma por tres registros de Application: un proveedor de búsqueda unificada, un notificador y una comprobación de configuración. Su licencia es AGPL-3.0-or-later, heredada de @nextcloud/vue, que el bundle compila.
Contratos de componentes y API¶
La ruta OCS sources¶
La aplicación publica una sola ruta OCS de lectura, sobre el mecanismo OCS de la plataforma:
GET /ocs/v2.php/apps/epidemiologia/api/v1/sources?format=json
OCS-APIRequest: true
La cabecera OCS-APIRequest: true es obligatoria: sin ella, APS Conecta Gestión responde 412 antes de llegar al controlador. La ruta exige sesión (#[NoAdminRequired]), limita a 120 solicitudes por minuto y por usuario (#[UserRateLimit]) y no declara #[CORS], que necesita un origen concreto.
Parámetro |
Valor |
Respuesta |
|---|---|---|
ninguno |
— |
200 con todas las fuentes, enteras |
|
un id de |
200 con esa fuente sola |
|
un id desconocido |
404 |
|
|
igual que omitido |
|
|
400: se retiraron con la única fuente que tenía filtro |
cualquiera, como arreglo |
|
400 |
Esquema de ocs.data (generado de ApiController::sources()):
<id>: {
fetched_at: int | null época de la última lectura; null cuando la copia sale de la tabla o cuando no hay nada guardado
degraded: bool true: se sirve la última copia buena, o nada
items: [ {
title, url, year, month todas las fuentes; month = 'YYYY-MM'
se? reports: la semana epidemiológica
date? epi_alerts, las tres del ISP y paho_alerts: el día del publicador, 'YYYY-MM-DD'
tipo? paho_alerts: alerta | actualizacion | otro
} ] todos los valores son string
total: int = cantidad de items
has_more: bool siempre false: cada respuesta es la fuente entera
}
Una clave que una fuente no tiene se omite, nunca es null. El contrato es openapi.json, que scripts/openapi.sh genera desde el controlador y nunca se edita a mano; su estabilidad es de diseño (ADR-0006 del repositorio). Las rutas anteriores ya no existen: /apps/epidemiologia/api/v1/… desde la versión 0.2.0, y /ocs/…/alerts y …/reports desde la 0.3.0.
La página y el estado inicial¶
PageController::index() entrega la misma carga útil que la ruta OCS en una sola clave de estado inicial, sources, y la dirección del tablero en otra, tablero; la página no hace una solicitud a sí misma al montarse. Su política de seguridad de contenido permite el marco de https://app.powerbi.com, sin el cual el tablero queda en blanco. En el navegador, una fuente ausente del estado inicial se lee como {items: [], fetched_at: null, degraded: true}: un panel vacío con aviso, nunca una aplicación en blanco.
La ruta del informe¶
GET /informe/{year}/{semana} es una ruta simple, no OCS, fuera del contrato generado: es el enlace del panel, no un contrato publicado. Exige sesión, como el resto de la aplicación.
Respuesta |
Cuándo |
|---|---|
200, |
la semana existe, su documento está guardado y el sha256 del archivo coincide con el de la fila |
303 a |
la semana existe, pero el documento no está guardado, falta en los datos de la aplicación o no coincide con su hash (este último caso deja una advertencia en el registro) |
404 |
la semana no existe |
El destino del 303 se construye con el año y la semana de la propia fila, siempre en https, nunca con una dirección del feed.
Referencia generada¶
La lista de rutas, ajustes y trabajos declarados es la referencia generada:
Configuración¶
Un solo ajuste está pensado para cambiarse, y tiene un valor por omisión que funciona; el resto de la configuración de la aplicación lo escribe la propia aplicación.
Ajuste |
Qué hace |
Valor por omisión |
Dónde vive |
|---|---|---|---|
|
Qué grupos reciben el aviso cuando una fuente lleva 2, 7 o 30 días sin poder leerse |
|
Configuración de la aplicación: |
|
Los contadores de fallas consecutivas que leen la notificación y Administración → Visión general |
ausentes hasta el primer sondeo |
Configuración de la aplicación, escrita solo por |
|
Que un flujo ya recorrió sus dos páginas anteriores; borrarlo hace que la siguiente pasada profunda las recorra otra vez |
ausente hasta ese recorrido |
Configuración de la aplicación, escrita solo por |
|
Con qué contenedor, usuario y dirección base conversan los scripts de desarrollo |
|
Entorno, leído por |
Los tiempos de caché son constantes, no ajustes: una hora de frescura, treinta días como última copia buena y, detrás de cada fuente, una tabla que sobrevive a la caché (AlertRepository, IspReportRepository). Un ajuste ahí permitiría que una instancia se configurara hasta mostrar como vigente una alerta que el MINSAL ya retiró.
Instalación y actualización¶
El primer arranque de un entorno de desarrollo está en el inicio rápido del README del repositorio. En una instalación real lo que cambia es la actualización: subir <version> en appinfo/info.xml es el mecanismo de entrega, porque deja la instancia en «requiere actualización» hasta que corre occ upgrade, y esa actualización hace llegar las migraciones, el paso de reparación y los trabajos en segundo plano.
Hacer
git pullen el clon.Ejecutar
docker exec -u www-data apsconecta-gestion-nextcloud-1 php occ upgrade.Reiniciar el contenedor web: los procesos web guardan las rutas y el estado de la aplicación en APCu, que
occno limpia (ver Trampas de la plataforma).
Antes de actualizar desde una versión anterior a la 0.11.0, respaldar la base de datos: su migración reescribe la clave primaria de epidemiologia_alert y APS Conecta Gestión no tiene migraciones de retroceso.
js/ y css/ están versionados, así que desplegar no requiere Node en el servidor; quien modifique src/ ejecuta npm run build y confirma el resultado, y scripts/smoke.sh avisa si el bundle quedó atrás. node_modules no debe vivir en la raíz web de una instalación de producción: .htaccess lo cierra, pero depende de Apache y se aplica a cualquier persona, autenticada o no. Lo correcto es compilar en otro lugar y desplegar solo lo necesario.
Añadir una fuente¶
El catálogo es una sola lista (lib/Service/Sources.php) y todo lo demás la consulta, pero consultarla sigue significando tocar cada consumidor que muestra, sondea, sirve o nombra la fuente nueva. Esta es la lista de verificación, escrita cuando seis sectores de alertas pasaron a un solo pipeline (docs/adr/0017-six-alert-sectors-one-pipeline.md). Con siete fuentes se conservaron a propósito los brazos match por método (D4 de ese ADR): un brazo omitido es un UnhandledMatchError con el que tropieza la batería de pruebas, nunca una respuesta equivocada.
La mayoría de las fuentes nuevas son fuentes de alertas —una página o un feed de alertas— y se suman al pipeline como configuración. Una fuente con reglas que el pipeline no tiene, como los documentos semanales guardados del informe, recibe un repositorio propio; en ese caso, en el servidor solo aplica lo de Sources.
Servidor:
Sources: la constante del id —nuncasanitary_alerts, queRetireSanitaryAlertsborra en cada actualización— y su fila enALL(orden de presentación).Para una fuente de alertas, su
FuenteenAlertRepositories::fuente(): el id; la dirección que lee; suhost(un host que ya existe comparte su freno y su enfriamiento, comoISP_HOSTpara www.ispch.gob.cl); el publicador y los dos nombres que usa el registro; su intérprete (PageParserpara una página de enlaces a PDF,FeedParserpara un feed de WordPress,PahoParserpara la lista de la OPS/OMS, o unParsernuevo con una captura real como fixture);backfill, las dos páginas anteriores, para un flujo y nada para una página vigente; ybundlesolo para un host que sirve únicamente su certificado hoja.El brazo de
Sources::get(), sindefaulta propósito: un id deALLsin brazo lanzaUnhandledMatchError, ySourcesTestrecorreALLpara que la falla caiga en la batería de pruebas y no frente a una persona del equipo clínico.Los brazos de
Sources::probe(),revalidate(),name(),kind()yhost(), con la misma regla.name()es español a mitad de frase, con la institución dentro;host()pregunta aAlertRepositories::host()en una fuente de alertas, de modo que un host se escribe una sola vez, en suFuente.SEARCHABLE, solo si la fuente trae alertas que alguien buscaría por tema, y entonces también el brazo desearch().ACCUMULATINGy el brazo deconverge(), solo para una fuente que conserva lo que lee y tiene páginas anteriores: todo flujo. Un conjunto vigente se reemplaza a sí mismo y no tiene nada que converger.ProbeJobyWarmJob: nada, ambos recorrenSources::ALL.RefreshJob: nada más allá deACCUMULATING, que recorre.El contrato OCS: una clave nueva de elemento entra en el
@returndeApiController::sources()y enApiContractTest::NOMBRES; regeneraropenapi.jsonconscripts/openapi.sh, nunca a mano.PageController: nada, porque una sola clave de estado inicial,sources, ya lleva todas las fuentes.
Interfaz:
src/panes.js: una fuente de alertas es un sector, una entrada del registro con susource(el id del servidor), su grupo y el nombre de un ícono, más su entrada enPANE_NAMES. De esa entrada se derivan el panel (unAlertsPanemás), la fila de navegación, la tarjeta de Inicio, las verificaciones de sectores descripts/layout.mjsy la clavija depanes.test.jscontraSources.php.src/App.vue: el componente del nombre de ícono enICONS.src/pure.js: su entrada enSITIOS, la página del propio publicador para esa lista, adonde el aviso de degradación envía a quien lee.Un publicador nuevo: su entrada en
FUENTES—name(a mitad de frase, con su artículo),caption(por sí solo: el rótulo de la navegación y de las tarjetas) ycount(su sustantivo para lo que contienen sus listas)—, un bloque con rótulo enApp.vueescrito como los tres que hay (los grupos no se pliegan,docs/adr/0008-nav-groups-do-not-collapse.md) y su nombre en el crédito del pie.src/state.js: nada, porquesource(id)lee cualquier id y una clave ausente se lee como degradada, nunca como cero.SURFACESdetests/unit/attribution.test.js, si un archivo nuevo muestra nombres de publicadores.
Compuertas que deben seguir en verde:
scripts/test.sh:SourcesTestrecorreALLpor todos los métodos yAlertRepositoryTestla recorre porAlertRepositories.npm testy luegonpm run build, con eljs/y elcss/reconstruidos confirmados.scripts/smoke.sh: su líneafor source in …nombra cada id a mano; agregar el nuevo.scripts/layout.sh: las verificaciones de sectores derivan del registro, y la verificación 7 vuelve a medir la navegación contra el ADR 0008.
Transiciones de estado¶
Lectura de una fuente¶
get() decide en este orden, igual en AlertRepository y en IspReportRepository. La «copia guardada» es la entrada de caché o, si la caché está vacía, el conjunto que devuelve la tabla.
Estado |
Condición |
Efecto |
Respuesta |
|---|---|---|---|
Fresca |
copia guardada con |
ninguno |
la copia, |
En enfriamiento |
|
ninguno |
la copia guardada, |
Frenada |
|
ninguno |
la copia guardada, |
Vencida |
copia guardada, sin enfriamiento ni freno |
encola |
la copia vencida, |
Fría, primera vez |
nada guardado, sin |
fija |
la lectura nueva, o degradada |
Fría, otra vez |
nada guardado, con |
encola |
|
Una lectura (refresh()) termina de una de tres formas:
Elementos leídos. Una fuente vigente escribe la caché y luego reemplaza su conjunto en la tabla; un flujo inserta o actualiza cada elemento en la tabla y guarda en caché lo que la tabla devuelve, así que un elemento que el publicador dejó de listar se sigue sirviendo. El informe inserta o actualiza cada semana y guarda en caché la serie completa.
Cero elementos, porque no hubo lectura o porque lo leído no dio ninguno: advertencia en el registro,
cooldown-{host}por 300 s y la última copia servida condegraded: true. No se escribe nada, ni en la caché ni en la tabla.Falla de almacenamiento: advertencia en el registro, nunca degradado. La lectura se sirve igual y la tabla se reintenta en la siguiente escritura.
Freno¶
Una respuesta 4xx o 5xx activa el freno del host, freno-{host}: la espera empieza en 2 horas, se duplica con cada negativa y llega como máximo a 24 horas. Cualquier respuesta posterior que no sea una negativa lo levanta de una vez, aunque lo leído no dé elementos. Los hosts son alerts, epi, isp y paho; las cuatro fuentes de www.ispch.gob.cl comparten isp. La negativa de una página antigua o de un documento nunca activa el freno: la lectura barata de la primera página no paga los problemas de la solicitud profunda.
Trabajos en segundo plano¶
Trabajo |
Clase base |
Intervalo |
Sensibilidad |
Qué hace |
|---|---|---|---|---|
|
|
24 h |
|
El sondeo: |
|
|
1 h |
|
|
|
|
24 h |
|
|
|
|
a demanda |
— |
|
Un trabajo TIME_INSENSITIVE corre solo dentro de la ventana de mantenimiento, que en Gestión va de 05:00 a 09:59 UTC (ver Trabajos en segundo plano y Trabajos en segundo plano (Cron)). La sensibilidad vive en la fila del trabajo, no en su clase: la primera ejecución de un trabajo insensible escribe time_sensitive = 0 y nada la vuelve a 1. Por eso el pase horario es una clase nueva y no RefreshJob con otra constante.
Sondeo y notificaciones¶
probe() responde uno de tres veredictos (Probe):
Veredicto |
Significado |
Efecto en el pase |
|---|---|---|
|
el host respondió y la fuente se leyó |
el contador vuelve a 0 y la notificación se retira |
|
el host respondió (una negativa, una página de WAF, un documento reestructurado) y la fuente no se lee |
cuenta como falla; las demás fuentes del host se consultan igual |
|
no hubo respuesta: tiempo agotado, conexión rechazada, DNS o TLS |
cuenta como falla; las demás fuentes de ese host se registran como fallidas sin consultarlas |
ProbeRecord guarda en la configuración de la aplicación los días consecutivos de falla (probe_failures_<id>) y la fecha de la primera (probe_failing_since_<id>). Una lectura sana borra la racha. ProbeJob notifica en los días 2, 7 y 30 de falla consecutiva, con una sola notificación por host y pase, que nombra las demás fuentes caídas del mismo host. La reciben los usuarios de los grupos de notify_groups (por omisión, ["admin"]). La comprobación de configuración «Epidemiología: conexión con las fuentes» lee los mismos contadores en Administración → Visión general: informa mientras el sondeo no ha corrido, marca éxito sin fallas y advierte con cada fuente caída y su fecha de inicio.
Relleno histórico¶
Un flujo recorre una vez sus dos páginas anteriores (
?paged=2y?paged=3en WordPress,?page=1y?page=2en Drupal), solo con una copia guardada y con su host sin freno ni enfriamiento. Un 404 es el fin de la historia; otra falla detiene el recorrido hasta la noche siguiente. Al terminar fijabackfilled_<id>en la configuración de la aplicación.El informe recorre el feed con un cursor (
backfill_page, que empieza en 2 y vale 0 al agotarse), hasta 63 páginas por ejecución. Una página ilegible tres veces seguidas (backfill_stalls) se salta, con una advertencia en el registro.El archivo de documentos guarda hasta 10 documentos por ejecución, del más nuevo al más antiguo, en la carpeta
informesde los datos de la aplicación. Rechaza un cuerpo de más de 8 MiB o que no empiece con%PDF; una fila sin documento conserva el enlace a la página del ISP y se reintenta la noche siguiente.
Las filas que agrega el relleno llegan al panel en la siguiente escritura de la fuente, que es la única salida de la tabla.
Taxonomía de errores¶
En la ruta OCS:
Código |
Significado |
Reintento |
Remediación del cliente |
|---|---|---|---|
200 con |
la fuente no se pudo leer y se sirve la última copia buena, o nada |
automático, en el servidor |
mostrar el aviso; nunca leer |
400 |
un parámetro llegó como arreglo, se envió |
no |
quitar el parámetro |
401 |
sin sesión |
no |
autenticarse |
404 |
|
no |
usar uno de los siete ids |
412 |
falta |
no |
enviar la cabecera |
Al leer una fuente, en el servidor:
Clase de falla |
Detección |
Efecto |
Recuperación |
|---|---|---|---|
Negativa del host |
respuesta 4xx o 5xx |
freno del host, de 2 a 24 horas |
la primera respuesta posterior que no sea una negativa; el sondeo diario consulta a través del freno |
Sin respuesta |
tiempo agotado (15 s), TLS, DNS |
advertencia en el registro; enfriamiento del host, 300 s |
automática |
Lectura sin elementos |
un 200 que no da elementos: reestructuración o página de WAF |
enfriamiento del host, 300 s; nada escrito |
automática; el sondeo cuenta el día |
Cuerpo demasiado grande |
más de 512 KiB |
se trata como una lectura fallida |
automática |
Falla de almacenamiento |
excepción del mapper |
advertencia en el registro; se sirve la lectura, sin degradar |
la siguiente escritura |
Documento del informe no guardable |
fetch fallido, más de 8 MiB o sin firma |
la fila conserva el enlace a la página del ISP; sin freno ni enfriamiento |
la noche siguiente |
Documento guardado alterado |
el sha256 no coincide con la fila |
advertencia en el registro; 303 a la página del ISP |
la administración revisa el archivo; la ruta sirve la página del ISP mientras tanto |
Concurrencia e invariantes¶
Cero elementos es una falla, nunca un resultado. Guardar una lista vacía reemplazaría todas las alertas reales por «no hay alertas vigentes», lo más peligroso que la aplicación podría mostrar.
Una fuente nunca leída está degradada y sin nada que servir: el panel muestra el aviso solo, nunca «Sin alertas», que es lo que dice una fuente que respondió sin elementos.
Reemplazo atómico de una fuente vigente.
AlertMapper::replaceSet()borra y vuelve a insertar el conjunto de una sola fuente en una transacción; un conjunto a medio escribir nunca es visible.Los flujos no usan bloqueo.
upsert()busca y luego escribe; dos lecturas que compiten por un elemento nuevo pierden una inserción contra la clave primaria(fuente, url), y la perdedora lo registra y sirve su lectura.El informe se lee de a uno.
refreshLocked()toma un bloqueo atómico (add()) de 1440 s en la caché de bloqueos; la solicitud que pierde sirve la copia que ya tiene. Lo que vuelve inofensiva una inserción concurrente es la clave primaria(year, se).Una revalidación por fuente. El marcador
revalidating-{id}(300 s) deduplica el encolado y se borra en unfinally; antes de leer,revalidate()vuelve a consultar el enfriamiento y el freno del host, porque otra fuente del mismo host pudo encontrar la caída después de encolarse el trabajo.Un solo escritor de los contadores de fallas. Solo
ProbeJob, a través deProbeRecord, escribe los contadores;WarmJobyRefreshJobno los tocan. Por eso la notificación y la comprobación de configuración pueden quedar incompletas, pero nunca contradecirse.Lo que sobrevive a la caché vive en la configuración de la aplicación. Los contadores, el cursor del informe y las marcas
backfilled_<id>están enIAppConfig, no en la caché: un vaciado de la caché no debe dar por sana una fuente caída ni pedir que se recorra otra vez una tabla que conserva la historia.La caché cambia de espacio de nombres con cualquier actualización de aplicaciones de la instancia (
docs/adr/0012-the-archive-is-a-cache-and-any-app-update-empties-it.md), así que la tabla es el respaldo durable deget(),probe()ysearch().La búsqueda nunca consulta la red. El proveedor de búsqueda lee la caché y, si está vacía, la tabla; un proveedor que consultara a un publicador haría esperar a toda la búsqueda de APS Conecta Gestión. La comprobación de configuración tampoco consulta la red.
Los PDF se leen de los enlaces, nunca se construyen. Los nombres de archivo de ningún publicador se pueden derivar de lo que identifica al documento: el ISP agrega un sufijo de versión cuando reemite una semana (
…SE35-08-09-2026-V3) y el MINSAL rellena dos de sushrefcon un espacio final dentro de las comillas. Cada intérprete lee lo que el publicador sirve y no lo que una regla predeciría; una reedición reemplaza su propia semana y nunca agrega una tercera fila.Identidad. Una alerta es
(fuente, url): el mismo PDF en dos sectores son dos filas. Un informe es(year, se): una reedición reemplaza su propia semana y nunca crea otra fila.El día pertenece a quien publica. La fecha de un elemento se calcula en la zona horaria del publicador (
America/Santiagopara el ISP), no en la del servidor ni en la del lector, y el año de una semana epidemiológica sale de esa fecha.El archivo se escribe antes de que la fila lo reclame, así que una fila que dice «guardado» siempre nombra bytes que existen.
Dominios de fallo¶
El host es el dominio de falla. Una negativa o un tiempo agotado de www.ispch.gob.cl detiene a la vez el informe y las tres fuentes de alertas del ISP; degreyd.minsal.cl, epi.minsal.cl y www.paho.org fallan cada uno por su cuenta.
El punto de vista importa. Desde un centro de datos, epi.minsal.cl responde con el 403 de Cloudflare y el ISP no responde (medido el 27 de septiembre de 2026): cuatro sectores muestran solo su aviso, y la notificación los nombra, junto con el informe, en los días 2, 7 y 30. Una instalación con conexión residencial lee los seis.
El certificado del ISP. www.ispch.gob.cl envía solo su certificado hoja. La aplicación trae el intermedio que falta (
certs/gsgccr6alphasslca2025.crt) yCertificateBundlelo une a las anclas de la instancia solo para las solicitudes al ISP, sin desactivar la verificación. Si la instancia no tiene un directorio temporal con escritura, la unión se omite y las cuatro fuentes del ISP se degradan.El WAF del ISP responde vacío a un User-Agent literal
curl/*, igual que una falla de TLS; la aplicación se identifica en cada solicitud, así que una reproducción concurlsin User-Agent no prueba que el host esté caído.Una lectura fría ocurre durante la carga de la página. La primera lectura de una fuente sin nada guardado se hace en línea, acotada a 15 s por fuente; las siguientes se encolan.
En la interfaz, un panel que falla no arrastra a los demás: cada panel tiene su propio límite de errores.
La búsqueda unificada no depende de los publicadores. Además,
appinfo/info.xmldeclara la extensiónintl, quefold()necesita, para que un host sin ella no pueda activar la aplicación y romper la búsqueda global.
Rotar el certificado del ISP¶
Si el ISP deja de responder, leer la cadena antes de suponer una caída. Esto muestra con qué firma hoy y dónde vive su intermedio:
openssl s_client -connect www.ispch.gob.cl:443 -servername www.ispch.gob.cl </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -ext authorityInfoAccess
Si cambió, hay dos arreglos. El de una instancia es importar el certificado de «CA Issuers» que muestra esa salida, con occ security:certificates:import. El durable es reemplazar certs/gsgccr6alphasslca2025.crt del repositorio por ese mismo certificado, conversión de DER incluida, como detalla certs/README.md. El 15 de septiembre de 2026 la hoja del ISP se reemitió bajo GlobalSign GCC R6 AlphaSSL CA 2025 mientras el almacén de confianza seguía con el de GlobalSign RSA OV SSL CA 2018: toda solicitud falló con cURL error 60 y la fuente quedó muerta sin que nada dijera por qué. Se midió en un host sin importación, el 16 de septiembre: cURL error 60 con solo las anclas del sistema y 200 con esas mismas anclas más ese único archivo. La verificación sigue activa en todo momento, porque un certificado en un CAfile es un ancla adicional, no una forma de evitar la verificación: nunca se usa verify=False.
Nada oculta la falla: la advertencia lleva el error de transporte, así que el código de cURL queda en nextcloud.log sin bajar el nivel de registro a debug.
Deuda técnica y límites¶
Los establecimientos reciben la versión que resuelve el instalador. La suite instala epidemiologia desde la release más nueva compatible con su APS Conecta Gestión, publicada y firmada en el repositorio propio de la aplicación (ADR-0017 de la suite); la versión queda registrada en el archivo de bloqueo del servidor. Esta página describe la versión de
main.Una actualización de
<version>deja la instancia en «requiere actualización» hasta que correocc upgrade;occ app:enablesobre una aplicación ya activa no registra los trabajos.La migración de la versión 0.11.0 reescribe la clave primaria de
epidemiologia_alerta(fuente, url), y APS Conecta Gestión no tiene migraciones de retroceso: el único retroceso es restaurar un respaldo de la base de datos tomado antes y reinstalar la versión 0.10.0.El certificado del ISP fija un emisor. El certificado hoja vence el 23 de noviembre de 2026 y nada detecta un cambio de emisor; si cambia, las cuatro fuentes del ISP fallan con
cURL error 60hasta reemplazar el archivo decerts/.Superficie de SSRF. La dirección del PDF de cada informe sale del feed y puede apuntar a cualquier host https; la protegen el cliente HTTP de la plataforma, que rechaza direcciones locales, el tiempo máximo, el tope de 8 MiB y la firma
%PDF. El modelo de amenazas no registra riesgos aceptados.Sin
#[CORS]en la ruta OCS, a la espera de un origen concreto.Nada propaga el establecimiento a la aplicación: lee
notify_groupsy sus propios contadores, y nada más.scripts/smoke.shyscripts/layout.shse ejecutan a mano, contra una instancia aprovisionada.Orden dentro del mes. La tabla no conserva el orden de las alertas de degreyd.minsal.cl dentro de un mes (no hay columna de secuencia y esa página no publica días); la caché sí lo conserva.
Topes sin medir o parciales. El tope de 8 MiB por documento nunca se midió contra el ISP; el tope de 512 KiB del feed acota la interpretación, no la descarga; la espera base del freno supone una cadencia horaria.
Drupal responde 200 vacío después de su última página, y el relleno lo lee como ilegible, no como agotado; hoy no ocurre porque la lista de la OPS/OMS tenía 38 páginas el 28 de septiembre de 2026 y el relleno recorre dos.
No construido: notificaciones al personal por alertas nuevas, facetas por enfermedad o por subtipo del ISP, deduplicación entre sectores, niveles de gravedad, las otras 21 categorías del ISP y cualquier forma de evadir un bloqueo.
Límites del dominio. El contexto no tiene palabra para una tasa local de enfermedad, porque ninguna fuente la produce, ni organiza nada por enfermedad: ninguna fuente publica a ese grano.
Trampas de la plataforma¶
occ app:enablesobre una aplicación ya activa no registra los trabajos en segundo plano. Sismoke.shinforma que falta uno, ejecutarocc app:disabley luegoocc app:enable.occ upgraderegistra todos: así la versión 0.11.0 agregóWarmJob. Después de habilitar,occ background-job:listdebe mostrar exactamente tres filas:ProbeJob,RefreshJobyWarmJob.La sensibilidad de un trabajo no se cambia en el lugar.
oc_jobs.time_sensitiveparte en 1, la primera ejecución de un trabajoTIME_INSENSITIVEescribe 0, nada vuelve a escribir 1 y una actualización no lo restablece.occ background-job:listtampoco imprime el intervalo: leeroc_jobso el campo «Next execution» deocc background-job:execute <id>(APS Conecta Gestión 34, 28 de septiembre de 2026).El bundle debe emitir una sola hoja de estilos. La configuración por omisión de Vite la divide en un archivo mínimo que importa con
@importun fragmento con hash en el nombre; el parámetro?v=de APS Conecta Gestión no alcanza ese@import, de modo que un archivo mínimo en caché sigue importando un fragmento que la compilación siguiente borró: estilos viejos o ninguno, sin aviso.build.cssCodeSplit: falselo evita.El tema no se puede cambiar, y la plataforma no lo dice. Con
enforce_theme=light,occ user:setting <usuario> theming enabled-themes '["dark"]'termina bien y se lee de vuelta, pero no cambia nada:ThemesService::getEnabledThemes()antepone el tema forzado. Forzardata-theme-darken el DOM cambia el área de contenido y no la navegación, lo que parece un tema oscuro roto.opcache.revalidate_freqes de 60 s, así que un cambio de PHP puede tardar un minuto en verse.Agregar o cambiar rutas por atributos exige reiniciar el contenedor.
CachingRouterguarda las rutas compiladas durante una hora en APCu, que es propio de cada SAPI: limpiarlo desdeoccno hace nada, porque la CLI y la web no lo comparten. El síntoma es998 Invalid querydesde OCS mientrasocclista la ruta.No encadenar
docker exec … | grep -q.grepcierra la tubería en la primera coincidencia, el que escribe muere por SIGPIPE y, conpipefail, una verificación que pasa se informa como falla.Subir
<version>enappinfo/info.xmldeja toda la instancia en «requiere actualización»:occpasa a modo limitado y las rutas web dejan de responder hasta que correocc upgrade.
Pruebas y verificación manual¶
scripts/test.shejecuta la batería unitaria dentro del contenedor (PHPUnit 11);npm test, la lógica del frontend (Vitest);scripts/smoke.sh, el cableado contra la instancia en marcha;scripts/layout.sh, la disposición en un navegador real a tres anchos;scripts/openapi.sh, la regeneración deopenapi.json.La integración continua (
.github/workflows/ci.yml) corre sobremainy los pull requests, no en cada push: la batería unitaria en la misma imagennextcloud:34que usa la instancia, más un trabajo de bundle que ejecutanpm test, recompilasrc/y exige quejs/ycss/no cambien.scripts/layout.shmaneja un Chromium sin interfaz a 360, 768 y 1366 px, tras ejecutar una veznpm install && npx playwright install chromium. Los 360 px solo se alcanzan así: la ventana de un navegador real en una máquina de desarrollo no se estrecha tanto.Sin una instancia,
scripts/layout.mjsaún ejecuta las verificaciones 6, 10, 11 y 12 contra el bundle compilado: servir, en 127.0.0.1, una página en/apps/epidemiologia/con un#initial-state-epidemiologia-sourcessintético (JSON en base64 que incluya una fuente ausente, una degradada sin nada, una que respondió sin nada y una degradada con última copia) y un#epidemiologiade montaje junto a losjs/ycss/compilados, y ejecutarEPI_TOKEN=x node scripts/layout.mjs http://127.0.0.1:<puerto> admin. La verificación 7 y el ancho del informe a 360 px necesitan el CSS propio de APS Conecta Gestión, así que se juzgan solo en una instancia.smoke.shylayout.shquedan manuales a propósito: sus aserciones necesitan una instancia aprovisionada con la aplicación habilitada, y su valor es justamente correr contra una real. Ejecutar ambos antes de cada versión.Los fixtures son respuestas reales capturadas del MINSAL, el ISP y la OPS/OMS, de modo que los intérpretes se prueban contra lo que esos sitios publican de verdad, rarezas incluidas.