Doctors can order exams up to ~45 min after the patient is registered at reception. With 10 min window the exam sync missed those cases. 60 min ensures late-ordered exams are always captured and pushed to WhatsApp Lab via upsert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
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)
|