Plain one-column résumés for tracking systems, October content, deploy to the OVH server
Changes
@@@ -8,7 +8,7 @@"build": "astro build","preview": "astro preview --host","vendor:fetch": "node scripts/fetch-vendor.mjs",-- "resume:pdf": "python3 resume/build_resume.py"++ "resume:pdf": "python3 resume/build_resume.py && python3 resume/build_ats_resume.py"},"dependencies": {"@astrojs/mdx": "^3.1.9",nnew file mode 100644@@@ -0,0 +1,99 @@++%PDF-1.4++%���� ReportLab Generated PDF document (opensource)++1 0 obj++<<++/F1 2 0 R /F2 3 0 R /F3 4 0 R++>>++endobj++2 0 obj++<<++/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font++>>++endobj++3 0 obj++<<++/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font++>>++endobj++4 0 obj++<<++/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font++>>++endobj++5 0 obj++<<++/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<++/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]++>> /Rotate 0 /Trans <<++++>>++ /Type /Page++>>++endobj++6 0 obj++<<++/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<++/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]++>> /Rotate 0 /Trans <<++++>>++ /Type /Page++>>++endobj++7 0 obj++<<++/PageMode /UseNone /Pages 9 0 R /Type /Catalog++>>++endobj++8 0 obj++<<++/Author (Alex Frison) /CreationDate (D:20261020184315-07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261020184315-07'00') /Producer (ReportLab PDF Library - \(opensource\))++ /Subject (Data Analyst: SQL, Excel and Power Query, compliance reporting and automation) /Title (Alex Frison - Resume) /Trapped /False++>>++endobj++9 0 obj++<<++/Count 2 /Kids [ 5 0 R 6 0 R ] /Type /Pages++>>++endobj++10 0 obj++<<++/Filter [ /ASCII85Decode /FlateDecode ] /Length 3015++>>++stream++Gb!#]>>s9I(4OT53,_0-D9%fXR!i+YaUYY=&7B-`X7F])g3+!i>3Tp54>f?P^XFdXL&_\uC+:B<**rEr^[n"[Lq<A<r+D0+*F@4,pCThK?-aqaGPZE07OQf+^:]<f-O3"D*5tcf+"uPuV`Vhh&O$&/k&53@e=>EklPsQX'^bMFEPs6n9epuamu=WaGY?Z'e+MU#g*ce\O>n5@)MrGcl+uhD&u#;R&/rs+'.f#n<0O:jmW?jNs%_eLhN\f#AWh_/iq$CN':8Rc!8i+?;7eb.O)RZUdspr/b\Pr^UoU]U]7>NPA?(M^U@WT/GfMc-ca@V:F6+1feDL1+b2QJ[Q,`6rHo-;WY<.BVgKjg(Z1O>($R]UcLhpYY!F:+7CXL70+u9j#8f)e&%=P7P=g\MbQC/@S,0@o()[DH"VaTWg/tCW5Z,,Y#HH2*1h=Gr"rTF*+Er!IU&dJ\`O>s$^DZ7L[_V`Od]d`ls3VCF_%UI6@lEOh'j)/q^2_%"I+e_K8IRpllQHM^[email protected]=+un$-VI5#lpeMq9cQlJnV8a\o]XbW:.[)HR?CcF]))2+HRg5:QcGH"eE1A/l"(7mYu6s"+k%daU/nbXrB%"<<YeH"'rB&"La-5-d^EgmWQ^'k5FqVSL,<R1h4dm$`Dd5/>(8c#,"8Xe<*;FrBdso+G8_WaFHIi6&IGb!hfP,dgTf<Dml9\dkH*+AAOoq('.'0U<*;=:D^F^$E/H-.X@bh]qYVjcXt*.7CY);a*oDdF6^J9'b*b.#150M+P6J'2(GA`V_8kUeG-9/2;655P+[/XIQHDl\]UN,U5ht`25A'1]dTkjrX>J93;1j9FFC55FCo"[sOg::L$F50e3*TLg\$a@>;)l]AD%;I;/Wtgs$7^qGo6b]lLANVub@g,)hkt+"=1\SFOO`5q:;$i:m_eXH$RBWtKse!tk@sgDh<RTPGjVsiDJop/>5,l"f>G^3$#LrNe?JhZP(&TlFic,O&(QG;(!c$eh@$eX;[email protected]_hZ*!WTb=Ueh-E]@HLX-R)f5jrEe;sCi@c\B\!.PTii[Q[_FS(Tr=/??*B[O6/952[/qUp`VdTCu$L2u+YfgV*'o(@`QP6/:u99n:R&GN]VWJ<+i0h%"k>b(5&6+=I$]<;OZ$U);Xqj?d)]"*qe?-ZON(I"(IYZg/:-E,\Z6ep/0>TY%OYA\nIGg6.MdWo.6.X<L]?fNg?L-Rr4+1.@aQ1UmcR;sh'E)@hZOrMEVAAP0UZo"fIjhSd5ei1%FKgja8##s:haA3PZarGL**dVq_iL=6dT<ApO8S7W?rg6\gecr9(2mefJ=F2PS?094#!;Heib0KH0mo#%]I=BqF1K@CuhY%l0>?$Ip59<X0W\WIq/@r75GF@'Df&hBF,bP!XEH6>No&8eCLX=n,_lLKRQM[?E05`07CYbDA#*LbNrC"ue'(hSeC17;.+#.<WDYIglKPnH?[dLY>3T%pK)s5\GYu`BS<P"F*FqU$e;U$ZY`^AZ">qc@0G%f8BFs!^IR!Re8n1Z#.2tOt/=o12tNP%*(qDLQ.pB?O<^"UqDFubjhc'_B'*(Eqo27GUVR1ImO@iH<gVO=aKpiWft!iT>?C^.9-,J(f;Alc=d7fD:SVll!`#<Ze0M.TTn!Uq4UJK+*Y9VK)#Opo_5oD.IG>q:\.>VNC8M4m1QSE^GgTj!@*W8Bq0Zs="@QkEtekI<r=jgJ5o=f&^C+_*;W4TS5=UUsNH#u)o,H=f2?%E92g9c;("eR_T:?`-L8aJO\XR98,PYb8*`V'QrV(1k4#R/ZYM9J$o9p?50f:j,!k.IbcEod'<emh?<t1[lN>mLJ+)MrGg9QS.B3%VS\C1jTY\fEo-N2%Uh%a5Frhg5B['H%?0W<f^XU3ESH>:[email protected]#hqI1](.:P(3Z=3)FDNCU>LBPdq@+$^'tH5Te9I.02H(>Cd:EKD.Yb0@j;QAlUn::kCADRG5<o<p39BS0]aA5Y+Li"cC^IS<J[C[p`KfesS#(,U,"VL@Au_nS.,)IOR%MfR@+G3c0-#L!a3;c8M'7l/a?*=%M-pOE;!GSdTN)W=;]5<?T^0;ZR#7N+&+86EO7"-5*j6MrJaVjfRZ:\:2cC#XncSR)VC='&Ho!l"gI[/QseUFssd@^N;4%,\.!bMH`X<pj%dg(1[,36"3i&U#:i*4RN_AO&]LKlLb(=b1d^(T^pgm:3Sf#K3:A<oMCTt6/[uiCL.5W![_Ei#[kbaXe5XPN4JT0F,21[/;=Z>`e,tP>*!DmVRDV:4rMtdlZsRhQ#Rq+m05Y=W"TK1bu%(7LM-dacU9Q-Nn_eXr=C[5GC47B!5FUZ%kRs_o46)s(?j'0/t)e=*VsN3a(aCmD2@3n9M7Y"r/5&+[[G./_Fq!n]L.n@(//\Qbk.oYeW%MgZ6&5-.q70L*3cK*ir+U@I"*;Vn'bHhDpt_Q3'4L'7^#cI(R('8fCAQ,e!E\!9BBCq?T-62'92$BB!.M6R-63&Q#/et]@=l>>([hLnlf=DJDY1]kkRs*<me1&']IiSN%b=_bC]BoeCU8qFPE5#nQ3S$"k's[nLh_L*&[jfI]6/W^2r[4a(Vg0q=-RskT095L(M#@m+?U=.Pt;IZfjVmM=CkRH#!qsh/##[AeT^fShWLQNY-K%TDQ;l3W0KIkGt&k5IK;U>F(@sTYq!R2\uo"Q6[+Q/d?J3O7pY/?R!pP!qEUFQAO%qQP[4gpRo"Ka>.\5_rgd#AX8MI9](*iMGrR]7jT=Ymk]sY3nJ$fg8k=?0(i38ipYV52N%@72bMYa.2,\^?WlIq9AjQG0p]*<H"Ao6U[[:i?gZto@NYB6Aj-oC'l#RmcU4SmY4_jT)DI7lmaeY\gV!-t6n%h(4UoW)T>%d=o'O'=TpMY]f[+6I^]Kk<f@d;j5(%24p4KmI'W?fg#rO3ApWTkM'hf(e$8f(A$j!'mi3uZ5<>\CaOZ8:s_/88"P5[Qhgu\?g^UOS<j4:@*+<Cs.0TIF,,*qLhIT=bek5X/a8)j~>endstream++endobj++11 0 obj++<<++/Filter [ /ASCII85Decode /FlateDecode ] /Length 1880++>>++stream++Gb!#\?#poC'Rf_Z\506]!b(m[,Yo9;e/:>k!7uY_MnCQB's4j*ZmK>Sj*RT`_b&XbBBmcM4L"1TY-dBKF7'VP2=ZR@H^k\=cKNf(6rAkSF,E#<%a?V?4?NCa'Q*:)p2KSuh/'GP!9Y<G*r\pugJ>.86Ek,>^X_Ps/49%F%K%Bd[i.COrci+YY`:/&=%ok&2tW!)9QEc>Sf;RXEm0@F?DB`p&YHJ*4(1+OgL"Xf2pHgE/,XqL\#YJaR%[brCU:Tj5/1H9Y?YY.#%<M=ArWp)X%?&"52/;n[5[;cZ$@%p78^N+Q*GmYZNS(!rc\A,poioB,g;"BSd0B0ogFF%HNSDXlZg:rOse1'XVTtq:'m"@,'H1T4C(]B2figkDJY;AXaFH*^0D?O'Q]iON5ADXiloG04(a>g7U&N2WYNRI>V]l+>D/#>*$\23LiCUPa($KY5c,0_$cQ)(OGsT4HbNkt'[Reo1H\[sCZE#fIKL!]S9humEL)`Y;odeK-c:!JSf1lc;MsaCeKZ1nQWfl!OGDPbA=#VT>%t=MWQlSQMi3<=dQ(Am>>eZ9Z;;(QQo=7aRbO3$jrq36.!JlK8Ek2$W!#Nf6>-4I9<?M_:Y."mbgJ2eKW<Gm9&WpZS>gXj/h]^0$g]]WONgYSkM`:;Q\pWphVf+DZk1Y/eP*-:Looj4o*e*.)&=lR't'LT>-lo[N?g?F(+eKV\Tn1q?_^d?h"pT?i>aXn>4XW5LXde]2SI_d,W:mM'JTrS-bBH(R;iRZ*St&R#&TT9/rXoUL;28b+ZWs_LlBHCUl>83Db`8_G==B4#LVLNI`gcbI_PnI5m^2`A*-3&\d`:U[p+Ld0Q;+BcoHK%6OC52Tfc-m(m'r:&SGa&$;I`k$EiCZS0te4kZ+W/hk:OO5UW3cgOtgEEU4A5&l.2_TKtc"pYD%2g=;5=>=Lh$q2_a$1P7&/1uiSU"<NVQU=6sdoEIs4&@%Z?">(^G^__<DV3<@8MW:(rZ?=,YH``VKJ6(@KUMe&PmJ`fc5:`NC+T<:pQh2nQ[cVooGN(Eo(9/E.EHH>oM+uMG@Nc_SK[-)%Z*RuK9Qk8e)B4gNF.IP%a6gJWguYZ<[\EKSZM5b/1S@kfQD`))877SCg,M^3s2OA<:9['XUI8IE-C1<9)L)C&Kslr3]u&U;G,ushh=n=Y:r3C3G7E[o0YRUrc<gC>hheu":)l.i0qP^WC=W*"4IV8Uk[nl6%S,!*\bh#M$g>tF(Z5%@npg6(8;3NhXO5qW,Y?H4_t<pMq0kB:gQ\!BeZR?!@Gs<RFCFECE]0VJeuh2GL#mEVb>^X)QfH]iBE29Xm%$e;gGiIM%N5eMk78d,dTonjap<dHHZp^FgT1)@qTjII.D/C1HSt7ZI/ETN&/C#2W@e_sL,&=D(e[gSr":j0H4D<&]&%.ni;Go>&9.YG0IbcqS%P-MBFFA$$1-Pjk=J]]!F9M%aq5tZ82,WJL_VOr'I=\ao@o]1_DgLWUB>iF"OLijHG%Qm`r40f:`EaAE@:g"32!BjqRSD1])pI"b$@Y2Gno`ihLVCbba@9Mhnkl8+RM!;`25N/7XZaMd>VXUm8k47DGA)fN%M*fn-ng'gB_U!kC*[#!=HP/GcQIkV2N`V@6ejXI^MQ!Be;u)aa"A)<D1-pfKG0)=,<&PTL=e-$b>'.?A3WN&A=<3NmDT5_qE$%qhc9gr@PMXMb:F)q]3p;Q]B:6p'n.1BkX&8lBP,lg20HsMpT.95[C.s@07:9VZ&LglU&FKR#DDUi+T2-k\Y;;,K%E;/9D-Yg?q,%fUCc6K7`-+%&Uf`9+*R93!-V$7r3c4@O\MThF"6P@:1=-7:@<sq_jpQ:U+B+FL`o$PM[GbWs^#$6HBH&^DAjjq?LK6m(<~>endstream++endobj++xref++0 12++0000000000 65535 f++0000000061 00000 n++0000000112 00000 n++0000000219 00000 n++0000000331 00000 n++0000000446 00000 n++0000000640 00000 n++0000000834 00000 n++0000000902 00000 n++0000001249 00000 n++0000001314 00000 n++0000004421 00000 n++trailer++<<++/ID++[<4db2b9d6052cdbd87e4d08758dd69ff6><4db2b9d6052cdbd87e4d08758dd69ff6>]++% ReportLab generated PDF document -- digest (opensource)++++/Info 8 0 R++/Root 7 0 R++/Size 12++>>++startxref++6393++%%EOFnnew file mode 100644@@@ -0,0 +1,99 @@++%PDF-1.4++%���� ReportLab Generated PDF document (opensource)++1 0 obj++<<++/F1 2 0 R /F2 3 0 R /F3 4 0 R++>>++endobj++2 0 obj++<<++/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font++>>++endobj++3 0 obj++<<++/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font++>>++endobj++4 0 obj++<<++/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font++>>++endobj++5 0 obj++<<++/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<++/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]++>> /Rotate 0 /Trans <<++++>>++ /Type /Page++>>++endobj++6 0 obj++<<++/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<++/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]++>> /Rotate 0 /Trans <<++++>>++ /Type /Page++>>++endobj++7 0 obj++<<++/PageMode /UseNone /Pages 9 0 R /Type /Catalog++>>++endobj++8 0 obj++<<++/Author (Alex Frison) /CreationDate (D:20261020184315-07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261020184315-07'00') /Producer (ReportLab PDF Library - \(opensource\))++ /Subject (Software Engineer: full-stack TypeScript and Node.js, payments, identity and live streaming) /Title (Alex Frison - Resume) /Trapped /False++>>++endobj++9 0 obj++<<++/Count 2 /Kids [ 5 0 R 6 0 R ] /Type /Pages++>>++endobj++10 0 obj++<<++/Filter [ /ASCII85Decode /FlateDecode ] /Length 3349++>>++stream++Gb!#]>Bedl&q801W4+auJfq59o<K2p@jZBY!&N:ga"NG%!l^=+dqY;4P93"r[4^*bR!?Ya:F:-J(8C-g<Rk0)9.g'_mAh$:S^-Km)jf><bl`!ubq@6ol$WKiY8;*PA?V&`K=9Ms`YWU/g$7V4!U[VejU3'jf&q6j*olTF\]9)@gZ=W[7ZmUBDfhAGj;#p(dJR?&m0!@T8F-il[D[5;C$0nG,1Jh0KXs!V()F:cWB(BMV,<(:+%LEo1m>eD8<V=tgq9Ab\CGHjE7O1`)P,i+J6N)DreFh^X)0YTVS)fa"qJA4I[DrT'u(6BfDHe?X_^(&L1Z](m"b%.RJ[XLUs9HP4r?K:f?IXhm1s&M/?S"i'ppl&V"8+7-;s+B$M#T[A4CgQ1uG*Lk34QW,SHa+.:4ttAIbPG'?e>P.eN6PRZ78.$L]6O##:*8c3%,>##DY$]uQ"*@E%\:=/5#PLEuR)oKm#Zf(e_n^*ruQrq[hD&'*0?1,rGOd;7\p]j[s*jp)Xi[3;4J?FKo@F!gsn$imD2MACISq&'2KBdA\6<%C3-9P3+..qP03=im%QSg884Kh:r.W0.fd,@LYM@g$Ie4kU%@L0_70N/[5G%q!)h+$-GIfr--Pir^n&-dL&GCsl:gJqgGY>3bWMR$Us8#nih1=[iFE!N16!b>`It/`)gs\4i\o+eJGiSW$MTbFr^RqC`P]4Foh"T1h]J4JW%YP/SS18@5eQ[sU9T=UElL,gnJ.?_m1U4f4OI&kS5&8\^A'ZPt?d>ij"N6Y`/o^PcXoP-l(Q`e7[,okC+]e9>k4PK#)l>pC+]&7=Nd&/bs%DO;1<51q[F_=9]'pLP6lla,Th4ABflo5P?9qSuB#h]lk,G&hjg4!_$8nG`$\l.&r.D&9.6JQ48mj86jDdA9C"\QI"UnEdii3kACt*;WCbM<aq`@ocjkB-/`HqGlLo`O/("\T$bAIKe+USfRAiVlj/%fS!1&=./i1%&dt.q<(^%FW?jAcfPudN,@&5FiYE'YV\diae8V^WqOsJfS0;;_lAc<0iJWm%Wmn@]8,#$Z0G""Z,`YUQ'nJ7f(E)`qhCFKAaSXDj66'r'lL'_WQn6ZRYS/l4ESuWdfXmu_p>+N)m'aZ58[<J4Fj>#p-sf586U$k6!C%^9$IN*,b?T06-(;Q2N8Ho3W)[&KLS@ci:u[8k1/MSFH;LWk(h.-H+H`%FYO)<WZ&Xm^mRK78/*d_0G0YdEqCQK88PFtjNtN>$-+;+OU3%cUq`ul":gDi"i#=%LYe)4ZYBSrmjDc.]%kgf"f_9:%`_7Um9Ukcl8AQi4+jonH\^0mYu9kQ(sW@h6att+Mq7pU9s_FKYt;H[RBbQ--U`#:+2`aKkpA0NVl[B<"2j>ncEFBZe8,Jt9HPo7W%8aO1L-Q3Aetp\6(CR*AcDT!*H$019ba#=4gt'L-W4j<)+:Q-@0UC&]ehm.Ra,nQS=d\>P6YrRNuK^h,ld3BpL1]L,f4chN!kV7>n,&T=O7nP)+Q1NK?/Y$MqoQi?R20t7]-)=l]fWSMq"ccUN<a(mr_7jN<`'W\(uPBShcc$1"Rfg2=_/s^p7@[UVs7@8jgqaKfR4fd]d8uKM2@QP9P\^`gfVp=EsCF8<GH#,[taI[&rcf2>ZqE?Ct"@kO6FRLP37bdHis8h5>":>\VKi9u2nfqZdg>?k];4R5t"V$Mq8DSO'k;*'ZJ:etfuH6`ZASW\ftq$6Nll;8/iLX5+"Wd!'&EO(!N.Mfr`/^:(!sX?q\>]<rYQ+%,CS3uD(6&G9O^Frq&02ERb5M]qks,3>,MY/3$7207HW]`]?tgh4O8F)Ia<$gEKb)3\#o6/Yc-n;H6$<jpF;cds"`.J"73!c)EZ3]Ii/#j!e&hStIM5VB3/j$p"Qfe/./"=[cbi&Q?g6UamH\[C<cL$"efffsFB,25#P9]j'(CtX1YGs#Gs#ONPC$fr:qj>Pfr'h)ksM_&5ne]TftF><*M&8-b)*2$\9,W6O$7?*B62K$3K47*_f-5$\7A*5bo7QN>m=+\d,;(A4d_)M9`!q@\UZ/gc(^b!hRFS??pT*.m=Lr`'9UELJj'#q0qGRQ)@a9b>GMCLI$1'(TIJOMQj&p-d"&Qt@NGuFG$P>"8[NcqAEf%W8#"rIB!+\udDmR/*rX,M2GOZ^<KNT+E!5QK(=Psh+1m&X8:j5d6\"hrr0JAp0'GRLLN@u:P!!rt%RE]9qR'AK^2#*bF4+o_>G<`TaN)cKB0bYBXX,E"75/A^+q4JuTKWEOV9#Jp_,@0%kJ5X`e3bB4piGmHp2IGD.<Ho4"c(ED6I["G,CX4_OjSK99MRZ?5shF(3@P1#0F#<5W)[H:=1'7o4b`.dN1g>1>pBuudb%U9^s(8n29riBmobY!3J3eJ3>%a$MVng$'Ba**_FH*:e%?pNU2kh\5ASi4i'Ach'$JgGC`3sC?N<j$A.;8&7eDMtJH:<fZp@rtl[:lRH91dn->^bqu!oltL*P&Ba1/(+(i-9'rgi5pUsHI$8.bX^4-\K1=`*L*=u;bs8Z5B+c5A,iW-Rp+,Xhf-cZ4l?eTc0$;nDore/>.sW1J=4G.h&3D>.QZS=X/H,X*VZ/:YPp-T6+]hY+L,VsXph$_!:6agKU<mr5)kc9h;M/#rPMsnk1p,c;Y1`h4e?Jtr,r6W>JFT5@db8DPXo%bliM%q&Y<k2-TbPuU?XX595S*cR/;*$2S"V^2%s)ek"B68*=HkCdMN^D-gC>[%3?VX%'`X$q;+Gj9L/H[+@nJ_(QC0$Rgnnk!&ObD1">H3S5u]0eO"/c$r#]pU&Eu4$PqFtPHRr>QE3?8_Ca;MZ#nMJf?A!E\_%J:No5EiV8R5Yp.X1)$+W$tWQ!/W\l]Pc/W];>95VdZNg:fN%>WaABFkI.HY=VBKA4HqD-JGbBm.P%?'p8`,/$68Cm3JV;4itjVThup]p1s7WdLE0<r-Z8QM6c56)%16j1t8V$]p3,=`?ZEg"J*'&.dhZ0QfuE"$Ds%66#J>ONo1IJG][P\(LK9;iBR[K4*B3(.f:pYM.k?/5[,t?#>q&C4T!C14Xok9&3PK@rFi3M/B5k.S(4$XGj7/$a`Ek<jY"0Jt1;Z15QKXYhbpq"IemH@GoI9`2rH%q6S3M9rapdVAH)m@[s/]RE8CBEd(Y`R$H=,cRTL*<ZEFGbH/l8;1Onld:*J@5%+WHVMNSl";a&(nKD:c\RLGacq6\l_&f<^^_`jfeNShA?:L6#d\f4i.0\F:dLL%C^s9sQ@:!`/j`,,fS)rQgO=OH:ec(6<[s0ZRpcFRI+7m\66NiidEF%-r&?j/m8'C;-rP[@GKqKROgGcIs6XTBXME@J$n3iT0Jr<t5%Gh)Vc2~>endstream++endobj++11 0 obj++<<++/Filter [ /ASCII85Decode /FlateDecode ] /Length 2981++>>++stream++Gb!#]flGh,(4FM1gnu%aN229Zih`8Xfh%]S@RX2SI`Mmb66b,/_#U->FnNf$\KE3!e*\oh;5tV6CbF@bQZLFU!p\bmn@t`Wk^6,Y?Nh-s1ga/-n+:=C9pbGf?X<kXbJ=`#O+Yp,&0:8Q'+XR$R(iQ8<\H./YT"[:@F,G#pZU+f*af84IPc*i\iZ\]3gQ>oD2[PG6I$dM$J2#qLVcT?LU.NCb";GWF?@)5r(Dr')(T-<=gVioNEW(DEg[Y_L&49k6CLYKi8qLOCf6i$J_u91IFk3_;<a7*?N*:_heE'33kDl5(q40>$O9%FHTQ.C^/FboMsSGERDg`rVn.hRWA"qeU*i[Q(/9b5Ue>64gX^4>[email protected]>lZL,!G2:ka?iBrWG%'R010mGKHjShe[IGl&'/[j#MRBnVCA_MiU/<9:d.t0&0Kea^pCDPr:\g4W-_nkHrhPZJ<PEK7DpdqXpL-#IPLK/!(qq%L/8/,jeD?]="bTQH9o>V1<aX-nrj1_Mk]:]jm,u5Rhe1p+Y7mIMHF8b2>Xpa=>fHnJe*Q5+G,2Gg<?dJL>tluL[+=AWRHjG_+m?"^/(+kMBVdC[Jpp"dg[5!,<.6KAVp,Ec@O!**N1csmOiOgW<U0Jb1#0<tb:<Lk-Kq+^K72-@33pCM]2TVr-Hng;d^F'8;RmrJ`lb"JQVn]h@7K!cY^8Uu4Bp"$Hc%!#]aZS9,+7k%@]@VD=[a$i7RS3Q1mUcP3P&9m/QAb*k#XZ;e\*dn=Y+Bpd)*`:"=)=;n7\d80cZRZ@cr8VWlb?oEp;6.mBqftI45=1bPak\Z(noX486;k9",]l@?jl1P*\#Q5XG6C8f6P6I6%T?9K"N^>TDiX0tL0nN@%^l=09qpBdJTN7gC(@^nuCTo/jKsEJ2eNKK"8kr7!qJP)V4b0eE/NeaP*?UYs]TR*ekd1aD=1U0G*bd19aTmECfP163'RbX3H-HAla#Eo13Ii-;ahe+:beg<MRiI:[4uH=+U%Pp"WOCIAQW8*BT)A<%XgcrnlD6Z38Q77\aH;_P4.A`R':(@<mC:)8is*EU?tOI5DJ$fMED!4Se@aJQf3AEVkQKV#To3fuLC<K=^GaJ#"mJ^.39q?)S+0>7\gItH6Z$f(kin\0S<#Q<[XGb?`YS9=.BlSZm)L0NCp^Nc4Ti!dYap/K&3#pH#epMYtn/!r[9;Ak!%;1tma>?Du+.Y2]gI-?9e0#<`)V02oK'cCj?'hK-d74N?4g'&Bs!H-=Tq0,:il(cYHh,<?._YL2Tf_VKncSpDXL>o7CNfCfmnZT0jn1$hfP9M6b,d5<bQptM(0LNBC-tqqZ*Pb!A_Y[)EC:9;=;ebMsN_OW#Dd&GQ,%j0^IElWO7c%Wc1lsU.Ba#;I<^%BB%'s`,*+RPTX`>5VP4J2+CkbH_LUaNP();@A6D]#/LR4i[OSK+Ci]'n3`7#g"Fq,nf?g0>d3l*]jnm83+0O2q==ZD9p"b)4foo(JCd.d5+\7'r@iubDY^&^gAJ:hQ66"ImO^#%<qcS(,)SLM(-o"52=E=>4U&4jr*:5,q,Xb8g;86UiY&L@kh-"mkY_1%9l<[IWIS/?H6W#ipTc\GIga;4TmKS;*4LL24MCklbM>Uj5(!N'1=]g?GT9%P6iN(>B2^Z3TgLN%-'WU@n"AV-=DQgI4Yp_Y0EVE,?KAALe9,lG0[0F[:u,FHd8QF4A*.k\uW@=U&[pUI`[1G6A6pa+O)>D-IW=XQ8dl.I<B$,@%t\TLI)*@DmRo+5cP!g`j=nEQPcNp4<d+#8*tGuXLaDC^-TNYX,93%Of:$C4)`pTLdWrrt_bH25gkp>aUL._m&YD:*#-TY;-gQbpQ7"umPJ\\b*)?Z]UX"`h%n$PmG2R-F5ahYWLC^XM_3od?p(\_&a?FEf3c!-Ep[RL06.F7st,]fs`WE=.f8J_;P]WYhJL;FrqB#>D8j[XTSHWcY]*G_jmB3Of@3.#(F)A9AgZ<'_7@YB7u.['/>1?>I]@i:QhSrks:B9X?LK8:5,cbCU+NMDa9_hWb.9"%Z>2.\M&uZj_do$&%-k%pJGO:nAl^R?9;.BY:0ce9L&\[)m(=_bkN8-.eulZ/aZ[mi+q5m54,h<;a>g:V>a_c\@$94;mKdp>,"GYZ3VJQ`m[(?F@/`lc\hrnq&4;S%#nr29jsXB-,J>YMSh=omci77IHXWOQmg*>tSi)j<0)k:J']HUM_#c'ls#:ORiZ;qNG+(c8?;j+*fMUO3WR=HhGcU`<6q!WamVZc=f7g#P[)h\XN:i]TnE>HsA1c<Ws*&QrQabKe@&eO+Nhok$h-:8#[t?bpNSK][hp-Yq+a8&*:7Z`""9A;Z]oXf%!#(P!4VrBaF44(]4tL333qQ=5/.!3+<@XTjfVZ>ZKk;`:%m%JhnJp(@-s+\*se8L^t>/f#J8Ng3^eh\lLG_%iq1So-G+I4W[4'*Fl3Zp9dFi$"r>QW?iCh/ks\PgWC;U=0Eb`pBtDc[Gn(;W\T`QLaE-9G)&:N.NZMU+<nXEIB3R0igl!6K@0$!%QZP!A=.V6(i=!TeNLF&-)]Wj?p96ji"^*.>pRE$:R)3fUuu+U!Zl8ahY\r62ld(r5?+@$fiV#n9TA^j&3jBaq;[-W)`_O\?IT?nd46!d<0LfgOD9ECJs5]%CdoD_;$_0-(V7!(fuR-.f,b=VDEc\?#uBr2nGCV`ksi(9!s^!RU&H>c%.np9Z>2E-EUPic;3^Mh[!t!op3%Uf7ks\Q!QIjLGa@sN(OT7d&3btba?e#?-uR1m@+bGJIIDZ[_97atj3deik'b.;W7t,IIf^%%V\%arE!/N-;*jkj"kBdn4P'nHUeU'4no?J43p"8o%]W[#ED"o!c[IFnW(]ih6"WAUTg0ZOI!jH$_OoTLQk^gcgCF&=cj?XI[pp/gg"qk0&`Nse3I;IP.UO)km5LAZ;Z!EY!0-Len6!X?,)kL.UF+?4Oie)G/cY6^HLX4or(:2~>endstream++endobj++xref++0 12++0000000000 65535 f++0000000061 00000 n++0000000112 00000 n++0000000219 00000 n++0000000331 00000 n++0000000446 00000 n++0000000640 00000 n++0000000834 00000 n++0000000902 00000 n++0000001263 00000 n++0000001328 00000 n++0000004769 00000 n++trailer++<<++/ID++[<3cb1e6404bca482100e41159cff17f6c><3cb1e6404bca482100e41159cff17f6c>]++% ReportLab generated PDF document -- digest (opensource)++++/Info 8 0 R++/Root 7 0 R++/Size 12++>>++startxref++7842++%%EOFnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Oct_2026_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Sep_2026_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_amber_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_amber_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_cyan_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_cyan_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_green_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_green_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_indigo_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_indigo_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_lime_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_lime_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_orange_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_orange_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_pink_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_pink_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_red_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_red_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_teal_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_teal_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_violet_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_violet_69f82554.pdf and /dev/null differnnew file mode 100644BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_yellow_68186113.pdf differddeleted file mode 100644BBinary files a/public/Alex_Frison_Resume_Themed_yellow_69f82554.pdf and /dev/null differnnew file mode 100644@@@ -0,0 +1,171 @@++#!/usr/bin/env python3++"""Build plain, one-column résumés for applicant tracking systems, from the same content as the site and the designed++PDF (build_resume.py), in two versions aimed at different roles:++++ public/Alex_Frison_Resume_Engineering.pdf software engineering (full-stack, payments, identity, streaming)++ public/Alex_Frison_Resume_Data_Analyst.pdf data / BI / operations analyst++++The designed PDF is multi-column with icons; a tracking system reads its text layer column by column and scrambles it.++These have one column, real text in reading order, standard fonts and plain section names, so a parser gets every++line right. Facts only come from src/data/profile.json and src/content/; what differs per version is the headline, the++summary, which skills lead and how many bullets each role keeps.++++Run from the repo root: python3 resume/build_ats_resume.py++"""++from __future__ import annotations++++import json++import re++from pathlib import Path++++from reportlab.lib.colors import HexColor++from reportlab.lib.enums import TA_LEFT++from reportlab.lib.pagesizes import letter++from reportlab.lib.styles import ParagraphStyle++from reportlab.lib.units import inch++from reportlab.platypus import HRFlowable, KeepTogether, Paragraph, SimpleDocTemplate, Spacer++++ROOT = Path(__file__).resolve().parent.parent++INK, MUTED, RULE = HexColor("#111111"), HexColor("#444444"), HexColor("#999999")++++VERSIONS = {++ "engineering": {++ "file": "Alex_Frison_Resume_Engineering.pdf",++ "headline": "Software Engineer: full-stack TypeScript and Node.js, payments, identity and live streaming",++ "summary": (++ "Software engineer who builds complete products and keeps them running in production: platforms, APIs, "++ "payment systems, identity providers and live-streaming infrastructure. Rebuilt PowerChat's payments "++ "across five processors and designed its developer API platform; founder and sole engineer of OpenVibe, "++ "a network of seven connected apps under one login; ten years running my own company's analytics "++ "platforms and servers. Comfortable owning the whole thing: design, code, deployment and support."),++ "skills": [++ ("Languages", ["TypeScript", "JavaScript", "Python", "SQL", "Go", "PHP", "Lua", "HTML / CSS"]),++ ("Backend", ["Node.js", "Express", "REST and event-driven design", "Redis / BullMQ", "Prisma", "WebSockets"]),++ ("Data", ["PostgreSQL", "MySQL", "SQLite (WAL)", "MongoDB", "TimescaleDB"]),++ ("Frontend", ["React", "server-side rendering", "performance profiling (Core Web Vitals, CDP)"]),++ ("Payments and identity", ["Stripe Connect", "Square", "PayPal", "NMI", "Tipalti", "ledgers and idempotency",++ "OAuth2 / OIDC providers", "JWT (RS256) / JWKS", "SSO", "webhook signing"]),++ ("Streaming and realtime", ["WebRTC (mediasoup, werift)", "RTMP / SRT / WHIP", "HLS / DASH", "ffmpeg",++ "TURN / coturn", "live captions (whisper.cpp)"]),++ ("Infrastructure", ["Linux", "nginx", "systemd", "Docker", "GitLab CI", "Cloudflare", "Backblaze B2 / R2 / S3 APIs",++ "zero-downtime deploys"]),++ ("AI", ["tool-use agents", "Anthropic and OpenAI-compatible APIs", "local inference (whisper.cpp, Ollama)"]),++ ],++ "bullets": {"PowerChat": 3, "OpenVibe": 3, "Amazon/analytics": 2, "Amazon/operations": 1, "Verizon Wireless": 2,++ "founder": 2, "earlier": 1},++ "projects": ["openvibe-live", "openvibe-network", "openvibe-games", "irdr-autonomous-bot"],++ },++ "analyst": {++ "file": "Alex_Frison_Resume_Data_Analyst.pdf",++ "headline": "Data Analyst: SQL, Excel and Power Query, compliance reporting and automation",++ "summary": (++ "Data analyst with an engineer's toolkit. At Amazon ICQA I owned the SOX-compliance IRDR workflow, "++ "researched 1,000+ inventory discrepancies, built the SQL and Excel reporting, dashboards and leadership "++ "reports behind inventory accuracy, and turned the research into tooling other associates used every day, "++ "including an autonomous root-cause agent. Before that, ten years running my own company's analytics "++ "platforms (200,000+ live game servers tracked) and three years in sales, as an assistant manager "++ "coaching a team."),++ "skills": [++ ("Analytics", ["SQL", "Excel", "Power Query", "VBA", "dashboards and recurring reporting", "root-cause analysis",++ "SOX controls and audit trails", "inventory accuracy and defect analysis"]),++ ("Automation", ["Python", "JavaScript / TypeScript", "TamperMonkey userscripts", "tool-use AI agents",++ "Anthropic and OpenAI-compatible APIs"]),++ ("Data", ["PostgreSQL", "MySQL", "SQLite", "TimescaleDB", "MongoDB"]),++ ("Communication", ["leadership reports", "a network-wide quality recap deck", "technical writing and runbooks",++ "coaching and team leadership", "consultative sales"]),++ ("Also", ["full-stack web development (Node.js, React)", "Linux and servers", "payments and billing systems"]),++ ],++ "bullets": {"PowerChat": 2, "OpenVibe": 1, "Amazon/analytics": 3, "Amazon/operations": 2, "Verizon Wireless": 2,++ "founder": 2, "earlier": 1},++ "projects": ["irdr-autonomous-bot", "irdr-quality-recap", "amazon-userscripts"],++ },++}++++++def load() -> dict:++ profile = json.loads((ROOT / "src/data/profile.json").read_text())++ roles = sorted((json.loads(p.read_text()) for p in (ROOT / "src/content/experience").glob("*.json")),++ key=lambda r: r.get("order", 99))++ projects = {}++ for p in (ROOT / "src/content/projects").glob("*.mdx"):++ m = re.match(r"---\n(.*?)\n---", p.read_text(), re.S)++ fm = {}++ for line in (m.group(1).splitlines() if m else []):++ k, _, v = line.partition(":")++ if v.strip().startswith('"'):++ fm[k.strip()] = v.strip().strip('"')++ projects[p.stem] = fm++ return {"profile": profile, "roles": roles, "projects": projects}++++++def plain(s: str) -> str:++ """Plain characters for the parser: dashes and middle dots become what a keyboard types."""++ s = re.sub(r"\s*\u2014\s*", ", ", s).replace("\u2013", "-").replace("\u2192", "to").replace("\u00b7", "|")++ return (s.replace("&", "&").replace("<", "<").replace(">", ">")++ .replace("‘", "'").replace("’", "'").replace("“", '"').replace("”", '"'))++++++def bullets_for(role: dict, counts: dict) -> int:++ key = role["company"] if role["company"] in counts else (++ f"Amazon/{role.get('kind')}" if role["company"] == "Amazon" else role.get("kind", ""))++ if role["company"].startswith("OpenVibe"):++ key = "OpenVibe"++ return counts.get(key, counts.get(role.get("kind", ""), 2))++++++def build(version: str, data: dict) -> Path:++ v = VERSIONS[version]++ pr = data["profile"]++ out = ROOT / "public" / v["file"]++ st = {++ "name": ParagraphStyle("name", fontName="Helvetica-Bold", fontSize=18, leading=21, textColor=INK),++ "contact": ParagraphStyle("contact", fontName="Helvetica", fontSize=9.2, leading=12, textColor=MUTED),++ "headline": ParagraphStyle("headline", fontName="Helvetica-Bold", fontSize=10.5, leading=14, textColor=INK,++ spaceBefore=4),++ "h": ParagraphStyle("h", fontName="Helvetica-Bold", fontSize=10.5, leading=13, textColor=INK, spaceBefore=9,++ spaceAfter=2),++ "body": ParagraphStyle("body", fontName="Helvetica", fontSize=9.4, leading=12.4, textColor=INK, alignment=TA_LEFT),++ "role": ParagraphStyle("role", fontName="Helvetica-Bold", fontSize=9.8, leading=12.6, textColor=INK, spaceBefore=5),++ "meta": ParagraphStyle("meta", fontName="Helvetica-Oblique", fontSize=9, leading=11.6, textColor=MUTED),++ "bullet": ParagraphStyle("bullet", fontName="Helvetica", fontSize=9.3, leading=12.2, textColor=INK,++ leftIndent=12, bulletIndent=2),++ }++ rule = lambda: HRFlowable(width="100%", thickness=0.6, color=RULE, spaceBefore=1, spaceAfter=3) # noqa: E731++ story = [++ Paragraph(plain(pr["name"]), st["name"]),++ Paragraph(plain(" | ".join([pr["locationShort"].replace("Seattle / ", "") + " (Seattle area)", pr["phoneDisplay"],++ pr["email"], pr["siteDisplay"], pr["linkedinDisplay"], pr["githubDisplay"]])), st["contact"]),++ Paragraph(plain(v["headline"]), st["headline"]),++ Paragraph("SUMMARY", st["h"]), rule(), Paragraph(plain(v["summary"]), st["body"]),++ Paragraph("SKILLS", st["h"]), rule(),++ ]++ for group, items in v["skills"]:++ story.append(Paragraph(f"<b>{plain(group)}:</b> {plain(', '.join(items))}", st["body"]))++ story += [Paragraph("EXPERIENCE", st["h"]), rule()]++ for r in data["roles"]:++ n = bullets_for(r, v["bullets"])++ head = Paragraph(plain(f"{r['role']}, {r['company']}"), st["role"])++ meta = Paragraph(plain(" | ".join(x for x in [r.get("location", ""), f"{r['start']} to {r['end']}"] if x)), st["meta"])++ items = [Paragraph(plain(b), st["bullet"], bulletText="•") for b in r.get("bullets", [])[:n]]++ if r.get("stack") and version == "engineering" and n >= 2:++ items.append(Paragraph(plain("Stack: " + ", ".join(r["stack"])), st["meta"]))++ story.append(KeepTogether([head, meta, *items[:1]]))++ story += items[1:]++ story += [Paragraph("SELECTED PROJECTS", st["h"]), rule()]++ for slug in v["projects"]:++ p = data["projects"].get(slug, {})++ if p.get("title"):++ story.append(Paragraph(f"<b>{plain(p['title'])}</b>: {plain(p.get('summary', ''))}", st["bullet"],++ bulletText="•"))++ story += [Paragraph("EDUCATION", st["h"]), rule(), Paragraph(plain(pr["education"]), st["body"])]++ doc = SimpleDocTemplate(str(out), pagesize=letter, leftMargin=0.7 * inch, rightMargin=0.7 * inch,++ topMargin=0.6 * inch, bottomMargin=0.6 * inch, title=f"{pr['name']} - Resume",++ author=pr["name"], subject=v["headline"])++ doc.build(story)++ return out++++++if __name__ == "__main__":++ data = load()++ for name in VERSIONS:++ print(build(name, data).relative_to(ROOT))@@@ -52,7 +52,7 @@ SELECTED_PROJECTS = []# Metrics strip: pick by a keyword found in the label, in this order.# Falls back to the first four metrics if fewer than four match.--METRIC_KEYWORDS = ["shipped", "lines of production", "payment processors", "sox compliance", "years running"]++METRIC_KEYWORDS = ["shipped", "endpoints and pages", "payment processors", "sox compliance", "years running"]FULL_ROLES = 2 # PowerChat + OpenVibe get full treatment on page 1COMPACT_BULLETS = 2 # bullets per role on page 2@@@ -1279,6 +1279,9 @@ def main() -> None:(ROOT / "src/data/theme-pdf-manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")++ # the one-column versions for tracking systems (build_ats_resume.py) live next to these: never swept++ keep |= {p for p in (ROOT / "public").glob("Alex_Frison_Resume_*.pdf")++ if p.name in ("Alex_Frison_Resume_Engineering.pdf", "Alex_Frison_Resume_Data_Analyst.pdf")}clean_old_files("Alex_Frison_Resume_*.pdf", keep)print(f"\ncontent hash: {h} (filenames only change when the résumé content does)")print(f"text font: {FONT['label']}; icons: Font Awesome 4")nnew file mode 100755@@@ -0,0 +1,10 @@++#!/bin/sh++# Build the site and publish it to the OVH server, where alexfrison.net is served from since 2026-10-20 (nginx:++# /etc/nginx/sites-available/alexfrison.net.conf, root /var/www/alexfrison.net; Let's Encrypt via Cloudflare DNS-01).++# GitHub Pages (.github/workflows/deploy.yml) still builds on push but no longer has the domain.++set -eu++cd "$(dirname "$0")/.."++npm run build++rsync -a --delete dist/ openvibe-ovh:alexfrison.net-dist/++ssh openvibe-ovh 'sudo rsync -a --delete ~/alexfrison.net-dist/ /var/www/alexfrison.net/ && sudo chown -R www-data:www-data /var/www/alexfrison.net'++echo "deployed: https://alexfrison.net/"@@@ -13,7 +13,7 @@"bullets": ["Designed and shipped the Developer API platform end to end: PowerChat became an OAuth2 authorization server with scoped consent, token lifecycle, HMAC-signed webhooks and an inbound API, so third-party apps act as pseudo-platforms feeding chat, view counts, follows, subs, currency and tips into overlays. Re-implemented Streamlabs as the first official app on the new framework.","Rebuilt the payments program as one system: a provider-agnostic charge rail with five adapters (Stripe Connect, Square, PayPal, an NMI card vault and NowPayments crypto), append-only ledgers where double-crediting is structurally impossible, Tipalti payouts, recurring Pro and viewer-subscription billing, signed webhooks per provider and an admin reconciliation surface.",-- "Delivered 400+ commits and roughly 300k changed lines in the first five weeks across integrations, payments and Rumble connection work, backed by 240+ governed backend integration tests and 1,700+ frontend unit tests, with lint, typecheck and empty-database migration proofs in CI."++ "Kept a fast-moving rewrite safe to ship: 240+ governed backend integration tests and 1,700+ frontend unit tests across the integrations, payments and Rumble connection work, with lint, typecheck and empty-database migration proofs in CI."],"stack": ["TypeScript",@@@ -33,4 +33,4 @@"Docker","GitLab CI"]--}\\ No newline at end of file++}@@@ -24,6 +24,10 @@"name": "PostgreSQL / Prisma","level": "strong"},++ {++ "name": "Go",++ "level": "familiar"++ },{"name": "SQLite (WAL) / MySQL","level": "core"@@@ -45,4 +49,4 @@"level": "core"}]--}\\ No newline at end of file++}@@@ -36,7 +36,7 @@"githubDisplay": "github.com/OpenVibers","linkedin": "https://www.linkedin.com/in/fris/","linkedinDisplay": "linkedin.com/in/fris",-- "resumePdf": "/Alex_Frison_Resume_Sep_2026_69f82554.pdf",++ "resumePdf": "/Alex_Frison_Resume_Oct_2026_68186113.pdf","availability": "Open to pretty much anything — engineering, platform, analytics, ops, sales, whatever fits · remote or Pacific Northwest","status": {"now": "Contract software engineer at PowerChat, building its developer platform and payments program.",@@@ -1,15 +1,15 @@{-- "amber": "/Alex_Frison_Resume_Themed_amber_69f82554.pdf",-- "blue": "/Alex_Frison_Resume_Sep_2026_69f82554.pdf",-- "cyan": "/Alex_Frison_Resume_Themed_cyan_69f82554.pdf",-- "default": "/Alex_Frison_Resume_Sep_2026_69f82554.pdf",-- "green": "/Alex_Frison_Resume_Themed_green_69f82554.pdf",-- "indigo": "/Alex_Frison_Resume_Themed_indigo_69f82554.pdf",-- "lime": "/Alex_Frison_Resume_Themed_lime_69f82554.pdf",-- "orange": "/Alex_Frison_Resume_Themed_orange_69f82554.pdf",-- "pink": "/Alex_Frison_Resume_Themed_pink_69f82554.pdf",-- "red": "/Alex_Frison_Resume_Themed_red_69f82554.pdf",-- "teal": "/Alex_Frison_Resume_Themed_teal_69f82554.pdf",-- "violet": "/Alex_Frison_Resume_Themed_violet_69f82554.pdf",-- "yellow": "/Alex_Frison_Resume_Themed_yellow_69f82554.pdf"++ "amber": "/Alex_Frison_Resume_Themed_amber_68186113.pdf",++ "blue": "/Alex_Frison_Resume_Oct_2026_68186113.pdf",++ "cyan": "/Alex_Frison_Resume_Themed_cyan_68186113.pdf",++ "default": "/Alex_Frison_Resume_Oct_2026_68186113.pdf",++ "green": "/Alex_Frison_Resume_Themed_green_68186113.pdf",++ "indigo": "/Alex_Frison_Resume_Themed_indigo_68186113.pdf",++ "lime": "/Alex_Frison_Resume_Themed_lime_68186113.pdf",++ "orange": "/Alex_Frison_Resume_Themed_orange_68186113.pdf",++ "pink": "/Alex_Frison_Resume_Themed_pink_68186113.pdf",++ "red": "/Alex_Frison_Resume_Themed_red_68186113.pdf",++ "teal": "/Alex_Frison_Resume_Themed_teal_68186113.pdf",++ "violet": "/Alex_Frison_Resume_Themed_violet_68186113.pdf",++ "yellow": "/Alex_Frison_Resume_Themed_yellow_68186113.pdf"}@@@ -39,9 +39,11 @@ const pitchParts = linkify(profile.pitch, LINK_TERMS);<button type="button" class="btn" id="themed-pdf-toggle" data-magnetic aria-expanded="false" aria-controls="themed-pdf-swatches"><i class="fa-solid fa-paintbrush"></i> Themed PDF <i class="fa-solid fa-chevron-down themed-pdf-caret" aria-hidden="true"></i></button>++ <a href="/Alex_Frison_Resume_Engineering.pdf" class="btn" data-magnetic><i class="fa-solid fa-file-lines"></i> Plain: engineering</a>++ <a href="/Alex_Frison_Resume_Data_Analyst.pdf" class="btn" data-magnetic><i class="fa-solid fa-file-lines"></i> Plain: data analyst</a><button onclick="window.print()" class="btn" data-magnetic><i class="fa-solid fa-print"></i> Print this page</button></div>-- <p class="resume__pdf-note">The <strong>Download PDF</strong> is the canonical résumé — dense and ATS-safe. <strong>Themed PDF</strong> is the same content, pre-built in whichever color you pick below (every color is generated ahead of time, nothing renders in your browser).</p>++ <p class="resume__pdf-note">The <strong>Download PDF</strong> is the full, designed résumé. <strong>Themed PDF</strong> is the same content, pre-built in whichever color you pick below (every color is generated ahead of time, nothing renders in your browser). The two <strong>plain</strong> versions are one column with standard fonts, for applicant tracking systems: one aimed at engineering roles, one at data and analytics roles.</p><div class="themed-pdf-swatches" id="themed-pdf-swatches" hidden>{themePresets.map((p) => (
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
"build": "astro build",
"build": "astro build",
"preview": "astro preview --host",
"preview": "astro preview --host",
"vendor:fetch": "node scripts/fetch-vendor.mjs",
"vendor:fetch": "node scripts/fetch-vendor.mjs",
- "resume:pdf": "python3 resume/build_resume.py"
+ "resume:pdf": "python3 resume/build_resume.py && python3 resume/build_ats_resume.py"
},
},
"dependencies": {
"dependencies": {
"@astrojs/mdx": "^3.1.9",
"@astrojs/mdx": "^3.1.9",
new file mode 100644
new file mode 100644
@@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
+%PDF-1.4
+%���� ReportLab Generated PDF document (opensource)
+1 0 obj
+<<
+/F1 2 0 R /F2 3 0 R /F3 4 0 R
+>>
+endobj
+2 0 obj
+<<
+/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
+>>
+endobj
+3 0 obj
+<<
+/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
+>>
+endobj
+4 0 obj
+<<
+/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
+>>
+endobj
+5 0 obj
+<<
+/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<
+/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>> /Rotate 0 /Trans <<
+
+>>
+ /Type /Page
+>>
+endobj
+6 0 obj
+<<
+/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<
+/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>> /Rotate 0 /Trans <<
+
+>>
+ /Type /Page
+>>
+endobj
+7 0 obj
+<<
+/PageMode /UseNone /Pages 9 0 R /Type /Catalog
+>>
+endobj
+8 0 obj
+<<
+/Author (Alex Frison) /CreationDate (D:20261020184315-07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261020184315-07'00') /Producer (ReportLab PDF Library - \(opensource\))
+ /Subject (Data Analyst: SQL, Excel and Power Query, compliance reporting and automation) /Title (Alex Frison - Resume) /Trapped /False
+>>
+endobj
+9 0 obj
+<<
+/Count 2 /Kids [ 5 0 R 6 0 R ] /Type /Pages
+>>
+endobj
+10 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ] /Length 3015
+>>
+stream
+Gb!#]>>s9I(4OT53,_0-D9%fXR!i+YaUYY=&7B-`X7F])g3+!i>3Tp54>f?P^XFdXL&_\uC+:B<**rEr^[n"[Lq<A<r+D0+*F@4,pCThK?-aqaGPZE07OQf+^:]<f-O3"D*5tcf+"uPuV`Vhh&O$&/k&53@e=>EklPsQX'^bMFEPs6n9epuamu=WaGY?Z'e+MU#g*ce\O>n5@)MrGcl+uhD&u#;R&/rs+'.f#n<0O:jmW?jNs%_eLhN\f#AWh_/iq$CN':8Rc!8i+?;7eb.O)RZUdspr/b\Pr^UoU]U]7>NPA?(M^U@WT/GfMc-ca@V:F6+1feDL1+b2QJ[Q,`6rHo-;WY<.BVgKjg(Z1O>($R]UcLhpYY!F:+7CXL70+u9j#8f)e&%=P7P=g\MbQC/@S,0@o()[DH"VaTWg/tCW5Z,,Y#HH2*1h=Gr"rTF*+Er!IU&dJ\`O>s$^DZ7L[_V`Od]d`ls3VCF_%UI6@lEOh'j)/q^2_%"I+e_K8IRpllQHM^[email protected]=+un$-VI5#lpeMq9cQlJnV8a\o]XbW:.[)HR?CcF]))2+HRg5:QcGH"eE1A/l"(7mYu6s"+k%daU/nbXrB%"<<YeH"'rB&"La-5-d^EgmWQ^'k5FqVSL,<R1h4dm$`Dd5/>(8c#,"8Xe<*;FrBdso+G8_WaFHIi6&IGb!hfP,dgTf<Dml9\dkH*+AAOoq('.'0U<*;=:D^F^$E/H-.X@bh]qYVjcXt*.7CY);a*oDdF6^J9'b*b.#150M+P6J'2(GA`V_8kUeG-9/2;655P+[/XIQHDl\]UN,U5ht`25A'1]dTkjrX>J93;1j9FFC55FCo"[sOg::L$F50e3*TLg\$a@>;)l]AD%;I;/Wtgs$7^qGo6b]lLANVub@g,)hkt+"=1\SFOO`5q:;$i:m_eXH$RBWtKse!tk@sgDh<RTPGjVsiDJop/>5,l"f>G^3$#LrNe?JhZP(&TlFic,O&(QG;(!c$eh@$eX;[email protected]_hZ*!WTb=Ueh-E]@HLX-R)f5jrEe;sCi@c\B\!.PTii[Q[_FS(Tr=/??*B[O6/952[/qUp`VdTCu$L2u+YfgV*'o(@`QP6/:u99n:R&GN]VWJ<+i0h%"k>b(5&6+=I$]<;OZ$U);Xqj?d)]"*qe?-ZON(I"(IYZg/:-E,\Z6ep/0>TY%OYA\nIGg6.MdWo.6.X<L]?fNg?L-Rr4+1.@aQ1UmcR;sh'E)@hZOrMEVAAP0UZo"fIjhSd5ei1%FKgja8##s:haA3PZarGL**dVq_iL=6dT<ApO8S7W?rg6\gecr9(2mefJ=F2PS?094#!;Heib0KH0mo#%]I=BqF1K@CuhY%l0>?$Ip59<X0W\WIq/@r75GF@'Df&hBF,bP!XEH6>No&8eCLX=n,_lLKRQM[?E05`07CYbDA#*LbNrC"ue'(hSeC17;.+#.<WDYIglKPnH?[dLY>3T%pK)s5\GYu`BS<P"F*FqU$e;U$ZY`^AZ">qc@0G%f8BFs!^IR!Re8n1Z#.2tOt/=o12tNP%*(qDLQ.pB?O<^"UqDFubjhc'_B'*(Eqo27GUVR1ImO@iH<gVO=aKpiWft!iT>?C^.9-,J(f;Alc=d7fD:SVll!`#<Ze0M.TTn!Uq4UJK+*Y9VK)#Opo_5oD.IG>q:\.>VNC8M4m1QSE^GgTj!@*W8Bq0Zs="@QkEtekI<r=jgJ5o=f&^C+_*;W4TS5=UUsNH#u)o,H=f2?%E92g9c;("eR_T:?`-L8aJO\XR98,PYb8*`V'QrV(1k4#R/ZYM9J$o9p?50f:j,!k.IbcEod'<emh?<t1[lN>mLJ+)MrGg9QS.B3%VS\C1jTY\fEo-N2%Uh%a5Frhg5B['H%?0W<f^XU3ESH>:[email protected]#hqI1](.:P(3Z=3)FDNCU>LBPdq@+$^'tH5Te9I.02H(>Cd:EKD.Yb0@j;QAlUn::kCADRG5<o<p39BS0]aA5Y+Li"cC^IS<J[C[p`KfesS#(,U,"VL@Au_nS.,)IOR%MfR@+G3c0-#L!a3;c8M'7l/a?*=%M-pOE;!GSdTN)W=;]5<?T^0;ZR#7N+&+86EO7"-5*j6MrJaVjfRZ:\:2cC#XncSR)VC='&Ho!l"gI[/QseUFssd@^N;4%,\.!bMH`X<pj%dg(1[,36"3i&U#:i*4RN_AO&]LKlLb(=b1d^(T^pgm:3Sf#K3:A<oMCTt6/[uiCL.5W![_Ei#[kbaXe5XPN4JT0F,21[/;=Z>`e,tP>*!DmVRDV:4rMtdlZsRhQ#Rq+m05Y=W"TK1bu%(7LM-dacU9Q-Nn_eXr=C[5GC47B!5FUZ%kRs_o46)s(?j'0/t)e=*VsN3a(aCmD2@3n9M7Y"r/5&+[[G./_Fq!n]L.n@(//\Qbk.oYeW%MgZ6&5-.q70L*3cK*ir+U@I"*;Vn'bHhDpt_Q3'4L'7^#cI(R('8fCAQ,e!E\!9BBCq?T-62'92$BB!.M6R-63&Q#/et]@=l>>([hLnlf=DJDY1]kkRs*<me1&']IiSN%b=_bC]BoeCU8qFPE5#nQ3S$"k's[nLh_L*&[jfI]6/W^2r[4a(Vg0q=-RskT095L(M#@m+?U=.Pt;IZfjVmM=CkRH#!qsh/##[AeT^fShWLQNY-K%TDQ;l3W0KIkGt&k5IK;U>F(@sTYq!R2\uo"Q6[+Q/d?J3O7pY/?R!pP!qEUFQAO%qQP[4gpRo"Ka>.\5_rgd#AX8MI9](*iMGrR]7jT=Ymk]sY3nJ$fg8k=?0(i38ipYV52N%@72bMYa.2,\^?WlIq9AjQG0p]*<H"Ao6U[[:i?gZto@NYB6Aj-oC'l#RmcU4SmY4_jT)DI7lmaeY\gV!-t6n%h(4UoW)T>%d=o'O'=TpMY]f[+6I^]Kk<f@d;j5(%24p4KmI'W?fg#rO3ApWTkM'hf(e$8f(A$j!'mi3uZ5<>\CaOZ8:s_/88"P5[Qhgu\?g^UOS<j4:@*+<Cs.0TIF,,*qLhIT=bek5X/a8)j~>endstream
+endobj
+11 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ] /Length 1880
+>>
+stream
+Gb!#\?#poC'Rf_Z\506]!b(m[,Yo9;e/:>k!7uY_MnCQB's4j*ZmK>Sj*RT`_b&XbBBmcM4L"1TY-dBKF7'VP2=ZR@H^k\=cKNf(6rAkSF,E#<%a?V?4?NCa'Q*:)p2KSuh/'GP!9Y<G*r\pugJ>.86Ek,>^X_Ps/49%F%K%Bd[i.COrci+YY`:/&=%ok&2tW!)9QEc>Sf;RXEm0@F?DB`p&YHJ*4(1+OgL"Xf2pHgE/,XqL\#YJaR%[brCU:Tj5/1H9Y?YY.#%<M=ArWp)X%?&"52/;n[5[;cZ$@%p78^N+Q*GmYZNS(!rc\A,poioB,g;"BSd0B0ogFF%HNSDXlZg:rOse1'XVTtq:'m"@,'H1T4C(]B2figkDJY;AXaFH*^0D?O'Q]iON5ADXiloG04(a>g7U&N2WYNRI>V]l+>D/#>*$\23LiCUPa($KY5c,0_$cQ)(OGsT4HbNkt'[Reo1H\[sCZE#fIKL!]S9humEL)`Y;odeK-c:!JSf1lc;MsaCeKZ1nQWfl!OGDPbA=#VT>%t=MWQlSQMi3<=dQ(Am>>eZ9Z;;(QQo=7aRbO3$jrq36.!JlK8Ek2$W!#Nf6>-4I9<?M_:Y."mbgJ2eKW<Gm9&WpZS>gXj/h]^0$g]]WONgYSkM`:;Q\pWphVf+DZk1Y/eP*-:Looj4o*e*.)&=lR't'LT>-lo[N?g?F(+eKV\Tn1q?_^d?h"pT?i>aXn>4XW5LXde]2SI_d,W:mM'JTrS-bBH(R;iRZ*St&R#&TT9/rXoUL;28b+ZWs_LlBHCUl>83Db`8_G==B4#LVLNI`gcbI_PnI5m^2`A*-3&\d`:U[p+Ld0Q;+BcoHK%6OC52Tfc-m(m'r:&SGa&$;I`k$EiCZS0te4kZ+W/hk:OO5UW3cgOtgEEU4A5&l.2_TKtc"pYD%2g=;5=>=Lh$q2_a$1P7&/1uiSU"<NVQU=6sdoEIs4&@%Z?">(^G^__<DV3<@8MW:(rZ?=,YH``VKJ6(@KUMe&PmJ`fc5:`NC+T<:pQh2nQ[cVooGN(Eo(9/E.EHH>oM+uMG@Nc_SK[-)%Z*RuK9Qk8e)B4gNF.IP%a6gJWguYZ<[\EKSZM5b/1S@kfQD`))877SCg,M^3s2OA<:9['XUI8IE-C1<9)L)C&Kslr3]u&U;G,ushh=n=Y:r3C3G7E[o0YRUrc<gC>hheu":)l.i0qP^WC=W*"4IV8Uk[nl6%S,!*\bh#M$g>tF(Z5%@npg6(8;3NhXO5qW,Y?H4_t<pMq0kB:gQ\!BeZR?!@Gs<RFCFECE]0VJeuh2GL#mEVb>^X)QfH]iBE29Xm%$e;gGiIM%N5eMk78d,dTonjap<dHHZp^FgT1)@qTjII.D/C1HSt7ZI/ETN&/C#2W@e_sL,&=D(e[gSr":j0H4D<&]&%.ni;Go>&9.YG0IbcqS%P-MBFFA$$1-Pjk=J]]!F9M%aq5tZ82,WJL_VOr'I=\ao@o]1_DgLWUB>iF"OLijHG%Qm`r40f:`EaAE@:g"32!BjqRSD1])pI"b$@Y2Gno`ihLVCbba@9Mhnkl8+RM!;`25N/7XZaMd>VXUm8k47DGA)fN%M*fn-ng'gB_U!kC*[#!=HP/GcQIkV2N`V@6ejXI^MQ!Be;u)aa"A)<D1-pfKG0)=,<&PTL=e-$b>'.?A3WN&A=<3NmDT5_qE$%qhc9gr@PMXMb:F)q]3p;Q]B:6p'n.1BkX&8lBP,lg20HsMpT.95[C.s@07:9VZ&LglU&FKR#DDUi+T2-k\Y;;,K%E;/9D-Yg?q,%fUCc6K7`-+%&Uf`9+*R93!-V$7r3c4@O\MThF"6P@:1=-7:@<sq_jpQ:U+B+FL`o$PM[GbWs^#$6HBH&^DAjjq?LK6m(<~>endstream
+endobj
+xref
+0 12
+0000000000 65535 f
+0000000061 00000 n
+0000000112 00000 n
+0000000219 00000 n
+0000000331 00000 n
+0000000446 00000 n
+0000000640 00000 n
+0000000834 00000 n
+0000000902 00000 n
+0000001249 00000 n
+0000001314 00000 n
+0000004421 00000 n
+trailer
+<<
+/ID
+[<4db2b9d6052cdbd87e4d08758dd69ff6><4db2b9d6052cdbd87e4d08758dd69ff6>]
+% ReportLab generated PDF document -- digest (opensource)
+
+/Info 8 0 R
+/Root 7 0 R
+/Size 12
+>>
+startxref
+6393
+%%EOF
new file mode 100644
new file mode 100644
@@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
+%PDF-1.4
+%���� ReportLab Generated PDF document (opensource)
+1 0 obj
+<<
+/F1 2 0 R /F2 3 0 R /F3 4 0 R
+>>
+endobj
+2 0 obj
+<<
+/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
+>>
+endobj
+3 0 obj
+<<
+/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
+>>
+endobj
+4 0 obj
+<<
+/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
+>>
+endobj
+5 0 obj
+<<
+/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<
+/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>> /Rotate 0 /Trans <<
+
+>>
+ /Type /Page
+>>
+endobj
+6 0 obj
+<<
+/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<
+/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>> /Rotate 0 /Trans <<
+
+>>
+ /Type /Page
+>>
+endobj
+7 0 obj
+<<
+/PageMode /UseNone /Pages 9 0 R /Type /Catalog
+>>
+endobj
+8 0 obj
+<<
+/Author (Alex Frison) /CreationDate (D:20261020184315-07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261020184315-07'00') /Producer (ReportLab PDF Library - \(opensource\))
+ /Subject (Software Engineer: full-stack TypeScript and Node.js, payments, identity and live streaming) /Title (Alex Frison - Resume) /Trapped /False
+>>
+endobj
+9 0 obj
+<<
+/Count 2 /Kids [ 5 0 R 6 0 R ] /Type /Pages
+>>
+endobj
+10 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ] /Length 3349
+>>
+stream
+Gb!#]>Bedl&q801W4+auJfq59o<K2p@jZBY!&N:ga"NG%!l^=+dqY;4P93"r[4^*bR!?Ya:F:-J(8C-g<Rk0)9.g'_mAh$:S^-Km)jf><bl`!ubq@6ol$WKiY8;*PA?V&`K=9Ms`YWU/g$7V4!U[VejU3'jf&q6j*olTF\]9)@gZ=W[7ZmUBDfhAGj;#p(dJR?&m0!@T8F-il[D[5;C$0nG,1Jh0KXs!V()F:cWB(BMV,<(:+%LEo1m>eD8<V=tgq9Ab\CGHjE7O1`)P,i+J6N)DreFh^X)0YTVS)fa"qJA4I[DrT'u(6BfDHe?X_^(&L1Z](m"b%.RJ[XLUs9HP4r?K:f?IXhm1s&M/?S"i'ppl&V"8+7-;s+B$M#T[A4CgQ1uG*Lk34QW,SHa+.:4ttAIbPG'?e>P.eN6PRZ78.$L]6O##:*8c3%,>##DY$]uQ"*@E%\:=/5#PLEuR)oKm#Zf(e_n^*ruQrq[hD&'*0?1,rGOd;7\p]j[s*jp)Xi[3;4J?FKo@F!gsn$imD2MACISq&'2KBdA\6<%C3-9P3+..qP03=im%QSg884Kh:r.W0.fd,@LYM@g$Ie4kU%@L0_70N/[5G%q!)h+$-GIfr--Pir^n&-dL&GCsl:gJqgGY>3bWMR$Us8#nih1=[iFE!N16!b>`It/`)gs\4i\o+eJGiSW$MTbFr^RqC`P]4Foh"T1h]J4JW%YP/SS18@5eQ[sU9T=UElL,gnJ.?_m1U4f4OI&kS5&8\^A'ZPt?d>ij"N6Y`/o^PcXoP-l(Q`e7[,okC+]e9>k4PK#)l>pC+]&7=Nd&/bs%DO;1<51q[F_=9]'pLP6lla,Th4ABflo5P?9qSuB#h]lk,G&hjg4!_$8nG`$\l.&r.D&9.6JQ48mj86jDdA9C"\QI"UnEdii3kACt*;WCbM<aq`@ocjkB-/`HqGlLo`O/("\T$bAIKe+USfRAiVlj/%fS!1&=./i1%&dt.q<(^%FW?jAcfPudN,@&5FiYE'YV\diae8V^WqOsJfS0;;_lAc<0iJWm%Wmn@]8,#$Z0G""Z,`YUQ'nJ7f(E)`qhCFKAaSXDj66'r'lL'_WQn6ZRYS/l4ESuWdfXmu_p>+N)m'aZ58[<J4Fj>#p-sf586U$k6!C%^9$IN*,b?T06-(;Q2N8Ho3W)[&KLS@ci:u[8k1/MSFH;LWk(h.-H+H`%FYO)<WZ&Xm^mRK78/*d_0G0YdEqCQK88PFtjNtN>$-+;+OU3%cUq`ul":gDi"i#=%LYe)4ZYBSrmjDc.]%kgf"f_9:%`_7Um9Ukcl8AQi4+jonH\^0mYu9kQ(sW@h6att+Mq7pU9s_FKYt;H[RBbQ--U`#:+2`aKkpA0NVl[B<"2j>ncEFBZe8,Jt9HPo7W%8aO1L-Q3Aetp\6(CR*AcDT!*H$019ba#=4gt'L-W4j<)+:Q-@0UC&]ehm.Ra,nQS=d\>P6YrRNuK^h,ld3BpL1]L,f4chN!kV7>n,&T=O7nP)+Q1NK?/Y$MqoQi?R20t7]-)=l]fWSMq"ccUN<a(mr_7jN<`'W\(uPBShcc$1"Rfg2=_/s^p7@[UVs7@8jgqaKfR4fd]d8uKM2@QP9P\^`gfVp=EsCF8<GH#,[taI[&rcf2>ZqE?Ct"@kO6FRLP37bdHis8h5>":>\VKi9u2nfqZdg>?k];4R5t"V$Mq8DSO'k;*'ZJ:etfuH6`ZASW\ftq$6Nll;8/iLX5+"Wd!'&EO(!N.Mfr`/^:(!sX?q\>]<rYQ+%,CS3uD(6&G9O^Frq&02ERb5M]qks,3>,MY/3$7207HW]`]?tgh4O8F)Ia<$gEKb)3\#o6/Yc-n;H6$<jpF;cds"`.J"73!c)EZ3]Ii/#j!e&hStIM5VB3/j$p"Qfe/./"=[cbi&Q?g6UamH\[C<cL$"efffsFB,25#P9]j'(CtX1YGs#Gs#ONPC$fr:qj>Pfr'h)ksM_&5ne]TftF><*M&8-b)*2$\9,W6O$7?*B62K$3K47*_f-5$\7A*5bo7QN>m=+\d,;(A4d_)M9`!q@\UZ/gc(^b!hRFS??pT*.m=Lr`'9UELJj'#q0qGRQ)@a9b>GMCLI$1'(TIJOMQj&p-d"&Qt@NGuFG$P>"8[NcqAEf%W8#"rIB!+\udDmR/*rX,M2GOZ^<KNT+E!5QK(=Psh+1m&X8:j5d6\"hrr0JAp0'GRLLN@u:P!!rt%RE]9qR'AK^2#*bF4+o_>G<`TaN)cKB0bYBXX,E"75/A^+q4JuTKWEOV9#Jp_,@0%kJ5X`e3bB4piGmHp2IGD.<Ho4"c(ED6I["G,CX4_OjSK99MRZ?5shF(3@P1#0F#<5W)[H:=1'7o4b`.dN1g>1>pBuudb%U9^s(8n29riBmobY!3J3eJ3>%a$MVng$'Ba**_FH*:e%?pNU2kh\5ASi4i'Ach'$JgGC`3sC?N<j$A.;8&7eDMtJH:<fZp@rtl[:lRH91dn->^bqu!oltL*P&Ba1/(+(i-9'rgi5pUsHI$8.bX^4-\K1=`*L*=u;bs8Z5B+c5A,iW-Rp+,Xhf-cZ4l?eTc0$;nDore/>.sW1J=4G.h&3D>.QZS=X/H,X*VZ/:YPp-T6+]hY+L,VsXph$_!:6agKU<mr5)kc9h;M/#rPMsnk1p,c;Y1`h4e?Jtr,r6W>JFT5@db8DPXo%bliM%q&Y<k2-TbPuU?XX595S*cR/;*$2S"V^2%s)ek"B68*=HkCdMN^D-gC>[%3?VX%'`X$q;+Gj9L/H[+@nJ_(QC0$Rgnnk!&ObD1">H3S5u]0eO"/c$r#]pU&Eu4$PqFtPHRr>QE3?8_Ca;MZ#nMJf?A!E\_%J:No5EiV8R5Yp.X1)$+W$tWQ!/W\l]Pc/W];>95VdZNg:fN%>WaABFkI.HY=VBKA4HqD-JGbBm.P%?'p8`,/$68Cm3JV;4itjVThup]p1s7WdLE0<r-Z8QM6c56)%16j1t8V$]p3,=`?ZEg"J*'&.dhZ0QfuE"$Ds%66#J>ONo1IJG][P\(LK9;iBR[K4*B3(.f:pYM.k?/5[,t?#>q&C4T!C14Xok9&3PK@rFi3M/B5k.S(4$XGj7/$a`Ek<jY"0Jt1;Z15QKXYhbpq"IemH@GoI9`2rH%q6S3M9rapdVAH)m@[s/]RE8CBEd(Y`R$H=,cRTL*<ZEFGbH/l8;1Onld:*J@5%+WHVMNSl";a&(nKD:c\RLGacq6\l_&f<^^_`jfeNShA?:L6#d\f4i.0\F:dLL%C^s9sQ@:!`/j`,,fS)rQgO=OH:ec(6<[s0ZRpcFRI+7m\66NiidEF%-r&?j/m8'C;-rP[@GKqKROgGcIs6XTBXME@J$n3iT0Jr<t5%Gh)Vc2~>endstream
+endobj
+11 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ] /Length 2981
+>>
+stream
+Gb!#]flGh,(4FM1gnu%aN229Zih`8Xfh%]S@RX2SI`Mmb66b,/_#U->FnNf$\KE3!e*\oh;5tV6CbF@bQZLFU!p\bmn@t`Wk^6,Y?Nh-s1ga/-n+:=C9pbGf?X<kXbJ=`#O+Yp,&0:8Q'+XR$R(iQ8<\H./YT"[:@F,G#pZU+f*af84IPc*i\iZ\]3gQ>oD2[PG6I$dM$J2#qLVcT?LU.NCb";GWF?@)5r(Dr')(T-<=gVioNEW(DEg[Y_L&49k6CLYKi8qLOCf6i$J_u91IFk3_;<a7*?N*:_heE'33kDl5(q40>$O9%FHTQ.C^/FboMsSGERDg`rVn.hRWA"qeU*i[Q(/9b5Ue>64gX^4>[email protected]>lZL,!G2:ka?iBrWG%'R010mGKHjShe[IGl&'/[j#MRBnVCA_MiU/<9:d.t0&0Kea^pCDPr:\g4W-_nkHrhPZJ<PEK7DpdqXpL-#IPLK/!(qq%L/8/,jeD?]="bTQH9o>V1<aX-nrj1_Mk]:]jm,u5Rhe1p+Y7mIMHF8b2>Xpa=>fHnJe*Q5+G,2Gg<?dJL>tluL[+=AWRHjG_+m?"^/(+kMBVdC[Jpp"dg[5!,<.6KAVp,Ec@O!**N1csmOiOgW<U0Jb1#0<tb:<Lk-Kq+^K72-@33pCM]2TVr-Hng;d^F'8;RmrJ`lb"JQVn]h@7K!cY^8Uu4Bp"$Hc%!#]aZS9,+7k%@]@VD=[a$i7RS3Q1mUcP3P&9m/QAb*k#XZ;e\*dn=Y+Bpd)*`:"=)=;n7\d80cZRZ@cr8VWlb?oEp;6.mBqftI45=1bPak\Z(noX486;k9",]l@?jl1P*\#Q5XG6C8f6P6I6%T?9K"N^>TDiX0tL0nN@%^l=09qpBdJTN7gC(@^nuCTo/jKsEJ2eNKK"8kr7!qJP)V4b0eE/NeaP*?UYs]TR*ekd1aD=1U0G*bd19aTmECfP163'RbX3H-HAla#Eo13Ii-;ahe+:beg<MRiI:[4uH=+U%Pp"WOCIAQW8*BT)A<%XgcrnlD6Z38Q77\aH;_P4.A`R':(@<mC:)8is*EU?tOI5DJ$fMED!4Se@aJQf3AEVkQKV#To3fuLC<K=^GaJ#"mJ^.39q?)S+0>7\gItH6Z$f(kin\0S<#Q<[XGb?`YS9=.BlSZm)L0NCp^Nc4Ti!dYap/K&3#pH#epMYtn/!r[9;Ak!%;1tma>?Du+.Y2]gI-?9e0#<`)V02oK'cCj?'hK-d74N?4g'&Bs!H-=Tq0,:il(cYHh,<?._YL2Tf_VKncSpDXL>o7CNfCfmnZT0jn1$hfP9M6b,d5<bQptM(0LNBC-tqqZ*Pb!A_Y[)EC:9;=;ebMsN_OW#Dd&GQ,%j0^IElWO7c%Wc1lsU.Ba#;I<^%BB%'s`,*+RPTX`>5VP4J2+CkbH_LUaNP();@A6D]#/LR4i[OSK+Ci]'n3`7#g"Fq,nf?g0>d3l*]jnm83+0O2q==ZD9p"b)4foo(JCd.d5+\7'r@iubDY^&^gAJ:hQ66"ImO^#%<qcS(,)SLM(-o"52=E=>4U&4jr*:5,q,Xb8g;86UiY&L@kh-"mkY_1%9l<[IWIS/?H6W#ipTc\GIga;4TmKS;*4LL24MCklbM>Uj5(!N'1=]g?GT9%P6iN(>B2^Z3TgLN%-'WU@n"AV-=DQgI4Yp_Y0EVE,?KAALe9,lG0[0F[:u,FHd8QF4A*.k\uW@=U&[pUI`[1G6A6pa+O)>D-IW=XQ8dl.I<B$,@%t\TLI)*@DmRo+5cP!g`j=nEQPcNp4<d+#8*tGuXLaDC^-TNYX,93%Of:$C4)`pTLdWrrt_bH25gkp>aUL._m&YD:*#-TY;-gQbpQ7"umPJ\\b*)?Z]UX"`h%n$PmG2R-F5ahYWLC^XM_3od?p(\_&a?FEf3c!-Ep[RL06.F7st,]fs`WE=.f8J_;P]WYhJL;FrqB#>D8j[XTSHWcY]*G_jmB3Of@3.#(F)A9AgZ<'_7@YB7u.['/>1?>I]@i:QhSrks:B9X?LK8:5,cbCU+NMDa9_hWb.9"%Z>2.\M&uZj_do$&%-k%pJGO:nAl^R?9;.BY:0ce9L&\[)m(=_bkN8-.eulZ/aZ[mi+q5m54,h<;a>g:V>a_c\@$94;mKdp>,"GYZ3VJQ`m[(?F@/`lc\hrnq&4;S%#nr29jsXB-,J>YMSh=omci77IHXWOQmg*>tSi)j<0)k:J']HUM_#c'ls#:ORiZ;qNG+(c8?;j+*fMUO3WR=HhGcU`<6q!WamVZc=f7g#P[)h\XN:i]TnE>HsA1c<Ws*&QrQabKe@&eO+Nhok$h-:8#[t?bpNSK][hp-Yq+a8&*:7Z`""9A;Z]oXf%!#(P!4VrBaF44(]4tL333qQ=5/.!3+<@XTjfVZ>ZKk;`:%m%JhnJp(@-s+\*se8L^t>/f#J8Ng3^eh\lLG_%iq1So-G+I4W[4'*Fl3Zp9dFi$"r>QW?iCh/ks\PgWC;U=0Eb`pBtDc[Gn(;W\T`QLaE-9G)&:N.NZMU+<nXEIB3R0igl!6K@0$!%QZP!A=.V6(i=!TeNLF&-)]Wj?p96ji"^*.>pRE$:R)3fUuu+U!Zl8ahY\r62ld(r5?+@$fiV#n9TA^j&3jBaq;[-W)`_O\?IT?nd46!d<0LfgOD9ECJs5]%CdoD_;$_0-(V7!(fuR-.f,b=VDEc\?#uBr2nGCV`ksi(9!s^!RU&H>c%.np9Z>2E-EUPic;3^Mh[!t!op3%Uf7ks\Q!QIjLGa@sN(OT7d&3btba?e#?-uR1m@+bGJIIDZ[_97atj3deik'b.;W7t,IIf^%%V\%arE!/N-;*jkj"kBdn4P'nHUeU'4no?J43p"8o%]W[#ED"o!c[IFnW(]ih6"WAUTg0ZOI!jH$_OoTLQk^gcgCF&=cj?XI[pp/gg"qk0&`Nse3I;IP.UO)km5LAZ;Z!EY!0-Len6!X?,)kL.UF+?4Oie)G/cY6^HLX4or(:2~>endstream
+endobj
+xref
+0 12
+0000000000 65535 f
+0000000061 00000 n
+0000000112 00000 n
+0000000219 00000 n
+0000000331 00000 n
+0000000446 00000 n
+0000000640 00000 n
+0000000834 00000 n
+0000000902 00000 n
+0000001263 00000 n
+0000001328 00000 n
+0000004769 00000 n
+trailer
+<<
+/ID
+[<3cb1e6404bca482100e41159cff17f6c><3cb1e6404bca482100e41159cff17f6c>]
+% ReportLab generated PDF document -- digest (opensource)
+
+/Info 8 0 R
+/Root 7 0 R
+/Size 12
+>>
+startxref
+7842
+%%EOF
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Oct_2026_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Oct_2026_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Sep_2026_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Sep_2026_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_amber_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_amber_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_amber_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_amber_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_cyan_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_cyan_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_cyan_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_cyan_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_green_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_green_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_green_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_green_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_indigo_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_indigo_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_indigo_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_indigo_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_lime_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_lime_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_lime_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_lime_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_orange_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_orange_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_orange_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_orange_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_pink_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_pink_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_pink_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_pink_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_red_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_red_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_red_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_red_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_teal_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_teal_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_teal_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_teal_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_violet_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_violet_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_violet_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_violet_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_yellow_68186113.pdf differ
Binary files /dev/null and b/public/Alex_Frison_Resume_Themed_yellow_68186113.pdf differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/Alex_Frison_Resume_Themed_yellow_69f82554.pdf and /dev/null differ
Binary files a/public/Alex_Frison_Resume_Themed_yellow_69f82554.pdf and /dev/null differ
new file mode 100644
new file mode 100644
@@ -0,0 +1,171 @@
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+"""Build plain, one-column résumés for applicant tracking systems, from the same content as the site and the designed
+PDF (build_resume.py), in two versions aimed at different roles:
+
+ public/Alex_Frison_Resume_Engineering.pdf software engineering (full-stack, payments, identity, streaming)
+ public/Alex_Frison_Resume_Data_Analyst.pdf data / BI / operations analyst
+
+The designed PDF is multi-column with icons; a tracking system reads its text layer column by column and scrambles it.
+These have one column, real text in reading order, standard fonts and plain section names, so a parser gets every
+line right. Facts only come from src/data/profile.json and src/content/; what differs per version is the headline, the
+summary, which skills lead and how many bullets each role keeps.
+
+Run from the repo root: python3 resume/build_ats_resume.py
+"""
+from __future__ import annotations
+
+import json
+import re
+from pathlib import Path
+
+from reportlab.lib.colors import HexColor
+from reportlab.lib.enums import TA_LEFT
+from reportlab.lib.pagesizes import letter
+from reportlab.lib.styles import ParagraphStyle
+from reportlab.lib.units import inch
+from reportlab.platypus import HRFlowable, KeepTogether, Paragraph, SimpleDocTemplate, Spacer
+
+ROOT = Path(__file__).resolve().parent.parent
+INK, MUTED, RULE = HexColor("#111111"), HexColor("#444444"), HexColor("#999999")
+
+VERSIONS = {
+ "engineering": {
+ "file": "Alex_Frison_Resume_Engineering.pdf",
+ "headline": "Software Engineer: full-stack TypeScript and Node.js, payments, identity and live streaming",
+ "summary": (
+ "Software engineer who builds complete products and keeps them running in production: platforms, APIs, "
+ "payment systems, identity providers and live-streaming infrastructure. Rebuilt PowerChat's payments "
+ "across five processors and designed its developer API platform; founder and sole engineer of OpenVibe, "
+ "a network of seven connected apps under one login; ten years running my own company's analytics "
+ "platforms and servers. Comfortable owning the whole thing: design, code, deployment and support."),
+ "skills": [
+ ("Languages", ["TypeScript", "JavaScript", "Python", "SQL", "Go", "PHP", "Lua", "HTML / CSS"]),
+ ("Backend", ["Node.js", "Express", "REST and event-driven design", "Redis / BullMQ", "Prisma", "WebSockets"]),
+ ("Data", ["PostgreSQL", "MySQL", "SQLite (WAL)", "MongoDB", "TimescaleDB"]),
+ ("Frontend", ["React", "server-side rendering", "performance profiling (Core Web Vitals, CDP)"]),
+ ("Payments and identity", ["Stripe Connect", "Square", "PayPal", "NMI", "Tipalti", "ledgers and idempotency",
+ "OAuth2 / OIDC providers", "JWT (RS256) / JWKS", "SSO", "webhook signing"]),
+ ("Streaming and realtime", ["WebRTC (mediasoup, werift)", "RTMP / SRT / WHIP", "HLS / DASH", "ffmpeg",
+ "TURN / coturn", "live captions (whisper.cpp)"]),
+ ("Infrastructure", ["Linux", "nginx", "systemd", "Docker", "GitLab CI", "Cloudflare", "Backblaze B2 / R2 / S3 APIs",
+ "zero-downtime deploys"]),
+ ("AI", ["tool-use agents", "Anthropic and OpenAI-compatible APIs", "local inference (whisper.cpp, Ollama)"]),
+ ],
+ "bullets": {"PowerChat": 3, "OpenVibe": 3, "Amazon/analytics": 2, "Amazon/operations": 1, "Verizon Wireless": 2,
+ "founder": 2, "earlier": 1},
+ "projects": ["openvibe-live", "openvibe-network", "openvibe-games", "irdr-autonomous-bot"],
+ },
+ "analyst": {
+ "file": "Alex_Frison_Resume_Data_Analyst.pdf",
+ "headline": "Data Analyst: SQL, Excel and Power Query, compliance reporting and automation",
+ "summary": (
+ "Data analyst with an engineer's toolkit. At Amazon ICQA I owned the SOX-compliance IRDR workflow, "
+ "researched 1,000+ inventory discrepancies, built the SQL and Excel reporting, dashboards and leadership "
+ "reports behind inventory accuracy, and turned the research into tooling other associates used every day, "
+ "including an autonomous root-cause agent. Before that, ten years running my own company's analytics "
+ "platforms (200,000+ live game servers tracked) and three years in sales, as an assistant manager "
+ "coaching a team."),
+ "skills": [
+ ("Analytics", ["SQL", "Excel", "Power Query", "VBA", "dashboards and recurring reporting", "root-cause analysis",
+ "SOX controls and audit trails", "inventory accuracy and defect analysis"]),
+ ("Automation", ["Python", "JavaScript / TypeScript", "TamperMonkey userscripts", "tool-use AI agents",
+ "Anthropic and OpenAI-compatible APIs"]),
+ ("Data", ["PostgreSQL", "MySQL", "SQLite", "TimescaleDB", "MongoDB"]),
+ ("Communication", ["leadership reports", "a network-wide quality recap deck", "technical writing and runbooks",
+ "coaching and team leadership", "consultative sales"]),
+ ("Also", ["full-stack web development (Node.js, React)", "Linux and servers", "payments and billing systems"]),
+ ],
+ "bullets": {"PowerChat": 2, "OpenVibe": 1, "Amazon/analytics": 3, "Amazon/operations": 2, "Verizon Wireless": 2,
+ "founder": 2, "earlier": 1},
+ "projects": ["irdr-autonomous-bot", "irdr-quality-recap", "amazon-userscripts"],
+ },
+}
+
+
+def load() -> dict:
+ profile = json.loads((ROOT / "src/data/profile.json").read_text())
+ roles = sorted((json.loads(p.read_text()) for p in (ROOT / "src/content/experience").glob("*.json")),
+ key=lambda r: r.get("order", 99))
+ projects = {}
+ for p in (ROOT / "src/content/projects").glob("*.mdx"):
+ m = re.match(r"---\n(.*?)\n---", p.read_text(), re.S)
+ fm = {}
+ for line in (m.group(1).splitlines() if m else []):
+ k, _, v = line.partition(":")
+ if v.strip().startswith('"'):
+ fm[k.strip()] = v.strip().strip('"')
+ projects[p.stem] = fm
+ return {"profile": profile, "roles": roles, "projects": projects}
+
+
+def plain(s: str) -> str:
+ """Plain characters for the parser: dashes and middle dots become what a keyboard types."""
+ s = re.sub(r"\s*\u2014\s*", ", ", s).replace("\u2013", "-").replace("\u2192", "to").replace("\u00b7", "|")
+ return (s.replace("&", "&").replace("<", "<").replace(">", ">")
+ .replace("‘", "'").replace("’", "'").replace("“", '"').replace("”", '"'))
+
+
+def bullets_for(role: dict, counts: dict) -> int:
+ key = role["company"] if role["company"] in counts else (
+ f"Amazon/{role.get('kind')}" if role["company"] == "Amazon" else role.get("kind", ""))
+ if role["company"].startswith("OpenVibe"):
+ key = "OpenVibe"
+ return counts.get(key, counts.get(role.get("kind", ""), 2))
+
+
+def build(version: str, data: dict) -> Path:
+ v = VERSIONS[version]
+ pr = data["profile"]
+ out = ROOT / "public" / v["file"]
+ st = {
+ "name": ParagraphStyle("name", fontName="Helvetica-Bold", fontSize=18, leading=21, textColor=INK),
+ "contact": ParagraphStyle("contact", fontName="Helvetica", fontSize=9.2, leading=12, textColor=MUTED),
+ "headline": ParagraphStyle("headline", fontName="Helvetica-Bold", fontSize=10.5, leading=14, textColor=INK,
+ spaceBefore=4),
+ "h": ParagraphStyle("h", fontName="Helvetica-Bold", fontSize=10.5, leading=13, textColor=INK, spaceBefore=9,
+ spaceAfter=2),
+ "body": ParagraphStyle("body", fontName="Helvetica", fontSize=9.4, leading=12.4, textColor=INK, alignment=TA_LEFT),
+ "role": ParagraphStyle("role", fontName="Helvetica-Bold", fontSize=9.8, leading=12.6, textColor=INK, spaceBefore=5),
+ "meta": ParagraphStyle("meta", fontName="Helvetica-Oblique", fontSize=9, leading=11.6, textColor=MUTED),
+ "bullet": ParagraphStyle("bullet", fontName="Helvetica", fontSize=9.3, leading=12.2, textColor=INK,
+ leftIndent=12, bulletIndent=2),
+ }
+ rule = lambda: HRFlowable(width="100%", thickness=0.6, color=RULE, spaceBefore=1, spaceAfter=3) # noqa: E731
+ story = [
+ Paragraph(plain(pr["name"]), st["name"]),
+ Paragraph(plain(" | ".join([pr["locationShort"].replace("Seattle / ", "") + " (Seattle area)", pr["phoneDisplay"],
+ pr["email"], pr["siteDisplay"], pr["linkedinDisplay"], pr["githubDisplay"]])), st["contact"]),
+ Paragraph(plain(v["headline"]), st["headline"]),
+ Paragraph("SUMMARY", st["h"]), rule(), Paragraph(plain(v["summary"]), st["body"]),
+ Paragraph("SKILLS", st["h"]), rule(),
+ ]
+ for group, items in v["skills"]:
+ story.append(Paragraph(f"<b>{plain(group)}:</b> {plain(', '.join(items))}", st["body"]))
+ story += [Paragraph("EXPERIENCE", st["h"]), rule()]
+ for r in data["roles"]:
+ n = bullets_for(r, v["bullets"])
+ head = Paragraph(plain(f"{r['role']}, {r['company']}"), st["role"])
+ meta = Paragraph(plain(" | ".join(x for x in [r.get("location", ""), f"{r['start']} to {r['end']}"] if x)), st["meta"])
+ items = [Paragraph(plain(b), st["bullet"], bulletText="•") for b in r.get("bullets", [])[:n]]
+ if r.get("stack") and version == "engineering" and n >= 2:
+ items.append(Paragraph(plain("Stack: " + ", ".join(r["stack"])), st["meta"]))
+ story.append(KeepTogether([head, meta, *items[:1]]))
+ story += items[1:]
+ story += [Paragraph("SELECTED PROJECTS", st["h"]), rule()]
+ for slug in v["projects"]:
+ p = data["projects"].get(slug, {})
+ if p.get("title"):
+ story.append(Paragraph(f"<b>{plain(p['title'])}</b>: {plain(p.get('summary', ''))}", st["bullet"],
+ bulletText="•"))
+ story += [Paragraph("EDUCATION", st["h"]), rule(), Paragraph(plain(pr["education"]), st["body"])]
+ doc = SimpleDocTemplate(str(out), pagesize=letter, leftMargin=0.7 * inch, rightMargin=0.7 * inch,
+ topMargin=0.6 * inch, bottomMargin=0.6 * inch, title=f"{pr['name']} - Resume",
+ author=pr["name"], subject=v["headline"])
+ doc.build(story)
+ return out
+
+
+if __name__ == "__main__":
+ data = load()
+ for name in VERSIONS:
+ print(build(name, data).relative_to(ROOT))
@@ -52,7 +52,7 @@ SELECTED_PROJECTS = [
@@ -52,7 +52,7 @@ SELECTED_PROJECTS = [
]
]
# Metrics strip: pick by a keyword found in the label, in this order.
# Metrics strip: pick by a keyword found in the label, in this order.
# Falls back to the first four metrics if fewer than four match.
# Falls back to the first four metrics if fewer than four match.
-METRIC_KEYWORDS = ["shipped", "lines of production", "payment processors", "sox compliance", "years running"]
+METRIC_KEYWORDS = ["shipped", "endpoints and pages", "payment processors", "sox compliance", "years running"]
FULL_ROLES = 2 # PowerChat + OpenVibe get full treatment on page 1
FULL_ROLES = 2 # PowerChat + OpenVibe get full treatment on page 1
COMPACT_BULLETS = 2 # bullets per role on page 2
COMPACT_BULLETS = 2 # bullets per role on page 2
@@ -1279,6 +1279,9 @@ def main() -> None:
@@ -1279,6 +1279,9 @@ def main() -> None:
(ROOT / "src/data/theme-pdf-manifest.json").write_text(
(ROOT / "src/data/theme-pdf-manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ # the one-column versions for tracking systems (build_ats_resume.py) live next to these: never swept
+ keep |= {p for p in (ROOT / "public").glob("Alex_Frison_Resume_*.pdf")
+ if p.name in ("Alex_Frison_Resume_Engineering.pdf", "Alex_Frison_Resume_Data_Analyst.pdf")}
clean_old_files("Alex_Frison_Resume_*.pdf", keep)
clean_old_files("Alex_Frison_Resume_*.pdf", keep)
print(f"\ncontent hash: {h} (filenames only change when the résumé content does)")
print(f"\ncontent hash: {h} (filenames only change when the résumé content does)")
print(f"text font: {FONT['label']}; icons: Font Awesome 4")
print(f"text font: {FONT['label']}; icons: Font Awesome 4")
new file mode 100755
new file mode 100755
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+#!/bin/sh
+# Build the site and publish it to the OVH server, where alexfrison.net is served from since 2026-10-20 (nginx:
+# /etc/nginx/sites-available/alexfrison.net.conf, root /var/www/alexfrison.net; Let's Encrypt via Cloudflare DNS-01).
+# GitHub Pages (.github/workflows/deploy.yml) still builds on push but no longer has the domain.
+set -eu
+cd "$(dirname "$0")/.."
+npm run build
+rsync -a --delete dist/ openvibe-ovh:alexfrison.net-dist/
+ssh openvibe-ovh 'sudo rsync -a --delete ~/alexfrison.net-dist/ /var/www/alexfrison.net/ && sudo chown -R www-data:www-data /var/www/alexfrison.net'
+echo "deployed: https://alexfrison.net/"
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
"bullets": [
"bullets": [
"Designed and shipped the Developer API platform end to end: PowerChat became an OAuth2 authorization server with scoped consent, token lifecycle, HMAC-signed webhooks and an inbound API, so third-party apps act as pseudo-platforms feeding chat, view counts, follows, subs, currency and tips into overlays. Re-implemented Streamlabs as the first official app on the new framework.",
"Designed and shipped the Developer API platform end to end: PowerChat became an OAuth2 authorization server with scoped consent, token lifecycle, HMAC-signed webhooks and an inbound API, so third-party apps act as pseudo-platforms feeding chat, view counts, follows, subs, currency and tips into overlays. Re-implemented Streamlabs as the first official app on the new framework.",
"Rebuilt the payments program as one system: a provider-agnostic charge rail with five adapters (Stripe Connect, Square, PayPal, an NMI card vault and NowPayments crypto), append-only ledgers where double-crediting is structurally impossible, Tipalti payouts, recurring Pro and viewer-subscription billing, signed webhooks per provider and an admin reconciliation surface.",
"Rebuilt the payments program as one system: a provider-agnostic charge rail with five adapters (Stripe Connect, Square, PayPal, an NMI card vault and NowPayments crypto), append-only ledgers where double-crediting is structurally impossible, Tipalti payouts, recurring Pro and viewer-subscription billing, signed webhooks per provider and an admin reconciliation surface.",
- "Delivered 400+ commits and roughly 300k changed lines in the first five weeks across integrations, payments and Rumble connection work, backed by 240+ governed backend integration tests and 1,700+ frontend unit tests, with lint, typecheck and empty-database migration proofs in CI."
+ "Kept a fast-moving rewrite safe to ship: 240+ governed backend integration tests and 1,700+ frontend unit tests across the integrations, payments and Rumble connection work, with lint, typecheck and empty-database migration proofs in CI."
],
],
"stack": [
"stack": [
"TypeScript",
"TypeScript",
@@ -33,4 +33,4 @@
@@ -33,4 +33,4 @@
"Docker",
"Docker",
"GitLab CI"
"GitLab CI"
]
]
-}
\ No newline at end of file
\ No newline at end of file
+}
@@ -24,6 +24,10 @@
@@ -24,6 +24,10 @@
"name": "PostgreSQL / Prisma",
"name": "PostgreSQL / Prisma",
"level": "strong"
"level": "strong"
},
},
+ {
+ "name": "Go",
+ "level": "familiar"
+ },
{
{
"name": "SQLite (WAL) / MySQL",
"name": "SQLite (WAL) / MySQL",
"level": "core"
"level": "core"
@@ -45,4 +49,4 @@
@@ -45,4 +49,4 @@
"level": "core"
"level": "core"
}
}
]
]
-}
\ No newline at end of file
\ No newline at end of file
+}
@@ -36,7 +36,7 @@
@@ -36,7 +36,7 @@
"githubDisplay": "github.com/OpenVibers",
"githubDisplay": "github.com/OpenVibers",
"linkedin": "https://www.linkedin.com/in/fris/",
"linkedin": "https://www.linkedin.com/in/fris/",
"linkedinDisplay": "linkedin.com/in/fris",
"linkedinDisplay": "linkedin.com/in/fris",
- "resumePdf": "/Alex_Frison_Resume_Sep_2026_69f82554.pdf",
+ "resumePdf": "/Alex_Frison_Resume_Oct_2026_68186113.pdf",
"availability": "Open to pretty much anything — engineering, platform, analytics, ops, sales, whatever fits · remote or Pacific Northwest",
"availability": "Open to pretty much anything — engineering, platform, analytics, ops, sales, whatever fits · remote or Pacific Northwest",
"status": {
"status": {
"now": "Contract software engineer at PowerChat, building its developer platform and payments program.",
"now": "Contract software engineer at PowerChat, building its developer platform and payments program.",
@@ -1,15 +1,15 @@
@@ -1,15 +1,15 @@
{
{
- "amber": "/Alex_Frison_Resume_Themed_amber_69f82554.pdf",
- "blue": "/Alex_Frison_Resume_Sep_2026_69f82554.pdf",
- "cyan": "/Alex_Frison_Resume_Themed_cyan_69f82554.pdf",
- "default": "/Alex_Frison_Resume_Sep_2026_69f82554.pdf",
- "green": "/Alex_Frison_Resume_Themed_green_69f82554.pdf",
- "indigo": "/Alex_Frison_Resume_Themed_indigo_69f82554.pdf",
- "lime": "/Alex_Frison_Resume_Themed_lime_69f82554.pdf",
- "orange": "/Alex_Frison_Resume_Themed_orange_69f82554.pdf",
- "pink": "/Alex_Frison_Resume_Themed_pink_69f82554.pdf",
- "red": "/Alex_Frison_Resume_Themed_red_69f82554.pdf",
- "teal": "/Alex_Frison_Resume_Themed_teal_69f82554.pdf",
- "violet": "/Alex_Frison_Resume_Themed_violet_69f82554.pdf",
- "yellow": "/Alex_Frison_Resume_Themed_yellow_69f82554.pdf"
+ "amber": "/Alex_Frison_Resume_Themed_amber_68186113.pdf",
+ "blue": "/Alex_Frison_Resume_Oct_2026_68186113.pdf",
+ "cyan": "/Alex_Frison_Resume_Themed_cyan_68186113.pdf",
+ "default": "/Alex_Frison_Resume_Oct_2026_68186113.pdf",
+ "green": "/Alex_Frison_Resume_Themed_green_68186113.pdf",
+ "indigo": "/Alex_Frison_Resume_Themed_indigo_68186113.pdf",
+ "lime": "/Alex_Frison_Resume_Themed_lime_68186113.pdf",
+ "orange": "/Alex_Frison_Resume_Themed_orange_68186113.pdf",
+ "pink": "/Alex_Frison_Resume_Themed_pink_68186113.pdf",
+ "red": "/Alex_Frison_Resume_Themed_red_68186113.pdf",
+ "teal": "/Alex_Frison_Resume_Themed_teal_68186113.pdf",
+ "violet": "/Alex_Frison_Resume_Themed_violet_68186113.pdf",
+ "yellow": "/Alex_Frison_Resume_Themed_yellow_68186113.pdf"
}
}
@@ -39,9 +39,11 @@ const pitchParts = linkify(profile.pitch, LINK_TERMS);
@@ -39,9 +39,11 @@ const pitchParts = linkify(profile.pitch, LINK_TERMS);
<button type="button" class="btn" id="themed-pdf-toggle" data-magnetic aria-expanded="false" aria-controls="themed-pdf-swatches">
<button type="button" class="btn" id="themed-pdf-toggle" data-magnetic aria-expanded="false" aria-controls="themed-pdf-swatches">
<i class="fa-solid fa-paintbrush"></i> Themed PDF <i class="fa-solid fa-chevron-down themed-pdf-caret" aria-hidden="true"></i>
<i class="fa-solid fa-paintbrush"></i> Themed PDF <i class="fa-solid fa-chevron-down themed-pdf-caret" aria-hidden="true"></i>
</button>
</button>
+ <a href="/Alex_Frison_Resume_Engineering.pdf" class="btn" data-magnetic><i class="fa-solid fa-file-lines"></i> Plain: engineering</a>
+ <a href="/Alex_Frison_Resume_Data_Analyst.pdf" class="btn" data-magnetic><i class="fa-solid fa-file-lines"></i> Plain: data analyst</a>
<button onclick="window.print()" class="btn" data-magnetic><i class="fa-solid fa-print"></i> Print this page</button>
<button onclick="window.print()" class="btn" data-magnetic><i class="fa-solid fa-print"></i> Print this page</button>
</div>
</div>
- <p class="resume__pdf-note">The <strong>Download PDF</strong> is the canonical résumé — dense and ATS-safe. <strong>Themed PDF</strong> is the same content, pre-built in whichever color you pick below (every color is generated ahead of time, nothing renders in your browser).</p>
+ <p class="resume__pdf-note">The <strong>Download PDF</strong> is the full, designed résumé. <strong>Themed PDF</strong> is the same content, pre-built in whichever color you pick below (every color is generated ahead of time, nothing renders in your browser). The two <strong>plain</strong> versions are one column with standard fonts, for applicant tracking systems: one aimed at engineering roles, one at data and analytics roles.</p>
<div class="themed-pdf-swatches" id="themed-pdf-swatches" hidden>
<div class="themed-pdf-swatches" id="themed-pdf-swatches" hidden>
{themePresets.map((p) => (
{themePresets.map((p) => (