# DataHub

`DataHub` es el módulo de datos por cliente y explotación. La clave funcional es:

En la navegación aparece dentro de `Salud y datos`, junto a Vetiquín y Calidad lechera, porque su función principal es conservar y explorar datos técnicos por explotación; los enlaces con Clientes y Explotaciones permanecen intactos.

```text
customer_id + farm_id + REGA + source_id + entity_type + subject_type + animal_identifier
```

## Carcasa Y Navegación

La cabecera común muestra el estado del almacén, mantiene `Sincronizar` como acción principal en `Prioridades` e `Histórico` y la conserva como secundaria en `Explorador`, donde `Consultar` es la única acción principal. Actualización y documentación permanecen en `Más`. Si una consulta falla, el formulario sale del estado de carga, vuelve a habilitar `Consultar` y muestra un error recuperable para reintentar. `Prioridades`, `Explorador` e `Histórico` son las tres vistas funcionales del módulo.

`Prioridades` usa `master-detail`: la comparativa técnica y el directorio REGA forman el índice, mientras la explotación seleccionada abre `Ficha` con sus acciones rápidas. Con cero explotaciones aparece una explicación breve con acceso directo a sincronizar o abrir el explorador, en vez de una superficie vacía. El explorador usa `editor-list`: deja visibles explotación, búsqueda y consultar; origen, sujeto, crotal, tipo de dato, indicador y fechas se conservan en `Filtros avanzados`. Histórico mantiene su comportamiento. Cada pestaña conserva sus endpoints.

`subject_type` distingue datos de `farm` y `animal`. En los datos de vaca, `animal_identifier` guarda el crotal visible y `animal_id` puede conservar el identificador interno del proveedor. LIGAL aporta actualmente datos de tanque a nivel de explotación; CEGCOL, visitas y tratamientos pueden aportar registros por vaca.

El catálogo visible contempla LIGAL, CEGCOL, sala de ordeño, visitas técnicas, meteorología y medicamentos/tratamientos. Una fuente puede aparecer como disponible, pendiente de credenciales o sin conector. Sala de ordeño está disponible mediante importación de archivos AFIMILK; no simula una API ni requiere credenciales del proveedor. Mostrar una fuente no implica inventar datos: la tarjeta indica cuántos registros, vacas, métricas y elementos de catálogo hay realmente almacenados.

## Integración con otros módulos

El almacén no debe copiar datos clínicos, facturación ni informes. Su responsabilidad es mantener datos externos materializados por explotación y exponerlos con claves comunes:

- `customer_id` para enlazar con ficha 360 y facturación;
- `farm_id` y `REGA` para enlazar con SIGE, informes y visitas;
- `source_id`, `entity_type`, `metric_key` y `period_key` para que otros módulos consuman resultados sin conocer la API original.

Cuando otro módulo necesite una relación explícita, debe usar `module_links` y apuntar al registro o métrica que corresponda. Ejemplo: una revisión clínica puede enlazar una acción correctiva con una explotación que el DataHub marca con células altas, sin duplicar el histórico LIGAL.

## Endpoints internos

```http
GET /api/datahub
Authorization: Bearer {token}
```

Devuelve el resumen del almacén, fuentes, explotaciones con REGA, estado de sincronización, contadores de registros y métricas. Cada explotación incluye una ventana suficiente de `metrics` para que la UI y otros módulos puedan calcular prioridad técnica sin abrir primero la ficha completa.

```http
GET /api/datahub/farms/{farm_id}
Authorization: Bearer {token}
```

Devuelve la ficha de datos de una explotación: métricas calculadas, registros recientes, analíticas estructuradas recientes y ejecuciones de sincronización.

```http
GET /api/datahub/query?farmId={farm_id}&sourceId=cegcol&subjectType=animal&animalIdentifier=ES123&q=texto
Authorization: Bearer {token}
```

Consulta manual de datos ya guardados. Filtros soportados:

| Filtro | Uso |
|---|---|
| `farmId` | Limita por explotación interna. |
| `sourceId` | Limita por fuente. Ejemplo: `ligal`. |
| `subjectType` | Limita a datos de `farm` o `animal`. |
| `animalIdentifier` / `crotal` | Busca por crotal, identificador interno o nombre de la vaca. |
| `entityType` | Limita registros. Ejemplo: `ligal_customer`, `ligal_average`, `ligal_average_result`, `ligal_sample`, `ligal_sample_result`, `ligal_test`. |
| `metricKey` | Limita métricas. Ejemplo: `ligal.average.i00001`, `ligal.sample.i00001` o `ligal.samples_lookback`. |
| `dateFrom` / `dateTo` | Rango de fechas o periodo. Normaliza ISO, `DD/MM/YYYY`, `YYYYMM` y `YYYY-MM`; los periodos mensuales se comparan por solape con todo el mes. |
| `q` | Búsqueda en código externo, identificador y payload normalizado. |
| `limit` | Máximo de filas, hasta 300. |

