Valor para el usuario
Hasta ahora, el modal de restauración solo enseñaba una lista plana de racks con checkboxes, y el resto del contenido de la maleta se escondía detrás de un desplegable “What’s inside this backup”. Desde v1.76.0 el modal abre con el MISMO árbol que el diálogo de creación (v1.74.0): las secciones del dashboard (Maps, Rows, Rooms y “No Map”, con sus racks y conteos) y los dominios completos (Monitoring, Wireless, UPS, DSM, ITSM, Terminal, Cables, Catálogo) — todo a la vista, nada plegado. Además, cada elemento que ya existe en la organización destino (por su identidad natural: nombre de rack o plano, IP de una ficha o target, nombre de script o playlist, stencil) lleva una etiqueta “already exists”, para que el usuario vea qué va a chocar ANTES de decidir. Y junto al árbol aparece el selector de modo: Add as a copy (el histórico: todo se recrea, lo que coincide se duplica) o Update existing (el motor de v1.75.0: lo que coincide se sobrescribe, lo nuevo se añade, nada se borra), con un texto de confirmación distinto y veraz según el modo elegido. Si el backup subido es parcial, el resumen lo avisa.
Tercera de las cuatro piezas de la ventana del task #236 (grill del 21-08). Queda la última: un informe final claro tras aplicar el restore.
Cómo usarla
- Configuration → File Operations → restaurar → seleccionar el ZIP.
- El cliente llama a
POST /api/racks/restore/full/preview; la respuesta cambia de forma — pierde la lista planaracksy ganatree(mismo esquema quebuild_backup_inventoryde la creación) ypartial(bool). - El frontend reutiliza
buildBackupTree()(compartida con el diálogo de creación) para pintar el árbol en el contenedorrestore-tree, con las badges de “ya existe” por rack, sección y dominio. - El usuario desmarca lo que no quiere traer y elige el modo con un radio button (
Add as a copy/Update existing); el texto de confirmación cambia según el modo. - Al confirmar, si algo quedó desmarcado, el cliente añade
selection(JSON del árbol, con los IDs de la maleta) a la petición; si todo sigue marcado, no manda selección y viaja la maleta entera. Elmodesiempre viaja. POST /api/racks/restore/fullrecibeselection+mode; el backend normalizaselectiony lo pasa comotree_selection, que prevalece sobre elrack_idsCSV legacy si ambos llegasen.
Implementación
racks/api/export/restore_preview.py(nuevo, 173 LOC):build_restore_tree(org, data)construye el árbol desde la maleta con la misma regla de sección que el dashboard (el primer plano, en el orden de la maleta, que coloca el rack; sin colocación → “No Map”) y calcula las banderasexists/existingcomparando por identidad natural contra la org destino:Rack.name,(Blueprint.name, Blueprint.mode),DeviceProfile.ip_address(por página Wireless/UPS/DSM),MonitoringTarget.ip_address,Script.name,Playlist.name,(Stencil.name, Stencil.image_path).filter_backup_by_tree(data, selection)poda la maleta (en sitio) a la selección de árbol — reutilizafilter_backup_selection()del restore selectivo ([[feature—racks—restore-selectivo-v171]]) para los racks y la nuevaprune_domains()para los dominios.racks/api/export/backup_selection.py:prune_domains(data, domains)se extrae deprune_backup_data()(antes un bloque inline) para que la poda de dominios sea la MISMA función que usan tanto la creación selectiva ([[feature—backup-restore—creacion-selectiva-v1-74-0]]) como la restauración por árbol — sin duplicar la lógica de qué colecciones caen por dominio, fichas por página, huérfanos cruzados y ventanas de mantenimiento.racks/api/export/restore.py:apply_full_restore_from_zip()gana el parámetrotree_selection=None; si no esNoneprevalece sobrerack_ids(que queda como compat).restore_preview()devuelve ahorapartial(del manifest) ytree(debuild_restore_tree) en vez de la listaracks.restore_full()ganaselection: str = Form("")(JSON), normalizado connormalize_selection()(debackup_selection.py) antes de pasarlo a la tarea Huey.racks/tasks.py:run_full_restore()ganatree_selection=Noney lo propaga aapply_full_restore_from_zip;selectiveen el resultado del job pasa a serrack_ids is not None or tree_selection is not None.- Frontend (
static/js/alpine-components.js,static/js/base.js,templates/base.html):buildBackupTree()gana un parámetrocontainerId(antes fijo abackup-tree) para poder pintar enrestore-treeademás del modal de creación; nuevas badgesexistsBadge/existingBadge. El modal de restauración sustituye la lista de checkboxes de rack y el desplegable “What’s inside” por el árbol compartido más el selector de modo (radioadd/update) conconfirm()veraz por modo — el texto anterior ya se había corregido para no prometer un “overwrite” con safety backup inexistente (incidente del 16-07-2026, ver [[feature—racks—restore-selectivo-v171]]). Cache-busting:base.js?v=5,alpine-components.js?v=4. - i18n: 11 strings EN+ES nuevos vía
scripts/i18n/add_restore_tree_strings.py(patrón polib ya usado en las piezas anteriores de esta ventana). - Tests:
tests/api/test_restore_tree.py(5 casos nuevos) + 1 adaptado entests/api/test_restore_selective.py. Área restore en Docker: 36 tests + mypy en verde.
Commits relacionados
ff6c90e8(2026-08-21) — feat(backups): modal de restauracion con el arbol completo, badges ‘ya existe’ y selector de modo - v1.76.0 (task #236 ventana) (#407).
Véase también
- [[entity—racks—service—apply-full-restore-from-zip]]
- [[feature—racks—restore-selectivo-v171]]
- [[feature—backup-restore—modo-actualizar-v1-75-0]]
- [[feature—backup-restore—creacion-selectiva-v1-74-0]]
- [[feature—backup-restore—extended-domains-v1-70-0]]
- [[feature—backup-restore—manifest-verificacion-v1-69-0]]
- [[feature—racks—backup-restore-async-v156]]
- [[crearack-tech—admin—backup-restore]]