feat: log de actividad por usuario

- Tabla activity_log en SQLite (migración automática)
- Registra: login, login_fallido, rda_enviado, rda_masivo, rda_directo,
  automation_run, contrato_creado, contrato_editado, contrato_toggle
- Endpoint GET /logs/actividad con filtros por usuario, acción y fecha
- UI /logs/actividad con badges de color por tipo de acción
- Enlace "Actividad" en navegación lateral

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-10 17:11:33 -05:00
co-authored by Claude Sonnet 4.6
parent 8e29e2d04f
commit c20e1a7476
10 changed files with 257 additions and 0 deletions
+46
View File
@@ -87,6 +87,52 @@ async def logs_page(
})
@router.get("/actividad")
async def actividad_page(
request: Request,
user: dict = Depends(get_current_user),
username: str = "",
accion: str = "",
fecha_desde: str = "",
fecha_hasta: str = "",
offset: int = 0,
):
conn = get_connection()
where = ["1=1"]
params = []
if username:
where.append("a.username = ?")
params.append(username)
if accion:
where.append("a.accion = ?")
params.append(accion)
if fecha_desde:
where.append("DATE(a.created_at) >= ?")
params.append(fecha_desde)
if fecha_hasta:
where.append("DATE(a.created_at) <= ?")
params.append(fecha_hasta)
w = " AND ".join(where)
total = conn.execute(f"SELECT COUNT(*) FROM activity_log a WHERE {w}", params).fetchone()[0]
rows = conn.execute(f"""
SELECT a.* FROM activity_log a
WHERE {w} ORDER BY a.created_at DESC LIMIT ? OFFSET ?
""", params + [PAGE_SIZE, offset]).fetchall()
usuarios = conn.execute("SELECT DISTINCT username FROM activity_log ORDER BY username").fetchall()
acciones = conn.execute("SELECT DISTINCT accion FROM activity_log ORDER BY accion").fetchall()
conn.close()
return request.app.state.templates.TemplateResponse("actividad.html", {
"request": request, "user": user,
"rows": rows, "total": total,
"usuarios": [u["username"] for u in usuarios],
"acciones": [a["accion"] for a in acciones],
"filtro_username": username, "filtro_accion": accion,
"filtro_fecha_desde": fecha_desde, "filtro_fecha_hasta": fecha_hasta,
"offset": offset, "page_size": PAGE_SIZE,
"has_more": (offset + PAGE_SIZE) < total,
})
@router.get("/detalle/{envio_id}")
async def detalle_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()