# Almacén Lechero

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

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

El primer proveedor conectado es LIGAL. Otras integraciones deben añadir datos con la misma estructura, sin crear acoplamientos directos en los módulos de negocio.

## 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 Almacén Lechero 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}&entityType=ligal_average&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`. |
| `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. |
| `q` | Búsqueda en código externo, identificador y payload normalizado. |
| `limit` | Máximo de filas, hasta 300. |

La respuesta incluye `records`, `metrics`, `analytics` 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.

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.

## 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.

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`, 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.

## Consulta visual

La pantalla `#almacen-datos` permite consultar a mano:

- explotación con REGA;
- tipo de dato guardado;
- métrica calculada;
- rango de fechas;
- texto libre.

La UI pinta un tablero de prioridad técnica por explotación antes del detalle. Ordena las granjas 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 privado de cliente (`/customer/{token}`) incorpora la vista `Almacén Lechero`. El backend añade `milkWarehouse` al payload del portal con datos filtrados por `customer_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 casi diarios 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`. En la cuenta LIGAL probada, `/api/SampleTypes` devuelve el código `I0` (`Pago por calidade`), que es el que expone los muestreos de calidad de leche casi diarios; `LE` no devuelve filas. En ambos casos se deduplican por código o fecha de muestra, se guardan sus determinaciones como `ligal_sample_result`, se materializan en `client_data_analytics` y se calculan métricas `ligal.sample.{codigo_ensayo}` usando `YYYY-MM-DD` como `period_key`.

## 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 de LIGAL, como capacidades y ensayos disponibles. |

## Sincronización LIGAL

La sincronización parte del `REGA` de `customer_farms`. 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` y seleccionar los códigos configurados en `DATAHUB_LIGAL_SAMPLE_TYPES` o, si no hay configuración, el código de pago por calidad descubierto (`I0` en la cuenta probada).
4. Para cada código de cliente LIGAL encontrado, consultar:
   - `ligal.samples.search` con los tipos de muestra seleccionados y 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`.
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`

Si el `REGA` no devuelve ningún cliente LIGAL, la ejecución queda en estado `empty`, se limpian los datos LIGAL de esa explotación y no se crean métricas con ceros. Así una ficha ficticia, duplicada o sin correspondencia no contamina los contadores ni las gráficas.

Variables de entorno:

| Variable | Valor por defecto |
|---|---|
| `DATAHUB_SYNC_INTERVAL_HOURS` | `24` |
| `DATAHUB_LIGAL_LOOKBACK_DAYS` | `365` |
| `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 `/api/SampleTypes` y selecciona el tipo de pago por calidad (`I0` en la cuenta probada). |
| `DATAHUB_LIGAL_PAGE_SIZE` | `200` |

## 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. Hoy LIGAL es el único adaptador implementado, pero `datahub_sync_due_farms` ya no aborta si una fuente concreta no está configurada: ejecuta las que tienen adaptador y credenciales, despachando por `source_id`.

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. 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 |

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á.

## 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.
- 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}`.
