From 51fbe69e13870ba9c672800473da374b482816d5 Mon Sep 17 00:00:00 2001 From: hack_21031301_c761d3 <21031301@itcelaya.edu.mx> Date: Sat, 23 May 2026 01:03:01 -0600 Subject: [PATCH] feat: WebSockets para ETA en tiempo real y endpoint alertas operativas --- backend/__pycache__/main.cpython-312.pyc | Bin 7687 -> 12822 bytes backend/main.py | 90 +++++++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index fdbc28f525d66282f5d431ab5c10682d4daf85a0..77fcb0424326a3bb43ae379c5a2ade6d1f9a5444 100644 GIT binary patch literal 12822 zcmdTqYj9IndiSOG%aV-W#x^!!0k(`W5Fn5k+r;K!SPUd+vr%N-YmlOcbFU1T6{$&> z#hJ}CFq?^;Ze}p;Y%vXO*vXG|rk%;M&1SRHKe~wQ6?eT8GVPCMrauH^W)r%d>G$2M zE6K8G_wi%fGtxQVIp;gyd3~>Q&ixmg%}hYJeaIP}sV9hk#f%n|8pu3+RYwpr1WT}F zgou(pGD`WVDD9)8jE{-xd^!^AsE9sl@EH`Cju@jRpGkq4h&gKUSrk|ou|{n^Th#8e zD|vmy5v}l5M4djTk~c(LJ{Qn3MBF~Nl2-aEm2`z~g_2hJs+6?aSIt$1yR@Me(r|YH z)}Zia4JxFZ?S}C^Qq7YDMQ|i*x?$EzeDz#?cv}HmgE#|gIYDaw8`k%j0)=M?)^>ql z?e9@)nXeJ3Holi@>RSmpOFq}cHHCXMs)dxR4(~4HG>CPu6*rvk(Q2L~pzVNGS4cJ5 zP+uQ@rckaytV^36>%Kr}*uFNv-lyRdQnoUWy`l(v4PYNEU}+Fr#jOY*(sG6LMzyvY zFnfx#Yc$75wgy@pE?{a9Tg#co7`E<$E9$P_1RF==Z44h*$935 z3N1CLWS&*w=e3$bs?EJ*B`9_!(0rkQr$M<oI%PxPp_4*){OTIUCuvMLlkLOponLdMxH& z570q+>+h;7eRWcIUzVkPwW=Vq7rcA!U2t!NVg5 zLM$8MP6v1nN(>{MAV6aP-Ma(9mrn5Ui5NQ)Jjt;W5svpzvYv~b2*o&Ae}YT+*-)Ommu; zc!&yghTsSv$r3(_B`**@nx$ZMX-)^4g}K1^^hJ3cNz9S8z66*GJs^o_*rNa=BB>YMRMw7&K64%5P2qr?OIDc;CffP=d5xB^h zfW(>~4SG+;qn!7Q_arP@0Hzu17~^80GaW43)7{g}ZsK|ZJw02xw~lgSTe~=R4etWv|nG9Te~@F=~QHz~yLeZsx&LB4k5uPtVpa zU+r8~6*fYH^Y6shu-^7+$-YvwuT0yUC1Z2S*qpJrXM3djZV`T#?!~T6ALyq0C2Ol_ zZJiqwtsNPoRWde-#-=%=X!JF3n-WKw0vbTwu=`{hk zY|n7e57EBBe&PB9R2ojf{L0rPDP={GMoCz@qLYNWmXx}N1{Rb}fM@wOXti|N%vhxg z*4c*TD_5v(z^0uL!OB_Oml9HSrwBhwXU1Alv__J3vuNFXGxVAD=`2lHy?i4ykquGM zW+1XlRs%WgHTXaL9Yh)jm!CJR$e~1j)*45iqswC_NnE9V;yWd@y1F9d0Im)N|WRn>Ws1rN&0LxK?LcbA_0>sA2+q4T+wBDp3(XviQ~i@ z2hY&QiPK~e-z$5?uY%5GlK`5;9~R;vF7Spd47She*5 z!Pr_ZQCW(1wq)uWu55T`!`$g~omZ;#ik0514r;Sz!sgVNBW>-tSqZ#x$J&u~0B2;4 z#EPzOvJ?r;D{E#3FAvThetZA#6drqZeeaXxKUDU%QMV|vca{DY%^+;3hvHkSNQB$m zz1wuRw(6i{*keX*8w*4^zh5@{{n0oYNr0_>|H~7Bh+4xV3-fx2K&d2nel=#>G0U)E zIC*65>x9Cvib~;8Unmip6rmrY3&aD8)ICjR^^9)Y++>!3WTEdNCRr<`>m}!gvIM3J z?uSa6H5zqWZo0DsBnyeJFi|@|+OjbeOK|+DK;%o*!eEIE(NGL@y&xNeM1W6Bj0d4S z9~gj82p%4RXog7SmHl$j&{*vpQBdSzoO($}5*j8dVod=vG44!)w*oG3VkkNh2_)jY zY*x$}KamKa(WKZ>)nuw`^#maE7~vsMK#e>_{F}-4#@P2yet+fE?nTBXF*PDnGwb?{ zX;7Bn5$RnrySxEMa|q{KlBGiA2;rfJ6_!8S0xnkba#v<-A{GQY9*D7Vz6V;W5o#UP zE--vNp5UK`JRUm*J492&J?n~8_4bryM~c~@4Cbue8<C?2Wwr3|p4h zYWk74C2U$POR@9~Mmzq!N)}kA+=xK;mFYF%(5Qvc)RhEigK|<5w7K2TX?>rgz)x9G zf(F@wJ_Zd+hC#QM25nwSJ*xPUP)wm{tmrkA1XiD<%FQs8Ustd#G;3gu?^3{mrs3=0 z7IWa)MM9Tgo6ouj5N@6fG^-552iW*^O16O1CME=MFIn)*SuO!c9*Q4;-et$hfB??B ze>@-vr{g>eI%r~4){k=F@AJ^sicXIpfYt^tQZ_4zKQTGZdF-<8EfC5 zXWr7!*e~1Dh9=425e=T3%umceHm3}pv|)#2=oJmUcMN@t_WF4`)iZR*zBgsuyJ&U2 zIsE$YT;;X8t96U(z0&%f;`*Hn>_^cLqv`egzoJRoFgZ1FkI`S;bAHeC<_iZh01llW zy08y{z32B%PuyWvETU`DC|Vn5C+4_oFI{~pwX!>H-840jX=u4wBU-knnC+_gTU7Cn z#Nn8;M7(jPig*wIpA&BoB#K{G5b-eTwvqrsSx!``vZ9((Qi3g!7tdvCBd<`xXu(PX ztNXpgO3xY;u`&+x{Xijf8AYn5kLOqpba@~Gr&QR5IM2i3I3VzQ0Egd;(LRj!L*%jY zc!suHV;oX%Y1pEj>Xdz|CDUJ_~P3GC3N8g_x!U#o#KMv6nCO5F7enrcF8){0J zT&M5`|02|&JPYR_0uHxVym|8VlW(1xdFApeY5N+<-X+?*ZYF+m_Qz*a_O7&jw`3m_ z?SpCi-l=EqT3qk8Np+oKUFUq_{d3pPEu4^c9Tj&Sy;FBA$N$53><3fEgT;({;#%}- zG_|rPZQWd!aoKn@6a>W-7r>7RBmzr(Y}Dck`~H>6zGZ#dFk+MhP1l~I6DR^oDCaV|5 zgD}Kn2{0`I0Gqi;vw*HiG@u9s=xn?d7-Bto*~ErGRL~2;Q?Sxrrom@}D0V_QD$C{x z)nvdeg2%y+!Ej_?=+ofP>_FJqcX0o~;i29UnTFdWRXR|%WK+M!T|5VQ{scxRv4#q< ziV@+T#iHjR^62>h)>wlRAiA-HDoW@u$(G!({4a5nP_K>=u1Ch=G7bDI;DVz)(M~ZA zv_rf<&)U2I2nXOV{1Bok0?rQZ>X{9fH%P8F(bblAt(n@l=x&hQt3~%}$?Xx{p854@ z_m-*scP-U9(K>#|?n@bci{=W+Tqm0A=8jx@;pz(uM?QM~!{;+ityj*xb7ua?`_El} zF4NX^vsP@|ky+DyEq*orm4UWwo$7-wbq2-SY*LNP5^Ez|IGC~6r}iq=WQhUEOJJoc zfo1P#{yhF68jybCO*p}x0H5|d8{xDJUQao{2ukrZP!gcDoYd0gOb$!E%Uq`3*u+3j z=jaiLNd-EM4!(!vLqHpJaSRfb(~w6M-u0M@bbS;{6*a4!^zl%j*fk4OsNne!YZ z$Rt~$(VY>Hr(!M>_%qNF?`8xRB49H!;i{aexm@$^8mVrbShp_iTrW8{i_XnIZTLmg zt)`T7bK3bW$$3!sE1{`MM8%Pw+zh&LBTgRMrd%h;!6%1k6|6M9P7Na6*jKeT)-{xl0%CV=B$Kr{tNU}PT5nMxmjk6 zDArI+9j=U-ACZrtLYFfYcs%$Ko>B3Dpz>}IhIh8QU(=dP^mts+++Y3x_Ulp0aJ)GY z;$q#}5^v?=clJ6uJ=#MNP zT5enVQ%t|=)|=o!KOX1tMuka)#^bVu<%A#~8b@Op#8uJ)S|CN576E``Jo9IgCS6j; z*CusgyaXu;CnJ%|IonjkpX=2f-)fx=J;2!y~0GV0R(SSpi$Yl$5>WnE38@I zN?d;M8I;xv9VAi0jLSR&o9Y?lww0fTCJFcp=#v0TRuF#>OI~W7>Ac)IxBuF~s|Rnp zHZA4Bd+tq{ipoq)Tc&zd#?^S=qPOa^gkGmtdCTR6lL`-_;*F1Ud;s2+a-#J;7)lVe zvljBa^>Q49z8}XOW8}=w1hY0w>PQXF!x!0e2YfP$ID^{&d+u9{tw`l8L z7);r^)3yQ0wokO}OWO`i?YV1l<`^NTW$slrAS1ZzGOo6atG<*Gs=TL#GNxO7sQNba zzl<4>(I5T?u!#EVVNzHa${>_`k1PLrfo!jaa_6$5TJ%6m!U+$0=#zS(9!_y(ZUb2cPHH(C z%U^INQOy45G1sX57sndCvSTK1==1gY3z9fup5IF%~lO#??1@%s^A83Z)sTiNX7`!-Fy%T(i z{1Q3aQ>R|$(c#kxyi-^1*3BaWeTN5*`1cJQmrcF z1tW2I!cko1ycYtljQTN&`W&bjFLakxk9R#nW7yab5GL`VN!AGwjvMC98YnW zsvCpaUOjml#siZPc!^c-BzI#uI@kPphy)Wv%56vC#>1BX-b3-x-vx|!5cNMn!znMa zS-nl?Saem*v|nzY@m}`Mt-ewDe$(})kJ(?uZpBgqM=yEPu4B`NOhxsK<+5dVAYHL) zT9zPnj!7dgh$AoDUgJ-@UX-ja zrmQb!?5;&u?M%nzjyWprYK6Dxl(lsc3ABo?*15iG`>*c5xhn12JlzL_&r~%@RUWa* zlUf&?AAx8g_)+A;Na|Ui^z5kk?C9-v!E_ZXRj{cFHftgr?ralb>-Yw)hA?I*odZ59 zc{Bcce6D9YjtgaM7me+AjrK?PnC`C;EkJ&K&)WKRR!=xN@|#7=$_E4qQ#CpyW1VQM zo82}SycW3{`IT`~mWGPo3I7C4@G3dfLtfb3Hds&mteqUJVt(df5bnV8pLO9xB)9ZZe9%)Ki83iJDH#B8H6pp&LJJ~Ndr0f6!S@ABf{IsA(HuINAC*Ad|E*c z(afh#9l}*4(D<}giR-Hex6_|?lY^V}pKj72{DdCpefks$n7<^Im{ww)XJ{k+%T5Yv zf7wMMypaajvt2ff!Pf|Z@eq#=2M-!4cm1wM2<1qw&S7*$^Pj+V!s$cc(RGpS!po6B z1ipNWPIkumD38)3w*QEs#~;wDs_;7J{2m zbPss+VR#fZnNhyCP_8`d=(*M$AogynN$twgN7N9ZqF&RZm+QY0!RqDds0 z=8mO^raQ!jFZ8xl7BwUwv-E6(NBC~4s+=`pRO%BWn>Hdl!$(_H| zSCL!p6Bu<7mWH&samw(8wJB|FnKESvlSDX0!gZS!2E)Uriv*^;i^dbh4+Zp{yYj)fc`XIrxbruQnUA1DP# zuO9}3h1oi7e(Td@)qS|Att~SiO9iRVxK?CoNbi|#SskRXHBe$e$V`|j9v}` tzv$k!u;v5reY%0{ncJ2nAX(^Bl1$U8ES6^4y$>)&5}x}MK*~+Se*>VlF-rgd delta 2309 zcmbVOU2GIp6rMY~vp?Pc?$T|$E#3amWuV;_3RXxVRuQEFRRp7Bv>A48%dRs!<<4#k z#H3LPJQ{KnWjW8Au-0p#B=U;w?m^ZPO{&ed(XMw z{q8yU?Cy2r>QwyOSgcKg=lajd%I)N}c<0Iuza)Jm8InOv$s!uDWKFhwnvdX|Z2B!l zQ-tj^16EKA3fpgntgsdqwqmwf5iMdxwWt^e%$OE~wE;7(#l<_JCB!?aC26cOfcv)| z{;;6Pl_CCdM|!2unr#_0vKCw=jUka_i4r5U7H-aH8Jel=ZUwgtsu*nxr1@Ektil=b z5e(tg#z4|KXh&siV*syc)1=ADI48sDbkvBg#aDfec_Kmh1izC^^L#4ohn@FhP%hC_ zV0ttjdiMoLZ*QCd)iAw!w;LaBa4O z2Iu`o_OjC2x4*^P`M-FNczC-!yn{_%wHtU}+|=2$w&+b%W*}AFe9^C@Hnqz}4ptxc z?9t=dqtMKmub0O8m-4pMlR5Bk7QCF>scJMb(dS{?)l{6yH*+U4>0ue$%F<7F8w2n` zwvaLCVH;^`OLhMj+b%B~GRVe|hhg+$K;7{Rrc3ql&y{}uhjNr33)J~7voK*d; zze+FESR4N^KFr?_&O}%Sq6MV+*YQF5vBZCkkA~f#MIA?9pu8tB*n3-IeHfNQ=mv28 z%(iPR4`wHyOJqroFDFI0%0G3n^>Z(Jb!U+QFokWn;GmWbg2jm!a)Gl&o*TSjk9xg z2b4^KrztbNp;IyKg>scmgKHFb7aL0F}q%)b7jMAt2=d_m2I%q z1bYGEU0Ayev{|%6&&?j6J$Y>URoAybYYioM4mW(nw#uclS+?0x7&B}Pb__jo*USk*G)fGcm40&9WwMX zSnLSErEF{SeAUj=YRRtFm~Pl?nE%+jd*xF5sd`O+!lkP+o5A&X!Y2wci?gC2q81|8 zFX5cnP9z}+N1vlL-NT@;*D&T41iUotG=S@aAV=v06K_nT6zolOBTVy~+iyW3PW0tS z@^zej13^O=<`?n@SKdNX5WfwU8=y`NPNjxC_h*aTO{FLt5-nB>9;DbJ&{@^>8;pgvnA0gy{G;vp&xG(L!FAdz6rXNUy z52V69sqjz=T#dXJxhG}rN|}37-(8*?n^YbJq*#8vJa&lipTfSK>z>% diff --git a/backend/main.py b/backend/main.py index 024c22d..bff5e75 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,8 +1,11 @@ -from fastapi import FastAPI, Depends, HTTPException +from fastapi import FastAPI, Depends, HTTPException, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session from apscheduler.schedulers.background import BackgroundScheduler from database import engine, get_db +from typing import Dict, Set +import asyncio +import json import models, schemas, auth, simulator models.Base.metadata.create_all(bind=engine) @@ -12,6 +15,32 @@ app = FastAPI(title="HackOnLinces 2026 - Recolección de Residuos") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) +class ConnectionManager: + def __init__(self): + self.active_connections: Dict[str, Set[WebSocket]] = {} + + async def connect(self, websocket: WebSocket, route_id: str): + await websocket.accept() + if route_id not in self.active_connections: + self.active_connections[route_id] = set() + self.active_connections[route_id].add(websocket) + + def disconnect(self, websocket: WebSocket, route_id: str): + if route_id in self.active_connections: + self.active_connections[route_id].discard(websocket) + + async def broadcast_to_route(self, route_id: str, message: dict): + if route_id in self.active_connections: + dead = set() + for ws in self.active_connections[route_id]: + try: + await ws.send_json(message) + except: + dead.add(ws) + self.active_connections[route_id] -= dead + +manager = ConnectionManager() + scheduler = BackgroundScheduler() scheduler.add_job(simulator.avanzar_rutas, "interval", minutes=2) scheduler.start() @@ -62,6 +91,14 @@ def crear_domicilio(data: schemas.DomicilioCreate, db.refresh(dom) return dom +@app.get("/domicilios") +def listar_domicilios( + current_user=Depends(auth.get_current_user), + db: Session = Depends(get_db) +): + domicilios = db.query(models.Domicilio).filter_by(usuario_id=current_user.id).all() + return [{"id": d.id, "direccion": d.direccion, "colonia": d.colonia, "route_id": d.route_id} for d in domicilios] + @app.get("/eta/{domicilio_id}", response_model=schemas.ETAResponse) def get_eta(domicilio_id: int, current_user=Depends(auth.get_current_user), @@ -95,10 +132,51 @@ def crear_reporte( "estado": "PENDIENTE" } -@app.get("/domicilios") -def listar_domicilios( - current_user=Depends(auth.get_current_user), +@app.post("/alertas/operativa") +def crear_alerta_operativa( + route_id: str, + tipo: str, + mensaje: str, db: Session = Depends(get_db) ): - domicilios = db.query(models.Domicilio).filter_by(usuario_id=current_user.id).all() - return [{"id": d.id, "direccion": d.direccion, "colonia": d.colonia, "route_id": d.route_id} for d in domicilios] \ No newline at end of file + estado = db.query(models.EstadoRuta).filter_by(route_id=route_id).first() + if not estado: + raise HTTPException(status_code=404, detail="Ruta no encontrada") + return { + "route_id": route_id, + "tipo": tipo, + "mensaje": mensaje, + "evento": "ALERTA_OPERATIVA", + "estado": "ENVIADA" + } + +@app.websocket("/ws/eta/{domicilio_id}") +async def websocket_eta(websocket: WebSocket, domicilio_id: int, + token: str, db: Session = Depends(get_db)): + try: + payload = auth.jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) + email = payload.get("sub") + user = db.query(models.Usuario).filter_by(email=email).first() + if not user: + await websocket.close(code=1008) + return + dom = db.query(models.Domicilio).filter_by(id=domicilio_id).first() + if not dom or dom.usuario_id != user.id: + await websocket.close(code=1008) + return + except: + await websocket.close(code=1008) + return + + await manager.connect(websocket, dom.route_id) + try: + eta = simulator.get_eta(dom.route_id, db) + if eta: + await websocket.send_json({**eta, "route_id": dom.route_id, "colonia": dom.colonia}) + while True: + await asyncio.sleep(30) + eta = simulator.get_eta(dom.route_id, db) + if eta: + await websocket.send_json({**eta, "route_id": dom.route_id, "colonia": dom.colonia}) + except WebSocketDisconnect: + manager.disconnect(websocket, dom.route_id) \ No newline at end of file