Initial commit: CloudRestoreAS v1.0.0 - Aplicación completa de restauración automática SQL Server

This commit is contained in:
2026-01-25 16:53:00 -07:00
commit 854ffa116f
43 changed files with 6357 additions and 0 deletions

257
app/ui/nodes_tab.py Normal file
View File

@@ -0,0 +1,257 @@
"""Tab de gestión de nodos."""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
QTableWidgetItem, QPushButton, QDialog, QFormLayout,
QLineEdit, QTextEdit, QCheckBox, QDialogButtonBox,
QMessageBox
)
from PySide6.QtCore import Qt
from ..db.node_repository import NodeRepository
class NodeDialog(QDialog):
"""Diálogo para crear/editar nodos."""
def __init__(self, parent=None, node=None):
"""Inicializa el diálogo."""
super().__init__(parent)
self.node = node
self.setWindowTitle("Editar Nodo" if node else "Nuevo Nodo")
self.setMinimumWidth(400)
self._setup_ui()
if node:
self._load_node_data()
def _setup_ui(self):
"""Configura la interfaz."""
layout = QFormLayout(self)
self.node_name_input = QLineEdit()
self.node_name_input.setPlaceholderText("Ej: BACKUP_SISTEMA.ZIP")
layout.addRow("Nombre del Nodo:", self.node_name_input)
self.db_name_input = QLineEdit()
self.db_name_input.setPlaceholderText("Ej: Sistema_DB")
layout.addRow("Base de Datos:", self.db_name_input)
self.active_checkbox = QCheckBox()
self.active_checkbox.setChecked(True)
layout.addRow("Activo:", self.active_checkbox)
self.notes_input = QTextEdit()
self.notes_input.setPlaceholderText("Notas opcionales...")
self.notes_input.setMaximumHeight(100)
layout.addRow("Notas:", self.notes_input)
# Botones
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addRow(buttons)
def _load_node_data(self):
"""Carga los datos del nodo a editar."""
self.node_name_input.setText(self.node.node_name)
self.node_name_input.setEnabled(False) # No permitir cambiar el nombre
self.db_name_input.setText(self.node.db_name)
self.active_checkbox.setChecked(self.node.active)
self.notes_input.setPlainText(self.node.notes or "")
def get_data(self):
"""Obtiene los datos del formulario."""
return {
"node_name": self.node_name_input.text().strip(),
"db_name": self.db_name_input.text().strip(),
"active": self.active_checkbox.isChecked(),
"notes": self.notes_input.toPlainText().strip()
}
class NodesTab(QWidget):
"""Tab de gestión de nodos."""
def __init__(self, engine):
"""Inicializa el tab."""
super().__init__()
self.engine = engine
self._setup_ui()
self.refresh_nodes()
def _setup_ui(self):
"""Configura la interfaz."""
layout = QVBoxLayout(self)
# Botones de acción
actions_layout = QHBoxLayout()
self.add_btn = QPushButton(" Nuevo Nodo")
self.add_btn.clicked.connect(self._add_node)
actions_layout.addWidget(self.add_btn)
self.edit_btn = QPushButton("✏️ Editar")
self.edit_btn.clicked.connect(self._edit_node)
self.edit_btn.setEnabled(False)
actions_layout.addWidget(self.edit_btn)
self.delete_btn = QPushButton("🗑️ Eliminar")
self.delete_btn.clicked.connect(self._delete_node)
self.delete_btn.setEnabled(False)
actions_layout.addWidget(self.delete_btn)
actions_layout.addStretch()
refresh_btn = QPushButton("🔄 Actualizar")
refresh_btn.clicked.connect(self.refresh_nodes)
actions_layout.addWidget(refresh_btn)
layout.addLayout(actions_layout)
# Tabla de nodos
self.nodes_table = QTableWidget()
self.nodes_table.setColumnCount(4)
self.nodes_table.setHorizontalHeaderLabels([
"Nombre del Nodo", "Base de Datos", "Activo", "Notas"
])
self.nodes_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.nodes_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
self.nodes_table.itemSelectionChanged.connect(self._on_selection_changed)
self.nodes_table.itemDoubleClicked.connect(self._edit_node)
layout.addWidget(self.nodes_table)
# Info
info_label = QLabel(
"💡 El nombre del nodo debe coincidir con el nombre del archivo ZIP (con extensión). "
"Se compara en MAYÚSCULAS y case-insensitive."
)
info_label.setWordWrap(True)
info_label.setStyleSheet("color: #666; font-size: 10pt;")
layout.addWidget(info_label)
def refresh_nodes(self):
"""Refresca la lista de nodos."""
nodes = NodeRepository.get_all()
self.nodes_table.setRowCount(len(nodes))
for i, node in enumerate(nodes):
self.nodes_table.setItem(i, 0, QTableWidgetItem(node.node_name))
self.nodes_table.setItem(i, 1, QTableWidgetItem(node.db_name))
active_item = QTableWidgetItem("" if node.active else "")
active_item.setForeground(
Qt.GlobalColor.darkGreen if node.active else Qt.GlobalColor.red
)
self.nodes_table.setItem(i, 2, active_item)
self.nodes_table.setItem(i, 3, QTableWidgetItem(node.notes or ""))
# Guardar node_name en la fila
self.nodes_table.item(i, 0).setData(Qt.ItemDataRole.UserRole, node.node_name)
self.nodes_table.resizeColumnsToContents()
def _on_selection_changed(self):
"""Maneja el cambio de selección."""
has_selection = len(self.nodes_table.selectedItems()) > 0
self.edit_btn.setEnabled(has_selection)
self.delete_btn.setEnabled(has_selection)
def _add_node(self):
"""Abre el diálogo para agregar un nodo."""
dialog = NodeDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
data = dialog.get_data()
if not data["node_name"] or not data["db_name"]:
QMessageBox.warning(
self,
"Error",
"El nombre del nodo y la base de datos son obligatorios."
)
return
try:
NodeRepository.create(
node_name=data["node_name"],
db_name=data["db_name"],
notes=data["notes"]
)
if not data["active"]:
NodeRepository.update(data["node_name"], active=False)
self.refresh_nodes()
QMessageBox.information(self, "Éxito", "Nodo creado exitosamente.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Error creando nodo: {e}")
def _edit_node(self):
"""Abre el diálogo para editar un nodo."""
selected_items = self.nodes_table.selectedItems()
if not selected_items:
return
row = selected_items[0].row()
node_name = self.nodes_table.item(row, 0).data(Qt.ItemDataRole.UserRole)
node = NodeRepository.get(node_name)
if not node:
return
dialog = NodeDialog(self, node)
if dialog.exec() == QDialog.DialogCode.Accepted:
data = dialog.get_data()
if not data["db_name"]:
QMessageBox.warning(self, "Error", "La base de datos es obligatoria.")
return
try:
NodeRepository.update(
node_name=node_name,
db_name=data["db_name"],
active=data["active"],
notes=data["notes"]
)
self.refresh_nodes()
QMessageBox.information(self, "Éxito", "Nodo actualizado exitosamente.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Error actualizando nodo: {e}")
def _delete_node(self):
"""Elimina el nodo seleccionado."""
selected_items = self.nodes_table.selectedItems()
if not selected_items:
return
row = selected_items[0].row()
node_name = self.nodes_table.item(row, 0).data(Qt.ItemDataRole.UserRole)
reply = QMessageBox.question(
self,
"Confirmar Eliminación",
f"¿Está seguro de eliminar el nodo '{node_name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
NodeRepository.delete(node_name)
self.refresh_nodes()
QMessageBox.information(self, "Éxito", "Nodo eliminado exitosamente.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Error eliminando nodo: {e}")