From ea556b36779fefc31e68737a48a988a079f1c7bb Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 6 Jan 2026 01:34:55 -0500 Subject: [PATCH] fledged out analytics, self gather, remove vercel speed insights --- bun.lockb | Bin 472039 -> 471422 bytes package.json | 1 - src/app.tsx | 4 + src/components/blog/PostBodyClient.tsx | 100 +++++++ src/components/blog/TextEditor.tsx | 17 +- src/db/types.ts | 8 + src/entry-client.tsx | 2 - src/lib/performance-tracking.ts | 163 ++++++++++++ src/routes/analytics.tsx | 350 ++++++++++++++++++------- src/routes/blog/post.css | 20 ++ src/server/analytics.ts | 138 +++++++++- src/server/api/routers/analytics.ts | 153 ++++++++++- src/server/api/utils.ts | 25 +- 13 files changed, 867 insertions(+), 114 deletions(-) create mode 100644 src/lib/performance-tracking.ts diff --git a/bun.lockb b/bun.lockb index 9628a29680e787bb193b66f8ddf24c826ce8215c..686ed1c25d5d1ab24cf347f1e45fa1498f8401f3 100755 GIT binary patch delta 76402 zcmeF4dz?O>-C4=;J9nLnD~6pSHDb~Z~H2i*c=AlNKO@s2L7@*|O^ zFq^@{!ABh40ye?E+Tm=6;~k#kFw5cb4jVg+fQ?DNC)wf-hc7vN6l@fUl$PCYm9ixc zOC1(CEGnEfZPtuPWIeTMO@=GLG_VwG3Fd)nQa{I^GIR@NI|dKxMcBR0S?}{8(q74juuY3Ci)k=nb4v zmS0$uHzB_`(t*sVTUi>|=opm4P#S{2l4}ES4_F`E2_6Q%9c)R+=(s=0#+r~ty)~$KE8&GBh^Lwi1l6RrG)Z>t6C;tj;BP0`y3EKeD$bi#G_5#q`UKUq>Dyz8g5J>%UTQEidVKyMGPhy*1v3hZ zN*?KEE0xgQ7IF`^3Y$4AcY5*M>EpldWQW15>3P#HB=1bpE4;X5Lb{mt*s6To*Kwcb z1YWHUr5$@&7aav^tTYAHBXt~3j@h2e0o6l8LFIPNsTT8!$LCJVosc(vTK=qjO8*^R zrT6J=<6jM`r+0u#H@9OZj*dhMXXFWwiY)A7T~r1tVSaJ??BZ#JP;FJj58kNNDs^FB zYkdi7F1-e7Zd@GrwOf_0I>TnZ0#sqI&~IwYWsc7;?%H`=eo16lKihBdGQMf07Hi%OimA!10viAqtf;$EqYPa$?!K%&c zf}ORSl@?MNP1nz;oY-+ix#RPSBa!#cwsxCyY`+(SKH*ey(zL>Hk*~4C_{0-GmVDH> zRzs`{5{wh)($IXpIo*iLxc@$J1+y|<vB#Y92#1aikp> zKV;i}e;T_n@jh{Ovr*R9w*|fZ4*u_>Z7zL+x&D|oNn@=m27nDn^WhlliX=KsH9S8! z;2&Qa8)wt}-NlNGw?q6kZ1v*!qVegcP?L52ZM+xAMX9G1PRK20I_FKWrNndZoo74l z6j1T1OQTdxiR0mwZ+>xZQBm&P(#S|MP(avykx1m)Np@?z1XQ()L4_COPtTu`TUZf(PB#&){La&}}3yd#~Ut+8Nmx4Yx z4QvV)7cl?060tRPiy7yN_Zaypd+fKC$`t+}Vu`MaZZiPKM*qzd*9*8e+)3qYlc?|l>h zYW;XUygGIYTfGM0$Ji?NIZ(OYy3qRZHtaUo%dWC(;}u{d?0KNvu;=QqBS)5fgdzvO z0?JRDKt)&qHV5N}q`A4%rsWrxL^_ah3;0PiL*cV#I#N43BE4jv&OeC$RA%+ zcw)J=3np+VBYhieb$8zM5;{7Mg>O=LWNS{oni$_vo*KFXY)VdLYf;pKTY`oST9saZ zllAt<3R~Q{pt56+D9FW;_uOn_Ps^P?DSuW$q~I32>yH4H?G?A$?rgTw&OaYi_n$$y zO6deDzh&g3(fYt`ws|*O>{NC+iX1%)Yye&W%8~H|JzNIyZe1DdY|t!q$DOvAt)OPz z6QEK*5HxJqsx-cMEOv!9UuAtfA$RT+7P81+;N{Z^r(kg1wAxm-3{>SOoRU7ia9Ux} zwSU+p`2~3q_;6ood#`OwG2t4jlR)(q;ffbL(9rLHI-JTF<-*Q&c6W!v)>y}^02>nT zWOzC1IEQJVkKH8j8pRrxJZM9I1Xa?d!Qe(2-bKN}My*Px(Hi++kZbJ^kJywm*V-DK z53iB%&wO7O|EhJa2S9b{Scm65X7fD@TO+g^sG)qE!zLhITw0c3m9pKC4;-zS9Kiv^s9j1ax@U6>e zr^B}#z8EHqly$`eiuc3Q7I%Vbz=BP761O6}8TORI;yjI$*WHq`5tJvU%q|XH7MU_T zSK*Osp0#Vy!DoW*O^z&$@2+P&ZynMDR3Co}hI8KWCx9w9Z%#>gm(`MN6y5+-Z?AdL z_T-PC?6s7yv2hQm{KkzhnyY<1empd`evQgXHd{|$4xs{03idW>ReH=TcCI!8#s0e0 z4vh zF?ckX?Jxs83cD$&YVCW^;@e_;8m2)4qW1FG4NePrju^&rEwwCpkzO|;pdy5TlZ z-89lA=n1OYo2ih-f8-PE{gT`nx#OngMLvz%bRT?boBbjvf87fn0bUC#{dBN3I0S47 zb^}v2&Re<&9;gIgP=NgRI;e5Fk%H@jo$*XK{&(A!-t=W8at!!D$LB9TtmCxRz{Z+~Mea2cp)Fl7&*$i*o{kc)DHz0I1H#*YTYWUpf1rC4QK#4Q{f zbR@5MMp1r2Ze;QIcAOOCPRdsZ|B5&qM*72RJ?I6h`cJ#`SN`3`UxVEUzT-YS+;75` ze=p20$(uL}$Lu2@1>y@(70^xPX`d~u{AB8oIf=`5{WGO)ppEiP@eb&)X2I2pvA=B zEEeTW%9}&{Tj5pl81aN9U@ADsrR(VUV?Y($AmW7{m^VAWWPB;7%}*fI(nX$UcT-RW zZ-ZA0Hh}8DyIs5sC+0ICBazP&y)eTrNnSYg?!Z=8_)(kwS9le;1YRrNU%@)yG*Bby z^;%w}=iw+%pa^dVRq)Q*w!mRT)X?k?YR)Wm6UiN4tpZiuhw9o? zFJde8W1y;a%VDfp(9&Vpz6SM@?;&(yC`>mxH zE&(5dEwK+AX$yK8Tj|GdRKbsfZNU3L)%bc)6`be7^Fb{~=Yn!|%l0-O4-8jZcrBLa zb@0M{sR~rQeka>>=Hya4n*Xal^~snMv@YD6X$yJ-RKjh? z1Xmq-Urxq)?3G(YsbYE*mD0**qWL%u$6ufw#s>lbmU)JR*+jXbuNWJW{I*Dpd1u0 zco<=Fz$Nf<*pwN0lem+P{65rXG{fvaK763&&3_CiQ6}t-HH{YR1ikt5%Ca z)pFJ-8$La+q#(C=YUCMgb=Rq*ZFl7rs~?IZ_hT!4{5VoddRkptHYu+peO6I^Bz_$K zB*!K^pNupu;!hrC6ctY5_ZY>MuA=kD*n;Qg&YGuwjz5$85nkhj=j-A4h(ClLkK5#y z^BoQV57RMXOP&`wk$_FPwqr+*vlG0|f{^L)~HJD=Yqz4{}xOHtYqK@-2hG@DU^v!BJ*1b%;l4S&qxbq=RE z?CbEzczB`BXFmlh-?zYH!TTN7nQ528tFTM80K5UADccu9D^44S+ly=;|At4zzYl7y ze%xU>s1@n{5?kP%pj>~_EIS5fOiC}AJ0q|7U2IJ)ccT^Af~_(1#4I)eDfdIDZ%B|o zy(BX-$7S3T)S7!NDA(5mwdNkch3b;qK~?+#PzA*3Qw_e9d3LaMcJ@u!;wKcAq%)3Y zT<^jsT*N-98fRZ*BYXy`LV43?&dM*!D=NG=NI0f_!l&~e>2%D*CXd{jxxRkF-o#*J zzh2%6L3zKJcVcCjSzS_aafxWygLHWQ~^kQbou%KdKR`d3B&$%umTIjLttJp|RJC?TwYDeJ($4CU z#FV$uO3}^?6TDchr7)o_cV4x&0`2rL-bdBivGmpHLE4}mNui=r9|*Oab9w~XLt@eG z%&*)aXGm7G8{IrA)NVk_4z)dK!$PeO3#<*TK(nDgq74m0`!iH>LhY_lOB>oFiBU8@ zq$Z--B3`Q2+Hyd!2`)#Q6vq1+Eicptv$~H7wFj%U`pi$8Vj7yQzzb+L-ZAV0mYa_@ zTxq?(1*s!qUi~1OpSJ|%{5&nF91-&a*1EG87m2~n5gFcxL3Vb`YaNtl$Nb5yBat%* zNeWgzkeP&WYEUsI%l}4NIFNQ`XGGgG4?71{BeJ}4LFLGpcSDdmD&}npvPZ@IBbc-8 z?U3uA;q?zH37H?Hj*fXR1=*ux{sF?X2(J_D9GT&D4k`(`B1p}N`7bl;``VNY)_OrX zTt8NfOt?tUW>kh(7NlMfi#`U|IjG3aO8D}aVEF~TqAgf&GJ~`MS^f}dM5p%I8U9ty zN(>Xf9+Z!Xc};>!ex4nqj*WQ>g6y%e=sH%dPRcI28|~zMUPSlA`R8JF4+~s5hGt-?QC_eTk3JhzPKf!3 zpA_#8CbmBeOR1?w|BPsWg*ygjd0T_ZyqJG$Fr(bj8D6`fd}_=q2r8$>{P#Q8aGcjP$etGSMg-;4V*c7LHl%jY zW+461C1_F5%R4E^E{ORT(7e;hj$fnE`rEOHOHsK4GyLsXwkL9DWF}|CM+Kb}?Tghl zNE?ym&y!|rM+OgKx$c^h;q3~t3uAtB2xa31J8Almpc3u|IHj`gdl^e3j!vGE;s5F^ zhJ8VX-=4vz&Wb3tUxY<)Sv1I9%Prq{2dT>-l&Aspb~C$&q!nh@oEL?t#^a$Suwv4qgAtm0XHxsdJWbY zLC)wb|4p>s@P~`{8^>(v%;u>X(Ne7Q!`b;ZnudFFIPe-%LDi#{*_nk0oE4P^{H0j( z73&fW(mmG#AQ;>XBJIrMemhz;mDH+jaSbc)3ye$7yH04geT#(_n#?|sbG-xw| zY3i)xAa`trUyjv}Fx#Iyv6Q>@+!3cSz-5`-F4Pf=s!%ekufG_pIvxJ0oEP(((A_F0 zDU8t)Sp=5` zmKqug&e>i-y)+7|95 z{y`Upzs6)l2lfv~AREZ#Xx8^DKg&#_@6Qgqav~a@!l5c=6_y->#U$8;rDoeIv>q5A zii$ZF%XXJGu$zPI1u_3CI2vg?AlmD!uuT+QhGqwW2HTrhowU~YNrP;aY=?ElQb&h_ zBU*&T)FJhKXli<`u!bLF{ZXeDc!Bm(A|2yT#ZnGQ;o!I%%eqA?`e!aITz>ox=hz8t z2kFIF)dMm57#7P4o7s`)A@>X}) zAwC^TOLzUStE#Z@k{dqh*><22lc80JrCe;Mu67oCHM>haeAk&U9F5fnOO9qpvi)%Y zSrqf%gwuw>+%Cw79z7}&iG{1zI5Z7kR(TFd4`DHtre-DlG%6T*ZLjDFxU*|e@ner< z6qQh0E24h`mfFf-U^cyo#geY$5ms`JttVj&`m?Zlg!yX(?j19B1bl5*w zi=4$WTArEAXi^Ppg)8hxEKLtqes+lagY1BVPHz0*p?=84>O~CpGLC$=1gST~{CDBh zE-Fuqx{OF!wZel@KP)vf+<~H(V090wMrB3UqxB1?Ys2w&XQ8SyGm|mYAz?R1S7C8( zWR-g#jf5=kO((=>Vw)+MLFJ7x|4ta|WSy3F2H7SS9hS#X3gh32*4I{GVn%egESRS2_V<{H-aN_oZ@)a?E;1tWT zbm!R_2n1A;Sn=Pv<9xW%vWJ z)KhkxFTm1_2sgIq1}sJoJHj5cGvL{rsaUTfyH&GqP_gM)cJM0fHZ1vvg(6^1Im_nW zwb$4CGV1nVsU{rmF3e26uzITFf|*zvX5o(QZ3xQmig_79B|j^I)VpK;zS%Y@ z2V#y29p_Mm@Jv}4R1$g*Hd{Lt&t(z+0ZYwbevZdxbL z*8O<*{qbt2TBU;geadVo@oE3;+wRG(al(0gQ^F5B!x=a%~^iu>*9sTeN%$eCu3eT$mZwSK{-EH z29;07{2#BkUFnXEfgNactozY%5xC6O&O_2Q*kHoOg3fJTg zOXIV)&7zEG0oM82alPk)?5AVi5kdLWF@OAx_I|+5`lqn^kTVu{E0G|3Q_Mft*zSSj zJa=hO3HKeGCQrR^D;v6ur~g>t(DiS`>V_2#3jbp)4f1-L?$NI08eBYlicUxC6;$bv zj^+jy!|Ver4Q88Hi{*B;OVU(}4v`gZUqov(Gn-D4A0kD%(tEdN(DIWFY=tlMi^^RUjT4%>pI8EvG>KZ(;*e3S9MT1 zmKFvoK{r>%!%Ul=91_;pn4E<fd(7|kh;0QObWes?5~NmgID*sK6As&iUmppUSN8HV*4k=>q<=A%x`ngb zOPR?S=V92-;Q;i*jOD#E-@@O&L7V3OR=m6RQIQ_vcg^=+ zj}f}I+EUH-R$I!Y{nPQ7N;ehDmY|V+S4}JGZ6YklS=%ESMeb$b(0S!pBf$AG_=bj2nYlc1GR$!fl71qz+jir+9h)H|Vrn3*93b0Nkj9QWBdaPd6xg@+~!`Q^O zWhP^2Vp9NT#*2f>-7$XyoHDkLUiM&VndLr|lV-=D^2?Zi>t@?k><*hU629FWjQpyX z$FCH=iY3h5672Y@SHhzqME7pdZvfeZqp?@?8!u@$qVd~4wD-^kg<9Kfk;vdsn~TP8 z#=h>6gpw0d>95A4%|ja%a&M#A>`r>kh5DDGsV&^M-t8u z*7of~BVIQE~k2WSqLreKw zBT3o}wER%3M9U4e?q7s`Md(sA8*e|FO*?#7Jm?q;bB+c*GC%N=#+l-6Kr zxfhFW`jRr1R(M%{zpuz83}qonxce(J((`&n>wc}Xu71IAH=6xb!(aDAB3Fd>H7Vaj zA{U0Cd(rHD)xd8fk$EBa0@`%N^G-3@i4?ZvyGW!cOqjTrgK|*Ct!o#wlPT6dg380v zLcrR#FT-E$EY_Q;naLP~31U6qh9cXfM!lH-!uQs{cD4EiOARDSenvvqAA%M|y%OgA z5G=pASHgoon31)-UjBg}>^jA&#iO2#zt_mrUxL-0^gI%{nTPdQXJOHIJaoz0XIIeh ze#W1TWy9LskP#jKBcodn0Hd#<@hcS0HuZk8E#Yy*Rs62cR3_tqg^+`Ss>ibue*DRd z^r_CY{dQt-E@jT#ij_%`;e*rYb}TaMpXIeM*>${FLdgNMypGo^;ot$Y1B?#(IlNzc zv_~?^AkvWww|Bd+_(dAOJ#B9)>*A%NU#jcAAR}SjFJ|Ony9!Hv815(0SFw78xku~#&RDYQ zXw+U-U5@G^yo1`^)}&3u3pb_knWn!F%Pk))80|dIZBZPXqIY41H?z@5g2!yuE#U~X z^TOZm+=q5<=oxMCzhcQxDdFA7+C(p`cKA>^`U6%U`}#nV7ybssKCxMgr5SH;BilqN z1Is?lnv2yZv{stTWQ?<`Bl@+x_>QaJ!D3yYeLngb8aIz<8MU3}FGN$O;k|lv9~O^w zcz}C)vKO8l!j03v0ZZ#7TMy0NhQ+-(=ipRd@zMNzwDW8}+~aP<;>L-`+k4S^!80#- zG}ggXHuYj&zDaGykUX3huyG&nJrtrb+?bP_(bR)DhIw$P<1JEf|*{z{q{!gHf-w zDTj=9Xy8S%O+^dkgF4)*AEFMn>d6i9id9Qd2U+!%P_1g=^+;;uMFv`O0&0J&u0cIL zRQ>N!c^b949YP(>Eh~>}n>Vooh~F3$W+r3ks6-IYn4iGX zs7wyp+?U~hi>0}ns=IIRc$3-+Cr>unKy=OF9s@g#HYI#0F9AEe?5q%naY4=m-#W( zv^a_qE^6V0zmVfj?N$~AtT3J5^$6PtR;H~yKE~1n31j)MV`&62*tci+$F!^&R)TfD zjmdfMblwivXt(JCER7gz?Zr|q);gW{#Ycx0k5;2Au{c0)&GH+yqWgn)CHF_o1={DLmZR4KD$NrmG ze|VvLJ1;Uh-oEHb@UdjKir;{UbFKGur$@en9&EYSc#9ti@n-=JO-r* z|28!BFHbV*-~Cv&*HqB#4qo`ujxFd%tm>Zgvro3Gi%oYoma2r~W@jd2gcIDfIh@A| z9c_*5o%Jj%H8{cKUcj*rt0){6T{_ihVS?!tPM7HSusODu&N)T%KO6!#qvo2N&R!3W z7|S~{Wjc5EB4bR&aV!I9ZlrvQT4+0LY8NlO6=ym#_n*QVX3|a}cGq~6=%uJ*Z2mh@ z3v5DOhKcBICu&x9qqvF_CAsN+pVTe9osMT%{ByYBJo`g2eg7GEoAeGU>uNiS>gh%DZTJuzh$5Jyk;@*&%;V5H#QpbUW6qVhjZRfJqGs=cj_YUh$Kp^k7V84S*ji>~B+MURmY+(>M+cZ4pzjT= zPRVU&H>@*F)v27=enRa@vJ|z;iSh&z{4`flM$fyL%a4A5$rI#kn>ioA%a>#KwcYU`1|E zKYy?-lDNlu8U7=fs#f?5spxkK3{N!Ohu9-m*kRr@Q+_%*J_#B2ZTLVVX(){&X~gWT zLr$|W-GF0gK7pkbmL3|J5%tgW^ephMEI)>(RfiJ}?^#}So*8+j*UM{W%FkpIo^C3E zgr|p@7X9!;{qw!>DcE7*P#A)xp0v*`mtrY@7Wx%z=~!C!?XPoA8cu(O=d6inT|-{K zLc7UX?7rMl?80(=%-N~+2s@&}6OMlYmMx1TLUe^J9n+&*(KxA@9^MEuav-Z%@9gkQ z!Xv_sXc|!59&vQ5ZBox-TR3MVV=>(M7o%A}Gmw)ptaIADmYF=t`prH~U4W(0i^Xa5 z6|AmdeJ(?P52kJLiZb@e*oseS>E)mnF2mOBm zZEk!G;Y_EK=}?B`-}Ai4jIee)xuKdc5ibW77iT4`oM?6orGq+7qSdBqC^K*_swNWm zUXwDSpJ4GC9!o*iWEN|9>Z3t|WyiQ42!D#DZX=BPw8^(lV`B39%WzXUj2gTIInCC9 zUuAFueLh2Y#T2vre3pe@rkEX|KVYgoLNJ{k&G7HRniwYGJn9{3%7-&nMo)t@Ip=fq zteR$8j9^-%o9q#k5EQUSgwb<1vEO5biA|fK^uPtv|G|9zAI#_ph03&Q7@6K&Xm(`d zk@rJNN&(=`&*t7{R(TKNn=bUoH^ID&@d~T%Ti^j zSLd4LxlqmK(Lhs?i!;tc)omSpU4IL_(c9AwH&Flg+ZEE<3JD9t% zltb7Q{|7AFOf@>=;<$UXQ_sUvOm0_r=)4}w-ioqJChWb~ET2Htj-1a8p5CnT$DnCC zhfjACmd`gM^XTr*mvE@D>_?eN7~zXF30p2PEhc*1OR1Ks7GA=m5Ih)O!bZq+YyJZ! zz%f_DOXwjpkwl~f(e-E80{+@CG{n+StwH>Mwr%+}YxEyipgAe;3RC|N(3Sr`PxwDA z=)Z`6dd(UiL8NC8A9>j&_;=7V1(VtKM_=PL$gV}YH(gqx#sg2k!|TsbWqshn{}p;> zkIHzTGDR| zh6X(k7sCvH0d@TurobmFvO}r9vxTa7J!k*-*jU;8V*<~lOz~Qov!-~-rSf=Vmsw2| z)r23y!(DhyE=F~eC=$ZM`XxKH=yyWmlI5wUoqDJ!bEc8v4g(_f&v;PbgUPZW~ zeBj~W{7^2s>xGa1fkSv4yQX_sGJKO&Tza?YLT;r%5M%Rmt5rV5*J<*l^+|M z`DBLIFdX@px=2DLz8v&S+DxwlXFxve=z@iZVH*X5%RsgE7EtcL1JosCIhg52!QGA) zN?q;nUdPu&QEMDu6SY)66)7GTiPG3T0P1+ySb9Di{7|s6))>E?g*em%}eX z<@c3HVUM^{167e9uvO5H4iC8Sn#l5Kv^*MyD?b{PYGpf2sT@kJ&kvQ@!0|%m)d;K) zwsHKwLJgs|#8cE09kzGrYG7$N#!hhoLKV~nl#9DLI}6lUJQdVc6UCnnub_S|{9mCO z7VeKBnkgk-9i|O$+_x>TIFZtN5Y(u5o-#)PTP}QOEXB`M;5q z%`C53s?skdy~-^ImDNrBV7Dx1K~S~t5a&?Y>f3j^gsS$vU=#3ZXKwrWSG{a1sC2ZR~DNkqkc+a>sSQ0=TFz6yNLrTec@M?DvjyNnCH zRvl$lx%3}`M`9mz_V1t?8amS<({hE^jOX=Km9o`7Uv>B&qOz?+I$>RxK15Tn!t2mc z?rsE41tN#LXic4+>Y@vkyoGGDjar4h+Zt2OI0{rY$1B*OqPKP7CxEg~bm2nTC%N$U zju(pW;B2At@96kWjt5(W-aFS3HPJI0=Xk9`uMKg*{|c4=c`n|+gB*Qs;+WW^!fV@5 zX-B%WLeJz>;2L>ytfPgBHqP1q3bi`syZBQ<O`XzOO@^7?4F=ndYZ%jpej7jg%1T4f4JkbL3v`N!%?6- zbOER2dGP^6LsV+7jQSI zmOlWh;DG)GY#qZ;=FQ|M^ zcX$RUkMsxSfwOh5En={XFa%Tv=Q%tdRK!uv&H+_{JZDb=m41q|r#YMss-;EFE&&y9 zHmF&>093_FFLlagpc4EQR0S4-{EIB+hYB>Ho+;i5D&buY?*{eAWG$!y);WGXs7sgu ze(S=&ceoEEpVG*WjyM3ymHMF;M?t7aT~HOP531k>pe9c$r~+F$Yz3;~ZJd1!sB~=| zp5(&YgUY7^7@oU3g$gS)s07`d-NRuFRK~p>o(3v|zRo@aR7LuOs_;1=|03t}BkVd* z`Hm6kQ2Ye!M9sfRF5)y7L8uH19TtJg;6hNYzRdAgIQ}Y-e~~5p&|X*J>^s2YuwMq1 z-!{kp4O9c(2E)f;@1UrUKX4H~1C_8Yq*~MvRNuD(74LX{Xb^P-)sn8D;`IPk(NjTP zLh)yS@?by5*TB*+!r26T zUG09Mvc=^gWQ0Us2bJV+pvrvPg$vc~?>YW`P<8$gR9$vD{$o&=Q0YGbMSaB&#s3CW z{O^+8Hh~geHBr=l7x8BoE>s2wK~>Pxdht)F0u$g>KoY3pNR0yMeQ7;?eM@z^fu%T)0pk?g1)Y&r%fC@KhH;s0#FT_Mf3LJcDpy zKNtTj7hfp;Y-bCV-w;p*4t4f$7hWnw6&UF-$3@5mm685`KV>ip)J&ZQDx(6&&jgje z#MyH|rJLvMOF+esH}q0$jiD<+(V?OOu5k&ja|xD!a@8_tmxC(kCQz496hTU~-z9By+7UUT>cs7t7fb^WJrI)rzZ{|m1=gq8RY-gL0@?>|WRpBDH} z>D>IUS%LrPRfjO!|G}#f@%XO%$X#*be*ym|Z#tyB>Tu{)2c2cO4!!CSy5Z2P4&kZy(5nu5<>An) z4!UtU^r{0+&?^r*Q5|~KAzUW^;Z+FTULShZ;n1rNhhBAHA{=_vAv{1Fdex!69(!{A zucO`sIP|K6Hl0JSIvje{!5+&Fz3Oo2Rfj{bI*j5saatD-z3Nco6$kCXdiCMZs}7+j z4!!Dt8xFndaOhQsL$5mg>8lQWE#*Id)nQ%b8tG*h@Jt&3bQbPxH5p)Bcj4|y8$Be28fCqAZ{_cMeGtWXeq=> zQ@#{p*;0suB5pVRZ-nS~BgC2;A?`E>MC=!lV<7G}s|~~|1Cg=}VztR$1~Fn8#6}VK z8owMOxg4UP9Ab^xAY#3UwB-;FnEd4slb1tm6Y-EqT>;U21;qRn5RaHGA~uUizX{?| zGxsKlIX6L6iCAaaRY0_@fLK%kvB6Y|*e;^y%@9wRg*QW7c{9Ww5gSdnTOcxTfvC6z z;%T!R!rNqe-wHfq$^o(C9_(v*?6}DTTC`! zM%+%)jkl9@tMTuENWKH2;0}mwW&?!xs%dy9@S4dNyl$QmykS!BB69P)h&=x;BEMy} zKzM&MZSDr%Hgg5X0(!nP6P52>ZreLVq z0+`Kj5}f`P!Nbhlw;<-c1yLnpxM}w{h_-)&SoAlDY*Q&>yNI4|LyR&D--fvIZHPT0 za!j{(ATr;9sCWlrjM*(>mxw{{LgbqAcOjO&3vp1yc+-D7M8EA2Yqmq=nFAvBi^!>j zm}FL0LaeHUNZA3AZ?bnljMxFOQN&c^zXy^09z?-=5Cvv~i1i}U-iIhO`R_waejj3+ zh?yp}3Zi)x#QZ9VVzWiWW)bNhK+H08KY*C?0YsIE*{0ox5N$t%So9&pTvI7xyNI42 zL0n`OegtvlM-Y2N%s1V3LS*iQsMrZnYIcj*C1TLW5DQHC#}LathBzqVGSmMPh<=|y ztoa1ua&th$ei1pJLj2XN{uE->rw}QhK`b=cpFxcH3}T~*tBwCTMDphl1)oDKG8;s! z7m@Y_#A1{G1;pepAhwBEVp4ZOG~Wd=e-}hxwusm)B7HZ+QZsiq#GKs_RU(XO_a#Kz zFCi9v2~ln;MQj(*^DBrIX5m*5SAGStM?{6`_BBN2*ANw7L)>C^i`XS%&>o1DrhE^? zvON$7Mci)se*@9)8;CXEK-_5#h}bV8=Ua%o&FXIjUi0>dain!PK zdm)ndLKN(USYtMbST7>&dx!^2{`U}*zlYc+;vtjz14Q#5Am;x7@rcSu_Q zUm&)a>|Y>8`~tC2#8$&+zVXMe5Cy+NY%?1~tQV1X5aKnHe-L8wL5OW4-Y}`ZK{Wpj zV*YOsZ<#G3Hj7CA9pY^>_jianze7}scz0>L#NkWZCZ;T1lsM3JrP%UnD3c| zULvmWAohr;GTjm&G7}&w5+FV_yG860F(?sYrzuZ_Se6KJP{b#ue^TOb?^Cl<@R>OP znEgov=R^tq!mN%$tcpUU)PmS;vTH$%s0Fc6#8<|z4Ut?MqM$a!9K5Z{{o zWQfVh5ZgrTHK{&Cb01>95AlQ9B4V?M^g0mx%-lKyDy_J}xWx}`v5ra)ArBy!kDG+(A9a@Y_vs2))~Q(liK%j!WK z6p?89*N5m=A7V{?h^RRrV!w!-1`xH)>IM+28bG8pgz!yvLx>R#AvTJrYy3tK$&DZi z8bPF(4I=%)91jO-X^$`%Oj(|vM3DM4Ew}cqc z5@Mr>6OEq+k(>rmkOq-%Hi%d+BJD_s4krIdh{;DnY!lIuubM+NZv`>G6~rlKi-^r4 z(py7xF>_l(%xMi#B_hMLYXi}?4aA~05Zz3ri0vYJ9tDwQ79It0j)urQ z8lvK8h?v29mF{%pIL7vw}aRw zVz5a)0iyW{5c5xf7;3hN*eoLbM2KN#?uig{PK2luG2FB}38L*u5Q|QN$TpQCwu|VQ z4l&9sOozBK9b%7&9Mi2mL}q)4iuMp=%x)38L=5Txk!#93KrHJ3aZto~)Bj|MekVh$ zIT<3)91yWzL{3MDNoI9Nh*cdSQaVB8o9s>yBRWBB6fxEKr$8j10#R@ZM1k2LV!ep8 z&Jcwrzca+-&Jf!~%rvQ8Aewi9nBN7W*lZE8SwwnQh*@TCSBN=XA*w{oHtjMJFJs8B z$w)jtbvD1$r(7ELG9R_m6;5>HD= zSl7YaoSArud2oDU(z+3S67Tb(SMuG=!`4yWR4+_YoL6vRUJ;yGaC&0n=pa7aTK|t= zugpBtFL6w~q~vIwzWfD>syj5Q*QQ=JZ=9KUTJ)nccp<&I5r==-k1yj<|MAoE+fOOB ze_|45^lhE>XN7;kQugdw>+0nv4oC>=aL>8cFNdvbcox-84eRLIKYi|u0{Q8m>TTXX zCows?V!SK&hp>hpotv2KwO*H!pO_J5oHBi){WRwFg{t*K8Hq=vT+LsY@CS@#1`Pc^ zDXhZu!Wm)IQ`T)8nmE++($}>amY9{060h%L{B=a=wj;dIo94<9i7gYJzD)j|q8!qy z9f=2dIp212apCkXR6f^q%uakH`qGvBS*hG~ky13Bca^P5O*iei+FUX+@$5P)t`5Hm zQPyLTN;p4k-pQkA@;d%Lq>=jnOzXS1jP>1X>z3psKH#PNdJ7*Vq4=h^@;McCWbLq@ zjS_R5$Q_$_OY~>{#3Lni$GXYKD)@bExWZSv2V2S2eMT$SNrCpeu=M`vayMC z{E2gB4>KZubj$WAfezX#{u30cL@l0ETup0mB$9-X@GiD|6b&lVDx0gbWC4xq_ z`&do2tvP#&B5)n&;^8?dcy)1HeKWHlsd1^^%;@WaGF*`UG_}9uG99N6J~83L ztDEB*V-0d#cR00AA3Pf2xIQl4;n?9fYrxYS*A#uVK7FaHuOpkGH<3rUPIsLCaH_fE z&TyRmCN|Y^XF5(_$!+1devZ@MEVXuAe>nA4OXO+Sg9GE=b4i2TMb{^pbd7LaJ9LfdQjMW(N1lM*#6=wG zxD(-iQO;bW;G}Ap{pPqF7cU*|TR43!O_fq`dt|SRH`c}L0QbG)a`l&z(oaU>-&Gy& zB6fu1tCZn2!Ev3inmaDfahl0#j+^MX&TxFeF}x4_Z3wRd7q1)ESuX8#$7$l7=i(K*v{`V&95>U&({C+Ch*SR;Ig;Hb z{4_2QDRzl9Ywr@lHOs||p|3_Xt@LRm<#Q^+w@@RRRs|Gip zQ?h^1e;UTAkgdRZE@EHDwFWP8-0A3x-N4kRmsIc>NPEZ4cifq9MFeVqUgEfZ=$qWw zihqWwzuF)FHfouRIDm-yo}UKP0>=$R*9-0%PjP^P1a>tzw z_qGynUE#QM(AS0>{{O2Z&xKs-BI-L%s_bAS{cElZNoDfV~fS9|IbI3LTW%==OPYAUnu`_EpeQd)!z|~lj|L)Wi>p$fq~;j zqWfBnxNdOVD0FpkA8@JT^sVyJ8(rVs=*S#MeQ-{faoh#yTOGH|abw^4N-C7#C$&uqBH#m1yIBo*m&5pa-ad~jV9e0c4Cc>TOxLX}J z39ggl^cgPIYcleKn-RB#9RAOTjDL<)-|4D_Jq7ua6l&#amv}0=K9H&k==)r9$TUQs z6VSEB#VbHBBA)WS-*MB?iyilX;|k$Q%)XMO`kgqyMrOOM{~ z*I9pEH(*v$J+Gtwi2GGUf7HE!$d4mWAo_sBBIH_RF>)QU1i9YKyfEqLDL0{3AU7ko zA}f*GklPWRWb{3Xt7YSYI}sgZ^omM4Ee_Vu%hkI>bDVJb^rkY($=_Wq!Lb z>DhKCpzA9>^^p2V1Ee9MF9&^x>_xsuelUB>l3JPMIZ3ULtfHwOAUX)WiM);Ib7*q? zkVJFAoTM|_EkwTx(bUvb)HKty(k#-f(JaxdxWPO#C#i9ohMIf_H{lIydRWQmqG?|C;Zb z%9^^Gs+yYmtV1@U>l4_VfFk!G_abYM`w{(`do!Xh0c=8^L7qkQ zr2u^iq9M`AN?>L49DS9dc}KPDG7R^l>A7m#8OlB%*UuYeZjpYHX@zCp9ZgMQwo`fwV;Q z<*45g8XrkO^o1#X#%mR_8o39#7r75vgWQkkyI-1y3&2Z}%aFeymm_*bSsyT)gccerCYvG{=aAKTI^s=OZ(iqWU?{K6k(h@lmX@#^#^k-E1 zEUpfJzal#DC1Te?l94({T|^)0{TkVWe1m+K5NQ^k`gF?skVbxhe1TpPF?U^(G%@W% z^iPq`5Pd25J>-2v-y}cWv@2yw9ff){avahYX^(V3PDVN+oslj`29k;BxYh$X72$vN zikyb%xOO_yA2}D%u}sIX;fRi2*~mzw0n!j@g#1KR_iIx+fbui43;7bc3%Li`iabcf zhYje;F5S{IkkSJ0Msf{EfK2pc*EK6!!Zy!!c z!2A~cE@Ga#IH_r=cCQ_jt-nKCgj|a(My^AaAXgz*BlqDrooh=G9jwP9xyWdwBhn0M zjwB#HvWYa$AkQMtAL#F=G#!hZZEAGAspKC(QSe{gm~0M4y@7jqHPa z9efkf3GsR41>{BKVdN1+r^H3bVq^)T17dg6?b4(pQjS8^`R)XyuF1VLsadT$sLw{s zC6^{0S$ZkC>jZZJG6oroj7D;h#>lUDC<1y&0-{q~Eu=Q`4M95*edI-7OxIUxRv^iU zPIh&W&*=SK$Zq6I(Z)h>nb%kRy?XNF$^%(geAQ%&$WxBIA)fqzO_NNkNj4 z4TwG%RDs-#+=ASS9EOzYt5Z5_{)p^B^f{}K5Pk0IU1U3=PhpKlEAzvdu>A{LV6R&U8--X$V6lkG8y?i zO8L%YRnk?Hvx1Og&qU(s=Jy(MbH&{qZXx5 zD1TDk2G;|?gcr>|5Xc!+3RDSc9})Kjlmg;K(|ZuFg?a7!7IYQ#7w9x-HmDHDn?G>s zfr}JKcN7`VfFeP>%!~z%2aN>v0DT4K8|XHO@jDRr5CXHX)fJiz0RurlBke4Nx`S{O z_^Sf(H4Byq+6h{q!^$Wg7mGnHK)-;3Kz<-DWFZ(#1LXzTf$~68T-baR@)rs?2jT@- z8t4{?3)l@>0cs1X2?_v7$Tv_&UnlCFEAS7Uv`1VAP)87d*DV?$|F#V0-;R(gw-dy> zbwhdOkH7tigw>R16COKxfyH|-JmdNR8SZmsL8+kIpl!f&Z{sm&9H;}RHmD+~0w@S{ z8)fYQz1Gs7ll0CWJeDj3Eda5P+63Ysh1dv6B<(Ny0#0jinGCUJBIKcFI%w(+tZ&-p zbDx51Wmj7ccsSy8u-~;%?}Kxdf;}PRBIbkQKs;2<1XNg znc^upUSCKzTSs~0F@UQOhN+}NS$H)&UuV2mBwyRbh%w?N6Dy&yD<{6iFf64rCxo%bP%2XZ$5Q=pTeGoTZojB$)%;xzRdwkET_ zfH+>Y=K`I>bq|C*eLRorfm&Ldx6KM}sd%~s^+8RSb`A4kYOXFb9@2sa>9aSA4fJY~L1jTh@(B=%F! zsV^?WcoKaKa9&U{n`gLZ7sf1}B7CHz^8}pPtE`zVr}H?`J&&s$ySL<0X1SmCuOEDl5Y{JF`y51q)>h zz47uw3(|mauygGay=O50A`koKQ@}8emmXYE*3VD4mOvcO%My-bItR*)dtRQ{QJ$q( zxjEpHSunAQdElN`JZ>NZh*v}IARY=hj#ow;=K^vC@hXY0d8Nd}JUE@PYRK0d&mpg* zGVqL%6WEl?BOwqJ0J4CJfr^5>K!riP{PG0xu)xgx5&D79Cp10?%Yw>)N`i`mn5VZA z^1PkznM&iDhZk{;(NIk=A<`FAP9dH>a~c<>YKUiK)j&L(=9x8*Luwk&vNK=vEG`(t z4O;N7w??9K;Jy{B&so)`Rtws1cB{6|P%?+JRbw z+JM@ER2RuvM}UTdhJpHldVxBE!a-aN)6cl(w7wut=d?~BPV0j(LW%3nU;XWZn{J@a zAO`R~hh3GBuhn=J(;I2bxCclrn8ThR6`wJl<1*q=u#5}mul{l(OB@9n1R4n9%>6-; zAWm1A4N%fD#;Iw8k*4O&n67Goap3|6F zG)NVKwWb!pT4mm>6;*SwxaNFZE>}8+YpgO++i?`)G76;%!y#uL4dTq4musClKi7=& ztA(;~V?dmLEW-VuB+xb+{E#b4v>CJsv=Oucv>vn$v=+1m#I^kuv>db=q#EH0T(1Nz z15E+3Pzj)=AOiKF)D`ehGjPfFI32_-z;?;9vmLVR9YHK3x47IY2&aLjf_?!_0&#sN zfYgQ>k87?EnxmhzIgZOf7$YaLe3O+(R+?HUXH*rb3ZcgFJvW9b0LN#}t75q# zEP%>a6?lkh;|ySioPooPt&>$8cSIEvhwFJDH9u>K^DxcN5WgV9^+M&kTTBKpn@_xQ z&)18T>%~gALLT>*=Wae6{l5#KQ!c-u)m1v0ikEQ*e=z6L#s#?fsM|{n;v<l{sGG-TUxTe2@va0F-Wi7-AHK6eSNiC?h zl)2eBUbTZgxZcg}FATxj*8de-V57@yE37?kU-q0_Fk?D`!a;1ntn!S4as1m7h|Jh# z+)ip69mVxw&>_%4n)RE$P|Oir-U3|*od6vN@uCa?Z|ceWUMF#L8gw3X7L)?|6Lbc2 z4aD)uAWq}>%yCx{HwnBLcNugE^b77U+Te7~U%0sdItO9^CvbQL1i<%rX2KXQh|{?+ zUr2ZZ;Y|>YUaJqJ!jtv3F{!w{2f7R5rw&KJG70nv_x*9tPa|wmM?O)(r!Pu_b|Rg3 zDtH^^3veGnA3*Ox??7)suR%{iPe6}BX`lz7N1%rwR{JaVKre9fOo`y@=NYaU^f&GS z)Vx&UIj%kgc!T>kpv$;Fj(aYU?>~cBc-}nbbt7ZH;r=Tq9n=n_MLYt;!vY!LiHIVg zJe&Yx4QvJR?sH2NkOQF}B!SxFp09I)egg4x1$Oov$Iln|nlZU?ZwJbyguK_oWf^ea zAJ>Hu7T_)K{Ghxb6=09+d>{ub8U5TQ3q6;OdBxgsfNocph}?fpg>Rnh&Sh$Czr{(w;=Qf zaoN5ge%8flOk9s~AbwiF3ratPykX1tDi1Ye`T3gVLEacmMRB32{9}TwjH;R9GGxri zM3WVc1Jrq(~=U~#PV%s};?nbgZ91gCL)Xr_4PlTkp{T0>w|UTQ_vdszc9832k( zWaiBg-%Lr%EC8pq1aW~a5b~2qw%oQH2Wkas4N?nIHNmEmQ3$?{;Y77<*v?ci+Z5Xo z3&ruO&8g`b?M}sY2d)>WCx|z1dLUemun)rCpuV6;&>zqY_e=cIzKwdxlWP)%1m0x& z8DUjqx`ViviMYXENFqJhs4q?qoAjT5(sl^&@%6D#{>}QbxH0*deM~!QZPvH5p+C0i z-3@Dz(N3d@4YF&w@?!AfZ#u^K`}z24w$ZC?dN;gmlq(5AG8IeGhvOaJ@k#n3cx&d@ zBz=Uo9lZ*WoPxm8#6H#)eBOu7cwxHd)dPG2eavPbi~OXl5K?---|k=K@9Gn+DNCsR zcCg+^L$>SBqM+J4^nTjYG-8K7%#Z?pEZ4TQf}t;mZapB&Wh(Du(%h$gcl6Fud2D~{ z>FW-?tKkE%`MB)L%Nr&}mdz<+1AGGD6UekvA7OF9#u)=#u2%J2wBDJSnV}VvcsPy| zg|YF`Wu*Iu-lvLUEU3OemAj&Mp?f>^jkHzB^1D7syMhSS|J*LgqYbXVjqM@Umvr@$7I&TP@X^Z zZU#Ot!+?izga2Mu_1$hOU|7%)k{ zzDIbz3PY^`-i79rocXGh-l?5?+M0rc56s&e07n2^KYcPaIT_H+3aCi=_CSV)R2qSy zi-Ieazl5K5%H3sF+z<)}z%oSv%&GX)tJk!JldJ%KXvIYwe-~RQdS&xD)|9QZfqC;; z6ej@6^mh5_^`Tz1tbiMo3V`mlEq&PoyRn6V;rwCjx>`Nk53K6F$O8PT|hb5$$3GJn7rhzwO*l z7Sn6}eJpXXK;wSU)8qsCP(xFc!w$*LJGxPt>*~Md{#PE|Pt%)T0jM2Hxeh|4$-vnI*LUJr z&Aoct;(+t_fzQ-v7Eo0H%5DMx)r-9_Zb|DkPb$4+MrI!~AQzCr_EUPm@`_gz9XC7T zHOuh_^+gP>&~siSP=_gzUX4!Ab=0N-S`IPA=>T}*&8!pz4eJ8OKH{^DX?Dnf(L)`z zRVt%X2l|-0BZWQI_^6Oho6h{Y+fmzGi5Y^B9b?3h*bv*G+!1oUWM4L#Y8-;eO{O6n z%%ydQ^kD%Pl_GNG^_<=&xM^O+pP+iFN{XRK%Ez#Rt>z&GZl5dutRVRuM*sK%gaeA& z-DFDgApa0aI7de0rcQ@Z4G$WDz)%k6C-=yHMe?4zussPlcr3YF5e!AyH=-y$y z*`V_jdU^G>%-RY1Wgi{2rQ{I{#gOTUK7t!>;Sv3xmQG&q-6;9+R;QnBYNlUTq&8#3 zL<*~R)vy^8)B7}YR8rs{htjNMy)$h+st<%ueR34?9i&cA^lnt}n7#+Io^%Xew?6GY zhNMWlsJ)nAlS4Netk&RQD8W?m{C^>BDrN%F*dl$d(gDv5Ae{)a`7kpoYK8 zV*C08VtAo%z#Brrfd{Gh1t;AH^xc_D24HG|i7>ud!2KMTY!PR|TL<{ZE@G?0NW}%W zqtO81^|g4$O#+Vl)|6uX;+t*i`UJQDp}}}M3xH)i0Nlmex;L1zef_=4G5}o!3sn$% zdHb>RuA6d>Ga|*;$KS^vhD{!4FrVl|&k*RInrZK!dYp=&VrTVfqTM61n=+t^vKg^v zDn5doZj^c%fhUzcr;pEW9sx=CkX@#E1bhUynGdC((}$sf>z~K`DVWxs$NVXb&YTC^ zD)bPMwr%}{P4}dR7ogpZxMiiRb$K`az31*2i`OOtopC&>qr4z~XEI(COIvRNencZmf6`1PRQgK)Hu6Q>&{wlhS`KrFb4^d%; z*NW+fE!Z1)*KR7aRi=FH$>tig{Lj7tfYLV_Uqj#Uq2GZ+-7x9%AiETOUcxg~G^n$ZQaK{IYxKlk zMOhwn2y_Wg+LMAtH35(h&2#TY<6}Lad32Bkmb*+-dIbPpR?T%CxE@u7(^(&B+SccJ zmnFa{Zo45>Wx%&;!hJNq@U-agNQc${1Pb?|FtVd*z~PNruyxkMM61&QjBv7N zSTsoI0YxiU%Vl3)7DOIku95u>XfC$8C}8NL6>|%HeeGwJ^cQM>101ph$PO%;W&wat zPrTAeZiejVx@YMYGkP3??G5PUL8IX|Va*rpkgH-Y4@(yuv>ox$4e*Pk+BeZuV`;=q zeNB9};=oPxF?k_Tni`$e>m|0_cpCOqUxsSjLfRTS_Eq1Rj@{B<#EZm>Z*y67M6atk ztLyGzg97y^3qu(9;8;3$8`a)XS5*5_gId2nnUU%bfTH;QR45fvoTL}e_09#+*cSoi zRl(jSJrC)=UYH3ecTn)UO??5iJOzN=#m_Cx9@C;5oCJXVBLE+e!mf2y+>)nK;f}#_ ziY$giyHoWMhU@jke5IPxt@#!9MEBP7tWSlLS zHt+cK+ozeG-pP5;%do{qdG4a}MH&jL{`)rgn&KAe-d|0P?)|{-aiRzpneL*J4G;7uF!xOBa zk@t~neJi2G#Ou4C78z8=9ku1PH8h_@n*qRd;}inpT^zS?0QV_f=WhPv+KF4Tw&gMF zb?dkXP@)aFJy4bCOkEzph3=#H2Pnd&t+0!UKb2e(wqnb#$jVLvF7hs&1wgx;(vVg6 ztsND72u{^^1U}T;Xn!27sNW;~cAoDPPtz}i!#t1%t2jdTk72|Y!bJgpocOC-YJ_i9 zxd6Eix9HYmX!W@Q@NDPaCqBWukRkyFpHE2PSwa1I6Rw}pUSu0l>|amokW~~RI~2hS zGxCop6JIzfi&>aqc}G$7`aL<4TDu?Yi2REB#54_`?qCUYZ#q)k6AajTO6P1XI*Gni zB&A#j?+!&CVh&>wBWqPMJq3FWb>l$frY%ol_H8KDEXk6Hvhk9>_8Blg6oec-pJAsV z%K~J_>rLmLpP*w;wE1Otp46C)El7dzSq zpkYxj0r>D~M2~~5=C0Cd$0LSUvrVb(ODszc;Ff0!U0WYIxWqmI^ZxaS;mKAOV+*!q zxC`6`g8i1s?$wi0wH)C-&mxBBfP$%^I))YYi4DpfT<&)A5Kr8dsUF51l#20)3pcc{ zDsWG`wrPH&(*6)xZ}RM?7u{f7Lwf%b&Gxj9(C^qwEsEcb3dtwyO`f52r|?%`lqJBA zr%^xCoLA@zFZ&8TygCwk<4T_QA=cGM9ZCfUeBJ0XGs>bR6=y9sK!lhgxZ&G2o4GlJ z@fAd$y%8xa{^^I#H^K|#}$QhQT>KNkB`x@`l?B_0ak{D7yT*uHT;Bz z(q6-nm8ZsUFf+bMMc%?C6s5?wP{Wr&qF;AQUsu5+DCRA4V*$!Sh((E^J{CmWvK8XKzm4ijCMA;Q-C(68^% zQ^rurJNU~fh|LfFaSpM;e(e)$qGMsl(#Pzpc|3$1-ovEjdINFq=`Q#Eh11VLL|&3X zkr|g{ZiYd_gzfy5Q1x+e>KEQTQx>8vXx4l5k@u1G=SS#6nZ#ayuXnUlrB~7n;lo9v z_-!vTZVE`lWrxw1V)#zrFrmnvl@Pj=ZKY@-OnK6N(w7F;vY)_pH8uPM1Kv7PG~n{h-|RjQ zzf>P;Q5t4Hjs2ug`af817epJ>iV%)w9 zV}+sZ4%!#eX~6DM!0`YA2l_0QihjZVz&jw=XPPQ4_^LZISEC?Mh8+#~g7(9>H}#9& z)%XoSb|%ub&o%pQ!uK4%_pEQ#Xg?5^rf?&6KzMkjdhNIM0huzEbR6i>9 z6+@LWOYQ$vpA#QG!wfY{Oiq)Od7LuU!spvIV^)U`2c6;otLR|K#ip$9Xu`ugH|=a% zs>OtQ`FJt$@sFN1|L^E6H|0uWJISm$M1?9zPKC;&C*1*volfmLeQK6EutR7DlWKqL zMe^)i_i{Wv*GjIsZ{x{9FF8uq`L*)tvJGb|+}=YNcwtvLqX=R8w1+>T0Em z_&!}mvARzbxhh7jHjL0O7aj&B`erIoSr<@Yo#c*dQw7OQE6e00aUBFxgRX)wl1FPM z+h4krg9lcB;Z?;Jy6$lZ?bS)HBCERCz$fCcdE>0dcDdMlp-MKAQIzPS_t#{qbYa$1 z!Y^X>+TrR;p9Tr;yby;RFe8mxgxn5X1k0)@%xtLVAtZcxcx3-Ymrq|nK6c(P^x@QA zk9L|cS$Mpo#sXg#r`fxzjLn+)Gz$P&!aBz7pCWpe-|Q~K4=hf=Tz@NK*cYFsTL2hu zE4Mpbk6ceq{(29L*z=nMHTal|qa>M?#|j11@4ZCMKWjf~QYbm#b4i0FsmPB8MMouR z9QJ1F=aBsHk-eB4lAC4QOd)GhXvCmON1F0-5aSS6<|tBlfL~K)cvZhvyL57jyis-$KF+cgfs5rJ>86_)57KyY!p_;B_Kw$8hC5nh#F=Arh;thGZq-=OpqlOG< z27m(?rBvBb-Z^^bA_aiCR99M-Q*x4c-5y1Y89NPFcFWUVB-L7(T4kS%mG|6M(m5_P ziJma-7;tXD?H@gF?uT8Y_@R+9oxM$ka-ucg&Z3UFWb-$v#c(fQNx^pDB7>YH?$xpk zn19WtKXStCRU?geN=}&|~#|h)_8UMWH(is)K zWi`rU@DnYH&7rV7DqE$k1%QQJlm}*Dy-nuZ4EFK^Ckn_bxx36>Bx+_!I9_b@ z#k@^)T0W;^OK0;+9_WbU^Gc-+Nx<+l=@>#DBuBVb{M2(|v+j&6B8Enb#gMnC z=)lgoCWT{1v^HWG+J++SFd5wICTuVbZFw?qxU|eab zsJzp+eYH0CxUwCSNG=&qiEGg<04%KlV2gdE$-gVtb8lWp@_G!S^+gKrGR-R$o7}zY z+Z*b_&ZL<@W%Gl#?J`lTlp2Qgyj9AL)@jR&6{yCAIsqUAb2FF~8LFrLQPo^}LkI4v zVxrZ7HZm@XVhVuY_~pX&OwTcXPyQZlA4n!Ri_r5M+T@ zLoTRBEO`J<4z5xIZ3zl@MN~~%YLJYy(N%I$lW)7irA5$ZSFmipLDb>Eid7?{>P*A# znnht5PvzXC@R&&(1;7zy+j80C19C1aYSRnNTDWXsmO56Es_ecbkBHK-*ixO1JdEw? zS9WmEzTGs#0vvYF9-q)$r;S%q{#g?fZ%`4xD#Nz6Gu=J8B&s+RClr+q-j+%Z#-wV>)JlX7d)H#e)sGb5j|{s+-d zMyWKmVEl|yb4!VJqI#))id|V%Zy0ZYik_l*y;Y?AZqv7GS@+u|kfIDjVfeZxOlH6~ zQM=*M#|>+io;@rB9nO3?OZ?# z+f8JHf^&LAOSP;isWgqVzEA+gAGY%GuQUd8H8pFxl4AMf+^>r{H_n)-DY@uOVHl9x zcEP*ss6yKcmpgON3h*ZTg5VthfISM|-dNMTLh-!;RzOW9rCZmx>2G%otzk`RM(vT+ z&_w~*e#X+)}#5G9&`v02x}&=s$5t zKpqTHDxnH130WXolV_I5jhg;A0 z=bB_q(a{KGH8}1NfGY0GCr|2Lw6_&doHhc0kDh9a02d0JBN%;K_sMC0^RCsbxQ>+0 zjG_Q=0-#mV7@Nau&R~y36>>6}&+TLvMsfpUt*kHBh(ewlWe4-V*oX!&bZ2AGJ;E5@{1NHTUz(%@J z6pkvv6OKyW-f+Y!6vLUg#wr%q=?BC%Qa_h+l|Jr#3&UZ1Hz8#zRrUfC0jAzwQd1o^ zi}rh=&KkPv1y51xuuy&AfhpVLYNVcpWXd|L5(T4Vd`_k<2lAb>9<%zJGy@U37l&zW zQOGF-v}`yk`fK$W52E@n|LHdPD4p&dQuryzu#UUx)v4081yXoOMpcujbTKS%vfLlz zm8xd&ar#nBGV8`2C*R_jyxur2ggu_-zdmZy&8IjO!A~Mg7Ce#eQXF%K^0c`)gl$Hr z5Ey%%6z5Pp-CA^R+pxqHl#C?}s;8NFGR|9a4Ok3-5u%k%UvJJ|}a$%geXq&2nmpDTk_KlJw^Hwk~v^HfLKz-J~&=0ar2h#-K`AQA%&lTv@P%Qlqr&A-spCMfL+MlUwQcF*UkHOZ&p$EQ zyLye9m6J+&JpqCpOXU=yI!h&sYmG-USMaGJfowy|%E4HNrqCbEG7i8($WWVZ!a#?zvR{KINLvB+$6Qmk@TR8rnE0*WonLj_aM$vb8 z141+VHq8VgU^x)%_@;fkSLS}zPG%W_{)Lw{kis6%>HNJipUw_TvZnl3xrgeO-=>=; zsk=^5JU*HpZbrk)8n;yB=^g1=NeO@)Own9*@|0tFU}vWa1;RUL*05@ef-P4n}1mWH}8?ZzZ9#Sw9t)D zqhw?dCq$hK!X?f~6B7!@$&Z^ISX}2f(Fm|WQ#YN&RPs?HfAW^XnrEr zpdQJCsn_&9;87AWw%|JYF-;GYt8^d`yu|(>z2qvX8B@uXfrWc-jJS)SbZb zNa4lf!{ayO1~xBJOr-FZso10SDC?W&nQ+p71Zj_*?{@X0n%PfNnl^&FT4ZxN!#kca zl+Kagw|IZpTy!;N&u#2;imiZ_-1wXtRm3=S@wpho>ez02HhTXmeukuMUy71cebJW+ zQbAFkx`+9liB2$e#0IQM)JTP7+!t0iYh_VgZ2UBe66IZIwJOlWzir+2t%RaTb+DP8EBANgKsCn$I1HuwNJ%X zN6*2R@`EvJkge2_Ox2}AP;ukxV55{EqlAtzFAdb}`bY`Y;n0&mik@BhXwDK%a~{L6 zREq0^zXwQhK}9@f)$7+KVGf>#swuxvks2yz7aKDKk?&>hj?}P*R7jWjnflj|LX`=H zQ$dU|MZXB&`*>AC!1|ObF6@V})r3Yh(JSUxm2TBUg~Nd3)zy~qDLV@p+=p6m?dU)V zM(4p_C>oO#!!!W7;rDnQ@U1&;+;}Tc;q+PIG`2z@LYb=|Y}D7^!?^PU^V zpLs~(=~u2N;Y~+W<|VJ1@~ehIYojatuH&1E*Oww4@HoUTH%+>!3A6r40{|fuFjJYd)jL+n-8uzg3n( z3INkZc?#h_W@eF!XAcz53{V;2JiE*&qGHaC(UIeWfK$%E;M@FA?|;UL0u`KbMGmbz z132*E;enOuhvr%9xH|_`sfYPtG7vlqbbsE`r~13d4^~9>DXBlvROJ1ai8wUAzaDzq z8av8cAEj)^Cy^XbO3KPDmSewm+l*3hw$v2h<98G(+^PS1(L1c#6esM`@d|}K^9?&{ zhrGJicJy<7@Y7jO=UiYhZ+KIH>v$6erfUTV-lG8@hT!M5`xZAvM}_G70ZuUR10QhWorpXT-g zP`+f#c83moU<(J#<%2?<=_~*Ng8|?&rq%p5V9M+_&-h&vJYa_(n}if@+`d}dOB3gI zNwB6Ywx@y((J)(qa0VhSV9=H$#-m%Uh!aTRh8g0u+~L;L=swnz+xFB4c?})wjiw zDmB9GRk}3?#$Jme8=-#loJ4o*F(~odC;N3h(Uwm-_DLh^#TO6CUzW(MXem{aw z(3lnNx*DoGYvt)z#Hq!HbKKnWoS&Zs;msOfJoI`6=DLB-l)Eu(W3n^dj6t}{nX2*q zF6X!&@)5DK&g3vb3dQ601C4pr?tgSJ@rei|hC$2EsbRdOiM zI%H}lN_PMsD%S=#q6o8El zHB!RZ3{^7XOTDOs4{eUHX4YNypeilDqg1J^;m3m1vHL%`S<`=1tXZc@@ngIqvzzW& zA$r*b&Ei8v+Cvwj8C->C-SB%;8(QPRkZ5w8hs#1=X5Gpnw5cOlD2AaHw6{ncw5TdY zZJ5jruQsJpzM^}jWp$CBRIuH5)`noFeVBsBT-zfOw|L9Ui7N^iokYsLg%F8Zg zTXBlTz2O`RV2`@qXX4*=n&emwH;DI9c(+n%gh$2cPX^k0Yc(YSJYTo%(mZ7+Kaqic z{JyH0w4EWc;#~}l0OTo;Vejt7A=iWBWFSTlyfjP=0W=N=fOh~Qyi&%@@=oPLkbHm; zoMwBI!!QW9-kX-dQ5kmu$$M093LLq6d)zIuB2~|r;!Pbmhtd;O52)Iz>giO=$!x6H z?v?$7@PKNE)xGwn=;3G#2P!ujIyTcrb|#JKT%_z%xHscjQaU0tb!y$P5_GLA#%9GA zD(+9~Ol`XXoY@%i2r?%t#;;V9d?Mh*Rg99R zQ~T@p>d7wip9S!M^F6boP_<$|h@m<>MMc8V`DJoWs#8_m;dckG=qOXP>yJZ?LF*$% z`A^e}J?_>MzlqFiQu!RMkzVz(YFZgRWUteVruDX_=x&sw>%Cz)itded@V$^R?{?kl7gQz617jFmak;_ZL%tL{0xd0W;caG3z@S&AuRJW)?u$_e!)0_|47*AR z*v8K&fL7_iif7N*D*d2n)%hsNO7~TVKh>U9)(T3MRIzHgMg7#V1Br$EW6mJ3#_T(O zMp2r{HIyeMKbB=|Y7xEIE7&;T!Mnq}yuciX7png%94jmfcA2fikbQGBpc@0>MF{Q1 zvvddvzE-J``ys88l5w>@;}DkOuEQ^>{NC6qfX$4J*hzyY~x&c zS~Lb$_zGbZylnm;t-N2K9+hYRFCFZ2TK&D5&PJiyEfhd;!>U-HF7>-u0Y3*(K}>-Z z{o)NY*S_^DtbLAEpB7Cw7FJ~mX{3CXrYp1 ziI&2QZ7SiX-++6%-7dejeqW8X{S~t}^aX;4*RHcG>#nD_zi&lk-^iG_{eP3NE{xRP z`aeEhP$#9CT~s#f4h5FkOXwa}ret^xOiccZKH-O6{(t5gRNZ9fP!uD}yw+4bi<&*N zYQHa1vr!E^uDs&?Vf0mdkM?^XIWhqQ@A<0qX#yT_DK94Y(7cH_#B-DmOq43>606}m z*6_H)gUNLg2HLT~)P0iFL3byZj!u#)7)Rs#$N58>ms4-!NDOv6MM=RJ z*^>%QhBH%G8V3Q$`z=p5R$k_q)Ew&qu;A~|Xq3VX*%$KPWEH9wYG@dul^;&^@1NKC zW+lI`R=Hcz3rH7`{TuULkd=3zeJUO5_GofbJ_Nx^LX`)GP!%Z2I2s7vOm$s4V!^aE z&BP;Kd4FV12(_CE^{I8wz7z!!Fu9g!k}Vr^%=ybyYl>CSEai|lb5TlOrIeXdF)<6q ze>$b;tko?a3(VBUba1NFz__=LSozJo9k3(th_9XXY-ws8Dl?6Doq*uouAIr1h+Luf zZ(0$#DSR59#TCX6r0|paJ4oWZnFCwKSW^<~(ig~OJf!5kFl1@{76%8vwj!?7CEw{V z-VZ?Vs`mH-&mxCkr?#~s4EQctVWhlh*s@2Xu`lqu4C<4OiZpUMMx>TR)6v`hu1BY* z<9vlUN@?)Ncl&c8oAc@lA6&{Mu0mred*oH=>M= z`gL<|&KBe`6RPNftlUkScK^H7y!W~IxijzG!|eJag*V-%yOsI0>3WX&NHJqkiPTY{ z)Mq9}x2d6&FcX`L@j!BK#fK%zo=A(b1yV8H?MUGjfPLNL9rIk@zecBBhZwHQu~2%> zd9UD}jk^Dx-0tTF4X(p^d0W#@^B5^SEZ4F*_S@9v7q|llhz-;9P^vVGXLey?Q&X42 zJF;OwgRhV&4Drk;Q*Qe9qZi5End$wV;au%x4zad?p zjh-{1Aywlrp&{j-jdZ+qcWPg;aa(my1 z-pz)$6#JX}DFi;kPiyB$?uKKKiB&T7X15SB&j~?pejEXXE9h@c>C+sDaG)9a%!NpQ zHKRInC9~z9x=d{*bomq^ca=g>x93-|YwaSl#J6d%`%D|>N)fv4q2xLbEU&hpP!8|4 zpxAj*tnO1d>Ef^)@ghqcrlj)gj*dn6{Q!*@J{ic5*=J984-VP9WFvU8U4Z5Jmb3(! zjM=|ed7~vgjl)>#MG`@I%B`_ZKXCx8+u$WJDQmt3TCIP2E9yr$Uh-uST_VUA)0$>3 z!0Q}Pig>&sP*XLkxe#hfY)8WvVlnAM$M_cId|3$8-S*@g4^%0t8;_^e`-$R_Ix?J2bF5-B z@5192;qC&tEy9F0ix*Xu^K>gZk;4Y)VPhv+gbc?0okZ_zyD4X@Zav5Kku?S^TDcP? z17W!j1lv~SX|MYp+_`6|jKB#wv*sgG*onIK>|CuK)UMM8`S{CclJb(rV$8pbbyk+M z1J>@Ho;+6!hB&ih(p1LJ0l?d}A=7d^F*j?50?C_F#nwHX{BZV6yeI`!5ci1%ohe~4 z+{7**_;|(Ki&y$K>pba=EQZYMx{{LgBGT#0lW`3=uMp#FXL^pjh9X^rKkjjDxO3Gm zhS!X+@QY-c5Gt|+azs!?1jfn0@p+ZszE*rNeK~&Rf;r*g4u0&S3-w{X)ox%zW~oAngHjLr7NvxE=kdY0d?g!>wZW zqE`UuW_G3AOVK3T72ubc_nyU8Bw!POA0$Bt&GoKS4G4>_n_yM2)XobYcC(JjtmH!u zC6ttaYh#LS>`|?vEFTX8ng%p&Df&(n0KDV4DcI6+sefu^86am}f|Sxo`M9@1|9kO^ zykw?$LWi%!cBAVkRyZ~D;9jHD{7;0FE;q`H=W!~Dp=@K621|H=oWd_&aoXJ34zc>sAe>@@p~JE6OQDgsk0CX%MCVHp*UU|7@`hL$&#@47~ zAso{5^^$9^%xZPe1`HEZwaTyuJ{YRoQ|@i&kntC-DZ4TaOV*SKWzgwAqd~ySTJPt| zuWS18$#SLKfGi#7e;8R6S!1%7HM6#@5@lbD%-VC%9zxA|mYz-hf{>Jb1@jCSlPe#a zV(p5pDLn%HnU^c3AkFOIJI?O;UAJzn+5!M&ka~ z+obTNwpE7TLT}(zP`hFU$8Cvj{NaUO$kJ%=)k?WjLT}xo1)FfP&OpB-$Y)SKEUf*=UcP=3mlm_XHu2A?L_`zXEuHrGxVXQDem(j zeMH|&A-5yUxvJrw8+YgPV#nva%4z7?6%h`Iun%wSQeu9s^S3vP7JA^jWA;`l*=EPk U9ny-!6dRgD+7Ta}BXr0A0(#OxJOBUy delta 77335 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 diff --git a/package.json b/package.json index c924057..9bc5a6d 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@trpc/server": "^10.45.2", "@tursodatabase/api": "^1.9.2", "@typeschema/valibot": "^0.13.4", - "@vercel/speed-insights": "^1.3.1", "bcrypt": "^6.0.0", "es-toolkit": "^1.43.0", "fast-diff": "^1.3.0", diff --git a/src/app.tsx b/src/app.tsx index 48bd74c..2e2499f 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -17,6 +17,7 @@ import { DarkModeProvider } from "./context/darkMode"; import { createWindowWidth, isMobile } from "~/lib/resize-utils"; import { MOBILE_CONFIG } from "./config"; import CustomScrollbar from "./components/CustomScrollbar"; +import { initPerformanceTracking } from "~/lib/performance-tracking"; function AppLayout(props: { children: any }) { const { @@ -29,6 +30,9 @@ function AppLayout(props: { children: any }) { let lastScrollY = 0; onMount(() => { + // Initialize performance tracking + initPerformanceTracking(); + const windowWidth = createWindowWidth(); createEffect(() => { diff --git a/src/components/blog/PostBodyClient.tsx b/src/components/blog/PostBodyClient.tsx index 10a9f90..15d37ca 100644 --- a/src/components/blog/PostBodyClient.tsx +++ b/src/components/blog/PostBodyClient.tsx @@ -84,6 +84,104 @@ export default function PostBodyClient(props: PostBodyClientProps) { }); }; + const processVideos = () => { + if (!contentRef) return; + + // Handle direct video elements + const videoElements = contentRef.querySelectorAll("video"); + + videoElements.forEach((video) => { + // Ensure videos play inline and don't trigger downloads + video.setAttribute("playsinline", ""); + video.setAttribute("controls", ""); + + // Remove download attribute if present + video.removeAttribute("download"); + + // Ensure proper MIME types on source elements + const sources = video.querySelectorAll("source"); + sources.forEach((source) => { + const src = source.getAttribute("src"); + if (src) { + // Remove download attribute from sources + source.removeAttribute("download"); + + // Set correct type attribute if missing + if (!source.hasAttribute("type")) { + if (src.endsWith(".mp4")) { + source.setAttribute("type", "video/mp4"); + } else if (src.endsWith(".webm")) { + source.setAttribute("type", "video/webm"); + } else if (src.endsWith(".ogg")) { + source.setAttribute("type", "video/ogg"); + } + } + } + }); + + // If video has direct src attribute, ensure type is set + const videoSrc = video.getAttribute("src"); + if (videoSrc && !video.hasAttribute("type")) { + if (videoSrc.endsWith(".mp4")) { + video.setAttribute("type", "video/mp4"); + } else if (videoSrc.endsWith(".webm")) { + video.setAttribute("type", "video/webm"); + } else if (videoSrc.endsWith(".ogg")) { + video.setAttribute("type", "video/ogg"); + } + } + }); + + // Handle iframes with video sources - replace with proper video tags + const iframes = contentRef.querySelectorAll("iframe"); + iframes.forEach((iframe) => { + const src = iframe.getAttribute("src"); + if ( + src && + (src.endsWith(".mp4") || + src.endsWith(".mov") || + src.endsWith(".webm") || + src.endsWith(".ogg")) + ) { + // Create a proper video element + const video = document.createElement("video"); + video.setAttribute("controls", ""); + video.setAttribute("playsinline", ""); + video.setAttribute("preload", "metadata"); + video.style.maxWidth = "100%"; + video.style.height = "auto"; + + // Set appropriate type based on file extension + let videoType = "video/mp4"; + if (src.endsWith(".mov")) { + videoType = "video/mp4"; // MOV files are typically H.264 which plays as mp4 + } else if (src.endsWith(".webm")) { + videoType = "video/webm"; + } else if (src.endsWith(".ogg")) { + videoType = "video/ogg"; + } + + video.setAttribute("type", videoType); + video.src = src; + + // Replace the iframe with the video element + const parent = iframe.parentElement; + if (parent) { + parent.replaceChild(video, iframe); + } + } + }); + + // Also check for any anchor tags wrapping videos that might have download attribute + const videoLinks = contentRef.querySelectorAll("a"); + videoLinks.forEach((link) => { + const hasVideo = link.querySelector("video"); + if (hasVideo) { + link.removeAttribute("download"); + } + }); + }; + const processReferences = () => { if (!contentRef) return; @@ -235,6 +333,7 @@ export default function PostBodyClient(props: PostBodyClientProps) { onMount(() => { setTimeout(() => { + processVideos(); processReferences(); if (props.hasCodeBlock) { processCodeBlocks(); @@ -286,6 +385,7 @@ export default function PostBodyClient(props: PostBodyClientProps) { createEffect(() => { if (props.body && contentRef) { setTimeout(() => { + processVideos(); processReferences(); if (props.hasCodeBlock) { processCodeBlocks(); diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index 55e6e5a..497f814 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -298,8 +298,23 @@ const IframeEmbed = Node.create({ return { setIframe: (options: { src: string }) => - ({ tr, dispatch }) => { + ({ tr, dispatch, editor }) => { const { selection } = tr; + + // Check if the src is a direct video file + const src = options.src || ""; + const isVideoFile = /\.(mp4|mov|webm|ogg)(\?.*)?$/i.test(src); + + if (isVideoFile) { + // Insert a proper video tag instead of iframe + if (dispatch) { + const videoHTML = ``; + editor.commands.insertContent(videoHTML); + } + return true; + } + + // For non-video URLs, create iframe as normal const node = this.type.create(options); if (dispatch) { diff --git a/src/db/types.ts b/src/db/types.ts index 08dacac..f6cff5a 100644 --- a/src/db/types.ts +++ b/src/db/types.ts @@ -150,6 +150,14 @@ export interface VisitorAnalytics { os?: string | null; session_id?: string | null; duration_ms?: number | null; + fcp?: number | null; + lcp?: number | null; + cls?: number | null; + fid?: number | null; + inp?: number | null; + ttfb?: number | null; + dom_load?: number | null; + load_complete?: number | null; created_at: string; } diff --git a/src/entry-client.tsx b/src/entry-client.tsx index 40c2305..22c7ee5 100644 --- a/src/entry-client.tsx +++ b/src/entry-client.tsx @@ -1,5 +1,4 @@ // @refresh reload -import { injectSpeedInsights } from "@vercel/speed-insights"; import { mount, StartClient } from "@solidjs/start/client"; // Handle chunk loading failures from stale cache @@ -26,5 +25,4 @@ window.addEventListener("unhandledrejection", (event) => { } }); -injectSpeedInsights(); mount(() => , document.getElementById("app")!); diff --git a/src/lib/performance-tracking.ts b/src/lib/performance-tracking.ts new file mode 100644 index 0000000..36a0e97 --- /dev/null +++ b/src/lib/performance-tracking.ts @@ -0,0 +1,163 @@ +/** + * Real User Monitoring (RUM) - Client-side performance tracking + * Captures Core Web Vitals and sends to analytics endpoint + */ + +interface PerformanceMetrics { + fcp?: number; + lcp?: number; + cls?: number; + fid?: number; + inp?: number; + ttfb?: number; + domLoad?: number; + loadComplete?: number; +} + +let metrics: PerformanceMetrics = {}; +let clsValue = 0; +let clsEntries: number[] = []; +let inpValue = 0; + +export function initPerformanceTracking() { + if (typeof window === "undefined" || !("PerformanceObserver" in window)) { + return; + } + + // Observe LCP + try { + const lcpObserver = new PerformanceObserver((entryList) => { + const entries = entryList.getEntries(); + const lastEntry = entries[entries.length - 1] as any; + metrics.lcp = lastEntry.renderTime || lastEntry.loadTime; + }); + lcpObserver.observe({ type: "largest-contentful-paint", buffered: true }); + } catch (e) { + console.debug("LCP not supported"); + } + + // Observe CLS + try { + const clsObserver = new PerformanceObserver((entryList) => { + for (const entry of entryList.getEntries()) { + const layoutShift = entry as any; + if (!layoutShift.hadRecentInput) { + clsValue += layoutShift.value; + clsEntries.push(layoutShift.value); + } + } + metrics.cls = clsValue; + }); + clsObserver.observe({ type: "layout-shift", buffered: true }); + } catch (e) { + console.debug("CLS not supported"); + } + + // Observe FID + try { + const fidObserver = new PerformanceObserver((entryList) => { + const firstInput = entryList.getEntries()[0] as any; + if (firstInput) { + metrics.fid = firstInput.processingStart - firstInput.startTime; + } + }); + fidObserver.observe({ type: "first-input", buffered: true }); + } catch (e) { + console.debug("FID not supported"); + } + + // Observe INP (event timing) + try { + const interactions: number[] = []; + const inpObserver = new PerformanceObserver((entryList) => { + for (const entry of entryList.getEntries()) { + const eventEntry = entry as any; + if (eventEntry.interactionId) { + interactions.push(eventEntry.duration); + const sorted = [...interactions].sort((a, b) => b - a); + const p98Index = Math.floor(sorted.length * 0.02); + inpValue = sorted[p98Index] || sorted[0] || 0; + metrics.inp = inpValue; + } + } + }); + inpObserver.observe({ type: "event", buffered: true }); + } catch (e) { + console.debug("INP not supported"); + } + + // Get navigation timing metrics + window.addEventListener("load", () => { + setTimeout(() => { + const navTiming = performance.getEntriesByType( + "navigation" + )[0] as PerformanceNavigationTiming; + + if (navTiming) { + metrics.ttfb = navTiming.responseStart - navTiming.requestStart; + metrics.domLoad = + navTiming.domContentLoadedEventEnd - navTiming.fetchStart; + metrics.loadComplete = navTiming.loadEventEnd - navTiming.fetchStart; + } + + // Get FCP + const paintEntries = performance.getEntriesByType("paint"); + const fcpEntry = paintEntries.find( + (entry) => entry.name === "first-contentful-paint" + ); + if (fcpEntry) { + metrics.fcp = fcpEntry.startTime; + } + + // Send metrics after a short delay to ensure all metrics are captured + setTimeout(() => { + sendMetrics(); + }, 2000); + }, 0); + }); + + // Send metrics before page unload (in case user navigates away) + window.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") { + sendMetrics(); + } + }); +} + +function sendMetrics() { + // Only send if we have at least one metric + if (Object.keys(metrics).length === 0) { + return; + } + + const path = window.location.pathname + window.location.search; + + // tRPC batch format for public procedure + const tRPCPayload = { + 0: { + path: path, + metrics: { ...metrics } + } + }; + + const apiUrl = "/api/trpc/analytics.logPerformance?batch=1"; + const payload = JSON.stringify(tRPCPayload); + + if (navigator.sendBeacon) { + const blob = new Blob([payload], { type: "application/json" }); + navigator.sendBeacon(apiUrl, blob); + } else { + // Fallback to fetch with keepalive + fetch(apiUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: payload, + keepalive: true + }).catch((err) => + console.debug("Failed to send performance metrics:", err) + ); + } + + // Clear metrics after sending + metrics = {}; +} diff --git a/src/routes/analytics.tsx b/src/routes/analytics.tsx index 8d019a4..0e683d6 100644 --- a/src/routes/analytics.tsx +++ b/src/routes/analytics.tsx @@ -30,13 +30,13 @@ interface PerformanceTarget { } const PERFORMANCE_TARGETS: Record = { - lcp: { good: 2500, acceptable: 4000, label: "LCP", unit: "ms" }, - fcp: { good: 1800, acceptable: 3000, label: "FCP", unit: "ms" }, - ttfb: { good: 800, acceptable: 1800, label: "TTFB", unit: "ms" }, - cls: { good: 0.1, acceptable: 0.25, label: "CLS", unit: "" }, + lcp: { good: 1500, acceptable: 2500, label: "LCP", unit: "ms" }, + fcp: { good: 1000, acceptable: 1800, label: "FCP", unit: "ms" }, + ttfb: { good: 500, acceptable: 800, label: "TTFB", unit: "ms" }, + cls: { good: 0.05, acceptable: 0.1, label: "CLS", unit: "" }, avgDuration: { - good: 3000, - acceptable: 5000, + good: 2000, + acceptable: 3000, label: "Avg Duration", unit: "ms" } @@ -57,22 +57,22 @@ function getPerformanceRating( function getRatingColor(rating: "good" | "acceptable" | "poor"): string { switch (rating) { case "good": - return "text-green-600 dark:text-green-400"; + return "text-green"; case "acceptable": - return "text-yellow-600 dark:text-yellow-400"; + return "text-yellow"; case "poor": - return "text-red-600 dark:text-red-400"; + return "text-red"; } } function getRatingBgColor(rating: "good" | "acceptable" | "poor"): string { switch (rating) { case "good": - return "bg-green-100 dark:bg-green-900/30"; + return "bg-green/10"; case "acceptable": - return "bg-yellow-100 dark:bg-yellow-900/30"; + return "bg-yellow/10"; case "poor": - return "bg-red-100 dark:bg-red-900/30"; + return "bg-red/10"; } } @@ -87,8 +87,6 @@ function formatNumber(num: number): string { } export default function AnalyticsPage() { - const adminCheck = createAsync(() => checkAdmin()); - const [timeWindow, setTimeWindow] = createSignal(7); const [selectedPath, setSelectedPath] = createSignal(null); const [error, setError] = createSignal(null); @@ -103,6 +101,17 @@ export default function AnalyticsPage() { } }); + const performanceStats = createAsync(async () => { + try { + return await api.analytics.getPerformanceStats.query({ + days: timeWindow() + }); + } catch (e) { + console.error("Failed to load performance stats:", e); + return null; + } + }); + const pathStats = createAsync(async () => { const path = selectedPath(); if (!path) return null; @@ -120,13 +129,13 @@ export default function AnalyticsPage() { return ( <> Analytics Dashboard - Admin -
+
-

+

Analytics Dashboard

-

+

Visitor analytics and performance metrics

@@ -139,8 +148,8 @@ export default function AnalyticsPage() { onClick={() => setTimeWindow(days)} class={`rounded-lg px-4 py-2 font-medium transition-colors ${ timeWindow() === days - ? "bg-blue-600 text-white" - : "bg-white text-gray-700 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700" + ? "bg-blue text-base" + : "bg-surface0 text-text hover:bg-surface1 border-surface1 border" }`} > {days === 1 ? "24h" : `${days}d`} @@ -150,7 +159,7 @@ export default function AnalyticsPage() {
-
+

Error loading analytics

{error()}

@@ -161,53 +170,218 @@ export default function AnalyticsPage() { <> {/* Overview Cards */}
-
-
- Total Requests -
-
+
+
Total Requests
+
{formatNumber(data().totalVisits)}
-
+
{formatNumber(data().totalPageVisits)} pages,{" "} {formatNumber(data().totalApiCalls)} API
-
-
+
+
Unique Visitors
-
+
{formatNumber(data().uniqueVisitors)}
-
-
+
+
Authenticated Users
-
+
{formatNumber(data().uniqueUsers)}
-
-
+
+
Avg. Visits/Day
-
+
{formatNumber(data().totalVisits / timeWindow())}
- {/* Top Pages */} -
-
-

- Top Pages + {/* Performance Metrics Section */} + 0 + } + > +
+

+ Core Web Vitals

+ + {/* Performance Overview Cards */} +
+ +
+
+ LCP (Largest Contentful Paint) +
+
+ {Math.round(performanceStats()!.avgLcp!)}ms +
+
+ Target: <1.5s (good), <2.5s (ok) +
+
+
+ + +
+
+ FCP (First Contentful Paint) +
+
+ {Math.round(performanceStats()!.avgFcp!)}ms +
+
+ Target: <1s (good), <1.8s (ok) +
+
+
+ + +
+
+ CLS (Cumulative Layout Shift) +
+
+ {performanceStats()!.avgCls!.toFixed(3)} +
+
+ Target: <0.05 (good), <0.1 (ok) +
+
+
+ + +
+
+ TTFB (Time to First Byte) +
+
+ {Math.round(performanceStats()!.avgTtfb!)}ms +
+
+ Target: <500ms (good), <800ms (ok) +
+
+
+
+ + {/* Performance by Page */} + 0 + } + > +
+
+

+ Performance by Page +

+

+ {performanceStats()!.totalWithMetrics} page loads + with performance data +

+
+
+
+ + + + + + + + + + + + + + {(page) => ( + + + + + + + + + )} + + +
Page + LCP + + FCP + + CLS + + TTFB + + Samples +
+ {page.path} + + {Math.round(page.avgLcp)}ms + + {Math.round(page.avgFcp)}ms + + {page.avgCls.toFixed(3)} + + {Math.round(page.avgTtfb)}ms + + {page.count} +
+
+
+
+
+
+
+ + {/* Top Pages */} +
+
+

Top Pages

@@ -217,24 +391,24 @@ export default function AnalyticsPage() { (pathData.count / data().totalPageVisits) * 100; return (
setSelectedPath(pathData.path)} >
- + {pathData.path} - + {formatNumber(pathData.count)} visits
-
+
-
+
{percentage.toFixed(1)}% of page traffic
@@ -246,11 +420,9 @@ export default function AnalyticsPage() {
{/* Top API Calls */} -
-
-

- Top API Calls -

+
+
+

Top API Calls

@@ -261,20 +433,20 @@ export default function AnalyticsPage() { return (
- + {apiData.path} - + {formatNumber(apiData.count)}
-
+
-
+
{percentage.toFixed(1)}% of API traffic
@@ -288,11 +460,9 @@ export default function AnalyticsPage() { {/* Device & Browser Stats */}
{/* Device Types */} -
-
-

- Device Types -

+
+
+

Device Types

@@ -312,12 +482,12 @@ export default function AnalyticsPage() { {device.type} - + {formatNumber(device.count)} ( {percentage.toFixed(1)}%)
-
+
{/* Browsers */} -
-
-

- Browsers -

+
+
+

Browsers

@@ -356,12 +524,12 @@ export default function AnalyticsPage() { {browser.browser} - + {formatNumber(browser.count)} ( {percentage.toFixed(1)}%)
-
+
0}> -
-
-

+
+
+

Top Referrers

@@ -388,11 +556,11 @@ export default function AnalyticsPage() {
{(referrer) => ( -
- +
+ {referrer.referrer} - + {formatNumber(referrer.count)}
@@ -409,14 +577,14 @@ export default function AnalyticsPage() { {/* Path Details Modal/Section */} {(stats) => ( -
-
-

+
+
+

Path Details: {selectedPath()}

@@ -424,26 +592,20 @@ export default function AnalyticsPage() {
-
- Total Visits -
-
+
Total Visits
+
{formatNumber(stats().totalVisits)}
-
- Unique Visitors -
-
+
Unique Visitors
+
{formatNumber(stats().uniqueVisitors)}
-
- Avg. Duration -
-
+
Avg. Duration
+
{stats().avgDurationMs ? `${(stats().avgDurationMs! / 1000).toFixed(1)}s` : "N/A"} @@ -454,7 +616,7 @@ export default function AnalyticsPage() { {/* Visits by Day */} 0}>
-

+

Visits by Day

@@ -467,14 +629,14 @@ export default function AnalyticsPage() { return (
- + {new Date(day.date).toLocaleDateString()} - + {formatNumber(day.count)}
-
+
{ @@ -23,8 +31,9 @@ export async function logVisit(entry: AnalyticsEntry): Promise { await conn.execute({ sql: `INSERT INTO VisitorAnalytics ( id, user_id, path, method, referrer, user_agent, ip_address, - country, device_type, browser, os, session_id, duration_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + country, device_type, browser, os, session_id, duration_ms, + fcp, lcp, cls, fid, inp, ttfb, dom_load, load_complete + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: [ uuid(), entry.userId || null, @@ -38,7 +47,15 @@ export async function logVisit(entry: AnalyticsEntry): Promise { entry.browser || null, entry.os || null, entry.sessionId || null, - entry.durationMs || null + entry.durationMs || null, + entry.fcp || null, + entry.lcp || null, + entry.cls || null, + entry.fid || null, + entry.inp || null, + entry.ttfb || null, + entry.domLoad || null, + entry.loadComplete || null ] }); } catch (error) { @@ -308,6 +325,121 @@ export async function getPathAnalytics( }; } +export async function getPerformanceStats(days: number = 30): Promise<{ + avgLcp: number | null; + avgFcp: number | null; + avgCls: number | null; + avgInp: number | null; + avgTtfb: number | null; + avgDomLoad: number | null; + avgLoadComplete: number | null; + p75Lcp: number | null; + p75Fcp: number | null; + totalWithMetrics: number; + byPath: Array<{ + path: string; + avgLcp: number; + avgFcp: number; + avgCls: number; + avgTtfb: number; + count: number; + }>; +}> { + const conn = ConnectionFactory(); + + // Get average metrics + const avgResult = await conn.execute({ + sql: `SELECT + AVG(lcp) as avgLcp, + AVG(fcp) as avgFcp, + AVG(cls) as avgCls, + AVG(inp) as avgInp, + AVG(ttfb) as avgTtfb, + AVG(dom_load) as avgDomLoad, + AVG(load_complete) as avgLoadComplete, + COUNT(*) as total + FROM VisitorAnalytics + WHERE created_at >= datetime('now', '-${days} days') + AND fcp IS NOT NULL`, + args: [] + }); + + const avgRow = avgResult.rows[0] as any; + + // Get 75th percentile for LCP and FCP (approximation using median) + const p75LcpResult = await conn.execute({ + sql: `SELECT lcp as p75 + FROM VisitorAnalytics + WHERE created_at >= datetime('now', '-${days} days') + AND lcp IS NOT NULL + ORDER BY lcp + LIMIT 1 OFFSET ( + SELECT COUNT(*) * 75 / 100 + FROM VisitorAnalytics + WHERE created_at >= datetime('now', '-${days} days') + AND lcp IS NOT NULL + )`, + args: [] + }); + + const p75FcpResult = await conn.execute({ + sql: `SELECT fcp as p75 + FROM VisitorAnalytics + WHERE created_at >= datetime('now', '-${days} days') + AND fcp IS NOT NULL + ORDER BY fcp + LIMIT 1 OFFSET ( + SELECT COUNT(*) * 75 / 100 + FROM VisitorAnalytics + WHERE created_at >= datetime('now', '-${days} days') + AND fcp IS NOT NULL + )`, + args: [] + }); + + // Get performance by path (only for non-API paths) + const byPathResult = await conn.execute({ + sql: `SELECT + path, + AVG(lcp) as avgLcp, + AVG(fcp) as avgFcp, + AVG(cls) as avgCls, + AVG(ttfb) as avgTtfb, + COUNT(*) as count + FROM VisitorAnalytics + WHERE created_at >= datetime('now', '-${days} days') + AND fcp IS NOT NULL + AND path NOT LIKE '/api/%' + GROUP BY path + ORDER BY count DESC + LIMIT 20`, + args: [] + }); + + const byPath = byPathResult.rows.map((row: any) => ({ + path: row.path, + avgLcp: row.avgLcp || 0, + avgFcp: row.avgFcp || 0, + avgCls: row.avgCls || 0, + avgTtfb: row.avgTtfb || 0, + count: row.count + })); + + return { + avgLcp: avgRow?.avgLcp || null, + avgFcp: avgRow?.avgFcp || null, + avgCls: avgRow?.avgCls || null, + avgInp: avgRow?.avgInp || null, + avgTtfb: avgRow?.avgTtfb || null, + avgDomLoad: avgRow?.avgDomLoad || null, + avgLoadComplete: avgRow?.avgLoadComplete || null, + p75Lcp: (p75LcpResult.rows[0] as any)?.p75 || null, + p75Fcp: (p75FcpResult.rows[0] as any)?.p75 || null, + totalWithMetrics: avgRow?.total || 0, + byPath + }; +} + export async function cleanupOldAnalytics( olderThanDays: number ): Promise { diff --git a/src/server/api/routers/analytics.ts b/src/server/api/routers/analytics.ts index d8a04e8..8f9c138 100644 --- a/src/server/api/routers/analytics.ts +++ b/src/server/api/routers/analytics.ts @@ -1,13 +1,147 @@ -import { createTRPCRouter, adminProcedure } from "../utils"; +import { createTRPCRouter, adminProcedure, publicProcedure } from "../utils"; import { z } from "zod"; import { queryAnalytics, getAnalyticsSummary, getPathAnalytics, - cleanupOldAnalytics + cleanupOldAnalytics, + logVisit, + getPerformanceStats } from "~/server/analytics"; +import { ConnectionFactory } from "~/server/database"; export const analyticsRouter = createTRPCRouter({ + logPerformance: publicProcedure + .input( + z.object({ + path: z.string(), + metrics: z.object({ + fcp: z.number().optional(), + lcp: z.number().optional(), + cls: z.number().optional(), + fid: z.number().optional(), + inp: z.number().optional(), + ttfb: z.number().optional(), + domLoad: z.number().optional(), + loadComplete: z.number().optional() + }) + }) + ) + .mutation(async ({ input, ctx }) => { + try { + const conn = ConnectionFactory(); + + // First, try to find a recent entry for this path without performance data + const checkQuery = await conn.execute({ + sql: `SELECT id, path, created_at FROM VisitorAnalytics + WHERE path = ? + AND created_at >= datetime('now', '-5 minutes') + AND fcp IS NULL + ORDER BY created_at DESC + LIMIT 1`, + args: [input.path] + }); + + if (checkQuery.rows.length > 0) { + const result = await conn.execute({ + sql: `UPDATE VisitorAnalytics + SET fcp = ?, lcp = ?, cls = ?, fid = ?, inp = ?, ttfb = ?, dom_load = ?, load_complete = ? + WHERE id = ?`, + args: [ + input.metrics.fcp || null, + input.metrics.lcp || null, + input.metrics.cls || null, + input.metrics.fid || null, + input.metrics.inp || null, + input.metrics.ttfb || null, + input.metrics.domLoad || null, + input.metrics.loadComplete || null, + (checkQuery.rows[0] as any).id + ] + }); + + return { + success: true, + rowsAffected: result.rowsAffected, + action: "updated" + }; + } else { + const { v4: uuid } = await import("uuid"); + const { enrichAnalyticsEntry } = await import("~/server/analytics"); + + const req = ctx.event.nativeEvent.node?.req || ctx.event.nativeEvent; + const userAgent = + req.headers?.["user-agent"] || + ctx.event.request?.headers?.get("user-agent") || + undefined; + const referrer = + req.headers?.referer || + req.headers?.referrer || + ctx.event.request?.headers?.get("referer") || + undefined; + const { getRequestIP } = await import("vinxi/http"); + const ipAddress = getRequestIP(ctx.event.nativeEvent) || undefined; + const { getCookie } = await import("vinxi/http"); + const sessionId = + getCookie(ctx.event.nativeEvent, "session_id") || undefined; + + const enriched = enrichAnalyticsEntry({ + userId: ctx.userId, + path: input.path, + method: "GET", + userAgent, + referrer, + ipAddress, + sessionId, + fcp: input.metrics.fcp, + lcp: input.metrics.lcp, + cls: input.metrics.cls, + fid: input.metrics.fid, + inp: input.metrics.inp, + ttfb: input.metrics.ttfb, + domLoad: input.metrics.domLoad, + loadComplete: input.metrics.loadComplete + }); + + await conn.execute({ + sql: `INSERT INTO VisitorAnalytics ( + id, user_id, path, method, referrer, user_agent, ip_address, + country, device_type, browser, os, session_id, duration_ms, + fcp, lcp, cls, fid, inp, ttfb, dom_load, load_complete + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + args: [ + uuid(), + enriched.userId || null, + enriched.path, + enriched.method, + enriched.referrer || null, + enriched.userAgent || null, + enriched.ipAddress || null, + enriched.country || null, + enriched.deviceType || null, + enriched.browser || null, + enriched.os || null, + enriched.sessionId || null, + enriched.durationMs || null, + enriched.fcp || null, + enriched.lcp || null, + enriched.cls || null, + enriched.fid || null, + enriched.inp || null, + enriched.ttfb || null, + enriched.domLoad || null, + enriched.loadComplete || null + ] + }); + + return { success: true, rowsAffected: 1, action: "created" }; + } + } catch (error) { + console.error("Failed to log performance metrics:", error); + return { success: false }; + } + }), + getLogs: adminProcedure .input( z.object({ @@ -82,5 +216,20 @@ export const analyticsRouter = createTRPCRouter({ deleted, olderThanDays: input.olderThanDays }; + }), + + getPerformanceStats: adminProcedure + .input( + z.object({ + days: z.number().min(1).max(365).default(30) + }) + ) + .query(async ({ input }) => { + const stats = await getPerformanceStats(input.days); + + return { + ...stats, + timeWindow: `${input.days} days` + }; }) }); diff --git a/src/server/api/utils.ts b/src/server/api/utils.ts index 11c940e..f473986 100644 --- a/src/server/api/utils.ts +++ b/src/server/api/utils.ts @@ -50,17 +50,20 @@ async function createContextInner(event: APIEvent): Promise { const ipAddress = getRequestIP(event.nativeEvent) || undefined; const sessionId = getCookie(event.nativeEvent, "session_id") || undefined; - logVisit( - enrichAnalyticsEntry({ - userId, - path, - method, - userAgent, - referrer, - ipAddress, - sessionId - }) - ); + // Don't log the performance logging endpoint itself to avoid circular tracking + if (!path.includes("analytics.logPerformance")) { + logVisit( + enrichAnalyticsEntry({ + userId, + path, + method, + userAgent, + referrer, + ipAddress, + sessionId + }) + ); + } return { event,