La respuesta incluye nombres de cliente y explotación, REGA, sujeto, crotal y payload normalizado en cada registro. También devuelve `records`, `metrics`, `analytics`, `matched`, `truncated`, `options` y `charts` con:

- `charts.recordTypes`: distribución por tipo de dato;
- `charts.recordsByPeriod`: conteo por periodo;
- `charts.metricValues`: valores de métricas para pintar gráficas.
- `analytics`: histórico estructurado de determinaciones, con ensayo, valor, unidad, atributo, periodo y `parameters` con metadatos LIGAL completos.
- `matched`: total real de registros, métricas, analíticas y vacas que cumplen los filtros, aunque la respuesta esté limitada.
- `options.sources`: catálogo de fuentes con estado y contadores reales.
- `options.animals`: crotales disponibles para la explotación y fuente elegidas.
- cada elemento de `metrics` incluye `inputs`, el JSON de entradas ya parseado. En los índices de salud de ubre permite inspeccionar de forma segura numerador, denominador, criterio, cobertura y cohorte sin exponer el JSON interno como texto opaco.

Si no se indica `farmId`, la consulta se limita a explotaciones activas en `client_data_farm_links`, para no mezclar datos históricos de fichas archivadas o duplicadas.

Permisos globales: `GET /api/datahub`, `GET /api/datahub/query` y `GET /api/datahub/farms/{farm_id}` aceptan `datahub.read` o `datahub.write`; la sincronización manual requiere `datahub.write`.

```http
POST /api/datahub/sync
Authorization: Bearer {token}
Content-Type: application/json
```

Payload:

```json
{
  "farmId": "farm-...",
  "force": true
}
```

Si `force` es `false`, solo sincroniza explotaciones vencidas según `sync_interval_hours`. Por defecto son 24 horas.

```http
POST /api/datahub/import/afimilk
Authorization: Bearer {token}
Content-Type: application/json
```

Analiza o importa un archivo delimitado exportado por AFIMILK. Requiere
`datahub.write`. AFIMILK permite elegir protocolos y columnas, por lo que
AnimalCharlie no presupone un esquema fijo:

1. `action=preview` recibe `filename` y `data` como data URL base64. Detecta
   CSV, TSV o texto delimitado y devuelve cabeceras, separador, número de filas
   y un mapeo sugerido, pero no escribe datos.
2. Un segundo `action=preview` con `mapping` valida la correspondencia elegida y
   devuelve filas válidas y descartadas, días, vacas y un `previewId`. Son
   obligatorias las columnas `animal`, `date` y `milk`, además de
   `dateFormat=dmy|mdy|ymd`; `time` y `name` son opcionales. Vaca, fecha, leche
   y hora no pueden reutilizar la misma columna.
3. `action=import` añade `farmId`, el mismo `mapping` y el `previewId`. Si la
   validación mostró descartes, requiere también
   `acceptRejectedRows=true`: confirmar sin haber visto el resultado o cambiar
   archivo/correspondencia obliga a validar de nuevo.

El formulario de `Explorador > Importar AFIMILK` ejecuta ambos pasos. La
explotación se elige de forma explícita entre fichas activas con REGA; nunca se
deduce por nombre ni por un identificador aproximado del archivo. Si existe
hora —en columna propia o dentro de la fecha—, las sesiones únicas se suman por
vaca y día. Sin hora, cada fila se trata como total diario y, si se repite vaca
fecha, prevalece la última. Un mismo grupo vaca/día no puede mezclar totales
diarios con sesiones horarias: sus filas se descartan en la validación para no
duplicar producción. El orden de día, mes y año siempre se selecciona
de forma explícita; acepta `/`, `-` o `.` y exige año de cuatro cifras. Los kg
aceptan punto o coma decimal. Las filas con distinto número de celdas se
descartan sin truncarlas. Una cabecera o celda que declare una unidad distinta
de kg se rechaza; `Milk volume (kg)` sigue siendo válido. Esta integración no
aplica conversiones silenciosas de volumen, peso imperial ni otras unidades.

Cada resultado se guarda como `milking_parlor_daily_yield` con
`source_id='milking_parlor'`, `integration='afimilk'`,
`subject_type='animal'` y una identidad estable por explotación, clave de vaca
canónica y fecha. La clave elimina puntuación y diferencias de mayúsculas como
el enlazado de Calidad, mientras el identificador original se conserva en el
registro visible. Reimportar el mismo día corrige esa identidad; no elimina días
anteriores ni registros ausentes del archivo. La ejecución queda en
`client_data_sync_runs` con `mode='file_import'` y se audita con nombre y SHA-256
del archivo. El binario original y sus filas crudas no se almacenan.

## Automatización diaria

