CreaRack-SL

Barra de Integridad F2: el radar interno plano↔realidad

Descripción general

La Barra de Integridad F2 cierra la visión interna del motor de validación plano↔realidad (iniciado en Motor de Integridad F1, v1.59.0). Permite ver el estado completo del radar de cada organización y gestionar la cola de reconciliación mediante veredictos humanos.

Ubicación (desde v1.63.0, 2026-07-23): pestaña Integrity de Observatory (Overview · CNS · ITSM · Integrity). Hasta v1.62.x fue página propia (/integrity/) con botón en el header; esa URL se conserva como redirect a /monitoring/?tab=integrity (bookmarks). Razón del traslado (decisión de producto de Edu): el bucle drift → filtro CNS → ticket ITSM → reconciliar vive en superficies vecinas y la audiencia es la misma.

Publicación: v1.60.0 (2026-07-17)
Estado: Nace completamente apagado — no visible para usuario alguno hasta que se active el módulo integrity en extra_modules de una org específica (beta org 1, activación manual post-deploy).


Por qué “nace apagado”

La v1.59.0 introdujo el motor que detecta divergencias plano↔realidad:

  • Plan dice “Device A está en U10”, pero la red no lo ve (stale).
  • La red ve “IP 10.0.0.5 es Cisco”, pero el plan no lo documenta (undocumented).
  • Dos devices afirman ser “IP 10.0.0.1” (IP conflict).

