Primera version

This commit is contained in:
Miguel 2025-09-01 17:50:14 +02:00
parent c8ceb8fddd
commit b9aed346ab
15 changed files with 1563 additions and 212 deletions

View File

@ -523,3 +523,9 @@ Una vez que tengas respuestas a estas preguntas, podremos:
4. Definir el plan de testing
¿Podrías revisar estas preguntas y darme tus preferencias para cada una?
Para ejecutar el programa se puede usar:
'conda activate autobackups ; python src/app.py'

View File

@ -1,7 +1,7 @@
{
"observation_directories": [
{
"path": "C:\\Projects\\Siemens",
"path": "C:\\Users\\migue\\Downloads\\TestBackups",
"type": "siemens_s7",
"enabled": true,
"description": "Directorio principal de proyectos Siemens"
@ -37,7 +37,7 @@
},
"web_interface": {
"host": "127.0.0.1",
"port": 5000,
"port": 5120,
"debug": false
},
"logging": {

View File

@ -1,8 +1,8 @@
{
"metadata": {
"version": "1.0",
"last_updated": "2025-09-01T08:30:15Z",
"total_projects": 0
"last_updated": "2025-09-01T15:49:25.929290+00:00",
"total_projects": 2
},
"projects": [
{
@ -49,6 +49,51 @@
"discovery_method": "everything_api",
"auto_discovered": true
}
},
{
"id": "",
"name": "Ssae04_14 - TIA",
"path": "C:\\Users\\migue\\Downloads\\TestBackups\\LineaB\\Ssae04_14 - TIA",
"type": "siemens_s7",
"s7p_file": "C:\\Users\\migue\\Downloads\\TestBackups\\LineaB\\Ssae04_14 - TIA\\Ssae0452.s7p",
"observation_directory": "C:\\Users\\migue\\Downloads\\TestBackups",
"relative_path": "LineaB\\Ssae04_14 - TIA",
"backup_path": "LineaB\\Ssae04_14 - TIA",
"schedule_config": {
"schedule": "daily",
"schedule_time": "02:00",
"enabled": true,
"next_scheduled_backup": ""
},
"backup_history": {
"last_backup_date": "",
"last_backup_file": "",
"backup_count": 0,
"last_successful_backup": ""
},
"hash_info": {
"last_s7p_hash": "",
"last_full_hash": "",
"last_s7p_timestamp": "",
"last_s7p_size": 0,
"last_scan_timestamp": "",
"file_count": 0,
"total_size_bytes": 0
},
"status": {
"current_status": "ready",
"last_error": null,
"retry_count": 0,
"next_retry": null,
"files_in_use": false,
"exclusivity_check_passed": true,
"last_status_update": ""
},
"discovery_info": {
"discovered_date": "",
"discovery_method": "",
"auto_discovered": true
}
}
],
"statistics": {
@ -59,4 +104,4 @@
"projects_with_errors": 0,
"projects_pending_retry": 0
}
}
}

View File

@ -1,5 +1,5 @@
"""
AutoBackups - Aplicación Principal
AutoBackups - Aplicación Principal Flask
Sistema automatizado de backup para proyectos Simatic S7
"""
@ -8,6 +8,10 @@ import os
import logging
from pathlib import Path
from datetime import datetime
from flask import Flask, render_template, request, jsonify, redirect, url_for
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
import atexit
# Agregar el directorio src al path para imports
current_dir = Path(__file__).parent
@ -19,104 +23,172 @@ from models.config_model import Config
from models.project_model import ProjectManager
from utils.file_utils import DiskSpaceChecker
from services.basic_backup_service import BasicBackupService
from routes import register_api_routes, register_web_routes
class AutoBackupsApp:
"""Aplicación principal de AutoBackups"""
# Crear instancia Flask
app = Flask(__name__, template_folder="../templates", static_folder="../static")
class AutoBackupsFlaskApp:
"""Aplicación principal de AutoBackups con Flask"""
def __init__(self):
self.config = None
self.project_manager = None
self.discovery_service = None
self.disk_checker = None
self.backup_service = None
self.scheduler = None
self.logger = None
# Inicializar aplicación
self._setup_logging()
self._load_configuration()
self._initialize_services()
self._setup_scheduler()
self._register_routes()
def _setup_logging(self):
"""Configurar sistema de logging"""
try:
# Crear directorio de logs si no existe
logs_dir = Path(__file__).parent / ".logs"
logs_dir = Path(__file__).parent.parent / ".logs"
logs_dir.mkdir(exist_ok=True)
# Nombre del archivo de log con timestamp
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_filename = logs_dir / f"autobackups_{timestamp}.log"
# Configurar logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(log_filename, encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
logging.FileHandler(log_filename, encoding="utf-8"),
logging.StreamHandler(sys.stdout),
],
)
self.logger = logging.getLogger(__name__)
self.logger.info(f"AutoBackups iniciado - Log: {log_filename}")
self.logger.info(f"AutoBackups Flask iniciado - Log: {log_filename}")
except Exception as e:
print(f"Error configurando logging: {e}")
sys.exit(1)
def _load_configuration(self):
"""Cargar configuración del sistema"""
try:
self.logger.info("Cargando configuración...")
self.config = Config()
self.logger.info("Configuración cargada exitosamente")
# Mostrar información básica de configuración
self.logger.info(f"Directorios de observación: {len(self.config.observation_directories)}")
self.logger.info(f"Destino de backups: {self.config.backup_destination}")
dirs_count = len(self.config.observation_directories)
self.logger.info(f"Directorios de observación: {dirs_count}")
self.logger.info(f"Destino: {self.config.backup_destination}")
except Exception as e:
self.logger.error(f"Error cargando configuración: {e}")
sys.exit(1)
def _initialize_services(self):
"""Inicializar servicios principales"""
try:
self.logger.info("Inicializando servicios...")
# Project Manager
self.project_manager = ProjectManager()
self.logger.info("Project Manager inicializado")
# Disk Space Checker
self.disk_checker = DiskSpaceChecker()
self.logger.info("Disk Space Checker inicializado")
# Basic Backup Service (Fase 1)
# Basic Backup Service
self.backup_service = BasicBackupService(self.config)
self.logger.info("Basic Backup Service inicializado")
# Project Discovery Service (temporalmente comentado)
# self.discovery_service = ProjectDiscoveryService(self.config, self.project_manager)
# self.logger.info("Project Discovery Service inicializado")
self.logger.info("Todos los servicios inicializados correctamente")
except Exception as e:
self.logger.error(f"Error inicializando servicios: {e}")
sys.exit(1)
def _setup_scheduler(self):
"""Configurar scheduler para tareas automáticas"""
try:
self.scheduler = BackgroundScheduler(timezone="America/Mexico_City")
# Agregar job de escaneo cada hora
self.scheduler.add_job(
func=self.scheduled_project_scan,
trigger=CronTrigger(minute=0), # Cada hora en el minuto 0
id="project_scan",
name="Escaneo de proyectos automático",
replace_existing=True,
)
# Iniciar scheduler
self.scheduler.start()
self.logger.info("Scheduler configurado e iniciado")
# Asegurar que el scheduler se detenga al cerrar la aplicación
atexit.register(lambda: self.scheduler.shutdown())
except Exception as e:
self.logger.error(f"Error configurando scheduler: {e}")
def _register_routes(self):
"""Registrar rutas de Flask usando módulos separados"""
register_web_routes(app, self)
register_api_routes(app, self)
self.logger.info("Rutas Flask registradas desde módulos")
def scheduled_project_scan(self):
"""Escaneo programado de proyectos"""
try:
self.logger.info("Iniciando escaneo programado de proyectos...")
projects_found = self.discover_projects()
self.logger.info(f"Escaneo completado: {projects_found} proyectos")
except Exception as e:
self.logger.error(f"Error en escaneo programado: {e}")
def discover_projects(self) -> int:
"""Descubrir proyectos en directorios de observación"""
try:
self.logger.info("Iniciando descubrimiento de proyectos...")
# Usar el servicio básico de backup
if hasattr(self, "backup_service"):
projects = self.backup_service.discover_projects_basic()
else:
projects = []
# Agregar proyectos al manager
for project_info in projects:
self.project_manager.add_or_update_project(project_info)
msg = f"Descubrimiento completado: {len(projects)} proyectos"
self.logger.info(msg)
return len(projects)
except Exception as e:
self.logger.error(f"Error en descubrimiento de proyectos: {e}")
return 0
def check_system_requirements(self) -> bool:
"""Verificar requerimientos del sistema"""
try:
self.logger.info("Verificando requerimientos del sistema...")
# Verificar espacio en disco
backup_destination = self.config.backup_destination
min_space_mb = self.config.get_min_free_space_mb()
free_space_mb = self.disk_checker.get_free_space_mb(backup_destination)
if free_space_mb < min_space_mb:
self.logger.error(
f"Espacio insuficiente en destino de backup. "
@ -124,150 +196,58 @@ class AutoBackupsApp:
f"Requerido: {min_space_mb}MB"
)
return False
self.logger.info(f"Espacio en disco OK: {free_space_mb:.1f}MB disponibles")
# Verificar Everything API (opcional en Fase 1)
if hasattr(self, 'backup_service'):
if self.backup_service.check_system_requirements():
self.logger.info("Requerimientos del sistema verificados")
else:
self.logger.warning("Algunos requerimientos fallaron")
else:
self.logger.warning("Backup service no disponible")
# Verificar directorios de observación
missing_dirs = []
for obs_dir in self.config.observation_directories:
if not Path(obs_dir["path"]).exists():
missing_dirs.append(obs_dir["path"])
if missing_dirs:
self.logger.warning(f"Directorios de observación no encontrados: {missing_dirs}")
self.logger.info("Verificación de requerimientos completada")
return True
except Exception as e:
self.logger.error(f"Error verificando requerimientos del sistema: {e}")
return False
def discover_projects(self) -> int:
"""Descubrir proyectos en directorios de observación"""
try:
self.logger.info("Iniciando descubrimiento de proyectos...")
# Usar el servicio básico de backup
if hasattr(self, 'backup_service'):
projects = self.backup_service.discover_projects_basic()
else:
projects = []
# Agregar proyectos al manager
for project_info in projects:
self.project_manager.add_or_update_project(project_info)
msg = f"Descubrimiento completado: {len(projects)} proyectos"
self.logger.info(msg)
return len(projects)
except Exception as e:
self.logger.error(f"Error en descubrimiento de proyectos: {e}")
return 0
def show_system_status(self):
"""Mostrar estado actual del sistema"""
try:
self.logger.info("=== ESTADO DEL SISTEMA ===")
# Información de configuración
self.logger.info(f"Directorios de observación configurados: {len(self.config.observation_directories)}")
self.logger.info(f"Destino de backups: {self.config.backup_destination}")
# Información de proyectos
all_projects = self.project_manager.get_all_projects()
enabled_projects = self.project_manager.get_enabled_projects()
self.logger.info(f"Total de proyectos: {len(all_projects)}")
self.logger.info(f"Proyectos habilitados: {len(enabled_projects)}")
# Información de espacio en disco
backup_dest = self.config.backup_destination
total_mb, used_mb, free_mb = self.disk_checker.get_disk_usage_info(backup_dest)
self.logger.info(f"Espacio en disco (destino backup):")
self.logger.info(f" Total: {total_mb:.1f}MB")
self.logger.info(f" Usado: {used_mb:.1f}MB")
self.logger.info(f" Libre: {free_mb:.1f}MB")
# Información de Everything API
if self.discovery_service.everything_searcher:
self.logger.info("Everything API: Disponible")
else:
self.logger.info("Everything API: No disponible")
self.logger.info("=== FIN ESTADO DEL SISTEMA ===")
except Exception as e:
self.logger.error(f"Error mostrando estado del sistema: {e}")
def run_initial_setup(self):
"""Ejecutar configuración inicial de la aplicación"""
try:
self.logger.info("=== CONFIGURACIÓN INICIAL DE AUTOBACKUPS ===")
# Verificar requerimientos del sistema
if not self.check_system_requirements():
self.logger.error("Los requerimientos del sistema no se cumplen")
return False
# Descubrir proyectos
projects_found = self.discover_projects()
if projects_found == 0:
self.logger.warning("No se encontraron proyectos para backup")
# Mostrar estado del sistema
self.show_system_status()
self.logger.info("=== CONFIGURACIÓN INICIAL COMPLETADA ===")
return True
except Exception as e:
self.logger.error(f"Error en configuración inicial: {e}")
return False
# Instancia global de la aplicación
autobackups_app = None
def main():
"""Función principal"""
global autobackups_app
try:
print("AutoBackups - Sistema de Backup Automatizado")
print("=" * 50)
# Crear y configurar aplicación
app = AutoBackupsApp()
# Ejecutar configuración inicial
if app.run_initial_setup():
app.logger.info("AutoBackups configurado correctamente")
# TODO: En las siguientes fases:
# - Iniciar scheduler de backups
# - Iniciar interfaz web Flask
# - Configurar tareas en background
print("\nConfiguración inicial completada.")
print("Revisa el archivo de log para más detalles.")
else:
print("Error en la configuración inicial. Revisa los logs.")
print("AutoBackups - Sistema de Backup Automatizado (Flask)")
print("=" * 55)
# Crear y configurar aplicación Flask
autobackups_app = AutoBackupsFlaskApp()
# Verificar requerimientos del sistema
if not autobackups_app.check_system_requirements():
print("Error en la verificación de requerimientos del sistema.")
sys.exit(1)
# Ejecutar descubrimiento inicial de proyectos
projects_found = autobackups_app.discover_projects()
print(f"Descubrimiento inicial: {projects_found} proyectos encontrados")
# Configurar Flask
host = autobackups_app.config.web_interface.get("host", "127.0.0.1")
port = autobackups_app.config.web_interface.get("port", 5000)
debug = autobackups_app.config.web_interface.get("debug", False)
print(f"\nServidor web iniciando en http://{host}:{port}")
print("Presiona Ctrl+C para detener el servidor")
# Iniciar servidor Flask
app.run(host=host, port=port, debug=debug, use_reloader=False)
except KeyboardInterrupt:
print("\nAplicación interrumpida por el usuario")
if autobackups_app and autobackups_app.scheduler:
autobackups_app.scheduler.shutdown()
except Exception as e:
print(f"Error inesperado: {e}")
if autobackups_app and autobackups_app.scheduler:
autobackups_app.scheduler.shutdown()
sys.exit(1)