El servidor arranca `start_datahub_scheduler()` al iniciar la aplicación. Ese proceso crea el hilo `datahub-daily-sync`, espera unos segundos para no bloquear el arranque y después revisa el almacén cada hora. La revisión horaria no fuerza llamadas externas: ejecuta `datahub_sync_due_farms(force=false, mode="daily")`, por lo que solo trae datos de explotaciones activas cuyo `next_sync_at` ya ha vencido. Cuando existe un `next_sync_at` válido, esa fecha es autoritativa aunque el último éxito sea antiguo; así una ejecución `empty` no provoca reintentos casi horarios antes de su próxima cita.

Las rutas de lectura de Clientes, Granjas y DataHub no ejecutan
`refresh_datahub_farm_links()`: consultan el estado materializado sin abrir una
escritura SQLite. La reconciliación de enlaces queda limitada a la
sincronización y al alta automática LIGAL, ambas serializadas. Esto evita que la
carga paralela de la interfaz compita con una importación CEGCOL larga y deje el
último intento en `database is locked`.

Antes de seleccionar las explotaciones vencidas, LIGAL ejecuta un descubrimiento de clientes vigentes. Cada registro nuevo con REGA crea automáticamente la explotación en AnimalCharlie con `customer_farms.ligal_enabled=1`. La deduplicación usa primero REGA para la explotación y después NIF/CIF para enlazarla a un cliente existente; si LIGAL no aporta identificador fiscal, solo se reutiliza un cliente con nombre exactamente igual. Si tampoco existe, se crea un cliente activo sin usuario de portal. Los registros inactivos o sin REGA se omiten. El alta queda auditada con origen `ligal`, crea el enlace del Almacén Lechero y entra en la misma sincronización. Se puede desactivar excepcionalmente con `DATAHUB_LIGAL_AUTO_PROVISION=0`.

El intervalo real se guarda en base de datos:

- `client_data_sources.sync_interval_hours` define el intervalo por proveedor. Para LIGAL se inicializa con `DATAHUB_SYNC_INTERVAL_HOURS`, por defecto `24`.
- `client_data_farm_links.sync_interval_hours` guarda el intervalo por explotación y fuente.
- `client_data_farm_links.next_sync_at` marca cuándo vuelve a tocar esa explotación.
- `client_data_sync_runs` conserva cada ejecución con `mode`, `status`, `records_upserted`, `metrics_upserted`, `started_at`, `finished_at`, `next_due_at` y `detail_json`.

Así, los muestreos `ligal_sample`, sus referencias de informe `ligal_sample_report`, resultados `ligal_sample_result`, métricas `ligal.sample.*` y medias se almacenan en SQLite una vez al día por explotación activa. Una sincronización manual con `force=true` reimporta el histórico y recalcula `next_sync_at`, pero la rutina automática mantiene la cadencia diaria. Las fichas de cliente y explotación muestran además cuántos informes declara LIGAL para cada muestra; el fichero solo se ofrecerá cuando el endpoint remoto de descarga responda correctamente.

Una autenticación LIGAL válida no garantiza que la cuenta ya tenga explotaciones provisionadas. Si la búsqueda por REGA devuelve cero clientes, la sincronización registra `emptyReason=no_ligal_customer_match`, conserva los registros, métricas y analíticas existentes, mantiene intacto `last_success_at` y deja en el resumen `staleDataPreserved` junto con los contadores `preservedRecords`, `preservedMetrics` y `preservedAnalytics`. Esta protección permite dejar preparada una cuenta nueva todavía en trámite sin perder el histórico importado por otra cuenta activa.

## Explorador visual

La pantalla `#almacen-datos` abre por defecto un explorador genérico, no una vista cerrada por proveedor. Permite consultar a mano:

- explotación con REGA;
- fuente de datos;
- sujeto `Explotación` o `Vaca`;
- crotal;
- tipo de dato guardado;
- métrica calculada;
- rango de fechas;
- texto libre sobre identificadores, REGA y payload normalizado.

Los resultados se pueden alternar entre `Registros`, `Métricas` y `Analíticas`. La tabla mantiene columnas comunes para cualquier origen: fecha, explotación/REGA, sujeto, fuente, tipo y valor. Al inspeccionar una fila, el panel lateral muestra sus metadatos y el JSON completo normalizado. De este modo se pueden probar fuentes nuevas sin programar una pantalla específica para cada una.

El catálogo superior diferencia claramente fuentes vacías de fuentes todavía no conectadas. `Sala de ordeño · AFIMILK` aparece como disponible porque acepta archivos exportados; `Meteorología` permanece como `Sin conector`. Los registros importados aparecen automáticamente en la misma tabla y en los filtros.