Pero la calidad del motor depende de su precisión, que a su vez depende del ambiente de testing. La v1.60.0 añade los veredictos humanos (IntegrityAck) que calibran automáticamente la métrica del gate (#202):

  • Click plan_updated o real_change → el aviso era SEÑAL (cambio real)
  • Click false_alarm → el aviso era RUIDO (error del sensor)

Hasta que el ratio señal/ruido sea digno de confianza, no se muestra a clientes. Por eso:

# core/migrations/0030_integrity_module.py
# El módulo existe pero NO se añade a ningún Plan ni org
SaaSModule.objects.get_or_create(
    slug="integrity",
    defaults={...}
)

Solo con org.extra_modules.add(integrity_module) → aparece la pestaña Integrity en Observatory + API.


Arquitectura de componentes

1. Módulo SaaS integrity (core)

  • Registrado en DB (migración 0030).
  • URL prefixes gateadas en module_registry: /integrity/, /api/racks/integrity.
  • Sin Plan, sin Organization.plans: siempre extra_modules (opt-in).
  • La visibilidad de la pestaña en Observatory la gatea el template ({% ifmodule "integrity" %} en observatory.html): una org sin el módulo ni la pinta.

2. Modelo IntegrityAck (racks)

  • Tabla: Veredictos de la cola. Una fila per aviso → veredicto usuario.
  • device XOR profile: Avisos de rack apuntan a Device; avisos “undocumented” (vivos sin plano) apuntan a DeviceProfile.
  • Veredictos: plan_updated (3 días silencio) / real_change (30 días) / false_alarm (30 días + cuarentena muted).
  • RLS: Tenant isolation (org_id, bypass admin).

3. Servicio Motor (racks/services/integrity.py)

Funciones públicas:

  • compute_integrity(org, fresh_days=7) — retorna estado completo: fidelity %, coverage %, hallazgos por rack + lista undocumented. Ahora integraba veredictos vigentes para:
    • false_alarm → saca el device de fidelity (cuarentena “muted”).
    • plan_updated / real_change → marcan hallazgo ack-ado pero lo contabilizan como señal.
  • gate_stats(org) — métrica del gate #202: {signal, noise, total, signal_pct}.

Sub-componentes:

  • _load_active_acks(org, now) — índice de veredictos vigentes (no mutados aún) por sujeto.
  • _ack_info(ack) — serializa veredicto para respuesta JSON.
  • Constantes: ACK_KINDS (matched_stale, ip_conflict, undocumented) / VERDICTS (plan_updated, real_change, false_alarm) / ACK_MUTE_DAYS {3, 30, 30}.

4. API (racks/api/integrity.py)

Dos endpoints públicos:

GET /api/racks/integrity/overview (usuario miembro org)

{
  "result": { "summary": {...}, "racks": [{...}], "undocumented": [...] },
  "history": [{"date": "2026-07-10", "fidelity_pct": 98.5, ...}, ...],
  "gate": { "signal": 12, "noise": 2, "signal_pct": 85.7, ... }
}

POST /api/racks/integrity/ack (usuario racks:edit)

{
  "kind": "matched_stale",
  "verdict": "plan_updated",
  "device_id": 42,
  "note": "El switch ya está retirado del rack"
}
→ 200 { "ok": true, "ack_id": 1234, "muted_until": "2026-07-20T...", "gate": {...} }

Gateado por módulo — 403 sin integrity activo.

5. Vistas (desde v1.63.0)

  • racks/views.py::integrity_view (/integrity/): solo redirect a /monitoring/?tab=integrity. El gating por módulo (middleware) aplica ANTES del redirect: org sin módulo → 403.
  • monitoring/views.py::observatory_view (/monitoring/): renderiza observatory.html, que incluye el markup de la pestaña Integrity (module-gated) y pasa integrity_can_edit (permiso racks:edit) para mostrar los botones de veredicto.
  • JS ES module trae datos vía /api/racks/integrity/overview + escucha clicks POST /ack; observatory.js acepta ?tab=integrity para aterrizar directo en la pestaña.

6. Template + CSS + JS

  • HTML: markup de la pestaña en templates/monitoring/observatory.html (dentro de {% ifmodule "integrity" %}): tabla por rack (fidelity/coverage) + histórico + cola con evidencia. Mismos msgids i18n que la página original (traducciones ES intactas).
  • CSS (static/css/pages/integrity.css): Solo tokens centrales (—card-bg, —text-primary, etc.). Grid responsive. Se carga solo con el módulo activo.
  • JS (static/js/pages/integrity.js): ES module, escHtml() estático para seguridad, t() en runtime para i18n. Auto-guardado: si #integrity-root no existe (módulo apagado), no hace nada.

Flujo de uso (beta)

  1. Admin añade módulo a org:

    org = Organization.objects.get(pk=1)
    org.extra_modules.add(SaaSModule.objects.get(slug="integrity"))
  2. Usuario racks:edit abre Observatory → pestaña Integrity (el enlace antiguo /integrity/ redirige ahí).

  3. Cola de reconciliación: Cada hallazgo (device stale, undocumented, etc.) es un botón.

    • Click plan_updated → Nota “ya corregí el plan” + 3 días silencio.
    • Click real_change → Nota “el cambio era real” + 30 días silencio.
    • Click false_alarm → Equipo va a cuarentena “muted” + 30 días silencio. No castiga fidelity.
  4. Gate #202: Con cada click, gate_stats(org) se actualiza automáticamente:

    signal_pct = (plan_updated + real_change) / total * 100
  5. Decisión de Go/NoGo: Cuando signal_pct > 85% (ej.), la feature sale de beta → se activa en Plan 4 (Professional) o superior.


Cambios en el modelo de datos

IntegrityAck (NEW)

class IntegrityAck(models.Model):
    organization: FK(Organization)  # Tenant
    device: FK(Device, null=True)   # Para avisos de rack
    profile: FK(DeviceProfile, null=True)  # Para undocumented
    kind: str  # matched_stale | ip_conflict | undocumented
    verdict: str  # plan_updated | real_change | false_alarm
    note: TextField  # Justificación usuario
    subject_label: str  # Nombre legible (cacheado en caso de borrado)
    muted_until: DateTimeField  # Fin del período de silencio
    created_by: FK(User)
    created_at: DateTimeField(auto_now_add=True)
    
    indices: [organization, muted_until]

RackIntegritySnapshot (modified v1.59.0)

Se mantuvo igual. Esta tabla almacena fotos diarias de fidelity/coverage por rack. F2 la lee para mostrar histórico.


Integración con el motor (v1.59.0)

La función compute_integrity() ahora carga veredictos vigentes al principio:

acks = _load_active_acks(organization, now)  # Índice por (tipo_sujeto, id, kind)

Luego, para cada hallazgo:

ack = acks.get(("device", device.id, CLASS_MATCHED_STALE))
if ack:
    finding["ack"] = {...verdict, muted_until, note...}
    if ack.verdict == VERDICT_FALSE_ALARM:
        counts["muted"] += 1  # No castiga fidelity
    else:
        counts[CLASS_MATCHED_STALE] += 1  # Sigue siendo hallazgo

fidelity_pct = 100 * matched_alive / (total - unobservable - muted)

Efecto práctico: Un false_alarm sobre “Device A” stale → Device A se excluye de fidelity durante 30 días. El plano sigue siendo inexacto, pero no penaliza la métrica.


Migraciones

core/0030_integrity_module

Seeds el módulo SaaS integrity en la tabla core_saasmodule. No lo asigna a ningún Plan.

racks/0016_integrity_ack

Crea tabla racks_integrityack con fields y índice org+muted_until.

racks/0017_integrity_ack_rls

Habilita RLS + crea política tenant_isolation con bypass admin.


Permisos y gating

  • Ver pestaña + API overview: Miembro de org + módulo integrity activo (middleware para la API y el redirect; {% ifmodule %} para la pestaña).
  • POST /ack: Además, permiso racks:edit.
  • RLS: A nivel PostgreSQL — usuarios nunca ven acks de otra org.

Testing

12 tests en tests/racks/test_integrity_api.py (10 de F2 + 2 de la reubicación v1.63.0):

  • Gating por módulo (org sin módulo → 403; /integrity/ con módulo → 302 a la pestaña).
  • Render de Observatory: pestaña presente con módulo / ausente sin él.
  • Veredictos y efecto en fidelity/coverage.
  • Cálculo de gate_stats().
  • Validación cross-org (RLS).
  • Muted_until reenters correctamente tras 30 días.

Nota CI (v1.63.0): los tests que renderizan páginas con {% vite_asset %} exigieron DJANGO_VITE dev_mode=True en config/settings/test.py — los shards de backend del CI no compilan el frontend y el manifest no existe.


Véase también

  • [[entity—core—module—integrity]]
  • [[entity—racks—model—integrity-ack]]
  • [[entity—racks—service—integrity-motor]]
  • [[entity—racks—endpoint—integrity-overview-ack]]
  • [[entity—racks—view—integrity-radar]]
  • [[concept—saas—multi-tenancy]]
  • [[concept—racks—calibracion-sensor]]
  • [[decision—20260717—integrity-apagado-por-defecto]]
  • [[feature—racks—motor-integridad-f1]]