From 17799a05ced450fe968dc151558b5d2c7c0fa084 Mon Sep 17 00:00:00 2001 From: Ruben Fischer Date: Mon, 16 Mar 2026 13:23:58 +0100 Subject: [PATCH] attempt to fix profile picture error --- profile_pictures/olivia.kibele@onyva.de.jpeg | Bin 0 -> 11312 bytes .../timo.uttenweiler@onyva.de.jpeg | Bin 0 -> 4093 bytes scripts/update_profile_pictures.py | 116 ++++++++++++++++++ src/orchestrator.py | 9 +- src/utils/post_cleanup.py | 22 ++++ src/web/app.py | 4 +- src/web/templates/user/company_accounts.html | 4 +- src/web/templates/user/company_manage.html | 8 +- src/web/user/routes.py | 81 +++++++++--- 9 files changed, 219 insertions(+), 25 deletions(-) create mode 100644 profile_pictures/olivia.kibele@onyva.de.jpeg create mode 100644 profile_pictures/timo.uttenweiler@onyva.de.jpeg create mode 100644 scripts/update_profile_pictures.py create mode 100644 src/utils/post_cleanup.py diff --git a/profile_pictures/olivia.kibele@onyva.de.jpeg b/profile_pictures/olivia.kibele@onyva.de.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b4130e7ba2ca94bf6dfa13d725f41b594e479929 GIT binary patch literal 11312 zcmb7qRa6~av+c&+b>ki!HW1u3xVy{7C1`L6PH=a34er5R5+Jy{yE~Wf`_K8udA-%^ zsn=NDHRh=5nl-E67v47j=rWShk^l$@0083S0=zE+!~jr`kpHX?gZ@}B@GvmY&@hN_ zaIo;mh{(uDh)75%sF>&|s2He7Na(oe7+BaiI5@~?c=)*3_?Xx@*#A5P0_x)oGz%bnMES@U|g_Q%y;*(V+RKl5hR1xC|-kb9i4 z17$NJR{t7AUOYRa^PKOv3~hhOK0bDkl2M@BXeWrUUOpeKJe$gwA8Du#pis`&S?-Dm zKN8{?DAF!lr{9GLu)80J6Gy%IJKhqB>a|_c%2A)IxHPw<6(6s^NILb~j+Bzk2b6JS zrZ7!HNlccviUJzw)q~ST7W%zhhrkg9Jq9M+N>QENaZg?@N`ed3r3Kc9GJoHjtD7nE zp(cXqky?neg7)*Y-s>z#ry{00bQlrLzjU&9QQ$UdO42I6hYb$YlR_;dxq$^1v8T!` zcUYNSJHzVDF;(&;uoM=QyY)q8hiM&a%a935lL z&n$|pe|J^m71TiW6mKxh;aTc$PJ+}w0^DNn63?XcPpj#JW^M2J*+KF9s20}0ep#={ z&LXo&nt0A>A%T;E^XM$C=Xum0mb)t3>#FK|$J#22ilp(nrY2fTt1pj#JaJlN?K9q2 z&4(p?@sh8g7i`S7EAr52n#c@Zb#KYwyJ;DSzfQeJ?%F-qr3}u?NjwVdSGF*Bybbo%=pb=Q_R`HL7xJ*}1>CrwCn?lx@!pUpp?4QLOQvka{E_i}6oi25T~(J9>u+S;@l zpxR`_r%(B>=JO%OrqtG6R0u@&^7135IOsF2^lUS`(CHJIhilqiEeTl{M|MvL|8_39 zq(5gEJ(o&4GQ+lFln=4OtX#fIY^hYqGenbZbIq5~9*RHnYiN51c*rytbbT3Km?>h= z6#XJeCC!`jk&7(o00?LZCV`HkF17(fqVzhytGSS{CxQpamuj9X56VTxb1BJXJ_Q@vDis&C#}oct8uwG*?9!b zAaxg&u(Rmr@-e}VI~Y-`$ZMeQWi@gz4h!RX3j=55%x?fCb9qWmDc=J2#L5ox1pArJ{=;0mC-U3YUg-??lmr>qN6;;T=#bu;WX6t{h(C0Gwka z@+&2s#4cQloACCw=p#Bqnd1#hcFu3yBh+#~UyOG&PNz5=GYAUH9(ruYTIw$!o@G(T ze7h7>@xv-%P#=6YO%C=8Aas(XFwcF!y9c!=;dX#(#fR#K6oVeuEmSGJ zIu@QJ{|+!HUc33hqUkAz1pax(+WmAn%!NR#?nQ~@%&8J7j6O;pRiI}vz^)^6Op~er zYEtX5t6%vog=EI*Vmhnso0HSBkh6F4*3tB3`4;Ow>QVZ|tN_ArgRDOn(nJ-7RT-Pf z=;0338Cs5#-upKn@pz!RvU0|*ZY-P)C$7(kQrrp_STngtr*CNXmnyDZs1#=bd!6uk z@TKrg=-T>;DQ6J1Od^FdJFVVCB$r4voi#HskeI|UVU+u<7BXHVOGt&V7Rwu7FD%g0 z6k_1?PxWwaDeJNGxbcuHbjZtpvH?9Ep>Xf8z)Ea{V+X&E=&X;P4nKbu# zVB+Z(0TwM4A29HwYvxexRjBkuY1)s}HiuPzC^o*_92hT{BjFbJBX(@>P4`CWCy=Y(${m8w)pMIeKG#;S1NqwYR4exYE4ijDvnr z=@gEKEX@>}`iSktG8qpPTYJci%f1Ltct+k_Hcz$ZW2w4zDlix@K6&TN|WmK$Y56uGj}0fuZVa#};I&;Xff&N&>^d+N#^3qMW=C`` zm%@$2sTsDD5YQsoD+hY)w4+Z6UVEQ1Xkn4*VC0j;Y1-a&-n7Nxg;{5elteZ1Tv-Ud zQ;2Lh9lClgsjZ!tM$K;Xy7*k>7&HFmlVMFd8Do7nT2i-;F1d(J4knvq^k#mAhj}i| z6z6XqGP&3s$!54h_fks&Y-ocFgs*xLMpg_~*s#`^@RSC9IKfz0uG32GN$e6eOXAj) z*yq)1&WAlSC>i&4x#1JvB=^U#J_9-FnIBFlPGciuLW)gH#<83SwwrMX2k_>0tgy6u z@ug`e1JxkP%)UoOV#=t}$#gf5SB8?N4?u}xe!gZ>mV&2{&@-4YX#br3$B>c_$rJH{ zP){@FGfA!YmgKKgJT_@)g*JFq*L0_rCdSL2_+2oQb?2Mp5u`Ldqvbo`NJXQvj*2{( zT*yde$bBo4U%jPe_iCnMZg5i*vD4Fk0yVFFda7jh@D~h$IFl`YEhtL0@&Sg5&M^jl z)S`8COPuCMWFgkt98=*9KEXU2ld4-})J`UM`wt2e(-d3whoaiHgp81ZYq!=aC%M8J zZyigW;JA&2<{y!FJdP?kKs9H@(dDfN}^=P!n$8SJc^qS1<4qrUsvY-l)Ee@ zevZ~1+*vsFfS+qjVmYr7J8({&&a>cz$h@u_%z zZ9OFaUHwBFT~UG^>@wmo$losHq7&DgEr9h&06pDYRXQ%ISVTbUn74H9N9WOs`ZDh_ zID8c(_qM@3pz=dzSI2hQCsSjN@(V_b9S-4sJU_a)fi{HZ3XP*lz@> z$ksP?E(R^ALP#ji?!wZvE4Cr+0(I)On`Zi!ZtTD4GGlfZ=P4cKB>qqjpp`{hP7W^8 zU`@$C^TnoJ_duG9hR#}U`_oM=^V&qCK2o%X^kCq}Y@L{@!lrJJ>wZ$;D+da8>4NGx z`;#NW%-n3Ff77SlE+Q*})Iv+v^TmNoT$*EvatA!We@4%ee$+u~3;-k~Bm^`R;J?fi z0uloXlN=fy3q(et%qAl0_;2-tf`AZ$cvK~wXv6Bf2qtJyBpH1^zngE5m#~%O=r`4* zVEwW^#eH=02aV{s`RV*i^#jf-lB=BKV*Cqle{qwc5HJC^w!X-B3g{@6uNSXT(cYWP z^oDuFOsCAhT$uLv9YFcFZduG4WEYo|W?XT)qlo+KCS~8&y(2P`-*l5xQWN`k1@j+q z)6Dz__U*(J7(|pOirV>wD*c$!C+lB#y$!*2r&s0)U#=c5$-7QdaThVLD%DjaI@czW zrqCK1KVeL3m}T!-oeu5WXGE5MInJV9wx%D#L$i>Lu{wQ_)}^@%?JLp*5nC{~iz3P_ z5*Jp*B#lhkE2+3EAFQB#nQul)Z5D9oLhfOy8J5PP)a#_shHSiwE5- zwcCcy=bVf%sb>$e1%*9fF%R`s9~@DX_>u zY$B}6#{a?%`48OBI`04&6iv3I3<|8-YoB>~=9r%LZOTF(L!^4_rWm}_iIm^wRa&=r zdhSGFDbo5 z@!K@|7<4?f%m$e$}q&7o1C%AHe`VyDh z?F}wE7!?&XjnM;lRLccK93aW0PIf=Iq3Ei*AENQtGw%RkwmoR34oNRZ?tI1~#xf&Q zF;20;I>$`oXTfmxG+DK{)H{H?p5w%vddhT&<`wCSieb<@Kp{+q?#?^@EqKSTL6PYV zC0w_ff+m6xs7LW&P9;kf*O3SlUi(6ryjAqseu0>Z?Hm;muOI4l6Mzeca3jT!ACRL^Q|dwMdp}iaI^kP zh>3b?VzZ|1b;-va30>N@D#xiST#Y&<3*T60OBcP%InY=ApB3+7)e0Y7n?hF<*vvem z*Ra-*_P(JE>ZJ}|{G0%(adosEw%AkcK${4Opc2@u!8W-~iVZF*pq0w1mYFj5y#uO4 zj63Dy3{h1D`kX25GY3Dju?5fQ(^wjxYYoX z*L`lx<5u@bSjr#+unnZ7j)>yndgUB0Z_ievj6c3kMxdotzKJ>~4PxSU@7tETLf+5E zlqNDhIgVJ}GPav*h5QSW_z;Njq)Qz{o&Xl3eIEO$P56efhRr%`A8#i6>4Nw#oi$`8 zATTI-jxG8s-%}>BQ^o1-`#QA^rJODZ`WA)fSp6M9_?^4tW++Ld)93!PLhtr9v!C>1 z;$4KYkm}>b;@qXaW=T-k%=lASAhNFP=-6_)nn81*+0sPxk3CZxS;p=*S5QhGZsLUa z;LHpxN!Hf-n332^>Eh#jhfbsB=50!Wwr;n614avEH`v1&Ld>$E;x)2M zTK&yBWZhCXucK{mVva!K5}I=_^A?q)7mS0)V6Ir$ABa?3&Ks_blG>Gq>D|-?_(Uh`A>L}<)CGwu6wN(~eeUq8 zHqY(>4|+?mZYHsX;`rwnw!g!kS+|CwxK`Zax-+7vXFwice}PufHKi7@_uWK$Z$(^C zyNUK<tF2ic>#(65m;B@d)bcs+dioVwzosD66?wLseMISE5ABW&h~DOHX}YnT31{@RJ8E~MS{j^j)U4ch08&`)W2m%gT&hAgeaXBhn$}E8>J6mv5=hIs_$yt#ifRr=E*?mc z8#9XEZb3xt3a!|p(hUR^S>}8;kXV|l%LHb9ViuPjABZeas1imI8|C}G-`#(c0^I7n z@BL98W1oKrT!$K-1?yEUuI-;QpcsVL|63BmgAu0m<9+(CbQ>-CSG#jbUw zzq99{hOW6NGfQ_By4d#oQ@K8&e<*yAAG+g*1o$7p0Rn(Q4iZr|c0~UkmsdO0b8$(w z`M*Mg5P6wO9@O8n*!gNfix>j>^aIflVnoHK!H!|KH0b;lgbW=g}S!cQ| z>!q*?kx4(*(aHo>LL2hfexN7kcTxTVI%K0KVWp}ka-BXttw#dNy)y;H2YeHooI=UM z;#@Xm?sh%MziV$a-Wr{OPpq7x^{22j=aL8RwX;}c0V3s42LsYEB6cE-!R;?z{hiWOp_$mHfItI)-b;D8)VjJ%OFmEyVb<4$>^NCYfU!@l4 zWgIfhZXF5c*w$pxt6{9j6k+W{o%#}%(mI08yvrMGI`nC1j~VqZw0H&8c+aP=jyTX> zHIgCuL6u^)OeUAzMd{}eKr#~Cd9hPeClckS@92G=Nar6=Z`Eo4B}I~Af>=|)1yUf)Ij^1SKZF zc*ttjuxegc@|v0R|GMV4qWPc_c1SIbEEjpFG)I(`olrRb10mut4mb?>jnNY`XpBXrs4(Tn#Ql__#^YPknAZ7y)xXbr7u;UY9p17=hV8C*a)m}~ z%yP}nEx%zEMLV@v`t{B~DNhQk5#bguo*tKKbJCj6=;!#j|KHtdW@>UT`T8Jl>3LCA zxVR-N|1tP!xBNp^7?Cb6`)e7;T~A5Ud`8SHpbqw9A87jG_SP# zvHhIm=<|4~B;$mQD7jBvLaLcJ?Wea*VlY0G~yG8+U3HqkvjkYccrS68L7I$F!pET+l6}tGO`Gq|et*FX(A&pnV z1}4<;l;5DV?R#iV>GYS zv(28wPHT(OOweF3i9mn;RWNXmdpZ&)8CEkRGy`&sz2o*?iYEk$-giWw((U`LKMUHc zR>&L$uK?2ao!Bj-Tl&OETA%Af#uQiHIrD5buoRx6Oa`u_yZ3N2rcKGie~^r?5;4)~ zLK#wr1hC(4*or*gE|~$2M>$8 zyWHW>*W}|h!Q?003Fz2?q5CXVTEx$eW=0_dho$3kCp27GZp*b2Ij-=!qd0uTp2Ych z;w9F4UsJ&6OGf=a$2I2cQNLT7D|g%nRIo6N16v!$e%oNZ(4(zNi5DhIIz@1&vHfP| z$ow9bgr{*XswufCDIM||+IgIGcvglq&6(!zPVTj0OUoLs0Qal&1)O#X`wS3Dra5}r1{7pNNJZ?qmj@tyJv%BJ)NLjKCnj zgsUu3sz-FKRz<8!OOiCJgkm|qBtSC<+s@mK#vuo$#j%qm^(i}bqf5nw>VmG=Be(`e zJBzLJ%GcmsT}G#(2o9Cg#LAeac{|24#+)0wewZ*9Q|v}PA*$T^O1s(ixT}9QATu({ z%{_pFMpbSfB;$pu%aGWWY%5^-WxobK0FR9XrKK6*yB`TYKvL46pHM7CqVL&5h2yJ@ zTTTWV(5XiMCYnAQ3UOB9513K%h1eKO75{5s!NN0qFimVtZ_a?D!oW2H=}Cm1R(nK~ zQ5UStw1o6>Y4K6P{~hK5{^{@087Y{vu|}l$9d4&EN~4NlTT9`e!xSe7HrF{t#9jk= zF=2BKD|`g87!4e@&R|W<$GU!&C6r(Kii$y?y9X;pNFl|_Armqolq~4~J$(m6lO1q| zQO?8rqjIxfEg|qrtLOBsIyX<-^!uDZA!4-6L1Th%6nm~0jC?N0AujaZ0UykR663>{ z55eI>arm$1@DKAq|DYaZae3csFaBj6{|G$jot~-=kl&E`oiFm;SlL`6LmK0lbZ{U? zFMB^fTET`{lm@7^NXUPcHCn(C(|QM3ASTI0^tnwT1zp_QMWmI6(3Oyde?j+n2NVPu z23ODQcJQlrZ%(klI$32V|80zOj z$$_bZF~De3Q-q0)E3W1es_`XEJk%%q2PnU=t=oej=ULL4Tqv&YLas0z#o9=N)khnA zD?~`1b;JCnK1{x-pj=WUunc?am??QIB_Mo){}Td>grMMvohMywnNOUfDk@}}WuE|= zMjry=JK%d^0eb+`>tGN1|CjKglYQX+{a?uc5qZ$@-$;n#pGgdb&<0V({2;7IUq&zNDXt#7S{bwQHNz$4 zD{{=H6@K}%53;`lcpfi=#NC(PPE)0};Kf#LvE^fE)VFn<-;ek|UQT`5OWFNuP)`!orjLctSWZtA{_YC(e*|dO=sO9@71sG=&D+Ltx zU7H-sHWll?=t#TX_1_^X(0;3dLz3qbv6w;Q^l6$V($q0Ax*J%xP&DTuru+I*o8enX zQmNq5^l9O-4lVq_7aVh@7mvW+Axl1uR%}P__6Sa9qIqg^wCn5D5ypsr$>zuCp&M$J zH?6Y29zmIB#)yH{`C6CvhswQ=N#6A|%AYx^k-3k|70lY?nuPaX&irrS{s+mAe*pf6 zGk;KK^xB?_ssGEF(L2aS(Kg@@x{mf@HgDMtFI54ZLhF}~uUG*`T6#4!BhTQxa7`@W zSC3>CkS807QMKR!Wyy-J|9~^(b?e5)+wZAZc??q+7uxvz)UuRc)MJ=2;`l}t>||e^S&WGjgyE2%r`7VJn3B;JG0EVMzX7!h1F&hF0eTnd~Y%_TPaLh((H%{6Ns6M3Qlsh;V3#~AFBci8Y98ba%G*Vu8d=Qbocc302JlTe5v~za6&B%u`jw6 zf1CHE@M(DEb%J%tsKX0$;!W6rpE%S_FdMy$>Pk+eLnVaAn~};yW>a-rJ4!+{c|AmC zh~2&(Q5w~-Hwo1;jr8*|>u-+M!Myb?B%1`npP`M&K8^LO`?9Gm&el|T&nVrak_hKL zjib=x44apYsTQfgzFwS^FM+jV$D{;!100&X@nyEYH|r1>!Lw8Ww--3O}8WliMsStK=|!oFzG30V8C{;sk9UY zx-bFqsyg8{SgRI6AL_FmA;FO^@cRovnN_mLJS(!vYXa#Fh6K~D5P%Ou}v-SzklYR0ncEEHDL7D4`CWu#}RqMdE_E5MwAY09`%VPhn4&YFyo_7GXe_-sND z=0+@qlY|V*!|OfpQ#9J+mN&R&2zP7_BSPslmWM{z8blA&W48pTgHX9VNRKOMg zV~k4Srg8mrL8`-p3@jM^5^a~m8s6Up#qqUY@A3ui!lN|wrU{iyj&YRsDMdmV-yG2W z4Q3G|UK_Sx(C}pEOwc2Fc@b?wWF@M4QyR$qnZKxjt5~9)|GIl5B(#ZG6s@ z5gx+~VpXW27W=vrJs;MNB}AVmC!2sRXT`mNFifdPD_RrV-GT1giVphD4j3vIC}tCN zhKuN}v+b7i71~?l)(%PHwH*EiOT-2iIPh4lw|H<+0lP z9$VDBy1k_>t9;e+7K@XiC;my8K{Yq3(uh61`_lT^BP_m3l}a$F5+Nhc3wzXvw-D5u zR6+l8m>u?Il70znBA1*wIt3u<3F6m6kL?JUG8^KJX$l222c`z@)~J6$g}h)Jw;mjV zzUJk}%XI77u@=^qE>jvOtP3g471UB33@M@gOMQ5BSCdYz(BLx=W#gqUb>rPH{nb;( zVbC|MBFsc*!86a0HLMECAJJ$zZcE=DDSxuStMAMf9#kc(>BKAJkdJ1(AE%P*Wd<{zchPaLL(>#n> z1DjbiqM#D%B(7p$nQMdH$kjpTI{-23N_b#>xMvmJUdHLWr1v{u zMfi`SN{!7q&L#GxoJI}1dE1#j-t9drHWN|B6r*HIsP8-AikvZJse@qhheM*vM#vwy&{<^jb?Qsgf`WSltY^HNEX&fwpt-z z#4T`VhJSOZW+z}-vsF?$F*XDf=SJNw1#$P%@zUeTIb&{8R*#hfOMB3mxh zujKh7N~*6SP^r7oZ4kgHWNErW7SEwtP7sw1G37(_4meTDb(Nn%F(*WpU^`{yBlAA; zkou9!9(0eNlp(lrF?AJy21&-tf2{03V35n1UENj6NMB*fOImRW;FguqxCGQi5ee?L z<=Gt0pZbj3LFZ}z2rk~%41IgczofaK@he0$#A(4dT!?gP6WmgVbiaB|Y&bRh(AJbo zmy*{F*?}vJoM~qdVbbubIM8^HU|On`T+f>UQzNu~)17YzDG+<82nOvEQVm_~mPFih zmZy$*l@hW;e~3J~&<8706x5h0ck2UhVn4=<)A|KbXJ&4^NhCIGRb4gN+VY6WVK(+7 z-Vm=V{f3x*Pf3z81wAQG;X*DE-=dA1w1BsIvrC+R2pSWc>Q!cq-vRc7wfcpJsR)cF ztjLaVX;QyK;dgbLQAleqS%dQWUF}3usKv>$18SglLER$vrGl(prE+9O^W5%W zf?xv!bM7qN3T0liz+XP?$@z09iafEal6JsISd}F6z7d_{#w?H(P&lV4?hmGZN0KZ5 zjy~-8cm(6L?;d4*i%jR;(R25e;2y^&oL+Os&9;)dR+=r4H;Kq53^9h{2vN9vmQUzv zD`XTXdZQU|xy~xCBp$$kq<_8#A6(qXrxxS?4tQqho@z}w*AO186(jkSCd*A3D$D() zf0+wSBOi-uwC9|2Xd{3mQz|Z`clI3+J5dEi`R({66vAgpgEymF9@$D*2KpzE~p*y9ar zzsiUUUC@ykh;Ab*QwrX9Q<{3tkPE9CHS88Qi<< z_NdfVBfBD_M#%Gn8)f#VSxsCmNARdrnD$u{zhDC34xk0Af*&8FFn<22ncH7sZC9#m+j zRpgq|A{d2`Qsq*3WYHl-@j0px~Lb0;2qW<6O=p}$;1g-#AARsgVMuH$n&`}q_PuB?p z9n1Z9!2u|Yfe{QjzU2i0^pF3#1RziV3+W`XvH_pg(XwKN!Xmq9hSUL0WKG zQO#}kVSg!~cv|sDY9O3YYrr2g!8o91@$gsdkP?!s-SgmPBVlZDr;De#&VgE)UgTH2 zGaED1Exs4?Wg*L~FSO|INg*{Juf@|tuGixF7Yh{CQK*f=Y3`*G2H`>7hoi9#JNcu} zmrB2UEU1YGMKE$*w5DWn$an7|bp&)RiirWje`nUsyu9I`dWpa&GSQ>IB90AZGmV=a zBapyh53AJ0wgXNTxS+fC)b<{W3|tYmJpycgfP0!It^J0iO|X+5L=WPWkIR}hIwj+J z;=o)({kPn25B^-Qm)5#_Dsew8;mr3;xi_e$^T~!LzeLYZ1s58~j??XuMF1cu2n2^g zVE=dq0fC_~IDlm5K#3?LFro@dXq+M^*J&|LExLt_bZbE{@PGx)%i@Q!dQ!DVxwl{S zlgaWT9!oak3uj6cwmVgocs>e!(6^Gv{E{SFqx6-$xbyKqOpmbqik-ta4zJ`JH2n$_ za(B?oM*n+1>FH*F8cq1Cvbe1c-kj88drf{1Rq2Hp!%Rs+kaYpcT==##R(+dqMri0@ z(Ua~$*ZiDOhTH4>9^Z#_RC_W@7A}30XB_E5fe_Lq4{8`lZ18b#IiQo8ON7I2uo zTh65ogi5N$B!gL$UISh!|}m$F^H79S9P#e zF+*%?cjdY2R?ovbukRlsX*GU%ol&$s{;W9L8TL`R(d;%*pvjorP26UA<^i1G!Yw~G zy8ChequXf_*`ITqUep9A6Q7dG2MKcts1klLvdaF@VS((JH%+uDYBMzRz$5!4nzLG~ zclbo%bzJ7crPVe|4%0PtDXq_*ITxU=(a?)npfiJGQv{~p@lhtEP$LUdW; zbXlO|Mg{vXSx}@11dT(`85YK9YT=_(eoF%%OS=ax{?Ln^nW38WCx!RK6p@%5>y0sD zmo_gyYgN$pT_!dO!YjURvf3T*Hw(eWyuI4DMZ|tfo-Su!m zlXXMXinh^@?=^k98pXnf-M)?)BeG1;MXAus3Ic|}p->3Ze`FoEB_umk1cyLt;++CG z6tn_`G3F`G(J`kLEnM=c9mDhglownBd@ZF0KZPmbiGiO8*!_&Rbv;>#%o|N-LQJ*2 zyRuKSyyVl(XY`j>F=%%$vlooQ?1FBxrJ+R)OKF2_sLXs}O0CW! zRMR=#W9dMtsdjrcKCwOrA2DF6tG9)xPBuLys10I@$@*>{y*#s@yH#1@L6u)MvVzUj*x;sLE1EiL5Bpx@t=zmY z7u)4-aV`43C6zbj-lG#IhM*HhXAVx!xPOU5ir|h}Lqwy|vA-e+!16J+%yeOCb$bxtxN`&&LGE^Uf-H-Y=49Zjy+LRAror zzS`j0l#u%5z3te%!pKRrS29{xd}J%?I%DU3)$UMoUOT_=Uvd*#9P%+S=X=Jj@_hdR z3pUg^0rJ$%GR0Yw zHWTHf*=6_J+Ifb88)knXFq-O<{LY;tKQi-J>%4|Ux{aTq4lV=^F<-sFBvQ^i&3RLt z71Zao!tgX=LyF>M$dkLQVIjiKEfcRzsdm~Cu#8A!-K?JObNqsNl#pKVr2mZ#enK;C zgXV^=DgXJ0yX-u+y^pn8Z~W0{8I97__S|YIPO-9IhevNuXC%@mI9=jC>p4NHL36I( ztOGN(NNhbw>BgpOnSF?3%;rZnxA)wYm2z`tOcumf9O}rIM3w#4Hn=pjBgEqR!-F>* zbohP`V8C&HAV>^8pE|Q!!?#U=q>Dc=`?Zj&VhwIuXRgJ8m=NA&<>sCoutVos6iMeA z4AAG3f3T*HsC2Fo$6)7o%txnChc|y?eVjXpI9X>2;d4exCoXSi>s(m@$0dv6FOG>= zu7vwA-sm`-s|~RV0R4H#Vq_VU8S62YI}tOoksD}sA15=h2C%q+PCoJOVy4Efz+C*# z+SSYzk1Sl)^S!Q%N+fqB{86c{VLq%l@bN>7k57zp=Zb)_!dt&Pvd1CxAC>g-WB5NR zH4*rL=wV?edVBw;_>Ln8k#O3VdDdk(z-QCcvrAbs#SNe)IvvlOK$`u2V$D30Q1$Uw zc;d7hEqbbqCK|#M(@YVND;t9=ivG|YN>ZN9i@dl<;}nyj9XPg&?*c8%-;aPfHlZNn zG53$G=n*ZNE3f!8TYx9GZdCCrG>4WSrd(l{4gTj>?x;nru}{xn(|H?e%<%3nrr>N5Jdbj{aeS5wR&-mThG!!cW6| zR;SY?n43@F1}s@4U%p(+VSaYNj$gJWrH~1Ib6j`E`-+(%s@|ckj{ElSpBmqBz{qA@ z12NUcVvLqOTZk4YxAjsEW5u{Wviwbi=g`!stu=Ab&UQ%sO$qviS)|C%L5{%XdH5N( zEvd){FT91C@C!p13~fE$1}uIjU$TsD1G`bJ-nZQ1J(Ro~tiLDP0kNcBPHU>h)>b{M zG$_95jdqYZ0s zxa~?~FQekk$EfnM zG%ku}{>z;wQ!fuiD`hW@C`!fV;S>94-;^o-c7BbXkHo$_#!1+=J4WAI$h%tOwmKPN zwJ7!g0Tbw;b?(E3HlcO0S3jS*B)9F1pN||&^Gk1Z4zMyp(L!-1gihWE(wY@U1$f5W z(idIq#-xn0(_is72hdoh?L4NX&Py*f^onF0W2l#&n)OS@=QF^+va~>|YLP z?0>)7o_({$g>EXe1Z7-LpfqwyvP#$ktq+P=Z1tSSzQe5r*h@?8TgMlUfFxHL=5>=j z@4)r|g>FrERPn04uv+>s1A(jjg(X_$1=}aC&jPgiBcSg`bcs%h99Ux9`5;b=F9Je% z<>-_88Zh{jsh&b%N}FC5>y(iS^UAAYvJm@TS7YS_Fim+ATDF@Vb`yICjVf9U2en)~ zioYS7nZi3XGQw&~66HzqJC+fRc5((baI5g!;jCbSb*w!fj+NShJ%8QDT)v5%^XR%C zcd7Q1cx{y&hTffAw=p>#)$nV?ZWl^T>T3kSoy{d#8TB@dw8TRglY1acs}*534vpMW zh;SPUD^KK{`*ONEBJ}fJSRAI(rfLhW?Wh<~yMegQQhsy77J!s&@LhIswUV^9l~!yW N@0;6}C@?sh_#fgm^ico+ literal 0 HcmV?d00001 diff --git a/scripts/update_profile_pictures.py b/scripts/update_profile_pictures.py new file mode 100644 index 0000000..51a25e7 --- /dev/null +++ b/scripts/update_profile_pictures.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Upload local profile pictures to Supabase and update user/profile records. + +Usage: + python scripts/update_profile_pictures.py --dir profile_pictures --apply + python scripts/update_profile_pictures.py --dir profile_pictures # dry run +""" + +import argparse +import asyncio +import mimetypes +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from loguru import logger + +from src.database import db +from src.services.storage_service import storage + + +def guess_content_type(path: Path) -> str: + content_type, _ = mimetypes.guess_type(str(path)) + return content_type or "image/jpeg" + + +async def process_directory(directory: Path, apply: bool) -> None: + if not directory.exists() or not directory.is_dir(): + raise ValueError(f"Directory not found: {directory}") + + files = sorted([p for p in directory.iterdir() if p.is_file()]) + if not files: + print("No files found.") + return + + updated = 0 + skipped = 0 + + for path in files: + email = path.stem.strip() + if "@" not in email: + print(f"Skip (no email in filename): {path.name}") + skipped += 1 + continue + + user = await db.get_user_by_email(email) + if not user: + print(f"User not found for email: {email}") + skipped += 1 + continue + + content_type = guess_content_type(path) + file_bytes = path.read_bytes() + + print(f"\n{path.name} -> {email} ({content_type}, {len(file_bytes)} bytes)") + + if not apply: + print("DRY RUN: would upload and update profile.") + continue + + try: + uploaded_url = await storage.upload_media( + file_content=file_bytes, + content_type=content_type, + user_id=str(user.id), + ) + + await db.update_profile(user.id, {"profile_picture": uploaded_url}) + linkedin_account = await db.get_linkedin_account(user.id) + if linkedin_account: + await db.update_linkedin_account( + linkedin_account.id, + {"linkedin_picture": uploaded_url} + ) + + if db.admin_client: + try: + await asyncio.to_thread( + lambda: db.admin_client.auth.admin.update_user_by_id( + str(user.id), + { + "user_metadata": { + "picture": uploaded_url, + "linkedin_picture": uploaded_url + } + } + ) + ) + except Exception as exc: + logger.warning(f"Failed to update auth user metadata: {exc}") + else: + logger.warning("No service role key available; cannot update auth.users metadata.") + + updated += 1 + print(f"Updated profile picture: {uploaded_url}") + except Exception as exc: + logger.error(f"Failed to update {email}: {exc}") + skipped += 1 + + print(f"\nDone. Updated: {updated}, Skipped: {skipped}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Upload profile pictures to Supabase and update users.") + parser.add_argument("--dir", default="profile_pictures", help="Directory with profile pictures") + parser.add_argument("--apply", action="store_true", help="Apply changes (otherwise dry run)") + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + asyncio.run(process_directory(Path(args.dir), apply=args.apply)) diff --git a/src/orchestrator.py b/src/orchestrator.py index 92ea072..f44d622 100644 --- a/src/orchestrator.py +++ b/src/orchestrator.py @@ -21,6 +21,7 @@ from src.agents.style_validator import StyleValidator from src.agents.readability_checker import ReadabilityChecker from src.agents.quality_refiner import QualityRefinerAgent from src.database.models import PostType +from src.utils.post_cleanup import sanitize_post_content class WorkflowOrchestrator: @@ -658,6 +659,7 @@ class WorkflowOrchestrator: company_strategy=company_strategy, # Pass company strategy strategy_weight=strategy_weight # NEW: Pass strategy weight ) + current_post = sanitize_post_content(current_post) else: # Revision based on feedback - pass full critic result for structured changes report_progress("Writer überarbeitet Post...", iteration, None, writer_versions, critic_feedback_list) @@ -677,8 +679,9 @@ class WorkflowOrchestrator: company_strategy=company_strategy, # Pass company strategy strategy_weight=strategy_weight # NEW: Pass strategy weight ) + current_post = sanitize_post_content(current_post) - writer_versions.append(current_post) + writer_versions.append(sanitize_post_content(current_post)) logger.info(f"Writer produced version {iteration}") # Report progress with new version @@ -750,7 +753,7 @@ class WorkflowOrchestrator: profile_analysis=profile_analysis.full_analysis, example_posts=example_post_texts ) - current_post = polished_post + current_post = sanitize_post_content(polished_post) logger.info("✅ Post polished (Formatierung erhalten)") else: logger.info("✅ No quality issues, skipping polish") @@ -772,7 +775,7 @@ class WorkflowOrchestrator: generated_post = GeneratedPost( user_id=user_id, topic_title=topic.get("title", "Unknown"), - post_content=current_post, + post_content=sanitize_post_content(current_post), iterations=iteration, writer_versions=writer_versions, critic_feedback=critic_feedback_list, diff --git a/src/utils/post_cleanup.py b/src/utils/post_cleanup.py new file mode 100644 index 0000000..6cac707 --- /dev/null +++ b/src/utils/post_cleanup.py @@ -0,0 +1,22 @@ +"""Utilities to sanitize generated post content.""" +import re + + +def sanitize_post_content(text: str) -> str: + """Remove markdown bold and leading 'Post' labels from generated content.""" + if not text: + return text + + cleaned = text.strip() + + # Remove leading "Post" or "**Post**" labels + cleaned = re.sub(r'^\s*(\*\*post\*\*|post)\s*[:\-–—]*\s*', '', cleaned, flags=re.IGNORECASE) + + # Remove markdown bold markers, keep inner text + cleaned = re.sub(r'\*\*(.+?)\*\*', r'\1', cleaned) + cleaned = re.sub(r'__(.+?)__', r'\1', cleaned) + + # Remove any leftover bold markers + cleaned = cleaned.replace('**', '').replace('__', '') + + return cleaned.strip() diff --git a/src/web/app.py b/src/web/app.py index 6f477ae..ea6c0b2 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -70,8 +70,8 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): "default-src 'self'; " "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: blob: https://*.supabase.co https://*.linkedin.com https://media.licdn.com; " - "connect-src 'self' https://*.supabase.co; " + "img-src 'self' data: blob: https://*.supabase.co https://supabase.onyva.dev https://*.linkedin.com https://media.licdn.com; " + "connect-src 'self' https://*.supabase.co https://supabase.onyva.dev; " "font-src 'self' data:; " "frame-ancestors 'none'; " "base-uri 'self'; " diff --git a/src/web/templates/user/company_accounts.html b/src/web/templates/user/company_accounts.html index 63c3eb6..d8c4e28 100644 --- a/src/web/templates/user/company_accounts.html +++ b/src/web/templates/user/company_accounts.html @@ -78,8 +78,8 @@
- {% if employee.linkedin_picture %} - + {% if employee.profile_picture or employee.linkedin_picture %} + {% else %} {{ (employee.display_name or employee.linkedin_name or employee.email)[0] | upper }} {% endif %} diff --git a/src/web/templates/user/company_manage.html b/src/web/templates/user/company_manage.html index fbc890c..afb0278 100644 --- a/src/web/templates/user/company_manage.html +++ b/src/web/templates/user/company_manage.html @@ -15,8 +15,8 @@
- {% if emp.linkedin_picture %} - + {% if emp.profile_picture or emp.linkedin_picture %} + {% else %} {{ (emp.display_name or emp.email)[0] | upper }} {% endif %} @@ -44,8 +44,8 @@
- {% if selected_employee.linkedin_picture %} - + {% if selected_employee.profile_picture or selected_employee.linkedin_picture %} + {% else %} {{ (selected_employee.display_name or selected_employee.email)[0] | upper }} {% endif %} diff --git a/src/web/user/routes.py b/src/web/user/routes.py index 558a636..7c14f03 100644 --- a/src/web/user/routes.py +++ b/src/web/user/routes.py @@ -47,6 +47,7 @@ from src.agents.link_topic_builder import LinkTopicBuilderAgent from src.agents.strategy_importer import StrategyImporterAgent from src.services.post_insights_service import compute_post_insights, refresh_post_insights_for_account from src.services.insights_summary_service import generate_insights_summary +from src.utils.post_cleanup import sanitize_post_content # Router for user frontend user_router = APIRouter(tags=["user"]) @@ -68,16 +69,16 @@ async def get_user_profile_picture(user_id: UUID) -> Optional[str]: Note: session.linkedin_picture (OAuth login) should be checked by caller first. """ - # Check for connected LinkedIn account first - linkedin_account = await db.get_linkedin_account(user_id) - if linkedin_account and linkedin_account.is_active and linkedin_account.linkedin_picture: - return linkedin_account.linkedin_picture - - # Fall back to profile picture from setup process + # Prefer cached profile picture (Supabase) to avoid LinkedIn hotlink blocking profile = await db.get_profile(user_id) if profile and profile.profile_picture: return profile.profile_picture + # Fall back to connected LinkedIn account + linkedin_account = await db.get_linkedin_account(user_id) + if linkedin_account and linkedin_account.is_active and linkedin_account.linkedin_picture: + return linkedin_account.linkedin_picture + return None @@ -103,6 +104,48 @@ async def get_user_avatar(session: UserSession, user_id: UUID) -> Optional[str]: return None +async def cache_linkedin_picture( + picture_url: Optional[str], + user_id: UUID, + http_client: Optional["httpx.AsyncClient"] = None +) -> Optional[str]: + """Download LinkedIn profile picture once and store in Supabase.""" + if not picture_url: + return None + + try: + import httpx + close_client = False + client = http_client + if client is None: + client = httpx.AsyncClient(timeout=30.0) + close_client = True + + headers = { + "User-Agent": "Mozilla/5.0", + "Referer": "https://www.linkedin.com/", + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + } + response = await client.get(picture_url, headers=headers) + if response.status_code != 200 or not response.content: + return picture_url + + content_type = response.headers.get("content-type", "image/jpeg") + uploaded_url = await storage.upload_media( + file_content=response.content, + content_type=content_type, + user_id=user_id + ) + + if close_client: + await client.aclose() + + return uploaded_url or picture_url + except Exception as e: + logger.warning(f"Failed to cache LinkedIn picture: {e}") + return picture_url + + async def get_employee_permissions_or_default(user_id: UUID, company_id: UUID) -> dict: """Get employee permissions for a company, returning all-true defaults if no row exists.""" @@ -2926,6 +2969,7 @@ async def linkedin_callback( linkedin_user_id = userinfo.get("sub") linkedin_name = userinfo.get("name", "") linkedin_picture = userinfo.get("picture") + cached_picture = await cache_linkedin_picture(linkedin_picture, UUID(session.user_id), http_client=client) # Get vanity name if available (from profile API - optional) linkedin_vanity_name = None @@ -2955,7 +2999,7 @@ async def linkedin_callback( "linkedin_user_id": linkedin_user_id, "linkedin_vanity_name": linkedin_vanity_name, "linkedin_name": linkedin_name, - "linkedin_picture": linkedin_picture, + "linkedin_picture": cached_picture or linkedin_picture, "access_token": encrypted_access, "refresh_token": encrypted_refresh, "token_expires_at": datetime.now(timezone.utc) + timedelta(seconds=expires_in), @@ -2973,7 +3017,7 @@ async def linkedin_callback( linkedin_user_id=linkedin_user_id, linkedin_vanity_name=linkedin_vanity_name, linkedin_name=linkedin_name, - linkedin_picture=linkedin_picture, + linkedin_picture=cached_picture or linkedin_picture, access_token=encrypted_access, refresh_token=encrypted_refresh, token_expires_at=datetime.now(timezone.utc) + timedelta(seconds=expires_in), @@ -2982,6 +3026,12 @@ async def linkedin_callback( await db.create_linkedin_account(new_account) logger.info(f"Created LinkedIn account for user {session.user_id}") + if cached_picture: + try: + await db.update_profile(UUID(session.user_id), {"profile_picture": cached_picture}) + except Exception as e: + logger.warning(f"Failed to update profile picture: {e}") + # Clear state cookie and redirect to settings response = RedirectResponse(url="/settings?success=linkedin_connected", status_code=302) response.delete_cookie("linkedin_oauth_state") @@ -4333,6 +4383,7 @@ async def chat_generate_post(request: Request): company_strategy=company_strategy, strategy_weight=post_type.strategy_weight ) + post_content = sanitize_post_content(post_content) # Generate conversation ID import uuid @@ -4446,6 +4497,7 @@ async def chat_refine_post(request: Request): company_strategy=company_strategy, strategy_weight=getattr(post_type, 'strategy_weight', 0.5) ) + refined_post = sanitize_post_content(refined_post) return JSONResponse({ "success": True, @@ -4470,7 +4522,7 @@ async def chat_save_post(request: Request): try: data = await request.json() - post_content = data.get("post_content", "").strip() + post_content = sanitize_post_content(data.get("post_content", "").strip()) post_type_id = data.get("post_type_id") chat_history = data.get("chat_history", []) @@ -4501,7 +4553,7 @@ async def chat_save_post(request: Request): for item in chat_history: if 'ai' in item and item['ai']: - writer_versions.append(item['ai']) + writer_versions.append(sanitize_post_content(item['ai'])) # Store user feedback as "critic feedback" if 'user' in item and item['user']: critic_feedback_list.append({ @@ -4620,7 +4672,7 @@ async def update_chat_post(request: Request, post_id: str): post_uuid = UUID(post_id) data = await request.json() - post_content = data.get("post_content", "").strip() + post_content = sanitize_post_content(data.get("post_content", "").strip()) chat_history = data.get("chat_history", []) if not post_content: @@ -4641,7 +4693,7 @@ async def update_chat_post(request: Request, post_id: str): for item in chat_history: if 'ai' in item and item['ai']: - writer_versions.append(item['ai']) + writer_versions.append(sanitize_post_content(item['ai'])) # Store user feedback as "critic feedback" if 'user' in item and item['user']: critic_feedback_list.append({ @@ -4751,6 +4803,7 @@ async def company_chat_generate_post(request: Request): company_strategy=company_strategy, strategy_weight=post_type.strategy_weight ) + post_content = sanitize_post_content(post_content) return JSONResponse({ "success": True, @@ -4777,7 +4830,7 @@ async def company_chat_save_post(request: Request): try: data = await request.json() employee_id = data.get("employee_id") - post_content = data.get("post_content", "").strip() + post_content = sanitize_post_content(data.get("post_content", "").strip()) post_type_id = data.get("post_type_id") chat_history = data.get("chat_history", []) topic_title = data.get("topic_title", post_content[:80] if post_content else "Chat Post") @@ -4794,7 +4847,7 @@ async def company_chat_save_post(request: Request): if not perms.get("can_create_posts", True): return JSONResponse({"success": False, "error": "Keine Berechtigung"}, status_code=403) - writer_versions = [item['ai'] for item in chat_history if 'ai' in item and item['ai']] + writer_versions = [sanitize_post_content(item['ai']) for item in chat_history if 'ai' in item and item['ai']] critic_feedback_list = [] for item in chat_history: if 'user' in item and item['user']: