CreaRack-SL

Higiene v1.66.17: Validación sincronizada + limpieza de playlists

Contexto

Fecha: 14 de agosto de 2026
Autor(a): Edu + Claude Code
Ámbito: Network (profiles), Signage (content management), Infrastructure (task scheduling)
Tarea(s): #226
Versión: v1.66.17

Esta decisión registra cuatro correcciones operativas de fondo (punto 1, 2, 3 y 5 de la tarea #226) que resuelven fallos críticos en la validación de datos y el ciclo de vida de contenido.


Problema: Datos Inválidos Bloqueaban su Propia Edición

Punto 1: Choices Declarados vs. Escritos

El síntoma: 57 DeviceProfile + 25 VendorProfile fallaban su propia validación (full_clean()) y no podían editarse desde el formulario admin ni API.

Raíz:

  • El auto-clasificador de dispositivos (network.models.auto_classify) asignaba device_type="media_player" (reproductores multimedia: SpinetiX, Crestron, Extron) sin que este valor estuviera en DEVICE_TYPE_CHOICES.
  • La migración 0036 sembró perfiles con device_type en {projector, audio_dsp, display} sin declararlos.
  • El módulo discovery.py escribía discovery_method="ping" sin estar declarado en DISCOVERY_METHOD_CHOICES.
  • Los VendorProfile sin CLI (SpinetiX, vendors de señalización) se creaban con ssh_show_version_cmd="" y ssh_show_interfaces_cmd="", pero estos campos no permitían blank=True.

Consecuencia: Al guardar, Django ejecutaba full_clean() implícitamente y rechazaba los datos por ValidationError, impidiendo editar perfiles válidos.

Punto 2: Borrar Contenido Dejaba Referencias Colgadas en Playlists

El síntoma: Eliminar un MediaAsset (imagen/vídeo) dejaba items huérfanos en Playlist.items. El siguiente publish de la playlist hacía desaparecer el contenido de pantalla sin aviso.

Raíz: Los endpoints delete_media y bulk_delete_media no limpiaban las referencias en las playlists que usaban esos assets.

Punto 5 (Parcial): Purga de Auto-Playlists Solo en Publish

El síntoma: Las auto-playlists envejecidas se purgaban solo al publicar (Playlist.publish), así que entre publicaciones el pool de playlists crecía sin límite.

Raíz: El método Playlist.purge_auto(org_id) se ejecutaba manualmente en la vista de publicación. Las tareas cron (cleanup_old_data) no lo llamaban.

Punto 3 (Decisión Edu): MIB desde Fuente es Modo Soportado, No Error

El síntoma: Cuando pysmi no podía compilar una MIB (ej. XIRRUS-MIB, 583 KB), el fallback extraía OIDs desde el ASN.1 fuente con un tono de “esto es una falla a medias”.

Decisión: La extracción desde fuente es un modo de operación soportado, no un error. XIRRUS-MIB funciona así desde febrero 2026 (1.736 OIDs extraídos) y el descubrimiento profundo funciona igual. La UI lo enseña como estado “Source” (ámbar). Solo 0 OIDs es un fallo real (“Failed”).


Solución: Alineación de Contratos

1. Sync Choices: Declarar lo que el Código Escribe (Migración 0061)

# network/models/vendor.py — DEVICE_TYPE_CHOICES
DEVICE_TYPE_CHOICES = [
    ("router", "Router"),
    ("switch", "Switch"),
    ("firewall", "Firewall"),
    ("load_balancer", "Load Balancer"),
    ("wireless_controller", "Wireless Controller"),
    ("access_point", "Access Point"),
    # Nuevos (task #226): auto-clasificador + migración 0036
    ("media_player", "Media Player"),
    ("projector", "Projector"),
    ("audio_dsp", "Audio DSP"),
    ("display", "Display"),
    ("other", "Other"),
]

# network/models/profile.py — DISCOVERY_METHOD_CHOICES
DISCOVERY_METHOD_CHOICES = [
    ("snmp", "SNMP"),
    ("ssh", "SSH"),
    ("snmp+ssh", "SNMP + SSH"),
    ("ping", "Ping"),      # Nueva (task #226): discovery.py lo escribía
    ("manual", "Manual"),
]

# network/models/vendor.py — blank=True en ssh_*_cmd
ssh_show_version_cmd = models.CharField(
    max_length=200, blank=True, default="show version",
    help_text="Comando SSH para obtener versión"
)
ssh_show_interfaces_cmd = models.CharField(
    max_length=200, blank=True, default="show interfaces",
    help_text="Comando SSH para listar interfaces"
)

Efecto: 82 perfiles existentes pasan ahora su propia validación; el form/admin puede editarlos nuevamente.

2. Limpieza de Playlists al Borrar Assets (signage/api/content.py)

def _remove_assets_from_playlists(org, asset_ids):
    """
    Quita de Playlist.items las referencias a los assets borrados (task #226):
    sin esto, borrar un MediaAsset dejaba items colgados.
    Los items url/stream (sin asset_id) quedan intactos.
    """
    ids = set(asset_ids)
    for pl in Playlist.objects.filter(organization=org):
        items = pl.items or []
        kept = [it for it in items if it.get("asset_id") not in ids]
        if len(kept) != len(items):
            pl.items = kept
            pl.save(update_fields=["items"])

# En delete_media y bulk_delete_media:
deleted_ids = list(assets.values_list("id", flat=True))
assets.delete()
_remove_assets_from_playlists(org, deleted_ids)

Efecto: Borrar un asset limpia automáticamente sus referencias en playlists; no hay desapariciones sin aviso.

3. Purga Diaria de Auto-Playlists (signage/tasks.py)

def cleanup_old_data():
    # ... limpieza de logs ...
    
    # Purga de auto-playlists (task #226): purge_auto solo corría al PUBLICAR,
    # así que el pool crecía sin límite entre publicaciones.
    from signage.models import Playlist
    org_ids = Playlist.objects.filter(is_auto=True) \
        .values_list("organization_id", flat=True).distinct()
    for org_id in org_ids:
        Playlist.purge_auto(org_id)

Se ejecuta a las 3:00 AM junto con las otras limpiezas.

4. Documentación del Fallback de MIB (network/api/vendor.py)

def upload_mib(request, slug: str):
    # ...
    if not extracted_oids:
        # Fallback: parse OIDs directly from ASN.1 source. MODO SOPORTADO, no un
        # error a medias (decisión Edu 14-08, task #226): hay MIBs de vendor que
        # pysmi no traga (XIRRUS-MIB, 583 KB, funciona así desde feb-2026 con
        # 1.736 OIDs) y la extracción alimenta el deep discovery igual. La UI lo
        # enseña como "Source" (ámbar); solo 0 OIDs es "Failed" de verdad.
        extracted_oids = extract_oids_from_source(content)

Garantías

  • Test suite (tests/api/test_hygiene_226.py): 4 tests nuevos verifican que:

    1. Los datos escritos por el código pasan full_clean().
    2. Borrar un asset limpia su referencia en playlists.
    3. Los items sin asset_id (URL/stream) no se tocan.
    4. Bulk delete y delete individual ambos limpian.
  • Migración 0061: AlterField sobre device_type, discovery_method, ssh_show_version_cmd, ssh_show_interfaces_cmd. Sin cambio de datos (solo ampliamos choices y permitimos blank).


Impacto

ÁmbitoEfecto
Admin / API82 perfiles desbloqueados; edición normal
SignageBorrar contenido limpia automáticamente playlists
Background tasksPurga de auto-playlists 24/7, no solo en publish
Deep discoveryMIB fallback documentado; “Source” es modo válido

Véase también

  • [[entity—network—model—device-profile]]
  • [[entity—network—model—vendor-profile]]
  • [[entity—signage—model—media-asset]]
  • [[entity—signage—model—playlist]]
  • [[concept—network—discovery]]