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

228
app/ui/jobs_tab.py Normal file
View File

@@ -0,0 +1,228 @@
"""Tab de Jobs."""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
QTableWidgetItem, QPushButton, QLineEdit, QLabel,
QComboBox, QSplitter, QTextEdit, QGroupBox
)
from PySide6.QtCore import Qt
from ..db.job_repository import JobRepository
from ..db.job_step_repository import JobStepRepository
from ..constants import JobStatus
class JobsTab(QWidget):
"""Tab de gestión y visualización de jobs."""
def __init__(self, engine):
"""Inicializa el tab."""
super().__init__()
self.engine = engine
self.selected_job_id = None
self._setup_ui()
self.refresh_jobs()
def _setup_ui(self):
"""Configura la interfaz."""
layout = QVBoxLayout(self)
# Filtros
filters_layout = QHBoxLayout()
filters_layout.addWidget(QLabel("Buscar:"))
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Nombre de archivo o Job ID...")
self.search_input.textChanged.connect(self.refresh_jobs)
filters_layout.addWidget(self.search_input)
filters_layout.addWidget(QLabel("Estado:"))
self.status_filter = QComboBox()
self.status_filter.addItems([
"Todos",
JobStatus.QUEUED,
JobStatus.EXTRACTING,
JobStatus.RESTORING,
JobStatus.COMPLETED,
JobStatus.FAILED
])
self.status_filter.currentTextChanged.connect(self.refresh_jobs)
filters_layout.addWidget(self.status_filter)
refresh_btn = QPushButton("🔄 Actualizar")
refresh_btn.clicked.connect(self.refresh_jobs)
filters_layout.addWidget(refresh_btn)
filters_layout.addStretch()
layout.addLayout(filters_layout)
# Splitter para tabla y detalles
splitter = QSplitter(Qt.Orientation.Vertical)
# Tabla de jobs
self.jobs_table = QTableWidget()
self.jobs_table.setColumnCount(8)
self.jobs_table.setHorizontalHeaderLabels([
"Job ID", "Fecha", "Archivo", "Nodo", "DB", "Estado", "Intentos", "Tiempo (s)"
])
self.jobs_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.jobs_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
self.jobs_table.itemSelectionChanged.connect(self._on_selection_changed)
splitter.addWidget(self.jobs_table)
# Detalles del job seleccionado
details_widget = QWidget()
details_layout = QVBoxLayout(details_widget)
details_group = QGroupBox("Detalles del Job")
details_group_layout = QVBoxLayout()
self.job_details_text = QTextEdit()
self.job_details_text.setReadOnly(True)
self.job_details_text.setMaximumHeight(200)
details_group_layout.addWidget(self.job_details_text)
details_group.setLayout(details_group_layout)
details_layout.addWidget(details_group)
steps_group = QGroupBox("Pasos del Job")
steps_group_layout = QVBoxLayout()
self.steps_table = QTableWidget()
self.steps_table.setColumnCount(5)
self.steps_table.setHorizontalHeaderLabels([
"Paso", "Inicio", "Duración (ms)", "Exit Code", "Error"
])
steps_group_layout.addWidget(self.steps_table)
steps_group.setLayout(steps_group_layout)
details_layout.addWidget(steps_group)
splitter.addWidget(details_widget)
splitter.setStretchFactor(0, 3)
splitter.setStretchFactor(1, 2)
layout.addWidget(splitter)
def refresh_jobs(self):
"""Refresca la lista de jobs."""
# Obtener filtros
search_text = self.search_input.text().lower()
status_filter = self.status_filter.currentText()
# Obtener jobs
if status_filter == "Todos":
jobs = JobRepository.get_all(limit=100)
else:
jobs = JobRepository.get_all(status=status_filter, limit=100)
# Filtrar por búsqueda
if search_text:
jobs = [
j for j in jobs
if search_text in j.job_id.lower() or
search_text in j.source_name.lower()
]
# Llenar tabla
self.jobs_table.setRowCount(len(jobs))
for i, job in enumerate(jobs):
self.jobs_table.setItem(i, 0, QTableWidgetItem(job.job_id[:8]))
# Fecha
created = job.created_at.split("T")[0] if "T" in job.created_at else job.created_at
self.jobs_table.setItem(i, 1, QTableWidgetItem(created))
self.jobs_table.setItem(i, 2, QTableWidgetItem(job.source_name))
self.jobs_table.setItem(i, 3, QTableWidgetItem(job.node_name or "N/A"))
self.jobs_table.setItem(i, 4, QTableWidgetItem(job.db_name or "N/A"))
status_item = QTableWidgetItem(job.status)
if job.status == JobStatus.COMPLETED:
status_item.setForeground(Qt.GlobalColor.darkGreen)
elif "failed" in job.status:
status_item.setForeground(Qt.GlobalColor.red)
self.jobs_table.setItem(i, 5, status_item)
self.jobs_table.setItem(i, 6, QTableWidgetItem(str(job.attempts)))
# Tiempo
if job.total_ms:
time_s = job.total_ms / 1000
self.jobs_table.setItem(i, 7, QTableWidgetItem(f"{time_s:.1f}"))
else:
self.jobs_table.setItem(i, 7, QTableWidgetItem("N/A"))
# Guardar job_id en la fila
self.jobs_table.item(i, 0).setData(Qt.ItemDataRole.UserRole, job.job_id)
self.jobs_table.resizeColumnsToContents()
def _on_selection_changed(self):
"""Maneja el cambio de selección en la tabla."""
selected_items = self.jobs_table.selectedItems()
if not selected_items:
return
# Obtener job_id de la fila seleccionada
row = selected_items[0].row()
job_id = self.jobs_table.item(row, 0).data(Qt.ItemDataRole.UserRole)
if job_id != self.selected_job_id:
self.selected_job_id = job_id
self._load_job_details()
def _load_job_details(self):
"""Carga los detalles del job seleccionado."""
if not self.selected_job_id:
return
job = JobRepository.get(self.selected_job_id)
if not job:
return
# Mostrar detalles
details = f"""
Job ID: {job.job_id}
Archivo: {job.source_name}
Ruta: {job.source_path}
Nodo: {job.node_name or 'N/A'}
Base de Datos: {job.db_name or 'N/A'}
Estado: {job.status}
Intentos: {job.attempts}
Creado: {job.created_at}
Iniciado: {job.started_at or 'N/A'}
Finalizado: {job.finished_at or 'N/A'}
Tiempos:
- Total: {job.total_ms or 0} ms
- Extracción: {job.extract_ms or 0} ms
- Filelist: {job.filelist_ms or 0} ms
- Restauración: {job.restore_ms or 0} ms
Último Error: {job.last_error or 'N/A'}
""".strip()
self.job_details_text.setPlainText(details)
# Cargar steps
steps = JobStepRepository.get_by_job(self.selected_job_id)
self.steps_table.setRowCount(len(steps))
for i, step in enumerate(steps):
self.steps_table.setItem(i, 0, QTableWidgetItem(step.step))
started = step.started_at.split("T")[1].split(".")[0] if "T" in step.started_at else step.started_at
self.steps_table.setItem(i, 1, QTableWidgetItem(started))
self.steps_table.setItem(i, 2, QTableWidgetItem(str(step.duration_ms or 0)))
self.steps_table.setItem(i, 3, QTableWidgetItem(str(step.exit_code or "")))
self.steps_table.setItem(i, 4, QTableWidgetItem(step.error or ""))
self.steps_table.resizeColumnsToContents()