fix(alertas): la saturación se mide contra la capacidad contratada, no contra el puerto
_RULE_EXPR dividía el pico entre velocidad_puerto_bps, que NO es la
capacidad contratada: es la velocidad del puerto físico, 1 Gbps en los 22
enlaces por igual. routers/ti.py::_ab_catalogo ya lo documentaba y por eso el
dashboard divide entre el catálogo de /admin; el motor de alertas se había
quedado con el otro denominador, así que la gráfica y la alarma medían cosas
distintas — justo lo que kpi_indicadores existe para impedir.
El efecto no era un desvío pequeño. Medido contra el datalake real:
enlace capacidad pico dashboard alarma (antes)
port5 100 Mbps 220 Mbps 220% 22%
port6 200 Mbps 220 Mbps 110% 22%
port3 50 Mbps 52 Mbps 104% 5.2%
Con ti_ancho_banda_util_rojo = 80 la alarma habría necesitado 800 Mbps sobre
un puerto de 1 G: no podía dispararse nunca. Los tres umbrales daban 0 de 7,794
filas y el 0 parecía una buena noticia. Ahora: 414 día-enlace sobre el rojo, 557
sobre el amarillo y 231 rebasando el 100%.
Casar la capacidad obliga a slugificar del lado del SQL: las claves de /admin
están slugificadas (..._cap_vlan_40_ti) y el datalake guarda el id crudo del
enlace ("VLAN 40 TI"). 17 de los 22 no coinciden. El CASE se arma con parámetros
ligados, nunca interpolando números.
Un enlace sin capacidad capturada da NULL, no un default: la fila no entra en el conteo, igual que el dashboard lo deja sin semáforo en vez de suponerle una. Si NO hay ninguna capacidad configurada se levanta ValueError y la regla se salta — se prefiere no evaluar a inventar un denominador.
Contrastado enlace por enlace contra _ab_catalogo: 0 discrepancias en los 22.
Regresión: los 7 indicadores de ti_licencias generan SQL idéntico al anterior,
sin que se les cuele ningún parámetro de capacidad.
De paso, las etiquetas de /admin dejaban de decir la verdad: la columna
velocidad_puerto_bps se rotulaba "Velocidad contratada (bps)", que es
exactamente cómo se llegó a este bug.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_0116hwhMUGwq5tsrsNxMYoZm