From 2db233cb787bf636b3a0c1d30eb328ac6c0881ab Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 8 May 2026 11:21:48 +0000 Subject: [PATCH 1/2] fix(web): attach ssh sessions without dashboard polling --- .../issue-251/dashboard-route.png | Bin 0 -> 52398 bytes .../issue-251/network-proof.json | 35 +++ .../issue-251/terminal-only-route.png | Bin 0 -> 52739 bytes packages/api/src/services/projects.ts | 4 +- packages/api/tests/projects.test.ts | 12 +- .../app/src/web/app-terminal-session-core.ts | 35 +++ packages/app/src/web/app-terminal-session.tsx | 212 ++++++++++++++++++ packages/app/src/web/app.tsx | 57 +++-- .../app-terminal-session-core.test.ts | 58 +++++ 9 files changed, 391 insertions(+), 22 deletions(-) create mode 100644 docs/pr-screenshots/issue-251/dashboard-route.png create mode 100644 docs/pr-screenshots/issue-251/network-proof.json create mode 100644 docs/pr-screenshots/issue-251/terminal-only-route.png create mode 100644 packages/app/src/web/app-terminal-session-core.ts create mode 100644 packages/app/src/web/app-terminal-session.tsx create mode 100644 packages/app/tests/docker-git/app-terminal-session-core.test.ts diff --git a/docs/pr-screenshots/issue-251/dashboard-route.png b/docs/pr-screenshots/issue-251/dashboard-route.png new file mode 100644 index 0000000000000000000000000000000000000000..f953567af3436dac4eb64eef59079d5a7e2e77b5 GIT binary patch literal 52398 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�zY}Ra4Z74CNnCpa41H?a9akZfwV(1dTZ<$;rM9)7l#T81$ z#UabVjx)5RS2Le>SEm}qFTeg7RJWrwLxdxp)?PJS$kQW}&nI-DM@JeA@13Xf-ned` z>{p2MbR!vNp7LB~jp(_SUlRf1n9=gPIbQ3brpp~_6I&b^b9f(;E3QkSL$ zAynQMbHU=A$a=&k9U1 zu3Var^c)7GlCzbqSDGbfj#YlUN}nPI`^T~7*R5`~%n~Ah%3SHTU*wrI*`Sg_rXhc- zwA}6$hQ?O5a;@6OPzD= zXWg&g=$AB}8x@KQoLg9`vktaPN;GRJSIxhiUE|rV+O|{rx`O=pA0L6i7E-Sq4!eR` z@X^MuGAYkiQ~l~#1JTx~D&tSk>)jgaF5Mq%5J+H57HH&AO!e4l9B)IS*$(?cH{sQw zje?a?BFl}rx-^oDM?vCOZb>d3@REx)Rm0zBC`Q80FR9Zt@TVafzmF^xRw~oGznlns zKWMN^!r3%@LO?`pTd#(;K3jLxAs!=Eu4jNh$$WJhe_89+mgBti!Z7NeXYCKH znlt%v;of9{I;Q&RHPCf!kv-77m@Dp}{IBzRg}NskNqz_D<IEXe@iM| zT$4UM){9rzJ2{KWtG-1oyvoMj=2>`v8yc#L-I8D6_RS9;@e&$-<`p?ihH&7894#etGr}FxH4WbIIQW`EOAJax+a`+d%h&Hzfcir zEmV31rE;|Qz0NaU+WjqO;IXM3Rx+~1IuxHdqwHWaHjJmkAH(DPy{*@IDNFgcvxgF| z-YnG2wO#~%LUnO69!ESvB7IT0(2w+MDQ%}6udkNvpnSuK=Y`d`;3RBcx)tfB@=J+D z=Y063yKq}=G#fm3hyJKqKihW5(Spm?87I&-@4CwcS|DAGRQ|wH?Sa#fh>QKKNoy8X zb7|B3>Z&d0AWz$|bDc|pZtU4|kQ2llQWpG){eIhRS+m1W3DXa#3EAlnPkJk^LM@~( z&J?PSny2g+P?u{;PNxP2^4RJMzS#d7L0O2b>GGs#3nr?qHHTEkWWLOWk>_jl=^x zkDkB;c&VLT#_gn}C%t!J2-hx7C%L8ek|iGsCV$}!&Xp5*d>!VYdFAEKHzK!=x8GS2 zfcg#Waio-)IKLO1VbTh>p02ROUTr8Y;jiwOPZ$~;pl;2%p__Vc$U29#SjpNA*PFMaa6N0;nD>WeCBJ;wCd=X00 zC^7@(2^77D9zUJ`+@qlj{0sK_qx8V;EME`r?S#2m{9mfa@K2`a-vu6isUZ+Dzw!Ii z4WMA*3tE&X>%w>6Xu-JmpWS}`-zi%7cf-%0Hl&gXpx@rgEm8j_Xs^rBZtkvh2Et5- zqy??lhg%u(f2|P3-|hc@8Oi^RvX1}M!!PW6eBTYO4mMJ|>Q`Jt4T7N}cnO62%@uta z5?lOLVK%z!+fi2s`#(P1H#pheyB*Vn4yp&c$~IxCvZ$$tG`t9ICA~@;|EC9q4$U5BZvbA`WpIrUNtuAtRQWB%fxKr zU@!OD*FP7zNsUO}t33V@BS&ce?xH;I@$2)q$Rr(>mnK^^gfY6V z3L*lI%Kt%L@|2@atj85uf#@7UC&daB4dLK zb$9uZ>;lI7rovnWxtKy%N)|;JH{cb%r-S-Z+TAx!A z?<eAIhLHspkPt59d6NcV=|l1+pljb0M+ioR*tEUS4J| z)kjxu#R?Wy48L#~?z@EB3!f+N*3j|6%D$rd4)wI4n<+ho>BqFS<*X!Zsft*tO92@OI z2Wd#0!`Oq1(h?qa79l=c%(e?PvtwT|)+Bg#^@a(YarF$&+!hqCpz<1rgrIk#m2@+U z*I4S*zgowYuA8DfXoJUgqz&+v?*%U0aw9d8tbjrvJe(1}cG@A?F8FZT4z&>Miz{>e zwNFyUvT|iMOC7)0^zBIRX)^o)L9fGr+t1~Wu*)@`X8Yoq1uF-l9QHNd{mq9P==DFp zxxi5&aW(QL?4m+PMlr5zpQhqe>l5LniG({A279GfjZVLBH#UOB5Dov&#}ZX0r@{kv zLJuv+vl7h=r9MGW%}%w@tS2`eAD>ja_hS(CBoh243qi@(P?agF)nDB-5c2js0&>x6 zS4%v|$>4i6@p6yLqfLYf3BK&p0BZ5n7f^oAE~T z5x=b9)C^@fS`M3Jg~lvql^j)cxH%duZMhnqrb-Cc-)kQ_LxC+0aufcMJ`Mh0akSl# zVj`X-hiiQ@T7mNdeOdc&qh;MAyslotb@3eR|geTNf#I*SzJqm}{r& zypWIi=QBqm#r7VX_9O}QUllZ+=ec%!!;;kZMlW}+p0`~#?Y64)l!x`;^BPUa|EhD@ z&aJ<{G#rUtUwU}8GAnkeK2^A~U~vR1U3oc2A}l0Se|?j#VByr;H#5b6!t{m}^Ht&q zLy>i@-8ZcJIVs*f#P`?j?A-pAAWoW@ry{rTof$@-j#K!3$=QTPZ(>#Hv*umw(Ueh*Ke$ zO6qp)a1h-u#Mwn7sJ@s~Mn=CrCeqWWm-pJ%xD(yp2u}CbyDBcMF`-}J8AOEm7Z2`8 zU|Ns=lZwRuq`Lk_6~C`HMn0b~KuHzPL8^ zETDC|AwZ55Dl&KCShn@eFDGV?mBMp16h6ZZhK`ySu1|LmNwzjLB(Kx5zT9)>GvG?m zryQj|jc?`#GvCe=f-Z$7HjFW))?fGpJ}+B zmuk6OfgU6nPaxLU-TUSod|ttBc(!=eV?jr8cIhUW`}S^4lU7k7z=$fs@lcZtUq7xp zuhBedhc56pa|Q%O#^K)>%xFxcGWq9r^ZltB85XuQ6o5@0(l^D*llIenBiT%0$HmW= z|C(AgJnps{kx@;{x&!D2JAdP5mftVJF&y=zof7E6ieyHBLHRBP~bI5Xr5X5!g>}zwFM`>zH+|2h?OnTa3iXyi|aDK zE@el3X{A1DUY1lXtFf(qrS$0G7gCnUA@5hbYe)OjVUNo%R$6GTfA8RPoz)?hEN33o z)eKx>)!#lxKZy+t_WJbk(>kL?EKu5(V-H)Id$jH6SMD2$#XD2w^%`joT=jKdb<-Rc zeTnQ|alEOyO7b#0k7+*#ZJZ+p2RYJPuEU**d1Y!#4p$4a7+bvtM}#o>UV0^@4Y{UF~X$`+L=TT-a?_exiV9nH%bfhX)Bz#7VY_>m z^R=sx#61WIQGWO_yU8P77K`yJL62dT@_hUARKGBDWQ)SfAD5#M);KlA<{`6|F6Ar1 zE1}IzYR*)@$$0e5cYz+=oL!%+!5TujD6^G za$B_UB(p|E=?GSj#`nLRmn_}3z+)xkedE7^ir&GGs ztDExl0FP_C9*HpZ$IR>SG$}N5X1Arf+vmHV`w0Frx90Bse=gehKNsi!3)5yA-OVIH z_{MBKDa_8!E?OV~*vke%PeM)L)T)XCEihJd+8cEUuSAZ?fm3si#j|QR!&QUe@UIg0 zfA^p=z9HQ&G@J1mmZwU(2XGm1xJ5nI6w0C)(N7q#3gD3cPr~e}C1mH98_zL7NF5xpdD`bPZ9697gr3!gm8>xhjeV(U)E{2i*EH zj$M_8^kVFHW()KTxvp*OQ#3lxXnZ88Uu7xilF;8-+J0bNAl+j}HJxPWDd^3T@<6iA z3%~CC`(2F0YNemZOddWxolhhENH7fP1gw1k$iOyL?U86yN`WosB>5rc9x&S zJzhjZFyjtSUZTen)FX2Y>?oulp#=@Ly4)_eLxCB5w*;^5#j$8l`wW!z%O$xat^iwH zNy*yw-04rC#wCr$SnKU)Aszy>v}N?j_G*^JlpY~H6ovZ>>2hVpDMLciK7L(H9btp` zoBe(!5-!BT^@iIE!RU{ZaNj~fE zOgn@ujpm++m{$wRo645PTb0~R2X`1w-~ApHyO)*MGEbqe!+p?wKa+2e3BzLitWciz zX2A7i=&dFvDu-*twrTbAL=ux;(Lmr15KA*Y(;;p#|EJ|o@JQz8Gv;g&yDa6dj|LiVh(As=rG`9`$Zlkde^SznFMI zW!Dt@5UVcdmgb-pjk+wQvPBl7`hRa2sC?PWiL~?Q-p|6XHaAi%vx;>n=FJ`y$YGo z4uif6S$s>Xr^wQG-;6@}1v&$TnufD}qwXl^N6G+whiWCwODlbU8)fJtSeh~-mH%gO zFAxSAk0E*ZNHIY<9D5woD8b8#TPBH7wfF=3_Aq-ci+y%4NQc&k{1l~3P;pd}JJ9}R zpn5h%*#u35vdn0}qKzF42L;xawX$s-4#75Mjj1rfO|aE75)U%hyVKSV8UN9XX^;sf zTyl9lc!^Z`kqUc7-*Y(=V?Q^62_Xg*wy4)`rBAk~LZ5~Q)S^k;v^zz@Ug6@30)(Xv zH>=jsVEv;pnAStaTk$C_UHb_4p-eIKRMDlm*vvwPnu6l}SpVhi>pR>CHR)KDre& z3jYSqJp1zOCOOuICIAuO_~-_l@3H;ZmIlr=9BUFjM?lzpv3l=+UVC=;)FH6$as~~8 z_F~U9Df@!WZelc*dd;_z0D6$%m_Ma@(m|qez5N!rCvfoe`agI-veJ1@MzvjSDRT0m zbFb}po@%`_*hG?f4dHs=_d;UAOAZH3x6I#_WoL8bg0r=I2T0#OJ37EdgZU~#=R)i- z+cd;!KkZ!gy=}QF*$GCgD|c=a9h`QMDd``(vu;5yW-eWN4iGpu z@fBljW$UojkWWabt8}$i~C`;)kWI%ivxz{y5LSkzMjcxNr^wC%GV`1?4?YK_x| z-KCCibX?ZgBoFX+*p^dE>8Gx9IawrC?LKK@)V9uA&kBc~yM*LaOtWp*eafED{Ytvi zMi=}89sSre=-IN|s&8#y-DTHmbp15r&ogMcClSFKv%?h4wW1o13!)OyIV=j<>3KGOLLM#f+3P5q|%Ys{CYmJgbS06_05^Z109=zx$FTOUL=Xt+MFx7UN6p-65 z2s$JE#X42IPQ)s5TU-R_WZ-rbcBp zx2|*6+Iq>d`Q=eSFfK#xam!yb1lvDT{z=NeRL}?2D?MnlA3u{03PH`9Ewer+;j9Ro zac+~YaUMC0Q;&A}x)S?co5StoS@dzY&Ot0J2W4H{`q|*w*yar1hMe=6Upqy4GB*xO zMH262;U>O0$#f4fiI zqSj{+sG$xgjP#x71xT-5H`U;yb{3bLE<#k-1Lz?+85+V$8!NSDCm9PR&yQ0alWeVu zc87h{yfkbj32oc3l*BGj+2Y$on+6mL%pBmF0AQu~26qf(>lj6k-QN)8K#USOoehun zFD6$u_}*X)k(x}P-b+1c?&?z%qz5QONjbv0X!sptEIZBiP=&Z!?|?CE2Ti|ztY*#D z>~{`tb^Xr@*h#(Q`THXdq$n5I1@qBnJE2gV2ILus%=^rp6A^**X%+J&iYvcvQS4d?;wLpm1(BB zojro|#~!eq#IswE%-1~gt$RF0)~X#PTbD1>)Fr+==Y{VvccQ5)ve4w5?hzF@!!W%r zFIv)AH()t=IX=>yIcQ4LxMw?%t5b_QWa08i%zAa*-}Ta8uY(U}HqJW_IVJ!^|FU`y}@zn9Mr5T1)4X;Hb9Jfz7 zTiHpx_<2lqEOGVDQ#@O^-27@@xndT5X*1|w5BcBe-X|t!FVlJ~oOe(9;AxCc@$IDd zcpq(X9Q>i>a0HL4O)6)t2@}PE{ack7_kIH&ruoa0eaS}Kmg;s(yCXOur4%SSodhs+ z!|l|-r{~KUe;I@y5RM2Tc21#GmmVOtm5)u~0)O#y)_+?>s~HF&1%(l|zm#Ny-~hqB z%FY`Vdvk!e01HgG5>TWFy9y`oZfbJe%fGqSE#I?96VSh;Rdfh9C<3_Pgs}So5U8Yn zpSuD01|0wXnlX^n&xCjO+I3p|@d3m*h@=WeD1k>W<7!(|RC9l-MtZm4$cL}=qNc{e z{HHao^7KnA2I7?q{Ig5n@TyIZy$v_D%};(WGoZJ{FG|dek6fbf4PN5;0}>k2yK){B z3ZTcu=HwKPq2Yq@2j{~>gAAy|mi;_A@`|0;L{mD)6f=>I6Eg)xBj{zoF(Ge&2sV-7 z7vB%MXBoudSb2d@T85pbhu4CvXDIikltBTak*ELi8a*asHZnPM*A>^; z^k_eKwVxhIP#^Yrem|gnnO0bgg_(KwOM~`QC_1e56G)|~vcHf*p*ndOq*D^PWK5hM zp5sv|8QB`r{qJ@pcsfn2XJqHMc^NKpTogj7~us8TqC^oQVuJ#WsAp z`LfF7k_N%ZA+_pK3zLpK`=XHAL!XSWIMJ&!|P0`Hbgi zxi}S&U(HTm94X8%_c=D*)@)qO`Ll^p-8V(a2(4GQagATwbG$#4+yCIRDW$^5qex7|1LtVBdzC^FBO_MAG zI9pNDMy2jTB8-b-T7k!v)WWKGFedIfJ+%N2DJe`$z|6JxLszQ@TcD5$)nvNi>1IGK zTWHLWaJe;13xor}U42_GU@CtoT0<8_L72%)RVX_fU=$qL{n-Oir05bb`9?d^1 za5o@I+Fg)p=qdIA5cA--Xu>FFn#TXuDq<>`ig`TOi@4--R*l1bg&nS~R{LS(_bx&vti#M|zv5?pZyNa?T zrF3_vD5f53x(4ec_$62nJB!25mFX=q99O%YwUWocFqOwNVMr7g@9qtSU)#GL6w39% z+d9bv$d}pV*YNiCQ3DA zs|`mCFsiN87R9KBEz!e^3T3}OXEhnciB7+<#{E8pW}bZYEGadT_yU5gQkW-R4@^P<0e@{qAR;%^DM)>AWIsGdMPvrTPt(vdIrpNbA8Y zA9bmkQbOfJkXV-Ni__E6*nNvQc(NXHMz5*RT?z-9y#+$~#!#q|iu_vD1D0w*2vA2I zX|1ZH?o#pX3xA{R9;%N<_Ti!BkuT#8LWksyiT6cltBOWbY$WgHsL03~4U&qFTjv{s zU+%*NE`g$J+;VMT`A#|T@=NQFTpb#jH1jK8t$O}t#rL5-oe3l8^41-Vh77Ex$I9kR z%f?{o^@vEpIMrl$C%ys5Er z5;IPkp^d~owmIY}dx>CGF6e5~L)p>ZyiOsp20CS2KyRUeuGQ1OTyBs4-ehD64^Khf zFlVbT_CT(bgY09MtIHc8(eG%`iMz|E=ajT3&NP)LoQYV)oQf^yHnh|22ul4rQzM5p zAoh8gV|9>?m&%IFw%zOLq!Ub)A~sekCgFr12DNWFsj>yf0!@B|nu;8akfIiF^p+F?iQBL((>TAySzd|$M%!LcX;DqbAF zZj*O*UwT}njCzWd-&nugemtPT-jgu`z;aI`6VLN(=-2 zEW?e5Nr8-ZK*MsD+%ZlzPw#(;vV0jSq44~9g9^%WJId`teX#p@H}42Z{XozNKoeNr zzkBbgn|})D{Vx$Hccek(*D4Z(;4g20j?U%m%CI3Ie0>0VaC5C1iq0=Ue~a5_AfTq5 z-}w1#6+Zl{E~xb`187$dzb)zVO@me;O~f0VdK0k z)=9g+^hs+E5o-xr(!@1^nkO-0)+H}Gr9f=^y+d7R!L{5FlgX$=V9C9)^MzqyuMjM!Y~$W57zKfZR#}Q!WOtzT6`?*yM#g!3CV#K5tj&QFhWs`J5O%_3P<^&kw@mCWI_ZV>yC`(eP7bX#LwInA;c z!Q~rI0%bpWOn7$63q(y;k^OamjozDbuq z;0MR8hlhUs52pCJvOj^)eO%olE?)MM$4h;6SN7_qCi86dBuCtcEm_s4`RDYOW3jaC z`UJo$|6Wv+fsMImKK+aBrzrUG8`Azl{N3p9nz^D$ObomN$p8gPzQF~OmXp)5Qjf(gzbO_vdz5#T;rB4pLFf9%dXJ3qA#)2$n3+f>v?9_QVBL>Ul43yQ=EM)x<%K zN}Qsazk}rq#ofr%@q|^dOQgrPYr<9yZ=j9a#)u8uCN*hB)-sB;Q5C-eM7O8|>Egq> zsMfDHTJ-F#kw05UEz44Ym+FEgA1&Ok`R>ya{-uZF#)Z&x5ZzaXFFx=ToE!}Ka$c%_ zv!bGhelWUbYd;}DLXxAQ@4#R#GSFb@hKDjXTOJv_`_tpOgc=rvN#qxcH0Y%AWsRDY z60#mW-9|6<#Y9JP#Cxz#_Y$s1b90q7G$UKd1gvG^LIqmdsN#WrTO9`n+mm@|BrUIi zWFbrXZvf^mvG;SuzHF;FcxWQPx5sOU{UtA*~U6>`ve$;)x2dlo= zM`2~NbgY|wK)^zCBu3CEgh?w8$X&XxSeBADm?j=syy7G+f8J#mT(-U+5I{+J7;l@u zz`?bY#bF%?eVw>SH;lbUkl9o60pD8Rt8-;h(JF3~M)8TTcr_0<`!tij9(1IR+yd1o z2PGo3)%^XqNiHW~b5Vx+Nlv+}ozQV$n-V2EAakj^FHLq?ALQ*X_JFuex+j$cJ~Ng= z4f2oD)6fU3R6ph?4=vwjZn@=K{f8i^wWnphsZ%>2s(7IS{l8;to+|ftdw_D#Y|dr0 zEU8NULKhdgB=!$+LPgB<`KF5nwmh9Em=GFB>CV+$h}-`S`D_(&H{hs20^V;Ujdf0u zDocT@6sMP98pqV(Z)L+mx-rxAUtj)7**bip4un|Ec!l=#&DGOMfP?=}FLIy8bj!)kere!#OGqG8z5@`C z67+_q0S-5R2(dFBhvgmv2^~{pN55XHq#Laxymq0Koxb@rUG+aKNhH1jY^ysC( zd6D~V<{JV|Yup0>M~)nvLQWynB-uLjKB)eZT_Zb105b!RV+bQouZy!My#+C&6Cv|H zf@(0P#0csCM9ka;c7Sj#2=CLRWL6CZP*|7ZM7q3{iH+!BNFd`F*Gx@AFY1Urd$lyM}?q;ava7Hpb8g0c@vsw#H2tQvPKGd920kd?le(16!hc0nrajc%+M zV~Q5K4qIOL*vK|r%!rsxX0oP?fXXkvar<#zx;F@cvw(-M>r{R4#trXx&}rmcZKE6e zZ9|Z*&kvO#?31o7I6KNfKayzVOc+OEZLiIcWVavv z6MgYUD?t%OtMY^jCq_IFT-^VhLw@u9_$&9x^rh@~84QeIGv5K@(E`y=Jsk|iz!dyC zdjeym6f#!@)zB<3+vt+ z@UfWrzN*2PJh2$x3a541%C(KGV!KNBiChIgLPBJ;+V zv3MZUIpvk2YQksi7ccoFPQCg+L#gHYOw-Ld+*-7eS~z;Q&27;slb#H(a2bq*p?eD$ zie)nc@9#jyqmO#6I-3unElf-_E_wCq?0Hg6^esWbwKBpD5EQJ(Pxj@W*x7h#s}LK# zqy(_{ffaI|ypKHfXY7qaHlNjQFTfQq#Zl>`&p0WP$@g)xn=H&%1k1{cxq1zO4Ana! zy_rccp_-9z2?MPNgbTR(iNBxz>JH@Hue?u1RjSt4wG;1Pi*1o}m#e}CaH6DQk{>&~ z1YFKY#9pFDz0c_V$;oIRAzxcH3<@EdU6v3DZ4bW)oyW37MiLZ{SZ_Yb3dpM&B*TsW zTvFFS9VqD&DdX`k1Y!VC-< z%(8Q>{HL|sWn&qZP5g6dzJh%;G-qENua5{xYD^I7~b$;i_;rk#pj zm~;;r9?D8)jhJV3`xWcIWzFxM$Tjtew~RKEiffV3C83CG*}@$8FN$=_yz>|n09OO6 z(TgwCq6!@)E$pJA?3{G3FA@X6 zvG6Kcd;&^q_&qrW1><~|V4x}mJ*Iv$^AWvVait`-l3h{yYkD%Hvfj@+j0QOtJ`#*y zP`XM|_}kuM#;8$+8wganGZA3N#j44-M(NNFEl~%B+g5r5u@g5(l@HEiO%)za^8g>; zS7y0Ihhkl0edME}@(RKIE_za)B1T=*14a4l&yMCCl-S{BPfc z_=}qb@Tn`s=OSzAS|hvADre4}{aD&N#n#CXBl~?t$VQgcJQF<>7STK9;iL6tpMnCa zm>(B?+qhfQ-VE@-Sm;(1Tkk|A)5h$IHPaXhgq)gra9$}Va1DG-aO!C;B&#f&Z^+iLVdDXYg ze6Ty1N9gt#%zYysrwlS;*{~i`T3^?d*$G`$33^?G*=$Rs7{H|5YSO}xC9J5pJiSxAl8C3b>m7k^0SJi$t0aIyJH|u2Y9-QO z<5M7<$Y*tVpCTlBUiOQc@R@mnSNy3Q7C)}2NVKu9Xkw=CvnHm2+uo_S0v;8aD2vEe z(o9{<YC-N=lT8aEgbLOOC82OeKYz8pnr@EJdi$y}hm~WC_ZFKV5boXZ zo${9G0#P}Pc4AlQxJK`KV!J_N5iMlo?Rm<3VU6zmjoMZLQsseur6$g|?waX{o&VyA z!Ik{%-J?Z0BJil1?9%G8eE}Kty-ro_7MUY(&2Qa{j%;h~G!?W@UwP)5k+wdPzU+7L z6)oj;i$_+dTo}pNATfysh`_r{IqxP^@pmSt1^DO!v2nw{SxW}}0$g9pM}YGY4~5Fi zeM2rZ^Va&W;MtgX#5>N^1wLHgmyj^!D-Bq-Q@sg$Y}WwdGVVa>*XFJV?^6TfDCt+0 z@vg0mX#Yr|mQo9z%paMDTi7P)v<5-L(M9hQf`@XUntJFtz2injG1e)*GY#2{{)5DE z`?m&viNQD93YUZ?<1om2H}kUl*UXZpp=F5R z-dB%5-T@8I;Kq9h4ft~}H31>As)CCG#4@O-0Lc=7cLX@&w)2bY^-Wkq{~vz=aOU6A zlN*;FPWXoQ^fF_TQNFutiaS{8u_lK^LTt{gP7y5nsVAQ0?-zX~f|{cYC|c5)x$TQ6e;S1{R16cWT6`EGs?#NF-K>90smsfrA_ zC88_;M|!N!ub~J5LI@xT zNEd+sApxXH3B8AMA3)F9`|R_+_udcpjyuLZ`2xvEeV(=E`p@6|&-J8Oh9DH;%LG>+ zB9cz3@8QH5!P-K38Ffo4;ITu-kW#g9@Q zQ!7bOA(q1OU*pMxbeB~&>2^y?v!=+7H<`&G%i-T=Twr-z`=hq0fmb+-rTq@gfuURW z!kbK0ky+8gV_WoSmh?H@H=)h z0p0dh9t&2;G~peEOL-|bh>e=8jn`wXTKCtStqI&ZXCy;QA=5^@>SlrL1(?88EGFxk zG1SA2ow6jFY!Wbr|00w!jKevu>F0AuAi9hGbbfgBww{!6qnAa zP083tRb)=l8v%GQ#QOd0iwI6z4lV63`7Z7{cGJmL=>jEl9qHwAiSYQDY2B&Tq;}XQ z)!HV-HS}fsB8b>h?GCff?dXq+>fCtkUq_3h`hQDv#_H&Rz(%kw&1J(c7jYQMZa?p8Y~?KA}f?{#t5LSost~9)r}EY-;nT zm`SFKvsckkbaeb5BHyq~xP8y;n|wzSce2b8iNyh9OotX0r&IQwCxo@r!q;K9O;uQu zy{=?(jyIli*4{N&LBR>v?v%t<1~jMdo+m=;t)_X}8`o@P|Za zW#&(sW$_5yHPL$K0|FJr@@hmy<(-tKrp%5?Q$9is!)K4bi+|B1D%wa$Q8?fpKgZ+e zQF=yMv1Jn(l5%Mo;SX*<3Y4;ZE@Qa@Mnm{Jy14|q5r{7J7~O9*p;65J+{2hRp(cL8 zZV*8mHPv#-bOBv`tM)!8SkIm9RHkEL{)(YKCC&_M_ zw)KC~Ku_kHmM1i;)^!VHUxw+Lv;oo3Gms=@I9>cZ$2k|E#ugt4suB_2DVqG7QYmu2 z=_L0iJG0v9fZepK0DF75q$ z5VP&4!3B%+JTiCP2df@+!J?0h9yu|pZ_MW}fTy@MFKLD!Bbsk}9Jxn`CiXUgp7G@1 zSG^da^|yxDI91CO^|EzvrSR-Q02*VGXoBHmj+>`4@?-i0cMfQai!5X>73gfX`_)#_ zO{#um$WR{bpjzXlLa8^BqKb~eOb)VW*xX)&U#d&6gFmO!gyBmrn>z$lCI zh@Jy{AfdAOU5SO2OD9`uCb257FvH7`Ap_H|5ccopQ>*u?&Q|`y4EnQw?b*9!1C>RL z{)QWje{)5QXn*{bcL`tncj*4#ML}0JqN4nKo8(gfv^lzsrT+{k(wmYzxd*gh08J40 zj^)fYiw`*o5zz?Iz|n=k9t|{T0aRuAI*!u+^z8Abr#__9r-09Y_=?u(M(1ptej3>c8V!O?>>H`{33gS*}{x_mZdUuqN2J0u|5)v0r2K$_S1B z?DCh;4ndhzLkH7t9Y9&PJ_T7SshW98*;Pjd_upr-`lXJ>!jA$Uf)j8(s*|M_x%5F% zdg6RaM}QUJ1bp)@@n%%_^R=X9n>omIu2t$-7L{tVUV^j8^nFSW8O%fMirpv*8airZ zlSv(NSCCPG0B#CyyS8`4hx0PlJ6OB@iiK?CvtPlkJHA8#3dKH*Z-J0D@`YTg5!gm8?CE_+vjLVL$6vDSv93 zTfb|3TBN+%&_PEVxQN8mNkDf!yL6>q>4}B2T}Oi`nfIde`^cWcR;em2b8K8*I*dVt zg5Fd#Lg_kSJ^WKKJSpVkB$p1JTs47Q>@Ew(Q8SvH0}WX5gtM28-}?>PcZPKTVi24( z%@TfIz#zUzNoqzEMY;9vXXjf>DXgN1 z<=%h_<%7FR+HzQ^GqU8^%oTLp1T~eoD4_W&DqAauv`fc*dRW7wz z^Fa!gioAzo+2?6ug`q=>X85zSx2}QkeVp0#GQ``yqB=P5-IK^crl)Ka& zq{Hz#_OSS0EjH3#BW&N?3Imj@`Sn3>TqkLYMn5b)hZU@`?K?AZJt0G{#1AS2Kl$>- z*oBwF@`PC-h?s92mD|m9hoiK(WT{y*zI@`c6pm^-F&%bBxK|8?3GqC+_4ZmSuGP8> zgHf@6g};O@gC>*2rQdpgoh=CPfaT23{w%KRk^i-v1f^KKedH2F%e#3o>nDMqA zktwGY71j68t|LI#z9*d0{!R5)+J7;%TAzQk+HohwHJOo5=Eywlim`wXUD0mU31AcA zGx$-B){M6_*>>K(m^(RFj$M#4mtBP(jqBOBax^z6J{yANe_0Q|GiZ@)c^s)_8t@Kh z`)copm9jUbw=u=&3Rd-#H0Kc|0Q}jW!}x(hWq%ecW8hb^)lTf_TMCx9#BXe)`I=l+ z{mkKc_w|jxGi1$oO^Uy-<4?%Oa52Z}r=}!3P?dec+KfgE1eLqe|E-knHL(Fx65GmA zNzp06nT-?~yJBLqW~LO;$*Jl6To*A?4;o2V6px|5UgxjOIH2t1KMM3-es@+<7PKmR z>4@z#OHvxUpIa7jQGSWu3&haZPH23jK^vqbImR+NPGfR@XWX{a1CG9RqPQZX zos$r+Kk;8JQIywUTFFTii5W^?%iL`{YP607SaIY&hs3LiztjlWenHK@)(A?vUmO|n zJ}i2aR-7z{FF5{LF8Ec1W~@K?7XRmp&=;4j%<~;|vq6%gvaJ>~RF}vo$X{tZ`#yLD zXg&@*97}mv_1@@k2~%wIacE5zD=L^W;|QQO)z_uIU6CBF7x-0!6`srwA5-z{y9FZQQJZ1`6+urd>pY$xV{K@-SJdDiTqV)W@uU~Ag>+QEnwjZyyAK8Jd&U&h0 zJ@cEYqEB^~%0T$k?V&gH4sf6$v`mBC=6MHXuu(en$T&wl&}hNb@s8Lwm=b7|&B*OA zE9SbzZ~i6l(5;@AJv9k78i#YG1W~~SH4U8{gpNIUd{SuD4>zOBbz07fx>V@QRiwil zE8$+MpAwSocW$w+UNPTEWVY#K$Uv}(q|^m#uyOF(f%+YoraTaAnqP_veVx@T%9RunM;gYx4vl9Pi><>DWsUdCHa_fyQ%efNBcGZDEGAuM1xO zZ9wO_BD>0$EW{ilr^d?7#3hLJL0BuF0EwG}cG;8UEJ*9cr?;BUWz^oeR}d4+Vt%#0pjXq3C=n_*t+U^U5ua9d&b4b@Je>(5D3?Gi}V->1uo| zex=WZfW5<$3tgufq%IRjDnD(7M>RXu{y~`H`n<~%$=p8PboQc(v8jpLl|;&$>AzV( zs4y3Xk4Y<69NGx@`nWzsew|ojZ&rv^XxJBfp`aQJEzbGZp7|o1Pv{!0Ti|%Wsb@KS zK0_*2#lp6lziPHcl{0CWC#O`f_??8&w(lhEopIh_8n(2Xw>lFQ-Vg>Gs@T<}brs!N z-pDsgwx58k@+Kgyw}6RU!5%VYL)*oqf@GMPo@jU}1;@%i%#zNElhX|Tr2$@uIqNZ% za*)Z|gWhY%tv@K#qi9mMXfAfc87sb?OJ3rQX(KC(>bviv{4P-2Wf}6o+}H3$IW#- zb@Hj9MbuCh*BnVQFF@lPwreo8p0@>3%Tp}!S`nq^tV486sl6olU&jIW%v{tme|@tVjB zjsq6FLN~J{Wn7@O&RH>9VLq#r`4LFPujBlD%3t-=^6eoQkQL$)vx1okI$L$(l*3e8 zji3;Y4+?Yj^pTNCu3{46YTtm!-JZ9hO5%7RDeO!#FbZrSom*fU7Bu?kr_XM{Gk zRqzLEJ2+ick#9?rGU4zOuJZr!Vo)2TATatWy&EL?*H(o##+^!STA;^DEL#(bI zyB_J^xsWNDB%%C6T=!yZ$&E!avb3oayn}JhJNu9Q2OM227;>KGc(hH7XJi6}!y|JZ zmuiUxu!R6sQoW%k=xO?|QXv!>@Ouk^|7BhCg+!l~wM753SM}ZNLDJI81N+}Ex#^uT zXDckf7}vNDJ#LgR6z}-7=W4pgW^4x>2BUod!!k2xS|ujS=iGfS#OfBdq6vh7a51nA zT$seROb7qQqAKE^u;5yv?S*o)u_1}ObMU>d4iI5J*?Ya6nk~g#gF5J$@NOR-9+3x+ znSt%gf98;pE9sva08C3KOO7I77y^(k0#bKFw z?h2R8266z>+FZz1iP@U$yvq)?D&$hm=#l3G*hv;QYg=Mu@`$Hzfm-*z^`Cbnn0<)| z;G4;@a#MKCuhP2w#9i$u@g~-M@}$cyl7n?%%r50DM)lKudNbAP6gs&YMiDgLwfT=> zio%={;h=s7<$j25$Jv$p;IxW&-En_Rm29qJeF{Wh@(hUcP2*YV3vXfcYCTkq>fZcP z2X2?~y zG;fs7B|~o{2{9)AyizD!#>5_Vbh&oAGaSd}4CzX($Ha@pL^`xdH$`N``5x4LvQ~c? zmS^o;2yaa^5f0XMV0o&UQ2umQ_`r!$2T0a|oBe;)kWnu!_0QcG48#mH(iIef-OOyz z)!EZ z_!Ga~s@b;vP|sL8jWri~U!L4{zjS`o?S_7w1%w1wcl?wWX;=My-}aW6<-hinKxZ4? z5^#EOlH>o>`@cykj-zEQ(W&jjAtZQ^e^JzNuWTUHANRn6n5$DolV*dQQCzEjeXa4K z`h%>C`mF<6*7Otpu`Jd}=2X403C$p~>R|FQqpZ?CDKDz_PF2NBa(+rdDbTe~+FW6I z%%6Q+aJ$QAhOmG6^mgY`$I-49Um7INuHRo>>tDJ-9Pqg{ z5TdMU<6+@qT+fPn3{48?KyTb8tsiHA;ipNP)=xH5h)$K}`6*LWH62(7}%# z_lAzXN*p`R8jso-Vs$Zm?m=5lRmt$Z#eMzf8f||!r;GUx65bYX3-`9oieLndF7hto z5*JZ1U-gk{1ov7W?*f5IZ`Q0*SI<39vU|~MmHdkv6#|RT{&W^4kH-dzRDQq(W1+7ChzXT3wiktiC7daJL$u+A(Hey~Y zfly=tH4@rb*Jvh8;YtN_Cr=(Us;zoPGlJlP(A|O#UwBIw^pn#&OYcCiipOK8aU~BV zrdvo(Hv)!!?PlNO8`evtMQSDBE-=n;NC>RaFnmhYh|7N61VKz0Y&CRz@p(y~8|RH@ zLzlS_2jQi{roePmG*rlne$dpX^By6AY3kE+US(ieLUN^}1X5={Yut-dM^2+ySu=4+ zekZ8M?W1my*UNFqN+|Nj)Q)OeCBjg|xcV8}v503$@RAk&I;QcpwRoN zoc$gVeS_Ery9&3mn%;{_Y+Rx##J2Lp?Ma;u6&TDQhFU~aEumG}OlP#i)iMDINm>W9X9xtOh50gq=ac_8WN8xg25Kkikj=^Pp$Bpr|Y{m1ZcmmzI_a)3um;#-cv%LT)_jg%q$V;x{4X#<- zu~?=qj((*`FfzIY2WiT^E%nfz4s;IR;?kyq#Lc~FH4}Km5FKFLwZ}RF@#qo_aY)HE*)aB@SjW&-Yz7b!`(rV>?%#!5Mj}u@)Et z8-aoNFH_V79!mm=TWFTlX|-a3BFf#DpM)!uU;$fM6{~NVAjNYA#)D5If;e)cJ_j)c|A+RYw&za=w> z8;NKz8(%qPOuw@J&E400Q`LCRf=9wIh2|n1e6+|hw)tQupl>WAb)j#Ud7AZj02(SM zfF7hPvAz$t&Qa4nXwSmdWu6O?xs*CkR+iNo`6al;?YponOosJl*VJ{G49bA?JHn2| zW}V^4-80f5bh#d7`7B-@GH1w;T~`D*@Zl0$>DQPDg3oFeedx>6tP~l_n_Y3e`6ggU z(&4A^jS#Bg`LOlLjwuC4!uGmrQ5jrgcuCB=M#X;0;yu}p0(~SxRof7_Qlk$>rwneW z%Mxb`8Q*hVT-F|)vk~jJ&8Z}M?mph!1{>P5jz2g%>c&Ad$J-0S>N5O%lomT!}?T?8t;ioQyuYk9*5Z0 zu41sEqmLnAG!m#Z_?o0*iuXb!CNg(VtD$t?702i2jgMA~6|;kdpXux5xzQH?fvViu z`T0$jco=ne&()UOSI^`4#1TZS1ayaXY$hI?`U-0i3Tt|js_?x^flSu zTYKnSuFYJ$ZZS*Wxx{p$en6~fBt6w_u-7<(giyihY9Z$6s9Ing&FGZO)EC6wZ z3Rr1IXpcOf{#sZ4O*7O8d#W73WA9jhKL!k!cY#~v{@}fpmLS$6`$YZ2ao;mzbfic8 zWV!t%-Kp(SlCwmQ^mdqN{LAWj>WS)+QwE$wUGKIi{lW3uroiGE;r56~@LtDJLErx<|Ge(h9?u~aU58>2vo z@8iN#!yjKK;j>@7O3O#yr8smbj1(C zhX;t#fc3i!A!!N`i2kyH*dy$J1b+4iyo|=5jM9QNL@f^wC%}FVbs}cJyLuIf3JI_F ztb=&{P`r>>e<7U&s-$YslG1g%UA%oK1vh@zfL?&#XL?2`V|uQCp)T}Bs?$lg7(<+wvvQ|{d|I;tj2Q-(Xd=}hUQW^30IoaPFTjh{irzcx zlT)#Xi&n9(E~I*Sbx(_)Frzy>it}=nOQ5Z*__W`+hJOReb42P^_hP)~%QUBJ^A`b4 z;h>lqf&bYvBbJ|PJl(eHVH^8!z-9{#qEn8k#aUyKd^m6BNThqtCf;XfQ^WD=mIlal zc6ob+L55+*(H=W*?}#nnF!c7RkDTz5cT@7!8>$k6UO}Q@{C--=azQBe%IdXZV|RDh z@F9ZQ77kv(@8S8nhDD@VBqHtY)#rQGbX_K9r+34$pk^ErdvQDlsls$FxQHS!y+oZ` z)!~nBT(k#tl8Lhi`6v9G0ktFlF`Ni)_*-v{?^vx z(?rXx&i|Aab89_Q&&b`XsT-v%?B>ey>}*|3bI6jh{~|5aA|{MBL5Mx47nX~wK@)1R zUKLu*;x)?rk*T`dwiO6oG51nVO z=9haqR11dZ`U+B!N@g4+Ztm`h-F3mLL#02urkgjXWyJWzcQ-ACBJy2E=SPYUp!oTW z0J_X-i9?-StfQE9jpKfVPpT`wM7tYxt5Pu@&vt`5=!!w|Fm?&-OE=HESa}^`*G2d^h=!FjO*eUGTrbP^UXpTE&PMwhG*gT}cWz;&MtBa#k@l1$j z4r#ArF?5A>2rZztR)ECesHa{n=&U&Nm!kH03+eg8X2m!)pU9wB{C09$NNOA1W^6G*!vg8@0D`i}~ zlXD2F9g2Cp{(Upe?3)9e&LIMW*I-m&K%U};&|J=NXB1|+a^FR>JsQFZL2SFe=7E4n z!s7VBexAhhU8{_ZdFpeBB)D!T+Eyw=dZ zcM0}fyfeqZQSagUnwgt}fbRL$j&IA)o)z~Cx7t~Fmgweog#^*L&R?tDO+#l?$F|*& zDa#lTpdxq=3lI;5Oi=bNy@*?Lf~F3wW8EZX-4=8YCq`0bXI*?#U0&zBJw-G|BN%Z? z5ZIV;;&DzX%4;Gd%)R1+g!JoR3@kpXwGr=)H@dSj`LZ35Pa5Tal1~xdPPzJq5R`gC z^-xE5Jfm!bEWUDHH>(sZUK6sh%qRjM5#2GYbiv?iR30e|tc;*?isUjN-HeJ$ zUAzBPx~b{+blK#ULpkjEwzk~ko0DS48yaVHMTT(_9-r3cM+c}!<5l}tJ7JX0pgt|6 zK~c6v8_I{45B+bH+qs(G%1xrKq<%?d_bA~bi2PLgX+BdBc0O!l)(Bs=Dzg+2AdZ(J zY-yqshsF=_=$Xe8gom<8v8$jZ>u=%{Ubb%GI-OQbNj3sHP_)6luX{@+@BwlAOCgE< z{ZMt|y;jxk-~~_d9xUc1Hblv{fYz*qR(3LNJ3<;$BC>`~YI0s~=>s6Ag*oa-TZa(iXyV z-moKXbh16@R7%xGDk^ZN=%6L_G?C|4_{Fjr*K2H^`aO$nGxE^jz*VVfsMsulw&un8 zH4$=>@K~0nzFi;b-Lphvn$wrTSFYFGR&mK#ZN@?PNGh{)J4D?p@ z8~~r%2}nslKvWL?>=UnxA@&>?(ST+&55)Ht?{XLdW|xzGAd}tdp-66jN> z#zHxd?7NTHjxp8|-Kk086nlxRJ^a{D*wJN^DZXzIJ_hVdk|TL|{z~fAd-Q25=l4MzwbVyFlI6 zDWZ3X-&z&dQTDv6ZoPPsljmgtb@Cjm=J%Z4NF7Qhg)>} z@ooba>P22t^jUHWDUwBO$;SQ0I6^n700P8>^$vG@8&>yD`XlmBbZ%))LgGhf3U!?-}3pdtt*!tNB~+ z>h1b&x7}jxhB&WoDX(uO1s?K=u52ud?8F#>y;qXZKi9K5L@o>hNkbPDN0j^(T)`<& z^E}ZYw}RBbr<58jkpyz^DOHUa*&E$avGp+hvD*nlwZh#)0cwF$(F^kt-aN@Q_b}i1^ zb7pyWAr?`bFG8vlrb{;47=LMvbyx!R%@0tM8=lQb_+c-sm62@5mk|BcAkPk4K-cv_ zGn?abt%+VsEK_Xi*9!i8Wk$!wNQ4NEhriD7CLJxR7O~aA{!Mo4u<8;3;_wtRK_XL< zHe-YH&U(1;SSnU_T-B=A=cRYmXMI~JEG!4A2Rs&=b?|iFB4F>Zt!^WSxn^S#3Eb$m z4Zp-qaVmUlY;(puVSz%ZdcPO_7vcJX1UPQkJoX!m#Xc1}5;mKa%L92|nj2tHrR6v& zm{&&hR|{3+hkC%;_u6LXNT!BtEMO(Mz}ULYh>^Oo9qq+{J|lN8dq>OC54RQ$_CGfv zI`_xE;61LK=*xzth6UX6s!!gx&LS1lozb9kLCr<>a8<;@XPk*KQ!Ju-WZ_&2>(pw? zXDGsea3q3X4*FRVc7GZRs2-5;mbDBDBl?Ok>{ipwk9yc%^|8vREq? z^||1-cIDW%)0HzDiwAUo=C<959Lq)pKlJK{6m?-tw<{Myb~6pj?! z@3eSW;U}i%8L;%?@_E(SUB%qvJs0syDaaxMx4|b_lvF_KpOj0uZGj8TllM_>`Ky>S&Pr%w+h% zj9-FJC7~|27jGg^?>aM@vM_2RzV^X2fd&9>=nUD640mKdkf6P?+ybKx&a{5!wDCx< zjB+*rS1*(AE4zAl&-M@GuDQ6)Tb4XSlCYdmz>SsdAL;KZ_o8}4Kq?#a{YSJqB= zi5t`GCz5T+%d6kT69c9HXy+!u5=&wpxEd-x8mM6CuDNGs`x~-bkSLmPn}cht z!f9!M5h$`T=gug?Czi5^PX*Lx!$iwQ(MBHdOEdBiU%a(m;sXNc=xZh(9kWEs* zwc*jjA73dC)_-h+eGRXSygi)V^Vr&2x(>c982AsoXS2qxSp9$_MuQbHrEH;*xa36j zPkqOp;_7fEJeJauq&9)Qt~FlnG?(kVgg5aY2Kx~b=ZTgVZ=5ERtNCs&(aqnM>6@^_iTvcGP!4*ad-mhF#q96>GlMB>%& zkmDWWB!&QKHMV!%zq>{Wzb5ZEK?pxqbB@dUwSY{UNjlos{U6fH`%=Bc!S+NW#T|e( zj!~|N|FIkq1OPXp?WvjwqCH38|2sv1r@3Bx?3NMH7|!|OO(M%L1mgN5VM|0b-XeU5 zDEXSj5!ilr_5@pQ=lDlol+7LhSJkb~$^mqf=!IYM3Q=-fJQ{3+ml$N3AyejOSX~jNy1MjmmqD@jCOk5V* zl-IY3+1FTK*nGt)I+*VxnoPAfwmfs#@m%DnJSgeJF{+(JjpP(Yltibnbxs=GtCs=S zX>S`gk{K@5e#?0U6o>(cSvL1~;)mNAFuTe(tgJ2FfI z*TJKkH-h)J`+V$w&V{M|Aiw5$N5LITcP1jkj$G>W4}W9=aJKxix_%7Ykq+xe1osDl z1>%lsRk}2EPYVT<1!ujk!ja6LcP<13-4eXq5a5oy4SddWfh$MdIcA0XwB6gAab~oF z{gl_;Hr*2+drkI;bsMvYCI93lO=SxwdF_z&;|K9+jS65KZMz8uo$}-~)ETW28`doq zQixFM)Dz8p?m;)oayy%`7Gtwl!Y{U(e-@fCy=lQJrc#qgcbV*7yyc8c27y|HfTAw+ zn1AeGlg$|g%5)(^44_cL!bgpRf@NJVh?FR6F+<>QyD#UbWuFW|;3I#u!ygjo8G(_i zI(8Isi%a;W{G`0iYEC zhzTlqRA&xMZCC4zqXcWYfZ@*!|9Fq+tHevpM2FiG5u}>_fJ4UL@C)HWy#(#0qhS%h z22AF|t;2|;4zJ~q)yqef?b2T_JE;Tw=gxl)$bSaKf9AsPy5Rp0@u21Gn1rt&Eimls ziyMw>xJTB)W!sq>R>0rinCpzue}Cu4A+UxX9=?FQ3k05c?nvIBCQ`hw4_B8l&IKMJ OdMd3XRU~Qj`hNgU1?JEI literal 0 HcmV?d00001 diff --git a/docs/pr-screenshots/issue-251/network-proof.json b/docs/pr-screenshots/issue-251/network-proof.json new file mode 100644 index 00000000..8f6df3c5 --- /dev/null +++ b/docs/pr-screenshots/issue-251/network-proof.json @@ -0,0 +1,35 @@ +{ + "generatedAt": "2026-05-08T11:19:39.087Z", + "mockServer": "http://127.0.0.1:4517", + "terminalOnly": { + "url": "/ssh/session/session-1", + "requests": [ + "GET /ssh/session/session-1", + "GET /css2?family=IBM+Plex+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;700&display=swap", + "GET /assets/index-CAnIq_BE.js", + "GET /assets/index-6EdQ78LN.css", + "GET /api/terminal-sessions/session-1?_=1778239175378", + "GET /s/ibmplexmono/v20/-F6qfjptAgt5VM-kVkqdyU8n3vAOwlBFgg.woff2", + "GET /s/ibmplexmono/v20/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2" + ], + "websockets": [ + "ws://127.0.0.1:4517/api/projects/by-key/octocat%2Fhello-world/terminal-sessions/session-1/ws?cols=145&rows=42" + ], + "requestedProjectsEndpoint": false, + "containsTerminalDomHeader": true, + "screenshotIncludesProofBanner": true + }, + "dashboard": { + "url": "/menu/select", + "requests": [ + "GET /menu/select", + "GET /assets/index-CAnIq_BE.js", + "GET /assets/index-6EdQ78LN.css", + "GET /api/health?_=1778239177547", + "GET /api/projects?_=1778239177553", + "GET /api/auth/github/status?_=1778239177566" + ], + "requestedProjectsEndpoint": true, + "containsDashboardText": true + } +} diff --git a/docs/pr-screenshots/issue-251/terminal-only-route.png b/docs/pr-screenshots/issue-251/terminal-only-route.png new file mode 100644 index 0000000000000000000000000000000000000000..f7bda74dd1a474c962f1c3acbb66db63e5b1f837 GIT binary patch literal 52739 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