`Explotaciones > Calidad` consume los registros individuales de sala con `source_id='milking_parlor'`, `subject_type='animal'` y un `animal_identifier` o `animal_id` enlazable. La importación AFIMILK aporta totales diarios `milkKg`; el backend calcula la media de los siete días naturales terminados en la última fecha disponible. Otros adaptadores compatibles pueden aportar la media explícita en `fields`, `metrics` o raíz mediante `milk7dAverage`, `milk_7d_average`, `averageMilk7Days`, `avgMilk7d`, `milkAverage7d`, `milkKg7dAverage`, `production7dAverage`, `kgdia7d`, `lecheMedia7Dias`, `mediaLeche7Dias`, `averageDailyMilk` o `dailyMilkAverage`, o registros diarios con `milkKg`, `kgdia`, `dailyMilk`, `milkDaily`, `production`, `yield` o `leche`. Si no existen identificadores enlazables o datos válidos, la vista devuelve `available=false`; nunca sustituye estos datos por producción CEGCOL o por un informe técnico.

La UI pinta un tablero de prioridad técnica por explotación antes del detalle. Ordena las explotaciones por riesgo operativo y muestra índice, estado, células, bacterias, crioscopía, urea y la primera acción recomendada. Al pulsar una fila abre la ficha de esa explotación.

En la ficha de explotación, el primer bloque técnico muestra `Evolución de calidad` con una línea de células y otra de bacterias cuando hay al menos dos periodos comparables. El SVG se puede pulsar o abrir con teclado para ver un modal ampliado con tooltip nativo por punto. Si todavía no hay histórico suficiente, el bloque queda visible con un estado vacío para que el usuario entienda que faltan datos y no que el módulo ha dejado de cargar.

La ficha 360 de cliente también consume el almacén y muestra una vista simple `LIGAL · Últimas 3 analíticas`. Esa vista se alimenta desde `client_data_records` filtrando registros activos `ligal_average` o `ligal_sample` de las explotaciones activas del cliente.

El `Portal AnimalCharlie` (`/portal`) incorpora la vista `Calidad lechera`; `/customer/{token}` conserva la antigua vista `DataHub` solo para compatibilidad. El backend añade `milkWarehouse` al payload unificado con datos filtrados por `users.client_id`, explotaciones LIGAL activas y métricas públicas ya normalizadas. El cliente puede ver:

- selector de explotación con REGA, estado de sincronización y semáforo de calidad;
- índice de calidad, estado operativo, número de muestras y última muestra;
- tarjetas de células, bacterias, inhibidores, crioscopía, grasa, proteína, urea y ratio grasa/proteína;
- gráficas SVG de evolución sin dependencias externas;
- histórico mensual y tabla de muestreos recientes;
- prioridades redactadas en lenguaje operativo, sin exponer credenciales, payloads crudos ni tablas técnicas.

La UI pinta tres gráficas simples sin dependencias externas:

- tipos de dato;
- registros por periodo;
- métricas numéricas.

También muestra listas inspeccionables de registros y métricas para revisar los datos sincronizados.

La ficha de cada explotación incluye una vista técnica pensada para calidad de leche. A partir de las métricas guardadas por REGA muestra:

- índice técnico orientativo de la explotación;
- prioridades de visita;
- cobertura de datos, clientes LIGAL vinculados, medias, muestras y no conformidades;
- tarjetas con evolución de células, bacterias, inhibidores, crioscopía, grasa, proteína, sólidos, urea y ratio grasa/proteína;
- plan técnico sugerido para orientar la visita;
- tabla mensual comparativa para revisar cambios de composición, higiene y nutrición.
- tabla de muestreos por fecha para revisar los datos casi diarios que proceden de muestras individuales o de determinaciones embebidas en las medias.

Los rangos usados por la UI son criterios operativos de lectura técnica, no sustituyen límites contractuales ni normativa de cliente:

| Indicador | Lectura operativa |
|---|---|
| Células | Bueno por debajo de 200 cel x1000/ml; vigilancia hasta 300; prioridad sanitaria por encima. |
| Bacterias | Bueno por debajo de 50 ufc x1000/ml; vigilancia hasta 100; prioridad de higiene por encima. |
| Inhibidores | Deben estar ausentes. |
| Crioscopía | Bueno en torno a valores iguales o menores que -520 mºC; alerta si se aproxima a valores menos negativos. |
| Urea | Rango operativo 180-320 mg/l. Fuera de rango se marca como revisión nutricional. |
| Ratio grasa/proteína | Rango operativo 1,10-1,50 para seguimiento metabólico. |

En LIGAL, las analíticas visibles de leche pueden venir dentro de `ligal_average` como campos dinámicos de ensayo (`_I00001`, `_I00002`, etc.). El almacén las normaliza en `payload.results`, guarda cada determinación como `ligal_average_result` y además las convierte en métricas `ligal.average.{codigo_ensayo}` por periodo para que aparezcan en tarjetas, tabla histórica y gráfica.

