feat: Version 1.11.0 - Mejoras en auditoría, SLA, frontend y correcciones de sincronización
- Refactorización de endpoints de auditoría y helpers - Mejoras en esquemas de auditoría (audit.py) - Correcciones en endpoint SLA - Actualizaciones en múltiples rutas del frontend interno: layout, tickets, usuarios, tenants, categorías, sistemas, SLA (at-risk, violations), auditoría (main + security), login, perfil - Actualización de tailwind.config.js - Eliminación de docs de versiones anteriores (CAMBIOS_v1.10.0, v1.8.0, OPTIMIZACIONES) - Nuevos scripts de prueba: generate_security_test_data.py, generate_sla_test_data.py - Script de prueba de sincronización crítica (test_critical_sync.ps1) - README actualizado en scripts/
This commit is contained in:
@@ -91,7 +91,7 @@ async def get_sla_dashboard(
|
||||
days=days
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
period_start = now - timedelta(days=days)
|
||||
|
||||
# Usar func.now() para comparaciones en SQL (evita timezone issues)
|
||||
@@ -357,7 +357,7 @@ async def get_sla_violations(
|
||||
sla_type=sla_type
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
db_now = func.now()
|
||||
|
||||
# Base query con carga de relaciones
|
||||
@@ -417,18 +417,16 @@ async def get_sla_violations(
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# Aplicar paginación
|
||||
query = query.order_by(desc(Ticket.created_at)).offset(skip).limit(limit)
|
||||
|
||||
# Obtener todos los tickets sin paginación primero (los ordenaremos por tiempo vencido después)
|
||||
result = await db.execute(query)
|
||||
tickets = result.scalars().all()
|
||||
|
||||
# Formatear response
|
||||
violations = []
|
||||
for ticket in tickets:
|
||||
# Asegurar que los datetimes de BD sean timezone-aware
|
||||
sla_response_due = ticket.sla_response_due.replace(tzinfo=timezone.utc) if ticket.sla_response_due and ticket.sla_response_due.tzinfo is None else ticket.sla_response_due
|
||||
sla_resolution_due = ticket.sla_resolution_due.replace(tzinfo=timezone.utc) if ticket.sla_resolution_due and ticket.sla_resolution_due.tzinfo is None else ticket.sla_resolution_due
|
||||
# Todos los campos son timezone-naive (TIMESTAMP WITHOUT TIME ZONE)
|
||||
sla_response_due = ticket.sla_response_due
|
||||
sla_resolution_due = ticket.sla_resolution_due
|
||||
|
||||
# Determinar tipo de violación
|
||||
response_violated = ticket.first_response_at is None and sla_response_due and now > sla_response_due
|
||||
@@ -479,10 +477,16 @@ async def get_sla_violations(
|
||||
resolved_at=ticket.resolved_at
|
||||
))
|
||||
|
||||
# Ordenar por tiempo vencido (de mayor a menor)
|
||||
violations.sort(key=lambda v: v.hours_overdue, reverse=True)
|
||||
|
||||
# Aplicar paginación en Python
|
||||
paginated_violations = violations[skip:skip + limit]
|
||||
|
||||
total_pages = (total + limit - 1) // limit
|
||||
|
||||
return SLAViolationsListResponse(
|
||||
violations=violations,
|
||||
violations=paginated_violations,
|
||||
total=total,
|
||||
page=(skip // limit) + 1,
|
||||
per_page=limit,
|
||||
@@ -515,13 +519,16 @@ async def get_tickets_at_risk(
|
||||
threshold=threshold
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
db_now = func.now()
|
||||
threshold_decimal = threshold / 100.0
|
||||
|
||||
# Query para tickets en riesgo
|
||||
# Query para tickets en riesgo con relaciones precargadas
|
||||
# Un ticket está en riesgo si: (now - created_at) / (due_at - created_at) >= threshold
|
||||
query = select(Ticket).where(
|
||||
query = select(Ticket).options(
|
||||
selectinload(Ticket.assigned_to_user),
|
||||
selectinload(Ticket.category)
|
||||
).where(
|
||||
and_(
|
||||
Ticket.tenant_id == current_tenant.id,
|
||||
Ticket.status.notin_([TicketStatus.RESOLVED, TicketStatus.CLOSED]),
|
||||
@@ -576,14 +583,15 @@ async def get_tickets_at_risk(
|
||||
else:
|
||||
continue
|
||||
|
||||
# Normalizar created_at a timezone-naive para evitar errores de comparación
|
||||
created_at = ticket.created_at.replace(tzinfo=None) if ticket.created_at.tzinfo else ticket.created_at
|
||||
|
||||
time_remaining = (due_at - now).total_seconds() / 3600
|
||||
total_time = (due_at - ticket.created_at).total_seconds() / 3600
|
||||
total_time = (due_at - created_at).total_seconds() / 3600
|
||||
elapsed_time = total_time - time_remaining
|
||||
risk_percentage = (elapsed_time / total_time * 100) if total_time > 0 else 0
|
||||
|
||||
# Cargar relaciones
|
||||
await db.refresh(ticket, ['assigned_to', 'category'])
|
||||
|
||||
# Las relaciones ya están cargadas por selectinload
|
||||
at_risk_tickets.append(SLATicketAtRisk(
|
||||
ticket=TicketBasicInfo(
|
||||
id=ticket.id,
|
||||
@@ -599,11 +607,11 @@ async def get_tickets_at_risk(
|
||||
sla_resolution_hours=ticket.category.sla_resolution_hours
|
||||
) if ticket.category else None,
|
||||
assigned_to=UserBasicInfo(
|
||||
id=ticket.assigned_to.id,
|
||||
first_name=ticket.assigned_to.first_name,
|
||||
last_name=ticket.assigned_to.last_name,
|
||||
email=ticket.assigned_to.email
|
||||
) if ticket.assigned_to else None,
|
||||
id=ticket.assigned_to_user.id,
|
||||
first_name=ticket.assigned_to_user.first_name,
|
||||
last_name=ticket.assigned_to_user.last_name,
|
||||
email=ticket.assigned_to_user.email
|
||||
) if ticket.assigned_to_user else None,
|
||||
sla_type=sla_type,
|
||||
sla_due_at=due_at,
|
||||
time_remaining_hours=time_remaining,
|
||||
|
||||
Reference in New Issue
Block a user