Descripción
Antes (s114), la página Terminal listaba solo racks.Device colocados en un rack y con Network Management (management_config válido). Los dispositivos detectados por Auto-Provision y derivados a otras páginas (wireless, UPS, Digital Signage, observatory) no aparecían en el Terminal aunque también se gestionan por SSH.
Ahora el Terminal une dos fuentes, deduplicado por IP:
racks.Devicecon Network Management → sesión SSH directa (credenciales guardadas enmanagement_config).network.DeviceProfile(registro que Auto-Provision crea por cada(org, IP)detectada) consupports_sshosupports_snmp, cuya IP no esté ya cubierta por un Device.
Sin modelo nuevo en la lista — reaprovecha el DeviceProfile existente.
Implementación
terminal/services.py::terminal_hosts(org): devuelve{"hosts": [...], "racks": [...]}. Cada host se normaliza a un dict conuid(device:{id}/profile:{id}),source,category,name,ip,status,derived_toy, para Device,device_id/rack_id/group_ids_str.category(filtro de tipo): para Device =rack. Para profile la decideDeviceProfile.assigned_page(Ficha Central F4 — el camposcopedel MonitoringTarget fue eliminado en la migraciónmonitoring/0025):assigned_pagewireless/ups/signage→ esa categoría; si no está asignada pero está enlazado a un rack →rack; resto →other. Importante: la categoría se calcula EN VIVO en cada carga (no se persiste) — re-ejecutar Auto-Provision no cambia la categorización, es código.terminal/views.py::index: usa el servicio en lugar del query directo aDevice.templates/terminal/index.html: el bucle iterahostsy ramifica porsource. Las filasdevicemantienen checkbox +TerminalApp.openSession(device_id). Las filasprofileusanTerminalApp.quickConnectHost(...).- Filtro primario por tipo:
<select id="device-type-filter">(All / Rack / Wireless / UPS / Digital Signage / Other) sobre los secundarios (Status / Groups / Racks).DeviceFilterServicefiltra pordata-category.
Herencia de credenciales SSH (PR #81 · migración network/0047)
Antes, las credenciales SSH que el usuario daba en Auto-Provision se usaban para descubrir y se descartaban (_persist_profile no las recibía). Ahora:
DeviceProfileganassh_username+ssh_password_encrypted(Fernet víaCredentialManager) + propshas_stored_ssh/get_ssh_password(). Mismo nivel de cifrado que elmanagement_configde los racks (clave dedicadaCREDENTIAL_ENCRYPTION_KEY, Plan Hardening E).provision_stages.py::_persist_profilerecibessh_username/ssh_password(desdeservice.py::provision_device) y los guarda cifrados SOLO si el SSH de discovery funcionó (ssh_datatruthy). Un re-discovery sin creds no borra las guardadas (las claves SSH se inyectan endefaultssolo cuando hay nuevas).- Endpoint
GET /auto-provision/profiles/{id}/credentials(network/api/profiles.py, gateadorequire_perm(network, edit)+ org) devuelve{has_credentials, username, password, port}con la contraseña descifrada — patrón gemelo del/device/{id}/credentialsexistente. - Terminal:
terminal_hostsexponehas_stored_ssh+profile_id.quickConnectHost(ip, name, profileId, hasStored)(enstatic/js/pages/terminal.js): si hay creds guardadas las trae del endpoint y pre-rellena usuario/contraseña en el modal manual; si no, las pide (Regla 13, no inventa nada).
Nota operativa: para que un dispositivo ya existente herede sus claves hay que volver a pasarle Auto-Provision con credenciales — las antiguas nunca se guardaron.
Tests
tests/api/test_terminal_hosts.py (13): unión, dedup por IP, categorías (incl. wifi por device_type), exclusión de profiles sin SSH/SNMP, aislamiento por organización, round-trip de cifrado, endpoint de credenciales (admin 200 / viewer 403 / sin-creds), has_stored_ssh, y un test de vista que parchea terminal.views.render (patrón de test_monitoring, porque el job Backend de CI no compila assets Vite).
Footgun
La plantilla del Terminal usa {% vite_asset 'src/pages/terminal.ts' %}. Un test que renderice la página real falla en el job Backend de CI con DjangoViteAssetNotFoundError (CI no compila Vite ahí). Patrón correcto: parchear render y verificar el contexto, no el HTML.