1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +03:00

sync new docs mostly

This commit is contained in:
Anton Afanasyeu
2026-06-12 21:15:41 +02:00
parent a8ef161e75
commit bb3609bcff
42 changed files with 5067 additions and 795 deletions

View File

@@ -8,3 +8,6 @@ out/
ota-publish/ ota-publish/
*.apk *.apk
*.jar *.jar
.venv-pdf/
docs/_pdf_build/mermaid_cache/

View File

@@ -9,3 +9,6 @@ build/native
*.iml *.iml
.idea .idea
local.properties local.properties
tmp/
.venv-pdf/
docs/_pdf_build/mermaid_cache/

4
.gitignore vendored
View File

@@ -65,6 +65,10 @@ ota-publish/
# --- Scratch (not for commit) --- # --- Scratch (not for commit) ---
tmp/ tmp/
# --- Docs PDF build (local; scripts/build-all-docs-pdf.sh, mermaid_render.py) ---
.venv-pdf/
docs/_pdf_build/mermaid_cache/
# --- Python --- # --- Python ---
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View File

@@ -453,7 +453,7 @@ endobj
endobj endobj
78 0 obj 78 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190915+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190915+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False
>> >>
endobj endobj
@@ -995,7 +995,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<bdb49c5d90f4a37cbafc93ebc8e3f036><bdb49c5d90f4a37cbafc93ebc8e3f036>] [<9a9de1b3349f63671f79c68f49ceb325><9a9de1b3349f63671f79c68f49ceb325>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 78 0 R /Info 78 0 R

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -107,7 +107,7 @@ endobj
endobj endobj
19 0 obj 19 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190925+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190925+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Stream dump \204 developer debug recording \(FR 0.1\)) /Trapped /False /Subject (\(unspecified\)) /Title (Stream dump \204 developer debug recording \(FR 0.1\)) /Trapped /False
>> >>
endobj endobj
@@ -220,7 +220,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<ba707e13386f6c1a64c9a32f17186d4f><ba707e13386f6c1a64c9a32f17186d4f>] [<3b9b84c2007c5695de8ad1fc8eea63e2><3b9b84c2007c5695de8ad1fc8eea63e2>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 19 0 R /Info 19 0 R

View File

@@ -148,7 +148,7 @@ endobj
endobj endobj
30 0 obj 30 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 42 0 R /Fit ] /Rect [ 45.68504 443.2874 191.069 454.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 42 0 R /Fit ] /Rect [ 45.68504 443.2874 97.70104 454.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
31 0 obj 31 0 obj
@@ -245,7 +245,7 @@ endobj
endobj endobj
44 0 obj 44 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190925+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190925+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 exposed services catalog) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 exposed services catalog) /Trapped /False
>> >>
endobj endobj
@@ -393,7 +393,7 @@ endobj
endobj endobj
72 0 obj 72 0 obj
<< <<
/Dest [ 42 0 R /Fit ] /Next 73 0 R /Parent 45 0 R /Prev 71 0 R /Title (URL shortener \(preview \204 not deployed\)) /Dest [ 42 0 R /Fit ] /Next 73 0 R /Parent 45 0 R /Prev 71 0 R /Title (URL shortener)
>> >>
endobj endobj
73 0 obj 73 0 obj
@@ -413,10 +413,10 @@ endobj
endobj endobj
76 0 obj 76 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1071 /Filter [ /ASCII85Decode /FlateDecode ] /Length 1045
>> >>
stream stream
Gau0D_/A!e&A@6Wk#AjK7MR@LP'_8_c8O<_lR#5"!=E">A$0B@92\@QpH+NV-V]ej%Tj!u!Kkr[I.N)_&V1cP1>Jo^^ekDVNsQ@)jVK%RPX9X&Mt5f@"7g;@b+btFE=-u_7r^:YnUEM<`<At<!`:_tik0diB69RT0NSMS:9oMoU"ggp"i>Qk8p^ONdjUo0K&^DOPF8G1Q8QmsF`9od%cV9)Xf0&CF"JnNU#M(kqW]r&s$HMd*.Inl,lA7XVatGG@eZ[$9Vr\Ya1m[,-P7;3PFauG'DE#4@Bt-4f?!gJm*rb#g%#:7f=[5'G>bJ]N5d\9N&)`.[t^N:ApHe&Hm&C[KS8\=pkOI>]3*GW=A2?$C'<`R:hf$EEn5jc3b<g=jA+05d$+t&TC-j-MRRc2SHjV6KYi5rXe=XQQKmbe\Cm+$_OYIR2<qp,>N#Tf]HlE2!hWTLTlD_5Q/Qlq7^Ul/HhlBJ7(tklOog&n9G&5mBjbs.)X];IKW86.]o8:h@4>HgNTN*'9+Fs7)@lm#/:jS!ft^gr\&th,P82.^4<?o=30YX[=`&hYqT8`(Taa9);OADS'F"YES9Vi5hZi!8&$s[kC\uA\dX!-l:Kk'g1mA[Q1;$n>7"H*M6O#FMbJR4;jr4>Fio>U.0c%1,`nW[[GXWc+6:Kj,Cc.Z5='/l*.)GUI%NUKb&%;8.fc3F&(I+-07XOLF/D6X]BG"nn8qbmgDch.fWJ0lX-2Z_W?@'hID*\e=JGq12:YMYo@$Tr2EX#g-WUFp+VT&PE584![hs/L^nkJ_[\BAd?81Xir-b:%I4)MW0cbe5iJfD29;fLN1Z)R:<^`F9N5GF)ihdJ[o&9FspCO"BJ34`#uNrV:uV7ZEYO9Vc-Z#Kp5HjmGj_OqIB5paIofJtGKTGCY.hMU#[Rho%<cg^d$X2F[C\_Qki*p=kO2=dt/XGgtDG+t3H;+:o`D!RU#lU\`aGejW9jO"3`F5<6V]GoT%(otR#kDQZB-hG<&[&qC<TqR2e1fA0Agh="pWF5Pb1%ZV)`e%EiqeasYN>R[9@2*X1WsTkUUIT*SKDtti((.8~>endstream Gau0Cd8&FM'Rf-pmaG.4&Rm+_d_1pKChRMJ+Rk%@\V["ddl=\)1B)_].29dP\]@^F</OtRj)a[smi`8%6i)#3"?=J2s,[5SOC&'GK,F/hR,g=nGKB0qQj$($_AL]X%Dlcqj8X'X#W!C(#_H`WKP:WVb31N+TW`NX9sT=A+^ZDr"i<;+8im"cdNG8mK]@=aK2Goh,tf(rA)W$f'Z@1E),YttH6ir-T\u,;l=q.++-=6k]g#R`_Vaj.MKD/dG#+Q\A=*pQr;2k''X0H3dd]h9_X>;r$ms<i>-gCS3B$>61d/_MMH@+E<iUI/7RNX2KaAC,gVXDp=W5X<m7BN*+X3B^m\(c^-5Q5;(5&.'.KSfI$<:;->N7s[1M(q2kYBU.f9GpkkP%$*RQ"i]X93ngq830]cCj0>.I4$aj!I.Y$[8[eUJU]q=pbMmlnnfn')9P8()-FO:hXb/TLGW_2&Ek7,gu'OO;R26@LCc!S9cM0U>crH8^q=HUf68!M-gU&9r3(@/'4JuFlbl=q'C",<X[ObF&Ui(Yd+8^LW=s:*;[g6>S[_+Ujf[6VXFY\IOU5`5RMo@S/jW*PQ(jEjW@O"&]SpJ@A21;$CA&/_kF8-po&HoGDH=D[YOLF#V>XAiK,\qgkh(.'$"+=@YM480QDt:P51!PM-)6$$=7+miM$J0*nb`kimc`F%n:%CID,Hj-.0lo>\;CW%"*su'42rBVOq;Jb,Q00!LIV`l[.q@o6(7.ZiJodE0mkgLYb)>YPAIX##!k8qMhiKEF0@WR,=jnU%=7B^UVa9<_7*00*\EgRTlVZ<)<Gbhh#W*_cq).hP-ui,/>6Xid[`)(Z/U`cOalo[<$TA*B:($/RAI&&PIK2^`iF&PG<+#F?UW1QV5<<YNWlq62mVD]h`o;h)JRCFQ0F[G\I'`/K:%_-72TOOGl;;Najo$jEABj/:[<5nt2p_(ZKj]i'Zl2^7S.qcB#u/dpQ=]XZ`OT^P,;2$No!Q00n)BrG[R@S[]HhJ=U2!S$qR_(62OkCL.%F$]MV&*pjhgIKLXV!/C~>endstream
endobj endobj
77 0 obj 77 0 obj
<< <<
@@ -441,10 +441,10 @@ Gb!#^lYkfA'*"Q<@_NhD('u9a0.T;lE-ARK2V3(](W?CU%,#6/D.H57kWJ9Ps8*6O=dEX@VX/2I#SMpT
endobj endobj
80 0 obj 80 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3145 /Filter [ /ASCII85Decode /FlateDecode ] /Length 3309
>> >>
stream stream
Gb"/*lZ8Od'*%C7U<@\'EAeD\i?=oEes5Xqj*%bc@O\e\6$4X[,DcOX"@dRIWSHB[4WlFF+@*BEY3NIVf.]o`k0a.nT!8>PI['W*]`L/46haIZKtqpCMC*$kIL?tPUG#-[L/QJHUT_iZoQkpW<_X%\_esj!a`o6aN)u[nOtL)0?4uH0IWG>m(N;.h(10QsA7BXkUSGn_QSACQMiK5^i:6q-O&/aPW-^%=)7R1Gb"]L7Zr<YG';?H#UfeM(?,3+o"uCbs0%M1ghLN;HI%P^=G<MD#go,Y\1Cj/D6U\grl4i>U:,ErM/pN]NcuAOg-A;"6=O0)g>(RK0GAc(rJlcna6[*u4V#(POru*[3s,]%aA%'?^k/4t6!u9rjl!$+r+7N2i0tKb`XX>HmS3U"/K&Y!'@cdqfk=r>T'6d0uTGb-sRdJI+f;=au%5]8WWE1^6LcO/#<dpgMS^l[TT:uNj&B'caC&mk=qaS.Fr@6jnFh."4J@Rl$;5g^"L[R`C=E5HNK5ML5N24#^orH$JKkPUKV1d'8Cb+Mo+Cq+Cc+A.478"P-^Y<%kUFZSlTRf[TYOV*37?`0A]mg8Ck1mkFbEkXb1/3W1Z^K0dJAWHt@`:gHUSHEPdM:fLA(`b(LOaXk:/c]k^CTN.6E@nBo("Y7DDt\U7UOF,rTc/5Eu(o0cD`V,6L-$WXg#$Va[c[p%.WLcLdbraY<rg]d0Mf5Jp?!1fC7diq4.;o1QGJS.J)>*B6HL.b^TYW8ZLkY5f\Elhi>%*3Z`Z1(+((5NW[m?*5=[42TjNln.DQ1mj&F(^OInUHB&_=3dus^WR_!a5gO>h7&qZ]N/XF+"$'ZI,5W>;!b#kO\5XGtQetk>\flZaU4[>s3K]K[-`X>$Umd%nl\!^9M5_q5epVoTT4keb&VV(K_-dsTR/_[f`FhPIk[c#;bYEfMn'+KdWTC1L#Ok193kkB2+%[H]8[?/HZ4jTFSC;$0b'+(^[`4&Q)/kJ-[@if66<&Qc@3!/Yj3?<[g\e\#+&lI<CCR'gH"h17L>$r0g,#<3=jOOL[oWo3S^Ni&%:-IUVd[:(m<NWL*nCWqJb7\h(uMTY5WpY4^rl8Ihde=HQa:60c\1_k"^O\m)5dX>K`Q&gIQRbuLD\nA5`X'kp+j,DgA&fcXfoYTCb+^YA\$Y%Qo:>)#4/`i3t`t_.Ra9,Zd7/$Ysd[!q#0GJlB@g:.o?.lF*3/Pq_%R4Ws>PY/It<jLR`iHD`sC.k].C!CbS9<3j=lm?l6t-?n:V0kF_(U<n`,,>R,_S*U(Ud*S<tF#VhMT(V41;NG]i<E05#L8Q@XC(q6ggWZ##iD%-@F@T&tLYn]?LV0pCTb(ZkZL*ZM(o)R&bZEN+iZ7.5kdQ<!RE#f-6.QYB-;6TZ$'0*FLO<1m#"mWH+bY?OdZ8Yk,7^'4T8ciFgjMe%7Iltkb<:7Z;/gI,AG8^).=jbc1aMiL/,C<4ONFZurGLkDg=7:<4P@VQb.oElQ<(YsYZq_"h&@X)j7!@buR-PLX3;V0/"Wf:MGY-gS#ZuoKTKBJro^,C4GqJ^dds/-:`e.kpXfeUjH'R\Ac0:*#SkG.#J]$h?>[ilD^NIWZBbG_Q.lMH3bY_60c5[.e<OG?=W?Y%Q#M$uEehCf1$!QIZLkm3XMS\muM!uSo0gDIX8KgY>T,\bom%-74l5Q1Ca`4j4LXfSVV">fTNGCb=aG?et[n@M,2kjV;IQIVW^"la%8gBI5p2`jW7&'6OD(?6a-Y>5==*^gS`O@r8f'n*(=DX4[C^$25>e`R$CsR<=nR]C,DpS.uj"reV*4AXfY,UOMYp:9^7\$qSime3I.mi3s,&/h/Uh&N9?N1UEcN$lDK^-J@+Y55&nRrbNIc3k>[o).A)W6AtXESVT2I'+=S-R-^g4V7sL$2+g*9.JTDA5RXMt8=@$:!<.&IPXF:1(m+\&_c,^.X$lGTp8$A]!rmePi?.2]*/oC0)YD_guG_/BM9V7[Ue]bp;Li3'RO/<@1kmlP7^0[a8s\5UlFI=QhABhst0aZIKGU<[O>`d:l'ON_]W$]X8aCAh'#Ih+`)>cp^nVBuY$oq`rN[0a+D"D+gfe$7Nd0#_,<L+tKs8.*>M[Y![,r@5:6,#f_fE,%57NB:^o0']^HD-_p"Y__]gs1L6KA?'l?s(\Fangi.Et](g7]V.Jfsfa5^h?NfV:\2pcS/5V=tA=._l%+W_#^-P:IJP%\LQH8hAo/kQ6bX=@eHpuDdM_#1OhZmS>,0NJ#b>C%_cN,Xoj"nR8J<q?&bY)QeHKGkCbUn[d@H>%(>@@UJhB$0]QF(cW37'APQb.%26+Q*=K*QCJ$_%f7c^M\-3RnM1+i<"QXdd`%)BEK:8ik9!1Ns't;nLh&;OqP32/h\1,&maQBThH\9<2s?g6W>rEMVO[l+?).ZP*-`7I9X9VdXds4tN"C#+C-mZ[dp>KB#EZ6[S8pl*][SW?XL#e;';.L.p)dHj44"HR]G=/cI!61&1FZGRX&]VaV.V3FRJ@U=R%GT#3k]l3lblYXS@u+Q@WliC?jMT;@Rj/_<M5aB@HT&6SZJZ^`fNq^5d",[_pQ,3bu%=!Ai_pCW7LrFuCCA-=J-dm#G1FcpM8V>shYR1rfM8ZT[$MMNa)#=G3qfc&\68hK)oM]F[&s5-YE,4Ej2b$\Pg;Au'9[qGZl__4r*lWA(h*C-MY3_f2M.B5#EA_&^O[9Z*2[leLu;Nqpn/1r7>V&P8tLC#+LK#kD&bRaO+O<s=h[^/h9,F^<7GD#-@&:V9SI>u0]flo&DFs%@tJVO7/?@`["a-]=TJkCt.X^E#@0fa)]9-Yli[TpcQ+MR;HIZE&1:"lut\be?\OE4o!g>>$(3JG)AbGjYK/prr@^jPM9UCqocQJL2P!tpU"e+9#bBFk%T`M7,"=RcJ9^VK[Ijk"2'^AJpcc`KY-e^GR9ho*RBJ5YY:^U;rnc?Zn.5nL>6cD`[o*?o.3]jNLmrE8K@7mU8YC[qH;RuP5M"(=3Ybqg!:.,Ta'ZjX-7Xp80F8*)%ps1-lUWMPla=HQTT1Po"dH;,TOf\boVHKW/^jlX<-JC>6&.Sa4*=,:5Pc=q=t>5WG4I6"c4@Zq:#q1^LieJNEDa6/#mmH'AVkp;4OZ88DL=OBl;^Upo3keF?Uh!+~>endstream Gb"/*D0+Gi')q<+Z/_7a3)0/P^?DDWl,^"J\!In$;Qfa[EuKpf,UO5AOH:JAPQ(8[O<RCrUe&X"V_cqERM5n7Md6=pca09%;"7&6s)urnk@7>k1scMe(SDYO!TR7#3Bm7\U2<C=VJLBLeUUDm/$"!B_"Y&P?dpi6RL\@8BW>m+f9Iif>p3"'J&3M%NO[4l&J140P%mRWg4'Ii&$bDEj;D3._Y+RVMB=:VWku9J'M!&qSYVf+4e!#3g)ZW)_>,DL]4`ni([c;WdeSs^XDHdl*\:QWVBWLu54]YIHcdM+i@UT:ohWjAoS*F=mE]K+GmY3O3Y6+>Sf=fI%_93U*?N$Q\>>n2\/3ltV<`Vk"c\#QRHQBGjt?8a^OJM&\T7N^0$69R.:!ch-E>H*$WB7)d'EblQ.*[o&X';dB]g3"R\E)(C3)4Y:2aEn:7aA2&a@H/EeQo_ahb[])kHB`%\NP2B4(s1]I5`K@(pjBBJ*Jtb4thD>l+&B\N\o?:gpQa]H)u?`H`$f'a84faU@e5B:4HHd8Fq]KZouKG/U:Ea?EFU.o'bJ.iMj2X/o2PbJ>ZA@$95Xls?f-d]]FTDd^TIpMf<d<RHnE-:NR?9[()/mZ/Qs@j=+`d;oTXRW0h1Ne>Y$@%N.s<055j"58OW/r6#[I1DK'%5t`HeMDRPE>-b($eI<"FFI_Tp.9"&@:OKo:P`^eidVb2/Hu;i'7(ci,EVm,nZka<nDo2sSGHJ/a&Br'oA&D;.N]ks5Cpp%S`L`>1l!Gc:,>\:p:m+)'2SM'Ja4sbT:[G8Aa7S2&Z65-Op@ml^D8)CNu_0a5>='.lM+aqA@GNI<"C[aCd?/D1>(M3q^:;+@2iR,<:mrKmjf41&kPPK_6f/k8m1Noh:+aAH^<CMK%r(Pcd9V1qtM'n1!H4_1h)#haXEshp]3g@TC!35//oeRiX@77]7%dH[;trs&TVl-SQpPgBLnZ&8TMU'ZBqq@>gpW4jem_V7q4'B(E\STHV#`SL$/Z+_`C9Tqr+T-f_i?**`Q?P)@a&fH#9`%VV6>Pg'aJ`e]a&HG>JZ"S^pj>O=o@Fe$+;dm:G`[4fhSX"6m8E0YV%[5WpY4^rl8IhdeCJf<\rnc]fKT"[bjS*J6\W_Lsgr42(a\LR?p.5`X'kp,[Eag;Zaa2HHOWgFBd>gTJ!hV@=<>JU!s<N^SdnL!$#IqagZ%,q=D`s#uso?.(H$%>t/q?_1Zr^.Y7!qB77rbC`[IU#quJ4-.pIEOc\C@PC733j=lm?lI+/?n:W[i1K>N<n`,,>R17"4"ui\3j=lk&CO360'8HW[W?dsKo8W_aLZ7k`Oqc':u0:jCNJ^3OHUTRMk/YnRdq2_=)M!^Zt-S>p5Y6),5^ZCV=nHEcoO\<^=e/?Mi%P>H.HbZli`?NGLGMHZTt'>K"5Ta5Rqh"b^6H:<0@O)KGP/:IX+o6W'#h?6Uiu:4Clq`B?ep>Q](UD7eWFV>)@;gGLkDi=.aZlOB'Kh<MFT!kd%`>d$b`i.;i@oLn)g$OR!X+*;s]W!tVk-]HqM=">%K>TZh3WA,BM&?;'>X1u9dRnXpc!2)RNQYB%CRS?>uQ\anAnbaHq@92$hT2i1cKP11_Yg!',NUZ-Zs"V-dKeIa)d*EC*AW)n<"5gkl,BZZ)B&OWo<cLVS)YQml<dbf(>a&Q&9gA/NE@6tOX(Wp-<q:bN9?JYtq5M`pCmR:s8e8tSL&V?k<q;@tX%U01HIh1lUD,b<pl2You1R?lTF/Jr]qS#2[qj6V-aKNXg8\V4N%diAl@@Tf:,+lNF[WfNMbX7@8mpt%:QT/33<Gs`lc8h[Ho9WT\Jr,3Ud^\-_.JFPYGna??qnPC-LHFaF%R6S<-t7U'C-,$S?+gafM(\X$M2QVX,GQ/_R4$aYEDGn_L(G"94IDa!DFgghc8"\Zh66p9CEg0,T7%'309N_HI[,OQMk#@-DVOQ%oB,.`!SDI+T0hg)Cec;%QkpLG*?`cl^H@T!NhOri3l_DEWW@Q1o!%T2`0&=?6_tHUU(_[7'Kq[dHL8N>gl5D'3O$k9])Ic0@rH$q;67A0qDkM/aK6s@,2:$41?e^3.ENY-p^VE,20))2UpkU6@2;@`kW*n/:a!_=l&CM74f*N#mg;Q2--G@N:5S6C+F7+U%;W]Ynh?_(&;6kpOHu\O'Mp%h&cNe"7+G8ed^5u;h1XWA6gr)B''q0$>9?`mKO^foN)C2VFebGNo,HM)/YZ)Kcn>VD8F6PH_]V&Y?4L=u6O)_+dH-9!VGM/>b5LIEc)+qOEqCLQ@Q531?]M!F,$WlKJZL\>TAm_)RYqJNZoC"4W]MtA>]KEHY\W(AjfV$Co`._0#.,Y/DjW1`O-+s20=Rta=\=p76#t+_Lf-5@g*%O!S>V+gPd@#'\`K?Z7dabh:a7O*_^\-4hnKcIh[r#-YK!&!qrX(!7s#6I@Mup@)/9;*G;T;R8E&[M%N@+Ap/jMcjdhdc2b`%(Q-Hs-Q1:7=2Y#`-Cd21olkulZJj1$rms&V-)I6j_N'nu`HN<qkLDd\?SL-eYf$n5$D\;eA&16(:Yd3TMe\5dIJY%HpK/_$UHZNB]roY53HuW(Q`-k"j\@G+]kAK?7[.h:V]^^ua%!FY:'2kWkCGuZ)LnN1/Fc<7McfBL>MEZ:`DENZ_j$Ae09[6`_[]"W<DY&+k^Ac&2VY_*dG[I95oK+u41;:s)!Ol>V"!_H0lTek8Hd&(J`S]g^]Fm+EK]UmsE`i;W5i:6/b0fsl]+TN*"-IWM0Im_Id6!.PHJRieWgO<0$(:d0HgX#,,haHW>0PJR@V*eJ.FLUbWN4U-id>N`e'i%8QWLV2jSlof4A@t4lcNUA]Mf_r^sQK\A++elHJ?5]S"'I>S^D/o)i@%]e@J,LZsD^(*X:)HU9$d4A9,[kDBuC?-A7Hd,2WllFRau4972?IcGl6q/Uqtskp?s]]TIW%eEkh3+XeW^.Nn47J\X&QC(D01kj"n\R*Z=DV53bYcg>mlGo8,_\F*^QIGTi.m9>["niKR-F(7Skhn0@&E49jJNJhZ"dLVB\HS\N.ccgPNRrJSY3$2DT/4=jKT1hA)<fJJU"QUA$?_I=cfs`W,a0Xt&ZMEoKVYhN;ZqsDdo398kjR\,^@h=<p=`A*bBQ$^&i)t#"eG1N<_P<d*J2MV&Ls8Csp0h>hR`u3QYUJC_c*Rk`:Taq,Bh)arq_.\@?>=1#/FF/(kRjrnbE7f3pX0)E^XKaIr\>()]#IF_C$X)ein'G!9e*C,GG6#R#);hZ;sPkCku-#eEN\<1]E4QaX1_*&e_>2A[%L;Z;(3R$J$e.pIkNRZ;u~>endstream
endobj endobj
xref xref
0 81 0 81
@@ -479,60 +479,60 @@ xref
0000003938 00000 n 0000003938 00000 n
0000004086 00000 n 0000004086 00000 n
0000004234 00000 n 0000004234 00000 n
0000004382 00000 n 0000004383 00000 n
0000004530 00000 n 0000004531 00000 n
0000004678 00000 n 0000004679 00000 n
0000004794 00000 n 0000004795 00000 n
0000005215 00000 n 0000005216 00000 n
0000005321 00000 n 0000005322 00000 n
0000005441 00000 n 0000005442 00000 n
0000005525 00000 n 0000005526 00000 n
0000005603 00000 n 0000005604 00000 n
0000005809 00000 n 0000005810 00000 n
0000006015 00000 n 0000006016 00000 n
0000006221 00000 n 0000006222 00000 n
0000006427 00000 n 0000006428 00000 n
0000006514 00000 n 0000006515 00000 n
0000006831 00000 n 0000006832 00000 n
0000006906 00000 n 0000006907 00000 n
0000007007 00000 n 0000007008 00000 n
0000007113 00000 n 0000007114 00000 n
0000007220 00000 n 0000007221 00000 n
0000007378 00000 n 0000007379 00000 n
0000007477 00000 n 0000007478 00000 n
0000007587 00000 n 0000007588 00000 n
0000007692 00000 n 0000007693 00000 n
0000007833 00000 n 0000007834 00000 n
0000007932 00000 n 0000007933 00000 n
0000008040 00000 n 0000008041 00000 n
0000008156 00000 n 0000008157 00000 n
0000008296 00000 n 0000008297 00000 n
0000008395 00000 n 0000008396 00000 n
0000008507 00000 n 0000008508 00000 n
0000008654 00000 n 0000008655 00000 n
0000008740 00000 n 0000008741 00000 n
0000008895 00000 n 0000008896 00000 n
0000008981 00000 n 0000008982 00000 n
0000009126 00000 n 0000009127 00000 n
0000009212 00000 n 0000009213 00000 n
0000009320 00000 n 0000009321 00000 n
0000009465 00000 n 0000009466 00000 n
0000009553 00000 n 0000009554 00000 n
0000009655 00000 n 0000009656 00000 n
0000009770 00000 n 0000009771 00000 n
0000009888 00000 n 0000009889 00000 n
0000010026 00000 n 0000009997 00000 n
0000010138 00000 n 0000010109 00000 n
0000010238 00000 n 0000010209 00000 n
0000010327 00000 n 0000010298 00000 n
0000011490 00000 n 0000011435 00000 n
0000015053 00000 n 0000014998 00000 n
0000018045 00000 n 0000017990 00000 n
0000020924 00000 n 0000020869 00000 n
trailer trailer
<< <<
/ID /ID
[<b8dfc902413467e181373d9a9bf51cbd><b8dfc902413467e181373d9a9bf51cbd>] [<c28c36aeaaa65554ca8ff15fe717eeb8><c28c36aeaaa65554ca8ff15fe717eeb8>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 44 0 R /Info 44 0 R
@@ -540,5 +540,5 @@ trailer
/Size 81 /Size 81
>> >>
startxref startxref
24161 24270
%%EOF %%EOF

View File

@@ -153,7 +153,7 @@ endobj
endobj endobj
27 0 obj 27 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190925+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190925+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False
>> >>
endobj endobj
@@ -320,7 +320,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<8e0b29f4527559737151344ff53c0eb9><8e0b29f4527559737151344ff53c0eb9>] [<ddd886f869ceb4f407d340fda95ecc82><ddd886f869ceb4f407d340fda95ecc82>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 27 0 R /Info 27 0 R

View File

@@ -92,7 +92,7 @@ endobj
endobj endobj
17 0 obj 17 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190926+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190926+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False /Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False
>> >>
endobj endobj
@@ -183,7 +183,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<512734ece359c2ed680f4edf5d59c938><512734ece359c2ed680f4edf5d59c938>] [<0031c88799808434ed1ff089b107fa6c><0031c88799808434ed1ff089b107fa6c>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R /Info 17 0 R

View File

@@ -214,7 +214,7 @@ endobj
endobj endobj
37 0 obj 37 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190926+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190926+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False /Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False
>> >>
endobj endobj
@@ -455,7 +455,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<13b148972764fd620c33507f36faea4e><13b148972764fd620c33507f36faea4e>] [<575181c260e696c2648cc1c1c1520a6e><575181c260e696c2648cc1c1c1520a6e>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 37 0 R /Info 37 0 R

View File

@@ -107,7 +107,7 @@ endobj
endobj endobj
19 0 obj 19 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190926+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190926+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False
>> >>
endobj endobj
@@ -221,7 +221,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<8cf59d7b8ceb1fe7a630b86e8c53b8c3><8cf59d7b8ceb1fe7a630b86e8c53b8c3>] [<0d35921c8624899e1bb669ca58d2afef><0d35921c8624899e1bb669ca58d2afef>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 19 0 R /Info 19 0 R

View File

@@ -67,7 +67,7 @@ endobj
endobj endobj
12 0 obj 12 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190926+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190926+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False /Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False
>> >>
endobj endobj
@@ -135,7 +135,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<9dc9ad10d8d6171032f49fab4ea6ad8a><9dc9ad10d8d6171032f49fab4ea6ad8a>] [<7ebfeb118c0996a64ce6b35643381699><7ebfeb118c0996a64ce6b35643381699>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 12 0 R /Info 12 0 R

View File

@@ -72,7 +72,7 @@ endobj
endobj endobj
13 0 obj 13 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185330+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185330+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190926+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190926+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False /Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False
>> >>
endobj endobj
@@ -141,7 +141,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<4c86bec4fe3d879d5004ff9d9fa30ccc><4c86bec4fe3d879d5004ff9d9fa30ccc>] [<fb883105861ad656bbbce6ba3242676b><fb883105861ad656bbbce6ba3242676b>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R /Info 13 0 R

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,633 @@
# Platform scaling and multi-environment architecture — design review (DR)
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20100612_1_scaling.txt](../drafts/20100612_1_scaling.txt)
**Specification:** [docs/specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md)
**PDF:** [20100612_1_scaling.pdf](20100612_1_scaling.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh` (Mermaid → PNG diagrams; requires `npx`)
**Date:** 2026-06-12
**Status:** Reviewed — planning / brainstorming (no implementation sprint yet)
**Severity:** Strategic
**Documentation index:** [README.md](../README.md)
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Problem statement](#2-problem-statement)
- [3. Architecture options](#3-architecture-options)
- [3.1 Option A — Dev cluster (draft default)](#31-option-a-dev-cluster-draft-default)
- [3.2 Option B — Tighter cost (minimal split)](#32-option-b-tighter-cost-minimal-split)
- [3.3 Option C — Cloud-first hybrid](#33-option-c-cloud-first-hybrid)
- [3.4 Comparison matrix](#34-comparison-matrix)
- [4. DR decision — recommended path](#4-dr-decision-recommended-path)
- [5. Current vs target diagrams](#5-current-vs-target-diagrams)
- [6. Service split rationale](#6-service-split-rationale)
- [7. Environment promotion model](#7-environment-promotion-model)
- [8. RSSH and connection capacity](#8-rssh-and-connection-capacity)
- [8b. Network bandwidth (DR)](#8b-network-bandwidth-dr)
- [8c. Global availability and GEO segments (DR)](#8c-global-availability-and-geo-segments-dr)
- [9. Future capabilities — priority weighting](#9-future-capabilities-priority-weighting)
- [10. Simulation lab design](#10-simulation-lab-design)
- [11. Cloud provider notes (GCP / Azure)](#11-cloud-provider-notes-gcp-azure)
- [12. RBAC evolution](#12-rbac-evolution)
- [13. Risks and mitigations](#13-risks-and-mitigations)
- [14. Estimates](#14-estimates)
- [15. Task dependency graph](#15-task-dependency-graph)
- [16. Resolved open questions](#16-resolved-open-questions)
- [17. Remaining open items (PO)](#17-remaining-open-items-po)
- [18. Suggestions (PO / dev discretion)](#18-suggestions-po-dev-discretion)
- [19. Changelog](#19-changelog)
<!-- /toc -->
**Documentation index:** [README.md](../README.md)
---
## 1. Executive summary
The project outgrew **one Alpine BE VM** that hosts consoles, APIs, MariaDB, Gitea, OTA, WireGuard, RSSH, URL shortener, and Docker builds. The draft asks for **clusters**, **dev/staging/prod**, **zero-downtime deploys**, optional **cloud**, and headroom for **RT cast**, **VPN commercialization**, **payments**, and **ffmpeg** pipelines.
**DR outcome:**
| Area | Decision |
|------|----------|
| **Timing** | **Post-alpha** strategic track; does not block alpha soak |
| **Architecture** | **Option B → A** phased: minimal VM split first, then full dev cluster simulation |
| **Edge** | Keep **Gentoo FE** as TLS/DNAT anchor; add LB upstream to N BE web nodes |
| **Data** | **Dedicated MariaDB VM** before scaling anything else CPU-heavy |
| **Build** | **First split candidate** — Docker off shared PHP VM |
| **Cloud** | **Hybrid** — object store + staging dress rehearsal before prod migration |
| **RSSH capacity** | Plan **100 concurrent SSH** / **5k heartbeats** per bastion pair before commercial VPN |
| **Bandwidth** | Document **WAN / FE→BE / per-service** Mbps & GB-month budgets ([SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress)); CDN before horizontal web scale for OTA |
| **Global + GEO** | **Worldwide reach** default; **deny segments** per content (e.g. no serve in `ZM`) via layered enforcement ([SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) |
| **Future slots** | Weighted backlog in [§9](#9-future-capabilities-priority-weighting); SFU before ffmpeg farm; **F10 GEO** with F3/F5 |
---
## 2. Problem statement
| Pain | Evidence today |
|------|----------------|
| **Blast radius** | Submodule sync wiped gitignored configs; one BE restart affects all services |
| **Resource contention** | Docker build spikes CPU/RAM during PHP console use |
| **No promotion path** | Single prod-like env; staging OTA path still pending ([20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)) |
| **Scaling ceiling** | RSSH + WG + future SFU on same VM as MariaDB |
| **Bandwidth ceiling** | Single FE WAN + FE→BE path; OTA/APK egress competes with consoles ([SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress)) |
| **GEO gap** | No per-content region policy yet; required before global recordings/commerce ([SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) |
| **Operational risk** | Topology changes on FE/router are hard to roll back ([bottomline_reminder.txt](../../bottomline_reminder.txt)) |
**Success criteria:** PO can deploy unstable work to **dev**, beta-test on **staging**, and promote to **prod** with rollback per service — without rewriting mobile apps or public URLs each time.
---
## 3. Architecture options
### 3.1 Option A — Dev cluster (draft default)
Draft proposal: **N VMs per environment**, each role class (web+OTA, DB, object store, WG, url shortener+Gitea, build, +N for new features).
```mermaid
flowchart TB
subgraph Cluster["One environment cluster (e.g. dev)"]
W["Web + OTA VM(s)"]
D["Database VM(s)"]
O["Object store VM(s)"]
V["WireGuard VM(s)"]
U["URL shortener + Gitea VM"]
B["Build VM(s)"]
X["Future service VM(s)"]
end
W --> D
W --> O
U --> D
B --> O
V --> W
```
| Pros | Cons |
|------|------|
| Clear isolation; matches future SFU/ffmpeg | Highest VM count / cost |
| Easy to simulate on FE XEN | Ops overhead for small team |
| Aligns with cloud MIG later | Overkill before alpha exit |
### 3.2 Option B — Tighter cost (minimal split)
**Phase 1 prod shape** (recommended start):
| VM | Bundled services |
|----|------------------|
| **be-web** | nginx, PHP (hub, crashes, graphs, builder **UI**, OTA **metadata**) |
| **be-data** | MariaDB only |
| **be-ops** | Gitea, url-shortener |
| **be-build** | Docker builder |
| **be-vpn** | WireGuard + RSSH bastion |
```mermaid
flowchart LR
FE["Gentoo FE"] --> WEB["be-web"]
WEB --> DATA["be-data"]
WEB --> VPN["be-vpn"]
OPS["be-ops"] --> DATA
BUILD["be-build"] --> DATA
```
| Pros | Cons |
|------|------|
| ~5 VMs vs 7+; fits “money-wise” draft | Gitea + shortener still coupled |
| Unblocks build/DB contention immediately | Second web node needed later for true HA |
| Low risk to current URLs | WG still single point until pool |
### 3.3 Option C — Cloud-first hybrid
Move **staging** entirely to GCP/Azure; keep **prod** on FE→BE until hybrid LB proven.
| Pros | Cons |
|------|------|
| Managed LB, DB, object store day one | Egress cost; UDP/WG complexity |
| Dress rehearsal for full cloud | PO learning curve; vendor lock-in mild |
| ffmpeg/SFU elastic scale | **Violates** “safest topology” unless staged |
### 3.4 Comparison matrix
Scores 1 (poor) 5 (excellent) for **current team size** and **alpha proximity**:
| Criterion | A — Full cluster | B — Minimal split | C — Cloud-first |
|-----------|------------------|-------------------|-----------------|
| Cost now | 2 | **4** | 3 |
| Time to first value | 2 | **5** | 3 |
| Zero-downtime readiness | 4 | 3 | **5** |
| Topology safety | 4 | **5** | 2 |
| SFU/ffmpeg fit | **5** | 3 | **5** |
| Simulation fidelity | **5** | 4 | 2 |
| **Global CDN / GEO** | 3 | 3 | **5** |
| **Weighted total** | 25 | **27** | 25 |
**Weights used:** safety and cost now > cloud elasticity pre-alpha.
---
## 4. DR decision — recommended path
```text
Now ──► Option B (minimal split) on-prem HVMs
├── Simulate Option A as DEV cluster (37 Alpine HVMs)
├── Add be-web-2 + FE upstream when metrics say so
└── Option C for staging only → then hybrid prod
```
| Step | Action | Owner |
|------|--------|-------|
| 1 | Document service boundaries (this SPEC/DR) | Agent / PO review |
| 2 | Move Docker builder to **be-build-1** | Sysop |
| 3 | Move MariaDB to **be-data-1** (hardest cutover) | Sysop |
| 4 | Stand up **dev cluster** simulation on intra network | Sysop |
| 5 | Staging promotion runbook + DNS prefix | PO + DEV |
| 6 | Cloud object store for OTA/APK | Sysop |
| 7 | **CDN + bandwidth dashboards** (SPEC §12.3) | Sysop |
| 8 | SFU SPEC + media workers (separate track) | PO spec |
| 9 | **GEO policy schema + edge rules** (SPEC §14) | PO + DEV |
**Not chosen now:** full Option A in prod; full Option C prod cutover.
---
## 5. Current vs target diagrams
### Today (single BE)
```mermaid
flowchart TB
R["Router 134.17.26.161"] --> FE["FE Gentoo 10.7.0.10"]
FE --> BE["Single Alpine BE 10.7.16.128"]
BE --> S1["PHP consoles + APIs"]
BE --> S2["MariaDB"]
BE --> S3["Gitea + url-shortener"]
BE --> S4["Docker build"]
BE --> S5["WG + RSSH"]
FE -->|"UDP 45340 DNAT"| S5
```
### Target phase B (minimal split + HA web)
```mermaid
flowchart TB
R["Router"] --> FE["Gentoo FE"]
FE --> LB["nginx upstream / cloud LB"]
LB --> W1["be-web-1"]
LB --> W2["be-web-2"]
W1 --> DB["be-data MariaDB"]
W2 --> DB
OPS["be-ops Gitea+shortener"] --> DB
BUILD["be-build Docker"] --> DB
VPN1["be-vpn-1"] --> DB
VPN2["be-vpn-2 optional"]
FE -->|"UDP DNAT pool"| VPN1
FE --> VPN2
```
---
## 6. Service split rationale
Draft asked to balance **cost vs CPU/RAM needs**. DR sizing rules:
| Workload | CPU | RAM | Disk | Pin to |
|----------|-----|-----|------|--------|
| PHP consoles | Medium | 24 GiB | Small | be-web* |
| MariaDB | Medium | **8+ GiB** | **Fast SSD** | be-data |
| Gitea mirrors | Low | 2 GiB | Medium | be-ops |
| URL shortener | Low | 512 MiB | Tiny | be-ops |
| Docker build | **Burst high** | **16 GiB** | Large ephemeral | be-build |
| WireGuard | Low | 1 GiB | Tiny | be-vpn |
| RSSH sshd | Lowmed | 2 GiB | Tiny | be-vpn |
| ffmpeg (future) | **Sustained high** | 832 GiB | Temp SSD | be-media* |
| SFU (future) | **High** | 8+ GiB | Low | be-media* |
| Recordings (future) | Low | — | **Large** | object store |
**Anti-pattern rejected:** MariaDB + Docker build + ffmpeg on one VM (draft implied bundle risk).
---
## 7. Environment promotion model
| Gate | dev → staging | staging → prod |
|------|---------------|----------------|
| Automated tests | `./gradlew :app:testDebugUnitTest` + BE smoke | + soak checklist |
| Manual | PO feature acceptance | PO sign-off + tag |
| Data | Synthetic | Anonymized copy optional |
| Rollback | Re-deploy previous image | Git revert + DB restore runbook |
Align app releases with [GIT_FLOW.md](../GIT_FLOW.md); infra releases may cadence independently with **config version pins** per environment.
---
## 8. RSSH and connection capacity
Draft question: *how many simultaneous connections per instance?*
### Assumptions
- Heartbeat poll **17 min** ([REMOTE_ACCESS_IMPL](../REMOTE_ACCESS_IMPL.md))
- Average SSH session **15 min** operator work
- Alpine **be-vpn-1**: 2 vCPU, 2 GiB RAM
### Estimates
| Resource | Limiting factor | Conservative | With tuning |
|----------|-----------------|--------------|-------------|
| **Concurrent SSH** | `sshd` `MaxSessions` / CPU | 50 | **100** |
| **Devices polling heartbeat** | PHP-FPM + MariaDB writes | 2 000 | **5 000** |
| **New SSH/min** | `MaxStartups` | 10 | 30 |
| **WG peers (lab)** | kernel + UDP | 200 | 500 / 2 nodes |
**Tuning knobs:** separate **heartbeat** PHP pool to **be-web** (off bastion); keep sshd on **be-vpn** only; connection pooling to MariaDB; read replicas for heartbeat read-heavy phases.
**DR recommendation:** Instrument **active SSH** and **heartbeat QPS** in graphs before selling commercial VPN (draft F4).
---
## 8b. Network bandwidth (DR)
Draft rev.2 adds **throughput** alongside connection counts.
### Baseline risk (today)
| Path | Risk |
|------|------|
| Single **WAN** uplink | OTA release or viral short link saturates all services |
| **FE→BE** proxy | All HTTPS doubles through FE NIC |
| No **CDN** | Every APK download hits origin BE |
| **Media future** | SFU + ffmpeg on same uplink = hard fail without PoP/CDN |
### DR sizing decisions
| Decision | Rationale |
|----------|-----------|
| **CDN before be-web-N** for OTA/static | Fixes egress Mbps cheaper than more PHP VMs |
| **Separate media NIC/subnet** when SFU lands | Isolate UDP/WebRTC from MariaDB LAN |
| **Egress budget alerts** at 80% ISP/cloud quota | PO notified before hard cap |
| **Simulate release day** on staging | e.g. 1000 concurrent OTA downloads |
### Planning numbers (single-site prod, until CDN)
| Traffic | Peak Mbps (conservative) | Mitigation |
|---------|--------------------------|------------|
| Steady HTTPS (API + consoles) | 520 | Normal |
| OTA drop (1000 devices × 40 MB APK) | **200400** burst 10 min | **CDN required** |
| Recording download (F3) | 50200 per hot file | Object store + CDN signed URLs |
| SFU fan-out 100 viewers | 50500+ | Regional PoP; not on FE WAN |
Full table: [SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress).
---
## 8c. Global availability and GEO segments (DR)
Draft rev.2: **global reach** + **segment isolation** (example: deny publish/serve in **Zambia `ZM`**).
### Policy model (DR decision)
| Rule | Choice |
|------|--------|
| **Default** | **Allow** worldwide for control plane and public APIs |
| **Content objects** | Attach optional `geo_policy` with `deny_publish` / `deny_serve` / `allow_only` |
| **Enforcement order** | CDN/WAF → BE API (trusted geo header) → object signed URL → app entitlement |
| **Operator UI** | Hub/console editor for geo segments when F3/F10 ship — not alpha |
| **Sanctions** | PO-maintained deny lists; not auto-synced from external API in v1 |
```mermaid
flowchart LR
REQ["Client request"] --> EDGE["CDN / FE edge"]
EDGE --> GEO{"Country in deny_serve?"}
GEO -->|yes| DENY["451 / blocked manifest"]
GEO -->|no| BE["BE + geo_policy DB"]
BE --> OK["200 / stream / redirect"]
```
### Phasing
| Phase | GEO capability |
|-------|----------------|
| **Now** | Implicit worldwide; no per-content rules |
| **CDN phase** | Edge geo block for **coarse** country bans |
| **F3 storage** | Per-recording `geo_policy` in MariaDB + object metadata |
| **F5 payments** | Entitlement includes `geo_allowed` |
| **Multi-PoP** | GeoDNS routes to nearest **allowed** PoP only |
### Example use cases
| Use case | Policy |
|----------|--------|
| OTA `stable` globally | `default: allow` |
| Beta APK EU-only | `allow_only: ["DE","FR",…]` |
| Recording licensed EU/US only | `deny_serve: ["ZM",…]` + `allow_only` optional |
| Short link to blocked catalog | BE returns 451 at redirect time |
**Open for PO:** whether **VPN exit** in denied country circumvents policy (DR: log + ToS; technical block on **VPN product** separate from HTTP geo).
---
## 9. Future capabilities — priority weighting
Weighted for **platform dependency** and **revenue enablement** (PO may override):
| Rank | Slot | Weight | Rationale |
|------|------|--------|-----------|
| **1** | F1 Multipoint cast (SFU/MCU) | Critical path | Unblocks WAN cast; already on [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md) |
| **2** | F2 ffmpeg transcode | High | Shared worker pool for F3, F9 |
| **3** | F3 Cast storage | High | Needs F1+F2 + object store |
| **4** | F5 Payments | Medium | Enables F3, F4, F7 |
| **5** | F4 Commercial VPN | Medium | RSSH/WG scale must be proven first |
| **6** | F9 3rd-party streaming | Medium | Uses F2 cast handlers |
| **7** | F6 Mobile add-ons | Lower | Can ship on-device first |
| **8** | F7 In-app transfers | Lower | Compliance heavy |
| **9** | F8 DNS / subdomains | Exploratory | Largest scope creep |
| **10** | F10 GEO enforcement | Medium | Required for F3/F5 global commerce; draft rev.2 |
```mermaid
flowchart TB
SFU["F1 SFU / multipoint"] --> STORE["F3 Cast storage"]
FF["F2 ffmpeg workers"] --> STORE
FF --> EXT["F9 3rd-party streams"]
SFU --> EXT
PAY["F5 Payments"] --> STORE
PAY --> VPN["F4 Commercial VPN"]
PAY --> XFER["F7 In-app transfers"]
PAY --> ADD["F6 Add-ons"]
DNS["F8 DNS / site builder"] --> PAY
GEO["F10 GEO policy"] --> STORE
GEO --> PAY
```
---
## 10. Simulation lab design
Sysop simulates **Option A** under dev without WAN changes:
| HVM | Alpine packages | Role |
|-----|-----------------|------|
| `dev-web-1` | nginx, php81-fpm | Web tier |
| `dev-data-1` | mariadb | DB |
| `dev-ops-1` | git, gitea (optional) | Gitea + shortener |
| `dev-build-1` | docker | Builder |
| `dev-vpn-1` | wireguard-tools, openssh | VPN lab |
**Network:** `10.7.0.0/8` only; FE routes `dev.apps.f0xx.org` to dev-web when ready.
**Deliverable suggestion:** `docs/SIMULATION_CLUSTER.md` + scripts in phase 1 — not blocking this DR.
---
## 11. Cloud provider notes (GCP / Azure)
| Concern | GCP | Azure |
|---------|-----|-------|
| **Managed LB** | ILB + NEG | App Gateway / Front Door |
| **Object store** | GCS | Blob |
| **Managed MariaDB** | Cloud SQL | Azure Database for MariaDB |
| **UDP / WG** | harder | harder — keep on-prem FE DNAT longer |
| **ffmpeg burst** | MIG (CPU) | VMSS |
| **Egress cost** | Watch OTA/APK | Same |
| **CDN + geo WAF** | Cloud CDN / Cloud Armor | Azure Front Door + rules |
| **Multi-region** | GCS dual-region; Cloud CDN PoPs | Blob + AFD PoPs |
**DR:** Use cloud for **staging cluster first** (Option C partial) — validates IaC without prod risk. **CDN geo rules** prototype on staging before prod F3.
---
## 12. RBAC evolution
| Stage | Who | Access |
|-------|-----|--------|
| **Alpha (now)** | Developers = operators | Max privileges; shared session |
| **Beta** | Split **developer** vs **support operator** | Tickets/RA without build admin |
| **Commercial** | Customer admin + platform admin | Tenant isolation; audit export |
New roles MUST NOT require mobile app changes until F5 entitlements API exists.
---
## 13. Risks and mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| MariaDB migration downtime | Prod outage | Replica + cutover window; practice on dev |
| Config loss on sync | 503 (seen url-shortener) | `ensure_*` scripts; secrets outside tree |
| FE upstream misconfig | Partial outage | `nginx -t`; weighted rollout |
| WG UDP cloud move | VPN breaks | Keep DNAT path until PO signs UDP plan |
| Over-scoping F5F9 | Never ship SFU | [§9](#9-future-capabilities-priority-weighting) order |
| WAN saturation on OTA | Global outage perception | CDN + [§8b](#8b-network-bandwidth-dr) budgets |
| Geo policy bypass via VPN | Compliance / licensing | Separate VPN egress policy; audit logs |
| Wrong deny list | Legitimate users blocked | Staging test + `X-Debug-Country`; PO approval workflow |
| Simulation sprawl | Sysop time | Cap dev HVMs at 7; destroy scripts |
---
## 14. Estimates
Person-days (**single sysop + DEV support**), planning accuracy ±40%:
| Phase | Work | Days |
|-------|------|------|
| **P0** | SPEC/DR + simulation design (this doc) | 2 |
| **P1** | Split Docker build VM | 2 |
| **P2** | MariaDB dedicated VM + migration | 3 |
| **P3** | be-ops split (Gitea + shortener) | 1 |
| **P4** | Dev cluster HVM simulation (5 VMs) | 4 |
| **P5** | Staging env + promotion runbook | 3 |
| **P6** | be-web-2 + FE upstream HA | 2 |
| **P7** | Cloud object store for OTA | 2 |
| **P7b** | CDN + bandwidth metrics on graphs | 2 |
| **P8** | GEO policy schema + staging CDN geo test | 3 |
| **P9** | SFU/media (separate SPEC) | TBD |
| **Total (through P7b)** | | **~21 days** |
**Severity:** Strategic — parallel to alpha; no alpha gate.
---
## 15. Task dependency graph
Legend: **1** = highest priority (PO: post-alpha infra). Assignee **Sysop** = PO today; **Agent** = documentation/automation; **DEV** = app/config changes.
```mermaid
flowchart TB
T1["1. SPEC+DR scaling docs<br/>Agent ✓"]
T2["2. Split be-build Docker<br/>Sysop"]
T3["3. Dedicated be-data MariaDB<br/>Sysop"]
T4["4. ensure_* config hardening all services<br/>Agent"]
T5["5. Dev cluster simulation HVMs<br/>Sysop"]
T6["6. Staging env + DNS prefix<br/>Sysop + PO"]
T7["7. Promotion runbook dev→stg→prod<br/>PO"]
T8["8. be-web-2 + FE upstream<br/>Sysop"]
T9["9. Cloud object store OTA<br/>Sysop"]
T9b["9b. CDN + bandwidth metrics<br/>Sysop"]
T10["10. GEO policy + edge rules<br/>PO+DEV"]
T11["11. SFU owner SPEC<br/>PO"]
T12["12. ffmpeg worker pool<br/>DEV post-SFU"]
T13["13. Payments / entitlements SPEC<br/>PO"]
T1 --> T2
T1 --> T4
T2 --> T3
T3 --> T5
T4 --> T6
T5 --> T6
T6 --> T7
T7 --> T8
T3 --> T8
T8 --> T9
T9 --> T9b
T9b --> T10
T10 --> T11
T11 --> T12
T12 --> T13
style T1 fill:#86efac,color:#14532d
style T11 fill:#e5e7eb,color:#374151
style T12 fill:#e5e7eb,color:#374151
style T13 fill:#e5e7eb,color:#374151
```
| ID | Task | Assignee | Blocks |
|----|------|----------|--------|
| 1 | Scaling SPEC/DR | Agent | — |
| 2 | Docker → be-build | Sysop | DB split |
| 3 | MariaDB → be-data | Sysop | HA web |
| 4 | Post-sync config scripts (all services) | Agent | staging |
| 5 | Dev HVM simulation | Sysop | staging realism |
| 6 | Staging cluster | Sysop | promotion |
| 7 | Promotion runbook | PO | prod multi-node |
| 8 | Second web node | Sysop | cloud OTA |
| 9 | Object store OTA | Sysop | CDN |
| 9b | CDN + bandwidth dashboards | Sysop | GEO test |
| 10 | GEO policy + edge | PO+DEV | F3 commerce |
| 11 | SFU SPEC | PO | media workers |
| 12 | ffmpeg pool | DEV | commercial storage |
| 13 | Payments SPEC | PO | F6F8 |
**Parallel now (non-blocking):** alpha soak, url-shortener ops, Gitea mirrors, ticket **SVC-OTA**.
---
## 16. Resolved open questions
| # | Draft question | DR answer |
|---|----------------|-----------|
| Q1 | How many VMs in a cluster? | **Option B:** 5 prod VMs to start; **dev simulation:** 37 ([SPEC §6.3](../specs/20100612_1_scaling.md#63-reference-cluster-layout-prod)) |
| Q2 | “Other” architecture approaches? | **Option C** hybrid for staging; not prod-first ([§3.3](#33-option-c-cloud-first-hybrid)) |
| Q3 | Cost vs CPU/RAM balance? | Split by [§6](#6-service-split-rationale) profile table |
| Q4 | Seamless scaling / LB? | FE upstream → N PHP nodes; cloud managed LB in phase 5 ([SPEC §9](../specs/20100612_1_scaling.md#9-zero-downtime-and-load-balancing)) |
| Q5 | Simultaneous connections / RSSH? | [§8](#8-rssh-and-connection-capacity) — 100 SSH, 5k heartbeats per bastion pair (tuned) |
| Q6 | Alpine simulation? | [§10](#10-simulation-lab-design) — intra `10.7.0.0/8` HVMs only |
| Q7 | Future MCU/SFU/ffmpeg/storage/VPN/payments? | Platform **slots** F1F9; priority [§9](#9-future-capabilities-priority-weighting) |
| Q8 | RBAC model change? | [§12](#12-rbac-evolution) three-stage |
| Q9 | Keep current topology safe? | **Yes** — [SPEC §18](../specs/20100612_1_scaling.md#18-safety-constraints-current-topology) |
| Q10 | Network bandwidth metrics? | [§8b](#8b-network-bandwidth-dr) + [SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress) — CDN before web horizontal scale |
| Q11 | Global availability + GEO deny (e.g. Zambia)? | [§8c](#8c-global-availability-and-geo-segments-dr) + [SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy) — default allow, per-content deny |
---
## 17. Remaining open items (PO)
| ID | Question | Needed for |
|----|----------|------------|
| O1 | Target **monthly infra budget** for phase B (5 VMs)? | Option A vs B timing |
| O2 | **Staging URL** pattern: `staging.apps.f0xx.org` vs path prefix? | DNS + TLS |
| O3 | **Cloud vendor** preference GCP vs Azure vs none for 12 months? | P7P9 |
| O4 | Accept **maintenance window** for MariaDB cutover (minutes of read-only)? | P2 schedule |
| O5 | **Commercial VPN** — same WG infra or separate product stack? | F4 design |
| O6 | **SFU** — Janus on `:8089` vs greenfield mediasoup? | F1 SPEC ([20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)) |
| O7 | **Payments** — Stripe-only vs multi-provider? | F5 SPEC |
| O8 | **ISP/cloud egress cap** (GB/month) on FE WAN? | [§8b](#8b-network-bandwidth-dr) budgets |
| O9 | **CDN provider** (Cloudflare vs cloud-native vs none 12 mo)? | OTA + GEO WAF |
| O10 | **GEO deny lists** — PO-owned static JSON vs admin UI first? | F10 rollout |
| O11 | Recordings **451** vs silent 404 for denied regions? | UX + compliance |
*When blocked on O1O7, append to `tmp/YYYYMMDD_implementation_summary.pdf` per bottomline_reminder.*
---
## 18. Suggestions (PO / dev discretion)
| # | Suggestion |
|---|------------|
| S1 | Add **infra smoke** to `validate_be_services.sh` per VM role after split |
| S2 | Version **nginx upstream** fragments in `examples/crash_reporter/backend/deploy/` |
| S3 | Use **Docker** for PHP-FPM pools only after MariaDB split — avoid double complexity |
| S4 | Keep **Gitea** on be-ops until mirror count > 10 repos |
| S5 | Document **connection pool** sizes when RSSH device count > 500 |
| S6 | Re-read this DR when **SFU SPEC** lands — revise [§9](#9-future-capabilities-priority-weighting) |
| S7 | Add **WAN Mbps** graph series on FE before next OTA wide release |
| S8 | Staging test matrix: `X-Debug-Country: ZM` for geo deny paths |
---
## 19. Changelog
| Date | Change |
|------|--------|
| 2026-06-12 | Initial DR from draft `20100612_1_scaling.txt`; options matrix; task graph |
| 2026-06-12 | Rev. 2 — bandwidth (§8b), global/GEO (§8c), F10, task graph + open items |
---
## Related docs
- [SPEC — Platform scaling](../specs/20100612_1_scaling.md)
- [INFRA.md](../INFRA.md)
- [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md)
- [20260608_ALPHA_PRIORITIES.md](../20260608_ALPHA_PRIORITIES.md)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
%“Œ‹ž ReportLab Generated PDF document (opensource) %“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj 1 0 obj
<< <<
/F1 2 0 R /F2 3 0 R /F1 2 0 R /F2 3 0 R /F3 15 0 R /F4 17 0 R /F5 19 0 R
>> >>
endobj endobj
2 0 obj 2 0 obj
@@ -17,17 +17,83 @@ endobj
endobj endobj
4 0 obj 4 0 obj
<< <<
/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << /Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 716.2874 129.725 727.2874 ] /Subtype /Link /Type /Annot
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>> >>
endobj endobj
5 0 obj 5 0 obj
<< <<
/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << /Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 705.2874 115.477 716.2874 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 694.2874 112.821 705.2874 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 683.2874 176.853 694.2874 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 672.8499 100.53 682.8499 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 662.8499 122.2125 672.8499 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 652.8499 135.1275 662.8499 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 642.2874 130.157 653.2874 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 631.2874 189.741 642.2874 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 620.2874 159.501 631.2874 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 609.2874 98.59704 620.2874 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
16 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R ] /Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 35 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
17 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
18 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 35 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans << >> /Rotate 0 /Trans <<
@@ -35,59 +101,170 @@ endobj
/Type /Page /Type /Page
>> >>
endobj endobj
6 0 obj 19 0 obj
<< <<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>> >>
endobj endobj
7 0 obj 20 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185339+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185339+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 35 0 R /Resources <<
/Subject (\(unspecified\)) /Title (Grafana vs Others GraphVis Pivot) /Trapped /False /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>> >>
endobj endobj
8 0 obj 21 0 obj
<< <<
/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages /Outlines 23 0 R /PageMode /UseNone /Pages 35 0 R /Type /Catalog
>> >>
endobj endobj
9 0 obj 22 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2776 /Author (Android Cast project) /CreationDate (D:20260612190936+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190936+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 Graph Visualization Pivot) /Trapped /False
>>
endobj
23 0 obj
<<
/Count 12 /First 24 0 R /Last 34 0 R /Type /Outlines
>>
endobj
24 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 25 0 R /Parent 23 0 R /Title (Context and constraints)
>>
endobj
25 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 26 0 R /Parent 23 0 R /Prev 24 0 R /Title (Executive summary)
>>
endobj
26 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 27 0 R /Parent 23 0 R /Prev 25 0 R /Title (Option comparison)
>>
endobj
27 0 obj
<<
/Count 3 /Dest [ 18 0 R /Fit ] /First 28 0 R /Last 30 0 R /Next 31 0 R /Parent 23 0 R
/Prev 26 0 R /Title (Role-based graph packs \(suggested\))
>>
endobj
28 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 29 0 R /Parent 27 0 R /Title (User graphs)
>>
endobj
29 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 30 0 R /Parent 27 0 R /Prev 28 0 R /Title (Slug admin graphs)
>>
endobj
30 0 obj
<<
/Dest [ 18 0 R /Fit ] /Parent 27 0 R /Prev 29 0 R /Title (Platform admin graphs)
>>
endobj
31 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 32 0 R /Parent 23 0 R /Prev 27 0 R /Title (Estimates \(engineering\))
>>
endobj
32 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 33 0 R /Parent 23 0 R /Prev 31 0 R /Title (Recommendation under your constraints)
>>
endobj
33 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 34 0 R /Parent 23 0 R /Prev 32 0 R /Title (Implementation slice \(proposed\))
>>
endobj
34 0 obj
<<
/Dest [ 20 0 R /Fit ] /Parent 23 0 R /Prev 33 0 R /Title (Source linkage)
>>
endobj
35 0 obj
<<
/Count 3 /Kids [ 16 0 R 18 0 R 20 0 R ] /Type /Pages
>>
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 737
>> >>
stream stream
Gat=-gNV@2'7\>80opEDN%JCNSnO:1p\WYi:$A0sa>lE#(8-Yl?-=R's8!bXp]>6#b970(,L;>2qAWjnN\<cuoiD-cVD.j_J3dR(#K+t@R!sb$l,Bh6A%U3524^*E!S?iugU&P[%`t1-&^rX(1WPODHD%f,)-[<@M[ST[<+:sJ$:PL[4=Pjn.DRYpgN<rAl-fV:P:/t3*l&4")8$O?_O$K69DF^/S;-q`Rdi@h8%t"i%l!r$VSBst/dp2(420FU[]H@W[Vh$E"8:XH[C-IF%2]n)O\-.UrA\4-iToRZ,@!RBi6aX1W;k#7?jKU<!R3"qGnP/4coBtj$[K[FON&HURSgm&TFhVl)>AhcSu"r&e($"mo;(s%%?@Qag7A``0rkbs5)J?AGRhp%1s[ttE.u%UKDBH,<5,ef`cg)P:%>Pts*ad+lQ"u$]4^Uj]n:_&e%RC`]ZJ`_j?Kp:^j'+O:04(SLB/iV[pWUB0_ijp(j,W"+L\0%[W)f?5A?Lk4jM<ml[We5MPhO>d+AqT9='%l/LNQ21!ti!V;i*NAFEn@31950*+eJ"408#k]P3N7D6CXgCN6r$"TNQKP;'iFKpP)fm$EebPH/u7_Z.?tPgeA_0->KMBT[?G.'f*.k_M$d_>ZV&5K1ce2uPd8jU+Z7AlR>1>cj0N^%6'hF"1Xj/G:75P5L+LS\b/gq-;!M]4.i9UMLokXK\oEi9);B=7H"^,?AE]SAd;o7%^q`QAmWg)T6P+LcA3/,;SM*&OGcAd?gcQkK3\`k?@eBZR&8`c\:_HCG(&_Z70Oh[X.R1r;HLZo8%1Ukl(7R0A$]lX_J9F\rOpE#g\C(2[T?+a*M6*@nR%OCb-#&Y"AaRNs5Vd,lq4>K7L"cJ8LuG^f<tA<E&a=9/)<E@=G1\[4%=;hUQuo_piLS$LK_.C4ko6Wsr?0od]`HeaR3ZBQ3^kr&j"T"sREVB\K"sPs!6*b%lf!hQF^u"]/'H:7's4SRCqr\8IQO>elfW=i__G_@>upn-j[__69B*S$KCe^f0B.\D_!1Vt(.MW7(J:qc<rEMQhU+!,ju(!nJskBCPP-E`8Q@Yem-WUBQ7@!1of)7Y1r3XL",r=Afq=##\ZjL4@FV;?>ld!mQi\ej>,]Xq9-B@>SHf/6'O\,R92\+H52[$7Qdp9)9Ng\3YNneO5/=XjHtfQGt=bSl24I^ni<]YM#,(]L&QP&2'[0nft(Bb-fCCKB5RoCAV`0JO+j"B/pJ)C#V*=E#:&4XuUhBGfUV`L6H8-Yk$i%j2blbBMP".Kmpua/'[0a'=qR\#]V&RV'.l+26Ru>@UI0^ZWp&]$bB5"3s'A)(t+H4R%5sM=a%43[$W3Z4=G'3o.\*N*-G?C'&XDHk$$UbJEsY<f3Q1*2o2LgN^KOqDRDX8$ZSc,Q@i#`E/qb;]83_rBVu`gVWPFT:<qktR)[tLOr-.eNmU,TE0_UO7du)Afe($^FNlXbpFHWs._.RZRD07oNX+'gfdk]/gDY7'JhMbNOB#H<M\U"t)?"&teL'moS=BI'edG1G+XD'K`KF5g@'E[Q1mSLfZL+6oKR)2NW]^ZJ=Eoe>Fg"S,dr&+R\L@>"fc,,"3Le@T1qToKEgU@M6;T/tedBSA<d\9eDXnb0iC0[(9O(X&)iL_(l'hJXZFJVF"+/`\j4$A<Nisf2p6sFg>NrQZ89N3+Q=(Yi_64j=+m>I5gg8I<lYoc]^ro?Y)HRCRS)"HXg0Ju25QL0:W]QOf]b2od%X,$m(WRCCN#19QaOKtS!MhO<:b*T,O`;Hu(=@Hf)9dA^<"+>qiTReLVouPOQKD/,P#3n?fG`W+oG'($p+g6L8_6CK_2l@ROK9[(_@T*=)=,!KW+,*5pYrt(Yr>5F.-%@3DSQkk73WgpTPmfqF`=t2heO((^^kau69`FhYhg7qUHC)Q!cqM_'\nhU*5uj6\i-BYAf%Lg/`(2g!sNnO&N?s%9Hi2ja<e&O*)5VKl^fi4!Wa*$:Xfr8eOa/]2!S)?<(Jr!4WGj4B^lO1Bn[/Bd8L'aPe7%:DR=):OqS$fG@u5cbMrfFC1G;7fO,tU=d*CJPFt<N9p/hDQ&)c86R(1r4Eha(;I$W.`=[_:*gcM6;:A7o.XWE(eY8j`Nf4*b9%;RKSdEe;qJ"m@eDR=?6<;Q"NW9?)?3VT\TR4$Eel'BLZ-lE4I#"oL:@uGC=uR@HY_aEF@GH6$$N)Pe;fC]C!h.\ec.n0pV279NkRmJNJpA-p)qf7l\:qe&;)<<'SL*@bS]Z+TFK9g-a2;5e\,<Vi;Z..iFfdFS)2oZujVhSpS949cCt4?G7#<pa0VdB<`V*2U48D`G6H$NCi7O'\.EDn<>'tudE=J`_UO[7IAgMI$EBrO/Tu'B:QE!RgJc2:S#rr7K=K?kA-!mGL!ia*"5D/n[%$gYS0<O.FQAgROo!%N+rnG_!BK#B,\f<"1/W9QKTk'$UR*d=,j+4jQP))gMhZu\h0Z7QR1)bnTp!o<A,[B3[5@VMN4\R5uiWA6'_[u76O5&DmA53q7h@2>m67Pj5neGFTrp7O'`\@e2HTKY(6^5,Bemi?K\A=kE>Uc9s_K^fki"CZ"jE/DZ^2*\iCV<L/(dV%NCeOiS=t3]F7J;\9pP+d+kVJTCR].ue^AWPP0o29)"\rob!VXYkpu>D6\.H3p^FW,Vs$aLn29BKkNaZXb[XUef!iR9S=G5m.JspFa#H2>jP3m5o\JY"QpC>0E$0D^VZXi2ZgSg4pC/G?``$daH+AKBpl(*pTZS^8gh<e:fE:GT(>fAu~>endstream Gatn$?Z2Df'ZJu$.IL\,WLdu+mk*joC/VHN/_a4"a9@?D!LqahD>3:-TM;mDgIR$[5_<dsO2JNB"L'sYGKq[gpF3bG'-J";2%Ul*OC#h93r(KR:R-BZ_Do=p6rAWAQhYHhH]2o;]taa2,pA:X5Z'c[T7AO@m[[lA'1#e^#mqqk,q*U6#c=c]Xi?Ib0V-<j/!7^Q1DZ:@7W>2,]n(+=1oqBUR2&?26L9q"KCh7$DrCQ>=#f1mmPmZV&equT#FHb[J]$ds[>0@*S$X9qSAFTPCREDTWo3/S2P5DdX`e<Qm"T;^.U(]"0WVd=6rj61q%'\P68X]`Yi>m.fbY(kS)D!=oYp<6b3hk3C(6SoQ:kut!4k.[Y4"ON(3dKT:Apn%IB^Lth?"OpR2Eqo]s/D,+*c@=Z/]5\Wabhps6=#Za&;qBG8\\*<AQ4cOos4ofDPZDl8Pa(kBG/>*4hq4c*FI&jjZ+U_2[RPnn\;&F?1!'Jc=4n.-S:BU\\`8mWf!^+UlC+^FboV=C):o*$H.8@8Sel<k:=,_oJPFKX%7\QFB`LZ4N1?/S$Ra$oAhRS_UVA`F[opo8Ut"/fn#f:YRBd&s#KRZ)UIbLhojt8s+++;6<j#beRi,d9&67c3gIV$Fs^O+!OF@1P>UJZ31J"jJh&P1XS:OM*9b.+_.dLmY5AFYO*,D\6!ta0)ph?F4^eJmhUN20^JGs\lROrB9m>T8iJr>NRA"O=9+Ol^LS5"~>endstream
endobj endobj
10 0 obj 37 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1790 /Filter [ /ASCII85Decode /FlateDecode ] /Length 2947
>> >>
stream stream
Gatm<>Bed\&:Vs/R)cS5NFhpS53:40GIMpfVEF!0Ym""j$S0Drm'#</N/aN]_7PI)*&7"CUMiWM2\9rt7EfD-"5HX[nf%Vh_K6"#ZTX\)L(Dak^G!ZSVB=NP9*6!I+L6F\Hh\SL*FkE,$Uc*XBEZd(!F:B,J?Gs3/j1.;b\!\N^krhhZ;SIg-$#kB4C.Me/dfn#.-=<DhSAbF9],+H^>`oA7c<#H%O*!Hd-lZs4-<0a$9!kG-]GhNI5gkU%]n%IK]!$**CV4X$f@,bln)]3f6U%#m@.Q(3"L2If+3/s+>.e_;fJrs`=VK)Cj:/R@6K.f4[ug5'&<O!_t-j5jMsOjM&Tn\EARmqpHg:E1.Z[;Q<^R&3l,@ZJJ6Bb:-LD#ncI"m+?K1o9jMtm&uMMB"giE]j._U0pVYss@''lb-<m'+8iC/_K2bC5.(I#4:=0>#SeY^\QB8CP24U6M%X]cFG0jf6KS.o$rih.XI@Lap=')EGiSi5fJlaVO-'m7n(L7qKGk/!3N&6K45crt'1lQAbSX>/,aFru6rj7!;,)*uDX11Sl3n+Nj=sr]4),:T0Gj_j=M`>?[AqYq[TgW&A+`h5gi^rq`iJ'<-3P*c@_"[R24*!^7-a<LHq!l5piO-??o?Ak@UC=jSK@BkVg%sCTbj3O+%tf(Ac[_eAJG3^&J>:[\\"IF#R^f.b]JatdcBIS6:"H\^)g#[a_hN#FmKRIuf:CE5H7/4!^<l?ie/q6AcM&uG+)g>d#cc(;WiC,=*n<.;T)rr)@LdlB.G4SN<65No)8$=L!f=keBqe#dK<%S_WsYI"`L\kl]`VDL*4)dbYU'sNrq/Fk-!?f0$?GjRP\[L@9BBV8R<EBg/B5qlOd^fFk&0!>_4f\<o-BuI_]qt"4Qf.J_L]9h\V*sc92pR5V'H_.3kSt6(jqD:@d(?lIpSe=2k1gilf\o638sFbKH]-;:m;=6Rk3#LiTTV!,cqlN8/e5\:X0bQ(*"f_%O[Js:pO+Tf3QD!5jKQfp@U3m"b3WmEXh!Ia7&gJj1&,h]T@1\78fPq)-c'oMt.U*YBb)]`<]pbiZFaYKpaHUT"Gc]4AZgHl5`'=*%ci8Th<G!hX(*NX[RueHI"/J$T?&ZQjJVL)`9V!CF7F87kf[5l>#YmWiVe%q4Jt6c(pS-XV>I)FnWm.^1:BKja4%FfF4+o/JSqVq*_>`@H@j0[d$#['G!a@+:tl=(dc9([l8M5]aNk&i8Y\fVpqbCYnUY+5$5!p*-N"@X4P.A#?`\%$>D!b:FqhjUJ:D^==q-/E_j#._gVRfbmH0?I(ZM9r=RjX(_#F#HQ/d^>e7Ceg;5/V)l:A'-pF*3]gDagierGWnJp:,Icugn^2KG^IHhlffJ2?L7\^%b0YJ@[Q:0$o3b"JP&ZGGOL>@6$&u6^+U.DG+T6J>P3JcsXbDAE(-7mD1@87km6o)0oN`iCD%]NUDb0NM!LC"rOHBY5q_O4BLS>eB,f77.BG9%i.a;shSW'u&.RkomQnGq+;DiNql4FtWeiAn?-arSSdeet"+SaUYX_d&3e]ou].;VQ'G[Ac:[=-TjOX/WhqRPYY9Um"X4`>S5MSjMJ)oih>k7Cj`C!uPWi"KJWGL*VU'TXiG"$6"&,[3\pP'brr.+DaFWY!r@h6b:$85G@Af#5uuYJZUhVpi6Tc?6$I@o:BPVTs"AjQ;6lU[1;FY@=!/r?auN6^G5'WToSA?E7>e&X7Qf[C(:/CYNg%p9Y/bbdS5os[[Kn<2OW'!Vq,sBm9gSI%7rq%Z+#A:hR&2[nLpgIF2e~>endstream Gatm>>C4gY'>Eq=R*2c]Nob?)<IqH[+0Ypc?#N\H8ORtnP#Lr?^QBKtDOQ3Km*f6D7EWhfNphp$^r@I*rf&^a[o4,n+@$p6c@C/*#Lh"<42(E;6.RDHh[1Rbp)o;<bBG<TILin>h^4P;_"M\$^He6`A`HrX9J^5=6>1VrqLd+5q49'Z%?BfY,9-Z(?GZ7M9`NPb^Z!L6!IQE)]hf*@kC@&2ThQ":YV"W&^&82QcuNqa3cMm@g`SSl+sa*5ipT+i:>,fbTBh@GXX_B=qAFd%b^*lED:C(@^+46Cr[=Fb!ca>q,q.*)m%HG[a*Ta-)hMAXkQ"@f.0@mQ9APdcKHH)kO4TK`S7a\?;XkSfhgDte-a"5Hc[#KpY+T$aUKO#D?XKbio_i!pLN)I#3u,&`W=btlD5.ahg_\I-7jHVn\7ubIe!G=\#uq\&W_@\3'ef.J/^c.pa\n_:p$jp09=WU2LddGe*iNTUhHG_%n9b_"J<C8.VCVnM$Gkg%X$1a7]lOYPQ(]=p8D.[mR@ka1i([,A"e)U@)"JRpa[C2kDjuF?#L@>ADLkK^3R@n'Hcih@Pc@k0ZC4)j>m-kMdD_aY\Ga>)9(=20=_/HcWug'on6GG9Ei-STSY=Vc93?62OM:3*4p.r1VE+RWQrl5>!!sGm<p@cF0?[4,0a#C8R]E\O/3FVHT&=$(6.-WD=+.I?KtFa<o)7EFkh$d.ID!IjOc,!HMF7L\bQoKp1P"I%!NYU7j+W"aC,)#J8dmEc$sh<:('Bc@WqJ7e(F=f6T=_YM\[MC\iZcQJSm_'7Z4mJ%(GF4S@eCjgZ?;^%ni=Os;>m*20`M.'mS`*mU47Rf$lRM$P<,UM8jgi<H-H+U7/W(b:.t3e?:%oo^t[VH$d?,BWfI_bg"#438YCnrbV/1&7?2qOO];Vu?oVH.pU<*MclEjj$^9%^8B**RIBnqV\u0/6InRZf*=GL@QUSWT>7f=I]Or\>Z6RE_?*N*U@##=rU$kZ;".aK1g*B'8-^I,i!(OoG."^,Z85fJhO$m=;LeH;h/47^8mGI4_+ibU1H:A_>]K(S%XWn$$3bo_u]Kh?,7)=>A,PBdKCB>/Zm95j''hCl/7B!a1.,Tkj5;I7a`sV1k'T!ET8Jo?h/g'i:G;1Up2Ibe,oF9.KNkQ7//c&`s3cOd3,'819-!drR[,#)t&U+3>\6T1)&>M:L[Q&].;F+G.L0S+aNYbYCq&3+1cI.I)-"_#)P)<hSJYl*hX;Aooko[u+G\MM[/2t.F]gsAaL8@SW1$,DMBuX)q6Q-%R>fOG+\.Ji(GD5<ShI(eAmEr#^`KH'(f*/m=U-ueofPtgi6(=5jAXb/<Pq/H2)S[K3D'`rX4CD(2%aInKks0s'\d(4_1![Tsm^1DD>%9a<(%dm#5Xl4&GeG^8m7jirBi>@l`TU;._`r:i@eu?.CUHYiU%l-%%9L^hD?$6#`l>JbL*]KN&j_=WHe*1,(s__g!BGLA4Wn10#!I6j@.Jii#oUnKUVTRd@:FkjqBUrN(HRX*2#8r2.)AYqWT!IabUQVL9t8GGp4km%PcOc]5\nX'Ep1M92U8c@bZZ/J8`@1%LMQi]h0*:2aB6;6QN.)J`uU]L_>f!j=HO8C5(W+YhuCDUPkEPAN,2G[EJ;K_CtRRim:RNqRrb,6]?)2bk\T1Wfg$P*ft^UoJUjN%JZF*L&?g7#c)G!US/c0>>?5[OMg!kJ]NXQG]3VMI^[4CD?8/Y$^th3[o%^H40G$Esf0+LE=29Dd0!OueNtN[0<qA>hU3A#W2?FG6T)d9M22W:Y-Mi@t#XA!X^bG434S!=CE`u3c_>0]`a&Z;T6EGtUYT.+ZYqem]cLoORQX)VSpYjTuIR=i/RWMMJ>+`'iA*8Pk-E5S(s'_`m)a?(NfLLU6Eis;e#ae8QWGluWP=)1^53#=*r2=)3N!,mZm>L9G?7K6tQI9k:.5T9&7j:/Y>;'OoR.$"KM09O2:h^=1iU2"4mK6tZa(?7@2aUci5$.cn]!SGjL6j]IONPf<;N2Y-!RKpG-^aN>m2$\*p<`n$4)/\C"1uqT,t2af5IFHbR*$I:30X@f67KL_"3m#WSu@?_c9)tSbUi3i+kQm[N5GdiGb,UF]qgP2%ba]=#"G&5/1Ng$.lk4r]9Jaeh]joXoC)=k`/T>hIA@4r%cX\@]<OhTUU=3hUiT3ZLe=+K#!ug2'A%>_^*_HVVN5u9GC$s5HT!,:*ES7kc8F*p>]jZ7lfa3p+m$dm^?"X[O"/qk_u6.!f_sBd:pXKKeC]G&JMMkd@injDZbtS>!_Qisl23k0H$9ZUGp3U6q_r_kE&C!G1/VKsgh.,\fOm^hO_pnO&bZ2cXq4r)C7gLoluQ_9m"nAlfKuA'8"DI+7%Y_i->+]WBi.3^$g^OY;ZL=J2E>MgK,M;@H+cfs$,V*Zlf_gjlu"W?VR\J<L,1Wa2uk1m_'H:V%auYTaab?O9R>*h,=_s%Y()](\V!K2*]f%FC4A;$eda)*WIB6Cp9QpE>(_*JU]PoC6US2g(Hk&?"dZld-^/Fml=@2k9-1RbE$jEieEr)^h*%XT_s.&nkW&Ol.)Y:F7MsCj)'#-[*M2e<SctZJk1cl0aT)t_VsfXt!@AXY5O*$9_%Wd?bcP,X0c:R5$FO8%'^"Ar-5`4K3GtDH'<#;,liH5dcJY]AJlp*_b8hL(T0?'rp4s]Iq#5n&/;p(('s;ildb8cS#AG26=Aq:#\8!q+DK[pWFj_D7jg9Areo'ptUoh-#%ako"9sE=VHO"9;Mg<Qus5:#b)[(e]nJlh6]bgB(\S>KeLc(V@f2-M#R;ds3f(7u!*='0O4On]E%HI\'oN-SL4^P@O,3X7a>kLDEIb;?`Yj#OO<j0P@<Lt>3?"#rVm#<[Eg$!iWK&^cE&.AKX=LnC!KLA)!EGjT;(,NnaQ!1hFq^$1Rh)pEYdE>PeZIMT)$lApKE;-r$~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 860
>>
stream
GatUqh/;4+&;BTM'QZjT,2b]lh)jiXY#Mg9bp<uXZ'0F%`*(iGo(G.gJ6%JDdoMc"(ruVfc8cAc4q?R9irWU,61:6BnKCMT%)!fP[r<(Qr&l4XQ,CBVNJ%3A_XZ7d#3S@$%faNRaaPo#$QppLo^2CTgMq$gm(8rhkkH2mCQUD4L$0%"n='TPVh\J)\5=I$YS)>cHa,,]U9W,#Q.rE%O9h\Bk@UJ2QLP!r1!Xb)otSX0nDJLfjh0NJ3Fn+bD.E[R?Sc(hB1kf24&-*=<05[1dF*!ZJCZ%L=u%eE`$pM=;EQi`f=F[BU[V"<GH=:bgOnibljQ%E0EsIn-FBS1;o9?k1>dUQJ-h,7QW#kra6DXd$<gH(9L5<i1`$R%1KY$KakBYIcFaFhJQ0b!akEs0K(hr+LaDcn7L@uNIOiBD#ASoC60BHS0^ht5<`3us5AbDO:Yo44PFWp>>k1:IHuDh.@Z*S'#5_u&lPp;U%4IWcd'9Z'+mJi:OH#j([V:gdmi/7;H9k=]a>d13af08pm@5+\kZhgi_&$;+.cuM84bL@[[PO`F1L6[V1`DeF9Og)-I$`K;g==[@J%arrh(;K>FQjZX?Dh)O$;nMa%P!ofh3:K504^`$NpKYHfKhH0httgrr*.!P==_T0k.Q9L<:O5?V9!LI)IS==.kI?+7/Y7<jJ%%1[!m5g$/4nHl^\JI\?DEJoDMrK]>"=nGepjar[cGNI.*s2.,7VDN33a@B8`MhrWc?[/*u*Wk9RtgA9b-i<'<%J0%&OC=I_&_#L8!r9:Ojo$J#(-!m11t=8tn`TBJ>3K:"D3le]nHjEbi*=fh6^,+R)><7*1"$,*:U8fFmBaT3BOqu]DT!Ik~>endstream
endobj endobj
xref xref
0 11 0 39
0000000000 65535 f 0000000000 65535 f
0000000061 00000 n 0000000061 00000 n
0000000102 00000 n 0000000135 00000 n
0000000209 00000 n 0000000242 00000 n
0000000321 00000 n 0000000354 00000 n
0000000524 00000 n 0000000501 00000 n
0000000728 00000 n 0000000648 00000 n
0000000796 00000 n 0000000795 00000 n
0000001102 00000 n 0000000942 00000 n
0000001167 00000 n 0000001088 00000 n
0000004034 00000 n 0000001236 00000 n
0000001385 00000 n
0000001533 00000 n
0000001681 00000 n
0000001829 00000 n
0000001978 00000 n
0000002094 00000 n
0000002386 00000 n
0000002464 00000 n
0000002670 00000 n
0000002776 00000 n
0000002982 00000 n
0000003069 00000 n
0000003387 00000 n
0000003462 00000 n
0000003567 00000 n
0000003679 00000 n
0000003791 00000 n
0000003961 00000 n
0000004054 00000 n
0000004166 00000 n
0000004269 00000 n
0000004389 00000 n
0000004521 00000 n
0000004649 00000 n
0000004745 00000 n
0000004820 00000 n
0000005648 00000 n
0000008687 00000 n
trailer trailer
<< <<
/ID /ID
[<4acbacb74072b1ab8591e187494a02fa><4acbacb74072b1ab8591e187494a02fa>] [<cbce618176a4381f5b7db1e5aea50bfd><cbce618176a4381f5b7db1e5aea50bfd>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 7 0 R /Info 22 0 R
/Root 6 0 R /Root 21 0 R
/Size 11 /Size 39
>> >>
startxref startxref
5916 9638
%%EOF %%EOF

View File

@@ -153,7 +153,7 @@ endobj
endobj endobj
27 0 obj 27 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190936+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190936+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False
>> >>
endobj endobj
@@ -319,7 +319,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<a00077b7d6dcf09115ddf11e77b9015b><a00077b7d6dcf09115ddf11e77b9015b>] [<5381a07cd3154e970925d8c0b9b894f6><5381a07cd3154e970925d8c0b9b894f6>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 27 0 R /Info 27 0 R

View File

@@ -112,7 +112,7 @@ endobj
endobj endobj
20 0 obj 20 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190936+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190936+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False /Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False
>> >>
endobj endobj
@@ -232,7 +232,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<a8f81650784ccaabadb1f16a786b68af><a8f81650784ccaabadb1f16a786b68af>] [<3a7ba17afddd149c4cf82e5c80686f4b><3a7ba17afddd149c4cf82e5c80686f4b>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R /Info 20 0 R

File diff suppressed because one or more lines are too long

View File

@@ -133,7 +133,7 @@ endobj
endobj endobj
24 0 obj 24 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Opus / Speex / stream protection \204 validation checklist \(task 3\)) /Trapped /False /Subject (\(unspecified\)) /Title (Opus / Speex / stream protection \204 validation checklist \(task 3\)) /Trapped /False
>> >>
endobj endobj
@@ -276,7 +276,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<614bd83675b640eed7dfdacefc7557db><614bd83675b640eed7dfdacefc7557db>] [<0ddefe241699114217c6a77061fbf8ca><0ddefe241699114217c6a77061fbf8ca>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 24 0 R /Info 24 0 R

View File

@@ -112,7 +112,7 @@ endobj
endobj endobj
20 0 obj 20 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False /Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False
>> >>
endobj endobj
@@ -233,7 +233,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<d6d86048f2e155d455a13032ad45a795><d6d86048f2e155d455a13032ad45a795>] [<9884c574c7519d156c7454ca12b46b44><9884c574c7519d156c7454ca12b46b44>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R /Info 20 0 R

View File

@@ -97,7 +97,7 @@ endobj
endobj endobj
17 0 obj 17 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False /Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False
>> >>
endobj endobj
@@ -202,7 +202,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<3fa21412f8ad7895eed1172469be6cd1><3fa21412f8ad7895eed1172469be6cd1>] [<dfaa46b9252e64daf7c85ea1894037ba><dfaa46b9252e64daf7c85ea1894037ba>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R /Info 17 0 R

View File

@@ -59,6 +59,7 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
| Document | Summary | | Document | Summary |
|----------|---------| |----------|---------|
| [20100611_3_url_shortener.md](specs/20100611_3_url_shortener.md) | URL shortener service — SPEC (`s.f0xx.org`, post-alpha). | | [20100611_3_url_shortener.md](specs/20100611_3_url_shortener.md) | URL shortener service — SPEC (`s.f0xx.org`, post-alpha). |
| [20100612_1_scaling.md](specs/20100612_1_scaling.md) | Platform scaling — multi-VM clusters, dev/staging/prod, cloud path ([PDF](specs/20100612_1_scaling.pdf)). |
--- ---
@@ -67,6 +68,7 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
| Document | Summary | | Document | Summary |
|----------|---------| |----------|---------|
| [20100611_3_url_shortener.md](DRs/20100611_3_url_shortener.md) | URL shortener — design review (auth, slug algo, task graph). | | [20100611_3_url_shortener.md](DRs/20100611_3_url_shortener.md) | URL shortener — design review (auth, slug algo, task graph). |
| [20100612_1_scaling.md](DRs/20100612_1_scaling.md) | Platform scaling — architecture options, RSSH capacity, task graph ([PDF](DRs/20100612_1_scaling.pdf)). |
--- ---
@@ -75,6 +77,8 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
### Infrastructure and deploy ### Infrastructure and deploy
- [INFRA.md](INFRA.md) — FE/BE topology, nginx, ports ([§1 Topology](INFRA.md#1-topology)) - [INFRA.md](INFRA.md) — FE/BE topology, nginx, ports ([§1 Topology](INFRA.md#1-topology))
- [specs/20100612_1_scaling.md](specs/20100612_1_scaling.md) — scaling SPEC ([§6 Target architecture](specs/20100612_1_scaling.md#6-target-architecture))
- [DRs/20100612_1_scaling.md](DRs/20100612_1_scaling.md) — scaling DR ([§15 Task graph](DRs/20100612_1_scaling.md#15-task-dependency-graph))
- [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker CI, builder ([§Docker CI](BUILD_DEPLOY.md#docker-ci)) - [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker CI, builder ([§Docker CI](BUILD_DEPLOY.md#docker-ci))
- [OTA.md](OTA.md) — OTA v0 layout ([§URL layout](OTA.md#url-layout)) - [OTA.md](OTA.md) — OTA v0 layout ([§URL layout](OTA.md#url-layout))
@@ -127,3 +131,14 @@ Section links use GitHub/Cursor-style heading anchors. After adding or renaming
python3 scripts/generate-docs-toc.py python3 scripts/generate-docs-toc.py
``` ```
## PDF export (markdown → PDF)
```bash
pip install -r docs/_pdf_build/requirements-pdf.txt # reportlab
bash scripts/build-all-docs-pdf.sh
```
- Renders every `docs/**/*.md` to a sibling `.pdf` (skips `drafts/` and `_pdf_build/`).
- **` ```mermaid ` blocks** are rendered as PNG diagrams via `@mermaid-js/mermaid-cli` (needs `npx` + Chromium); cached under `docs/_pdf_build/mermaid_cache/` (gitignored — regenerated on build).
- Dedicated diagram PDFs (pre-built PNGs): `build_git_flow_pdf.py`, `build_graphvis_pivot_pdf.py`, etc.

View File

@@ -2,7 +2,7 @@
%“Œ‹ž ReportLab Generated PDF document (opensource) %“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj 1 0 obj
<< <<
/F1 2 0 R /F2 3 0 R /F3 15 0 R /F4 17 0 R /F5 18 0 R /F1 2 0 R /F2 3 0 R /F3 15 0 R /F4 17 0 R /F5 19 0 R
>> >>
endobj endobj
2 0 obj 2 0 obj
@@ -17,68 +17,78 @@ endobj
endobj endobj
4 0 obj 4 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 716.2874 95.92504 727.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 716.2874 95.92504 727.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
5 0 obj 5 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 705.2874 95.03704 716.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 705.2874 95.03704 716.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
6 0 obj 6 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 694.2874 99.91704 705.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 694.2874 99.91704 705.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
7 0 obj 7 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 683.2874 74.14104 694.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 683.2874 74.14104 694.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
8 0 obj 8 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 59.68504 672.8499 142.6425 682.8499 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 672.8499 142.6425 682.8499 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
9 0 obj 9 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 59.68504 662.8499 132.21 672.8499 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 662.8499 132.21 672.8499 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
10 0 obj 10 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 59.68504 652.8499 106.38 662.8499 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 652.8499 106.38 662.8499 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
11 0 obj 11 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 59.68504 642.8499 107.2125 652.8499 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 642.8499 107.2125 652.8499 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
12 0 obj 12 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 632.8499 125.97 642.8499 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 632.8499 125.97 642.8499 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
13 0 obj 13 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 622.2874 102.165 633.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 622.2874 102.165 633.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
14 0 obj 14 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 611.2874 109.701 622.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 611.2874 109.701 622.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
15 0 obj 15 0 obj
<< <<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font /BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>> >>
endobj endobj
16 0 obj 16 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 600.2874 158.933 611.2874 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
18 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R /Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R ] /Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 35 0 R /Resources << 14 0 R 16 0 R ] /Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 37 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 >> /Rotate 0
/Trans << /Trans <<
@@ -86,29 +96,14 @@ endobj
>> /Type /Page >> /Type /Page
>> >>
endobj endobj
17 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
18 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
19 0 obj 19 0 obj
<< <<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 35 0 R /Resources << /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>> >>
endobj endobj
20 0 obj 20 0 obj
<< <<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 35 0 R /Resources << /Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 37 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans << >> /Rotate 0 /Trans <<
@@ -118,104 +113,119 @@ endobj
endobj endobj
21 0 obj 21 0 obj
<< <<
/Outlines 23 0 R /PageMode /UseNone /Pages 35 0 R /Type /Catalog /Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 37 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>> >>
endobj endobj
22 0 obj 22 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Outlines 24 0 R /PageMode /UseNone /Pages 37 0 R /Type /Catalog
/Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False
>> >>
endobj endobj
23 0 obj 23 0 obj
<< <<
/Count 12 /First 24 0 R /Last 34 0 R /Type /Outlines /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False
>> >>
endobj endobj
24 0 obj 24 0 obj
<< <<
/Dest [ 19 0 R /Fit ] /Next 25 0 R /Parent 23 0 R /Title (All documents) /Count 13 /First 25 0 R /Last 36 0 R /Type /Outlines
>> >>
endobj endobj
25 0 obj 25 0 obj
<< <<
/Dest [ 19 0 R /Fit ] /Next 26 0 R /Parent 23 0 R /Prev 24 0 R /Title (Specifications) /Dest [ 20 0 R /Fit ] /Next 26 0 R /Parent 24 0 R /Title (All documents)
>> >>
endobj endobj
26 0 obj 26 0 obj
<< <<
/Dest [ 19 0 R /Fit ] /Next 27 0 R /Parent 23 0 R /Prev 25 0 R /Title (Design reviews) /Dest [ 20 0 R /Fit ] /Next 27 0 R /Parent 24 0 R /Prev 25 0 R /Title (Specifications)
>> >>
endobj endobj
27 0 obj 27 0 obj
<< <<
/Count 5 /Dest [ 19 0 R /Fit ] /First 28 0 R /Last 32 0 R /Next 33 0 R /Parent 23 0 R /Dest [ 20 0 R /Fit ] /Next 28 0 R /Parent 24 0 R /Prev 26 0 R /Title (Design reviews)
/Prev 26 0 R /Title (By topic)
>> >>
endobj endobj
28 0 obj 28 0 obj
<< <<
/Dest [ 19 0 R /Fit ] /Next 29 0 R /Parent 27 0 R /Title (Infrastructure and deploy) /Count 5 /Dest [ 20 0 R /Fit ] /First 29 0 R /Last 33 0 R /Next 34 0 R /Parent 24 0 R
/Prev 27 0 R /Title (By topic)
>> >>
endobj endobj
29 0 obj 29 0 obj
<< <<
/Dest [ 19 0 R /Fit ] /Next 30 0 R /Parent 27 0 R /Prev 28 0 R /Title (Development process) /Dest [ 20 0 R /Fit ] /Next 30 0 R /Parent 28 0 R /Title (Infrastructure and deploy)
>> >>
endobj endobj
30 0 obj 30 0 obj
<< <<
/Dest [ 19 0 R /Fit ] /Next 31 0 R /Parent 27 0 R /Prev 29 0 R /Title (Alpha and QA) /Dest [ 20 0 R /Fit ] /Next 31 0 R /Parent 28 0 R /Prev 29 0 R /Title (Development process)
>> >>
endobj endobj
31 0 obj 31 0 obj
<< <<
/Dest [ 19 0 R /Fit ] /Next 32 0 R /Parent 27 0 R /Prev 30 0 R /Title (Backend apps) /Dest [ 21 0 R /Fit ] /Next 32 0 R /Parent 28 0 R /Prev 30 0 R /Title (Alpha and QA)
>> >>
endobj endobj
32 0 obj 32 0 obj
<< <<
/Dest [ 20 0 R /Fit ] /Parent 27 0 R /Prev 31 0 R /Title (Product and codecs) /Dest [ 21 0 R /Fit ] /Next 33 0 R /Parent 28 0 R /Prev 31 0 R /Title (Backend apps)
>> >>
endobj endobj
33 0 obj 33 0 obj
<< <<
/Dest [ 20 0 R /Fit ] /Next 34 0 R /Parent 23 0 R /Prev 27 0 R /Title (Agent bootstrap) /Dest [ 21 0 R /Fit ] /Parent 28 0 R /Prev 32 0 R /Title (Product and codecs)
>> >>
endobj endobj
34 0 obj 34 0 obj
<< <<
/Dest [ 20 0 R /Fit ] /Parent 23 0 R /Prev 33 0 R /Title (Maintaining TOCs) /Dest [ 21 0 R /Fit ] /Next 35 0 R /Parent 24 0 R /Prev 28 0 R /Title (Agent bootstrap)
>> >>
endobj endobj
35 0 obj 35 0 obj
<< <<
/Count 3 /Kids [ 16 0 R 19 0 R 20 0 R ] /Type /Pages /Dest [ 21 0 R /Fit ] /Next 36 0 R /Parent 24 0 R /Prev 34 0 R /Title (Maintaining TOCs)
>> >>
endobj endobj
36 0 obj 36 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 669 /Dest [ 21 0 R /Fit ] /Parent 24 0 R /Prev 35 0 R /Title (\376\377\000P\000D\000F\000 \000e\000x\000p\000o\000r\000t\000 \000\(\000m\000a\000r\000k\000d\000o\000w\000n\000 !\222\000 \000P\000D\000F\000\))
>> >>
stream
Gatn$?#SFN'Sc)P($C`SRcf_k:3HjGUe.]>Y.<=^l59!r[c`<erV(A"$QEQ?M]tfN'HcL`c\KgiJAWT0;f2W5"ajVf+;B\ZO9?k+>f42kr>Le7bClWIj#@&_"Mu]c_RZn1E(1^XJ68CPOT?)bjp;(')MN[("%GP\%1!;oG6gf&!$MCMJQ^VY.MtrTjfP]DWbbl66)fCWb2$*d[VW?/TM;83It6N+'Hc;_^P@Hm]<KTF9aTfL]c<4u-#i]Ee]SN].jghZ&/3iI\;'OjgF[fF:M)P,[&mZb]Rf5Id7j3-o*1-n%-$;=n0+Is(sm8"51Br+&u7C%#[A#d0VnUEI.eJLXH*kUl'&\BUb%7ZfhgaVVkdX6fb[6`3mHncka?2Xb(*QS>!9itZV0k;(-R)#eiGB-T]5oHOoYIH(*L?+#<hm`=o*)1rD'd_YFm]e<6Ebb3C6>!XH]-!<4-jSc%5IeW[9&!HrpuEg9&O^%!Rm@H87%">0;g3m*`E]h_/Y&Dp`L:X[4'#hFh'%)5DcB_4&ZLbP["#D5bM'7Bgbq'`4cW@5qtrbmK>H=##_jSsPV!r-'.MS0@H4E5B%n-faLKm^uM+NY?1m=;rR`O]lP,C#I,09pi)&4'Ou%`KY;6?'d-"46N\`_d9CV.I3(*\[K4<-]Z(-%kku`#l~>endstream
endobj endobj
37 0 obj 37 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3307 /Count 3 /Kids [ 18 0 R 20 0 R 21 0 R ] /Type /Pages
>> >>
stream
Gb"/)>BALj&q9SYkbkQD/BAacad\6#LD'q1E3cCa<N<;@5d?Cb?*M#WIX<>57ZKd$D!f..Q7K4q*i<l)h;@i5!>0fPp:mp9\6-WD'`fMpEXbDK'NgC3>+Y[T,;1hf<R;.S<'WAN6'VZ^ILjP07ZKB>\,n)qPlqhf]51%`>>aH'N8_\$F.EQ/i48gh,QS#ko5V93h%^T2_48FQr/;Q3O)u2TURQ(*+-J4.<T'Z7jsP-BiS%!iVtW=kVGS=$4eVc]G!VY6KLQKQi6d<6@XFG)`ciMYO0JN_b<NKt2u*!I8uePBMU#3_1-lN]>pRH<W+%dGB@RcNE)T:?fDkb@b/S3FlinP*cRW"RO34I?Jg_9I'[\mJ,`%1#X"jtO5SJ8B"C78s$K;!RnS>FHTSN\9,*E.Y,"':Vr<KLIIu67LqVE28>>Tm[^Pd$h:8qH#)@S[.//_Xib.?A>APK75))jj:Q"?e@AVkM3=fh??)0GBk1$UnmZn<CFC:sk/d`$i,Y4j%^>s(ZrH4LLs3)HgQT@Uu0C;'B&:>sg?GON,GS<B-M$1oe;9>Asg))I;%Z?G;oj(3p1<]2R;o&J-i.*7XFM-?gr([5g-UZXRO++Bj6YnQtqe$ZEYKEj%S&X/>(9(q':B$D-X]i]Zj_^GeCR!Q"Xk7S*5<<Is0F.`=-(#/8I)6mJ/0VFHDiY_"HpZlmbjF$?@%*[DgmN[r_*A=096W&nJ><qXM=(WP.EP;?(\4=8]RO6egg'\skWSa;T`OWfGL`U9;Er?IF*UU(Nj!:?=I@R'#BF=$Y>7.u!:AS2+B;]^%N((qRBE5q.a0X*h4e%M0o?hSh/K1To1AmE`TE#mV'k^3RS?N8>l3<B?4@A=05@[Y"p&W\Z"`bt^g-BmLU\O*ASfta55fKH7@9.!(O=&^#F6I@OF'N*,=S$I+Ts_+EMqY>KNbC5<[-)aD6-Q(:lS-u=YQoO?\Z9N_5b-1H7@b5r[fr-eo5#WI>benXFk1uGS6P-o^:^#\lq?[(p:?[Km5\Lt!?I.\7Z?bC`IW1P1?YH>b+P516nBA49V'anqP#)F;240W#58e./%-q_)M:Bjl3/-B?*2!h]8,-<O68ECXcl4\`TMH5aF-qO'Cil],%(./PK6p3P@VMdbR>L-cbabJE9$MX*&7._eq4nT@X$l,KCue\/%2=SQ^l:^OsG6Lb?dle0?nTScXF'fU'/Ku=,6tT!7$lj%j'kLT#X^/?#\`BPKFKB[;t0kRp0qr>5i*jTT[l80q*Q.VJ!a3BU0u`ZaBN;R(oK3_m6'*9uc83hst.:-f!>Q$FBLk`q8UOGL&(V)_sN1Z]e-1K-n+,RN-69j%IT`Zgm[k7d>0TJ\^[S<>]1=@`$sjC-5dj4h+9nO6c$5IN0II=g?<]SkghL[)PDLl""8JLEglq9YL->0%lhA6-,f"L*i:QkUM=.(n!0NK$D<9ELGZ%g'm-k!m.u1S'IAO]U;gf\X7j(5OX1<PA<SH/YqSL]&K)+$kZ1h;tR'(3aF(hg);\OO-]LiGl#+A>&#'CqBbbd+q2(8j#Hf;(F_#IBU;#_[p\G?hG!Oo;t=PKh8^Q@WlJe3R<TYrC\J.!E([e[k^ClI8o$P14ZBRgYGT!om3((+B!W)RK\O0ZgS)4iP%LS,cZb1!3S6X;/EY(=3hgET;iFr$NHo6oTt2V:Q!8m.dFYFEZWc'knQ?R-d_$Nei!BZ$`:G@d`fqGGE+KcU<tE&d-`L;"rV>W,FW&Xa>Za,pZ&0^qn1iDN7Yp*lqK*&eX[1Q\Apq'!h_[,_qt5_IaCCrV=F?5A\sUBn*[Hk@2[hBuUOl^97Tc3ekHoPECQ+IV[knn.(,g((g#ls^G2U7Wdg;!U@U=D+dIj'nI"PpXR'6i,OYnS*cl1t=B=A7q0sds2f\[6Z>aij58>?9j0TFBp+_+<8Z>VC/`F/`Wr5hSNhNFsATQNk*UG\pc,2Yqp:+N*deG#@9*dnOH"SQ:8O*A=M`\!*qF)nTl@O.iFW7D`i:;[Co[+:UV2P8^#frY%Y?erP%e9:rspudb;N=H&$LBffb9sU7M)<qYIY(rZhF%S<t24g4?PNgN$bon.j(A;DsCPJA51]$N/loLuj$MFVX?0L5oXiGraCFE+]W+F>`B/b#4oEg94&]CZ<0N9N?H9k)I:ZcbWWO6VBo5\<c)YT@u='p&AG"0T7c4M89XG)i3M"$r!qC\[VB2042LjgFuki"?FCb*GE2#Lf7(Np`t4)f0OcmEaP)!SjoqMq;M8OAtbG&N.g%6q&p9NcZSe?oK)[u1XkoAsLn&'`)!R4hc_Ph^J%H#A$3c3rh\N>S68;(q"f:Da>#=Q-5$Es2*+Jq)dp+(p0Kl&&*kmI0lDR@qE,Gq6FP]gCXE%i-$S4=#`JO2Za.#?-X6`,@LD8N87m7&l>f=HN<;jT12K99PuK82r.l\m-E;YU!ot$2LeN.=-]$_ic8q'bV!LP!gcF*0qkT@TGZD8Gn53JAd3haf&<o%ni'dYm);9,dh(j4tK8t`>X,+P2qKXpol$+MDb15,`*7On6nU$</`PV[=qpAL&PfnX^VS'eIH?5%-jl.jGFQ1K,mhF&-iuL)PGGaOp62ZE[-g$@aj0RH^g[a)U=S3UJDC7;&47J5Q5mnek%lqjkBpVLQ8uiSLhh55>I<*6V]M"'E/(mNK$haa,@t2C]Efc@u#C7,)R9^[VVk(B?.Z=MHN)W;am.a:N#?*-s;ed.TtLHO#4a]].1(i(8%1O+POJBRdafB!o",T]RRk7?>PWkl,ch%c?m=SF:Ir1mi@C1=WRk6nMlgh'56\$9H]6c3MC#W?Y5(<G^2,G)sCtu"&Ak,o`'M.Dh1G0JY!&?l>ZXi7W=f4HOtPg#1!C@TU0e!o'XX"<'X*fKL);I7miU"[&]XCS1a+2]k'T=4S-RhldW[uP_a^?LQOg],Y#V'Vl'lg&\jltP:1RZ4%J.UaH,QpfSZCGC+&jY(`Q!&Okp[<)+=Wehh,"]q1Vg,CWdc7eoAY\H%uYE:%UcdqcGKo0a=h)kp52V)mikqK.F.bQ7J<),W$-/+8i#/nePI_'5s?,`M-_*Mo3Fps.GK:Wqn2@b>%VL99rk.L/GbD+uNba=S'43ql4k6DQ3Xda34MgpIVn1:>bmb9l>B<iZ/K;TEN6!;BlRlj>T6ug',Ukfsr/^PJChMr\ra,Af?LPlCArboA`F$`iS>[i+\">\[u(=G[?NuT>,otp_PS+.Y`bM+Cn2_@*!#5ItM:N$]/-$nhEKadq9SRcS(?[l:"SULEa.ub=-kI$coS=Uu<E1l^(uHV/m"a?6?qE"4$lL[-'K8~>endstream
endobj endobj
38 0 obj 38 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1189 /Filter [ /ASCII85Decode /FlateDecode ] /Length 717
>> >>
stream stream
GatUrCN&2l'SaBs<ubq].qZ8D,ihG0\WF8IE(Q8?eOW53R^eG2`#7=&jkm846FPXBC,uW?abfP5Er;N\2F*T^U]/Iq!7qPLD@A3m8=mW$1^fI$prUL9$f`qhZ8K\/FUMI#e*R(LP5g`Tp%gRN?H="E6ffkrAj`l'kOe$gFVP`\B48;&bX-f:;&Eu9*#>F96;]?A(Ec)LkU]ENSQBTqjsmV1qA#1.+o+Lagrn:u,hLN;o8o,CpruTD*6liDK#379'\P8S*<(WG6BJPXMrUC5T%aP,TII`>Ni?+m`po4geS80)>ST=M)RqiPTanLRZe\\_3XKohBY]Z+#;T4:*So+;*;L+o'`0kMmr66Z7<uK,Qu=pQPjKDR7Q+3W8@+7oITY9ZlnmqDIEgOBrT+Ep>%Pl;\$EXj)=O!)U$I_Ii'-#'2<HF3"=XXm\ED6VROH+J5<Qg[ERJ$TIHOj4NhT=eNhWDrS?@&]#?m6&IV`SF't!&(a:7aJ:iu1:3q6NU@_tA6?L&kI1uDmHdP?T:k-Ehc-ZfQ*>KUnCRJ>/`5i\^qC!NCW!=E$2G\7sd(sOa$;3P;:!T^YQgq%1HS;qs)D9V$UHoIJsG_k@TiD:+h#'mO*LiS[p/C)p<JKF=Z*0#U342+3Ep>\-"9cGGO!2NkrAO\SR'Q:[X%A9Pc]&bEWdf]GY1Z47E%7BZTk#&qZqDCH?.\oR,"W,Tn>JM)Yju0bFXdoCi_P&>#Rl6P&;@;uUOH.G>\<L?@o.*G+/19ea"bZ6WpUhS$R%Zf"]nt*cH_0m$2-oXHPEZdHm(F?P$gMZ$.o5mDLY-%8%QmUc)P5>#cVcCLR#F1II_BQp#.HmMDoT,u)X9g>"gnE??'?%i@scM%ML@7.?\9WccX!rL'Mh/@"kW?R4Mi=-*7)>lU<=55M9GV)k9U\%Be:CC[=>lE&ZI04WH1>6r\TI3+Y-`\nus?EK!\-gC7_IFn$\p,l-4`S[c&Ks,k<N_L60W6D-#/.[/T<#+88#o1:99^\gX12CcPA5]t<ZADn'AKZ7Zsj23E#QEs8@m7N'h9d(XcGPs)oRR;UOC2bio*bM/'ArB0/=0o'jUqSBUVi<$IH2&5m<A41V&-5D-n@ER#BjC'%i+\h$//$HGU::1lg>5!M[XJc9j/_0)0'r7>JN->HMZtfV*U6@^"B'?,bU(=d/(M`UDLB~>endstream Gatn%>Ar4L'Z],,'^(TRRVP!)SEp[l8<[5ZQTXiu3X.>P4**_AhgS=n0G(9i'LbWipt3@]\_+q*Y^8q8Q6j=+L$KTW66TV7#uLum/POX0T)mVJL($L*_]a!E&IsuNc#^c.a#%0)THK(FNra5iPE:<#%c!L`O5Z1d%RGZM(-uXB+G:ubET"jc%#-!t=d*K;,O,KqP]Fj(gltDiU</CL>\E?h$I[gnIY7+0PF^80pRK\O&]>0*)(%pCi:MAr2aD2[R'b:I9Q9mh(5=V\8pHkd-?iBfAfkbO_)%*bbWG`JK7N:tr8UB<5+qNZcs\Dq$O(,Cdce[SY4`Fjdf"<;n.D5$M[o"8US?D=b_M.0^;s*8P5-;g?(lM9UP/>pSu=\WC9ZtL=0FgOpl^#s7???"0i]hhT`"+P&d&#E(Eg0B#9EW<?<F[0m7&uQX$GkW10#(Oh*XfaYEXli;2)"'RY/a$WG8%NY^T$"MJ=HK0Kb:Vk[D6uZ&ib_fV5BnI)"UTq2Bc@:$1`6o3RkZ8KSBUpNGaOgWVl$e$s]<Pm86UIBi6/m[@(WF1m8!f8YRG/;a7UhbRjWhC<#@ZQ,:JXu#[LTp'FY$][pDPXW38j#+V(1;ECB9i4;fo%u(>@Z&7Pi.<R#SoiX5Za!F,3&K@VI@sWH]*T1^?J"[]m+P_P=s'D#I$Lh-=c!@J:"=s:,tD)i$<N[:ASd?$*/249'2&ab~>endstream
endobj
39 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3411
>>
stream
Gb"/)>BALj&q801kbg#q/-q6<O>e%fLQ)TWiFG`LX&NR`J79Z6?*M#WIX<=c99'$EWSN?c;m+%[HC2FkFFVb&"@W/2l2T$Ei,Sk3.fr+j`<qDF.'U2^F7J5P#BH32W.NT%70W=4?Voj8K&8r$R$,A[#@)foMb8R.Mf$%95@%.I+5FZNcuYaJT]!B[,K'Tc46GRBS[_<>JAn&2%K)G/_2i-=K-rV7\3@hM#0XeR[`jfkro2Xk5:6s^(a`Z*KH6`b'"1(?_h<c*bM^Kt?-)^lH]%5U$6N^H!u?B*F`-a4?'SAT(DWLAFC]tKoQ?99T:h(\KV)7K&Ah-b8R2;PQ'@;1OP:3s%*pG'MiPr[o`\2>.O\c@`(1I:P,ueKa9,/dq]P+&1f+JPLQ>f+jZ\P]&0rH6cm:H#$s%l9Rf"P*,H:.V&^ggtGl'T&(@IE..nbL\Mj:-QG(+".DR6VqSmN?o6&V">jo!76BsY*ihLf@c`e1(:hLe43j$BE2pE3>eG`^#04\/*_%heEF#7#)cW1WgVf#E.'H1/kWNaE7Q^$E7=8t&?td*oKXrK9Z]N&^[UglZ0._[;#QD2\tpnS!03A4b-9XjuE3ZJEsujiuN@n+/Yb66C5/(Zq-i`)74&(ho^TccpCjgbN`iqJ>W;?l)5\MsMjU8D#P1.,:?ieF,$CQKQJRD1e2VFW;/cQrh%0pJ@%0b4QSgZgmua."jXQ7T"n&+#UKWhNb7&/dgWbh/2Y3I(M4l2r0`84OmBF3GXh"K2iIHd;\@HC\U!F@I6O(83-P1TgWQ$lKpJD[Rh0W2g!A@i<U<b<kkI3qR1B"qeXokq04qB0?0kh9C7>iYC$O7d*SgQ@mZrXL$2>l#i57ZFrp<T7YP4dW4"+9HV)Wi_D<+L:UUcBi=uDD(.JS9;Od#d]K'I3?#=G=90<XFr7cI0=uL7qg\"VkX_3j[>VAg-=L`Pq#$\\)^n,l?1>N*F`AC=dBC"h8#ErhmF\#lbkcRLZFgi--e!Sd_]?/8<R?r-Lp%3.InS/QtB[ebIF:1_O[%eX_:?nYag?D&E_P-YPC!<M+$6g'n=?PR$I`??GQFU3EE+hqY?k'WHFa.@+E&[4SfS6POP0g9)r&tk+.r&?6QFnd>Q/=BSX>Mk,HJ%POTuH$UnQ[g(\3)-oN;\ZWi7<G"?2NGc[HbPjX_?_5-_n3tafX'uXf:ieSo2e5=IUFliPtX";*Z5>feDU:_jJ^/3,5i18q&n%*"a,?pW^Fc1N(3j5ODZSd^6+GoP6TQF(Rq;1jL%:NGlW1%J:a-??L0b-5V0TIMnbs?i*KTiqj#!ZbtQ]O6H"liJj7rF=J0X_*uo4GW3>S@2t`l9`M2jD@7KkllK#X+eD76\8.[qV[K7%%uKUl4e9BL!X'`@[e#*;P7%"&j.P@Q1,7t0Z#N]<^#g;(CsTb%a2785`Yb!)14gR+N>][NHL78CRGJe7%-IfragjF$c55WUSlX+H(k1DXCkDkCCIk30,j62YjHsYj6SS0udAR!7FIFIiA&NIP?J&=<e*ltZ;U5B>?]NuSLrXDN3"A.1,5$JE[OH<E$^Q,.At%&;j#<1i5BcOhHAk<1M)R`5Eh>&l-e(4Cmuo(r)WR7O\T)2.2=,a%#Or!QRuMr0\(SBU]lWp6>gIN:q$mu`#?s`e76EP8LNGt5MW`Q1,G&)JW:!5gji/ETr:Of20:0+LHklq0,caDSGQU88na[!h0dHu)>a`.cjmlL@,uq"W5PlES04T6]9="^^b[QAjI#N0W&Y`sF3ZS8mA'd*(R9sgL2t[U.I/RK"j=sIe9YSRQ?-M7r#NF+,CBbWp;8E]76Hm`XF':`30ucf8>8Yp-bJqdjqclqA(4:@83i:;7`,cGb]"[d!D/bFE',KRRnk=iY1^5thfpu\Y1Xl9"r=&eJaB6q\)D-4$P,N$\(jj8aI5dVVQN=bga+lF_.@IA)l]e:F`g@*sHnE$8Xc,SFhe+!ToHd%;fS^c_hYHRH[k-slamEPg-S]m3[5O+I)G]Ln29%JZq:%<]HJ,e'l42sNXqMqnmm6e/3Xh)j?Jmq):-0UTe/L#G%mf1NP\OrC4>SoDdtMKT6bLWGp38?nLRaehqX0+)3hbLFp&s1<cGNA&*WmmOThJI[L52ol],T?tSmck"L+%%OL.Z#G0A'-.P;uoXC961>(eorlc$E:Jm#+V1mjQl>3CF";Q;Fk*&`,<LhrU*pcH3p7dI1!F5+]cI9nc].LL88ZAN*Jo"W%sUD&hZ(OUbWkmjP1Sa4/e<I6/FMq0hXC_NL+UXt^KuZS(XkUWM.0)V1XS0,nhob]bc+R@f$$!r>;PL,V`S$i,!V`k`p4Gj7d=_7Z](5OdQk%mjXO'pZShht-.EJdP:B=*"2l6Jt3AXHXur$"E"+Yr&^BjiBd$X'$e.YTOOW6Gd/+iZ62TM%9tl?`T9-=HIX86Gd.sdOGn#@6mf`$42Ik8QI;d*#9fV<(Ju"87KQ>I$g+BLTj2+_h-CA=G%+lP#egl*u6-0@LYLP84pi>HruKkMDb15+H_O=poklt'l3;I:)[)j0mL*[?8705C'h"O#jp.PC;gVHWtj]Q&`hB#W,@ES"f_dJop;I?G!<GKMF"sjDi_S+:dbb*9$5PZ#Dso;\u$R!]HHO9R*b>>!ocSf?+E]Kj3KqbONF06l-//jV8h8C2P!^Eg_n'sj7_P`Z9.khDb`nVHBU`22*u-4:.!9eGi9BY6!1I<hmDZh2nsoq8(!;fX7^]N)ZEp!M1h/hr%&!0qG")Ghl[P%VZ>dOnYnsh1,Xr"'0C5J$ES_='k`<jmH)0ZnL=gRE+"l4@[hVIg\j#2a%LY+.cRCAh`I&rH[#]rG*!IbbHs?t$[-N>0cOE3Di=Jf>,_X'V.R/::+,AU13,6PX/0?.c#kB9WH'>Gb![&qj"Z]FJu5NfUTDt3M?s`EkU8$E^u7T5fcVcd@YrQ@61k19Lc2ag<.\d%17%$-a(t9<]]h]B-#:/fXb0APgr]t-s'rm.L<dg#2([Sf%<a1Bl+l)[5(c4HeA7&d=o5scjZCjbl=7WW[H$4B;?^'aO4)-8cKPe5jlHk'\@^&^=1`]#3PqP`4[1:lSB6;2;!'OqY=bNK9X7s@X(GL[W<QR,'1D4U]bNoUdJ(u]"OV5G\B),EQ.`c+M^Cg=)b<$oG]*!?Ar%cpORGEb&5Z52mF"TlrY?dE]mG\oRIg#n5\OCl%2Ld*Zab&RC5FIA^M2$A]U#dsH#?CTDCPA2&5c>Se"Uc,l'YqUk',\Nc3,[([=gp2npC)RMXd*.]R7F0o?)>*%aQc\#Bf)FLaW`dS[:DDLPC*Np&S#BJ!O3sq7_U?H&q!dk./?_"^Y?/-7JPQ1HV,p!;Gf$JG]/33c7:ko(I)E)erR0L97!B=qWu'+^C'sET"/cW#0F[[D0cd+8u^ug7]5~>endstream
endobj
40 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1820
>>
stream
GatU3CN&2l'`C51i_#tOd7t!nObE.h9nB`dBV?T?eOW6aO;Q%q`#7<OVYf]R3eaq85u!86+#ql_a60%@Cho^'7k9$Y.tBDt>3?f;3r!dC9Z7-a#^ua%cVN7AdVLG"doK2I*DKCjNf!As0i$6@G<&87r"Wn*SD?6XTd'g)`H)dH!ZGsF01-#O(iJ+Rn:,t</CWD4B3-S+<n_,bm9?1F7)8Y/;@250d&V1p#`YfqgY`R^Q`dWO@B/WObS_T(OWm"c(0QjHk:gaHFjkdB`Zpq9ktqn#D>P1W+%I)?7dek@hU&q?:;a[QVTU[>+!;*5Z5+_'pip)i_W["&/!uIN8>UG=X\78e-^TD9+-/P!n!+;L/EBH+k:$l#)E?6lTiDqhqq$B\*g?qnCdGQ2*84BN[K%NWl;YUb3/:Dl&%Z-FrZpu&DXY^gQcMRH2%gnC^dTG<=9FKA*(nP2kFT<;DK*o(.[GbX@)9^-H(XRK:[Y21_%jj-MSU4Pm,sAA'@7Lb$1(6b"YsB6/_*aAeBk\=2sCP\e5OY+:3i5tJ?N%TO?Jj'(g0^eSKC:Ro1`dZEj'uo2[/'XT9"KW>#i!@DL\7ALpqJ+;cQK>![CgR1uM$L"N^p"ESl(0-q@]$mWZ.uq\-`Q.,3=HU?m0o#2A`ASiKO68IT:8)>m`LIjHr3[?)@(DP>Q7[5"HH3YI/&7l3]LCF%C>[Zp4DlDj;q#O\fA-B'sl@,R&\!)O1aOKg_O>kc?>OU&\j.(M16d@BUL_7">F3=@Hd=+d13:Ab`[*'kM4@8da%-8i"e#9/Bo/\&?6LBIR'E.e%)Ikhn#Y"MIV2^$d/bTa>:RAbEt*It"rX/7HLmLEprAO&]#RO7RjqGtE&qhrA?6:.,<._%`jXT?F\'YV_uX)dM)_RX`=9cIQ>A:j^pka7lr'E!KP?]kG$0k$t;0^/5EotF]c9nC.R\bC2R'BnU.>s+dDA/'pfXn+I'Ea%MQ;4Z!ED"FY2bUJ_bcY*q]+W#8pVQJMnVbL7N@c.LS_pL[pl5;rT/V1$&;T!P/WqD=UGG[;5n3_ZVPhSbb:SLcI],_<,6*EKoNkQa4P9&hCPPVoS#nR[tD_Juo=<H_Q,<5p^3X>DRJ*g#WOZ?,k4RA_6OBUlX`NW?ZD]>.2<X-4C%Bi)dJj4K8"Fnq';,PfL'PDWq[sMLL,.Tj'RUgU^esshAhW9(7lE2#&XNAFK8s#AnQ@PdX.,tYNg,CWRYm2,f_Nk&6dPRR\=$*;P3oj;b-65T@=D'Kr^dKR(1@W>O&a[Y?-j5CI+Ue%c6efbY%G'Q??>T"gr';lJB&Md0-+Vs9KfiXoJi$sUnYVtAP!?TEZkJLsT\RfjqG>Eli19]]pWe?fcLKP>/qn.`(XlaEQ;_9,r\tpZ6Z$W17">"8n25@4b&Er&en):rO4bdKCZcm8;eRqJI("VhSd2TEP/](CI]a>bClH#`_hu6H!T5bK9<L/^B3Vm*RCotF$WP>fi_(@D,\Q]0>88n5>qlc:g05`<eeDa1nA1lMLK@`n_Z74pm%LHU@eId^X^cIFWr*%>NKGTpF@S\I\@u_U[))3oSA.\4_E%0cNL&bV9'!<.<2-9\2=/\'X*_D*N&jq',(7]3D[#$UT^%]M)E(6&FN7ajNtH(r83Pf164W&A$]'uXYDatApISImr84!Q2h1>Jp>78m?B^lk'#,VNbC<S<^Hd"bkUK`=VFQ,%'Lk^uZBg$L<*!Q!N;a9:TU8_27\*GEC8pM0b-jh",XTD,\#`0)OI+UiOetMq,c,aX3j!F"/0S+@,+,;#QX&(l/"bC3Z=hUDHiCaY3t;Mbl$krYqpt~>endstream
endobj endobj
xref xref
0 39 0 41
0000000000 65535 f 0000000000 65535 f
0000000061 00000 n 0000000061 00000 n
0000000135 00000 n 0000000135 00000 n
@@ -232,39 +242,41 @@ xref
0000001683 00000 n 0000001683 00000 n
0000001831 00000 n 0000001831 00000 n
0000001979 00000 n 0000001979 00000 n
0000002095 00000 n 0000002057 00000 n
0000002387 00000 n 0000002205 00000 n
0000002493 00000 n 0000002321 00000 n
0000002571 00000 n 0000002620 00000 n
0000002777 00000 n 0000002726 00000 n
0000002983 00000 n 0000002932 00000 n
0000003070 00000 n 0000003138 00000 n
0000003382 00000 n 0000003225 00000 n
0000003457 00000 n 0000003537 00000 n
0000003552 00000 n 0000003612 00000 n
0000003661 00000 n 0000003707 00000 n
0000003770 00000 n 0000003816 00000 n
0000003912 00000 n 0000003925 00000 n
0000004019 00000 n 0000004067 00000 n
0000004133 00000 n 0000004174 00000 n
0000004240 00000 n 0000004288 00000 n
0000004347 00000 n 0000004395 00000 n
0000004447 00000 n 0000004502 00000 n
0000004557 00000 n 0000004602 00000 n
0000004655 00000 n 0000004712 00000 n
0000004730 00000 n 0000004823 00000 n
0000005490 00000 n 0000005050 00000 n
0000008889 00000 n 0000005125 00000 n
0000005933 00000 n
0000009436 00000 n
trailer trailer
<< <<
/ID /ID
[<5465d663d38226836f882a539aca7e31><5465d663d38226836f882a539aca7e31>] [<d1eb6aec62dda0de1b321db25aa5cc93><d1eb6aec62dda0de1b321db25aa5cc93>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R /Info 23 0 R
/Root 21 0 R /Root 22 0 R
/Size 39 /Size 41
>> >>
startxref startxref
10170 11348
%%EOF %%EOF

View File

@@ -133,7 +133,7 @@ endobj
endobj endobj
24 0 obj 24 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False /Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False
>> >>
endobj endobj
@@ -283,7 +283,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<a61d1e541febafda5803ca3349ad0aad><a61d1e541febafda5803ca3349ad0aad>] [<648e459d5f2c35d90d3f442a04b8bc8c><648e459d5f2c35d90d3f442a04b8bc8c>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 24 0 R /Info 24 0 R

View File

@@ -107,7 +107,7 @@ endobj
endobj endobj
19 0 obj 19 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False /Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False
>> >>
endobj endobj
@@ -220,7 +220,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<46b18678d01cdbe7e73839fa89d85491><46b18678d01cdbe7e73839fa89d85491>] [<84e36839b1116312a237a780b9dc4734><84e36839b1116312a237a780b9dc4734>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 19 0 R /Info 19 0 R

View File

@@ -168,7 +168,7 @@ endobj
endobj endobj
29 0 obj 29 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False
>> >>
endobj endobj
@@ -284,14 +284,14 @@ endobj
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3099 /Filter [ /ASCII85Decode /FlateDecode ] /Length 3099
>> >>
stream stream
Gb"/)=d.i)&q7#kTr3b4"68.5qhYNWS!c<NNgYldNZa!,^sg$;QL!1YrU$*FKeXG!^dQDa##SrjjLg`"r%_ganaI.Ms+Z0QQT=?f#tPWTI#S8sPL9:LNTN0Lh$E7=<TeTu:lqPGn>.MMA+:uE"Ig>H-Aa48Po_:[n>.@#-m34%reBa<]V`lNHqb]7Gt/q8A$.L3'hjPI&eCe$&;V#n;LCgP[=]is*J#8Yrq<T]p^ICo&nG4,78Xl'Yjp1HYMZ&LT4n4qEVL$9*>_Qc3L*ZibRSp:UiT"n0^*]D\HMl@NCe?_P[sR_&K"8/JpSgE$.@5niS,IC.OC4-gM$tYZ31]EYKOQ*@*-nN0g_p"ZjFCd+.FCJk>TQFZ$h4PD@!MLd>)7,Jq04Y8H0$HJ)5.UW6'pIb1Gn0)br[PZ*3Z<8_hNZ1\`-f)tnW-P7R9^^[q9IE-$Cj\6iYEmZITJlm5gkHML;3h"QtgA;*s)KpQ5Cn=6?^)RIr`#qF8Xl%JVZK$%IgJS3TQYOqcn*P$E+'4eX%>uNJGEB>D0>4*`]I[?8U1K(\H6q,p],Tf?/)A)6X5O^'UQns7o/;BA<+`]#L&WEeR\j[c^9bJT8R]p]>@Qfq5,GJE%Z<PXtJI5KC9KPTi)CJ0*$mdCVGf`^4^s"']M35aO^1\hI>!;bdYf!+2."odf-fh[5TIe#1]`LEYf0r8n&1>G:$ZVUZp>e?u$-.\T?<e2)a1/^raRcg(FmTUSRTrKe(!`I&la:g\V(`(=m=Gb/du?/:GUc';8eGMe>)atN)J"KoKP)"J-nLKuOmMkoi?,K.CsTo'9EU&lBm:sZ68%oQWSdmTol83arecs%4/P'r4p+%s4H\7f?JEsj#eVkX(nTbl&.(uI2VImG/us8ZMF/VmHl@00O_("&(K1TKS]j^\k*\4g9r?"^/>K1]/]1VP\4h`3kQ.#:T%H!KEYV^=L2]Vq?L>^&Z-Ng$#7[$?[bRLn6p;l]rg3.H+9Wmrg^<d1e))a@Gb`nP_$ns:H:SkVCMXR7'!#[TYbfIAB@:%%eJ5g#\^P)UXE_O$FI&+$]fn0i!;d0n`"j4;O.G8!RA<n_4\NK5$-ps6b:ue$X8mM=SjB?h?7`\\,)RFtC#K*IprLKa8@JSX2d9l`(-(X)csH!ZIuGD\+]F!;PFbfPHmAurasDqL#8qmYncKdmi1C/2LjF6Q%=n!9JZi<115bXf*Mmlgg9\h.PRPA<%?b(6[e$EUdm\tOY[=[AoefKB.L@u?;N[&;WdaO0-\8#2+b%H:&85JTm]ljYR=t[Ob6@rN('`\L)Z]e<(kB57!)R06!:J.4f7^_e:$G6>T_>2(3^hB3!R_:>bF_aOH**;I=#31)C]<Zq8,_DaLi^cEKor,&3*h]tC5HIQT?sl#RKqISI4q6:qB`<>4N"M+SNj07A5t'tX=91:qMIO<cDWQ6Z+eZMSO'_V/U(VXU,L++HR2%n:^oZ\CZ6/],>.9X^)0TfjdBY=,F^SX(Y;D#i1=f'pL$lPB7$q,a\?\HnG?ZW!96COj:_;$O]R(\Q:"p2Yc$*Ai<0D8_KH%*d[]GAA2Tngrf1%GN(0j8,g?$\@fR'YJ"p;K*IApJ@eKt&6FY!3GndEIak/W;W\nIMm3.?*PRPkdbkd'cCVO-X<j]P&Y3F3(8j;]';CR])pY^S5K*.2N$3P76",PeTJEU:L':d3G#qTUeiY2A&Q"aK(GtsRAC?-OrH9S$6StBUP?oRE&0)ek?jX$$Bn>g>Uo(<I2hDl%as20VEKNeOBcYd=&VgBS-d-KC)#MklnOXE&9@g@1*!t%qa^)cocI)pUnGh]Vj(8j2b]>i[7q\Y=NfR?B:K!6-N(D-KH4%sWb#[aG:A:"NhWu8?uJZX5qXYJH1TKB3<07`QVYrU6Jh-f#G8ke<JfB?dmU_V\"T7#rXmmd9O@QI]L9h1t.O4pE)%,!M)l^hpsnQ(r#F5SOg[9Vd#f34@bh59mbo-s1<fM*!-5TK6\+>5u5<cD=U3d)JUKV0^XGCPCbOoV]lA'"]JpOLUf!Tm]'M9_9+#o\&3'VC+4`lR[DP,e*[c%qF5f_N*,?1TZeGg=HPM:k;Op-PT.:VfVki6iB0:(S"J7bI-^aXM2W)?X<OcMJ0'1_D@6jhT0]0(G\*TDP2[6R7$W._0!"/>$%l/h7LumBs3F]/E\9r;F[pekDW,i$o$o-`\Wk:Oj0?<U<^1>>^aX#q^^DjpA8gHnV99Cd\-#n!T#iI[hXu&?s4>[jlh4RT=m2@PPPdi>0R&iYLf7^#D"k9;s"^l?5O4c+gu5?EbkAo&F!gJj\0Z-FVTNSp["-2@lB2K7$.pa_62kT,;!<j9;,V"^ri3UVL3u6?%t\&VUEqY<=u).(9Tu6,`'*e4r38q\a6%$G6%M"=h`Z%>"=rbRefGBg\[cX>p`9r)X-(S-iF]$d1RYj;'qf[DC@XIF-WbA!`@o,ntq]TK0q'p^98dFd?0'>%)Es@a2!9T';mndPYD_C6rZt.3'd"Zh$'hT;4=%/Z>#9I#hA!0optI//H3=/WSn3m]sO1]'m7pK3'_6_-LU+1#O`IhhDYO=9G0b!D>.uLJ8$NBS^&WLBNH.r(uT+QWduUqpJ'*0sO+@JS.HoRnl5FgO`t2AeKrjel/U7G_$V+l%IW1I'QqQB/E^B5up#9(o*WnlG?qX:m&\s;H*cRU)7sj`j3CV?4UNjL@m1<7j`]Z4]!`VB#h+e"V]q&"3FL:@]--"*&hPP4hm$?bIdYL5B9IW!BGO9_#a9i%0,/S1mbl'ng&aC`:eRj/S)L32KSZ8n?.n"U/G+5o0Vj)h6`g?<cIQ?;>86>;:"uQDpoj&$A)M:oi\S>>drco!V&>F=(E15=[G5j`j$20QS8$u\00!aLiET%_kR0dTO6(?5(pq8gH"pjE%Xg[@OB33QEu6q&@>uqE&m..k.+;UOCV>:+4-:_j+#XK%%\L]7ao2`J%r=GaUZG5r<`C7h3J7`E/I(GS<k@r:Wn3:RL?skEQ@Pk4`lnVi4;aK:lF%\D5@G&:_bXlcI6=r\Em#240\i,DB[kRTE%>9Yj_i(h9!1C=fu_KM4WikY>*CcK&pDoY<;D&$.-rs!0B2:j8~>endstream Gb"/)=d.i)&q7#kTr3b4T#UQ6l$]:Vl=Q?^U,ch/2`cO%[K')BbAGFFm'#<208qo-6VYK]C_0Ud8Bsn?VD>n[=[F<[V"h^47HeWDg@l5CA2IL68JP2])kk+7k3%_gcI>QS<$VnjL&t^C.QeK-nkZL(nR.N],?=%8Z34N<P(Eae<Zc75N\26IMTXQC+ddK7+Yi*d;c:Gj=>'D7.KtAoYp@:9UjR=S`-+YD_k4\lm.UF8!mCpjSZTe+)_J>fK<A5sd-OObqoCdFK?oR9XN5uGl8rU54CZ`!UJ)MHgSZ)Yhf!#Qm<^E&^3p-='9HV?"X$)lRQc,rrPM2u']=.@!j+#i_t;.3IaD<gAX=7-jVqA.2"HD[a@LaA:_10Nf]S-RRju*I<[7s,I>%62nFu+*@*45(8Z7$-I(9hGoFh%YLpi/0\S)=7jF5!rcYrA_EW:5;ru#^7cNj1p%)?.(+Np<deXTrpP<S`CU`rePFAiiQfI3J%(m;]mFaScn$?A2$Cf"4hkYMof2=W2<hl6bh03s/L&#D1LP8Ui._O0Ac_sDS)q)osFqd%eFrC?E%1h?^(>N?$R&W8^QGf@<[:)tOFiYKAOP-PN(M.`"P@F'[VCD#`t"J67[&<G^HcN>Z68S?a+68JaE3MS!*bJm2$OsI1W=\b2,a7i"S\^'d!It\pBj@:@uVEQ0,-O%XZSCT.Y'1>c"1^+1X*4M#;BQ?VfA"Id)kj?>Xl$//f$B1]]8_GpQ+De!AFEQgL\uJn)=4%@R^hd(_Ln&@Q+lm\^U/m/5MpZ:\aL4PrBMNfp0T3d\km#7)C`NF_$c)HEW+0$kA.+=L`!V3[Q;bW'kdSSUhtfuDQLEJS`TA$<1?"Y6Ap0W?em:VZY[u4amm&-LA_:PV$b3jnK2`goJ`F)@]o3r_DL%U8&8Pi\g=4Hm\SAF(]UNLA]r>VR-&KYYd=ueZV3!(u'sq'INkHck<P>Qt@\hZ>L$+)Wk&DF1#I0C%8Ypbm4:?g-UkW)^'Jo^DYmDGhg'OXlma+><-s0o9^m8(l.tB3oXI`#TDH>B-VWpp??"3>SkNFHE+*r@0MB;IOMMO.<:EI-`iA4'_D<Y3gX\Wo)=tI]VKs:^;fm<q"qKd/6L.2S7&DP#a^@@>H_iS^8(p>l'NBD/\X.d)Rl2DN;nX<,W[a09RM;faG5c7k]#^_Z@(GC8,]a+If+<3-$B%m;&h]DR8]1#1lI:l&aS:gP-E>TuhcpNfRf9kslWW.'?i?R`V/5gVMPE\[;7BKN7,aGC%]_k?oBQ6SunGrbr(kK;8!)R99!:I,n^m9ZT\B>'V58FI]/fOf?=;ZS#Ms<1!WaO::MBM.(j^]C+RG\)0KTkOo._!&kb)<;L+iq!>ZmUKDs'[$dm2C\@Aon/S;>&BNWCB(F@;.P+YS[-m?m5@mY5"qah:'ZAeOHRA'f]5.7n.ViTPG?C)Oao:Blku#>Gme!Z5D!TA;S!_+ZE@0&CQs@Q1]sa324ar@n:l'=lIG5[Eum>-Ok%@VW#-GY*QCSfrF+F2C8-854!i@Te>mAqh3$@^h@@R4BAedf9kM6#L(#%%Eq_Bjr\+oRJ$Q:<pZ&>b<mQ!q(\oT&9!&n]i.#d5`5C0%PbgYa&%$q/f2X^-0V?`BTDa/b6F1>cR@$6L=C;%Xo^,n?62&LH;Go[G9!RraQRti5i9BY8.1+P\B=n@B+I7LhRO;ib'"D"W4F0d!$"b2EE*eY6]k.h%okOZ#n?M(,c^t#=grSRYKAE"aC9^-<=;d-DCBZ\Bdis&4rbj'$Li=\/$h7l/!5Hee_W:LhIMX/G4?M9Yj5IhO?2aqY^b\#f#,>YY5L(>f1F[RmbYR:;%&EOLZBulgAua(_11R%.9dG%QcU5XeACWfL<X+6UL$o0%/'\bc=uAEO?Pn[7s^.6N,-XnNTb,2?8b;W_;3N6CNjb_kg&=S_;]o!)#S$n,fOE;Y.e-cZGtI"=cU^W:H>T<Te5u.'rfZ!C(<1p!e5j9,iJu=9:lNb!IAa;pB@8.m?RkuX:oa2^I)F$!*Fn/jF@/E5j4j@F5I^h:6Tc+/"gboT8(XHBM33*0'>$Z&g%@`o<hG1o/>m]mF(N\?"^RiKh>XBJWnU"V%&<P+<nmeYhKMK;rFH/#/!o6oH=2P10d.Y$Tf;SKLXqXpH8P;8G;m[C<^p)54M%QX?`_jf>d+Bg/o`]n)"VaB$31@>SY?p<;fMj$*^.sbFR,'<]64j6:9\"Glhe2T8'G$?&2k<4Yp70#gtGf!(3bnX8":b7er%$q.,/q%Fg*M.hlUXM=\Xp@t+3`h%#.(q"cJe?sS*>qhHF$O2$]PG<FqpT>MNP2XMoCh3W+rrr[snSqS"MG6?KIj`=']m)>.GJ-:J=n]Pqnb]fW>cQLoi/On'oqKAmgb_#AFmGXU2%i_+F.Yf&iJO^PF;1NM>BBg:\XLp*j<m?4204I'`9s__VP_?C^C_c!a'or`88'h*@%E1ga20d<a[7_S"AUL)U`GrJHiqsOHlo7SI1ot208\\:2pQ=d]aMMhCm6;#q#!RqHSkB-Bm4J$/+2MP!$'NU[Oe"n7I][5Ee%8Z7Fbq-U&#^KpY;l?FD$lF8l:IcWE^r2sYY0_8:M=";-e>T&<Pr"'V@1Bu7eOZ)INjomV,Hpf0qoLi`QE5:V-X[B+9m1ZiXH^=r=PuoBCk^q'W1>cnWV"75[q2t<BmY5qWn&7S1X/!:OhFU>mKH[c%I[Ya'k?RQ`#,iQ'E]t-6t5:\[IUmB"?^Ua8Pm=b>Yd")(uB*O8"iU^:&WGh[eG&5T'0n9@l%1Y9[le<F7#0+LFdJ'bI"3T93X^J7+>^*+nQUOOS&/40JL%&Rk`k<abS"'(cS.eqMJ\p0-Wcr7qZ(Wn4g(KV/L0L5TA^3m36BK-Z(arZ-^Yi`pJ&Bp7E&_o#m1)g4_5NS+.cI+TIWJ)45*QG\[AhGnTT-VoY<GEN?Mj)N8:O<3,tGAph95+'5m_REF&+DAhtCZfCoG[(4ggPH['E!,WJ%@%]<JV2R-_mO5fFSTYd4jA.VRpn_fk2C6X4Gu(WNMW]+EAnu"qMY>\4F6kuc;uHN&to#JOAC6CY<b<VfM<(TI$_R@PtPIVntDQQ!>g,##6~>endstream
endobj endobj
51 0 obj 51 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 459 /Filter [ /ASCII85Decode /FlateDecode ] /Length 400
>> >>
stream stream
GatVW?#.;2'F*KBJ&Sr:DRck:qR+L+lR%j1gF*=oLi(XZD,ZY`nD>mB>/p%uO?`m1,_mbN!P2H(ZR5oA+2cRS!5erP!lNG;^]F5?ZgAU-S!L0:Sul5THCe_5^KBclC?nSuX?116q5TlmC,Y"ERHZRRoj].qm_WpKkYF^bJ.2?I0V3&K$4G=)@DJ'[-=FC-o!AY-VX(<g_\f-iUo_`7!EM5\^'FQBpu,loe[jg_4\`]9pDTb#SOEHf*+;'HKo)IC"$:(fknO]K]eu^s#S#/d,'un+]jX6j[@l&^T1MplHX=1eVCWlj^*T[$Kr09Pd<9HQb1'-UGB?d&2=mTq-aAQ3'"m4ek*!ek;,D7GV7_1L=[C4F"=n`AJ0kSdnd;DYr/($>n-j*/k7=,^BL]if$:]886:>9IN^r,j<eb4jh9e?c&&?<s/JD0hZA,TWFmjbioFD;"+(+<\"o~>endstream GatVW?YdM4&-24oIYrIjDQkb<qJVGJW1+p@[[Af\6Ioc"@@EWa_/I(j[>a's+9r5.?&i:;"(]=8c9_Rp;g9.*i*i;9i;'MdF:8IuerFt@oE.%2"6;f`a):DAjP+meN4Q<$3-N0<eP<qu!*"+tj)--r-cRj-HVjo!)9Lc<QFp!LRUiQP@?,=1L$#7a9s[a1]ES-6oPSS<D4GZ8Z&>c+]Ok\F-A64F;f*hf@11>LC:c=(=`.6obIY'3de^Zlf-PP#$]JM9mB/.[+USgil^B,&T!7"QY0j7HL9D"A)_&>SohnfIQU:8S">-Ea"n85t``tb<U-Is+.m1Xol01Q*.[dR6K8`=mPGNro8Y2!?P?p^jYhnm"ZJC)1.pa#YMRnMKUCD)N(\n:>k8.2.WC[~>endstream
endobj endobj
xref xref
0 52 0 52
@@ -350,7 +350,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<9c7fc54d8829b4dd20be9a391b353f80><9c7fc54d8829b4dd20be9a391b353f80>] [<800c79e27ed019b09a790354c7c3deb4><800c79e27ed019b09a790354c7c3deb4>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 29 0 R /Info 29 0 R
@@ -358,5 +358,5 @@ trailer
/Size 52 /Size 52
>> >>
startxref startxref
18616 18557
%%EOF %%EOF

View File

@@ -102,7 +102,7 @@ endobj
endobj endobj
18 0 obj 18 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False /Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False
>> >>
endobj endobj
@@ -209,7 +209,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<9a6afd45fa8ce10be0ffbc92fd23c7bc><9a6afd45fa8ce10be0ffbc92fd23c7bc>] [<4fad3f4eec3a3b3d3d5fc9ff821b245e><4fad3f4eec3a3b3d3d5fc9ff821b245e>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 18 0 R /Info 18 0 R

View File

@@ -82,7 +82,7 @@ endobj
endobj endobj
15 0 obj 15 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190938+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190938+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False /Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False
>> >>
endobj endobj
@@ -159,7 +159,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<9a2835e2004e186f32352a8c6c16eef0><9a2835e2004e186f32352a8c6c16eef0>] [<d79a73d4072e12be58fa83a481c816e2><d79a73d4072e12be58fa83a481c816e2>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 15 0 R /Info 15 0 R

View File

@@ -0,0 +1,394 @@
# Media service (SFU / MCU) — AI deployment prerequisites
**Document type:** Requirements / handoff (pre-SPEC)
**Date:** 2026-06-12
**Audience:** PO / sysop → AI agent for full deploy + tune on fresh Alpine HVM(s)
**Related:** [docs/specs/20100612_1_scaling.md](../docs/specs/20100612_1_scaling.md), [docs/DRs/20100612_1_scaling.md](../docs/DRs/20100612_1_scaling.md), [examples/sfu/README.md](../examples/sfu/README.md), [docs/20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) §SFU
**PDF:** `tmp/20260612-MEDIASERVICE-AI-requirements.pdf` — regenerate with `.venv-pdf/bin/python docs/_pdf_build/md_to_pdf.py` on the copy under `tmp/`
**Code names (product):** **SPIDERWEB** = SFU-alike, **SPIDER** = MCU-alike ([draft glossary](../docs/drafts/20100611_1_net_topology_glossary.txt))
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive answer — HVM vs Docker vs monolith BE](#1-executive-answer-hvm-vs-docker-vs-monolith-be)
- [2. What you give the AI on day zero](#2-what-you-give-the-ai-on-day-zero)
- [3. Recommended instance layout (up to 3 HVMs)](#3-recommended-instance-layout-up-to-3-hvms)
- [4. Network, DNS, and firewall prerequisites](#4-network-dns-and-firewall-prerequisites)
- [5. Integration with existing BE (isolation model)](#5-integration-with-existing-be-isolation-model)
- [6. SFU vs MCU stack choice](#6-sfu-vs-mcu-stack-choice)
- [7. Packages and software (installed later by AI)](#7-packages-and-software-installed-later-by-ai)
- [8. Sizing and bandwidth (200/200 Mbit context)](#8-sizing-and-bandwidth-200200-mbit-context)
- [9. AI workflow after prerequisites are met](#9-ai-workflow-after-prerequisites-are-met)
- [10. Out of scope for first media PoP](#10-out-of-scope-for-first-media-pop)
- [11. PO checklist (copy before handoff)](#11-po-checklist-copy-before-handoff)
- [12. Changelog](#12-changelog)
<!-- /toc -->
---
## 1. Executive answer — HVM vs Docker vs monolith BE
### Recommendation (DR-aligned)
| Approach | Verdict | Why |
|----------|---------|-----|
| **SFU/MCU on current androidcast BE monolith** | **No** | PHP/MariaDB/RSSH/Gitea share CPU/RAM/NIC; `pm.max_children=5` today; blast radius; [INFRA.md](../docs/INFRA.md) keeps `:8089` as **other** Janus stack |
| **Fresh dedicated Alpine HVM(s)** | **Yes (primary)** | Isolated **media plane**; snapshots; UDP/WebRTC without touching hub/crashes |
| **Docker on dedicated HVM** | **Yes (preferred delivery)** | Reproducible Janus/mediasoup/coturn/ffmpeg; easy rollback; same host still “empty” at handoff |
| **Docker on existing BE** | **No** | Does not isolate bandwidth or failure domain |
**Practical model:** PO provides **13 empty Alpine HVMs** with SSH + DNS. AI installs **Docker (+ compose)** on them and runs **SPIDERWEB (SFU)** or **SPIDER (MCU)** stacks in containers. **Control plane** (signaling auth, room lifecycle, RBAC) talks to existing crashes BE via **HTTPS/OpenAPI-style APIs** — no co-location of MariaDB or PHP-FPM with media.
```mermaid
flowchart TB
subgraph Today["Today — monolith BE (unchanged)"]
FE["Gentoo FE apps.f0xx.org"]
BE["Alpine BE :80<br/>PHP MariaDB Gitea RSSH"]
end
subgraph New["New — media PoP (13 HVMs)"]
MED["media-1 Alpine HVM"]
DOCK["Docker: SFU + TURN + signaling sidecar"]
end
MOB["Android app"] --> FE
FE --> BE
MOB -.->|"WebRTC UDP/TCP<br/>post-alpha"| MED
DOCK --> MED
BE -.->|"REST: token room RBAC<br/>OpenAPI"| DOCK
class New fill:#e8f5e9,stroke:#2e7d32
```
**Not too heavy:** one HVM + Docker Compose is enough for **lab/stress**; three HVMs match **prod-shaped** split (SFU / TURN / transcode). Start with **1**; split when metrics or PO asks.
---
## 2. What you give the AI on day zero
Minimum handoff — **no media packages required yet** on the empty machine.
### 2.1 Access
| Item | Required | Example |
|------|----------|---------|
| **SSH target** | Yes | `ssh media@media-lab.example.org` or intra `10.7.x.x` |
| **Sudo** | Yes | passwordless `sudo` for `apk`, `rc-service`, docker |
| **SSH key** | Yes | laptop → HVM; optional: HVM → git read for deploy scripts |
| **Jump host** | If no public SSH | `ProxyJump` via `f0xx-monstro` / `alpine-be` (document in `~/.ssh/config`) |
### 2.2 Machine identity
| Item | Required | Notes |
|------|----------|-------|
| **Hostname** | Yes | e.g. `media-1.intra.raptor.org` |
| **OS** | Yes | Alpine 3.x x86_64 (match prod BE) |
| **vCPU / RAM** | Yes | See [§8](#8-sizing-and-bandwidth-200200-mbit-context) |
| **Root disk** | ≥ 40 GiB SSD | containers + logs |
| **Intra IP** | Yes | `10.7.0.0/8` address stable across reboots |
| **Public reach** | For external test | DNS name OR FE proxy only (see [§4](#4-network-dns-and-firewall-prerequisites)) |
### 2.3 PO-owned (snapshots, DNS, firewall)
| Item | Owner | AI needs |
|------|-------|----------|
| **VM snapshots** | PO | “Snapshot before AI session” + rollback name |
| **External DNS** | PO | A/AAAA or CNAME for WebRTC/signaling test |
| **Firewall / DNAT** | PO | UDP/TCP port list from [§4](#4-network-dns-and-firewall-prerequisites) opened on router/FE |
| **TLS certs** | PO or AI via DNS-01 | Lets Encrypt on FE or on media host |
### 2.4 Integration secrets (existing BE — read-only API)
| Item | Required | Purpose |
|------|----------|---------|
| **Crashes base URL** | Yes | `https://apps.f0xx.org/app/androidcast_project/crashes` |
| **Service account or API token** | Yes | Mint rooms / validate session (new `sfu` API or extend OpenAPI) |
| **RBAC model** | Reference | Same pattern as remote access / short links |
| **Gitea / deploy** | Optional | Pull `android_cast` branch `feature/sfu-relay` or deploy tarball |
AI does **not** need MariaDB root on monolith BE if all integration is **HTTP APIs**.
### 2.5 Decisions PO should state (or defaults)
| Decision | Default if silent |
|----------|-------------------|
| **Topology code name** | **SPIDERWEB** (SFU) first; MCU later |
| **Engine** | **mediasoup** (greenfield) — Janus on `:8089` is **other** stack, avoid unless PO picks Janus on **new** host |
| **Instance count** | **1** for lab; **3** for prod-shaped |
| **Public path** | Subdomain `media.apps.f0xx.org` → FE → media HVM (keeps TLS at FE) |
| **TURN** | **coturn** on same HVM (lab) or instance 2 (prod) |
---
## 3. Recommended instance layout (up to 3 HVMs)
### 3.1 Lab — single HVM (minimum handoff)
```mermaid
flowchart LR
subgraph HVM1["media-1 (single Alpine HVM)"]
NG["nginx :443 reverse proxy"]
SIG["signaling sidecar<br/>Node or Go"]
SFU["mediasoup / Janus SFU"]
TURN["coturn STUN/TURN"]
FF["ffmpeg worker optional"]
end
Client["App / browser"] --> NG
NG --> SIG
Client --> TURN
Client --> SFU
SIG --> SFU
BE["Existing BE PHP"] -.->|HTTPS| SIG
```
All services in **one docker-compose** stack; fine for stress and first WAN cast tests.
### 3.2 Prod-shaped — three HVMs
| VM | Role | Containers / daemons |
|----|------|----------------------|
| **media-1** | **SPIDERWEB SFU** | mediasoup (or Janus), nginx edge, signaling sidecar |
| **media-2** | **TURN + signaling** | coturn, optional dedicated signaling if scaled |
| **media-3** | **Transcode / MCU path** | ffmpeg workers, optional MCU (SPIDER) if not SFU-only |
```mermaid
flowchart TB
FE["FE TLS"] --> M1["media-1 SFU"]
FE --> M2["media-2 TURN"]
APP["Android"] -->|UDP WebRTC| M1
APP -->|UDP TURN| M2
BE["Monolith BE APIs"] -->|REST| M1
M1 --> M3["media-3 ffmpeg optional"]
```
PO can hand **one** machine first; AI documents split boundaries so instances 23 are **additive**, not rewrite.
---
## 4. Network, DNS, and firewall prerequisites
### 4.1 Traffic paths
```mermaid
flowchart TB
Internet["Internet clients"]
Router["Router 200/200 Mbit"]
FE["Gentoo FE"]
M["Media HVM(s)"]
BE["Androidcast BE :80"]
Internet --> Router --> FE
FE -->|"HTTPS signaling<br/>wss / REST"| M
FE --> BE
Internet -->|"UDP WebRTC<br/>after DNAT or 1:1"| M
```
**Do not** attach media to androidcast **`:8089`** vhost ([INFRA.md](../docs/INFRA.md) §2).
### 4.2 Ports to plan (PO opens on FE/router → media HVM)
| Port / proto | Service | Notes |
|--------------|---------|-------|
| **443/tcp** | HTTPS / WSS signaling | Via FE reverse proxy recommended |
| **3478/udp,tcp** | STUN/TURN (coturn) | Required for many NATs |
| **4915265535/udp** | WebRTC media (ICE) | Range size depends on engine; document in SFU SPEC |
| **8080/tcp** | SFU HTTP API (internal) | **Not** public; FE or intra only |
| **22/tcp** | SSH | Intra or VPN only in prod |
Exact ICE port range: set in mediasoup/Janus config during deploy; PO must allow **UDP range** on path to media HVM.
### 4.3 DNS names (examples)
| Name | Points to | Use |
|------|-----------|-----|
| `media.apps.f0xx.org` | FE → proxy → media-1 | Public signaling + health |
| `turn.apps.f0xx.org` | FE or media-2 | TURN URI in app |
| `media-lab.intra.raptor.org` | 10.7.x.x | AI + sysop intra testing |
---
## 5. Integration with existing BE (isolation model)
Media HVM **must not** share MariaDB or PHP-FPM with crashes console. Integration is **API-only**:
```mermaid
sequenceDiagram
participant App as Android app
participant FE as FE nginx
participant BE as Crashes BE PHP
participant SIG as Media signaling
participant SFU as SFU engine
App->>FE: HTTPS session / cast token
FE->>BE: existing auth APIs
App->>SIG: WSS create room (bearer)
SIG->>BE: POST validate token / RBAC
BE-->>SIG: room allowed + ttl
SIG->>SFU: create room
App->>SFU: WebRTC publish/subscribe UDP
```
| Integration | Method | Existing artifact |
|-------------|--------|-------------------|
| Health gate | GET | `api/sfu_health.php` (stub today) |
| Feature flag | config | `config.php``sfu.enabled`, URLs |
| Room mint / revoke | **New REST** | OpenAPI under crashes or `media-api` submodule |
| Auth | Bearer + session RBAC | Same as remote access / short links pattern |
| Metrics | POST ingest | Optional → `graph_upload.php` or new series |
| No direct DB | — | Media DB (if any) is **local** to HVM (Redis/SQLite) |
**OpenAPI:** extend [examples/crash_reporter/backend](../examples/crash_reporter/backend) or new `backend/media-signaling` repo later — AI needs **written** API contract before prod wire-up.
---
## 6. SFU vs MCU stack choice
| | **SPIDERWEB (SFU)** | **SPIDER (MCU)** |
|---|---------------------|------------------|
| **Use** | Multiparty cast; subscribers receive per-stream | Mix down; single output; higher CPU per room |
| **Engine candidates** | **mediasoup** (preferred greenfield), Janus VideoRoom | Janus MCU plugin, ffmpeg bridge, custom |
| **CPU** | Moderate per stream; scales with subscribers | High per room (encode) |
| **Latency** | Lower | Higher |
| **First milestone** | **Yes** | Phase 2 / media-3 |
**Janus on existing `:8089`:** treat as **foreign** — do not piggyback androidcast nginx. New Janus instance on **media HVM** only if PO rejects mediasoup.
**Docker vs bare metal on HVM:** use **Docker** for SFU/TURN/signaling; host only needs Alpine + docker + nginx + firewall tools.
---
## 7. Packages and software (installed later by AI)
PO delivers **almost empty** Alpine; AI installs when handoff complete.
### 7.1 Host (Alpine packages)
```text
# Baseline (all media HVMs)
openssh sudo bash curl wget git ca-certificates
nginx openssl iptables ip6tables
docker docker-cli-compose
# Optional host tools
tcpdump bind-tools jq python3
# If bare-metal fallback (no Docker) — not recommended first
# janus-gateway mediasoup — prefer official containers
```
### 7.2 Container images (typical compose)
| Service | Image / build | Role |
|---------|---------------|------|
| **mediasoup** or **janus** | Official or pinned Dockerfile in repo | SFU |
| **coturn** | `coturn/coturn` | STUN/TURN |
| **signaling** | Custom Node/Go from repo | Rooms, BE API client |
| **nginx** | `nginx:alpine` or host nginx | TLS termination at edge or intra |
| **ffmpeg** | `jrottenberg/ffmpeg` or alpine | Transcode sidecar (media-3) |
| **redis** | `redis:alpine` | Room state optional |
### 7.3 Repo deliverables (AI produces after deploy)
| Artifact | Location (proposed) |
|----------|---------------------|
| `docker-compose.yml` | `examples/sfu/deploy/` or `backend/media-pop/` |
| nginx FE fragment | `examples/sfu/deploy/nginx.fe-media.snippet` |
| BE config keys | `config.example.php``sfu.*` (already stubbed) |
| Smoke script | `examples/sfu/scripts/smoke_sfu.sh` |
| OpenAPI | `examples/sfu/openapi.yaml` |
---
## 8. Sizing and bandwidth (200/200 Mbit context)
WAN **200/200 Mbit** sits behind FE; media UDP may be **DNAT** to media HVM.
| Profile | vCPU | RAM | Notes |
|---------|------|-----|-------|
| **Lab / stress (1 HVM)** | **48** | **816 GiB** | SFU + TURN + signaling; ~1030 parallel WebRTC streams |
| **Prod SFU node** | **8+** | **16 GiB** | Dedicated NIC budget; see [SPEC §12.3](../docs/specs/20100612_1_scaling.md#123-network-bandwidth-and-egress) |
| **TURN only** | **2** | **4 GiB** | Mostly relay bandwidth |
| **ffmpeg node** | **8+** | **1632 GiB** | CPU-bound transcodes |
**Bandwidth rule of thumb (SFU):**
| Streams | Approx WAN need |
|---------|-----------------|
| 10 × 1 Mbps video | **~1015 Mbps** + overhead |
| 50 × 1 Mbps | **~5080 Mbps** — approaches 200 Mbit shared with rest of estate |
| 100+ viewers | **CDN/SFU PoP or second site** — not single 200 Mbit |
Stress test: measure on **intra** first, then **public DNS** through FE.
---
## 9. AI workflow after prerequisites are met
```mermaid
flowchart TD
A["PO: empty HVM + SSH + DNS + firewall list"] --> B["AI: verify ssh sudo intra ping"]
B --> C["AI: apk + docker + compose"]
C --> D["AI: deploy SFU stack lab compose"]
D --> E["AI: nginx FE fragment + BE sfu.enabled URLs"]
E --> F["Smoke: sfu_health + WSS + ICE"]
F --> G{"PO: load test OK?"}
G -->|yes| H["Document + optional 2nd/3rd HVM split"]
G -->|no| I["Tune UDP range CPU docker limits"]
I --> F
```
| Step | AI action | PO action |
|------|-----------|-----------|
| 0 | — | Snapshot empty VM |
| 1 | Probe CPU/RAM/NIC, `apk update` | Confirm DNS/firewall |
| 2 | Install docker, compose, pin versions | — |
| 3 | Bring up mediasoup + coturn + signaling | — |
| 4 | FE nginx `location /sfu/` or subdomain proxy | Reload FE nginx |
| 5 | Enable `sfu.enabled` + URLs on monolith BE **only** | Sync config.php |
| 6 | Run smoke + optional load/WebRTC client | Review metrics |
| 7 | Snapshot “golden” media PoP | — |
---
## 10. Out of scope for first media PoP
- Replacing LAN UDP cast (alpha path unchanged)
- Moving crashes/tickets/Gitea to media HVM
- Full GEO policy ([SPEC §14](../docs/specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) — later with F3/F10
- Production payments / recording storage
- Merging with Janus stack on BE `:8089`
---
## 11. PO checklist (copy before handoff)
```text
[ ] Alpine 3.x HVM provisioned (13); specs noted
[ ] SSH + sudo for AI; key installed
[ ] Intra IP 10.7.x.x stable
[ ] External DNS name for test (or FE-only proxy agreed)
[ ] Firewall: UDP 3478 + WebRTC range → media HVM
[ ] Snapshot taken (empty baseline)
[ ] Decision: SPIDERWEB (SFU) first — mediasoup default
[ ] Crashes BE base URL + API token for room validation
[ ] Confirm: do NOT use androidcast :8089 / monolith BE for SFU
[ ] Optional: 2nd/3rd HVM reserved for TURN + ffmpeg
```
---
## 12. Changelog
| Date | Change |
|------|--------|
| 2026-06-12 | Initial prerequisites — HVM vs Docker, 3-instance layout, BE API isolation, PO checklist |
---
**Documentation index:** [docs/README.md](../docs/README.md)

View File

@@ -28,6 +28,8 @@ SUBTITLE = {
SUBTITLE_BY_REL = { SUBTITLE_BY_REL = {
"specs/20100611_3_url_shortener.md": "URL shortener service — SPEC (post-alpha, s.f0xx.org)", "specs/20100611_3_url_shortener.md": "URL shortener service — SPEC (post-alpha, s.f0xx.org)",
"DRs/20100611_3_url_shortener.md": "URL shortener service — design review", "DRs/20100611_3_url_shortener.md": "URL shortener service — design review",
"specs/20100612_1_scaling.md": "Platform scaling — SPEC (clusters, bandwidth, GEO, cloud path)",
"DRs/20100612_1_scaling.md": "Platform scaling — DR (architecture, bandwidth, GEO policy, task graph)",
} }

View File

@@ -22,6 +22,11 @@ from reportlab.platypus import (
) )
from md_common import TOC_END, TOC_START, collect_headings, github_slug, strip_md_link_text from md_common import TOC_END, TOC_START, collect_headings, github_slug, strip_md_link_text
from mermaid_render import (
cached_mermaid_png,
diagram_flowables,
ensure_diagram_caption_style,
)
def _xml(text: str) -> str: def _xml(text: str) -> str:
@@ -187,7 +192,9 @@ def build_pdf_from_markdown(
# Body — skip title, TOC block, index link # Body — skip title, TOC block, index link
skip_toc = False skip_toc = False
in_code = False in_code = False
code_lang = ""
code_lines: list[str] = [] code_lines: list[str] = []
ensure_diagram_caption_style(styles)
i = 0 i = 0
while i < len(lines): while i < len(lines):
line = lines[i] line = lines[i]
@@ -215,12 +222,29 @@ def build_pdf_from_markdown(
if line.strip().startswith("```"): if line.strip().startswith("```"):
if in_code: if in_code:
story.append(Preformatted("\n".join(code_lines), code_style)) block = "\n".join(code_lines)
if code_lang == "mermaid" and block.strip():
try:
png = cached_mermaid_png(block)
story.extend(diagram_flowables(png, styles))
except Exception as exc:
story.append(
Paragraph(
f"<i>Diagram render failed ({_xml(str(exc))}) — source:</i>",
small,
)
)
story.append(Preformatted(block, code_style))
story.append(Spacer(1, 0.08 * cm))
else:
story.append(Preformatted(block, code_style))
story.append(Spacer(1, 0.08 * cm)) story.append(Spacer(1, 0.08 * cm))
code_lines = [] code_lines = []
code_lang = ""
in_code = False in_code = False
else: else:
in_code = True in_code = True
code_lang = line.strip()[3:].strip().lower()
i += 1 i += 1
continue continue
if in_code: if in_code:

View File

@@ -0,0 +1,145 @@
"""Render Mermaid diagram sources to PNG for PDF embedding (mermaid-cli + Chromium)."""
from __future__ import annotations
import hashlib
import subprocess
import tempfile
from pathlib import Path
from reportlab.lib.units import cm
from reportlab.lib.utils import ImageReader
from reportlab.platypus import Image, Paragraph, Spacer
BUILD_DIR = Path(__file__).resolve().parent
CONFIG = BUILD_DIR / "mermaid-config.json"
CACHE_DIR = BUILD_DIR / "mermaid_cache"
MERMAID_CLI = ("npx", "--yes", "@mermaid-js/mermaid-cli@11.4.0")
MAX_DIAGRAM_WIDTH_CM = 15.8
MAX_DIAGRAM_HEIGHT_CM = 17.0
CAPTION_STYLE_NAME = "DiagramCaption"
def _content_hash(content: str) -> str:
return hashlib.sha256(content.strip().encode("utf-8")).hexdigest()[:16]
def estimate_mermaid_pixels(content: str) -> tuple[int, int, int]:
"""Return (width_px, height_px, scale) heuristics for flowchart layout."""
lines = [ln for ln in content.splitlines() if ln.strip() and not ln.strip().startswith("%%")]
n_edges = sum(1 for ln in lines if "-->" in ln or "---" in ln or "-.->" in ln)
n_nodes = sum(
1
for ln in lines
if "[" in ln or "(" in ln or "{" in ln or (ln.strip() and "-->" not in ln and "subgraph" not in ln.lower())
)
n_sub = sum(1 for ln in lines if "subgraph" in ln.lower())
lr = any("flowchart LR" in ln or "graph LR" in ln for ln in lines)
scale = 2
if lr:
width = 2200 + min(n_nodes, 12) * 120
height = 650 + n_sub * 180 + min(n_edges, 10) * 30
else:
width = 1700 + min(n_sub, 4) * 200
height = 650 + n_edges * 70 + n_sub * 220 + min(n_nodes, 14) * 25
width = int(min(max(width, 1200), 3600))
height = int(min(max(height, 550), 3400))
return width, height, scale
def render_mermaid_to_png(content: str, dest: Path, *, width: int, height: int, scale: int) -> None:
if not CONFIG.exists():
raise FileNotFoundError(f"mermaid config missing: {CONFIG}")
dest.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", suffix=".mmd", delete=False, encoding="utf-8") as tmp:
tmp.write(content.strip() + "\n")
mmd_path = Path(tmp.name)
try:
subprocess.run(
[
*MERMAID_CLI,
"-i",
str(mmd_path),
"-o",
str(dest),
"-c",
str(CONFIG),
"-b",
"white",
"-w",
str(width),
"-H",
str(height),
"-s",
str(scale),
],
cwd=str(BUILD_DIR.parents[1]),
check=True,
timeout=180,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError as exc:
msg = (exc.stderr or exc.stdout or str(exc)).strip()
raise RuntimeError(f"mermaid-cli failed: {msg}") from exc
finally:
mmd_path.unlink(missing_ok=True)
if not dest.is_file() or dest.stat().st_size == 0:
raise RuntimeError(f"mermaid-cli produced no PNG: {dest}")
def cached_mermaid_png(content: str) -> Path:
"""Render or reuse cached PNG for Mermaid source."""
CACHE_DIR.mkdir(parents=True, exist_ok=True)
key = _content_hash(content)
png = CACHE_DIR / f"{key}.png"
if png.is_file() and png.stat().st_size > 0:
return png
w, h, scale = estimate_mermaid_pixels(content)
render_mermaid_to_png(content, png, width=w, height=h, scale=scale)
return png
def diagram_flowables(
png_path: Path,
styles,
*,
caption: str = "",
max_width_cm: float = MAX_DIAGRAM_WIDTH_CM,
max_height_cm: float = MAX_DIAGRAM_HEIGHT_CM,
) -> list:
"""ReportLab flowables: image (+ optional caption) sized to fit A4 body."""
reader = ImageReader(str(png_path))
w_px, h_px = reader.getSize()
max_w = max_width_cm * cm
max_h = max_height_cm * cm
scale = min(max_w / w_px, max_h / h_px)
img = Image(str(png_path), width=w_px * scale, height=h_px * scale)
img.hAlign = "CENTER"
out: list = [Spacer(1, 0.12 * cm), img, Spacer(1, 0.06 * cm)]
if caption:
cap = styles[CAPTION_STYLE_NAME] if CAPTION_STYLE_NAME in styles else styles["Normal"]
out.insert(0, Spacer(1, 0.06 * cm))
out.append(Paragraph(f"<i>{caption}</i>", cap))
out.append(Spacer(1, 0.1 * cm))
return out
def ensure_diagram_caption_style(styles) -> None:
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib import colors
if CAPTION_STYLE_NAME not in styles:
styles.add(
ParagraphStyle(
CAPTION_STYLE_NAME,
parent=styles["Normal"],
fontSize=7,
textColor=colors.grey,
alignment=1,
spaceAfter=4,
)
)

View File

@@ -1,4 +1,5 @@
reportlab>=4.0 reportlab>=4.0
# Diagrams: npx @mermaid-js/mermaid-cli (uses bundled Chromium). # Diagrams in markdown (```mermaid blocks): npx @mermaid-js/mermaid-cli (uses bundled Chromium).
# Do NOT use rsvg-convert on Mermaid SVG — HTML labels are dropped. # Do NOT use rsvg-convert on Mermaid SVG — HTML labels are dropped.
# Example: npx --yes @mermaid-js/mermaid-cli@11.4.0 --version

View File

@@ -1,3 +1,7 @@
# Draft — source for SPEC/DR (rev. 2 — 2026-06-12)
# SPEC: docs/specs/20100612_1_scaling.md
# DR: docs/DRs/20100612_1_scaling.md
#
The problem: The problem:
Now we have a small VM instance which serves not so much of things. A short list of services include: Now we have a small VM instance which serves not so much of things. A short list of services include:
- own web service (nginx+php) for UI/UX and some control panels, serving issues, tickets, graphs, build control (orchestration) - own web service (nginx+php) for UI/UX and some control panels, serving issues, tickets, graphs, build control (orchestration)
@@ -32,6 +36,17 @@ Ask AI to collect the information, make its decision based on all that we curren
Also, the matter of seamless scaling is super important, better to have the load balancing ready to use just from cloud provider. Also, the matter of seamless scaling is super important, better to have the load balancing ready to use just from cloud provider.
Also, let AI think of how many simulatltaneous connections can we have per instance - the question is for RSSH implementation and future improvements Also, let AI think of how many simulatltaneous connections can we have per instance - the question is for RSSH implementation and future improvements
Network bandwidth (missing in rev.1 — add to SPEC/DR):
- we scale not only by CPU/RAM/connections but by **network throughput**: ingress/egress on FE WAN, FE→BE backhaul, per-VM NIC, CDN/object-store egress, SFU/ffmpeg media pipes, OTA/APK download storms, WireGuard UDP
- need **budget metrics** per service (Mbps sustained, peak, GB/month egress) and alerts before link saturation
- single FE uplink + single BE path is a hidden bottleneck today; cloud/hybrid phase must model **egress cost** (OTA, recordings, transcodes)
Global availability and GEO (missing in rev.1 — add to SPEC/DR):
- long-term: **the world can reach our services** (consoles, APIs, OTA, cast/signaling, short links, future commercial VPN) with acceptable latency — multi-region or CDN edge as we grow
- simultaneously: **segment isolation by policy** — not all content or features in all countries; example: certain recordings, OTA builds, or published cast archives must **not** be offered in Zambia (ZM) or other denylisted regions unless PO overrides
- enforcement should be layered: DNS/Geo routing, CDN/WAF geo rules, app/BE **geo_policy** on content objects, tenant RBAC — default allow globally, explicit **deny segments** per resource or catalog
- dev/staging may stay single-region; prod gains GEO rules when F3 cast storage / F5 payments / F9 streaming go live
The future key role requirements: The future key role requirements:
- multipoint casting and routing outside the intra network, usage of 3rdparty or own multipoint solutions on the backends(MCU, SFU...) - multipoint casting and routing outside the intra network, usage of 3rdparty or own multipoint solutions on the backends(MCU, SFU...)
- new RBAC control, new roles - now we're thinking as developers and deliver the max of services to developers, but we will migrate to the other model sooner or later - new RBAC control, new roles - now we're thinking as developers and deliver the max of services to developers, but we will migrate to the other model sooner or later

View File

@@ -157,7 +157,7 @@ endobj
endobj endobj
32 0 obj 32 0 obj
<< <<
/Border [ 0 0 0 ] /Contents () /Dest [ 45 0 R /Fit ] /Rect [ 45.68504 419.2874 129.269 430.2874 ] /Subtype /Link /Type /Annot /Border [ 0 0 0 ] /Contents () /Dest [ 46 0 R /Fit ] /Rect [ 45.68504 419.2874 129.269 430.2874 ] /Subtype /Link /Type /Annot
>> >>
endobj endobj
33 0 obj 33 0 obj
@@ -275,7 +275,7 @@ endobj
endobj endobj
48 0 obj 48 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260611185331+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185331+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260612190939+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612190939+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (URL shortener service \204 specification) /Trapped /False /Subject (\(unspecified\)) /Title (URL shortener service \204 specification) /Trapped /False
>> >>
endobj endobj
@@ -429,7 +429,7 @@ endobj
endobj endobj
78 0 obj 78 0 obj
<< <<
/Dest [ 45 0 R /Fit ] /Next 79 0 R /Parent 49 0 R /Prev 77 0 R /Title (18. Routing \(normative\)) /Dest [ 46 0 R /Fit ] /Next 79 0 R /Parent 49 0 R /Prev 77 0 R /Title (18. Routing \(normative\))
>> >>
endobj endobj
79 0 obj 79 0 obj
@@ -456,10 +456,10 @@ Gatn'?#SIU'Sc)J/'_]eX;jIs\N=_LLB/:P7d=7ZK9d@4ZQuObjX1#A8%+ni6(61%+kQuM9$b[Jnsl%W
endobj endobj
83 0 obj 83 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3526 /Filter [ /ASCII85Decode /FlateDecode ] /Length 3529
>> >>
stream stream
Gau0F=``=W&q801ka/DL-8rj?E,$JbfTTjs)*=#D9iWWuKHqlY_#[+L]=%Ze)a#C'i[JM58JDM>GGEPNIpW9baS8W<J))-\LBUA$=$TVkYm*X`FBI>7mGcCJDj6uoDFgXAa?!G3/K("Pj*TF;0.M<OWarnS#Dt(Hmnf_c[Laj"+23<J%<Ku=%229=,'rnt(-aRf=g&.U+)X;T^FamJb.lo5aT0MMjDtctZ]r'94AOJLM11MrnMG$=VlHi>ZDf80[I5OT^;jHt',jb$--s[5)boW5:85#.$%^PqT0>+OGr+cjE9@Zad'-j8`.TCMHt(b2Mj$dH6sB`98P#He5Yk8ffh]f#mig[Sj*pP#47WICemBfoj<n1,E!n:TLl5!VJ<;#;lfS$BS^U(YbW$KU=6fVGDLcoT4PtDpkpl1.fV5]I$n.]E6h\/eOsn&5:1Iui5<hf"D[+jk)WuHZ7O&+75[5cSBqKZ$ir5aI2oY!mgdb`MS`ODB-fpgerbH5aA>pL0nI,5k\h]M%SgaOk-)&AImuf[+*c^n+\k/e;bI7R9V\lU>&h[=j=l>kGXO0U<=K85u%^?R'\W:EKrUAo:"N0+gj@_.:(CV19.]K^kM.&5bO<$p!A>Q&R=BpU;d6WsI=ggCQaHCD/m4(fYhKPpo>#BsR<OHqUGcl?rOl.DE&W:A<02,Ye<MLS\W,f1=3ICB]>+_U,%PN?e0(Z"HJ'ICj?m0c:LETfm\M`CYY]'[Ni>\6(&hLI*@`fd@9L=1:Ub7*hKBRCh#rsQiMS1j(n?(%N6,?.0G`40>3a3"6I)P-.D5JkC=[4W4#>JH^5)<M47nJ(/lSM0nKcOCn!EufUpok7\;nRL9EN*+Q1>KGa76<0S\1=?Q\Al229uKU$":+i=8cu`0Bp,q!Id,P*Q*khOIf4ci2quLUGNp<Wm:q/4<NSr<O1P12K2Hb#DVtn=ZCI$G"[4RT&&R/kiQ/^L2k9l<qODlq0C`BfUJ$KP/Y'f2ke1fd0fk'a$!44Z*2P#%cUjM(Gt\sMP8'c;9Q4K8BUgI%P7T8<93>&]GRb%trTSFUc=8Eq=\+/&j]%+f.*[<8ERmt"#E-RWo'MP/bKBVoV]Dt?7hYT'2Y_s'0eQFle*IYUitUN;'T:6_mM68UjpV;Ur-=F,.8JoZKjN`J5N_]i%^=f/Pk/N`Qf&VoMkq2%qg:U2R/SHCd`m8GEq>Y,Rmf;k0nqH<6c&h5#Q<_%I%56i],1`J>Hp]V9]hoe1nR7W&/:m^pGWM%hdPU6O5W9Hd8@#HR"?<NB\cW;3[EBZ2+%I;F%k8m6*^pLd1m7*+hm(EiuCWj<"gl=1PcM12\[])72L?NTo5.le"QbH]OVqoEXh[M\XLtt(L+o&`I;*51R>!%Of>9,'N"s:mTdZk:7hi3Z69&X@U?qI`)(e[3`)9FWKJ^"kGYmd'<rj-W2[oPl_JrZVDUWN;Upe4.J*mX""Lb.90_3M<QL:VQhXu?:km.b]Y3B^^U^"2&"g2`1W1Hs9tJUu)TR8p`"Z5Q6\NTd'kq(>@l;JJk6jO'$h'_g[4'$F\:=O?`[gtCHTg,WMB,!<66u+?K'!PS)!Ti_Va66.Q$3f!rqMH0?O?JKbm0%<K_YLu)R&QRo#./fQq6^u)*J076Y+.ADs,.ZW-2)\'#L1p"^*^Q9DOU(*/G2XGpl>E1"js#-<a/b^i0;[-AM_9C!,oGh[(g-]^>f!\Gmg..u?<>ZeXMpCK^EoH5ZK>?XOS1p*8OI:r"I;"p(ZDE-&Q%0P8C&`uFP1o`T_6$[-6GppflR#dq5jG-$F:1Ct6(*rc)%c)JLc3T.`t5%+*(lMI%!M>>E2d"*(A?m,TlQ<jG"r;5N6A?2#6-rBr8I<pMgjf-JK]0fW9VZ>j^K3\@\55JDGoW.Ut(5%IWdXY,)""*J/"$VtZR'J4Opf'.Z>-9^?;qZ[sPm7.b2IMQI1uM<hTW1aN$c;-+6XfVr2VaS>0su1.Y&!aF6Ot2T-u^;<:2ti$]'=aF(M(BuJg$+)rB@:REQU8\3r!JV9gs>'&7;5Tg"IZZc>-()R;gqT+f*VG/+>[@4_KBT']TK9-`(c<gN2,GW`urPipR#o+,!F4c&d%tBs1:qZF93U&t^?gRH<d3Uq4(n(sR*TFP-hGYY/Pfpu,>E!ad\ToqGk0BD/!p#7W1q,Bc2$Q\nXoMWg+Ppp(uT.uJb0A>e<q]+NilaL1(U@ufX8k:d"?T,FSBVs7SBXr'O3dP2ZT)3X;TMlj(:Z]V_kO3U9PA\_t9X`]o<+f5UB0<kA$h";_&^nU13CL8O4JQ#5e,#\AggU)iA#tMo`n7l"0'DR'70^gAPll$P5!+!rA*Q!Mj?(qgJ8L\gBL>:hf'&t4>a_E4]=7"$oh&"7+%;h]d]ITP$EAeT;%Ub$DO@RcoRF^gGV_NDh7>q'7WpU0Z-#DhV2RJBg[u>+pFXi2<XkS34T?f5Ed?,IZ?!l*s&<Iq'k2bip`Uq(GApPJSE`f5$mEkoXV@VVU9bj==Z;T1),EclH`goU7_%W`I*C>aY]n3&221DS\qF=a^D&o`^+)M&;lU0<\E=l':`H?h&IeblLD8hV.6]!EZmu/<Z3InIqm2AJfj(*Oal?P-Vi\X)ChQ*0((8^;?q,>A?fD_>%oMG@b(jo<VXSeXrfIm8&8;4^naf11P2(run\5"!XX'#>'/#<`:N5aeZk-HR/kUp>7n<X23e)q"#meci18Wr#(Eo#[Z$Cm5:&PcL->SP:90&.1;lM14;4b/'CQ7<78jakW4YN<f(*/4bt+c^[J<A;&6`onVkbSeX$7jO(^\>"Mn#E8UTUQ_8:dk/G5g,X[`3sQV.)JW_==fmTT(rQa@(^KlQ!.#5eU\::^S-7?jQL:dkP<9JI.k/@D!J6V:ag,Uj.g6nR2V/#1Jcr3T2Ao\PW[l+prD5flPbm*0*/Y$IOj20;PC97J53T83N3a"hd>sOU#9DMqTgCgT.b(4[KrT;s(J6WnQ5<NRQ5!83^D<KpqS'ib<*"_KQDHnEp03\_-Wd&Q,."`.J$buCF0sEBA7&+@RgJ7n:MDBOs1o)`9DoU4d*tN,a:)I1AeQW["K(cI&k=m'BT=&).Fl6;`Es1`M>C;E(>R4T?H<c6ML"3X!p'Fah2(l4-)Lbi)r[:fr,B<u)7!Vj.dm,[bfS+^?Nmn#"";rLWRn-4-^]e124dg0T9/-c$DtaaBcKp2@(lJ9J2A@`SiY0CgS4>ogL1H8r!rKdJW_)E!?QXjgN6GV`qc;.>7X>m+Bj5\?QRqifEXf0h!\B7j*QaQWP:OlE;_N+BU#p3Dr>n\=-\9sZ,h:m;sPZC-[u"E_uNB7HU(4im;\]2"**SjE^aGgTm+Asq#r<H^8U"gib$d6<U^8->np1<_na[6?SaU?LqtWF`?kB>SE'qlRE@#[bGaH__R<LXi[BuWB^PO:D+B`u7lK,*WE=oc,EDkI$*LhNob6VJ]64;%,`IAYIlq\](*#8!rq/N9BaJ^S]G%,oRDUJc4?(QhZ&&b!&Cn`ACph%OGC$@khY[n.8Q.h~>endstream Gau0F=``=W&q801ka/EW-T8s@E,$JbfTTjs)*=#D9iWg%KHqlY_#[+L]=%Ze)a#C'i[JM58JDM>GGEPNIpW9baS8W<J))]la#QhB=$TVk^&g`iFI:t%mJPetn!'QEDFgXAa?!G3/K("Pj*TF;0.K%dWarnm$]6LLmnf_c[Laj"+23K9)s4#%)CCQY7eGsck!(W7=g'WK4ktM/I[g?aP^g_LP6$t$aep%2@juUNGatju,Q/[)j%7f9:K1JZAQ)GI[E"L:O7@tpKSG^aa\+D)7al[9PA'<d6f@UYVpEUWhFp!^gj0&21orVq0d0+,+'J<QioH7*d>#Hr/>Op*TI21Dm&.hu#Dfn9\=^FNQFoPsnQ-#ZqoK%+hL\BEiaU@^?t>)h\p:l:BCq#f1QD\/NI$&b*M?[o:NtY_\dWs&2X%4e",^a@O?INF,`8cPP?;#10/!sulN+Gn`p'(_dYL1'&=88Cg1T9*\3F]k9ecDrGL9KA-Vg"4k+r*tQrs5W.Ds6]93][WWaQs_\ne=H74IGadN]D<HCXf&9B,"eip6nmP>7#Ok6%6Q(iNLs/m5;Yi#TA3d9dPhW2<RTYn2)%+49TSiTlU3L;/qnQX7cl(buM";J"Ys_eAH\4-F3t8k0,kc4%C]K-ja,Cj0u_-/192<LBYslVR2\U'Oi$C?]F%>kPHJ_*c?i(=R_>R(CkK7Q[,;j_2bVp>gS-5"XEI?2tF?k"uZgRbl$!6La1SA$S:>/&i`?i>N?Gf//=1Qu=CkPX"u2O8Khc?2TE]dIDMqE`fUZgM.G]>G3ErJs1lX(milt^)GoC/r+7MARWdaH;Q`ebf/duf]@RH`##XMPE]"uZDEU41u8dk=9\%O?CYM1WYU6rGB$1$cH?r?ctGYH0e[Y_0iC6P!C)@f!:O]lV!Yg%HF@g)m2W@b!5A6oc"2EpG*(C#2*d_4<[PFK?%'K(W4E0ln-)KgS<<:U^<GaN_VH<l^qITDSGo)US9jBPnR!3098^,/SAeOm]klCW3emD9=00smV+2KN;4+A/9GOFg'VcpII$Z*8cL^C"L_]e5/1F=VLP4Xc<"CtI!YJq!cU&6IYK'ND>%nIGMR/'&4daRa(?\')0.WqrS9DE>Sk.ddg#*O8`jeiN::!GIN)P]4:b4Yo\/R7YRkk?JnE36KVDsP[(4(\prg;OA3P]sY;,3`H?%Q9[/='J0mIU9i@.KpL8D!lY8^h0IC_ktH`Yci8%LE5r*V>Q1p2SAHls^eoDo<]J1RWT5d!Q`L=\t(.h;9p1?&+QS5DK'Z77Xah@1&TVU30D6k`B=ZeI2m4bqWeQ#0AW"7or*GL%q)]N,p7E;,+R=bTJg^hb^-D'OC4)L+Z@N:5A[inWIVYau.Z^jD%Gn>\k92(]/?sbm:)58#E"%Ysf+<\T84H4+/Hkab'"tLj9<i'1;.Yks/s_VFm_%Stb4.9uN@QTr';4XHW>YRaqK+8l[t$VU-QT%B.oS0E#k!<?YYb>iLOD5T)1%n`GuZ'^('24ab@lcObkj2sEA5CE3(_&lVm:$kXt-<MY2>O4>qtS]q2//A-`>e=me`-j0)PBtV;Y\C\&=Wn7r+-q#8_+_^:?L6:>`E#Q*>=a:N$mZ0$rrJbcggjFc76&=D_W:B.:+_CS.M2;A=R*fF50LV4cn91h5JSI.*+X,Vi;ik"s\Wh#haFP0?R"qs]*g81XB;3@J%N$eu_;p]n/57!Gi&b@9Pa=XC!r&;jAgL;eE0`T`E%Q-\9.pBslO/XqdXEH(&.81Xg`QT>%W3oN!lbWdl?FP&Vk1]mBYcS9&g(3S)PL3``/`C;I0tS#^r)e:ZT/bf2?u@3fa#aX=FK(I3\e8B(HO7&(+_l3fWqRXJU`0^PAJdt%0c%Se'IIoUcb!SH+VBpp_:LpCQPU3b6L8TqgOeOc!iB3<uUS-Cr#+;Bu$XP!E40<!M@L=9Zr\*Hn$*)WR?<Hap-(pPm2WY)cYA0B0dBJHROjPhM/Wt3MtA&bB;(dFm,O:%0[50bUG0+F]Slgq2.jj751O"V;Go35>E*:=9j=%W2Z`f$0$l-/mEZJb^"%s0GBJ9.>3q3E<VrS\EBr5JRUb#ihR9+VE8\-:j)(^:Ah5Eq1J19Xr3D*3pkH(?.O0>7cI@B,L`/'MGtDd)l$P2RkD=G%sFqr&O.7M;rAUl^0WXtilphuoUoX3CrD>=n5;!@:_EX\&7>,5D+1^*OR2V/Cc?t#*k@TlAB]U0r=ik:oeQ\Mm8VmL`YDuS<]WY/2a_WABX*g;EAI)co[+OjWXK`(VR<St2T/RGoECQ\J@jYO%9Si8F6tG'3H0ji6q"'4D4=R'/)c:d"^]GGFoq\>&E`qeqd&4j.6n^A;29+a6^O_R>Z-G"NZiUk^Voo'CU+jNbnW;q;"qrgXG/WoC>P6$^,RHt=9gOt6-,g8d8]H+NRO7VqkD(.GuaTlTG"^*;B+=Y]j&K6qDo?ZUt4o8gs9RLQfpu$L9<1E?jOjIH1UCWfD4Lflt18YM/l^r471DqGH)23nkBjlDR*0i/gHNqY/VJV=gb$(NmV%j[i2#k[&):'dAlle((e2DKXDX5&Z&W82[Jjgk;d6,L7B>XFRaWho^L'*UqLKo3*7(hj4dGWUIc)r.>%.tj]&T8<PQkP6C,=YmsLr.b%J]OK<f.OpIrA?$&Cntj:tEb?%f2&OgDrJgA"sml??M_Fa.H@kBAeU>sUfd&`\PZ(6Fru7$r'\e5o5)"TO_P%c:4CN3nF\b3J/a8=jh[Y'<jbf8&\n"E_"M;6V\aA0uRR5GmEo9TTU$X<XZqfpt#j(MTbl?nOSdBhZK_"Z/DNlt74Wm<"t=&MDVZ!Nb!8^l[)C'<B5i?!P5h:)M#2a:AL%M\mD@pN5LFTUn)a=3W_J,I+eB1;"/@BM"I?S*$qQ^qO<"/YFRad$e^5TiJ&&Elcf*,m6M:W_LXMLu99*m-HU_#E%l]*p^U$CHia95`*5,TcFgs[!eW74Xa:.C1h[_F@0hh6'Z:Y0pVHJ!K+#J3h/HH<L@W*)ROFt-(/mr2<l;>CKpa-48LBr48hkHmDQ5sE5)"0`\RQ9]PtO5?U5&oD=iD'!*3bkOhsk$3_L];FAmtW\5=>H*DB<-'g\=fJ(88LrN+t3/?pdp-XJ9!*[&ghR,:nQg-`.Xc5FD.a%MsIqh;-2Sfc2sh)tI4?u%ZN_Msn.SU,a:WE<Ec=U+[.8UddL6<oksnH90LlRrE4ZN9Elj8?PA9!>o[VXVguJH$92km8HiaX=;aQh>$^m0g/kTQCL7jnJT4U"taB7g-bYbUONl0tO:^me#Q4Vu38df$_9`kJ^_,rftMJZN3b%O/O)poreo/,g#HT/HiK%Fo!/tV5qccM$8Xj$8:QYYXJ;]L7a?m=d(e\,><2Z5[b*(8%X5pfVftuZXaf;kkfFa"'f@dDZD>`N^F0.>4IKF8@G;$hh/9V8N%RACqliVm5p7h#1+[+k3g5bW%7;i%60P*p:C7n_!+I1g][H)"6$%%]Tr%_PR_:8VsO!035<H[,4Iou(Or[Nmb6Rg[/tUI5?<J>P5~>endstream
endobj endobj
84 0 obj 84 0 obj
<< <<
@@ -484,17 +484,17 @@ Gb"/)CNH71(B'h3EBh$I3Z.C1VKT#g8NQ7l`(<&9c>JoJ;T-X7'+>"_\ij9phtM^3#0]FYajeg@=1T'_
endobj endobj
87 0 obj 87 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2854 /Filter [ /ASCII85Decode /FlateDecode ] /Length 2847
>> >>
stream stream
Gb!#^=``=W&q9SYkXT:*joO<p:JN*04<#Xh8XUr-V'$D6Qr'R_+FWU[hZcMIUb#iXGAGBe-k':-&<6\'DHI,QJ+WYWIfo1h%dLG33e@)>FAjo&rY['SZ[^9m6IN4TgU/jO*61o5__^h0\;XeE.p]^ro*l6T^T:b*r-=^6C;<niXqLt&Z>GggDciODKD?%Fs!#_cKg0o_[Yk?3$Z*__=].)C(mY2[iRs[[K&TnN1NcZE(<`m!(Ad5D6mjltl3;23b?n)Dr$c4([3IG!Y0_j>cgk;/f;mT2:JgBr]m:^$-A((bdVHG-HB5hB*kdQB=#j4Dc#\\kPG\t;O['-MB<d_57PqH,aQ0<WGZ)BR:!,ArGsVq6fU@5OikK2lYli8DDp-VV&IE&m^HoPNE0`(68WZ@L[PV8#LhVqWZYj5<rLYVNe;Qna@\*"Q`Jh901;ItO"u0\Y,X$Nl8";4i^JsFaC.$tS^,2q.oJ]cUC/7Vt_H/4H".k00($YHE:OXu^4VCcVgc=/^8)XQ^J(G:7CQ4>IH@d6Frb+sL_kU47lDkLi"cIg.]M2Dj]df6rro<N7'#XC<1J3F]NUm#NK#HhDdU&$98-`jIjJ;W;\bc*rW()V>UNIbYFe,6iU!i=/W']m-`U_$g'!K1IWBUealVbA_Yli6:%U3[c5ZtD9rE^os%_#Z.?BfW&Z3tj/aBW;ul)GEr$o"3VJ_tme]=bkim14WXl5ADcbt=-)W.6.!1Cfd-Eq^^8&?l.qC!)$u8@X=$T`I8Rc=rf+NZ\SWm"QmsP_\nkqu._[0lg/VOS[peSG?s/Thl`@,(n3]j1Z$QUuTj1'%99+TTF>g\"efVFiua=EG,2[)_qGg`17:o=Y*qoY&HNtiX6$%5KX%laYO[S`7u^qB6#ehGki`dq/N:A`%`ha%R6k@S1,49<jZ\LV[`2OJm#g`eJGU4IqppXRRi>\8aGY7pfK*kGH3CUA%@8WXgWgWjT<l9g&aI'$VnSABC@SgY1`q@PmbjpCG%P/XrJe,g[2V[<'?JNrcO;K'd^<u95tq75MF-]Lo$2"i:'k,<acd.Rf(sgG.2`T^*G.*]mm>LZ_5/]GDAga9JdZ)f2U=S#9&:^Sm*=b*cBHEo2H""mgJhp0`DX[9IO@uW&0F]0S2Uk63/W%/V2>r?>>$(`AH:UK]Qq0qUjZD:gh<@N/-Y=Ds(T@p>/JMj.^8$FPV(7T^.WkX4PouBJ5>DG)2[:%dDg::iC^>%\X8i-=Di[dN?9n7Ot'B08fYrk/+An6T"BpP"D>;TSamTDS-:/'ZiS7B38*;7aO\lY[E\OY)72KNK7amUq&e(HbA'H;8slDhI_ZooT;pn1!i[iGk=/DVUtd<RsJiRC1A[\O[/r"JrN#fq+C("RN0m\SN)KRIoQ08M7@dRWjPEa&_(?.4IMM'MsGPYjpas4(4(;Z`^p*daU(I]Ue#]=9_na5eK9nJ-Q%po04acdf'j6=P!DPN)*T[E+m#,RjEGe\\jFk9\"B:V'j=fN(_r!1rImBWNK95;/@-a>T&\^>:pK*R1:sbeXD)<TUDqJhK9"#d[9qFaS;sDe:DR:iW5*8\]"@bYBWqeQpiZb-OF3E%+W8G++T9qa^Q.8f9k(,:!uYu&fYVTCM@fgRPgnAb;R8Y\c"[9A]s`Ge'd[].@Um`>8V7P;4Wi77-LpNdWeK',>h*pj"cJ5b9$p>Q;!^NAFoQZ][Gm,kHc:"JkpMJKe*Y\@S@'O^JkTuC8U5X[0q#pB+V&miihu%bZIK:nn69?%^GHs*E=LB7pYKj>J[#X4<8d(/dU'Kj,G^_mirGfm'6_mLP&VCBTD*MU.&Ylc.^r0@PlJKR6Yl==pp5A6!sV>l'n9aeRRN"3LQb)HoVg`75/F_mB8B[&)DZOYDh2T!qkg#=>o$;G)XR'?'*X/:/$,=7\;Z.gc$8VX<4.ncWTthiYXS?DU%V\7o2QQZZ@tP"Z69,_5ha[0T[*]-c[DC+E29bXYi+_IHT2GgNDYTsE6Mj::RPs4XN&2u,6'/Y[<lLML@$B)iajVa)&>+5Ktq@a]k&W'ICW^Z_TD\!^K$43r$73L$S\3E<9rcT\3(!V]P`ks?U![m`tReDeh#"b`Oe\d'$h_/CLFo.WRafZ$plFpZ!_>r0P=\I8;XDNhr!9]+0t1F#hZYX*BJ7%%\(_dmFQsNOC#C&P4Z;[rX?GV3N>(/l&c$(*$nCl5_1I-;SL8X]?Nh6ro1NXkf'Wlb4L%+djGZ%SF?uI_LJnM*!\-)Nf#bP?12Ft2!Y.];a=s'_,fcZd&[1M>29pATaGMB]O`OI_/G!>NLBEH1`LmZ<-9-W(c/rSF6(!Gf^d'?Md^gDaBj)Fdb=A:K:q'1JpRGc`uAHXhMlYu!9MlSh&!>PU(a^N$jk<2J[(#>@saN<EpbCSA;`-crh+Jj`U@YYpohj&Z0]FJer%9;]&UTh0`D@a\Z+q)kqY^$kB#4Fes1[>ZYD4eAH,"DdU\Qn+p%[(&X]e\B/re&YQ&tHf^Z^3r[U*Ac`&06heiW8FNi>@HkWEZ\0o7:/]*%3&+We"j@nfF\Fg\"I%&CM@#=eN?@F'..7tL06S=DJfb]\6$Lt3@Jtj8k,qq5_*<i;kb-0shmop4iL7!is)*@)jjYIXiYCO`W$W<(AO?F)XXIHUO:U5?fA>YQkO1,d6'C=Ke5$]e)."GZ_E]_&oc$8VK\bU8/]rHIiSs1X3*=<Hq5?MhVaTUAhjCs.^SfqHP=$td/0[U/l@pSsqRnWjrq(S6(?XRk$MtDtNl1+`JG.5N-PK+gamh5FS?fg,9/+2ZP?0,#AB^G/[;iY#-mtB^mG&;G_+SPqIO.D3L0$3ZNI=Ij[\FeElVSUr8!,G9rir~>endstream Gb!#^gN)%.&q0LUoV43PP6?-\b`e73'@4[T7oJKk<@N`W,UO5)+\5u6laK0B'IiW'HagF_Nm+0L(SK1=1ggA'IRs19]*3Go!;2c\Ts.W"0ba^*T4,;jqr`q!P(or<Ni<n(gU<j@'*I.0@=43%\d3l+(+>G$X4K1T?=flM`+q=[<.U*X(IZSR*[^>`nH0QA82GbSk?uGQci>f:U6CH\7<>DFlCd>S9=#X8Cm.U&Jr7l@q3k`bNt[3[C;D4D+m-$*"a.s`+et5J)'r3p_!uX=(3(gB@<;sI-e8T]*PL'7>AH"MUXsI2p$1ZGrhB@oH,t#^Jt8_LI??cU:;=MMas&(qfgB?APStlH_>-gB\#jpBFfJQ4Fctb!=ci2!c.0;bDWln0C'+m-WkitlgdYTgE7hF+1E`P\-B+s]-$+S,cr8CQ\.kD\:3ak?<_I*mZc>Nij4btBT!npm-VeH&R-^k@meNP9XSq3=<F'<7:J)>)<>Kn#jEiKmanK'\rSVSA&6S2R3S^53r^5r(\L?V;ra!PY39Jj2\=V^3]RL+mbqBg?9n:A"<t0BuGj*/n]0qqSW'Vr2UcDn?6F`V17VU>V;8/ZS(V+N"[9)TJdQuT&M5ibD=FGRE<.i8'_'TeO\k18?:t]_6<2"KR@SXq_'!D3&WBUftF2.*`pIMWW@@<Q%:fdMbYZ?VerS<J](M,e]A=*5"QHRE#B'W2ce3P1i=mnQ1?!cFEp4kh<opfm(O@<"H9L+hYGRTrpEqLQ4_;h-UZ@mRO&UssM`&r9TEQl9fF9G0pZ(BaVfOl;`pBO9Z(e]!=P2d!3G;jom+Q%?f#+sqmI32Eoak0Fe+^5-QMJMC(CJ<6]4!(^2=2p5C4r7$2`s[^P:r(Xhj1$1b(Z:'dOEr==AL\CNGt@"`bJe;enk]2ZEMb_F2G=1mhm5Pm'GDN\Y-pK4$2GFR4<)_K"'Y*,[>J?\Ud^F,HIX0d/;K>6<JU(K(d]9F)8W4[!4c9E!2#,L]B4ZmE7q0-0b-+'HW::>W>Ilm<>iTMr;]8)S?9E1<'D#$rcOGO,u;"KV:[]J[W\tWYnWW7*,c3i.a0*2amVBZGNsP899^S!q^;&HmZk<Clu?`nM8k/'L<5nR^]NE:EXRO>qn(p65NMtEqiZFF4FZd*/ekQN<gnc"+$_FY^&Xbl(/JZ#09neF`R*?e_K8='5.704'ps&$NW%qK\",7XE@T6*FYfK;bMaK=":fAEG=`Q1U$'cM]mAol4VI#IOa;?aB*_eAFTr4)OmW3u2d5`&I8SdTG9"%K)PXb],FJZ]2l[_3j2RQR8<Ng+G_QY@)[).6"\[-M*im.bGN5GUUh?`r)oTA/X.A?`9G6Sq\dWs&q/dDT*T-b=B@m*R)l0ZMHGNG7,D#h#3?*O&`G<$D8T\3I$+1LBIT5L/M7@dRWjPD6&QE9mgmP%%M%cpco9La4$U$Rh@jM*CjFkh2q*qq/@a]V5WpYDU5J_kDGp0L9ZR5C..1(YQOlGEl)3QroP5O`l6<@i2FSkko>,/LuJFeE@Wk\$.V"$aXL/f,pomQbM$n@L3?=juhM@Jk[CimSj@F,c-XnI[!Q-k^>qT6Xu,nmd>=*HU<"7rsM#iBMB'p9)K+XBGN'r2pul.7XpX\MPID'6_J"^;g]71nO/Q'0i+8m5`Vp43l]2+h[*(bqb/Q'+lPPdJGcWE#YDF`uTc<HG?J\Z0)-$5O;gQ(f.^Tieijlh^'ACWRG%p57rsdSCXoVq\6_)/DZ'"I[oPPHsiUA%I2;8o=>__f*GaVs?4_i*0Xj^GIL934`P+H]Jh;#`N>^L&t_KP4l2X+!Af2(4$Pe>JHqF(5u2$[#3Z\b%W^S$.g#a8P``5s0'Yn*-0P*,B0::X:I9])QicZ?a2FI<]urm)P`?3DV5\NRsg$AL[-+]dGlfTc#DMRe>:>48CrKB*cE7c0Q2Zq1C9DaYbW/?*K-pu:d9(9V(q<J^A"T)QH4H=HG42=d#9N3rrnLUB]-..!VbbpS8iMDZ2%foj0A;s(q+0L/XAC*KM/jf%'HN`CF;"erEfS?mQrFqlDC>DLmP6"U`0RO\\Ml@^#_iP]5kSQM3I]&IOc9p\5=.k(LT?gWTMYC`,Zr1n@CueJ,)@f/rm.Tf^L^Z0&E3W,4QWS[Q=DBWRafZFJ>*'A&6_q@)9IdOTJZ7cJQH/+0mf]3nFqiBY7gB]8pf^c(c6H=+]Lp?FLE-m(&nj%(pFk<#iHMT`>Nlace>R+:X*jD`[m3ZgkJ)9k=+6]]kWL-]I%:$iGMGM[ThD@"F3E/D2#gk#pC=N2M3MI#G,R)kg@5?+%6sqEbqFCiRWr*!X.iT%2,^#G]uA>GJLY#lKbd2OPf9]CR=uh!bmKQ\PE\[k4^f?T/7RD@e)l(EIFIFefuB./l-03k?tZkR+#IFtf_FPQG>,_nu*u:#rj_E2+Gjh"_':\;N"WP)6#;V9Pn(`>[<b_"YdUiNjX*"GGPuG-PkK+T$4ihOW4&qi4[5q'>hZ;NXB%X;2+]2j(n/>s?F6Q1k'(Y`Bh)/*"C]:%k)J,4b,5J3hA+D88)=_Xi+Pgt0F;3;Sa8PmUdPr<nr56\eHq`>u%AhoNto,WC!?K9[O:Aifh+;6&!"fhQLBY*B&ue(%K,`e3Vm?N.XW!,mr4fqks;VCal.62^;`4E<^po&0,[!L>UL'';cORmCXE^%ut8<OUc@/`F"c.LJ<52(uH;Y"]<b-XH[^IkH/[Sm@UF>Yb`l0TUpeGrSp0&'4t/`R3-7LE/`Wjf9Au)T?_j[.!VNm-gDSe+mDRo/g*NB2"m<hk0B2CCG4T3$dO$rRK@Wqhf08F]\F(2&c?RkTn/!(uU&ofsR)`4#m*J:hSJH~>endstream
endobj endobj
88 0 obj 88 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2061 /Filter [ /ASCII85Decode /FlateDecode ] /Length 2091
>> >>
stream stream
Gau0DD0+E#&H;*)YsZtpUunI7]n#<k1,pG^1JU8eb]K_h*[)#T/L3b4*8Dh>]mGnB+X7)\<\g>smHrZqZb'iW.c*#XG3/c@"&RiPTF;#<TK:J/E"/ASak%2p;<e??Rtdq1UY-oIB'Hf8Vf<kco4#Xg_&TC'J$0NG:!$4+mUusX^?\QX%em:p`))*&/J94"8Q6F8U+VcXLo/in[^o(3PUq1/mTtJ3O!3Lm2r[R@@V/+MiY`!C\?PmT#jlj`5a%(3E>uE:ge)5a0;XZ#n_RiT^9R:]DT!ng1q2M4O1TRGT<(`?G(?Hd*frjZno5jr\;'9"N^M2lP_+mn5SZPh6W^grb)G(]4G3<lfjS^')D]38HZ!U'hY06_KTsXip0ne*1&%<dI`5\sIFf@Pf*.3V+Gc]fWBWpA%r48&IZ9T\_HT]`P0fgb8*]7YF?dL_F&)NsnN*,FVQro%%BC)5nmr4.[-JC5DZ07a4JV&bND3!?m#2UAYQesc38$l2>0Sq@Tn0#gP!F?\\_N5ffoDC`Fo<VTn[=('rF'R;lpYQ&VL."@GrG8X\Ll5_EF6$?-,C<c_=HP3["X$2;r(J#=n;.Ym;i.[?]o@Dmaq4u]Cm@UL0;UXd5,)'*2cHjAH;M?ge_T=b(KmB:Q$`+^'7cWB4\;&/X*-5AsL96Bb2L634]U5mBg9,n3$Ko]1R\*j^X)K\EFX-&r(=uT"m9$ZZLcb''i:#8kU!F:N.jL4S441&AB*<4<E(pr\A"BN3saf2Y.!p/1;E,%lY0Rj.`L2)NW!b`AA)r>*peF%MpA9Fd-a5js4?o.kXD66'4p.p*A@T\p][$(nmh'Q,ecbXpGG1Uf@2PZ.+CW)<Y9r/HTeE/V!+>TaaB/0Te"4&"FcP)r]r[ob]lpV=.1SCmFII:@9]]h]?4P,b2pb0uc\kF,/HtW'`_k6Te8p=eUfM4LgOM&M`%DR6e6gAqu[!WZ!1kM(_4q#;4a/cM8+_c\egQmWEj=&&BHia3d5He9W+-Bgom)]8'PXr,fnubU7'gO,2+ljW9m&`=)UCijo'E.r"uC4^G<`fHe(/T85[1?:`n\gT='SD>fO&^hp9s*+"3T0UI7SQZ2d^n8TYT%j"'o!]pf&$?kb?^2b_*s0T"<%&_u5.'(drS5rsl(?3eGA]1iJNRHFXLQ^8/N22oAW^!t0lX,,+%<a^8Z0@')g0!OX`5J,A$V[:&%V1,j'N;7B"(1Q*6E/M0ItcH#*%UL\V@a?4hLX.jLXjSaT_mb18.B;CCLn!Y#GB$,&=^D0gY;EBG(P"A,fM:mrkm&:_c6>p"+sLGX4/\S\=oq@ap="3fpaNGcS>E&Yej9n]ZIV[+?\o@lDY&f;D!%?>SA*FLmX'VBKX?6qf6h0-J1VM$.>WB\k5pB>>lAdb>f*SX<;GkisOR`/7<CB(ch<nfW`pVVX,GdOW@q\:CmqUgUF)./j!(Z^#q^OSNNGXc>g20kY^cgF'=^-06[CA^)n(AhnB?9NlJB=g@OA2Lu6:LLP[.n-cb2uK.:iFE';D__qH"'5n*J0K;u:#CNG:<&*_'(l^&D-'6dNQ#&$^]k?O`>R:^+t1s8i;9csmt=ZVhqkMn=MiN?:9;.`-8I?1;Qa\-.nmu.CMq[O7%Yg]<@Rk6t\I=1SP;^BJYT;@g?Z:?9k>YLbX;i7E1JX$57mDo]5l9M4$@]JRs3%s88!:u+[9U$L5RKnYDH#Xp2N*SjV49dX<=ZHq/Z/<-X\AqLK'P:MFZ[nP3gc1EJAiBe/FWKOWNs*3`n"TclfK.\@(DKH+,1R$fV+PNR!I^bbY$I1K&WhG'Sc*Fd?BE+t':Q7;q/mM$PQUUoGSD#;Y`B%YfCSo!Ha:oj4aq9E8<YoQK0o+;p[/1Un$c<*Qu>6J,7jXmKURCoDS9(0F?,67CbXE#-?->\^entNs&Kf#2f]Qk8gbI^;C>%-lTll^F'2YoRO2`N(+Utu>/MU:,<5P0o>,T^Y0XpUr^e<<-kt#N4-`,=3Kn*igL);25?iq6-#*h`&GY,"UQ_62&blAtjN9e<cNPpkd[b$A9b1D)-YFT=i8e6(BO=4~>endstream Gau0DHZRn1&HCX[@Y^gjS;*5LH@Y4(1-!+T1JU8eb]K_h*a'I*>=aWH34MPoHh[.4ECL-p/Js,IHi(DQmI!OJ(#N3khjjk>%7*#+5bg$'J7]>pL0)_B*dp3"E_aLt<m+GOBJu>JeSPE?+W2)2.p8;F_`:OC(+\Q)]q2$!(#g[l[2+=B\4F8?$DVnY`cENtmb-gb"GXB>Hmpn$`qMBn(+&*dX[j"<2-#$2V1,d0oaXflces6fncVirauP=n9Qa)Wl>fJ5Ga]<:eTFc/delS4G6q"uL7!t6R(3OmR1hAIAdj?#C&.3/3Q[54?l==h2Y;Ts]n`5%mH:[dQqc!O-#D@X@cs\/Beob#Sjbn(AVm#m@D`i0B1Wa+[\?;VU&fB:`JH_=?;kI[nWOil?F_0?VPk(aX]=F?..D]Tkp'_:[YX+Q0MYa>I.;o@;>jdE#[\B!IXj,,hP\1D<P?\LP1HhAnt#Z9kt_q#Y!#/<k<AN%^+2k%jFR4i=L,*XFUF,n0q5I[)uapt'=D9Kp"aqj=[+3#Cs<WYd&,A.H*W^OC=3j7^56B6'oDS&pK3M>nf;)Re-hT+`d&@<#G[LMe?eQIV0C<;75g3j[Abp[#[b13(!.30iUYqgE]Aht9AMEN?1RRUmIGi`>EWY8mC)dSHgO)8%',K9ib+#83C+J&b5q*^KqZY%e-RQnSlSptHj)>UAlZ(@ROV1>]2cbk[#(2,*8H=p]5<odHlp)p0@OJ)fGKb_cFS$t_Xs,i$%2?K8kWt:fQZ%Gftn;GS72;'OWQae&%j<"alnEEDF9^uYMsN*RqLr^A7uT?>YdJSES82D*n0FABGlo#7$9u/M]:PpPtdd71go:+Q__#SjqFj[c*jutmhI(*BU5c%k6_\s"iI$tP"hU%]+p:<-$3N_.2MJCm"673G;o<MD4YIr`*-m4?LVZ+n)Y\d(I@!7i69-qfS_H(DB/B\X\mH"7UCjPXCP;5N7g>+(A8>(gMNA+C9B,B[l%m"=j`_k1.$'?X>B(34u)O@Cu%k$gHU)l(NC[V(EBh@Oq:o_)2FbH<kZuB6KFM.d!lgpJ0i?$%O$]O##iM8K"KXOrDW*n&<ViAGH3IW"/rmE[V_%/[0kd1+'F6N>a#E*#,>%pr&-1cpe;SJ+__?,,1ZrGM@^+S;hopEbJW7?=tk]kmkrTpCY+S^0/=(.f;mU7__F$oDJ'PRD^PjslB\ILh"5H&nYnb.&o>P7DQ'Us%;&?dm('mH,S'!&(>)'\'_<^]3*R8bH-HS\k\VfS#!ZK6ZNqIT3<"8(^DIH-5H_lIU%[]G._q#TP[kiod$ILSS$Bk&Y\]T_Gg3pOfI9jgg9,U,ZqOr-RT0Z6UM19$B/g`=DtCP&a8B^?JYi#03oBN,n:[R.ML__8dSO!R#8SL'1SHUrj8OJ;gqscLXAku'71D)lY\]99[o*I)EX-m5>`[pB@fq)4a3;(A5Y!2=\R^I-Mk(":H\u3u4+T2!F"sZjGc]sMlbr$]3lfJ\2ih]<T$q5YJ_0u7pjU`"!p:V0-Q\3FlHI^%+T=kSI4PL-rc2;!1T1@f$dZk^0fjR`6(!1$6r4Wt@rIC3i#t>"o7H7^A$tg=XdPhdH7??lD,K-=J`sP=2HrG8-FF:WA7'l19T$Gk'<4n;)Amf`KhbsK8Fau.Ye+g^+fRF'jR7I.o'V[!GFr>9+#VEH'5R6fEs>mN4c-$E9=Y$a"]EAU'r!I6<2[Z4X_143,VfACMa^B?4$m.uW7/oS5'WI,MF_tcBZR2oQJ?ksGD3WK>3lZ0q=q:NRH%VcZ,:5+*7K%qjOoIp^Hqn$o0sdl*)R)S<^l.#J6?Tn%[bTJ#3X.h3?A4?kL%s'QGXFD9KurG4]Ga+&Pj;&>_,D-nK.S;LO>jJJ%[5!Eo5p?p5$(hdkEY@^\dJ[Yl9)%Wl9>p>.jE7O:p(6I<FSY]&kM_C_q`#VF85#Jq-oK!r`o;X;`Ge?=a73m4:dpe*8./oXUtRi<Vf>[S87kUi*2cjpVJ:G<S"SbR58tksH)D9gM*uQrKjsnk,GJDSmERo@\^[C/GqkLG83>*hDZ9M'*'i+BLBlEA$_tZ>MhkXCk*"+fCP_E:bf[^j;4~>endstream
endobj endobj
xref xref
0 89 0 89
@@ -582,15 +582,15 @@ xref
0000011129 00000 n 0000011129 00000 n
0000011232 00000 n 0000011232 00000 n
0000012440 00000 n 0000012440 00000 n
0000016058 00000 n 0000016061 00000 n
0000019295 00000 n 0000019298 00000 n
0000022787 00000 n 0000022790 00000 n
0000025731 00000 n 0000025734 00000 n
0000028677 00000 n 0000028673 00000 n
trailer trailer
<< <<
/ID /ID
[<43846ba034f0f13d3c3b73c3a2995bc3><43846ba034f0f13d3c3b73c3a2995bc3>] [<551952d3ad7bea00713487e65aa30a36><551952d3ad7bea00713487e65aa30a36>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 48 0 R /Info 48 0 R
@@ -598,5 +598,5 @@ trailer
/Size 89 /Size 89
>> >>
startxref startxref
30830 30856
%%EOF %%EOF

View File

@@ -0,0 +1,718 @@
# Platform scaling and multi-environment architecture — specification
**Document type:** SPEC
**Source draft:** [docs/drafts/20100612_1_scaling.txt](../drafts/20100612_1_scaling.txt)
**Design review:** [docs/DRs/20100612_1_scaling.md](../DRs/20100612_1_scaling.md)
**PDF:** [20100612_1_scaling.pdf](20100612_1_scaling.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh` (Mermaid → PNG diagrams; requires `npx`)
**Date:** 2026-06-12
**Status:** Planning / brainstorming (pre-implementation)
**Severity:** Strategic (post-alpha evolution)
**Scope:** FE/BE topology, service decomposition, dev/staging/prod clusters, cloud migration path
**Documentation index:** [README.md](../README.md)
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Purpose](#1-purpose)
- [2. Vocabulary](#2-vocabulary)
- [3. Current state (baseline)](#3-current-state-baseline)
- [4. Goals and non-goals](#4-goals-and-non-goals)
- [5. Requirements](#5-requirements)
- [5.1 Must (platform)](#51-must-platform)
- [5.2 Should](#52-should)
- [5.3 Future capability slots (normative intent)](#53-future-capability-slots-normative-intent)
- [6. Target architecture](#6-target-architecture)
- [6.1 Environment tiers](#61-environment-tiers)
- [6.2 Service domains](#62-service-domains)
- [6.3 Reference cluster layout (prod)](#63-reference-cluster-layout-prod)
- [6.4 Traffic and control planes](#64-traffic-and-control-planes)
- [7. Service inventory and scaling profile](#7-service-inventory-and-scaling-profile)
- [8. Deployment and promotion pipeline](#8-deployment-and-promotion-pipeline)
- [9. Zero-downtime and load balancing](#9-zero-downtime-and-load-balancing)
- [10. Data and storage](#10-data-and-storage)
- [11. Simulation lab (Alpine HVMs)](#11-simulation-lab-alpine-hvms)
- [12. Capacity planning framework](#12-capacity-planning-framework)
- [12.1 RSSH / remote access connections](#121-rssh-remote-access-connections)
- [12.2 Cast and media paths (future)](#122-cast-and-media-paths-future)
- [12.3 Network bandwidth and egress](#123-network-bandwidth-and-egress)
- [13. Cloud migration (GCP / Azure / other)](#13-cloud-migration-gcp-azure-other)
- [14. Global availability and GEO policy](#14-global-availability-and-geo-policy)
- [14.1 Reach model](#141-reach-model)
- [14.2 Segment isolation](#142-segment-isolation)
- [14.3 Enforcement layers](#143-enforcement-layers)
- [15. RBAC and operator model evolution](#15-rbac-and-operator-model-evolution)
- [16. Commercial and paid services (platform hooks)](#16-commercial-and-paid-services-platform-hooks)
- [17. Observability and SLOs](#17-observability-and-slos)
- [18. Safety constraints (current topology)](#18-safety-constraints-current-topology)
- [19. Phased delivery map](#19-phased-delivery-map)
- [20. Out of scope (this SPEC revision)](#20-out-of-scope-this-spec-revision)
- [21. Related docs](#21-related-docs)
<!-- /toc -->
**Documentation index:** [README.md](../README.md)
---
## 1. Purpose
Define how Android Cast evolves from a **single Alpine BE HVM** hosting all backend services into a **multi-VM, multi-environment platform** that supports:
- Stable **dev → staging → prod** promotion
- **Horizontal scaling** and **zero-downtime** deploys where services need it
- Optional **cloud** footprint (GCP, Azure, or hybrid) without breaking the existing Gentoo FE → Alpine BE production path
- **Network bandwidth** budgets and egress planning alongside CPU/RAM/connection limits
- **Global availability** with **GEO segment isolation** (worldwide reach by default; denylisted regions per content/policy — e.g. no publish in Zambia)
- Room for **future RT/AV**, **VPN**, **payments**, and **third-party streaming** integrations described in the draft
This SPEC is **normative for planning**; individual services retain their own SPEC/DR pairs (URL shortener, remote access, SFU, etc.).
---
## 2. Vocabulary
| Term | Meaning |
|------|---------|
| **FE** | Forward-End — Gentoo bare-metal / XEN host; TLS, public nginx, UDP DNAT |
| **BE** | Back-End — Alpine HVM(s); app services today on `artc0.intra.raptor.org` |
| **Cluster** | Named set of VMs + shared config for one **environment tier** (dev, staging, prod) |
| **Control plane** | HTTPS APIs, consoles, heartbeats, RBAC, orchestration metadata |
| **Media plane** | UDP/TCP cast, SFU/MCU relay, ffmpeg transcode pipelines (future) |
| **Sysop** | System operator (PO today) — owns infra simulation and promotion decisions |
| **PROD** | Production cluster serving end users |
| **RSSH** | Reverse-SSH remote access track ([REMOTE_ACCESS_IMPL](../REMOTE_ACCESS_IMPL.md)) |
| **GEO segment** | Country/region group (ISO 3166-1 alpha-2) used in allow/deny policy — e.g. `ZM` (Zambia) |
| **geo_policy** | Per-resource or catalog rule: where content/API may be **served**, **published**, or **streamed** |
| **Egress budget** | Planned GB/month or Mbps peak per service/link; triggers scale or CDN move |
---
## 3. Current state (baseline)
Today one **BE Alpine VM** co-locates (see [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)):
| Domain | Services | Stack |
|--------|----------|-------|
| **Web / console** | Hub, crashes, tickets, graphs, builder UI | nginx + PHP-FPM |
| **APIs** | Upload, heartbeat, remote access, graphs ingest, short links admin | PHP |
| **Git** | Gitea mirrors (`AndroidCast/*`) | Gitea + MariaDB/SQLite |
| **Database** | `androidcast_crashes`, `url_shortener`, Gitea DB | MariaDB |
| **OTA** | Channel JSON + artifacts | nginx static / PHP |
| **VPN** | WireGuard lab (`wg0`), RSSH bastion | kernel + sshd |
| **URL shortener** | `s.f0xx.org` | nginx + PHP (submodule) |
| **Build** | Docker builder orchestration | Docker on BE |
```text
Internet → router (134.17.26.161) → FE (10.7.0.10) → BE (10.7.16.128) :80
```
**Constraint:** This topology is **production truth** until a phased migration explicitly replaces it ([INFRA.md](../INFRA.md) §1, [bottomline_reminder.txt](../../bottomline_reminder.txt)).
---
## 4. Goals and non-goals
### Goals
| ID | Goal |
|----|------|
| G1 | Split services by **scaling profile** (CPU/RAM vs storage vs RT media) |
| G2 | Three **environment tiers**: dev (unstable), staging (beta), prod (stable) |
| G3 | **Seamless deploy/update** with rollback per service |
| G4 | **Cost-aware** sizing — no oversized DB VMs; burst CPU for build/ffmpeg |
| G5 | **Simulation lab** on Alpine HVMs before cloud spend |
| G6 | Provider **L4/L7 load balancers** ready for stateless tiers |
| G7 | Preserve **dual-remote** git flow and existing public URLs during transition |
| G8 | **Bandwidth visibility** — per-link and per-service throughput metrics before saturation |
| G9 | **Global reach** for public services; **segment isolation** when PO denies regions for specific content |
### Non-goals (this revision)
| ID | Non-goal |
|----|----------|
| NG1 | Alpha release blocker — scaling work is **post-alpha** unless PO reprioritizes |
| NG2 | Immediate full cloud cutover |
| NG3 | Replacing Gentoo FE in phase 1 |
| NG4 | Implementing payments, SFU, or DNS resale in this SPEC — only **platform hooks** |
| NG5 | Full **multi-region active-active** prod on day one — phased via CDN → hybrid → regional PoPs |
| NG6 | Legal counsel for country block lists — platform provides **policy hooks**; PO owns compliance text |
---
## 5. Requirements
### 5.1 Must (platform)
| ID | Requirement |
|----|-------------|
| P1 | Every deployable service has an **owner**, **health endpoint**, and **config outside git** documented |
| P2 | **Prod** changes require promotion from staging (except hotfix path per [GIT_FLOW.md](../GIT_FLOW.md)) |
| P3 | **Stateless** HTTP tiers scale behind a load balancer; **stateful** tiers (DB, object store) have backup/restore runbooks |
| P4 | **FE** remains TLS termination and edge routing; BE/cluster addresses are **internal** until cloud edge replaces FE |
| P5 | **WireGuard UDP** and **RSSH TCP** paths remain explicit in firewall/DNAT docs when split across VMs |
| P6 | Simulation cluster MUST be reproducible from repo scripts (Alpine + package sets) — see [§11](#11-simulation-lab-alpine-hvms) |
| P7 | No single VM hosts **both** primary MariaDB and **unbounded** ffmpeg transcode workers |
| P8 | Every **bandwidth-heavy** service (OTA, object store, SFU, ffmpeg, CDN origin) has documented **Mbps / GB-month budgets** and alert thresholds |
| P9 | **geo_policy** model: default **global serve**; explicit **deny segments** (countries/regions) per content class or tenant — enforceable at edge and BE |
### 5.2 Should
| ID | Requirement |
|----|-------------|
| S1 | Use **managed** MariaDB / object storage in cloud phase when cost ≤ ops time |
| S2 | **Blue/green** or rolling deploy for PHP-FPM pools and Gitea |
| S3 | Central **secrets** store (SOPS, Vault, or cloud SM) — no secrets in git |
| S4 | **Per-environment** DNS: `dev.apps…`, `staging.apps…`, prod unchanged |
| S5 | Connection and CPU **dashboards** before scaling RSSH and SFU |
| S6 | **IaC** fragments in repo (`deploy/`, nginx snippets) for each new VM role |
| S7 | **CDN or edge cache** for OTA/APK and static consoles before scaling origin VM count for bandwidth |
| S8 | **GeoDNS or latency routing** when a second region/PoP is added; single canonical URL preserved |
### 5.3 Future capability slots (normative intent)
Draft “future key roles” map to **service domains** (implementation deferred):
| Slot | Description | Depends on |
|------|-------------|------------|
| **F1 Multipoint cast** | MCU/SFU relay outside LAN; TURN/STUN | SFU SPEC, signaling |
| **F2 Stream transcode** | ffmpeg any→any; RT and non-RT | Media worker pool, object store |
| **F3 Cast storage** | Recordings on BE; paywalled retrieval | F2, billing |
| **F4 Commercial VPN** | Anonymous RT VPN; capacity planning | WG pool, payments |
| **F5 Payments** | Low fixed cost at scale; usage-based optional | F3, F4, add-ons |
| **F6 Mobile add-ons** | Masks, background replace | On-device or cloud GPU |
| **F7 In-app transfers** | P2P or ledger between users | F5, compliance |
| **F8 DNS / subdomains** | User subdomains + site builder | FE DNS API, tenant isolation |
| **F9 3rd-party streaming** | Ingest HLS/m3u8 or RTMP; re-emit as cast | F2, cast handlers |
| **F10 GEO enforcement** | Region deny/allow on publish, stream, download | F3, F5, edge CDN/WAF |
---
## 6. Target architecture
### 6.1 Environment tiers
```mermaid
flowchart TB
subgraph Dev["DEV cluster — unstable features"]
D_FE["Edge / FE stub or shared"]
D_SVC["N × Alpine service VMs"]
D_DB["MariaDB (disposable)"]
end
subgraph Staging["STAGING cluster — beta"]
ST_FE["FE mirror or path prefix"]
ST_SVC["Prod-shaped VM count"]
ST_DB["MariaDB + anonymized seed"]
end
subgraph Prod["PROD cluster — current + expanded"]
P_FE["Gentoo FE apps.f0xx.org"]
P_SVC["BE VMs + future workers"]
P_DB["MariaDB primary + replicas"]
end
Dev -->|"promote on PO sign-off"| Staging
Staging -->|"promote on stability gate"| Prod
```
| Tier | Audience | Data | Deploy freedom |
|------|----------|------|----------------|
| **dev** | Developers, agents | Synthetic / disposable | Daily; may break |
| **staging** | PO, QA, friendly beta | Anonymized prod subset | Weekly; feature flags |
| **prod** | End users | Real | Hotfix + scheduled; zero-downtime target |
### 6.2 Service domains
Logical grouping for VM assignment (see DR §Architecture options):
```mermaid
flowchart LR
subgraph Edge["Edge tier"]
FE["FE nginx TLS"]
LB["L4/L7 LB optional"]
end
subgraph App["Application tier"]
WEB["Web + consoles + OTA metadata"]
API["Control APIs PHP"]
GIT["Gitea"]
SHORT["URL shortener"]
BUILD["Build / Docker"]
end
subgraph Data["Data tier"]
SQL["MariaDB"]
OBJ["Object store / recordings"]
end
subgraph Realtime["Realtime tier"]
WG["WireGuard pool"]
RSSH["RSSH bastion"]
SFU["SFU / MCU future"]
FF["ffmpeg workers future"]
end
FE --> LB --> WEB
LB --> API
WEB --> SQL
API --> SQL
SHORT --> SQL
GIT --> SQL
BUILD --> OBJ
SFU --> FF
SFU --> OBJ
WG --> RSSH
```
### 6.3 Reference cluster layout (prod)
**Phase A (minimal split)** — cost-conscious; matches draft “tighter” intent:
| VM role | vCPU | RAM | Disk | Services |
|---------|------|-----|------|----------|
| **be-web-1** | 2 | 4 GiB | 40 GiB | nginx, PHP-FPM (hub, crashes, graphs, builder UI, OTA JSON) |
| **be-data-1** | 2 | 8 GiB | 200 GiB+ | MariaDB primary; nightly backup to OBJ |
| **be-ops-1** | 2 | 4 GiB | 80 GiB | Gitea, url-shortener, small object cache |
| **be-build-1** | 4 | 16 GiB | 100 GiB | Docker builder; ephemeral |
| **be-vpn-1** | 2 | 2 GiB | 20 GiB | WireGuard + RSSH bastion |
| **fe-1** | (existing) | — | — | Gentoo FE — unchanged |
**Phase B (scale-out)** — when metrics justify:
| Add | When |
|-----|------|
| **be-web-2+** | PHP-FPM CPU > 70% sustained or p95 latency SLO miss |
| **be-vpn-2+** | RSSH session count > [§12.1](#121-rssh-remote-access-connections) per node |
| **be-media-N** | SFU/ffmpeg SPEC approved |
| **CDN edge** | OTA/release origin egress > [§12.3](#123-network-bandwidth-and-egress) budget |
| **Regional PoP** | p95 RTT &gt; 300 ms for target markets + media revenue (F1/F3) |
| **Managed SQL** | DBA ops > 4 h/month or replica lag requirements |
### 6.4 Traffic and control planes
| Plane | Protocols | Termination | Notes |
|-------|-----------|-------------|-------|
| **Control** | HTTPS :443 | FE → BE :80 | Heartbeats, upload, consoles |
| **Git** | HTTPS / SSH | FE path or dedicated | Gitea behind same FE prefix |
| **OTA** | HTTPS static | FE → BE or CDN | Large artifacts → object store |
| **VPN lab** | UDP :45340 | Router DNAT → FE → BE | Unchanged until WG pool |
| **RSSH** | TCP :443 | `ra.apps.f0xx.org` | Bastion may move to dedicated VM |
| **Cast LAN** | UDP/TCP :4123441235 | Device-local | No BE in alpha |
| **Cast WAN** | UDP/TURN/WebRTC | SFU tier (future) | See [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) §SFU |
---
## 7. Service inventory and scaling profile
| Service | Stateful? | Scale axis | Co-location rule |
|---------|-----------|------------|------------------|
| Hub / crashes / tickets | No (PHP) | Horizontal | With API pool |
| Graphs ingest + UI | No | Horizontal | Same pool |
| Builder UI | No | Horizontal | Same pool; triggers build VM |
| Docker build | Ephemeral | Queue workers | **Dedicated** CPU VM |
| MariaDB | Yes | Vertical → replica | **Dedicated**; no ffmpeg |
| Gitea | Yes (git) | Vertical; mirror RO | Low traffic; ops VM OK |
| URL shortener | No | Horizontal | Small; can share ops VM |
| OTA artifacts | Yes (files) | CDN / object store | Split from PHP |
| WireGuard | Yes (peers) | Pool by region | UDP-friendly VM |
| RSSH bastion | Session | Horizontal + sticky | See §12.1 |
| SFU / ffmpeg | Session | Horizontal | **Dedicated** media subnet |
---
## 8. Deployment and promotion pipeline
```text
feature/* or next ──► dev cluster (auto or manual)
▼ QA + PO
staging cluster
▼ soak + sign-off ([ALPHA.md](../ALPHA.md) pattern extended)
prod cluster
```
| Step | Actor | Gate |
|------|-------|------|
| Deploy to **dev** | DEV / CI | Unit tests green |
| Promote to **staging** | PO or release delegate | Integration + e2e smoke |
| Promote to **prod** | PO | Staging soak; [GIT_FLOW.md](../GIT_FLOW.md) tag on `master` for app releases |
**Config rule:** `config.php` and secrets are **environment-specific**; post-sync scripts (e.g. `ensure_url_shortener_prod_config.php`) must be idempotent per environment.
---
## 9. Zero-downtime and load balancing
| Tier | Strategy |
|------|----------|
| **PHP-FPM** | N≥2 VMs behind FE upstream block or cloud LB; `max_fpm_children` tuned per VM |
| **MariaDB** | Primary + read replica; app read/write split optional later |
| **Gitea** | Single primary; maintenance window for upgrades unless Gitea HA adopted |
| **WireGuard** | New peers on least-loaded node; existing peers sticky until session end |
| **Static OTA** | Blue bucket + CDN swap or versioned path (`v0/ota/…`) |
**FE nginx pattern (multi-BE):**
```nginx
upstream androidcast_be {
least_conn;
server 10.7.16.128:80 max_fails=2 fail_timeout=10s;
server 10.7.16.129:80 max_fails=2 fail_timeout=10s;
}
```
Cloud providers: prefer **managed LB** (GCP ILB, Azure Front Door/Application Gateway) when FE moves partial traffic to cloud.
---
## 10. Data and storage
| Data class | Store | Backup |
|------------|-------|--------|
| Relational (crashes, tickets, RA, short links) | MariaDB | Daily logical dump + binlog optional |
| Git mirrors | Gitea disk | `gitea_collect_backup.sh`; off-VM copy |
| OTA / APK / build artifacts | Object store or large volume | Version immutability |
| Cast recordings (future) | Object store + lifecycle policy | Glacier-class after N days |
| Audit / graphs time series | MariaDB or TSDB later | DR §open |
**Rule:** Object store VMs optimize for **disk IOPS and egress**, not CPU.
---
## 11. Simulation lab (Alpine HVMs)
Sysop MAY simulate a cluster on FE hypervisor:
| Property | Value |
|----------|-------|
| Base OS | Alpine 3.x (match prod BE) |
| Pre-install | `nginx`, `php81`, `php81-fpm`, `mariadb`, `git`, `docker`, `wireguard-tools` |
| Network | Private `10.7.0.0/8`**no WAN topology change** |
| Count | 37 HVMs typical for dev cluster |
```text
FE XEN
├── hvm-be-web-dev
├── hvm-be-data-dev
├── hvm-be-ops-dev
└── hvm-be-build-dev
```
**Deliverable (future script):** `orchestration/sim/cluster-up.sh` — out of scope until PO prioritizes; SPEC reserves name.
---
## 12. Capacity planning framework
### 12.1 RSSH / remote access connections
Planning numbers for **single bastion VM** (2 vCPU, 2 GiB RAM, Alpine):
| Metric | Conservative | Target design |
|--------|--------------|---------------|
| Concurrent **SSH** sessions | 50 | 100 with `MaxSessions` tuning |
| Concurrent **heartbeat** devices (poll 17 min) | 2 000 | 5 000 (mostly idle HTTP) |
| Peak **new sessions / min** | 10 | 30 with queue |
| WireGuard **peers** (lab track) | 200 | 500 split across 2 WG nodes |
**Bottleneck order:** MariaDB connection pool → sshd `MaxStartups` → PHP-FPM for heartbeat API → CPU on bastion.
When **staging** exceeds conservative column for 24 h, add **be-vpn-2** before cloud migration.
### 12.2 Cast and media paths (future)
| Mode | Latency | Scale unit | ffmpeg role |
|------|---------|------------|-------------|
| LAN P2P | Lowest | N/A (alpha) | None |
| SFU relay | Low | 1 SFU ≈ 50200 viewers (codec dependent) | Optional simulcast |
| HLS/m3u8 bridge | Mediumhigh | 1 worker ≈ 1030 transcodes | Heavy CPU |
Detailed SFU sizing deferred to owner SFU SPEC ([OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md)).
### 12.3 Network bandwidth and egress
Scale planning MUST include **throughput**, not only CPU/RAM/connections. Hidden bottleneck today: **single FE WAN uplink****FE→BE backhaul****single BE NIC**.
#### Link inventory (baseline)
| Link | Typical role | Metric |
|------|--------------|--------|
| **Router WAN** (`134.17.26.161`) | Public ingress/egress | Mbps sustained + peak; GB/month |
| **FE ↔ BE** (`10.7.0.10``10.7.16.128`) | Proxied HTTP(S), internal sync | Mbps; must exceed sum of BE-facing services |
| **BE origin** | PHP, MariaDB, Gitea, OTA origin | NIC util %; egress GB/month |
| **UDP DNAT** | WireGuard lab | Mbps per active peer × peers |
| **Future CDN origin pull** | OTA, recordings, HLS | Origin Mbps; **CDN egress** billed separately |
#### Per-service bandwidth profile (planning)
| Service | Direction | Order of magnitude (prod planning) | Scale trigger |
|---------|-----------|-----------------------------------|---------------|
| Control APIs (upload, heartbeat) | In ≪ out | 120 Mbps peak; low GB/month | p95 latency, not Mbps |
| Consoles / hub | Out | &lt; 5 Mbps sustained | CDN cache static assets |
| OTA APK / `.otapkg` | Out | **Burst 50500 Mbps** during release | CDN **required** before wide rollout |
| Gitea clone/fetch | Out | 10100 Mbps burst | Mirror geo cache |
| URL shortener redirect | Out | Low | — |
| Cast recordings (F3) | Out | **100 Mbps1 Gbps** per hot object | Object store + CDN; geo replicas |
| SFU relay (F1) | In+out symmetric | **550 Mbps × streams** | Dedicated media NIC / region |
| ffmpeg transcode (F2) | In+out | **10100 Mbps** per job | Worker pool + queue depth |
| Commercial VPN (F4) | UDP bidirectional | **15 Mbps × active tunnels** | WG pool per region |
#### Budget template (PO fills per environment)
| Metric | Dev | Staging | Prod (initial) | Alert |
|--------|-----|---------|----------------|-------|
| WAN egress GB/month | — | — | _TBD_ | 80% of ISP/cloud quota |
| Peak WAN Mbps | — | — | _TBD_ | 70% of measured cap |
| FE→BE peak Mbps | — | — | _TBD_ | sustained &gt; 60% 15 min |
| OTA release peak | — | soak test | CDN absorbs &gt; 90% | origin &gt; 100 Mbps |
| Media egress GB/month | — | — | _TBD_ (F3+) | cost budget |
**DR rule:** Do not add **be-web-2** for bandwidth until CDN/OTA offload is evaluated — horizontal PHP nodes do not fix a saturated WAN link.
---
## 13. Cloud migration (GCP / Azure / other)
**Hybrid recommended** for first cloud phase:
| Stay on-prem (FE + latency-sensitive) | Cloud candidate |
|---------------------------------------|-----------------|
| Gentoo FE TLS (or parallel cloud LB) | Object store (GCS/Azure Blob) |
| WireGuard UDP if DNAT chain kept | Managed MariaDB |
| LAN cast | ffmpeg worker MIG / VMSS |
| | SFU pool (GPU optional) |
**Migration sequence (normative):**
1. Object store for OTA + build artifacts
2. Read-only analytics replica or backup target
3. Staging cluster entirely in cloud (dress rehearsal)
4. Prod PHP tier behind hybrid LB
5. Media plane last (highest risk)
**Egress rule:** Keep **one** canonical public URL per service; DNS/CNAME moves — no app hard-coded IP changes.
**Bandwidth rule:** Model **cloud egress pricing** in phase 1 object-store migration; prefer **CDN in front of origin** for any artifact &gt; 1 MB served to &gt; 100 users.
---
## 14. Global availability and GEO policy
Draft intent: **anyone in the world can use the platform** (latency permitting), while **specific content or features may be excluded by region** — e.g. a recording or OTA channel must **not** be published or downloadable in **Zambia (`ZM`)** when PO sets that policy.
### 14.1 Reach model
```mermaid
flowchart TB
subgraph Users["Global users"]
U1["Americas"]
U2["Europe / Africa"]
U3["Asia-Pacific"]
end
subgraph Edge["Edge layer"]
DNS["GeoDNS / Anycast DNS"]
CDN["CDN + WAF geo rules"]
FE0["Primary FE TLS — on-prem"]
end
subgraph Origin["Origin cluster(s)"]
BE["BE VMs / cloud origin"]
OBJ["Object store"]
MEDIA["SFU / media PoP future"]
end
U1 --> DNS
U2 --> DNS
U3 --> DNS
DNS --> CDN
CDN --> FE0
CDN --> OBJ
FE0 --> BE
BE --> OBJ
MEDIA --> OBJ
```
| Phase | Reach | Latency target (control plane) |
|-------|-------|--------------------------------|
| **Now** | Single FE/BE (EU-centric) | Best-effort worldwide HTTPS |
| **Phase 56** | CDN for static + OTA | p95 &lt; 300 ms TTFB via edge |
| **Future** | Optional regional **media PoP** + signaling | RT cast &lt; 150 ms LAN-equivalent where PoP exists |
**Normative:** Public APIs and consoles remain **globally reachable** unless PO declares a **maintenance** or **sanctions** block at edge (separate from per-content deny).
### 14.2 Segment isolation
| Concept | Definition |
|---------|------------|
| **Segment** | ISO country code, optional region group (`EU`, `AFRICA`, custom list) |
| **Default** | **Allow serve** worldwide for control plane; content inherits tenant default |
| **deny_publish** | Content must not be **made available** in listed segments (upload/mirror/index) |
| **deny_serve** | Existing object must not be **delivered** (HTTP 451 / empty manifest / VPN reject) |
| **allow_only** | Exclusive list — only these segments (stricter catalogues) |
**Example (illustrative):**
```json
{
"resource": "cast_recording:abc123",
"geo_policy": {
"default": "allow",
"deny_serve": ["ZM"],
"deny_publish": ["ZM"]
}
}
```
Same pattern applies to OTA channels, short-link targets (optional), and paid stream entitlements (F5).
| Content class | Typical policy |
|---------------|----------------|
| Crash/ticket consoles | Global (operator VPN/RBAC unchanged) |
| OTA `stable` channel | Global unless PO restricts beta regions |
| Cast recording (F3) | **Per-object** deny segments |
| HLS rebroadcast (F9) | Per-channel geo + copyright rules |
| Commercial VPN exit (F4) | **egress region** choice, not arbitrary bypass of deny_serve |
### 14.3 Enforcement layers
| Layer | Mechanism | When |
|-------|-----------|------|
| **L1 DNS / GeoDNS** | Route to nearest healthy edge | Multi-PoP phase |
| **L2 CDN / WAF** | Geo block or cache vary-by-country | OTA, static, recording download |
| **L3 FE nginx** | `geo` module or auth subrequest | Optional coarse blocks |
| **L4 BE API** | Resolve client country (CDN header `CF-IPCountry`, GeoIP) → **geo_policy** check | Normative for dynamic content |
| **L5 Object store** | Bucket policy / signed URL with segment claim | Large artifacts |
| **L6 App / entitlements** | Mobile checks BE before playback | F3, F5, F9 |
**Headers (normative when behind CDN):** trust `X-Geo-Country` or provider equivalent only from **FE/CDN** — strip at edge from client.
**Audit:** deny_serve events logged to audit table (who, resource, country, time) for operator review.
**Dev/staging:** may use `X-Debug-Country: ZM` header to simulate segments without VPN.
---
## 15. RBAC and operator model evolution
| Phase | Model |
|-------|-------|
| **Now** | Developer-centric; shared `ac_crash_sess`; privilege sets for tickets, RA, short links |
| **Staging scale** | Separate **operator** vs **developer** orgs in Gitea; audit log export |
| **Prod commercial** | Tenant-scoped RBAC; customer admin roles; SSO optional |
Platform MUST support **per-environment** user tables or row-level `environment_id` before multi-tenant commercial features (F5F9).
---
## 16. Commercial and paid services (platform hooks)
No implementation in this SPEC — only **interfaces**:
| Hook | Requirement |
|------|-------------|
| **Billing** | Webhook idempotency; `tenant_id` on all paid resources |
| **Metering** | VPN minutes, storage GB, transcode minutes, **egress GB** — export to metrics |
| **Entitlements** | Mobile app checks BE `/api/v1/entitlements` (future); includes **geo_allowed** flag |
| **Add-ons** | Feature flags per user; GPU queue for masks/background |
Payment provider SHOULD have **no monthly minimum** at low volume (draft: “near 0 for developer”).
---
## 17. Observability and SLO
| Service | Health | SLO (prod, initial) |
|---------|--------|---------------------|
| Control APIs | `/api/…` + upload smoke | 99.5% monthly |
| Consoles | HTTP 200 login page | 99.5% |
| URL shortener | `/api/v1/health` | 99.9% |
| RSSH heartbeat | device poll success rate | 99% |
| Gitea | mirror sync age < 2× interval | Best effort |
**Bandwidth metrics (add to graphs / future central metrics):**
| Metric | Source | Alert |
|--------|--------|-------|
| `wan_egress_mbps` | router or FE interface | &gt; 70% cap 15 min |
| `fe_be_backhaul_mbps` | FE→BE link | &gt; 60% cap |
| `origin_egress_gbytes_month` | nginx / object store | &gt; 80% budget |
| `cdn_cache_hit_ratio` | CDN analytics | &lt; 85% during OTA |
| `geo_deny_serve_total` | BE audit | anomaly spike |
| `sfu_media_mbps` | SFU (future) | per-PoP capacity plan |
Use existing graphs console + future centralized metrics ([GRAFANA_vs_others_graphvis_pivot.md](../GRAFANA_vs_others_graphvis_pivot.md)).
---
## 18. Safety constraints (current topology)
From [bottomline_reminder.txt](../../bottomline_reminder.txt) and [INFRA.md](../INFRA.md):
| Rule | Detail |
|------|--------|
| **No risky WAN changes** | Router `134.17.26.161` → FE `10.7.0.10` chain stays until DR-approved migration |
| **Port 8089** | Janus/other — not androidcast vhost |
| **BE listen 80** | Canonical for FE `proxy_pass` |
| **ssh access** | `ssh alpine-be`, `ssh f0xx-monstro`, `ssh f0xx.org` — inspect `tmp/FE_gentoo`, `tmp/BE_alpine` before infra edits |
| **Simulation only** | New HVMs on intra `10.7.0.0/8`; no public IP experiments without PO |
---
## 19. Phased delivery map
| Phase | Scope | Alpha relation |
|-------|-------|----------------|
| **0** | Document + simulation design (this SPEC/DR) | Parallel to alpha |
| **1** | Split **build** Docker to dedicated VM; OTA on object disk | Post-alpha |
| **2** | **be-web-2** + FE upstream; staging cluster on HVMs | Post-alpha |
| **3** | Managed backup; MariaDB replica | Post-alpha |
| **4** | SFU + ffmpeg workers (separate SPEC) | Post-alpha |
| **5** | Hybrid cloud staging + **CDN for OTA/static** | Strategic |
| **5b** | **Bandwidth dashboards** + egress budgets on graphs | Strategic |
| **6** | Commercial hooks F5F9 | Product-dependent |
| **7** | **GEO policy** enforcement (F10) for recordings/OTA/catalog | After F3 or PO mandate |
---
## 20. Out of scope (this SPEC revision)
- Concrete Terraform/Pulumi modules
- Pricing quotes for GCP/Azure
- Legal/compliance for payments and VPN resale
- Mobile app changes for commercial features
- Replacing MariaDB with Postgres
- Sanctions-law mapping per country — PO/legal owns lists; platform stores **deny segments** only
---
## 21. Related docs
| Doc | Link |
|-----|------|
| Infra topology | [INFRA.md](../INFRA.md) |
| BE service map | [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) |
| Build / Docker | [BUILD_DEPLOY.md](../BUILD_DEPLOY.md) |
| Remote access | [REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md), [20260602_REVERSE_SSH_proposals_summary.md](../20260602_REVERSE_SSH_proposals_summary.md) |
| Open tasks graph | [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md) |
| Alpha priorities | [20260608_ALPHA_PRIORITIES.md](../20260608_ALPHA_PRIORITIES.md) |
| URL shortener | [specs/20100611_3_url_shortener.md](20100611_3_url_shortener.md) |
| Design review | [DRs/20100612_1_scaling.md](../DRs/20100612_1_scaling.md) |
---
## Changelog
| Date | Change |
|------|--------|
| 2026-06-12 | Initial SPEC from draft `20100612_1_scaling.txt` |
| 2026-06-12 | Rev. 2 — network bandwidth (§12.3), global availability + GEO policy (§14), metrics |

File diff suppressed because one or more lines are too long