feat: implementar Fases 3, 4 y 5 del roadmap (Planificacion Avanzada, Historial de Corridas y Copia Local Duplicada)

This commit is contained in:
Carlos Tello
2026-08-15 00:58:05 -03:00
parent 2c2cfeb867
commit 929f9affd7
11 changed files with 677 additions and 57 deletions
+14 -2
View File
@@ -10,12 +10,24 @@ from app.services.event_service import log_event
router = APIRouter(prefix="/auth", tags=["Authentication"])
import logging
logger = logging.getLogger("uvicorn.error")
@router.post("/login", response_model=TokenResponse)
async def login(credentials: LoginRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.email == credentials.email.strip().lower()))
email_clean = credentials.email.strip().lower()
result = await db.execute(select(User).where(User.email == email_clean))
user = result.scalar_one_or_none()
if not user or not verify_password(credentials.password, user.hashed_password):
if not user:
logger.warning(f"Login failed: User not found with email '{email_clean}'")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password"
)
if not verify_password(credentials.password, user.hashed_password):
logger.warning(f"Login failed: Password mismatch for email '{email_clean}' (sent password length: {len(credentials.password)})")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password"
+91 -8
View File
@@ -5,8 +5,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from app.core.database import get_db
from app.models.models import User, Client, BackupJob, BackupFile
from app.schemas.schemas import JobCreate, JobUpdate, JobResponse, AgentJobRegister, PurgeOrphansRequest
from app.models.models import User, Client, BackupJob, BackupFile, JobRun
from app.schemas.schemas import (
JobCreate, JobUpdate, JobResponse, AgentJobRegister, PurgeOrphansRequest,
JobRunResponse, JobRunStartRequest, JobRunCompleteRequest
)
from app.api.deps import get_current_user, require_admin, get_current_client
from app.services.event_service import log_event
from app.ws.manager import ws_manager
@@ -46,9 +49,9 @@ async def agent_register_job(
current_client: Client = Depends(get_current_client)
):
"""Called by the Windows Agent to register a new local job/folder on the server."""
count_res = await db.execute(select(func.count(BackupJob.id)))
job_count = count_res.scalar() or 0
job_code = f"JOB-{job_count + 1:03d}"
max_id_res = await db.execute(select(func.max(BackupJob.id)))
max_id = max_id_res.scalar() or 0
job_code = f"JOB-{max_id + 1:03d}"
job = BackupJob(
job_code=job_code,
@@ -61,6 +64,7 @@ async def agent_register_job(
skip_system=payload.skip_system,
skip_readonly=payload.skip_readonly,
sync_deletions=payload.sync_deletions,
local_destination_path=payload.local_destination_path,
schedule_cron=payload.schedule_cron,
keep_daily=payload.keep_daily,
keep_weekly=payload.keep_weekly,
@@ -124,9 +128,9 @@ async def create_job(
if not client:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Client not found")
count_res = await db.execute(select(func.count(BackupJob.id)))
job_count = count_res.scalar() or 0
job_code = f"JOB-{job_count + 1:03d}"
max_id_res = await db.execute(select(func.max(BackupJob.id)))
max_id = max_id_res.scalar() or 0
job_code = f"JOB-{max_id + 1:03d}"
job = BackupJob(
job_code=job_code,
@@ -139,6 +143,7 @@ async def create_job(
skip_system=payload.skip_system,
skip_readonly=payload.skip_readonly,
sync_deletions=payload.sync_deletions,
local_destination_path=payload.local_destination_path,
schedule_cron=payload.schedule_cron,
keep_daily=payload.keep_daily,
keep_weekly=payload.keep_weekly,
@@ -322,3 +327,81 @@ async def purge_orphans(
"freed_bytes": total_freed_bytes,
"message": f"Successfully purged {purged_count} orphan files from server."
}
@router.post("/{job_id}/runs/start", response_model=JobRunResponse)
async def start_job_run(
job_id: int,
payload: JobRunStartRequest,
db: AsyncSession = Depends(get_db),
current_client: Client = Depends(get_current_client)
):
"""Called by the Windows Agent to register the start of a scheduled or manual job run."""
res = await db.execute(select(BackupJob).where(BackupJob.id == job_id, BackupJob.client_id == current_client.id))
job = res.scalar_one_or_none()
if not job:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found or not owned by client")
job.status = "RUNNING"
job.last_run_at = datetime.now(timezone.utc)
run = JobRun(
job_id=job.id,
client_id=current_client.id,
started_at=datetime.now(timezone.utc),
status="RUNNING"
)
db.add(run)
await db.commit()
await db.refresh(run)
return run
@router.post("/{job_id}/runs/{run_id}/complete", response_model=JobRunResponse)
async def complete_job_run(
job_id: int,
run_id: int,
payload: JobRunCompleteRequest,
db: AsyncSession = Depends(get_db),
current_client: Client = Depends(get_current_client)
):
"""Called by the Windows Agent to record completion metrics for a run."""
res = await db.execute(select(JobRun).where(JobRun.id == run_id, JobRun.job_id == job_id, JobRun.client_id == current_client.id))
run = res.scalar_one_or_none()
if not run:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job run not found")
job_res = await db.execute(select(BackupJob).where(BackupJob.id == job_id))
job = job_res.scalar_one_or_none()
run.completed_at = datetime.now(timezone.utc)
run.status = payload.status
run.files_scanned = payload.files_scanned
run.files_copied = payload.files_copied
run.files_skipped = payload.files_skipped
run.errors_count = payload.errors_count
run.bytes_transferred = payload.bytes_transferred
run.error_summary = payload.error_summary
if job:
job.status = payload.status
job.last_run_at = run.completed_at
await db.commit()
await db.refresh(run)
return run
@router.get("/{job_id}/runs", response_model=List[JobRunResponse])
async def list_job_runs(
job_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Called by the Web UI to retrieve execution history for a job."""
result = await db.execute(
select(JobRun)
.where(JobRun.job_id == job_id)
.order_by(desc(JobRun.started_at))
.limit(50)
)
return result.scalars().all()