U$2zeQyu5vrS$vVJLc>xXkmF;OpRgo*F`uxO!kO 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⁣z30f&3WRq8L2Q(yN>L&1$9DP*jMdgoH%L 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 listProjectItems.pipe( - Effect.map((projects) => projects.map((project) => dbProjectDetails(project))), - Effect.catchAll(() => Effect.succeed([] as ReadonlyArray)) + Effect.map((projects) => projects.map((project) => dbProjectSummary(project))), + Effect.catchAll(() => Effect.succeed([] as ReadonlyArray)) ) export const applyAllProjects = (activeOnly: boolean) => diff --git a/packages/api/tests/projects.test.ts b/packages/api/tests/projects.test.ts index 15436942..88555eee 100644 --- a/packages/api/tests/projects.test.ts +++ b/packages/api/tests/projects.test.ts @@ -258,7 +258,7 @@ describe("projects service", () => { }) ).pipe(Effect.provide(NodeContext.layer))) - it.effect("lists project inventory from .docker-git with conservative runtime defaults", () => + it.effect("lists lightweight project summaries while getProject returns project details", () => withTempDir((root) => Effect.gen(function*(_) { const path = yield* _(Path.Path) @@ -299,19 +299,26 @@ describe("projects service", () => { expect(projects).toHaveLength(1) expect(projects[0]).toMatchObject({ id: projectId, - projectDir: projectId, status: "unknown", statusLabel: "unknown", sshSessions: 0, startedAtIso: null, startedAtEpochMs: null }) + expect(projects[0]).not.toHaveProperty("sshCommand") + expect(projects[0]).not.toHaveProperty("authorizedKeysPath") + expect(projects[0]).not.toHaveProperty("envGlobalPath") + expect(projects[0]).not.toHaveProperty("codexHome") expect(details).toMatchObject({ id: projectId, projectDir: projectId, status: "unknown", statusLabel: "unknown" }) + expect(details).toHaveProperty("sshCommand") + expect(details).toHaveProperty("authorizedKeysPath") + expect(details).toHaveProperty("envGlobalPath") + expect(details).toHaveProperty("codexHome") }) ).pipe(Effect.provide(NodeContext.layer))) @@ -368,7 +375,6 @@ describe("projects service", () => { expect(projects).toHaveLength(1) expect(projects[0]).toMatchObject({ id: projectId, - projectDir: projectId, status: "running", statusLabel: "last known: running", sshSessions: 0, diff --git a/packages/app/src/web/app-terminal-session-core.ts b/packages/app/src/web/app-terminal-session-core.ts new file mode 100644 index 00000000..e31f8197 --- /dev/null +++ b/packages/app/src/web/app-terminal-session-core.ts @@ -0,0 +1,35 @@ +import type { ProjectTerminalSessionLookup } from "./api.js" +import { type ActiveTerminalSession, buildProjectActiveTerminalSession } from "./terminal.js" + +export type WebAppRoute = + | { readonly tag: "Dashboard" } + | { readonly tag: "TerminalSession"; readonly sessionId: string } + +const terminalSessionRoutePrefix = "/ssh/session/" + +export const readTerminalSessionRoute = (pathname: string): string | null => { + if (!pathname.startsWith(terminalSessionRoutePrefix)) { + return null + } + + const rawSessionId = pathname.slice(terminalSessionRoutePrefix.length).split("/")[0] ?? "" + const sessionId = decodeURIComponent(rawSessionId).trim() + return sessionId.length === 0 ? null : sessionId +} + +export const resolveWebAppRoute = (pathname: string): WebAppRoute => { + const sessionId = readTerminalSessionRoute(pathname) + return sessionId === null + ? { tag: "Dashboard" } + : { tag: "TerminalSession", sessionId } +} + +export const buildTerminalOnlyActiveSession = ( + lookup: ProjectTerminalSessionLookup +): ActiveTerminalSession => + buildProjectActiveTerminalSession({ + projectDisplayName: lookup.projectDisplayName, + projectId: lookup.session.projectId, + projectKey: lookup.projectKey, + session: lookup.session + }) diff --git a/packages/app/src/web/app-terminal-session.tsx b/packages/app/src/web/app-terminal-session.tsx new file mode 100644 index 00000000..0d62c2c9 --- /dev/null +++ b/packages/app/src/web/app-terminal-session.tsx @@ -0,0 +1,212 @@ +import { Effect, Match } from "effect" +import { type CSSProperties, type Dispatch, type JSX, type SetStateAction, useEffect, useState } from "react" + +import { deleteTerminalSessionByPath, loadTerminalSessionById, resolveApiBaseUrl } from "./api.js" +import { buildTerminalOnlyActiveSession } from "./app-terminal-session-core.js" +import { Box, Text } from "./elements.js" +import { TerminalPanel } from "./panel-terminal.js" +import type { ActiveTerminalSession } from "./terminal.js" +import type { ViewportLayout } from "./viewport-layout.js" + +type AppTerminalSessionProps = { + readonly sessionId: string + readonly viewportLayout: ViewportLayout +} + +type TerminalOnlyState = + | { readonly _tag: "Loading"; readonly sessionId: string } + | { readonly _tag: "Ready"; readonly message: string | null; readonly session: ActiveTerminalSession } + | { readonly _tag: "Closed"; readonly message: string } + | { readonly _tag: "Error"; readonly apiBaseUrl: string; readonly message: string } + +type TerminalOnlyStateSetter = Dispatch> + +const terminalOnlyContainerStyle: CSSProperties = { + display: "flex", + flexDirection: "column", + height: "100%", + minHeight: 0, + overflow: "hidden", + padding: "8px", + width: "100%" +} + +const terminalOnlyMessageStyle: CSSProperties = { + background: "#101419", + border: "1px solid #3a4652", + borderRadius: "8px", + color: "#f6d27b", + flexShrink: 0, + marginBottom: "8px", + overflow: "hidden", + padding: "8px", + textOverflow: "ellipsis", + whiteSpace: "nowrap" +} + +const terminalOnlyLoadingState = (sessionId: string): TerminalOnlyState => ({ + _tag: "Loading", + sessionId +}) + +const terminalOnlyErrorState = (message: string): TerminalOnlyState => ({ + _tag: "Error", + apiBaseUrl: resolveApiBaseUrl(), + message +}) + +const terminalOnlyClosedState = (message: string): TerminalOnlyState => ({ + _tag: "Closed", + message +}) + +const loadTerminalOnlyState = ( + sessionId: string +): Effect.Effect => + loadTerminalSessionById(sessionId).pipe( + Effect.match({ + onFailure: (message) => terminalOnlyErrorState(message), + onSuccess: (lookup) => ({ + _tag: "Ready", + message: null, + session: buildTerminalOnlyActiveSession(lookup) + }) + }) + ) + +const closeTerminalSession = (session: ActiveTerminalSession): void => { + void Effect.runPromise(deleteTerminalSessionByPath(session.closePath).pipe(Effect.either, Effect.asVoid)) +} + +const updateReadyMessage = ( + setState: TerminalOnlyStateSetter, + message: string | null +): void => { + setState((current) => + current._tag === "Ready" + ? { + ...current, + message + } + : current + ) +} + +const TerminalOnlyMessage = ({ message }: { readonly message: string | null }): JSX.Element | null => + message === null ? null :

{message}
+ +const TerminalOnlyReady = ( + { + session, + setState, + state, + viewportLayout + }: { + readonly session: ActiveTerminalSession + readonly setState: TerminalOnlyStateSetter + readonly state: Extract + readonly viewportLayout: ViewportLayout + } +): JSX.Element => ( +
+ + { + setState(terminalOnlyErrorState(`Terminal websocket closed before attach: ${session.session.id}.`)) + }} + onDetach={() => { + setState(terminalOnlyClosedState(`Detached SSH terminal: ${session.session.id}.`)) + }} + onKill={() => { + closeTerminalSession(session) + setState(terminalOnlyClosedState(`Killed SSH terminal: ${session.session.id}.`)) + }} + onMessage={(message) => { + updateReadyMessage(setState, message) + }} + session={session} + /> +
+) + +const TerminalOnlyClosed = ({ message }: { readonly message: string }): JSX.Element => ( + + + SSH terminal + {message} + + +) + +const TerminalOnlyLoading = ({ sessionId }: { readonly sessionId: string }): JSX.Element => ( + + + SSH terminal + session: {sessionId} + Attaching terminal... + + +) + +const TerminalOnlyError = ( + { apiBaseUrl, message }: { readonly apiBaseUrl: string; readonly message: string } +): JSX.Element => ( + + + SSH terminal unavailable + target: {apiBaseUrl} + {message} + + +) + +const renderTerminalOnlyState = ( + state: TerminalOnlyState, + setState: TerminalOnlyStateSetter, + viewportLayout: ViewportLayout +): JSX.Element => + Match.value(state).pipe( + Match.when({ _tag: "Loading" }, ({ sessionId }) => ), + Match.when( + { _tag: "Error" }, + ({ apiBaseUrl, message }) => + ), + Match.when({ _tag: "Closed" }, ({ message }) => ), + Match.when({ _tag: "Ready" }, (readyState) => ( + + )), + Match.exhaustive + ) + +export const AppTerminalSession = ({ sessionId, viewportLayout }: AppTerminalSessionProps): JSX.Element => { + const [state, setState] = useState(() => terminalOnlyLoadingState(sessionId)) + + useEffect(() => { + let cancelled = false + setState(terminalOnlyLoadingState(sessionId)) + void Effect.runPromise( + loadTerminalOnlyState(sessionId).pipe( + Effect.tap((nextState) => + Effect.sync(() => { + if (!cancelled) { + setState(nextState) + } + }) + ), + Effect.asVoid + ) + ) + return () => { + cancelled = true + } + }, [sessionId]) + + return renderTerminalOnlyState(state, setState, viewportLayout) +} diff --git a/packages/app/src/web/app.tsx b/packages/app/src/web/app.tsx index 8c7ba119..4659905b 100644 --- a/packages/app/src/web/app.tsx +++ b/packages/app/src/web/app.tsx @@ -6,6 +6,8 @@ import { UiProvider } from "../ui/primitives.js" import { loadDashboard, resolveApiBaseUrl } from "./api.js" import { createDashboardRefreshReducer, type DashboardState } from "./app-dashboard-state.js" import { AppReady } from "./app-ready.js" +import { resolveWebAppRoute } from "./app-terminal-session-core.js" +import { AppTerminalSession } from "./app-terminal-session.js" import { ErrorScreen, LoadingScreen } from "./panels.js" import { resolveViewportLayout, type ViewportLayout, type ViewportSize } from "./viewport-layout.js" @@ -170,26 +172,47 @@ const renderDashboardState = ( Match.exhaustive ) -export const App = (): JSX.Element => { +const AppFrame = ( + { children, viewport }: { readonly children: JSX.Element; readonly viewport: ViewportLayout } +): JSX.Element => ( +
+ + {children} + +
+) + +const AppDashboard = ({ viewport }: { readonly viewport: ViewportLayout }): JSX.Element => { const { refresh, state } = useDashboardController() + + return renderDashboardState(state, refresh, viewport) +} + +export const App = (): JSX.Element => { const viewport = useViewportMode() + const [route] = useState(() => resolveWebAppRoute(globalThis.location.pathname)) return ( -
- - {renderDashboardState(state, refresh, viewport)} - -
+ + {Match.value(route).pipe( + Match.when({ tag: "Dashboard" }, () => ), + Match.when( + { tag: "TerminalSession" }, + ({ sessionId }) => + ), + Match.exhaustive + )} + ) } diff --git a/packages/app/tests/docker-git/app-terminal-session-core.test.ts b/packages/app/tests/docker-git/app-terminal-session-core.test.ts new file mode 100644 index 00000000..84d2a92e --- /dev/null +++ b/packages/app/tests/docker-git/app-terminal-session-core.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest" + +import type { ProjectTerminalSessionLookup } from "../../src/web/api.js" +import { + buildTerminalOnlyActiveSession, + readTerminalSessionRoute, + resolveWebAppRoute +} from "../../src/web/app-terminal-session-core.js" + +const lookup: ProjectTerminalSessionLookup = { + projectDisplayName: "octocat/hello-world", + projectKey: "octocat/hello-world", + session: { + createdAt: "2026-04-21T10:00:00.000Z", + id: "session-1", + projectId: "project-1", + sshCommand: "ssh -p 22 dev@172.18.0.7", + status: "ready" + } +} + +describe("terminal-only SSH route core", () => { + it("routes direct SSH session URLs outside the dashboard", () => { + expect(resolveWebAppRoute("/ssh/session/session-1")).toEqual({ + tag: "TerminalSession", + sessionId: "session-1" + }) + expect(resolveWebAppRoute("/ssh/session/session%2Fencoded")).toEqual({ + tag: "TerminalSession", + sessionId: "session/encoded" + }) + }) + + it("keeps dashboard and project SSH links on the dashboard route", () => { + expect(resolveWebAppRoute("/")).toEqual({ tag: "Dashboard" }) + expect(resolveWebAppRoute("/menu/select")).toEqual({ tag: "Dashboard" }) + expect(resolveWebAppRoute("/ssh/octocat/hello-world")).toEqual({ tag: "Dashboard" }) + }) + + it("ignores empty SSH session routes", () => { + expect(readTerminalSessionRoute("/ssh/session/")).toBeNull() + }) + + it("builds terminal-only sessions without dashboard refresh callbacks", () => { + const session = buildTerminalOnlyActiveSession(lookup) + + expect(session).toMatchObject({ + browserProjectId: "project-1", + browserProjectKey: "octocat/hello-world", + browserProjectName: "octocat/hello-world", + closePath: "/projects/by-key/octocat%2Fhello-world/terminal-sessions/session-1", + sessionPath: "/ssh/session/session-1", + websocketPath: "/projects/by-key/octocat%2Fhello-world/terminal-sessions/session-1/ws" + }) + expect("onReady" in session).toBe(false) + expect("onExit" in session).toBe(false) + }) +}) From e1aef591296cb061abfacc8d1ae6aef5c8feadf4 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sat, 9 May 2026 10:16:07 +0000 Subject: [PATCH 2/2] fix(web): satisfy ci lint for ssh terminal flow --- .../app/src/web/actions-project-create.ts | 13 +-- packages/app/src/web/actions-projects.ts | 104 ++++++++++-------- packages/app/src/web/api-types.ts | 5 +- packages/app/src/web/project-event-payload.ts | 13 +++ .../tests/docker-git/actions-projects.test.ts | 93 +++++++++------- .../app/tests/docker-git/controller.test.ts | 2 +- 6 files changed, 130 insertions(+), 100 deletions(-) create mode 100644 packages/app/src/web/project-event-payload.ts diff --git a/packages/app/src/web/actions-project-create.ts b/packages/app/src/web/actions-project-create.ts index 9c2f5eb6..680fecb6 100644 --- a/packages/app/src/web/actions-project-create.ts +++ b/packages/app/src/web/actions-project-create.ts @@ -7,21 +7,10 @@ import { appendOutputLine, appendOutputLineHandler, notifyProjectEventRateLimit import { type BrowserActionContext, withBusy } from "./actions-shared.js" import { ProjectDetailsSchema } from "./api-schema.js" import { type ApiEvent, loadProjectDetails, type ProjectDetails, startCreateProject } from "./api.js" +import { readEventPayloadString } from "./project-event-payload.js" import { openProjectEventStream } from "./project-events.js" import { outputScreen, projectPickerScreen } from "./screen.js" -const readEventPayloadString = ( - event: ApiEvent, - key: string -): string | null => { - const payload = event.payload - if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { - return null - } - const value = Object.entries(payload).find(([name]) => name === key)?.[1] - return typeof value === "string" ? value : null -} - const readCreatedProjectId = (event: ApiEvent): string | null => event.type === "project.created" ? readEventPayloadString(event, "projectId") : null diff --git a/packages/app/src/web/actions-projects.ts b/packages/app/src/web/actions-projects.ts index 276c3b02..67d89eea 100644 --- a/packages/app/src/web/actions-projects.ts +++ b/packages/app/src/web/actions-projects.ts @@ -13,9 +13,9 @@ import { } from "./actions-shared.js" import { loadSelectedProjectTasks } from "./actions-tasks.js" import { + type ApiEvent, applyAllProjects, applyProject, - type ApiEvent, deleteProject, downAllProjects, downProject, @@ -23,15 +23,24 @@ import { loadProjectLogs, loadProjectPs, loadProjectTerminalSession, - startProjectTerminalSession + startProjectTerminalSession, + type TerminalSession } from "./api.js" import type { BrowserMenuTag } from "./menu.js" +import { readEventPayloadString } from "./project-event-payload.js" import { openProjectEventStream } from "./project-events.js" import { outputScreen } from "./screen.js" import { buildPendingProjectActiveTerminalSession, buildProjectActiveTerminalSession } from "./terminal.js" export { submitCreateInputs } from "./actions-project-create.js" +type BrowserRandomSource = { + readonly getRandomValues: (values: Uint8Array) => Uint8Array + readonly randomUUID?: () => string +} + +const browserRandomSource = (): BrowserRandomSource => globalThis.crypto + export const loadSelectedProjectInfo = ( context: BrowserActionContext, options?: { @@ -79,43 +88,20 @@ const resolveProjectTerminalKey = ( } const randomHex = (bytes: number): string => { - const getRandomValues = globalThis.crypto?.getRandomValues - if (typeof getRandomValues === "function") { - const values = new Uint8Array(bytes) - getRandomValues.call(globalThis.crypto, values) - return Array.from(values, (value) => value.toString(16).padStart(2, "0")).join("") - } - - let fallback = "" - while (fallback.length < bytes * 2) { - fallback += Math.floor(Math.random() * 0x1_0000_0000) - .toString(16) - .padStart(8, "0") - } - return fallback.slice(0, bytes * 2) + const values = new Uint8Array(bytes) + browserRandomSource().getRandomValues(values) + return Array.from(values, (value) => value.toString(16).padStart(2, "0")).join("") } const createPendingTerminalSessionId = (): string => { - const randomUUID = globalThis.crypto?.randomUUID - if (typeof randomUUID === "function") { - return randomUUID.call(globalThis.crypto) + const source = browserRandomSource() + if (source.randomUUID !== undefined) { + return source.randomUUID() } return `pending-${Date.now().toString(16)}-${randomHex(8)}` } -const readEventPayloadString = ( - event: ApiEvent, - key: string -): string | null => { - const payload = event.payload - if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { - return null - } - const value = Object.entries(payload).find(([name]) => name === key)?.[1] - return typeof value === "string" ? value : null -} - const readTerminalSessionCreatedId = ( event: ApiEvent, requestId: string @@ -148,6 +134,31 @@ const readTerminalStartupFailure = ( return readEventPayloadString(event, "message") ?? "SSH session startup failed." } +const addAttachedProjectTerminalSession = ( + { + context, + projectDisplayName, + projectId, + projectKey, + session + }: { + readonly context: BrowserActionContext + readonly projectDisplayName: string + readonly projectId: string + readonly projectKey: string + readonly session: TerminalSession + } +): void => { + context.addTerminalSession(buildProjectActiveTerminalSession({ + onExit: context.reloadDashboard, + onReady: context.reloadDashboard, + projectDisplayName, + projectId, + projectKey, + session + })) +} + export const connectProjectById = ( projectId: string, context: BrowserActionContext, @@ -188,6 +199,11 @@ export const connectProjectById = ( stream?.close() stream = null } + const failPendingTerminalSession = (error: string) => { + pendingSessionFinalized = true + appendOutputLine(context, `[error] ${error}`) + context.addTerminalSession(renderPendingTerminalSession(error, "error")) + } const attachCreatedSession = (sessionId: string) => { if (attachedSessionId !== null) { return @@ -198,23 +214,20 @@ export const connectProjectById = ( effect: loadProjectTerminalSession(resolvedProjectKey, sessionId), label: "Attaching SSH terminal", onFailure: (error) => { - pendingSessionFinalized = true - appendOutputLine(context, `[error] ${error}`) - context.addTerminalSession(renderPendingTerminalSession(error, "error")) + failPendingTerminalSession(error) closeStream() }, onSuccess: (session) => { pendingSessionFinalized = true context.reloadDashboard() context.closeTerminalSession(pendingSessionId) - context.addTerminalSession(buildProjectActiveTerminalSession({ - onExit: context.reloadDashboard, - onReady: context.reloadDashboard, + addAttachedProjectTerminalSession({ + context, projectDisplayName, projectId, projectKey: resolvedProjectKey, session - })) + }) context.setMessage(`Project is ready. SSH terminal is connecting for ${projectDisplayName}.`) closeStream() } @@ -225,9 +238,7 @@ export const connectProjectById = ( effect: startProjectTerminalSession(resolvedProjectKey, pendingSessionId), label: "Opening SSH terminal", onFailure: (error) => { - pendingSessionFinalized = true - appendOutputLine(context, `[error] ${error}`) - context.addTerminalSession(renderPendingTerminalSession(error, "error")) + failPendingTerminalSession(error) }, onSuccess: (accepted) => { appendOutputLine(context, `[ssh.prepare] SSH terminal request accepted (${accepted.requestId})`) @@ -237,9 +248,7 @@ export const connectProjectById = ( onEvent: (event) => { const failure = readTerminalStartupFailure(event, accepted.requestId) if (failure !== null) { - pendingSessionFinalized = true - appendOutputLine(context, `[error] ${failure}`) - context.addTerminalSession(renderPendingTerminalSession(failure, "error")) + failPendingTerminalSession(failure) context.setMessage(failure) closeStream() return @@ -306,14 +315,13 @@ export const attachProjectTerminalById = ( effect: loadProjectTerminalSession(resolvedProjectKey, sessionId), label: "Attaching SSH terminal", onSuccess: (session) => { - context.addTerminalSession(buildProjectActiveTerminalSession({ - onExit: context.reloadDashboard, - onReady: context.reloadDashboard, + addAttachedProjectTerminalSession({ + context, projectDisplayName, projectId, projectKey: resolvedProjectKey, session - })) + }) context.setMessage(`Attached SSH terminal for ${projectDisplayName}.`) } }) diff --git a/packages/app/src/web/api-types.ts b/packages/app/src/web/api-types.ts index 4e66774f..840fb37a 100644 --- a/packages/app/src/web/api-types.ts +++ b/packages/app/src/web/api-types.ts @@ -23,8 +23,9 @@ import type { export type ProjectSummary = Schema.Schema.Type export type ProjectDetails = Schema.Schema.Type export type CreateProjectAcceptedResponse = Schema.Schema.Type -export type StartProjectTerminalSessionAccepted = - Schema.Schema.Type +export type StartProjectTerminalSessionAccepted = Schema.Schema.Type< + typeof StartProjectTerminalSessionAcceptedResponseSchema +> export type ProjectPortForward = Schema.Schema.Type export type ProjectBrowserSession = Schema.Schema.Type export type ProjectDatabaseForward = Schema.Schema.Type diff --git a/packages/app/src/web/project-event-payload.ts b/packages/app/src/web/project-event-payload.ts new file mode 100644 index 00000000..503748ce --- /dev/null +++ b/packages/app/src/web/project-event-payload.ts @@ -0,0 +1,13 @@ +import type { ApiEvent } from "./api.js" + +export const readEventPayloadString = ( + event: ApiEvent, + key: string +): string | null => { + const payload = event.payload + if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { + return null + } + const value = Object.entries(payload).find(([name]) => name === key)?.[1] + return typeof value === "string" ? value : null +} diff --git a/packages/app/tests/docker-git/actions-projects.test.ts b/packages/app/tests/docker-git/actions-projects.test.ts index f7c2483d..89e3e05e 100644 --- a/packages/app/tests/docker-git/actions-projects.test.ts +++ b/packages/app/tests/docker-git/actions-projects.test.ts @@ -3,16 +3,26 @@ import { Effect } from "effect" import { afterEach, beforeEach, vi } from "vitest" import { applyProjectById, connectProjectById, runApplyAllProjects } from "../../src/web/actions-projects.js" -import type { ProjectDetails, StartProjectTerminalSessionAccepted, TerminalSession } from "../../src/web/api.js" +import type { BrowserActionContext } from "../../src/web/actions-shared.js" +import type { + applyAllProjects, + applyProject, + loadProjectTerminalSession, + ProjectDetails, + startProjectTerminalSession, + StartProjectTerminalSessionAccepted, + TerminalSession +} from "../../src/web/api.js" +import type { openProjectEventStream } from "../../src/web/project-events.js" import type { ActiveTerminalSession } from "../../src/web/terminal.js" import { makeBrowserActionContext, waitForAssertion } from "./browser-action-context-fixture.js" -const applyAllProjectsMock = vi.hoisted(() => vi.fn()) -const applyProjectMock = vi.hoisted(() => vi.fn()) -const eventStreamCloseMock = vi.hoisted(() => vi.fn()) -const loadProjectTerminalSessionMock = vi.hoisted(() => vi.fn()) -const openProjectEventStreamMock = vi.hoisted(() => vi.fn()) -const startProjectTerminalSessionMock = vi.hoisted(() => vi.fn()) +const applyAllProjectsMock = vi.hoisted(() => vi.fn()) +const applyProjectMock = vi.hoisted(() => vi.fn()) +const eventStreamCloseMock = vi.hoisted(() => vi.fn<() => void>()) +const loadProjectTerminalSessionMock = vi.hoisted(() => vi.fn()) +const openProjectEventStreamMock = vi.hoisted(() => vi.fn()) +const startProjectTerminalSessionMock = vi.hoisted(() => vi.fn()) vi.mock("../../src/web/api.js", () => ({ applyAllProjects: applyAllProjectsMock, @@ -91,6 +101,30 @@ const startTerminalAccepted = (requestId: string): StartProjectTerminalSessionAc requestId }) +const makeSelectedProjectContext = (overrides: Partial) => + makeBrowserActionContext({ + ...overrides, + selectedProjectId: "project-1", + selectedProjectKey: "octocat/hello-world" + }) + +const connectProjectAndWaitForStream = (context: BrowserActionContext) => + Effect.gen(function*(_) { + connectProjectById("project-1", context, "octocat/hello-world") + + yield* _(waitForAssertion(() => { + expect(openProjectEventStreamMock).toHaveBeenCalledTimes(1) + })) + }) + +const readFirstProjectEventHandler = () => { + const handlers = openProjectEventStreamMock.mock.calls[0]?.[1] + if (handlers?.onEvent === undefined) { + throw new Error("missing event handlers") + } + return handlers.onEvent +} + describe("web project actions", () => { beforeEach(() => { vi.restoreAllMocks() @@ -118,25 +152,13 @@ describe("web project actions", () => { openProjectEventStreamMock.mockImplementation(() => ({ close: eventStreamCloseMock })) const addTerminalSession = vi.fn<(session: ActiveTerminalSession) => void>() const closeTerminalSession = vi.fn<(sessionId: string) => void>() - const { context, reloadDashboard, setMessage } = makeBrowserActionContext({ + const { context, reloadDashboard, setMessage } = makeSelectedProjectContext({ addTerminalSession, - closeTerminalSession, - selectedProjectId: "project-1", - selectedProjectKey: "octocat/hello-world" + closeTerminalSession }) - connectProjectById("project-1", context, "octocat/hello-world") - - yield* _(waitForAssertion(() => { - expect(openProjectEventStreamMock).toHaveBeenCalledTimes(1) - })) - - const handlers = openProjectEventStreamMock.mock.calls[0]?.[1] - if (handlers === undefined || typeof handlers.onEvent !== "function") { - throw new Error("missing event handlers") - } - - handlers.onEvent({ + yield* _(connectProjectAndWaitForStream(context)) + readFirstProjectEventHandler()({ at: "2026-04-21T10:00:01.000Z", payload: { phase: "created", @@ -194,31 +216,28 @@ describe("web project actions", () => { it.effect("starts SSH terminal creation when randomUUID is unavailable", () => Effect.gen(function*(_) { - const dateNowMock = vi.spyOn(Date, "now").mockReturnValue(0x1234) - const mathRandomMock = vi.spyOn(Math, "random").mockReturnValue(0.5) - vi.stubGlobal("crypto", {}) + const dateNowMock = vi.spyOn(Date, "now").mockReturnValue(0x12_34) + vi.stubGlobal("crypto", { + getRandomValues: (values: Uint8Array): Uint8Array => { + values.set([0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00]) + return values + } + }) startProjectTerminalSessionMock.mockImplementation((_projectKey, requestId: string) => Effect.succeed(startTerminalAccepted(requestId)) ) openProjectEventStreamMock.mockImplementation(() => ({ close: eventStreamCloseMock })) const addTerminalSession = vi.fn<(session: ActiveTerminalSession) => void>() - const { context } = makeBrowserActionContext({ - addTerminalSession, - selectedProjectId: "project-1", - selectedProjectKey: "octocat/hello-world" + const { context } = makeSelectedProjectContext({ + addTerminalSession }) - connectProjectById("project-1", context, "octocat/hello-world") - - yield* _(waitForAssertion(() => { - expect(startProjectTerminalSessionMock).toHaveBeenCalledTimes(1) - })) - + yield* _(connectProjectAndWaitForStream(context)) + expect(startProjectTerminalSessionMock).toHaveBeenCalledTimes(1) const requestId = startProjectTerminalSessionMock.mock.calls[0]?.[1] expect(requestId).toBe("pending-1234-8000000080000000") expect(addTerminalSession).toHaveBeenCalledTimes(1) expect(openProjectEventStreamMock).toHaveBeenCalledTimes(1) - mathRandomMock.mockRestore() dateNowMock.mockRestore() })) diff --git a/packages/app/tests/docker-git/controller.test.ts b/packages/app/tests/docker-git/controller.test.ts index 1196b376..1872c890 100644 --- a/packages/app/tests/docker-git/controller.test.ts +++ b/packages/app/tests/docker-git/controller.test.ts @@ -71,7 +71,7 @@ describe("controller reachability", () => { it.effect("detects remote Docker hosts", () => Effect.sync(() => { - expect(isRemoteDockerHost()).toBe(false) + expect(isRemoteDockerHost("")).toBe(false) expect(isRemoteDockerHost("unix:///var/run/docker.sock")).toBe(false) expect(isRemoteDockerHost("tcp://docker.example.test:2376")).toBe(true) expect(isRemoteDockerHost("ssh://docker@example.test")).toBe(true)