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) asignabadevice_type="media_player"(reproductores multimedia: SpinetiX, Crestron, Extron) sin que este valor estuviera enDEVICE_TYPE_CHOICES. - La migración 0036 sembró perfiles con
device_typeen{projector, audio_dsp, display}sin declararlos. - El módulo
discovery.pyescribíadiscovery_method="ping"sin estar declarado enDISCOVERY_METHOD_CHOICES. - Los
VendorProfilesin CLI (SpinetiX, vendors de señalización) se creaban conssh_show_version_cmd=""yssh_show_interfaces_cmd="", pero estos campos no permitíanblank=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:- Los datos escritos por el código pasan
full_clean(). - Borrar un asset limpia su referencia en playlists.
- Los items sin
asset_id(URL/stream) no se tocan. - Bulk delete y delete individual ambos limpian.
- Los datos escritos por el código pasan
-
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
| Ámbito | Efecto |
|---|---|
| Admin / API | 82 perfiles desbloqueados; edición normal |
| Signage | Borrar contenido limpia automáticamente playlists |
| Background tasks | Purga de auto-playlists 24/7, no solo en publish |
| Deep discovery | MIB 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]]