Los muestreos se guardan como `ligal_sample`. Pueden venir de `ligal.samples.search` o embebidos dentro de las medias mediante `MEDIAS_DETALLE`, `MEDIAS_DETALLE_DETERMINACIONES`, `DETERMINACIONES` y `MUESTRAS`. La sincronización consulta todos los tipos publicados y usa su `DefaultDateFilterType`: `I0` (`Pago por calidade`) usa `4`; `01`-`05` usan `2` en la cuenta probada. En ambos casos se deduplican por código y se guardan todas sus determinaciones como `ligal_sample_result` y `client_data_analytics`. La proyección `ligal.sample.{codigo_ensayo}` es una serie diaria determinista: si coinciden varias muestras o resultados del mismo ensayo y fecha, conserva los originales en analíticas y guarda en la métrica la media aritmética de los valores numéricos —o los textos distintos ordenados— junto con `sampleCount`, `resultCount` y los códigos de muestra usados. La reparación local reconstruye una sola vez las métricas antiguas que todavía no declaren esta agregación.

`04` (`Sanidad Animal`) aporta bacteriología y mamitis individual. Cuando el material es `000006` o aparecen los ensayos nucleares `000036` —RCS—, `000037` —identificación— o `001175` —N+L—, el almacén también materializa:

- `ligal_mastitis_sample`, enlazada a una vaca cuando el nombre CEGCOL es único;
- `ligal_mastitis_sample_result`, preservando resultados repetidos del mismo ensayo;
- `ligal_mastitis_report`, con descarga PDF mediada por backend.

`SampleDate=1900-01-01` se descarta como sentinel. La fecha efectiva usa recepción y después análisis. Las referencias ambiguas o sin nombre CEGCOL reciben una identidad `LIGAL-NAME-*` no resuelta; no se asigna un crotal aproximado.

## Tablas

| Tabla | Uso |
|---|---|
| `client_data_sources` | Catálogo de proveedores del almacén. Ejemplo: `ligal`. |
| `client_data_farm_links` | Relación diaria entre cliente, explotación, REGA, fuente y estado de sincronización. |
| `client_data_sync_runs` | Histórico de ejecuciones, registros procesados, métricas calculadas y errores. |
| `client_data_records` | Registros normalizados por fuente y tipo. Guarda payload normalizado JSON y hash. |
| `client_data_metrics` | Valores calculados por explotación, periodo y fuente. |
| `client_data_analytics` | Histórico estructurado de analíticas por explotación, ensayo, periodo, valor, unidad y parámetros LIGAL. |
| `client_data_catalog_items` | Catálogos globales por fuente: capacidades y ensayos LIGAL, REGA observados por CEGCOL y metadatos de actualización. |

## Sincronización LIGAL

La sincronización descubre primero los clientes vigentes publicados por LIGAL y da de alta los REGA que todavía no existen. Después parte del `REGA` de las explotaciones con `customer_farms.ligal_enabled=1`. El check se gestiona desde `Clientes > Explotaciones`: activarlo lanza una primera sincronización; desactivarlo conserva el histórico y desactiva el enlace para que no entren datos nuevos. Una sincronización manual o forzada reimporta el histórico desde `DATAHUB_LIGAL_HISTORY_START_DATE` y poda los registros, métricas y analíticas LIGAL previos de esa explotación antes de volver a guardar lo que devuelve la API. La sincronización diaria mantiene una ventana más corta con `DATAHUB_LIGAL_LOOKBACK_DAYS`.

Flujo:

1. Buscar cliente LIGAL por `ligal.customers.search` con filtro `rega`.
2. Guardar coincidencias como `ligal_customer`.
3. Consultar `ligal.sample_types.search`. Si existe `DATAHUB_LIGAL_SAMPLE_TYPES`, seleccionar esos códigos; en caso contrario, usar todos los tipos publicados con su `DefaultDateFilterType`.
4. Para cada código de cliente LIGAL encontrado, consultar:
   - `ligal.samples.search` con cada tipo de muestra, su filtro de fecha predeterminado y la ventana histórica o diaria.
   - `ligal.averages.search` con la misma ventana.