View File

@ -12,6 +12,7 @@ from typing import Dict, List, Any, Optional
class ProjectStatus(Enum):
"""Estados posibles de un proyecto"""
READY = "ready"
BACKING_UP = "backing_up"
ERROR = "error"
@ -22,7 +23,7 @@ class ProjectStatus(Enum):
class Project:
"""Clase para representar un proyecto individual"""
def __init__(self, project_data: Dict[str, Any]):
self.id = project_data.get("id", "")
self.name = project_data.get("name", "")
@ -32,21 +33,21 @@ class Project:
self.observation_directory = project_data.get("observation_directory", "")
self.relative_path = project_data.get("relative_path", "")
self.backup_path = project_data.get("backup_path", "")
# Configuración de schedule
schedule_config = project_data.get("schedule_config", {})
self.schedule = schedule_config.get("schedule", "daily")
self.schedule_time = schedule_config.get("schedule_time", "02:00")
self.enabled = schedule_config.get("enabled", True)
self.next_scheduled_backup = schedule_config.get("next_scheduled_backup", "")
# Historia de backups
backup_history = project_data.get("backup_history", {})
self.last_backup_date = backup_history.get("last_backup_date", "")
self.last_backup_file = backup_history.get("last_backup_file", "")
self.backup_count = backup_history.get("backup_count", 0)
self.last_successful_backup = backup_history.get("last_successful_backup", "")
# Información de hash
hash_info = project_data.get("hash_info", {})
self.last_s7p_hash = hash_info.get("last_s7p_hash", "")
@ -56,7 +57,7 @@ class Project:
self.last_scan_timestamp = hash_info.get("last_scan_timestamp", "")
self.file_count = hash_info.get("file_count", 0)
self.total_size_bytes = hash_info.get("total_size_bytes", 0)
# Estado actual
status = project_data.get("status", {})
self.current_status = ProjectStatus(status.get("current_status", "ready"))
@ -66,13 +67,13 @@ class Project:
self.files_in_use = status.get("files_in_use", False)
self.exclusivity_check_passed = status.get("exclusivity_check_passed", True)
self.last_status_update = status.get("last_status_update", "")
# Información de descubrimiento
discovery_info = project_data.get("discovery_info", {})
self.discovered_date = discovery_info.get("discovered_date", "")
self.discovery_method = discovery_info.get("discovery_method", "")
self.auto_discovered = discovery_info.get("auto_discovered", True)
def to_dict(self) -> Dict[str, Any]:
"""Convertir el proyecto a diccionario para serialización JSON"""
return {
@ -88,13 +89,13 @@ class Project:
"schedule": self.schedule,
"schedule_time": self.schedule_time,
"enabled": self.enabled,
"next_scheduled_backup": self.next_scheduled_backup
"next_scheduled_backup": self.next_scheduled_backup,
},
"backup_history": {
"last_backup_date": self.last_backup_date,
"last_backup_file": self.last_backup_file,
"backup_count": self.backup_count,
"last_successful_backup": self.last_successful_backup
"last_successful_backup": self.last_successful_backup,
},
"hash_info": {
"last_s7p_hash": self.last_s7p_hash,
@ -103,7 +104,7 @@ class Project:
"last_s7p_size": self.last_s7p_size,
"last_scan_timestamp": self.last_scan_timestamp,
"file_count": self.file_count,
"total_size_bytes": self.total_size_bytes
"total_size_bytes": self.total_size_bytes,
},
"status": {
"current_status": self.current_status.value,
@ -112,30 +113,36 @@ class Project:
"next_retry": self.next_retry,
"files_in_use": self.files_in_use,
"exclusivity_check_passed": self.exclusivity_check_passed,
"last_status_update": self.last_status_update
"last_status_update": self.last_status_update,
},
"discovery_info": {
"discovered_date": self.discovered_date,
"discovery_method": self.discovery_method,
"auto_discovered": self.auto_discovered
}
"auto_discovered": self.auto_discovered,
},
}
def update_status(self, status: ProjectStatus, error_message: str = None) -> None:
"""Actualizar el estado del proyecto"""
self.current_status = status
self.last_error = error_message
self.last_status_update = datetime.now(timezone.utc).isoformat()
if status == ProjectStatus.ERROR:
self.retry_count += 1
elif status == ProjectStatus.READY:
self.retry_count = 0
self.last_error = None
def update_hash_info(self, s7p_hash: str = None, full_hash: str = None,
s7p_timestamp: str = None, s7p_size: int = None,
file_count: int = None, total_size: int = None) -> None:
def update_hash_info(
self,
s7p_hash: str = None,
full_hash: str = None,
s7p_timestamp: str = None,
s7p_size: int = None,
file_count: int = None,
total_size: int = None,
) -> None:
"""Actualizar información de hash"""
if s7p_hash is not None:
self.last_s7p_hash = s7p_hash
@ -149,9 +156,9 @@ class Project:
self.file_count = file_count
if total_size is not None:
self.total_size_bytes = total_size
self.last_scan_timestamp = datetime.now(timezone.utc).isoformat()
def update_backup_info(self, backup_file_path: str) -> None:
"""Actualizar información después de un backup exitoso"""
now = datetime.now(timezone.utc).isoformat()
@ -164,29 +171,29 @@ class Project:
class ProjectManager:
"""Clase para manejar la colección de proyectos"""
def __init__(self, projects_file_path: str = None):
if projects_file_path is None:
# Buscar projects.json en el directorio del proyecto
current_dir = Path(__file__).parent.parent.parent
projects_file_path = current_dir / "projects.json"
self.projects_file_path = Path(projects_file_path)
self.projects: Dict[str, Project] = {}
self.metadata = {}
self.statistics = {}
self.load_projects()
def load_projects(self) -> None:
"""Cargar proyectos desde archivo JSON"""
try:
if self.projects_file_path.exists():
with open(self.projects_file_path, 'r', encoding='utf-8') as f:
with open(self.projects_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
self.metadata = data.get("metadata", {})
self.statistics = data.get("statistics", {})
# Cargar proyectos
for project_data in data.get("projects", []):
project = Project(project_data)
@ -197,38 +204,38 @@ class ProjectManager:
self.save_projects()
except Exception as e:
raise Exception(f"Error cargando proyectos: {e}")
def save_projects(self) -> None:
"""Guardar proyectos al archivo JSON"""
try:
data = {
"metadata": self.metadata,
"projects": [project.to_dict() for project in self.projects.values()],
"statistics": self.statistics
"statistics": self.statistics,
}
with open(self.projects_file_path, 'w', encoding='utf-8') as f:
with open(self.projects_file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
raise Exception(f"Error guardando proyectos: {e}")
def _create_default_projects_file(self) -> None:
"""Crear archivo de proyectos por defecto"""
self.metadata = {
"version": "1.0",
"last_updated": datetime.now(timezone.utc).isoformat(),
"total_projects": 0
"total_projects": 0,
}
self.statistics = {
"total_backups_created": 0,
"total_backup_size_mb": 0.0,
"average_backup_time_seconds": 0.0,
"last_global_scan": "",
"projects_with_errors": 0,
"projects_pending_retry": 0
"projects_pending_retry": 0,
}
def add_project(self, project_data: Dict[str, Any]) -> Project:
"""Agregar un nuevo proyecto"""
project = Project(project_data)
@ -237,23 +244,53 @@ class ProjectManager:
self.metadata["last_updated"] = datetime.now(timezone.utc).isoformat()
self.save_projects()
return project
def add_or_update_project(self, project_data: Dict[str, Any]) -> Project:
"""Agregar o actualizar un proyecto existente"""
project_id = project_data.get("id", "")
if project_id in self.projects:
# Actualizar proyecto existente
existing_project = self.projects[project_id]
# Mantener algunos datos existentes si no se proporcionan
if "schedule_config" not in project_data and hasattr(
existing_project, "enabled"
):
project_data["schedule_config"] = {
"schedule": existing_project.schedule,
"schedule_time": existing_project.schedule_time,
"enabled": existing_project.enabled,
"next_scheduled_backup": existing_project.next_scheduled_backup,
}
project = Project(project_data)
self.projects[project.id] = project
else:
# Agregar nuevo proyecto
project = Project(project_data)
self.projects[project.id] = project
self.metadata["total_projects"] = len(self.projects)
self.metadata["last_updated"] = datetime.now(timezone.utc).isoformat()
self.save_projects()
return project
def get_project(self, project_id: str) -> Optional[Project]:
"""Obtener un proyecto por ID"""
return self.projects.get(project_id)
def get_all_projects(self) -> List[Project]:
"""Obtener todos los proyectos"""
return list(self.projects.values())
def get_projects_by_status(self, status: ProjectStatus) -> List[Project]:
"""Obtener proyectos por estado"""
return [p for p in self.projects.values() if p.current_status == status]
def get_enabled_projects(self) -> List[Project]:
"""Obtener proyectos habilitados"""
return [p for p in self.projects.values() if p.enabled]
def update_statistics(self) -> None:
"""Actualizar estadísticas generales"""
self.statistics["projects_with_errors"] = len(

5
src/routes/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# Routes Package
from .api_routes import register_api_routes
from .web_routes import register_web_routes
__all__ = ["register_api_routes", "register_web_routes"]

164
src/routes/api_routes.py Normal file
View File

@ -0,0 +1,164 @@
"""
API Routes - AutoBackups
Rutas de la API REST para operaciones AJAX
"""
from flask import request, jsonify
def register_api_routes(app, autobackups_instance):
"""Registrar rutas API de la aplicación"""
@app.route("/api/config")
def get_config():
"""Obtener configuración actual"""
try:
return jsonify(
{
"observation_directories": autobackups_instance.config.observation_directories,
"backup_destination": autobackups_instance.config.backup_destination,
"global_settings": autobackups_instance.config.global_settings,
"web_interface": autobackups_instance.config.web_interface,
}
)
except Exception as e:
autobackups_instance.logger.error(f"Error getting config: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/api/projects")
def get_projects():
"""Obtener lista de proyectos"""
try:
projects = autobackups_instance.project_manager.get_all_projects()
projects_dict = [project.to_dict() for project in projects]
return jsonify({"projects": projects_dict})
except Exception as e:
autobackups_instance.logger.error(f"Error getting projects: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/api/projects/<project_id>/backup", methods=["POST"])
def manual_backup(project_id):
"""Ejecutar backup manual de un proyecto"""
try:
autobackups_instance.logger.info(
f"Backup manual solicitado para: {project_id}"
)
# TODO: Implementar backup manual real
# Por ahora solo simulamos la respuesta
return jsonify(
{
"status": "success",
"message": f"Backup iniciado para proyecto {project_id}",
}
)
except Exception as e:
autobackups_instance.logger.error(f"Error in manual backup: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/api/projects/<project_id>/config", methods=["PUT"])
def update_project_config(project_id):
"""Actualizar configuración de un proyecto"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "No data provided"}), 400
# TODO: Implementar actualización real de configuración
autobackups_instance.logger.info(f"Config actualizada para: {project_id}")
autobackups_instance.logger.info(f"Nueva config: {data}")
return jsonify({"status": "success"})
except Exception as e:
autobackups_instance.logger.error(f"Error updating project config: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/api/scan", methods=["POST"])
def scan_projects():
"""Escanear directorios en busca de nuevos proyectos"""
try:
projects_found = autobackups_instance.discover_projects()
return jsonify({"status": "success", "projects_found": projects_found})
except Exception as e:
autobackups_instance.logger.error(f"Error scanning projects: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/api/system/status")
def system_status():
"""Obtener estado del sistema"""
try:
backup_dest = autobackups_instance.config.backup_destination
total_mb, used_mb, free_mb = (
autobackups_instance.disk_checker.get_disk_usage_info(backup_dest)
)
return jsonify(
{
"disk_space": {
"total_mb": total_mb,
"used_mb": used_mb,
"free_mb": free_mb,
},
"projects": {
"total": len(
autobackups_instance.project_manager.get_all_projects()
),
"enabled": len(
autobackups_instance.project_manager.get_enabled_projects()
),
},
"everything_api": {
"available": hasattr(
autobackups_instance.backup_service, "everything_wrapper"
)
},
}
)
except Exception as e:
autobackups_instance.logger.error(f"Error getting system status: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/api/backup/all", methods=["POST"])
def backup_all_projects():
"""Ejecutar backup de todos los proyectos habilitados"""
try:
enabled_projects = (
autobackups_instance.project_manager.get_enabled_projects()
)
if not enabled_projects:
return jsonify(
{
"status": "warning",
"message": "No hay proyectos habilitados para backup",
}
)
# TODO: Implementar backup de todos los proyectos
autobackups_instance.logger.info(
f"Backup global iniciado para {len(enabled_projects)} proyectos"
)
return jsonify(
{
"status": "success",
"message": f"Backup iniciado para {len(enabled_projects)} proyectos",
"projects_count": len(enabled_projects),
}
)
except Exception as e:
autobackups_instance.logger.error(f"Error in backup all: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/api/logs")
def get_logs():
"""Obtener logs recientes"""
try:
# TODO: Implementar lectura de logs
logs = []
return jsonify({"logs": logs})
except Exception as e:
autobackups_instance.logger.error(f"Error getting logs: {e}")
return jsonify({"error": str(e)}), 500

101
src/routes/web_routes.py Normal file
View File

@ -0,0 +1,101 @@
"""
Web Routes - AutoBackups
Rutas principales de la interfaz web
"""
from flask import render_template, redirect, url_for
def register_web_routes(app, autobackups_instance):
"""Registrar rutas web de la aplicación"""
@app.route("/")
def dashboard():
"""Dashboard principal"""
try:
print("=== DEBUG: Iniciando dashboard ===")
projects = autobackups_instance.project_manager.get_all_projects()
print(f"DEBUG: Proyectos obtenidos: {len(projects)}")
# Verificar el tipo de los proyectos
for i, project in enumerate(projects):
print(f"DEBUG: Proyecto {i}: tipo = {type(project)}")
print(f"DEBUG: Proyecto {i}: atributos = {dir(project)}")
if i == 0: # Solo el primero para no hacer spam
break
enabled_projects = (
autobackups_instance.project_manager.get_enabled_projects()
)
print(f"DEBUG: Proyectos habilitados: {len(enabled_projects)}")
# Información de sistema
backup_dest = autobackups_instance.config.backup_destination
total_mb, used_mb, free_mb = (
autobackups_instance.disk_checker.get_disk_usage_info(backup_dest)
)
stats = {
"total_projects": len(projects),
"enabled_projects": len(enabled_projects),
"total_space_mb": total_mb,
"used_space_mb": used_mb,
"free_space_mb": free_mb,
}
print("DEBUG: Llamando render_template...")
return render_template("dashboard.html", projects=projects, stats=stats)
except Exception as e:
print(f"DEBUG: Error completo: {e}")
import traceback
traceback.print_exc()
autobackups_instance.logger.error(f"Error en dashboard: {e}")
return render_template("error.html", error=str(e))
@app.route("/projects")
def projects_page():
"""Página de gestión de proyectos"""
try:
projects = autobackups_instance.project_manager.get_all_projects()
return render_template("projects.html", projects=projects)
except Exception as e:
autobackups_instance.logger.error(f"Error en página de proyectos: {e}")
return render_template("error.html", error=str(e))
@app.route("/config")
def config_page():
"""Página de configuración"""
try:
config_data = {
"observation_directories": autobackups_instance.config.observation_directories,
"backup_destination": autobackups_instance.config.backup_destination,
"global_settings": autobackups_instance.config.global_settings,
"web_interface": autobackups_instance.config.web_interface,
}
return render_template("config.html", config=config_data)
except Exception as e:
autobackups_instance.logger.error(f"Error en página de configuración: {e}")
return render_template("error.html", error=str(e))
@app.route("/logs")
def logs_page():
"""Página de visualización de logs"""
try:
# TODO: Implementar lectura de logs
logs = []
return render_template("logs.html", logs=logs)
except Exception as e:
autobackups_instance.logger.error(f"Error en página de logs: {e}")
return render_template("error.html", error=str(e))
@app.errorhandler(404)
def not_found(error):
"""Manejo de error 404"""
return render_template("error.html", error="Página no encontrada"), 404
@app.errorhandler(500)
def internal_error(error):
"""Manejo de error 500"""
return render_template("error.html", error="Error interno del servidor"), 500

241
static/css/styles.css Normal file
View File

@ -0,0 +1,241 @@
/* AutoBackups Custom Styles */
:root {
--primary-color: #0d6efd;
--success-color: #198754;
--info-color: #0dcaf0;
--warning-color: #ffc107;
--danger-color: #dc3545;
--dark-color: #212529;
}
/* General Styles */
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
}
/* Cards */
.card {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
transition: box-shadow 0.15s ease-in-out;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.card-header {
background-color: #fff;
border-bottom: 1px solid #dee2e6;
font-weight: 600;
}
/* Statistics Cards */
.card.bg-primary,
.card.bg-success,
.card.bg-info,
.card.bg-warning {
border: none;
}
.card.bg-primary .card-title,
.card.bg-success .card-title,
.card.bg-info .card-title,
.card.bg-warning .card-title {
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.card.bg-primary .card-text,
.card.bg-success .card-text,
.card.bg-info .card-text,
.card.bg-warning .card-text {
font-size: 2rem;
font-weight: bold;
margin: 0;
}
/* Tables */
.table {
background-color: #fff;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.025);
}
.table th {
border-top: none;
font-weight: 600;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
vertical-align: middle;
border-color: #dee2e6;
}
/* Badges */
.badge {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
}
/* Buttons */
.btn {
border-radius: 0.375rem;
font-weight: 500;
}
.btn-group-sm > .btn,
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
/* Alerts */
.alert {
border: none;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.alert-dismissible .btn-close {
padding: 1.25rem 1rem;
}
/* Modals */
.modal-content {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.modal-header {
border-bottom: 1px solid #dee2e6;
padding: 1.5rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #dee2e6;
padding: 1rem 1.5rem;
}
/* Form Controls */
.form-control,
.form-select {
border-radius: 0.375rem;
border: 1px solid #ced4da;
}
.form-control:focus,
.form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
/* Loading States */
.loading {
opacity: 0.6;
pointer-events: none;
}
/* Custom Utilities */
.text-truncate-50 {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
/* Responsive Design */
@media (max-width: 768px) {
.table-responsive {
font-size: 0.875rem;
}
.card.bg-primary .card-text,
.card.bg-success .card-text,
.card.bg-info .card-text,
.card.bg-warning .card-text {
font-size: 1.5rem;
}
.btn-group-sm > .btn {
padding: 0.125rem 0.25rem;
font-size: 0.75rem;
}
}
/* Animation Classes */
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Status Indicators */
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 0.5rem;
}
.status-ready { background-color: var(--success-color); }
.status-error { background-color: var(--danger-color); }
.status-warning { background-color: var(--warning-color); }
.status-info { background-color: var(--info-color); }
/* Progress Bars */
.progress {
height: 0.5rem;
border-radius: 0.25rem;
}
.progress-bar {
transition: width 0.6s ease;
}
/* Custom Scrollbar */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #ced4da #f8f9fa;
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f8f9fa;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #ced4da;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #adb5bd;
}

260
static/js/app.js Normal file
View File

@ -0,0 +1,260 @@
// AutoBackups JavaScript Application
class AutoBackupsApp {
constructor() {
this.init();
}
init() {
this.updateCurrentTime();
this.setupEventListeners();
// Update time every minute
setInterval(() => this.updateCurrentTime(), 60000);
}
updateCurrentTime() {
const now = new Date();
const timeString = now.toLocaleString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
const timeElement = document.getElementById('current-time');
if (timeElement) {
timeElement.textContent = timeString;
}
}
setupEventListeners() {
// Global backup all button
const backupAllBtn = document.getElementById('backup-all-btn');
if (backupAllBtn) {
backupAllBtn.addEventListener('click', () => this.backupAllProjects());
}
// Save project configuration
const saveConfigBtn = document.getElementById('save-project-config-btn');
if (saveConfigBtn) {
saveConfigBtn.addEventListener('click', () => this.saveProjectConfig());
}
}
backupAllProjects() {
this.showAlert('info', 'Iniciando backup de todos los proyectos habilitados...');
// Get all enabled projects and trigger backup
fetch('/api/projects')
.then(response => response.json())
.then(data => {
if (data.error) {
this.showAlert('danger', `Error: ${data.error}`);
return;
}
const enabledProjects = data.projects.filter(project =>
project.schedule_config && project.schedule_config.enabled
);
if (enabledProjects.length === 0) {
this.showAlert('warning', 'No hay proyectos habilitados para backup.');
return;
}
// Backup each enabled project
let completed = 0;
enabledProjects.forEach(project => {
fetch(`/api/projects/${project.id}/backup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(backupData => {
completed++;
if (completed === enabledProjects.length) {
this.showAlert('success',
`Backup completado para ${enabledProjects.length} proyectos.`);
}
})
.catch(error => {
console.error(`Error backing up project ${project.id}:`, error);
});
});
})
.catch(error => {
this.showAlert('danger', `Error de conexión: ${error.message}`);
});
}
saveProjectConfig() {
const projectId = document.getElementById('modal-project-id').value;
const schedule = document.getElementById('modal-project-schedule').value;
const time = document.getElementById('modal-project-time').value;
const enabled = document.getElementById('modal-project-enabled').checked;
const config = {
schedule_config: {
schedule: schedule,
schedule_time: time,
enabled: enabled
}
};
fetch(`/api/projects/${projectId}/config`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config)
})
.then(response => response.json())
.then(data => {
if (data.error) {
this.showAlert('danger', `Error: ${data.error}`);
} else {
this.showAlert('success', 'Configuración guardada correctamente.');
// Close modal
const modal = bootstrap.Modal.getInstance(
document.getElementById('projectConfigModal')
);
if (modal) {
modal.hide();
}
// Refresh page after a short delay
setTimeout(() => location.reload(), 1500);
}
})
.catch(error => {
this.showAlert('danger', `Error de conexión: ${error.message}`);
});
}
showAlert(type, message, duration = 5000) {
const alertsContainer = document.getElementById('alerts-container');
if (!alertsContainer) return;
const alertId = 'alert-' + Date.now();
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show fade-in" role="alert" id="${alertId}">
<i class="bi bi-${this.getAlertIcon(type)}"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertsContainer.insertAdjacentHTML('beforeend', alertHtml);
// Auto-dismiss after duration
if (duration > 0) {
setTimeout(() => {
const alertElement = document.getElementById(alertId);
if (alertElement) {
const alert = new bootstrap.Alert(alertElement);
alert.close();
}
}, duration);
}
}
getAlertIcon(type) {
const icons = {
'success': 'check-circle',
'danger': 'exclamation-triangle',
'warning': 'exclamation-triangle',
'info': 'info-circle',
'primary': 'info-circle',
'secondary': 'info-circle'
};
return icons[type] || 'info-circle';
}
// Utility method to format file sizes
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Utility method to format dates
formatDate(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// Method to show loading state
showLoading(element) {
if (element) {
element.classList.add('loading');
const originalContent = element.innerHTML;
element.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span> Cargando...';
return originalContent;
}
}
// Method to hide loading state
hideLoading(element, originalContent) {
if (element && originalContent) {
element.classList.remove('loading');
element.innerHTML = originalContent;
}
}
// Method to make API calls with error handling
async apiCall(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP error! status: ${response.status}`);
}
return data;
} catch (error) {
console.error('API call failed:', error);
this.showAlert('danger', `Error de API: ${error.message}`);
throw error;
}
}
}
// Global functions for backward compatibility
function showAlert(type, message, duration = 5000) {
if (window.autobackupsApp) {
window.autobackupsApp.showAlert(type, message, duration);
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
window.autobackupsApp = new AutoBackupsApp();
});
// Export for modules if needed
if (typeof module !== 'undefined' && module.exports) {
module.exports = AutoBackupsApp;
}

70
templates/base.html Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AutoBackups{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">
<i class="bi bi-archive"></i> AutoBackups
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard') }}">
<i class="bi bi-house"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="scan-projects-btn">
<i class="bi bi-search"></i> Escanear Proyectos
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="system-status-btn">
<i class="bi bi-gear"></i> Estado del Sistema
</a>
</li>
</ul>
<span class="navbar-text">
<i class="bi bi-clock"></i> <span id="current-time"></span>
</span>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container mt-4">
<!-- Alerts -->
<div id="alerts-container"></div>
{% block content %}{% endblock %}
</div>
<!-- Modals -->
{% block modals %}{% endblock %}
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

383
templates/dashboard.html Normal file
View File

@ -0,0 +1,383 @@
{% extends "base.html" %}
{% block title %}Dashboard - AutoBackups{% endblock %}
{% block content %}
<div class="row">
<!-- Statistics Cards -->
<div class="col-12">
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center bg-primary text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-folder2-open"></i> Total Proyectos
</h5>
<h2 class="card-text">{{ stats.total_projects }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center bg-success text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-check-circle"></i> Habilitados
</h5>
<h2 class="card-text">{{ stats.enabled_projects }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center bg-info text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-hdd"></i> Espacio Libre
</h5>
<h2 class="card-text">{{ "%.1f"|format(stats.free_space_mb) }} MB</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center bg-warning text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-hdd-stack"></i> Espacio Total
</h5>
<h2 class="card-text">{{ "%.1f"|format(stats.total_space_mb) }} MB</h2>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Projects Table -->
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-list-task"></i> Proyectos
</h5>
<div>
<button class="btn btn-outline-primary btn-sm" id="refresh-projects-btn">
<i class="bi bi-arrow-clockwise"></i> Actualizar
</button>
<button class="btn btn-success btn-sm" id="backup-all-btn">
<i class="bi bi-archive"></i> Backup Todos
</button>
</div>
</div>
<div class="card-body">
{% if projects %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Proyecto</th>
<th>Ruta</th>
<th>Tipo</th>
<th>Estado</th>
<th>Último Backup</th>
<th>Próximo Backup</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr>
<td>
<strong>{{ project.name or 'Sin nombre' }}</strong>
<br>
<small class="text-muted">ID: {{ project.id }}</small>
</td>
<td>
<span title="{{ project.path }}">
{{ project.path[:50] }}{% if project.path|length > 50 %}...{% endif %}
</span>
</td>
<td>
<span class="badge bg-secondary">{{ project.type or 'N/A' }}</span>
</td>
<td>
{% if project.current_status.value == 'ready' %}
<span class="badge bg-success">Listo</span>
{% elif project.current_status.value == 'backing_up' %}
<span class="badge bg-warning">En progreso</span>
{% elif project.current_status.value == 'error' %}
<span class="badge bg-danger">Error</span>
{% elif project.current_status.value == 'files_in_use' %}
<span class="badge bg-warning">Archivos en uso</span>
{% else %}
<span class="badge bg-secondary">{{ project.current_status.value }}</span>
{% endif %}
</td>
<td>
{% if project.last_backup_date %}
{{ project.last_backup_date[:19] }}
{% else %}
<span class="text-muted">Nunca</span>
{% endif %}
</td>
<td>
{% if project.next_scheduled_backup %}
{{ project.next_scheduled_backup[:19] }}
{% else %}
<span class="text-muted">No programado</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary backup-project-btn"
data-project-id="{{ project.id }}">
<i class="bi bi-archive"></i>
</button>
<button class="btn btn-outline-secondary config-project-btn"
data-project-id="{{ project.id }}">
<i class="bi bi-gear"></i>
</button>
<button class="btn btn-outline-info view-project-btn"
data-project-id="{{ project.id }}">
<i class="bi bi-eye"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-folder-x display-1 text-muted"></i>
<h4 class="text-muted mt-3">No hay proyectos disponibles</h4>
<p class="text-muted">Ejecuta un escaneo para buscar proyectos en los directorios configurados.</p>
<button class="btn btn-primary" id="first-scan-btn">
<i class="bi bi-search"></i> Escanear Proyectos
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block modals %}
<!-- Project Configuration Modal -->
<div class="modal fade" id="projectConfigModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Configuración del Proyecto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="projectConfigForm">
<input type="hidden" id="modal-project-id">
<div class="mb-3">
<label for="modal-project-schedule" class="form-label">Frecuencia de Backup</label>
<select class="form-select" id="modal-project-schedule">
<option value="manual">Manual</option>
<option value="daily">Diario</option>
<option value="hourly">Cada hora</option>
<option value="3-hour">Cada 3 horas</option>
<option value="7-hour">Cada 7 horas</option>
<option value="startup">Al iniciar</option>
</select>
</div>
<div class="mb-3">
<label for="modal-project-time" class="form-label">Hora de Backup (para programación diaria)</label>
<input type="time" class="form-control" id="modal-project-time">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="modal-project-enabled">
<label class="form-check-label" for="modal-project-enabled">
Backup habilitado
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" id="save-project-config-btn">Guardar</button>
</div>
</div>
</div>
</div>
<!-- System Status Modal -->
<div class="modal fade" id="systemStatusModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Estado del Sistema</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="system-status-content">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Event listeners for project actions
document.querySelectorAll('.backup-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
backupProject(projectId);
});
});
document.querySelectorAll('.config-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
openProjectConfig(projectId);
});
});
// Other event listeners
const refreshBtn = document.getElementById('refresh-projects-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => location.reload());
}
const scanBtn = document.getElementById('scan-projects-btn');
if (scanBtn) {
scanBtn.addEventListener('click', scanProjects);
}
const firstScanBtn = document.getElementById('first-scan-btn');
if (firstScanBtn) {
firstScanBtn.addEventListener('click', scanProjects);
}
const systemStatusBtn = document.getElementById('system-status-btn');
if (systemStatusBtn) {
systemStatusBtn.addEventListener('click', showSystemStatus);
}
});
function backupProject(projectId) {
showAlert('info', `Iniciando backup del proyecto ${projectId}...`);
fetch(`/api/projects/${projectId}/backup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
showAlert('danger', `Error: ${data.error}`);
} else {
showAlert('success', data.message || 'Backup iniciado correctamente');
}
})
.catch(error => {
showAlert('danger', `Error de conexión: ${error.message}`);
});
}
function openProjectConfig(projectId) {
document.getElementById('modal-project-id').value = projectId;
const modal = new bootstrap.Modal(document.getElementById('projectConfigModal'));
modal.show();
}
function scanProjects() {
showAlert('info', 'Escaneando directorios en busca de proyectos...');
fetch('/api/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
showAlert('danger', `Error: ${data.error}`);
} else {
showAlert('success', `Escaneo completado. ${data.projects_found} proyectos encontrados.`);
setTimeout(() => location.reload(), 2000);
}
})
.catch(error => {
showAlert('danger', `Error de conexión: ${error.message}`);
});
}
function showSystemStatus() {
const modal = new bootstrap.Modal(document.getElementById('systemStatusModal'));
fetch('/api/system/status')
.then(response => response.json())
.then(data => {
if (data.error) {
document.getElementById('system-status-content').innerHTML =
`<div class="alert alert-danger">Error: ${data.error}</div>`;
} else {
document.getElementById('system-status-content').innerHTML = `
<div class="row">
<div class="col-md-6">
<h6>Espacio en Disco</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<span>Total:</span>
<span>${data.disk_space.total_mb.toFixed(1)} MB</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Usado:</span>
<span>${data.disk_space.used_mb.toFixed(1)} MB</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Libre:</span>
<span>${data.disk_space.free_mb.toFixed(1)} MB</span>
</li>
</ul>
</div>
<div class="col-md-6">
<h6>Proyectos</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<span>Total:</span>
<span>${data.projects.total}</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Habilitados:</span>
<span>${data.projects.enabled}</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Everything API:</span>
<span class="badge ${data.everything_api.available ? 'bg-success' : 'bg-warning'}">
${data.everything_api.available ? 'Disponible' : 'No disponible'}
</span>
</li>
</ul>
</div>
</div>
`;
}
})
.catch(error => {
document.getElementById('system-status-content').innerHTML =
`<div class="alert alert-danger">Error de conexión: ${error.message}</div>`;
});
modal.show();
}
</script>
{% endblock %}

28
templates/error.html Normal file
View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Error - AutoBackups{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">
<i class="bi bi-exclamation-triangle"></i> Error
</h5>
</div>
<div class="card-body">
<p class="card-text">Ha ocurrido un error en la aplicación:</p>
<div class="alert alert-danger">
<code>{{ error }}</code>
</div>
<div class="text-center">
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Volver al Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

11
test_json.py Normal file
View File

@ -0,0 +1,11 @@
import json
with open("projects.json", "r") as f:
data = json.load(f)
print(f"Total projects in JSON: {len(data['projects'])}")
if data["projects"]:
print(f"First project name: {data['projects'][0]['name']}")
print(f"First project ID: {data['projects'][0]['id']}")
else:
print("No projects found in JSON")

20
test_project_manager.py Normal file
View File

@ -0,0 +1,20 @@
import sys
import os
from pathlib import Path
# Agregar src al path
sys.path.insert(0, str(Path(__file__).parent / "src"))
from models.project_model import ProjectManager
# Crear project manager y ver qué proyectos hay
pm = ProjectManager()
projects = pm.get_all_projects()
print(f"Proyectos cargados por ProjectManager: {len(projects)}")
for project in projects:
print(f" - ID: {project.id}")
print(f" - Nombre: {project.name}")
print(f" - Tipo: {type(project)}")
print(f" - Atributos: {dir(project)}")
break # Solo el primero