import os import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from fastapi import FastAPI, Request from fastapi.responses import RedirectResponse from fastapi.templating import Jinja2Templates import uvicorn from apscheduler.schedulers.asyncio import AsyncIOScheduler from app.database import init_db from app.auth import decode_token from app.services.scheduler import sync_recientes from app.services.whatsapp_sync import guardar_sync_log app = FastAPI(title="RIPS Manager", version="1.0.0") _scheduler = AsyncIOScheduler() templates = Jinja2Templates( directory=os.path.join(os.path.dirname(__file__), "app", "templates") ) app.state.templates = templates def get_user_from_request(request: Request): token = request.cookies.get("token") if not token: auth = request.headers.get("Authorization", "") if auth.startswith("Bearer "): token = auth[7:] if token: decoded = decode_token(token) if decoded: return decoded print(f" [AUTH] Invalid token: {token[:30]}...", flush=True) return None @app.middleware("http") async def auth_middleware(request: Request, call_next): if request.url.path.startswith("/auth") or request.url.path.startswith("/static"): return await call_next(request) user = get_user_from_request(request) print(f" [AUTH] path={request.url.path} user={user['username'] if user else None}", flush=True) if not user: if request.url.path.startswith("/api/"): from fastapi.responses import JSONResponse return JSONResponse({"detail": "Not authenticated"}, status_code=401) return RedirectResponse(url="/auth/login") request.state.user = user return await call_next(request) @app.on_event("startup") async def startup(): init_db() from app.routes.config import ensure_defaults as config_defaults from app.routes.queries import ensure_defaults as query_defaults from app.routes.contratos import ensure_defaults as contratos_defaults config_defaults() query_defaults() contratos_defaults() async def _job_sync(): try: resultado = await sync_recientes(ventana_min=60) if not resultado.get("ok") and resultado.get("error"): guardar_sync_log( {"total": 0, "created": 0, "skipped": 0, "updated": 0, "errores": 1, "detalle": [ {"ok": False, "action": "error", "message": resultado["error"], "doc": "", "nombre": "scheduler-error"} ]}, None, origen="scheduler", modo="upsert" ) except Exception as exc: import traceback guardar_sync_log( {"total": 0, "created": 0, "skipped": 0, "updated": 0, "errores": 1, "detalle": [ {"ok": False, "action": "error", "message": str(exc), "doc": "", "nombre": "scheduler-exception"} ]}, None, origen="scheduler", modo="upsert" ) _scheduler.add_job(_job_sync, "interval", seconds=20, id="wa_sync_recientes", max_instances=1, coalesce=True) _scheduler.start() @app.on_event("shutdown") async def shutdown(): _scheduler.shutdown(wait=False) @app.get("/api/scheduler/status") async def scheduler_status(): from fastapi.responses import JSONResponse from app.database import get_connection from datetime import timezone, timedelta COL = timezone(timedelta(hours=-5)) job = _scheduler.get_job("wa_sync_recientes") next_run_str = None if job and job.next_run_time: next_run_str = job.next_run_time.astimezone(COL).strftime("%H:%M:%S") conn = get_connection() row = conn.execute( "SELECT created_at, total, errores FROM sync_wa_log " "WHERE origen='scheduler' ORDER BY id DESC LIMIT 1" ).fetchone() conn.close() ultimo = None if row: from datetime import datetime ts_utc = datetime.fromisoformat(row["created_at"].replace(" ", "T") + "+00:00") ultimo = { "hora": ts_utc.astimezone(COL).strftime("%H:%M:%S"), "total": row["total"], "errores": row["errores"], } return JSONResponse({ "running": _scheduler.running, "job_exists": job is not None, "next_run": next_run_str, "ultimo_log": ultimo, }) @app.get("/") async def root(): return RedirectResponse(url="/dashboard") from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation, test_rda, debug_fb, pacientes, contratos, ventas, envios app.include_router(auth.router) app.include_router(dashboard.router) app.include_router(config.router) app.include_router(queries.router) app.include_router(envios.router) app.include_router(terceros.router) app.include_router(transaccion.router) app.include_router(logs.router) app.include_router(automation.router) app.include_router(test_rda.router) app.include_router(debug_fb.router) app.include_router(pacientes.router) app.include_router(contratos.router) app.include_router(ventas.router) if __name__ == "__main__": import socket hostname = socket.gethostname() try: lan_ip = socket.gethostbyname(hostname) except Exception: lan_ip = "0.0.0.0" print(f" Local: http://localhost:8080") print(f" Red: http://{lan_ip}:8080") uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=False)