154 lines
5.6 KiB
Python
154 lines
5.6 KiB
Python
"""
|
|
Endpoints del Módulo B con caching
|
|
"""
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Depends
|
|
from pydantic import BaseModel
|
|
|
|
from app.data.repositories.ruta_repository import SQLiteRutaRepository
|
|
from app.services.simulador import obtener_simulador
|
|
from app.services.ws_manager import ws_manager
|
|
from app.core.cache import cached, cache_client, invalidate_route_cache
|
|
from app.core.dependencies import get_current_user
|
|
from app.db.database import get_connection
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _repo() -> SQLiteRutaRepository:
|
|
return SQLiteRutaRepository()
|
|
|
|
|
|
# ── GET /eta/{address_id} con caché ─────────────────────────────────────────
|
|
|
|
@router.get("/eta/{address_id}", summary="Ventana ETA para un domicilio")
|
|
@cached(prefix="eta", ttl=30)
|
|
async def get_eta(
|
|
address_id: int,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Devuelve ETA + ventana horaria solo para el domicilio solicitado.
|
|
Cacheado por 30 segundos para evitar consultas repetidas.
|
|
"""
|
|
# Verificar que el domicilio pertenece al usuario (RBAC)
|
|
conn = get_connection()
|
|
addr = conn.execute(
|
|
"SELECT user_id FROM addresses WHERE id = ?", (address_id,)
|
|
).fetchone()
|
|
conn.close()
|
|
|
|
if not addr or addr["user_id"] != current_user["id"]:
|
|
raise HTTPException(status_code=403, detail="No autorizado")
|
|
|
|
resultado = _repo().calcular_eta(address_id)
|
|
if not resultado:
|
|
raise HTTPException(status_code=404, detail="Domicilio no encontrado")
|
|
|
|
return {
|
|
"address_id": resultado.address_id,
|
|
"route_id": resultado.route_id,
|
|
"status": resultado.status,
|
|
"eta_minutos": resultado.eta_minutos,
|
|
"ventana": {
|
|
"inicio": resultado.ventana_inicio,
|
|
"fin": resultado.ventana_fin,
|
|
},
|
|
"mensaje": resultado.mensaje,
|
|
"cached": False,
|
|
}
|
|
|
|
|
|
# ── WS /ws/{address_id} ───────────────────────────────────────────────
|
|
|
|
@router.websocket("/ws/{address_id}")
|
|
async def websocket_address(websocket: WebSocket, address_id: int):
|
|
"""WebSocket para recibir notificaciones en tiempo real"""
|
|
zona_key = str(address_id)
|
|
await ws_manager.conectar(websocket, zona_key)
|
|
logger.info(f"[WS] Cliente conectado — address_id={address_id}")
|
|
try:
|
|
while True:
|
|
await websocket.receive_text() # mantener vivo
|
|
except WebSocketDisconnect:
|
|
ws_manager.desconectar(websocket, zona_key)
|
|
logger.info(f"[WS] Cliente desconectado — address_id={address_id}")
|
|
|
|
|
|
# ── POST /alerts/breakdown ────────────────────────────────────────────
|
|
|
|
class BreakdownPayload(BaseModel):
|
|
route_id: str
|
|
mensaje: Optional[str] = "El camión reportó una falla mecánica."
|
|
|
|
|
|
@router.post("/alerts/breakdown", summary="Reportar avería de camión")
|
|
async def reportar_averia(payload: BreakdownPayload):
|
|
"""Endpoint para reportar avería - invalida caché automáticamente"""
|
|
sim = obtener_simulador(payload.route_id)
|
|
await sim.forzar_averia(payload.mensaje)
|
|
|
|
# Invalidar caché de esta ruta
|
|
await invalidate_route_cache(payload.route_id)
|
|
|
|
return {
|
|
"ok": True,
|
|
"route_id": payload.route_id,
|
|
"mensaje": "Avería registrada y usuarios notificados",
|
|
}
|
|
|
|
|
|
# ── Admin / Demo ──────────────────────────────────────────────────────
|
|
|
|
@router.post("/admin/route/{route_id}/start", summary="Iniciar simulación")
|
|
async def iniciar_ruta(route_id: str):
|
|
"""Iniciar el simulador de camión"""
|
|
obtener_simulador(route_id).iniciar()
|
|
await invalidate_route_cache(route_id)
|
|
return {"ok": True, "mensaje": f"Simulador {route_id} iniciado"}
|
|
|
|
|
|
@router.post("/admin/route/{route_id}/delay", summary="Forzar retraso")
|
|
async def forzar_retraso(route_id: str, mensaje: str = "El camión reportó un retraso."):
|
|
"""Forzar retraso en la ruta"""
|
|
await obtener_simulador(route_id).forzar_retraso(mensaje)
|
|
await invalidate_route_cache(route_id)
|
|
return {"ok": True, "mensaje": "Retraso notificado"}
|
|
|
|
|
|
@router.get("/admin/route/{route_id}/status", summary="Estado interno del camión")
|
|
async def estado_ruta(route_id: str):
|
|
"""Obtener estado actual del camión"""
|
|
repo = _repo()
|
|
ts = repo.obtener_truck_status(route_id)
|
|
if not ts:
|
|
raise HTTPException(status_code=404, detail="Ruta no encontrada")
|
|
return {
|
|
"route_id": ts.route_id,
|
|
"current_position_id": ts.current_position_id,
|
|
"status": ts.status.value,
|
|
"last_update": ts.last_update.isoformat(),
|
|
"ws_clientes_activos": ws_manager.zonas_activas(),
|
|
}
|
|
|
|
|
|
# ── Endpoint para limpiar caché (admin) ──────────────────────────────
|
|
|
|
@router.post("/admin/cache/clear", summary="Limpiar toda la caché")
|
|
async def clear_cache():
|
|
"""Limpiar caché de Redis y memoria"""
|
|
await cache_client.clear_all()
|
|
return {"ok": True, "mensaje": "Caché limpiada"}
|
|
|
|
|
|
@router.get("/admin/cache/stats", summary="Estadísticas de caché")
|
|
async def cache_stats():
|
|
"""Estadísticas del sistema de caché"""
|
|
return {
|
|
"enabled": cache_client.enabled,
|
|
"redis_available": cache_client.redis_client is not None,
|
|
"memory_cache_size": len(cache_client.memory_cache),
|
|
} |