# backend/main.py """API REST del Dashboard de LEADS (FastAPI).""" from fastapi import FastAPI, Query, HTTPException, Body from fastapi.middleware.cors import CORSMiddleware from datetime import datetime import uvicorn import services from cache_manager import start_background_refresh, cache_stats app = FastAPI(title="Dashboard Leads API", version="1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.on_event("startup") def _startup(): # Precarga en SEGUNDO PLANO (hilo aparte) para no bloquear el arranque del server. import threading def _precarga_bg(): print("[startup] Precargando datos de Leads (2do plano)...") try: services.precargar_todo() print("[startup] Precarga completa.") except Exception as e: print(f"[startup] Precarga falló: {e}") threading.Thread(target=_precarga_bg, daemon=True).start() # Otros General en un hilo aparte (sus consultas son mas pesadas) def _precarga_otros_bg(): try: services.precargar_otros_general() except Exception as e: print(f"[startup] Precarga Otros falló: {e}") threading.Thread(target=_precarga_otros_bg, daemon=True).start() start_background_refresh(services.refrescar_todo, interval=900) @app.get("/api/health") def health(): return {"status": "ok", "hora": datetime.now().isoformat()} @app.get("/api/cache/stats") def stats(): return cache_stats() @app.post("/api/cache/refresh") def refresh(): services.refrescar_todo() return {"status": "refrescado"} @app.get("/api/leads/filtros") def get_filtros(): try: return services.opciones_filtros() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/leads") def get_leads( ano: str = Query("TODOS"), mes: str = Query("TODOS"), dia: str = Query("TODOS"), programa: str = Query("TODOS"), sede: str = Query("TODOS"), ): try: return services.leads_dashboard(ano, mes, dia, programa, sede) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/vendedores") def get_vendedores( ano: str = Query("TODOS"), mes: str = Query("TODOS"), dia: str = Query("TODOS"), ): try: return services.vendedores_dashboard(ano, mes, dia) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/roas") def get_roas( ano: str = Query("TODOS"), mes: str = Query("TODOS"), dia: str = Query("TODOS"), ): try: return services.roas_dashboard(ano, mes, dia) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/conjuntos-sin-sede") def get_conjuntos_sin_sede(): try: return services.conjuntos_sin_sede() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/conjuntos-sin-sede/guardar") def post_conjunto_sede(body: dict = Body(...)): try: cambios = body.get("cambios") or [] return services.guardar_conjunto_sede(cambios) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/conjuntos-sin-sede/borrar") def post_borrar_conjunto_sede(body: dict = Body(...)): try: return services.borrar_conjunto_sede(body.get("conjunto") or "") except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/otros-general") def get_otros_general( ano: str = Query("TODOS"), mes: str = Query("TODOS"), dia: str = Query("TODOS"), ): try: return services.otros_general(ano, mes, dia) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/conjuntos-sin-pauta") def get_conjuntos_sin_pauta(): try: return {"conjuntos": services.conjuntos_sin_pauta()} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/programas-ocultos") def get_programas_ocultos(): try: return services.programas_ocultos() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/programas-ocultos/encender") def post_encender_programas(body: dict = Body(...)): try: nis = body.get("num_indices") or [] return services.encender_programas(nis) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/programas-disponibles") def get_programas_disponibles(): try: return services.programas_disponibles() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/leyenda-anuncios") def get_leyenda(): try: return services.leyenda_anuncios() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/leyenda-anuncios/guardar") def post_leyenda(body: dict = Body(...)): try: cambios = body.get("cambios") or [] return services.guardar_leyenda(cambios) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── LEYENDA (campanias de Supabase): listar / agregar / borrar ── @app.get("/api/campanias") def get_campanias(): try: return {"filas": services.campanias_listar()} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/campanias/agregar") def post_campania(body: dict = Body(...)): try: return services.campania_agregar(body or {}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/campanias/editar") def post_editar_campania(body: dict = Body(...)): try: cid = body.get("id") return services.campania_editar(cid, body or {}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/campanias/borrar") def post_borrar_campania(body: dict = Body(...)): try: return services.campania_borrar(body.get("id")) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/pauta/uso") def get_uso_pauta(pauta: str = Query(...), excluir: str = Query("")): try: return services.uso_de_pauta(pauta, excluir) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/curso/guardar-pauta") def post_guardar_pauta(body: dict = Body(...)): try: ni = str(body.get("num_indice", "")).strip() pa = str(body.get("pauta", "")).strip() cj = body.get("conjunto") or None contar = body.get("contar") # "SI" / "NO" / None if not ni: raise HTTPException(status_code=400, detail="num_indice es obligatorio") return services.guardar_edicion_curso(ni, pa, cj, contar) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/ultima-actualizacion") def get_ultima_actualizacion(): try: return services.ultima_actualizacion() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/leads/alertas") def get_alertas(): """Valores de programa/sede en cartera_junta que NO están en el diccionario.""" try: return services.alertas() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/debug/cursos") def debug_cursos(ano: str = "2026", mes: str = "1"): """Diagnóstico: lista cursos del periodo con inscritos, tipo y meta.""" try: return services.debug_cursos(ano, mes) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=False)