Primera version
This commit is contained in:
parent
c8ceb8fddd
commit
b9aed346ab
|
@ -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'
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
258
src/app.py
258
src/app.py
|
@ -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,28 +23,36 @@ 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
|
||||
|
@ -50,15 +62,15 @@ class AutoBackupsApp:
|
|||
# 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}")
|
||||
|
@ -72,8 +84,9 @@ class AutoBackupsApp:
|
|||
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}")
|
||||
|
@ -92,20 +105,79 @@ class AutoBackupsApp:
|
|||
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:
|
||||
|
@ -126,148 +198,56 @@ class AutoBackupsApp:
|
|||
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)
|
||||
print("AutoBackups - Sistema de Backup Automatizado (Flask)")
|
||||
print("=" * 55)
|
||||
|
||||
# Crear y configurar aplicación
|
||||
app = AutoBackupsApp()
|
||||
# Crear y configurar aplicación Flask
|
||||
autobackups_app = AutoBackupsFlaskApp()
|
||||
|
||||
# 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.")
|
||||
# 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)
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
@ -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,13 +113,13 @@ 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:
|
||||
|
@ -133,9 +134,15 @@ class Project:
|
|||
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
|
||||
|
@ -181,7 +188,7 @@ class ProjectManager:
|
|||
"""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", {})
|
||||
|
@ -204,10 +211,10 @@ class ProjectManager:
|
|||
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}")
|
||||
|
@ -217,7 +224,7 @@ class ProjectManager:
|
|||
self.metadata = {
|
||||
"version": "1.0",
|
||||
"last_updated": datetime.now(timezone.utc).isoformat(),
|
||||
"total_projects": 0
|
||||
"total_projects": 0,
|
||||
}
|
||||
|
||||
self.statistics = {
|
||||
|
@ -226,7 +233,7 @@ class ProjectManager:
|
|||
"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:
|
||||
|
@ -238,6 +245,36 @@ class ProjectManager:
|
|||
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)
|
||||
|
|
|
@ -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"]
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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")
|
|
@ -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
|
Loading…
Reference in New Issue