5. Extraer de las medias cualquier muestra embebida en `MEDIAS_DETALLE`/`DETERMINACIONES` y mezclarla con las muestras directas.
6. Guardar registros como `ligal_sample`, `ligal_sample_type` y `ligal_average`; para sanidad animal, crear además las entidades `ligal_mastitis_*` por vaca.
7. Consultar la ficha de cada ensayo encontrado con `ligal.tests.get` y guardarla como `ligal_test`.
8. Extraer resultados analíticos embebidos en las medias (`_I00001` grasa, `_I00002` proteína, `_I00005` bacterias, `_I00006` células, etc.) y en las determinaciones de muestras.
9. Guardar el catálogo de capacidades y ensayos en `client_data_catalog_items`.
10. Guardar cada resultado como registro propio `ligal_average_result` o `ligal_sample_result`.
11. Guardar cada resultado en `client_data_analytics` con todos sus parámetros (`customer`, `relatedCustomer`, `relation`, `test`, `result`, filtros de consulta y metadatos de origen).
12. Calcular métricas operativas y analíticas:
   - `ligal.customer_matches`
   - `ligal.samples_lookback`
   - `ligal.averages_lookback`
   - `ligal.average_results`
   - `ligal.sample_results`
   - `ligal.average.{codigo_ensayo}`
   - `ligal.sample.{codigo_ensayo}`
   - `ligal.non_conformity_averages`
   - `ligal.latest_sample_date`
   - `ligal.mastitis.samples`
   - `ligal.mastitis.matched`
   - `ligal.mastitis.ambiguous`
   - `ligal.mastitis.not_found`
   - `ligal.mastitis.pathogens`
   - `ligal.mastitis.contaminated`
   - `ligal.mastitis.resistant_samples`

Si el `REGA` no devuelve ningún cliente LIGAL, la ejecución queda en estado `empty` y conserva el histórico existente, marcándolo como potencialmente obsoleto. No interpreta una cuenta nueva todavía sin asociaciones como una orden de borrado.

## Materialización CEGCOL y datos locales

Toda consulta CEGCOL correcta cataloga primero cada REGA observado en
`client_data_catalog_items` con `catalog_type='cegcol_rega'`, aunque todavía no
exista una explotación vinculada. El catálogo deduplica por REGA, conserva la
última fecha de control y análisis, registra capacidades y operaciones de origen
y alimenta las sugerencias del alta de explotaciones. Después, el registro
técnico solo se materializa en `client_data_records` cuando el REGA coincide con
una explotación activa que tenga `customer_farms.cegcol_enabled=1`. El tipo
normalizado de la operación se conserva como `entity_type`; si la respuesta
contiene `crotal`, `dib` o `identificacion`, el registro queda como
`subject_type='animal'` y pasa a colgar de ese crotal en el explorador. La
respuesta de Integraciones incluye un bloque `warehouse` con registros
guardados, REGA catalogados, animales detectados y filas sin REGA habilitado.

`GET /api/client-farms/rega-suggestions?q=&limit=8` requiere `clients.read` y
devuelve primero los REGA no asignados a explotaciones activas, ordenados por su
fecha disponible más reciente. La primera lectura vencida refresca de forma
controlada los últimos 30 días mediante `getDatosUltControl`; el marcador
`cegcol_rega_refresh` evita repetir la llamada durante
`DATAHUB_CEGCOL_REGA_REFRESH_HOURS`, seis horas por defecto, incluso cuando la
consulta devuelve cero filas. Si CEGCOL falla, el mismo marcador impide repetir
la petición en cada pulsación y aplica una espera corta controlada por
`DATAHUB_CEGCOL_REGA_RETRY_MINUTES`. Las consultas CEGCOL posteriores siguen
enriqueciendo el mismo catálogo.

`refresh_datahub_farm_links()` crea un enlace `source_id='cegcol'` solo para las
explotaciones marcadas. La sincronización diaria consulta una ventana máxima de 30
días, el censo activo, lactaciones en curso, inseminaciones, partos y lactaciones
finalizadas; cuando existe fecha de último control añade controles, acumulados y
laboratorio extendido. Una sincronización manual divide los últimos 400 días en
ventanas válidas de 31 días, obtiene todas las fechas de control y materializa
`getControles` para construir comparaciones consecutivas por vaca. Desmarcar
CEGCOL desactiva el enlace sin borrar registros históricos.

El censo activo se trata como snapshot: tras una respuesta válida se eliminan de
`cegcol_census_animal` las vacas que ya no aparecen, sin tocar su histórico de
controles, partos o lactaciones. Al arrancar o abrir DataHub también se repara de
forma idempotente un snapshot previo si la última ejecución demuestra que todos
los elementos del censo fueron vistos. Si la ventana diaria no contiene un
control nuevo, `cegcol.latest_control_date` y `cegcol.controls.latest` conservan
el último control materializado en vez de sustituirlo por fecha vacía y cero.

Al abrir el DataHub también se materializan datos locales vinculables:

- informes de Calidad lechera con esquema `milk-quality-cmt/v1`, siempre que su REGA coincida con una explotación;
- observaciones CMT por vaca como `technical_visit_animal`;
- medicamentos y tratamientos de Vetiquín vinculados por `farm_id` a una explotación con REGA;
- tratamientos por vaca cuando sus metadatos incluyen `crotal`, `animalIdentifier` o `earTag`.

Los informes sin REGA y las recetas sin explotación permanecen en su módulo original y no se asignan por semejanza de nombres, evitando mezclar explotaciones.

