Resumen
La página Terminal ahora muestra todos los dispositivos de red registrados, no solo los que están colocados en un rack con Network Management. Incluye un nuevo filtro primario por tipo de dispositivo (Todos / Rack / Wireless / UPS / Digital Signage / Otros).
Sesión 114 · Merge PR #80 edu/terminal-all-devices · 2026-06-08
Cambio de comportamiento
Antes
- Terminal listaba solo
racks.Devicecon Network Management habilitado (credenciales SSH guardadas). - Los dispositivos detectados por Auto-Provision (puntos wifi, SAIs, reproductores de signage) no aparecían aquí, aunque sí en sus páginas especializadas (Wireless, UPS, Digital Signage).
- Imposible ver en un único lugar el inventario completo de dispositivos SSH-gestionables.
Ahora
- Terminal lista
racks.Device(con creds) másnetwork.DeviceProfiledetectados en otras páginas. - Cada dispositivo aparece una sola vez (deduplicado por IP).
- Equipos de rack con credenciales guardadas → sesión SSH directa (comportamiento anterior, sin cambio).
- Dispositivos detectados sin creds SSH guardadas → modal manual pre-relleno con IP (nuevo patrón, reusa
TerminalApp.quickConnectHost(), similar aDiscovery.quickConnect()). - Nuevo dropdown Tipo (arriba del listado) filtra por categoría:
- All Types — muestra todo.
- Rack — solo equipos de rack.
- Wireless — puntos de acceso WiFi.
- UPS — Sistemas de Alimentación Ininterrumpida.
- Digital Signage — reproductores de cartelería.
- Other — dispositivos detectados no clasificados (observatorio, etc.).
Implementación
Nuevo servicio: terminal_hosts(org)
Ubicación: terminal/services.py
Normaliza y deduplica dos fuentes:
racks.Device.has_network_management→source="device",category="rack".network.DeviceProfile→source="profile",categorysegúnDeviceProfile.assigned_page(wireless/ups/signage; sin asignar → rack u other).
Devuelve un dict con hosts[] (unificado, deduplicado por IP) y racks[] (para el filtro).
Código:
def terminal_hosts(org):
devices, taken_ips, racks_by_id = _device_hosts(org)
profiles = _profile_hosts(org, taken_ips)
racks = sorted(racks_by_id.values(), key=lambda r: r.name)
return {"hosts": devices + profiles, "racks": racks}
Véase entity--terminal--service--terminal-hosts para detalles de estructura.
Refactor de la vista: terminal/views.py::index(request)
Antes:
all_devices = Device.objects.filter(rack__organization=org, ...)
devices = [d for d in all_devices if d.has_network_management]
# renderizar devices
Ahora:
result = terminal_hosts(org)
hosts = result["hosts"]
device_racks = result["racks"]
context = {
"hosts": hosts,
"total_nm_devices": len(hosts),
"device_racks": device_racks,
...
}
La vista delega toda la lógica de unión + dedup al servicio.
Frontend: nuevo filtro y handler
static/js/utils/DeviceFilterService.js:
- Nuevo parámetro
device-type-filter(dropdown). - Cada item del listado tiene
data-category(extraído dehost.category). - Filtro compone: búsqueda + tipo + estado + grupo + rack.
const typeFilter = document.getElementById('device-type-filter')?.value || 'all';
const matchesType = typeFilter === 'all' || category === typeFilter;
item.style.display = (matchesSearch && matchesType && matchesStatus && matchesGroup && matchesRack) ? '' : 'none';
static/js/pages/terminal.js:
- Nuevo método
TerminalApp.quickConnectHost(ip, name): abre el modal manual de SSH y pre-rellena el campohostcon la IP. - Para Profiles:
data-action="TerminalApp.quickConnectHost".
quickConnectHost(ip, name) {
const host = document.getElementById('manual-ssh-host');
if (host) host.value = ip || '';
const portEl = document.getElementById('manual-ssh-port');
if (portEl && !portEl.value) portEl.value = '22';
this.openManualConnectionModal();
}
Template: templates/terminal/index.html
Cambios estructurales:
-
Nuevo dropdown de tipo (filtro primario):
<select id="device-type-filter" data-on-change="DeviceManager.filterDevices" aria-label="Filter devices by type"> <option value="all">All Types</option> <option value="rack">Rack</option> <option value="wireless">Wireless</option> <option value="ups">UPS</option> <option value="signage">Digital Signage</option> <option value="other">Other</option> </select> -
Iteración bifurcada sobre
hosts:{% for host in hosts %} {% if host.source == "device" %} <li data-id="{{ host.device_id }}" ... data-action="TerminalApp.openSession" ...> <!-- Equipo de rack: clic directo en SSH --> </li> {% else %} <li class="device-item-detected" ... data-action="TerminalApp.quickConnectHost" ...> <!-- Detectado: clic abre modal manual --> <span class="device-derived">{{ host.derived_to }}</span> </li> {% endif %} {% endfor %} -
Mensaje vacío actualizado:
{% empty %} <li class="empty-state">No network devices registered yet</li>
Testing
tests/api/test_terminal_hosts.py · 7 tests:
- ✅ Device con Network Management →
category="rack" - ✅ Profile detectado sin asignar →
category="other" - ✅ Profile con
assigned_pagewireless/ups/signage → categorización correcta - ✅ Deduplicación por IP (Device + Profile con la misma IP → 1 fila)
- ✅ Exclusión de Profiles sin SSH/SNMP
- ✅ Aislamiento RLS (otra org no ve los hosts)
- ✅ Vista renderiza hosts + dropdown + dispositivo detectado visible
Suite terminal/racks/network: 100 passed, 0 regresiones.
Notas de diseño
Sin credenciales inventadas (Regla 13)
- Dispositivos de rack: creds SSH guardadas en
device.management_config→ sesión directa al hacer clic. - Detectados sin creds SSH: se abre el modal de conexión manual con la IP ya rellena.
- No se inventan credenciales — el usuario debe introducirlas manualmente en el modal, o la conexión falla sin sorpresas.
Deduplicación por IP (transparente)
- Si un Device y un Profile tienen la misma IP:
- El Device ganador (primero en resultado).
- El Profile se descarta sin error.
- El usuario ve una sola fila, no dos.
- Esto evita confusión cuando un dispositivo está registrado en múltiples sistemas.
Filtro primario vs secundarios
-
Primario (nuevo): Tipo (All Types / Rack / Wireless / UPS / Digital Signage / Other).
- Rápidamente acota el dominio de búsqueda.
- 1 clic → ve solo el tipo de dispositivo que importa.
-
Secundarios (existentes): Estado / Grupos / Racks.
- Orientados a equipos de rack (Status, RackGroup membership).
- Permanecen bajo el filtro primario para afinar.
Impacto en otros módulos
racks: Device sin cambios; se enriquece en la vista Terminal con el nuevo servicio.network: DeviceProfile sin cambios; ahora visible en Terminal.monitoring: MonitoringTarget sin cambios (hoy ya no interviene en la categorización: la decideDeviceProfile.assigned_pagedesde Ficha Central F4).- Auto-Provision: Sin cambios; Terminal consume los registros que ya crea.
Véase también
- [[entity—terminal—service—terminal-hosts]]
- [[entity—terminal—endpoint—index]]
- [[entity—racks—model—device]]
- [[entity—network—model—device-profile]]
- [[entity—network—model—monitoring-target]]