openvibers/afresume Commits

Plain one-column résumés for tracking systems, October content, deploy to the OVH server

OP OpenVibers committed 23 days ago · 0d56047cbc · 35 files, +408 −20

- resume/build_ats_resume.py: Alex_Frison_Resume_Engineering.pdf and Alex_Frison_Resume_Data_Analyst.pdf from the same
  content, one column with standard fonts so applicant tracking systems read every line; `npm run resume:pdf` builds
  them after the designed PDFs, and build_resume.py no longer sweeps them.
- PowerChat: lead with the test evidence instead of the commit and changed-line counts.
- Skills: Go (OpenVibe.Node). The designed PDF's stats show endpoints in production instead of lines of code.
- Résumé page: links to the two plain versions.
- scripts/deploy-ovh.sh: alexfrison.net is served from the OVH server since 2026-10-20 (the GitHub Pages certificate
  expired behind Cloudflare and would not renew).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UgqWFqsrjYdYbusKn5547T

Changes

ddiff --git a/package.json b/package.json
iindex de52bc9..02fd2f9 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/public/Alex_Frison_Resume_Data_Analyst.pdf b/public/Alex_Frison_Resume_Data_Analyst.pdf
nnew file mode 100644
iindex 0000000..274f46a
---- /dev/null
++++ b/public/Alex_Frison_Resume_Data_Analyst.pdf
@@@ -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
ddiff --git a/public/Alex_Frison_Resume_Engineering.pdf b/public/Alex_Frison_Resume_Engineering.pdf
nnew file mode 100644
iindex 0000000..ea5b120
---- /dev/null
++++ b/public/Alex_Frison_Resume_Engineering.pdf
@@@ -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
ddiff --git a/public/Alex_Frison_Resume_Oct_2026_68186113.pdf b/public/Alex_Frison_Resume_Oct_2026_68186113.pdf
nnew file mode 100644
iindex 0000000..10f61a9
BBinary files /dev/null and b/public/Alex_Frison_Resume_Oct_2026_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Sep_2026_69f82554.pdf b/public/Alex_Frison_Resume_Sep_2026_69f82554.pdf
ddeleted file mode 100644
iindex 1f0df4d..0000000
BBinary files a/public/Alex_Frison_Resume_Sep_2026_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_amber_68186113.pdf b/public/Alex_Frison_Resume_Themed_amber_68186113.pdf
nnew file mode 100644
iindex 0000000..284c138
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_amber_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_amber_69f82554.pdf b/public/Alex_Frison_Resume_Themed_amber_69f82554.pdf
ddeleted file mode 100644
iindex 79e5d86..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_amber_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_cyan_68186113.pdf b/public/Alex_Frison_Resume_Themed_cyan_68186113.pdf
nnew file mode 100644
iindex 0000000..035d6c7
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_cyan_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_cyan_69f82554.pdf b/public/Alex_Frison_Resume_Themed_cyan_69f82554.pdf
ddeleted file mode 100644
iindex becd939..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_cyan_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_green_68186113.pdf b/public/Alex_Frison_Resume_Themed_green_68186113.pdf
nnew file mode 100644
iindex 0000000..1c4b8a0
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_green_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_green_69f82554.pdf b/public/Alex_Frison_Resume_Themed_green_69f82554.pdf
ddeleted file mode 100644
iindex 99b3b40..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_green_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_indigo_68186113.pdf b/public/Alex_Frison_Resume_Themed_indigo_68186113.pdf
nnew file mode 100644
iindex 0000000..0557280
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_indigo_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_indigo_69f82554.pdf b/public/Alex_Frison_Resume_Themed_indigo_69f82554.pdf
ddeleted file mode 100644
iindex 9af1e75..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_indigo_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_lime_68186113.pdf b/public/Alex_Frison_Resume_Themed_lime_68186113.pdf
nnew file mode 100644
iindex 0000000..7b2cfde
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_lime_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_lime_69f82554.pdf b/public/Alex_Frison_Resume_Themed_lime_69f82554.pdf
ddeleted file mode 100644
iindex 05d7eef..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_lime_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_orange_68186113.pdf b/public/Alex_Frison_Resume_Themed_orange_68186113.pdf
nnew file mode 100644
iindex 0000000..a3c5b1a
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_orange_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_orange_69f82554.pdf b/public/Alex_Frison_Resume_Themed_orange_69f82554.pdf
ddeleted file mode 100644
iindex 5e8df22..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_orange_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_pink_68186113.pdf b/public/Alex_Frison_Resume_Themed_pink_68186113.pdf
nnew file mode 100644
iindex 0000000..dd035f1
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_pink_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_pink_69f82554.pdf b/public/Alex_Frison_Resume_Themed_pink_69f82554.pdf
ddeleted file mode 100644
iindex 80703c2..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_pink_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_red_68186113.pdf b/public/Alex_Frison_Resume_Themed_red_68186113.pdf
nnew file mode 100644
iindex 0000000..a48d991
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_red_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_red_69f82554.pdf b/public/Alex_Frison_Resume_Themed_red_69f82554.pdf
ddeleted file mode 100644
iindex 4219a7e..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_red_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_teal_68186113.pdf b/public/Alex_Frison_Resume_Themed_teal_68186113.pdf
nnew file mode 100644
iindex 0000000..a92bdc0
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_teal_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_teal_69f82554.pdf b/public/Alex_Frison_Resume_Themed_teal_69f82554.pdf
ddeleted file mode 100644
iindex 2dc8f76..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_teal_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_violet_68186113.pdf b/public/Alex_Frison_Resume_Themed_violet_68186113.pdf
nnew file mode 100644
iindex 0000000..e2f7d37
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_violet_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_violet_69f82554.pdf b/public/Alex_Frison_Resume_Themed_violet_69f82554.pdf
ddeleted file mode 100644
iindex 7ab0677..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_violet_69f82554.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_yellow_68186113.pdf b/public/Alex_Frison_Resume_Themed_yellow_68186113.pdf
nnew file mode 100644
iindex 0000000..4dc0128
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_yellow_68186113.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_yellow_69f82554.pdf b/public/Alex_Frison_Resume_Themed_yellow_69f82554.pdf
ddeleted file mode 100644
iindex 40bad6c..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_yellow_69f82554.pdf and /dev/null differ
ddiff --git a/resume/build_ats_resume.py b/resume/build_ats_resume.py
nnew file mode 100644
iindex 0000000..777d107
---- /dev/null
++++ b/resume/build_ats_resume.py
@@@ -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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
++ .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))
ddiff --git a/resume/build_resume.py b/resume/build_resume.py
iindex 5361671..833df65 100644
---- a/resume/build_resume.py
++++ b/resume/build_resume.py
@@@ -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 1
COMPACT_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")
ddiff --git a/scripts/deploy-ovh.sh b/scripts/deploy-ovh.sh
nnew file mode 100755
iindex 0000000..4acf1b1
---- /dev/null
++++ b/scripts/deploy-ovh.sh
@@@ -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/"
ddiff --git a/src/content/experience/01-powerchat.json b/src/content/experience/01-powerchat.json
iindex 7d9af6d..a7e2364 100644
---- a/src/content/experience/01-powerchat.json
++++ b/src/content/experience/01-powerchat.json
@@@ -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
++}
ddiff --git a/src/content/skills/01-engineering.json b/src/content/skills/01-engineering.json
iindex 6b320e9..90551cc 100644
---- a/src/content/skills/01-engineering.json
++++ b/src/content/skills/01-engineering.json
@@@ -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
++}
ddiff --git a/src/data/profile.json b/src/data/profile.json
iindex 7480537..b52e338 100644
---- a/src/data/profile.json
++++ b/src/data/profile.json
@@@ -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.",
ddiff --git a/src/data/theme-pdf-manifest.json b/src/data/theme-pdf-manifest.json
iindex a17f96b..969cd43 100644
---- a/src/data/theme-pdf-manifest.json
++++ b/src/data/theme-pdf-manifest.json
@@@ -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"
}
ddiff --git a/src/pages/resume.astro b/src/pages/resume.astro
iindex 66d8806..f9319e0 100644
---- a/src/pages/resume.astro
++++ b/src/pages/resume.astro
@@@ -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) => (