Variables de entorno:

| Variable | Valor por defecto |
|---|---|
| `DATAHUB_SYNC_INTERVAL_HOURS` | `24` |
| `DATAHUB_LIGAL_LOOKBACK_DAYS` | `365` |
| `DATAHUB_CEGCOL_LOOKBACK_DAYS` | `30` |
| `DATAHUB_CEGCOL_CONTROL_HISTORY_DAYS` | `400`; histórico de controles individuales y lactaciones finalizadas que reimporta una sincronización manual. |
| `DATAHUB_CEGCOL_REGA_REFRESH_HOURS` | `6`; vigencia del refresco de REGA recientes usado por Clientes. |
| `DATAHUB_CEGCOL_REGA_RETRY_MINUTES` | `15`; espera antes de reintentar un refresco CEGCOL fallido. |
| `DATAHUB_AFIMILK_MAX_BYTES` | `4194304` (4 MiB); tamaño máximo decodificado de una exportación AFIMILK. |
| `DATAHUB_AFIMILK_MAX_ROWS` | `25000`; máximo de filas no vacías por archivo. |
| `DATAHUB_LIGAL_HISTORY_START_DATE` | `2018-01-01` |
| `DATAHUB_LIGAL_SAMPLE_TYPE` | Sin valor; si se configura se usa como compatibilidad con un único código. |
| `DATAHUB_LIGAL_SAMPLE_TYPES` | Sin valor; si se configura admite varios códigos separados por coma. Si no hay configuración, la sincronización usa todos los tipos devueltos por `/api/SampleTypes` y respeta `DefaultDateFilterType`. |
| `DATAHUB_LIGAL_PAGE_SIZE` | `200` |

Las lactaciones finalizadas CEGCOL usan una identidad compuesta por DIB, parto,
fecha de secado y número de parto. Cuando una sincronización encuentra una fila
legada identificada únicamente por DIB, la elimina dentro de la misma transacción
y registra el total en `warehouse.legacyPruned`, evitando duplicados físicos sin
perder ninguna lactación histórica.

## Registro de fuentes (adaptadores)

El motor de sincronización es agnóstico de fuente. Las fuentes se declaran en `DATAHUB_SOURCE_ADAPTERS` (`server.py`), que el seed de `client_data_sources`, `refresh_datahub_farm_links` y `datahub_sync_due_farms` recorren. LIGAL y CEGCOL tienen adaptador programado; AFIMILK usa un volcado manual autenticado; las fuentes locales se materializan desde AnimalCharlie. `datahub_sync_due_farms` no aborta si una fuente concreta no está configurada: ejecuta las que tienen adaptador y credenciales, despachando por `source_id`. Los enlaces de fuentes por archivo no vencen ni se envían al planificador.

Cada entrada del registro declara:

| Campo | Uso |
|---|---|
| `label`, `provider`, `integration` | Metadatos que se vuelcan en `client_data_sources`. |
| `configured` | Nombre de una función a nivel módulo que devuelve si la fuente está lista (`None` = siempre lista, p. ej. fuentes que vuelcan de tablas locales). |
| `sync` | Nombre del método `(farm, mode, user, full_history) -> dict` que materializa los datos de esa fuente. |
| `capabilities` | Nombre opcional de la lista de capacidades a publicar. |
| `creates_links` | Si la fuente genera sus propios `client_data_farm_links`. |

Añadir una fuente nueva = registrar su adaptador + escribir su método de volcado (que debe seguir las reglas de la sección «Cómo deben escribir otras integraciones»). No requiere tocar el orquestador ni la UI: las métricas y registros nuevos aparecen por su `source_id`/`metric_label`.

## Métricas cruzadas (`derived.*`)

Tras cada sincronización correcta de una explotación, `datahub_sync_due_farms` llama a `datahub_compute_derived_metrics(conn, farm)`. Ese motor recorre el catálogo declarativo `DATAHUB_DERIVED_METRICS` (`server.py`) y persiste los resultados en `client_data_metrics` bajo `source_id='derived'`, sin tocar las métricas de las fuentes originales.

Cada definición declara las fuentes que necesita (`requires`). Si una explotación no tiene datos de alguna fuente requerida, la métrica queda **inerte**: se guarda un placeholder `current` con `value_text='pending'` (visible en la UI como «Pendiente de fuente») y se activa sola cuando se vuelque esa integración. Si la fuente ya existe pero falta una entrada concreta, se guarda `value_text='unavailable'` con `inputs.reason`: por ejemplo, `missing_cost` muestra «Pendiente de coste» y `missing_census` muestra «Pendiente de censo», sin afirmar erróneamente que falta Vetiquín. Las métricas se recalculan desde cero en cada pasada para no dejar periodos obsoletos.

