From 279994609170a9629977d6582e803d2a739d919a Mon Sep 17 00:00:00 2001 From: ponzischeme89 Date: Fri, 1 May 2026 17:40:47 +1200 Subject: [PATCH] Improve backend error handling, fix lean logo on login screen --- Imagotipo-azul.png | Bin 0 -> 35780 bytes backend/pyproject.toml | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/src/lib/api.test.ts | 20 ++++++++ frontend/src/lib/api.ts | 63 +++++++++++++++++-------- frontend/src/routes/+page.svelte | 45 ++++++------------ frontend/static/lean101-login-logo.png | Bin 0 -> 35780 bytes 8 files changed, 81 insertions(+), 55 deletions(-) create mode 100644 Imagotipo-azul.png create mode 100644 frontend/static/lean101-login-logo.png diff --git a/Imagotipo-azul.png b/Imagotipo-azul.png new file mode 100644 index 0000000000000000000000000000000000000000..2c6ca39860cfa70f758f303879f5e6abd2480a26 GIT binary patch literal 35780 zcmeGE_di_U^8k)tC5RFPA$myEXwiF%kVHvzQA46c^j_8~&yc7Q5iKmy5;a0}c14ho zSP7!r)uLN{vBl@&`FwxBpMT-=c-{`6iL0wUGswf zVIt{8AKmiy@-lNrsIy%)yGz)CxPMkv54>xdF*N+SeBbaqIQ##e|4HC~68N74{wIO| zN#K7H_@4y+CxQPjN+5qbM5rJHb|8xr!3?5 zv3QhPlby3==vm*;^&!L+(EdfGD> zva5hT>gxq`noQdZjm^}*XKx+!$Rn4_?+4iY*^AQ6J|imf3IkAr=@u@1re?y!b_y5S z&IC9Mm4Kou%|2Uz$o7kXvz{ZR_ZoS7Wpd5G=<>bDBQL+}TzNi@$XFpeLpLBuil804 z-|JRjG)#GtkN`kFJ$dAn;FA`t79WHL<` zenm{?bSz-Y574U1-!4Bwoc%q(NQ&638zl*U1ui)ixBG>&*W-o&Tb_Jb@@li%rk>xw zpynW`@7(J_E!c;D<)XoH9_Tze;Y{#9&g2+@*q@-JKvJ5_{u4kw0}#8;Sz^O;papR_ z!?te#u=Qq|>aJ0!{T~0#!xcQUsDwm|+s09L$gKpx%mpI`*2fKJb(GX5uI zx=HB&Rx|%9iNk10;R%2Lt-FRWDdMBO;Qv2=9w5iWt~373|Krbt3eXNqG^wHwFZfRf zb3m5&vrK!|KmGTnLK9#c7yhjUD_S-fqE!3Mc}8OEKY(Q21jyj(5rY5c*?JzfT{u><+NcxXF47llmzPlH7hLMUC zF#yp$(aQg%?Jx>rQ#UN*|E*;tb7qMMNe~@8v22bHv^Uw^f`8{_yF}u!RO-{)5+zae z>`VvQE`YBZZa?@xoy3C3jvXmCZ)Z>6{QoW9ZU!Jp+XM2J;Z|@)$I>+t2e!Nyyl0lG zp$X=C3%O8ACZ-Ra-{E!O3 zg3=ur(cB3*+N(R6>sxwbaS7Vp1aYio+Gm_`I>944DORV{(K9$aKoVe^W`hkfHtyH2-;gz zwM^B*cQB#Kx<{s@omL(8-5qLW6kBNTA-%8gzDKp8JtxqwOODg2s=HKj2-9p9SW+lU z{1fHFDjA9CseR4bQz6~1PzE=d(IJm~*r?`Cn(q>`$R=Y{uh6NS^ey>l#qA<~y{uMg z5a>K?P5yl(Y!72vWM-H}MiZ7Dn{q}S>jl~-R@j?@B!7gts^oaoF8ykxSz6rb+7-Al zVj!>5`B1eKK!R@dVSFxpF`Hy5YwHd4SdZybOkA7g`%O)4|2M1hDml(?g7+s7Mf>P< z?x3#=)03n7J2)hj^$TI0W2JyN2Ra~avH7e6*4}X?9WDbqUgp_~3&XBf% z9oqeCB3q(3JgQSQ(ulCS#r}$9N?F3B>8bqvvc*x(HxXvg6z)GrV^FR1Klcau|9BAr z*+m}Xuux?%Xc&;0OffPU(-GUUc@o14zY5THUX*{9LeUQx#mDtNbI#W^R>#hTML%>6 z9l5Xn1X{Q0`tm}cH*xgxMYjZ0&JaZ)pTwKSr)XvIZe)8;>|n}lsIL7{km?4v{O)x* z;zodpDk}rY%<&Lpqn}gc+>w8kyPX>D?rSDS;BN-FRBX<|lo6~adr00d{054pCDP4! zcTSH+)ku+HErh{!r9C9O$lbZNy_Wibn7R)Wb2H`9g<=;$ul~=R+L1*7Pn&PYXg<_l zAmRBK(}{0l%j66jpadd6>j&;C=^9&qBAZ)jtUk(4yr*IHbNy){iXd>*#}fd*{24#R z{immH`WmNSrTrUt3v7Eef*Z+npWgPo(Ecre?a0keeH1Bs4$#>0xkSKv>Q*x`X-OR_ z4$iM3WK@R+hZZBn;P=;_I;|78#TRq}Z7)3M8t&2AlRi(eV+w`qwOHSyHUc#!B1Sk!!e2bcjbv$2K>SYwHsx)?~q=ab4L*{t8>j>6G`@8{4aIDQDJl8KTrHacXt(T!;aKgq2`>I=%?`mjK{uL;iP~Dv?%; zymP;%a5<*LCFLEmr)Z(l_AbH*L{p9Hnd5?Q@Q(>CyrRKwRJLv4DEn+t(eS$0ASSVd zM|*4l?Ub&;$H5wlB8Z)9e8+*GQ8fosY1~S8jZ5Kc!SX`!y&59bbr9Deg#QwQPWwRbGi1Zb>^; z-W{mDBon(w0S_cT_4w=@JUN=ugEj&C{*uz(xBI4K+eKDvjL z4nCxr{tgqT5R<(lwXE3IR3)epMVz8pe>(S}UCJ=zKEl*V#@9=Akh$`hmU9>u1!`Bu zdlK4psk@ALRcPA)7)^aNxfXcb9jqo0SATm^{`Me;tc6>x4;YssR~IcHEJwT&YtKXB z(H!9@c|()w%5k~-+E{l2?pW;zQ+_OW2en~JtkxxjP1c)N#jIh5DfTZ%z5)&Z*!eb^ z6#(|#jM1DtG`XwqBW^{?<&?yoqd5}wKN@VE9J|yKG0eZ<1Ak6rNFePFix4c2+_2xn z1nK;}w}T9_JPtpuIu5Dpfl1pPk3)uCfIFbA5nkjpA-oJ5ff1YiH&i( zkQ=_CJJ&pTfC$j(EXN*9+L#iE+IwO8mPpz2{u|uE*=tYddLmL|QgPPI4A?Y@K2%P& z=#R_S|L)ZsHG#Pcj;J@`5-|3+{YZC@IKBEjhDfUcM$jL_YGvvWUhQt(fQb1}m&3|t zsr5bc-d#xH z&=)&3vO%tJFP*=<30|&m0m-#~m4ycvI)mA!Jzx57c4~jybxt)G1)Lng)TW-Nw!6{K z7_FbuQ(F6aaL$vzdtgzVfy?jpHfgL;AJP3c`*5H7T5t+_Uvq z*V?|wnYHq(Y`CZib;~Zj0g!aTd6t4ug6%+AUP-5n$L{gIUnUuN@-YBjVUcFxN&+C7 zr&$zwGQ5alMgwB?+?c;FgqF&JofDK|M~-gPAv#+P(!jC#$A2+s3ELHp)C=LCVk~k> zYLS|b zv}>5b4fFF6>QoQTMVB^n1Au$tW8Dw>>~L7R;GrqH6=QQXlvmBEp%0PvtmR`hZc1cf zr>Mi?MVAqro)2dWKkC+;l22xjDArk-uyK2I^+y61AH$^opEqU0DygyXchkQw?+=iaRo|L4DLTd_%w8GPZ?#6*XYBm&eDT@`r^42#&g#mRl=$=amhM z0`}{*ce?M?Q6n_Zqio04`{k}RHxJ8S2po`qXT)g%DTF7G`SL16hcQxlZo4F|23(=C z`FO1R=9RO%9k>h0_dfh+1gd|VPVXTu?&1O0i2OG_E}17|EgZ$Id>fype}PfI(cwKb zr+vhcykaplY{{WcQa0ofNXlO&in6;o3oed0 zV_vwg0N3sxg8I0`Co4I{OIgL7c(azQHqgX>cKyUd+Tz#Cu<9PG zNqB-opK>s%7m6Xb_p%lvFS_L)``0WL-0!6csur7kc5B0@WqfS^3B%4q0&@acHZ@<> zC5l$A`KnEPR&aXuU7AXP%|KMjR$FjRw$UNp&YXN)17Q2pphq8t8q?tBG+fP@D{{gR zuP%g|Fil+oB6aT!jX*Z6^7YwWY1qdqyt=E>P;Yb%?;fhj=4AUa%{`hCf4%py)^Bnt zNZ;_VbpV~a#?Shp>98%Nws?`dWg2q_IRDaYnRvWwligr5V zz?&3)*v&@vrGDseYDj2-{W8TZ0I1NXeml1#aqMNcXYWXA%I<0>5;#w=!zg8`uOuWa zNIx|+I21UYf2%dbYet+Iry|=_D~)+tn6pihQqu0O{%2upEi}CG_^-_9&+CW>!{qgm zD~}=S?W{Sp#J{_QV*37_NeOVKt?sHVyt(E~;-RNeP}4PlAEu z?vnwLGs5>RusaDv(}?8Jz#Fa=h9XGIB9%kpc6%UEN#GqnL-_P3*fk^P#b+%?suiUl z5*cfcl)L-utDbm*Nz_Ax4ZnIAD(RVp!CIbr#b9VH4%5RH)K=DGI&cRpaO_B6QiMzU zv*{l3HkR##!D3S#HyjiQQY<%RE#$G5EAspS2*@Jtn|eVYB-)Ykwals zWnnamXjBez8`Beo8tkC;+ySnT!Uf4%{}#Ts$gqEKGAGqF(hot%k}-n$3z-2P8>PID zqoFgtRg9C}M+nM2zZH+%X0C(JuRcTPS`hv95w7oAC(Itm?~s>=ZK%jHk*k#lg$}Ch zRV~wkskbLOsI{W{HD$E6{e7;q^H}o?GGN#4MQAf@l&4PQf{>s)r<&G{- zyddcN@l)=DIqopBgJPY6ov+wlQRRtH-VAlown*oa>E%DziPo^@wKloG(h@)1m<*3s2%!xkZ=#fU-sAHjDYj= zSoQ<12e~MNBx5@%mjFdT1OA$g%--VwzuAOvH?q;n3vL&yChSFFR&zZNb74b)sB2zD z;T!1*Ulwy{h?n3aa$ssKXSx&k4VAJu{5kaTLt5Lbz>fj$#5mRi+v%eW`flyYot6)5 z*R8-+7~r|KvgB!wlsDQGs}El%3%EDd6gWbe92$oeXrB4C7*Lyw+j-<-4WFmuy(TaF zRpD=CtjE0H#7%}#o4+Y)o|{4K;y8#M17$8a$GsoMRrcUEg9$#=cx%$$xaH;iszRI+ zSUPG{5YW`M&byY!Uq7b0`2k_HU7GOSuC7IGGa$FaZCM!p3RoB18UzduKsVR(U3w=+IuX&8Qo;SFW(K6i&y`@6-yBO(exPT9D zI~wk&PCirI3O@w7P#Y5$8h#=07aHz7NLr;@*YXq#HTGYz? zKzJ2BQ@*P>RhttY@_!0FAe=_Jc<^Z$)sMUubZ$CMW?;<%ES+Wh@-})XJDmiwM$~zO z{b9`qmy>cmtBo%_`oY7sl?8!qzs<_^fTY!qr0Wy#?bC|iJ=I#aLULJD=CW=P3X-Z< zw)%Vskb?u-4Yf%;|8O3m0g?SY%wq zP)$VgmK}4pP>RgJ#Kw=qm%WA;zBq6DVJAZ4?(5&+sh`i~DzZ72R1@v79+v&7K2eZh zpsQZU*vQn%(H|_>j=NJrP;Jn2kVsy|8NLWZ&iJ+~P`(K{@Wf~Peza&A2nz%TSwr^v z2LkbbNv2bk;9*Q;s}o7!;Y0wt?|g%{_c$_NDJq3t5}#)q92 zqhI^1eeR4S*a+b<3_I8ojr|=H4Ix`%BXMXJq2N#LUH)ml{I*%~y|QJMLo%ghK*YqOx7$Tu zo}~u>K;`{+b{p@!2`v)m`nURvi_^zJb53X9t2d-ME_d0u-!>XL8%@e}|Hj^qJqMo4hTkyDrN9W3`vYmZN@x0(c`0RU&lckk%Kl5%g$)dquA z`B#uw^8~c!qm}qX+x0R{r8}uQqAuRq;kCi?D+EI?$C2u;KN>;vi0Pt%vA-)_^>;p) z4iv9ylW|=E^fTt!sT8MnMl=cY^Z^lf|Lo)|Hkq5+)MnlU3yeHYOZP>XzPdTGI=%3V z@J@AW+y6tEjEVhLv8O&9)O9SD@}BkW;G@vL6wO7Z4qj$qZ}um(=dxelIj34f!xjrV zx~k_aoz*MD+nojdUL#$y3F!+N6jlJBY{5s^vDJ2CfY;40#!R_-K^!7CL|Smzv>{|= zE3`X#Eu^4!>>Mo_KcLV0HD)$PlTOR`=aqm69KXQgMuu?fNnLd`{i5X5!P>p8nMi)adpOLkbWUqg@rK0tPCh_?;%6DjH(WO~)_FY0jp(;qN@Ge|BkRNHuu$jYWQPFKJxv*z@>oscD-6yW$)TG+qdiEexa=R7Iu za;~4b1NDL8*$jA)@pD=t2HttOu`6GU6uyGrx27GCc=Mrc=j&AfSV*Tzw3BUR*z=3> z8~v^s^tnMzQiD~WI@iU{MWA5nPWru%!3-2&v2uTUDApJg6e?Y{ioeynn0i;8?Igdp z&#WT@ggn8-55@tB3z6@6hsY|5Dw$}nq`%HUyLp3$I#<_Y-*Zxw^;y-hp0n0T&3W8a z?s+p4xRU$_%7J5)q7=6Gc`T{H80^N}8r5^p7R1JCa$x7{_fq%{ozfld6*Bmp$G>ez zcS9IV^&F8u=4KVH-f7}uHd%|+3H|sgxQEDeoi3L18;@q zY`OEyayHsLF%n?QGpa!gI1si16Ke#|_3wrVV&qGAT134|UL+V*&mSQe$&(#6wPFCg z>YvNSH?qjriR&ds&-(deXlWxsx_oVnB6oefFNv1(ZI<^-NHCHe;uZ{jPQu$i_up}E zIQB5TyC$$*v|R`TkHqw7Dna4Px&1nbnL@F$e)u7d=1)3L5CrtCf2>lrM0^j3kT9v* z30t~xjCSvJYg74lY_rA;uMSEk12$VPMh%p6!51%9vm&|j z?7*eo@E?GJEb73Y7^(~%wi%h5R{mIB<#cN$n$gKX%afsEgP{v3s5KFbKTa9k5S$(t z2?Xqi+zRM^fE7LNm=xoz*Uw==N9}&94Hup4Kj4?-!8x3AE!Sj4yahmELevwtuUxzH z{2MQsD%QwAgJ!q<@6GATkT*jIbP2Cx+Aa3J{_!Kr98N|n^snyr=6QrWEMlt@|r zd40iuInaO%2rvKorLQH_*kh!fIrw-Bi&~|?tA_=pO?4-U)3OnA^X&Jiv+XqJ3vLxy zFogJ)mY6#{IrqxkA@tn4k|4*BbI!LoA6RLQiZ(WjE~VUy;>r@ggV(Gf<}Gq6z2M4- ze=ouu$r}7Cr0byL`d*vUpQ0!&l2ASiglCzMwFJeM*JrM%3`-JyCV!_rObQOauX@lD z{6s^)e_Ew@e_Gzss^}(ArC4iPtG)GJi`~K|;RERU&Ot5tX;R%lvhl%m5cCdsh{@nvaeTfKefMJQSp(FU8yP&HJib{&F>+pD8V={iD!4 zAbWO%wub?~{zq9tmwm}3;ZA-FdE^I^w*?bbsp0Qrrp>G&c3_13=sTwXmE*kLiCje{n8u{xZ>XCNJS z%ba0bfqQ1l&J#)5^?*zzS!F^kBm$NoO^#AIXPHxT!!f6ypqw-z2z!IwW(@<6jk(hK zxf}=0zlQgFC(?V;9ek)hcv;U7%JcLJdEMP)mh9i^K|52IdfZ$nxgYa}(fX$Wk^xVg zfrDy)L9@jf32)a>PeDk);<@vrx_myuo>aJxNxwmwX$&0`d# zlHwO%@v{ilqn1SzT2j;|ee5Sb@|IBU-gj*>1|F(TRUG!v+=V z?$6;mz=~ap0tt-eJ5XK;A)A>RtXOO>iV@zY+}O!8s=1H-*+~!7HPZ1+NXO9X9!-A3 zSm=NI!AMR-J4fZ_wcUO`afL)Cjs@{_ZXi)k&@GH5UmL7ZHbhWDSqnxNv9_z&!zs8Q zlnbP5aCw_}Tyzlt9*F2V>=eu}@xq-Ix|@DTeDqWIVJDA@`m3M33|FPi9MWmhLmc*S^u0b|AStspwDQbX;$r?TT= zA5lA{aN42r%&!4UH>-p-1FI=^q}uk4RmN}Lt= zsT{0=ngn=IoQZf-NVwtA(eN{s;;1l(V&C`jQPX3F0Zzr=>JrRO^z&8=1bga>%{oA4 zGrqhYW<%c6zFX1iQs8x>d=A;Md}6x>M(tY;hfTgSB$1a6#X6-ntZ!Vo(Ic{864u;d z2kErb4L4`dNb+rk9~ZNQ%?2KbNS&nN-qs}UohY@7#U#T6A9;@(l+lOr$fNn|9#fuzD zzq|qyGk&lXRDcuqqkRGl1o%ik%A5{?icdTni7!g`U31}>==FHQzo;-m@5wbih&dKo8|Sz*5FDoJgkqa3}_&KqvLgXL2#aCYLy7BH5-#Gc_E$qxVDu2>tuy zaHGYHwlaYfugDj5RBuD(lhMtSdxq7ADYYAiti!=hQWjy^rUeAIqL|FK-W$^3HME zG`FVT$GDRX%?voa-Rg+={>x70A%hYa-l;7=iJ$1&-XRK`)mTA>6FTqbSG&cMm(LIh+)Us5Juz|JO2=Gt^B0b}s(&IRU^p?PY@Mu=MryY59 zbg@A=soYKIw-(BOo`dlB9qE8JTqHVHbibPGk-~3VQTXxvQ^xH_utT&nY2@@r(o9=A zW)lB6u<~qCw7c4?kF^pag=g#^db|X+Ei)ZjE3bp^AZ!6py*-I0nU!{5{#;ECVm z#|quHeSN$m^%4Sj62N~@pAirxMeJ(kn2dLuwP(1&>?Jnqh4v}!il2GT-B~Y4pwd8w zn~~F*uUY3f4X@NKc>EZxI&l?CC8UvdFI8XK65dheD}(MK=HXpr7--R7uRUF5!5ckT z`q0e{$c(q~xz=$qDe)-1x-Hlv@^?@&kABh|7zo)1oGt39qb`8%U?SeM-XqMXi$o8H z57enq-_&unc(4>jL#Y9q{!>5d>K~?7zi?s(P5d5xqZZ$O>9RF7fgLN&AjehigLWke z_ttiB)4Y!;S%h0rv4Is7+>H*v?$^%2BIWGsBN3l%A1vg~wS;@SlV}uLy*Q811j%jI zyxZ>wMljq5?y{T3ixdF+FJfpp8@ohL#Ahs5Uqu=}-7fFryQDz&_b~Rz7%E^e04|5e z&uR>Xi5i8LjE)>?X#yHz9@TC_pQ>D;vbHrw9?2cZYA*M?{aSgII%>_|0YGU4MH0Ln zqyv0ze>sPqi+Q(H@O?~%?unf+Ef5$Uhc~z~z*KnMV`Fq=*fQAOUK4HD+DGfyS zvk8$pbWuArSAD=eZe_(^YtqBI!8u&J&~qLKKEJz%x3P1NULXrbOMhLz1uPj&@LJ%O z+y(mt_pf#D#L*IR*&fMXVFa>W3a9{ue^?OS;qq=MaVp{WW0;|^xkJ~()r@=Xj_O1^ z{WOBh1D5dF5|EB>HR|h*V}DkBy`_6Z(0=QpfNRk+J^;-?itx6f7hadZ7iTg^uKJu0 zAD=xA;ilqlaRc&qe2WA&j(Y##DAHxYwWQ+%XnO(*s7BEx8K`D(@5?X~+&U)I;lrRM z>79TEcARoixt?CGY1LoRp#Rbp48V%u~gN)0oYx5&-GWuK!sj z{f|K81)=@IpQ*17HV`{({{+%zt2rz6yB;$&gA+$^Uq zg0rWM&*AR=_^3TC@qs2C;9*u|+t2t<>%^RkeUE)u#5uL*^CbBu`mZn+)=Ij{@8Y`u_$8VbX|Wcj91UOqYS|E4mt<`D|fM z!Dy7dkk%5|L)~pMgZ{v|5FaB2@Uo6ZgbS1`K(o&alBny1Bm%2I4ZJ zPl6V(Ei9kAE21`_6iQsv!aphkW#gPys}ox|%2&?gYvUrEU*H6i2>|c!RuP=(&(9i0 zzYbbe6Cwp%{~3#d9^Z6MvHQjd{NavGKEo!ixSs=2aDDb;{(4Dd1W!?PgeODyPJts3 zIWBgCvr`1Up{z0-R3l40aD@~B(#vUI!+xzNa(=2qSBRo@I=0hd%>RWM0=NfPzrm>u zG~zC_b&n$kG@%ycAoP&K>tzM3RkCpfUj%FSPUf4V1_q0c`VrpF!yey8?;KK%O5ciJ zyqQ$sXfSsSTo8=F3wR5{&N$FcW+}l^7DL6Q6-0B2L)ef;%-4XSCvRrh1Ofs^HWBb+HUSs# z^iuO*(2!Us{m%Kd`}aNlGnN(jbV6DAQGj>kk=gqF&R44bsJy*xJC!>3VEmX>Ngn_^cq3O>L=6q4Ilr%(M|(v2sx4~gosyIQF>pCpZq z_Vr9WEZ6+Ow!ITFUpk#z)~0s-l*38ZE%;D{NB9o#{`>rO!=+xvk7>h&KjSNu6vrRg zvk3U-oDLXfM>?%6YR2;4K5m8CEE6J3UIPP1K}W7*a?1B#SIR!I#4)%EomyGh8+Ulq zhj)|OsnZg=W4RoX6p;@W9(-0bf8FM3VN?u_-C#PkttX26DW8qT9{SR;rJQI%ksevw zH}9epfq>8VYPfWhR|N>tyI?g6BzO2S(rI%5#4E0R5A&a0$B*_;)6`Pdj$EW4Cq62>#d;Q;jc=t~P!zrmEi&@G11iuSuwQ(<9 zPzb*hwK{sctguh&8N+u^9i-YVTLbu)ShMdn#zSh4Ll!L0aaz#Qv4E_dSPt>%<&2)x zfwFl%ojLiE-(&k!;^okrr=;NHA&y4UoVPdStKCZG!tb4Em`!I2E1Q zw;C<~)cwk9)tsWShF^l!j8Cy^w#{s}dHP!Hg!lS$mO~5TPBrtJyGqIj7$q2ZSv@r8 zvv{zMI$0PYtO(i>Zk7Ejul+(uy~&FT;xq13!{BX5$x3!QMv3V6b4#bMH$^!D?`^Bl zcHyOW&rOkcJ?bsj*R4Hb4ZU;9<)m*BL%1Gp*f^;=D_Q!4j6z42&Fj`2o+><|L+mVR ztG*PomBeRxXSiXG$BmrjTE);RV<}5~=8PT>vj8w8?(z~NQmIkQ?Sn$uEU14zVg;k% zljVHtdyX80wM)LCMmKk;ai3HpDW2E9s@|lAbXbnQM;^>O+Sf#U-hp}efYYfU=kL=p z|J6GVhwXnTeh`$EymKEMv@Wilx5jz3o6aRzF~Bzu+Nn z!GdqoFNc%lQJqN2`#q=_puunnxFoz3qzccYkOg;X6h`U**B;G(m$I#TB^r)(86BG>1R?nxs1|0HRmji`eub-LeTu0S_~7#&w!B4tlkZ>@Ti zM!cuYs2sYQE=spc32qH^Hxyg!h)R|3SQ?*FMx+v)R`-2v4wb)!&pW0!K%h|<&-^4< zH$@WR0U{I^Kmq%ocnQ*Tk++&?r6%q~=s46`PTk3JEZ)s{>GEa1ndY*8q`|3aj2hL< z@`Gg^TphjGUT->BQ~-Sog|I+q8;wpgdeU)Io+7G;`Y0}N+u`tqFsxi8gAFemb#YYh zba^1TE=v6tI%7Z=@2peI1`WWA3TxM6eY{oG`p-mbklODBI>%`EPnnxidewxeof-!y?tkKh>qFjIhJKNPEE*%;dw`n|oNcZ}!d9i_IHw#B6MA&Y0p zBi}qzTOR5Ci`$kkah)KYY#ATkXw@ndYrs8HQFeD%B>w@k7dCS$+OsQ zkCM{JEuSPZPmO7^QWgflIfoSzP7-o{-7BBU8xFMQk3!Ec z8Cfq(w>ww*j^u|?uH?6q3hhrpIN~F(#U(hcWAdU`$GbGZ#{aQ5{>{`B7C!?=o(g<#6SD=V?p?-uRI!vx& zJ)Ss`nBD?1ZIuWn=V~(SSxA&WqU6?$7C%&Sp}&uPf!s;B&fF1&Y!!1JUs4Z-W*?H!zTDi7dE2hS^E9YNWy?Z3^9_ zqn)fN3&~mD-VtDS>K+f0z=nV>Kub9@(PIw9a-|w=0yRvzd{lb@K?S`&YvlkZUf!b9X)e^UG zSOJ;zlGS&@-@gxOc$i5z@p{6M!UEdapN|#Sc*wKHRx9>K zBJ53&hPP2iALaUrZ+LX=WxvOw^8>5p{Bl;lXO1^oGEOcI4WwwIGuHF6mPwG`uO~q} z62N6{h;F{n^`i=+d;?OR-`^P=G28vHa4Vd-2pan~X(@sCoY|mq7baAd_d#88g{2ot zwLHz1*3ovuDt6;=A8wER&@kuc1i)MMF`W^bT?ziGuKRoOOguwj_f(W|Lso;kL2gbt z1ytm48p7DN%kZsB2ag-%rXmO;G(4l=6|1<`AFO@Imfahu0A6%TUb;CgF?mYnwTJ+v zjWE@>$r;)i4Z3@A6os{pak=)_cKE=e*)0pLXv&6pFU&v>+m_s5yD&-V&!i-s<3> zBe}CNxH?b=Y|gKF=^ky=v>%vvq@vD?*UUr|PAAiCm|&kaY>W&pRKx7tj#)n(o`bgd zPZ|9NFIfD>9=`p=^${|bCjlh{94d;1Bu6w{tlAoJ-}29$%?m#wd|YmNk-$HD&{J|f zWJ|GG>R0g~7Qy>f|3#OaYpEEXBBfw7Y_A-9LwCcXEA-bTUU0|CEK}07;INxb8+sY0 z^1BJ&@~~Jsl{EepD<0dOdy*)9B;djs;-gxjC2AT^S9=oGv83>})(v`!nahc7yr~mo|b^`vpt>K==+8uB{(Tc&{ z?Fc=(rmj@v42tgh|N#F8U{nhOBNF z#X^!U?0hqeWv^DEk0Qx*fc>-70S6h3>={^p05qyuXyRrUtJBds_Y@)=H58JI3E9L) zd$^YPxtj8=l>_AQ-D4ZeDH{JIK$6p*@qhz~I!}rnvhP5doFV|O5vi$>cKGA?n)**Z z^}12E`PC;S(@_OBxy@F`n41iZDdUWI+s1p~Y(SlsRJ)Q10{}gQ#sg1ksYzi%@p(e~ zEd*&9=mO?T7wHL5WZ+4QP90e&nZp%glEuNrB+`^j(nG@@;pe1C_E>qUM1|)5< zx-|Vh4Dr01pNA1#GI=T5H7h}DO(&cIK$!rbKO*+Pzsdr%h7_Cn3(uGHdUh~<&;(KF z#HCSKQ{*YwfFW1~@t>@Cyr^s$;n^8Xe0YfLlT=KLZ}8~e38n)>=GJ1ALrp8@lK9or z0d;9C2!CtFZ=p|TvgoyI*bmGfEWHuhKWvL=_FShU-jZfzI)xiQsj@eEZ8fE6O{&*Z zUH@%57eaf`jquW+o7#^gSg@Ym_B;hUXj@q1A&a*F%aPLO*xlkYH$qMdlMNCDdZ?*D zDZ)-04b3Ui^=p7W&)3L5PL%CEyk9k(XcaEuei&!)P5DhOVv+ZXRyX36S;4Cb;4^Lj zWNzfV?LvEHn9E+!PQ7%LMu>8k1oIij1FoPgH=nahmibfQdL!yrW8Tl9c58>hiyq)JKnETx z^vplK)Z9dU(6n=}=vjNc`X))dKN6&&eBh@)5~+F}mgLULbqd}LMiT8!OW(1HQGIMw z+)eY!LcdjeOp#mCoBF2!fCt#11lRAsR50-&AC@3$;RPej-lvSPvJtZ_3QI1Jz?qt7 zxIxc|%;yK0g7Hmxz@qIs@2$~?yF<|zyF`}Fnitiw#9_}~aGc>hJ=O|^<5l+8lvCco zF#D8--8+M?HxA|ep)gc{59!z9C;vi%9aa${#+q=h&JPngm%!wsZL=`?1GABa?Dv9j|tJy?2qOm4R_RWjwxPc#N4D`dlAB%15DKG8Vwznct^8O1Lw^Q7}C-5%wQZ=tP%w+Bo zj;tsBlTX*v92OiNY!#T=2}!ATYlY2~q53Fba~kK!PltdvIy>($l1H${LyU1x8D`_h zz-884?T=7NAwglTP}JhYrjcI1?M4jR%mq&iDB!S9;DPg^fD7n|Vh~Kvw(8WqZRqeH zSa4)1i={@Is|=34i<&9b6W5u`BP7_fw-3Px_td6&rAZttS!0}adTq+1Xy(AS4Bek9 zEv5cQm=O*yxT;^}5m1@k;L|Z0c3?mHZQ6GR{nO&dViYkvVRr03=Qcgq zkZ}1YQ1n(Sl_9I=uN5)n}V0^s$PCmrxW?O-%HCBgDE`G%4qxz;_4DUBwwiQU7y8kY2Dipkm0zUZ<%mMB6?j23g z0#cYMBrN{{ui&F*T&@^CKk)wwbN!FuU62z~SKoTe2yrdiThBK0H7rfmb~wI(R{@}L zGmRbgr`VRng!5W5RQZK?&Ry_;aT~g|#4_O3UH{fyBkR1@-Fa ze5;JF?i|=d$kI=FFe~F8amqN8#LAU(GBV#!D)UlQ*CYRUCfq9u%iCX?=DH3ge8Rt# zn@=o?JiV!&pHUPUE0Fb8Ra`xz4;sc1KU`Rc{5iO{YOyJAX!%?iW@H<2%EbE&=7E8m zI=$$ihbrp!MCpuHcC54P-$d{VD8E-g7c_9l;T8_&ukO>fTIZ z;O>&Uw-oVIyUh0_s|25u+w-mod`p)stdp5<@vDfd!@$yfvkTUN_Ne~h4e%czoMLKw zL=S?2-Xnp5%zB@)Y68qa25Zq^vkdWad^+6x9atX%BNpv3GPdy&%V7nYxkDl5j>af0 z#q37;oyW-Esx8P_SYnb#e?@jfwoMiAp?kVfSfel18^S~*{E_i#(zavq=zU{a3I=Y1i`&-E0sjK=wimslM@$Z;_T7%z%up#5XFajp>I_BpaSb}N& zl;E)RDJp-Ee*fDwvs)h2f4ej`Uucbu@i8ywVZydN&PAN^pKk)vQ*~l>QEfF}M(^lv z;T84!tHOMtiCPV0aAS>~vST9VI2n8Fb|C8hrFt1g-}nEt_uWrTZBg4O3Koir0@9*a z1qA_7iqwc)q(}fkdKE;Z_f7(cfC_?A6@-9Nq=w#;fQS)LLQkln8bW~3LI@%79liIR zZ@!uLANXdjKQQCT+3T#m_D;^;Ydz1iu?H~@cfWTW>|qa7u1c~z-3x;@?rH7qoq6@L zvS% z*yLc)w?sU7_fOoNkVa!ZsuKfWWGw_WdEynlp!!x5ehl+`EG3Wsy2s=6x159!Vs%)- zo=66Uq4kq+t3F<txJuCb{C3D8n&OI6z_+AUJw07*}90t(Yjz~(HMVv zQx8=&ow((!BRI#Dfio8r6O6MCnz}ZGq?8T8=&c_}#Vx4igRm>tv1G3>k($?(?)ZhO zLW~sIqv!Uf^8N+J6yLct($(RjmR1P|nb{ozFWeLnUK9CThG@`a)|pwAe)KCY zOA7iPgvF02S=w0O>Zbf!9Vbg{WJ_m6R7@4qn9D)uYSHY)p1Y?4K@rV7Z^YPv0w4KR z?yr`!J1v|>Of^11S_bmiSzg?M_`73w3<};a!|&2_58U(z2!fJHJmmPEOPqn+>0nB0 zh5du2GTDOOowYBNhB~eY+j{r_GQf<;A~3AlL%OAh9TA`2YU;hC3DKWr@w1`PYt-Kr z62#0%YILDjxs#3^z0@rWRbH&z6eb9xf|K>m_~ao-SeG$Z(xQ#PqM|JVgNd@C27NkQNo!MrNMUMuH)b4I+b>| z+k$#(dub-2ab;))Q0SS2(sG+O&z`a(8V`o-8eZkCL-SKog=giL`SxrgxXZ?>#I$Zx z#$ORw?UaTvxeToDvcbFn1;1qtYKaIPA923%+HUXshI?0INs3z2;3%-E+xqz@_5L$9 z>ZNUJ+aOK4JFn#k?9uQ}Vs`Fr!`9->N^R=Q&oQx))6(S|NL^NL6T!mKR*Y1Gxh;{i zDFIPls7S$kv@FfPR%u6`A}3Q+1s|(GST6BebBq#1X@nWg-e%=+X+(r)N8&jRM}~G$vD)O88Z5j( z1X?W2K$h4)P+Fy}h31m$HtpQ|(L1-0LAg~}pJlD*I5AH|PMl8HrZ>8C(@^Wc|CGSZ zi%^W)nVvw z^&72FebEK)0538#y4GnT>XS?RIE(P5K6D1*t_q>fT)@bw97k}kg>%U1yN)?g>YddQ z!FJ3u@|2%jwZ5X0cH^)HD2=Sd38wH^xWBE0cOL2t`Hn4L`j9j36fxIcbteILdy0o3-y49(d!tGP3&5K2Ip8&0@B$g4RW%{UNeY?eQ9WrT}Tlk zXA|Er>S{pUg7$X&QXQS8bYA#|3VT15R*OlZqQqO`eDmQI4tC!)k78Pt0$VOj8eoV8?~%jQ7H(FWYX>`?JyhEzunD-A-v)Y%+v9O-bnUYow_UcG3dBdnyolBu9LE@y>e zU;UCEHds_Y7E+R>P=`Sat%+RlY7uv~bIx2%D;!E=pn_~q$X3*UXd0gh;V&f4Y{l%8 z5L)~P<}cN;2oIfz526X~C7+%=ihtOP-S!qi_o2cNy7qiPHQQP9hU_YLc#go|(Oj?| zO#tjg%SNRiMjMWJ@7LyC+JcSi|1I!0%3Orv=lTN?6gztXNX#A}y2}px*p>HOVvo(N z*7dB-vyIqCy}AWA5WL-J9tbWc1Z7;iy62*yrCgC8hX2*FGA@&-o>@)IFHGEaG}~6t z4ZzG=#6`#fKN?uvuKiC}@1?)CG|H5{59pH8W%VPmx99Mwp8!ssk}J7qIBMQL2a+K=t(OqTAad=-D$K% z>-p7Qw0sEWo^7FiCoBXcf%S~y=w>~%44b0@2J#6(U#sG~q$vAa zDtd!{svKDuz9hNh`(UccYS-m+HYpG*kcwoRWzGSU3paD~H5hvB{G}I;8aTx<+*aJ# zzB}6O{rDT`GlUV?n8b9ZG@EgLCoDC8`hadcMAGfE=(ED zp(&?+SuI4coAvBm;e?pa8*xq+)OcT9qtX_0r#m-7Xd>KivAb-G<{09o)VosQ!aaCr z0m?~(jxyHVxJxIJw94!f$MoIu26I+}2-?L`5@B)H%RGkKl(KkEsxwej*EB6^Py}KH zt#%!OipNe!=)iG#bLH_FAY3i=CdU%!eE!HT8| zeUfrXrc%Owyc7gw8X{JF;H@4V&}30lCv=hVByGU;RZjxp(Ydeq+K&r^BGDc}R#Wj! zQ&~<7g4Fi;;N?3^U)%NhR#4~@k5W7FCzZ{k-AnT>u`El47uGNu+huyRpM_m-)>XaV z`Z?L3c!nmdaS&Wnj5lv%?vuvn@*9KR!{fS6dh|cgO!=#O>^c!Lm84v!a8GdQv(TJD ze05;4*fT&wi`5MhThqBwli3A!8y%I-=?(MU-Y13mzipFt|K#o+ka_8+vFmd?&qWqa z@#P(k#C}ppJq_zuHL;|P*z@TKxNc<d#AgIPlyUAby5t*kx?t{sp zO9tv7W@P$aNmL)0&^#K`wl9pEC&N!#aEe)|omglPOhIA++yC`p}eKIW) zj|Ue^d-vC( zx+3RfuWvt_(@H$UT7qrR#}`G$S{cb}mXCd`4ROZjG_T8Di7{!Y#Y6a||Bj!0Wy=%^aFZQ>v=zW@Cq~ z%<@5~W6&-?D=HGHc6-Cz7@5w<9vo7d+3)j%NIdQtLn>gJ14+bGX4!foC6g2zWW5>` zJX(L88!M_$Z6_uNIS(_7s@p~&i%ont4<{7#l@EjO!zi>L;7{(>zx;7&VZ}M+mu;wA zv~?){15oSv(VtH&y1qauD@j8u50Q(S2z7|EP+^6&GufV_BE6H&54o#=1cZIO=w1Yn z8{^4Lx#!Lcq}=$jCx}5RU<;VT`;&WLW_ZsHtB=d>`6+Caepon9&PdW-C+z{#w<@JvsO`*Ar)D?c_F2RgWOsG6w6iXYfQ5-a( z_=2RlLb&g+&2ciL<1c}^Q#d^aN>9K{g~Fr+BT%l>`IHpSfJgf529^0}VC+_aG!h2- zeQ5#|ygu~(C8@aO-6|c^hR{Dc{}vftd67A<^9wE`dET4fAyf0Y`{?72xFqC|LiQ89Af>v$c@f6dJ zsqT7+c2b+%lq96sR2cAG!O~n}B&gj>8B>UT$q-M_{jml3SxAxaP`wV7{CPe}O5#|F zdl=|o3;LKi^Fo)PAP2cEKKa+Dj|qZVM(Q*v&ae`Oh(c}FjOO9Cw$**$&GRm5+JQSA zfFHXMySIpK2#fwV4>a3jvZw|}!NiLE4r9%w@4-LX$Gi;O1Rm2HH5J`;#gw%P1@WzX zi?0T1bI%RAb*ycEed!W!MwHY=&E-bZv__Tf(?z+3CS$wI`=7>jq#kv^`qiBm*yciv zKiIa`$9XbE%!dNt^1~;uX=n+1BI^6U|eC~4@NTh70-w1Bp}$byXCu3a3)s-b$LE2}xXdD5hJPZ?n)gaTf5`XNC9-z$RY#1cs$iSRcl5Jz zol^6AZU@ZDM`@3@7N+`bw2E?$oXt5|_xhWUjm9oSLC3E3ZXv-@;oH zZ+x55RhI``-;~qHdfBdZ*+hNlV%HxClHuh4?wJ$rHk z!SW4BvSl|QyC@1}Fuj#sKl5Rr{|G&u(&Sjb;20KKwb1m-yJm(cw;*gZg5G(G4$HjD z!v9@@%}vZXr3m>f-v4LBNzIbQoe&UA&%|ea8N2`XFv`b^K<;IO0u|5Ny<>ZK1G-I5 zbhbbDlJt0<5wPZ4DAeL=SXAwPW3TQIyc!Iu+Y^xz+_4On8EX^&kYdItq6Ag3jMsS#vI6nUhY{Mj%#l4(#F!57b%DEb5P^J2vzyVho_n8 z9&OfGyHQuhU~K*~sjPWEOg8HH;lU$^+wM?bsYcHc)s6<4!AUh>GAxb`EP_0~XNe2$ zR7F=_V{LbFea>yQ4Rv7ms^1sv1F2R~4oZsi9Ba`Jl_FO4C(8T?XY_8#0wFs;^&A9N(bq4@?su)i2Od^ROPJ z={I4}!LTZ?k)G+xj=Ag6HwmRvaQ9#~wE%zcX1$7_0ak12l8wUj9!zms*r=|$OAp;# z@VU<=8XiYzP$5`T-s~WI28Qj1RZ*LF#~9`!1g8_Zo&bQzgl8v4$-1@gt~hoD=k*BXP>sGRLY#j-8i%dZnYH+w}2$iBWZz8}>jB9vCG?1kTsP z2$_E0qK%XnMccZaYGD7~Bjkm8G@1yq%Ar)y_UMNeRF8qV>($=f*m`WK*>bH;a)i7R z=7ch4Sc@+TNK%H$oK)9VE}1tQ&KyT@_(Um26C~%Dthd>Nya?b8R@~811&?!L1~knO zr{wLei8oW7&0jd3!Q`MC-1lB`0dy*s&)OV&ysYn)j(R`#sSFuyOpMmcVoQxB@hG1x zJy&0KOU`Zff~|E^y$Y7P`#4oeAa2Jc&H5@xRyCH&f{{{>=D(^YN%!RQX!bH*du+a; z1+}n{{ocC!cw|V`&X|ZUgDpV>`X3rdk1W-_o1ETgyLt8GhTgcPzToz)M3nrpih)K5 z%Yi5G`^veeaD}B6-PeDBFb&9u@DV+#XZ_u{pM?Wp2g zf>pE45S+tGvr6GMJ%j3etv+f%{80&C!>?VD?uJhS`OSOc$WLa_{J2YX#cWs5xYWRY zhn~$CYk_eoUCQXdwteTd5@{o*iK&;(h`9)N6*rH35;(gMYz2x4eX6bN^&295p#k6n z34k2Tiy(p0#&|s2*>* zZ8`B~J2)d({d%;48#?3C8~-0S`7^@fo(_CU(~N%e=;&gm1uG(8FUL-Il^%?k+A1qS z9S3uzer*mt+il$HVU|H9)t<^7B^$nz0T)|7_Psz?zi8Q_Oiq1{vU6@7sx|)q0 zQ-n3~7VC^w6Pt|prs(i^5u&!|9yTY+#D<^pzT_}pbn<4l+~ z;te)SFWsr=xdlo@c@0V%Snv-8f*q_Cj&tXQaZP}dVLlT>&Q&q_&phvB*YCALXlqi`Z!V#{vvseo zK^(!P`*7o|F+z$+X~EC+yW$M#0Gvkg%DU*Xe3dJ7kA#)o_qW>I;Q`bpIJ~b$**&f0 zH?)9Ff!$tE1|OV%?oc_`Q-iG1Pux@O9=HWxy|wtp^g_h}(sIDg%lttNwkh>!h??vQ zEJ0$ys83-^gJ2gN^py;KbOVhv@^*j6ra5bS%DxQ}E zN3!C*cXo`I*~4{sJStW|KTtp&1dj_AaYlbTX9djrpBaZ=Rb7O=eRdKZ>%hyXz zI^Cl-^_)W?JoUO3f)N50!{se6sB5l!*G5Wtf>>Gr&?bI3SWR1+@b*jONFcvM+%!1* znJoS^82x-D`b*VC5$DJHgB)1L0RJa$LpsB$*Yz>j_Jx6P1wW3)Q(=lMc<@p1)Gjrf+M)V`r+{sqgu)%+#q?OhKkS{ z2}*a?kf;rUeNKEJs5*BDP=H(kpC1^xcA4PdOm{kqZP+44mE5_59cHk@b+({kH1!8R3LN_DHj9 z>R`dYmtydAbj{t3BBiHD+jx(PayA9|BVU)SnPF!(p1gQTbx1M(LMYmGGwlrfc=6Ce zbtGoC?WR?PU1$9_`BEj=_!`FpPXSnOcs2DE*?FZMQ~{?2q;Jz4e-!e(N~K}53*7>X zQ(e*b;veEv;kT8|$(KA%9K(=zJTMp5%H~4^pR<)!|5QAEjLTbK<4M$0qw%Mpb>R)@ zigk|Yk~H{r_Vu(!d)JN~u8|C5VQqc#d#h&}Mtgns*p`NSD#oG!yUm?gdRCs(Hi0%8 zAB5e^O!qHSTlIbcH|>g<9MJ#u5XkFsH$9mlK8zJ(EQ^I+_PHU+^@@2(|H6%T6D|f} zN|@%$6%#;7lr)NzF2?l+;mIXsQlz`*!TCxbUrI#4jfSPc2iio19`7I3jvN9T&>psZ z?&2D-R|x3f!cp51f(!Q)NP0v%K6%%^QR9Y~n^g?F!XSKAETF!8l==SE$~*q>7asT_ zltrsy5NxapSqouW#kI!k7w{->oX1t=g2X!HR5*qjt#<_n$Gg)?5QzD9e{ZO|F1%v=g7!sP8)_oPQ6mZ!80Su`B&rDq;?6fC#5<8`^;Wj(0+R-`jsd2x{C8 zL&sjqSD5;OxPqoJF0QAdMRe^g22L>ZOZdfvqcuWg^mWkY_@lH*SjvHkN zE^WN~wpsqL?=>2>t}09$M{$7>^+|!F6~B^_Lsv6m!-wq@S{ri3ov=SDFA`1VMT~k{ zWD1^1S;@7G8KVNA&$gyo^clqwqu!HXx%}699YA)cjA!Slyu#PHNDsX$TjlwKw^STT zy78yYLG~{LMz+$>X7#TY3JYhi-#;Hzy?}ZzeXXc~ z!e_;}xFEW7az=Mp_xd{F82x?t4Dpb5B=PlHyf7PEiBg=4jSOorct zx7H$i?jjuGtDiD~pH_X^wyt3`B1PSQifYu%+fi=TF4Y<|wl zFcATm$CTIS8pO^0&(-9>%nCmbqwRzgNo)`wt);beKpBR-UTCW2S?*r2^%PUz?Rd%? zQIhjAz}~{!CbK8d27@1wBwKy(q|<3rex5t~J;beY7qQUE0XaeXhG{xXVi+sb5`FStDVK|Rjh|A%Tw;3Rm>wZ~ED z)8OsTc{LkVhZ=f+#_I2_aD`k2nAV%e44qCh z@rSE)PHud;@U^I(`|d;YC-G~WCEc5RrjfAg#kfM<>T0r|aCh`AyPIszx@ImT=k`V` zyTgqIFPK?580R_;<-g_&L|7Jct1)m#Te3X1`6^d;~H2~M#_XLeIA z`}=(vo&65RUQP^O`8b%#sB_}!$r=#378dtheCncH0$4{8YYU{KV1igg2e}v>8FjUf=BfaDY_dcghbrW%Jy@E9V2e+P^0NA@XLnG9pp_SpRzHN|DYbmw>5I zV>aAaSfW-G)!X*G1@%eJ&uuq5eAi*adXlr67kMAvsBVfk@kf_fG1) zNmq!9<;8>MN-vNY*n4T%-44^Y;VsdrDU%CM;$>GUr6$_D^_4`ErO|NU1v|V z>Ca4Ab@A6n_2g@>_>EjlvYH|qN&vmVs)-+7OdRU7Q~2zi``JCfN7yAqEhC)RUz2Dl z^tY{aG(&)0GV$gM<`*?Zh=r;VRa`04SOHlJ8a6!cVS-@3`KFdOzRMDiR7?sou#5Yd zV(#1OH!J`=nN%*7RH0(a-Cr{LAscaVFyB0)Fmq-I3Gu$DOy<=%4 zTfns>!(P?1ZR-AY9&TpZ|5~*>yGG_*tqDPpT!-OX@-4lXfffr4RNKo!s7im8IUeb`bgPf<8bSD_|0ws!2 zEQb*@oCAP0yz7U=iGF}i_VIN=?PNo+`1*zxl+Y*Xw>SB+y@Ew}ArfVLW69BPg2I91 zONHxrarH)n^bG`n8QTRmrOicUJj&gwx8ylhn6-QlElEMky^v*Z2o9#ql~~S}6i3cZ z6;O8h1ia?0AB}$XwE*QRss;-5^Yuyx=J$wlTI=}WCAEZ}FB*lEHH^_+%tbbo6y?oTfc`j_Q&4-j@B9}unh%V8(+ThsRJsEd4j#&(!MVc!e zd+SIU;%XjUTTZMmV=dm#nW{B#u3&~bGR=AMNvv+8X02c6B*6rQ3CFC%Q%!ongitwZ zoUe>1^r*Cr1u1Y;S~ACf1P*BTFE8^mN;3*x*a+QtIr}Sy)cc!{wy49JDtflC64B?Fx&vp+@)%oH0_B~#1xIfuD8yaw0JDfqAYj1Xg`alvhv z(l^_$PSFCN*aCDF3Z97+KD6?`zrc79plM0EArTS?P9JhJzG6yNZb;U-jy3JgK#I*W zR>CUI@w%M(27k0Ul}0HExAU;1qVj~$%`4TZwA%dE`|Rdv<}0|RyE>0$M*G+lZ+oX6 zF3PGFn-B_S5@I%3^i_ypKgh@XloU!{_)$NVB$_a1z228!9=i9~Q;CHzzcoG3{6-9M z#;W2h{aO~D#<%;EVwnmpT4I!!B?>+dpEIzNSJ6@sVXyx^jPKid7IXQGm|wqb+D3P} z7|tnio=BBLNekY%V92|u;$kS4=RUkLd3rStFJn`daX-;wNz}Ps%#{><;6%ZzUuDCg01C6dOXnO5wXnTzhnXn1Xcs7rh(Krx zk3+T^VejRAygEciZei3L|)M$vgs47$9IEf;JLj!7%90QLNfe0B* z3^E4*{6freTvH-=e2AB;^Ku##(Yd53-olkJCgx5Clrk2p@Go1(`o6P|P0BRuvGS8& zuk5s94n4ykf5BQVy&9Lrb_RAIGb(%GrpK41nqaP;P3G2@5Rprh8->2tTX-?nuZ7q{ zaDZ}NRX|J%2MJ{nm>cr=BAJ;IOcm@_x}REBQ3bCPzaG!2$(WMHPOT6T z6IKT8L`TS)E{!pFlCplvmE)O7TH417CaWVudJ3KwUPsWqB=*jK#d5h%UjVfcUTGsZ z|5vJkQoQ3OnRB7;apBMc5#fAiVn=Lfh)Em@>pxXWYb&(ZZdHga{s{nV=-h(@`bx~E z9W<^a+6I(P+;scpXuIm$OJe%*bPvu}OjQj!Q}CGNo2t-20}k7(*!u>v?bwdFn!pF6 z%FEZN+ZzW>y4Vg%R>gRYn?m?Pi0x~!THl+0hocMXmt0~n2hLsZFkLln>xwpUajfH? z2c=dt^{K$ZV?XnQYV7*nUjctbt_CDXrE@Nx>URDFFT@sY9sJN0SQ_`|@)VFa-&S;= z!WQN??Y&i@yOX*tz0|D!y%sc>xf5<6xNYMl z@4xJrEqm(|qbPL6n3XICt93bZ-&%2NyN_JUdt~Ztr4@1ELD5 zE9!1oV`oyAT9l|T*&}mJ36>{G-`fLqBuQLhf5bS-Qgz_%^E~jI6tT8EMYTJ~n1vh? zu}Z=kGpkNu)?rMQ|x50)_$ zXy?9e#^y%Z_F&sHrO=v1CtGC|m)hswEYnt&W5RTp13$=4!^Sz)36v3t%VW5*BuNY@ zs{aORWs-Y!7)z-S=l)QZ&9LvG-Qn;@QmiU(QnRJ&7nQjK7Zng=zR%FZ)-|_9)&n&_ zH!+*15Ej-Crgq7+FZEZN;CUY3-r>l@gOU{;VeNLm-#s@JTMRfw87I^glr?>F7a^|8 zpfMMV0D&oa9~14RA-((ii>M0qxm2U2+CxNR`Oo`#Q4Pz5>KWtEZKqHD0fX05x$kle z<`_IbIMUNI_OzyA6@!~~lbiG|uhw^XSuCCbS13mPph~^Oyp%vr4bfo+w5>vh<(+B) zC+gZK|Gn2J+moGdicl}3#wtT1CT%*MfBlpZ{r)Yqd6w1&O`^mH3lm~hQUV;`e`}kG znf6-1`hGZj8r(E?BdHh#9W3zgkk)JLrQdR3zIEFp(F1Ng`DqKJ`Cf=RS3TNQOCWX_ z<}chfx!PB(|G@n=rv1gcG4aA(tCfRlkLD7*&JOb9J=$M>Vwc&J*xk@Ui@EC^5(muW z!DLF1S%>N=TK#VE2gk_JxC;Zz$B0eZVKva}^tqkHZYZ}}dN{)I=6BlV4P@Rwn55w__E}X+#x~2PYp}|UiX(np3 zYH7f+{;gR6$(0*t*AXZIwccUBmJwaVEnbxi8J6X*36Z zQ%m%NB!GP$0+pw%1mb#(kTdW74!j!h-{1eXl2s!Q$^U~}&%?JoD!lXOa%3SO2NDbc z>ioXjQeRC9FryMj{(N9B4Ky5obuC8B*+*{e=f>ve|7ifI>EMUwmzB=CuYyu6~-budK(kSU8k{09lnk_a3Cm@5u|&SfOG7XGhe zCBWSeJ$&Vl15$D?zx?Au2|(5G<^XQ>x#DeoTE+Mu!*|XBoBBi--|fo1ugIu5k@k~E z|B&Wb=7HM{0df{|gJdkyad zLjNbAP{0k@&UyEA0IJ?@|L@^aa$$A5a~zjyuYu|FMI`yF6;KqoG-G)g*}`48zVk`pj62B@3-?s4(IJstrbR0{#3 z)ww~KV*8&SV1XXg0)uvOg?mp8|L@s_z`=klD+++(P^as3mVJc%pC>KK0Z)mvKV;bH<}RDGJi8CN`JtQh;se~)AQBb8zUu&bmZ zpgaeVM}O?zp#hHh+W?L^0TO$+{)F4Z z{i8{>>-zr=!tZQzpEJ;$?>7|#5c>Snz8uh_xx;Y;{M*?Z0Jr?l#oTNlSUWxQfDY+! zo5H{Ic1(+fKN57#B@gj?ej)!EfyiwjpPxJNo3YSpw{NA#$;|!N1RXFD3}}bc?|rT9 z%jm&=`nSO^U`pF7r_|^GVKcDwu0P55$B{_2UEqsv?>tAhr;}GUZn@9XA-%1HKkh7E zk%&Bm2g2b!A$WWBbn8?6=qTbJX3NxXYY{0m41h%*gF60m6cqgHpTX@j1BOA!(>D;{ zi2(LI+D5|z)&DUk5qA0T8yrAj+YddrGzz+1p$((__2;8MjUGCxfB<4l?cN6=lGwMi zLtgN0u|IT{$Q+>p}0`WI( zwjI>r88vP3f^ov2_sNOVf2_iQZ{8i|x6MX{HGtmkc6C65JBLZSezrI#!Vb3DG4Gz& z_UFF;_0P`1a{HfOm&gkdWKPk~|LaZvUHmTs|3%=x2>cg;|03{T1pbS_e-ZfqD+2t- aZ?F`V=Vk^|+3~+U*U{AfyW+0>v;PD8lIQ~f literal 0 HcmV?d00001 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 29ffa8d..f08a14c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "data-entry-app-backend" -version = "0.1.3" +version = "0.1.4" description = "Costing platform MVP backend" requires-python = ">=3.11" dependencies = [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ef4e4e2..f90791d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "data-entry-app-frontend", - "version": "0.1.2", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "data-entry-app-frontend", - "version": "0.1.2", + "version": "0.1.4", "devDependencies": { "@sveltejs/adapter-auto": "^3.2.0", "@sveltejs/adapter-node": "^5.2.12", diff --git a/frontend/package.json b/frontend/package.json index d736a83..cc0fcde 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "data-entry-app-frontend", - "version": "0.1.3", + "version": "0.1.4", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/api.test.ts b/frontend/src/lib/api.test.ts index 9838f3a..f474da7 100644 --- a/frontend/src/lib/api.test.ts +++ b/frontend/src/lib/api.test.ts @@ -57,4 +57,24 @@ describe('api fetch injection', () => { expect(injectedFetch.mock.calls[0]?.[0]).toBe(`http://127.0.0.1:8000${path}`); expect(globalFetch).not.toHaveBeenCalled(); }); + + it('shows a backend-unavailable message for login network failures', async () => { + globalThis.fetch = vi.fn(async () => { + throw new TypeError('Failed to fetch'); + }) as typeof fetch; + + await expect(api.clientLogin('user@example.com', 'secret')).rejects.toThrow( + 'Unable to reach the server. Check that the backend is running and try again.' + ); + }); + + it('shows a backend-unavailable message for authenticated read network failures', async () => { + const injectedFetch = vi.fn(async () => { + throw new TypeError('NetworkError when attempting to fetch resource.'); + }) as typeof fetch; + + await expect(api.rawMaterials(injectedFetch)).rejects.toThrow( + 'Unable to reach the server. Check that the backend is running and try again.' + ); + }); }); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 274ffee..69ae4c5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -37,6 +37,7 @@ import type { import { getStoredAdminSession, getStoredClientSession } from '$lib/session'; const DEFAULT_API_PORT = env.PUBLIC_API_PORT || '8000'; +const BACKEND_UNAVAILABLE_MESSAGE = 'Unable to reach the server. Check that the backend is running and try again.'; type AuthMode = 'none' | 'client' | 'admin' | 'manager'; type ApiFetch = typeof fetch; @@ -85,6 +86,24 @@ function getToken(auth: AuthMode) { return null; } +function normalizeRequestError(error: unknown) { + if (error instanceof Error) { + const message = error.message.trim(); + const isNetworkFetchFailure = + /failed to fetch|fetch failed|networkerror when attempting to fetch resource|load failed|network request failed/i.test( + message + ) || error.name === 'NetworkError'; + + if (isNetworkFetchFailure) { + return new Error(BACKEND_UNAVAILABLE_MESSAGE); + } + + return error; + } + + return new Error('An unexpected error occurred while contacting the server.'); +} + async function fetchJson(path: string, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise { try { const token = getToken(auth); @@ -100,7 +119,7 @@ async function fetchJson(path: string, fallback: T, auth: AuthMode = 'none', return (await response.json()) as T; } catch (error) { if (auth !== 'none') { - throw error; + throw normalizeRequestError(error); } return fallback; } @@ -112,30 +131,34 @@ async function request( auth: AuthMode = 'none', fetcher: ApiFetch = fetch ): Promise { - const token = getToken(auth); - const response = await fetcher(buildApiUrl(path), { - headers: { - 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - ...(options.headers ?? {}) - }, - ...options - }); + try { + const token = getToken(auth); + const response = await fetcher(buildApiUrl(path), { + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(options.headers ?? {}) + }, + ...options + }); - if (!response.ok) { - let message = 'Request failed'; + if (!response.ok) { + let message = 'Request failed'; - try { - const body = (await response.json()) as { detail?: string }; - message = body.detail ?? message; - } catch { - message = response.statusText || message; + try { + const body = (await response.json()) as { detail?: string }; + message = body.detail ?? message; + } catch { + message = response.statusText || message; + } + + throw new Error(message); } - throw new Error(message); + return (await response.json()) as T; + } catch (error) { + throw normalizeRequestError(error); } - - return (await response.json()) as T; } export const api = { diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 0d6734d..805999e 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,6 +1,5 @@