Conceptoactiveverificado 2026-04-15#rack-editor#konva#frontend#backend#stencils#django#agent-context#ux
Agente · dev-rack-editor
Propósito
Desarrollo y mantenimiento del Rack Editor de CreaRack-Pro.
Cubre la app racks/, los 20 módulos JS del editor, el canvas Konva.js, el sistema de stencils, SSH integrado y backups de configuración.
Estructura de archivos
Frontend: static/js/editor/ (20 archivos, ~7.500 LOC)
| Archivo | LOC | Responsabilidad |
|---|---|---|
main.js | ~363 | Bootstrap: carga datos del DOM, inicializa Konva, orquesta módulos |
state.js | ~55 | Objeto STATE global, constantes de grid, helpers de posición |
konva_setup.js | ~189 | Stage/Layer Konva.js, zoom, pan, frame del rack |
devices.js | ~432 | Creación, drag-drop, snap, colisión de dispositivos en canvas |
history_adapter.js | ~172 | Undo/Redo via StateManager — snapshots inmutables |
editor_api.js | ~86 | Auto-save debounce 500ms, serialización hacia backend |
ui_library.js | ~272 | Panel de stencils: búsqueda, carpetas, drag al canvas |
PropertiesPanelRenderer.js | ~563 | HTML del panel de propiedades del dispositivo seleccionado |
PropertiesPanelEvents.js | ~925 | Eventos del panel: nombre, puertos, acciones (SSH, Backups, QR…) |
auto_provision_panel.js | ~239 | Panel flotante de dispositivos pendientes de Auto-Provision |
ssh_integration.js | ~411 | Terminal SSH embebido, sesiones, integración SFTP |
SshConfigModal.js | ~254 | Formulario SSH: IP, puerto, credenciales, vendor autodetect |
import_export.js | ~506 | Importación Visio (.vssx/.vdx/.vss), categorías de stencils |
backups.js | ~623 | Snapshots de configuración de red, diff viewer, restore |
sftp_ui.js | ~340 | Navegador de archivos SFTP via Local Agent |
ConnectionModal.js | — | Modal de conexiones inter-dispositivo (cables, puertos) |
DeviceInfoModal.js | — | Modal de información detallada del dispositivo |
PortConfigDiff.js | — | Visor de diferencias de configuración de puertos |
modal_helper.js | — | Utilidades compartidas para modales del editor |
Backend: racks/
racks/
├── models.py # BoxCategory, RackGroup, Stencil, Rack, Device, ConfigBackup
├── views.py # rack_editor (canvas), index (lista racks)
├── schemas.py # Pydantic schemas
└── api/
├── racks.py # CRUD racks + dispositivos (~360 LOC)
├── library.py # Stencils + Visio import (~745 LOC)
├── export.py # CSV/JSON/PDF export (~1023 LOC)
├── labels.py # QR labels API
└── trash.py # Papelera racks
Estado global (state.js)
export const STATE = {
rackData: null, // Datos del rack cargados del DOM (#rack-data)
devices: [], // Array de dispositivos con nodos Konva
selectedNode: null, // Nodo Konva actualmente seleccionado
isLocked: false, // Bloqueo de interacción del canvas
currentDragItem: null, // Item arrastrándose desde la librería
stage: null, // Konva.Stage
layer: null, // Konva.Layer principal
rackGroup: null, // Konva.Group del frame del rack
pos: { x: 50, y: 50 }, // Posición del rack en el canvas
CONFIG: {
U_HEIGHT: 35, // Pixeles por unidad de rack (U)
RACK_WIDTH: 400, // Ancho fijo del rack en pixeles
RACK_HEIGHT: 0 // Calculado: height_u × U_HEIGHT
}
};
export const SLOT = { FULL: 'full', LEFT: 'left', RIGHT: 'right' };
export const HALF_WIDTH = 195; // (RACK_WIDTH - 10) / 2
Estructura de un dispositivo en STATE.devices[]
{
type: "switch",
originalName: "Cisco 2960",
u_height: 1,
u_position: 15, // Posición U desde abajo
notes: "",
image: "/media/uploads/...",
db_id: 42, // null si es nuevo (no guardado aún)
half_width: false,
slot: "full", // "full" | "left" | "right"
ports: {
"eth0": { live: "up", label: "WAN", desired: "trunk" }, // formato enriquecido
"sfp1": "10G" // formato legacy (string)
},
management_config: { ip: "192.168.1.1", vendor: "cisco" },
meta: { ports: {...}, notes: "...", half_width: false },
konvaNode: Konva.Group // Referencia al nodo visual
}
Konva.js — estructura visual
Konva.Stage (draggable, zoom con wheel)
└── Konva.Layer
├── STATE.rackGroup (Konva.Group)
│ ├── Rect (fondo del rack)
│ ├── Lines (separadores de U)
│ └── Text (números de U: 1, 2, 3...)
├── DeviceGroup_1 (Konva.Group, draggable)
│ ├── Rect (fondo), Image (stencil), Text (nombre)
├── DeviceGroup_2 ...
└── DeviceGroup_N ...
Conversión coordenadas ↔ posición U
// U → Y pixel
y = STATE.pos.y + (RACK_HEIGHT - u_position × U_HEIGHT)
// Y pixel → U (al hacer drop/drag-end)
uPosition = (RACK_HEIGHT - (yRelative + deviceHeight)) / U_HEIGHT + 1
Snap y colisiones (devices.js)
Snap (dragBoundFunc)
- Distancia snap: 60px a los raíles del rack
- Full-width: snap al raíl izquierdo (
STATE.pos.x) - Half-width: snap a LEFT o RIGHT según proximidad al centro
- Vertical: snap a incrementos de
U_HEIGHT(35px)
Detección de colisiones
| Función | Descripción |
|---|---|
checkCollisionU(targetU, uHeight, ignoreNode) | Solapamiento vertical (1D) |
checkCollision(targetU, uHeight, slot, ignoreNode) | Vertical + horizontal (slot). FULL bloquea cualquier posición. LEFT y RIGHT coexisten si no se solapan verticalmente |
Flujo drag-end
dragend →
1. Validar dentro de límites del rack
2. Calcular u_position desde Y pixel
3. checkCollision(targetU, uHeight, slot)
4. Si colisión → revertir posición anterior
5. Si válido → actualizar device.u_position y device.slot
6. Disparar auto-save
Undo/Redo (history_adapter.js)
Usuario edita → saveState() → createSnapshot() → STATE_MANAGER.push(snapshot)
Usuario Ctrl+Z → STATE_MANAGER.undo() → restoreState(snapshot)
→ limpia STATE.devices
→ re-crea nodos Konva
→ restaura selección
→ dispara auto-save
Auto-save (editor_api.js)
Debounce 500ms. Serializa STATE.devices y envía:
PUT /api/racks/{rackId}/devices
Body: { devices: [{ db_id, type, name, u_position, u_height, notes,
image, half_width, slot, ports, management_config }] }
Panel de propiedades
Secciones (PropertiesPanelRenderer.js)
- Cabecera: nombre editable, tipo
- Imagen: preview stencil
- Posición: U position (lectura) + altura en U
- Management Config: IP, vendor, hostname
- Puertos Ethernet: grid hasta 48 puertos (formato enriquecido o legacy)
- Puertos SFP: grid hasta 8 puertos (layout 2 columnas)
- Notas: textarea editable
- Acciones: Delete, Clone, Save to Library, QR, SSH, Backups
Formato de puertos
// Enriquecido (preferido)
{ "eth0": { "live": "up", "label": "WAN", "desired": "trunk" } }
// Legacy (string simple)
{ "eth0": "connected" }
El renderer detecta automáticamente el formato.
Eventos clave (PropertiesPanelEvents.js)
| Evento | Acción |
|---|---|
| Input nombre | Actualiza konvaNode.findOne('Text') + debounce auto-save |
| Input puerto | Actualiza device.ports[key] + auto-save |
| Blur cualquier campo | dispatchEvent('request-save-history') → snapshot Undo |
| Botón Clone | Duplica en siguiente U disponible |
| Botón Save to Library | POST /api/racks/stencils/create_from_device |
| Botón SSH | Abre SshConfigModal → ssh_integration.openSSHSession() |
| Botón Backups | Abre modal de snapshots de configuración |
Sistema de stencils (ui_library.js)
Estructura de un stencil
{
id: 123,
name: "Cisco Catalyst 2960",
image_path: "/media/uploads/stencils/cisco_2960.png",
default_u_height: 1,
manufacturer: "Cisco",
category: "Switches",
extra_data: '{"half_width": false, "ports": {...}}'
}
Drag desde librería al canvas
- Stencil con
data-id,data-name,data-type,data-height,data-image dragstart→ metadata enSTATE.currentDragItemdropsobre canvas → calcula U desde coordenadas del mouseaddDeviceToRack()con datos del stencilSTATE.currentDragItem = null
Importación Visio (pipeline 2 pasos)
POST /api/racks/visio/analyze(FormData .vssx/.vdx/.vss) →session_id+ masters encontradosPOST /api/racks/visio/confirm{ session_id, masters, category_name }→ crea stencilsDELETE /api/racks/cleanup/session/{id}→ limpia archivos temporales
Auto-Provision Panel (auto_provision_panel.js)
Lee localStorage['auto_provision_pending'] al cargar el editor:
// Formato v2 (actual)
{
version: 2,
racks: {
"rack_42": [
{ name: "SW-Core-01", type: "switch", stencil_id: 123,
management_config: { ip: "10.0.1.1", vendor: "cisco" } }
]
}
}
Solo muestra dispositivos pendientes para el rack actual. Al arrastrar un stencil al canvas, el management_config se auto-rellena desde el pending device y se elimina del localStorage.
SSH Integration
Flujo de conexión
- Usuario click “SSH” en panel de propiedades
SshConfigModalmuestra formulario con datos demanagement_config- Check de alcanzabilidad:
GET http://localhost:5050/check?host={ip}&port={port} - Usuario confirma →
openSSHSession(deviceId, displayName) - Modal con terminal xterm.js embebido
- WebSocket:
ws://localhost:5050/ws/terminal/{sessionId}via Local Agent
Estado global SSH
window.editorSSHClient // Instancia SSH activa
window.editorDeviceId // ID del dispositivo conectado
window.editorSFTP // Instancia SFTPFileManager
Backups de configuración (backups.js)
Snapshots de running-config via SSH.
| Acción | Endpoint | Descripción |
|---|---|---|
| Crear | POST /api/device/{id}/backup | SSH → running-config → almacena con hash SHA256 |
| Listar | GET /api/device/{id}/backups | Historial con timestamps |
| Diff | Frontend | Compara dos backups lado a lado |
| Restore | POST /api/device/{id}/backup/{bid}/restore | Aplica config al dispositivo |
Vendors soportados: Cisco IOS/IOS-XE · Arista EOS · Juniper JunOS
SFTP Sidebar (sftp_ui.js)
sftp_ui.js → HTTP → Local Agent :5050 → asyncssh → Dispositivo de red
Endpoints Agent:
| Endpoint | Descripción |
|---|---|
POST /sftp/list | Listar directorio { client_id, path } |
POST /sftp/download | Descargar archivo |
POST /sftp/upload | Subir archivo (multipart) |
POST /sftp/mkdir | Crear directorio |
POST /sftp/delete | Eliminar |
Modelos Django
Rack
class Rack(models.Model):
name, location, notes
height_u = IntegerField(default=42)
status # active | planned | deprecated | maintenance
organization = FK(Organization)
groups = M2M(RackGroup)
deleted_at = DateTimeField(null=True) # Soft delete
is_template = BooleanField(default=False)
Device
class Device(models.Model):
rack = FK(Rack, on_delete=CASCADE)
name, notes
u_position = IntegerField()
u_height = IntegerField(default=1)
model_data = TextField(default="{}") # JSON: half_width, slot, ports, image, type
management_config = TextField(default="{}") # JSON: ip, vendor (credenciales Fernet encrypted)
status # online | offline | warning | unknown
@property
def management_ip(self) # Extrae IP de management_config
@property
def management_vendor(self) # Extrae vendor de management_config
Stencil
class Stencil(models.Model):
name, image_path
default_u_height = IntegerField(default=1)
manufacturer, category
extra_data = TextField(default="{}") # JSON: half_width, ports
organization = FK(Organization, null=True) # null = stencil global del sistema
ConfigBackup
class ConfigBackup(models.Model):
device = FK(Device), rack = FK(Rack)
config_text = TextField() # Running-config completo
config_hash = CharField(max_length=64) # SHA256
backup_type # auto | manual | pre_change
vendor, device_version
created_by = FK(User, null=True)
API Endpoints (41+ bajo /api/racks/)
Racks CRUD
| Método | URL | Descripción |
|---|---|---|
| POST | /api/racks/ | Crear rack |
| GET | /api/racks/{id} | Rack completo con dispositivos |
| PUT | /api/racks/{id} | Actualizar metadata |
| DELETE | /api/racks/{id} | Soft-delete |
| PUT | /api/racks/{id}/devices | Auto-save — sincronizar dispositivos |
| POST | /api/racks/{id}/clone | Clonar rack completo |
| POST | /api/racks/{id}/rename | Renombrar |
| POST | /api/racks/{id}/create_template | Guardar como template |
| POST | /api/racks/{id}/apply_template | Aplicar template |
| GET | /api/racks/list | Lista ligera |
| PUT | /api/racks/bulk-update | Actualización masiva |
Stencils y librería
| Método | URL | Descripción |
|---|---|---|
| GET | /api/racks/stencils | Listar (sistema + org) |
| PUT/DELETE | /api/racks/stencils/{id} | Editar / eliminar |
| PUT | /api/racks/stencils/{id}/move | Mover categoría |
| POST | /api/racks/stencils/create_from_image | Desde imagen upload |
| POST | /api/racks/stencils/create_from_device | Desde dispositivo en canvas |
| POST | /api/racks/visio/analyze | Analizar archivo Visio |
| POST | /api/racks/visio/confirm | Confirmar importación |
| GET | /api/racks/library/backup | Exportar librería JSON |
| POST | /api/racks/library/restore/analyze | Analizar backup |
| POST | /api/racks/library/restore/confirm | Confirmar restauración |
| DELETE/PUT | /api/racks/library/category/delete / rename | Gestión categorías |
Export, Labels, Trash
| Método | URL | Descripción |
|---|---|---|
| GET | /api/racks/export/csv | Todos los racks CSV |
| GET | /api/racks/{id}/export/csv | Rack específico CSV |
| GET | /api/racks/labels/{target} | QR labels |
| GET/POST/DELETE | /api/racks/trash/racks* | Papelera (listar, restaurar, eliminar) |
Flujo de carga del editor
GET /editor/{rack_id}/
↓ Django → template editor.html
↓ main.js bootstrap
├── Lee #rack-data del DOM → STATE.rackData
├── Calcula CONFIG.RACK_HEIGHT = height_u × U_HEIGHT
├── konva_setup.js → Stage, Layer, rackGroup
├── fitRackToView() → centra rack en viewport
├── devices.js → addDeviceToRack() por cada device
├── ui_library.js → carga stencils en panel izquierdo
├── auto_provision_panel.js → check localStorage pending
└── history_adapter.js → snapshot inicial
Convenciones y restricciones
- El auto-save tiene debounce 500ms — no llamar a
PUT /api/racks/{id}/devicesdirectamente, usartriggerAutoSave() - Siempre guardar snapshot Undo antes de operaciones destructivas:
dispatchEvent('request-save-history') - Los nodos Konva se destruyen con
konvaNode.destroy()— no eliminar del arraySTATE.devicessin destruir el nodo primero management_configen el modelo Device tiene credenciales cifradas con Fernet — no acceder directamente, usar_decrypt_if_needed()- Stencils con
organization=nullson globales del sistema — no eliminarlos desde la UI de organización - Soft delete en Rack: usar
rack.soft_delete()— nuncarack.delete() - Formato de puertos enriquecido
{live, label, desired}es el preferido para nuevos dispositivos — el renderer soporta ambos pero el legacy (string) es solo para retrocompatibilidad
Véase también
- [[concept—racks—editor]] — concepto del Rack Editor
- [[crearack-tech—backend—rack-editor]] — implementación backend del Rack Editor
- [[crearack—conceptos—rack-editor-conceptos]] — conceptos base del Rack Editor
- [[crearack—racks—crear-rack]] — crear un rack desde la UI
- [[crearack—racks—dispositivos]] — dispositivos dentro del rack
- [[crearack—racks—stencils-crear]] — crear stencils personalizados
- [[entity—racks—model—rack]] — modelo principal del Rack Editor
- [[entity—racks—model—device]] — dispositivos instalados en un rack
- [[entity—racks—model—stencil]] — plantillas visuales de dispositivos