| `metric_key` | Requiere | Cálculo | Estado hoy |
|---|---|---|---|
| `derived.fat_protein_ratio` | `ligal` | `ligal.average.i00001 / ligal.average.i00002` por periodo. | Activa |
| `derived.farm_health_index` | `ligal` | Score 0-100 por periodo con los rangos operativos (células, bacterias, inhibidores, crioscopía, urea, ratio); el valor `current` añade chequeos estructurales (vínculo LIGAL, no conformidades). | Activa |
| `derived.treatments_vs_cells` | `ligal` + `vetiquin` | Tratamientos por periodo (`client_data_records` con `source_id='vetiquin'`) junto al recuento celular LIGAL. | Pendiente (sin fuente `vetiquin`) |
| `derived.med_cost_per_head` | `vetiquin` | Suma de `vetiquin.cost` por periodo dividida por el censo de la explotación. | Pendiente |
| `derived.udder_health.*` | `cegcol` | Prevalencia, no infectadas, NI, C, S, CR, SP, IP, NIP, CS, NCS y LS según `INDICES_SALUD_UBRE.md`; los índices de periodo seco usan todos los NC1 evaluables del histórico. | Activa cuando existen controles individuales. |

Contrato de entradas para futuras fuentes: las derivadas de Vetiquín esperan registros `client_data_records` con `source_id='vetiquin'` y, para el coste, una métrica `vetiquin.cost` por periodo; el censo se lee de `customer_farms.census` (o `capacity_ugm` como respaldo). El valor `current` del índice de salud guarda el nivel (`good`/`warning`/`alert`) en `value_text` para que la UI lo coloree.

La ficha de explotación (`#almacen-datos`) muestra estas derivadas en la cabecera (gauge del índice de salud + chips de fuentes volcadas) y en la sección «Salud y cálculos cruzados», donde las pendientes quedan visibles con la fuente que las activará.

`GET /api/datahub` devuelve una sola fila por explotación, aunque tenga enlaces
LIGAL y CEGCOL. La fila agrega `sources`, `sourceStates`, vencimientos y estados,
y carga explícitamente las métricas necesarias para el tablero técnico —incluida
`ligal.customer_matches`— en vez de depender de las últimas 60 filas por fecha.

Las métricas `derived.udder_health.*` guardan el valor actual y, dentro de
`inputs_json`, numerador, denominador, fecha de control, objetivo, criterio,
cobertura, conteos de clases, histórico y cohorte de periodo seco. NCS usa como
base todas las vacas multíparas cuyo último control anterior o igual a la fecha
real de secado estaba `>=200` y como numerador las que también están `>=200` en
NC1; no exige controles consecutivos. `basis.events` conserva la vaca, el NC1,
el control presecado, la fecha de secado, el método usado y el resultado CS/NCS.
Un denominador vacío se persiste como valor nulo con estado
`insufficient_data`, no como cero.
La lista de métricas de la ficha presenta además la base visible, por ejemplo
`2/8 de base` para NCS, usando esos `inputs` autenticados.

## Cómo deben escribir otras integraciones

Cada proveedor debe registrarse en `client_data_sources` y escribir:

- Datos atómicos o documentos normalizados en `client_data_records`.
- Indicadores calculados en `client_data_metrics`.
- Estado de ejecución en `client_data_sync_runs`.
- Estado diario por explotación en `client_data_farm_links`.

Reglas:

- No guardar secretos ni tokens externos en el almacén.
- No convertir ausencias de coincidencia externa en métricas de cero: deben quedar como estado `empty`.
- Mantener `source_id` estable.
- Mantener `entity_type` estable por tipo de dato.
- Marcar `subject_type='farm'` para tanque, meteorología o datos generales de explotación.
- Marcar `subject_type='animal'` y completar `animal_identifier` para analíticas, ordeños, incidencias, medicamentos o tratamientos de una vaca.
- Usar el crotal oficial como `animal_identifier` siempre que exista; conservar identificadores propios del proveedor en `animal_id`.
- Usar `external_id` o `external_code` cuando el proveedor lo tenga.
- Guardar `payload_json` normalizado y con claves estables.
- Usar `period_key` para datos temporales: `YYYY-MM`, `YYYY-MM-DD` o `current`.
- Calcular métricas derivadas en `client_data_metrics`, no dentro de la UI.

## Diseño para crecimiento

Crecimiento horizontal:

- Nuevas fuentes se añaden como filas de `client_data_sources`.
- Nuevos tipos se añaden con nuevos `entity_type`.
- Nuevas métricas se añaden con nuevos `metric_key`.

Crecimiento vertical:

- Si un proveedor necesita tablas especializadas, deben derivar de `client_data_records` y conservar la clave `farm_id + source_id + entity_type`.
- Los módulos consumidores deben leer primero desde `/api/datahub` o `/api/datahub/farms/{farm_id}`.
