From 8cc329ab17651d3ca9809a7740a9249bdf4b410e Mon Sep 17 00:00:00 2001 From: migatu Date: Fri, 24 Jul 2026 17:57:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(prezentacja):=20zapami=C4=99tane=20predykc?= =?UTF-8?q?je=20okresowe=20(PRE-22)=20+=20wymaganie=20o=20cache-bustingu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Piata wskazowka partnerow: Kalendarz ma pozwalac liczyc predykcje dla KILKU okresow i je zachowywac, zeby wszystkie trafily potem do raportu („Skompiluj", PRE-23/24). Nowy predictions.js — magazyn w localStorage, tak jak wspolne dane formularza (PRE-21): serwer zostaje bezstanowy, zadne dane urodzeniowe ani tresci z baz nie laduja po stronie uslugi. Zakladka „Skompiluj" odczyta to samo miejsce przez window.astrololoPredictions (jedno zrodlo prawdy). Decyzje projektowe: - KLUCZEM TOZSAMOSCI JEST OKRES. Ponowne policzenie tego samego zakresu podmienia wpis zamiast dokladac duplikat — inaczej lista puchlaby przy kazdej probie z innym modelem albo budzetem. Dzieki temu zapis jest idempotentny, wiec dziala tez wariant bez strumienia (zapis przy wczytaniu strony z gotowym wynikiem) i odswiezenie niczego nie mnozy. - progress.js OGLASZA gotowy horoskop zdarzeniem `astrololo:horoscope` z profilem, zamiast sam zapisywac. To okno postepu, a nie magazyn — zapisywanie zostaje odpowiedzialnoscia predictions.js. Filtr po profilu pilnuje, zeby interpretacja natalna nie trafila na liste predykcji okresowych. - Przepelniony magazyn (horoskopy bywaja dlugie) jest ZGLASZANY uzytkownikowi, a nie polykany — inaczej wynik znikalby po cichu. UI: lista zapamietanych predykcji na Kalendarzu — okres, data zapisu, objetosc i przycisk usuwania. Skrypt podpiety w timeline.html (nie w base.html), zeby nie kolidowac z rownolegle otwartym #29. PRE-26 — dopisane wymaganie o wersjonowaniu plikow statycznych. Przy PRE-25 przegladarka podala STARY styles.css i powiekszanie kosmogramu „nie dzialalo", mimo ze klasa byla nakladana. Objaw jest zdradliwy: szablony sa nowe, wiec strona wyglada na zaktualizowana, a funkcja po prostu milczy. Po wdrozeniu moze to spotkac uzytkownikow. Weryfikacja na zywej aplikacji: dwa okresy zapisane; powtorzenie tego samego zakresu podmienilo wpis (dalej 2, tekst zaktualizowany); lista posortowana po dacie; predykcje przetrwaly przejscie na inna zakladke i powrot; interpretacja natalna NIE wpadla na liste; usuwanie zmniejszylo licznik 2 -> 1 i przerysowalo liste. Testy: 14 nowych. Calosc: prezentacja 101 passed. Co-Authored-By: Claude Opus 4.8 --- docs/astrololo_wymagania.xlsx | Bin 37303 -> 37744 bytes .../presentation/app/static/predictions.js | 150 ++++++++++++++++++ services/presentation/app/static/progress.js | 5 + .../presentation/app/templates/timeline.html | 8 + .../presentation/tests/test_predictions.py | 91 +++++++++++ 5 files changed, 254 insertions(+) create mode 100644 services/presentation/app/static/predictions.js create mode 100644 services/presentation/tests/test_predictions.py diff --git a/docs/astrololo_wymagania.xlsx b/docs/astrololo_wymagania.xlsx index 7935e61a98edc863ce1a0c50811a174445f55397..707ecd134445c4bafae6da0e1d637c2a9146292f 100644 GIT binary patch delta 10076 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

?$hzo-BwsE=#%nv`X~_qIzw z3{FP4ygMg98>86tFqtSApbsqIZEt0c9fRs4WC4}-E-orCiZ5WAOe}innWL-jMl;Ey zm?qKeHDXWDs%fz8rl5vYHC|EkUVA*x(Ab8##wiASik-|2d|=qk16Wzkj1BM}1`!Rb z@uq_HG!>kxsF}d*0Vff}Bo$|un%7+JDVWyaunO3+?1R+RoiLkXKMPsMS)mL#QzdZP zv#_A30bU*WA@4lv$uLJ)yK6!0WwEDnun(qtgC>baHj7c^7I}Omu3BpNn4VxBNvO{I zmV{l?zruMCD-#A9;Kw?{{A%?juxGOfO;=W>yD#=5Ul@0ixd^}9LMSV{NVx4@GEj(t78LkNfOWrw5SV{L}(Rwv)cDRzc)xZ;MV1&U|kb2e{ZvMNL*QVo4Z`a}bPnMV?A z&>EN7g35;~H0g&lZO(G94ucJYWx>fju>>-%Z|i1tvPR8BPTPe$woNTNwox~%q%Gf> z4q>V#V_pv>0eL5c)eWUp-&-uZh{>&#K0fWJ20M1N(Q>3vXo&At{B0#vXs{O_g|riX zRFU|yJJtw&{g#>v@UIC9cJYY_v$`NHqRJ7E1$d?TTte8qal&bGf|u(}?vL zx+mY=4O(whdtuNYhY0=_^A~aYe9h`c4B|xODVM_#7f{79^$}b=iYciHlPh2k?eziU znvw!8@&Y#Qg5$(Vx;Vs(MbIt(`7$C(>se{i{i}_zU`M7=HT!pt(7{uN1o?RJIKA$o z-57gVs(f^~S3&6}=m7+eK`ZOi>2USzyF9Zh23>RxE^&VT%MOT6Js9z!&<7D_4Pd|-)x6hXQ(Pqv8$RtUWw^-`+c}He@vr(`r7&)jKbKM-%Mmsejb$vVx&^=b#Ma*F z3cq5#YLvX$t!JGk4bpG!jZhr_Cwvy?N78wvegJ~R6B@nP9rorr_M$zAY;_&)(XQoo znwYkv_4TLBUh;u`t5SU@cj&Qk`UXjGD?Vh{oJLR6(t_6upOYeuARDceI{bKJRI5N% zjZ*)z4e*TiVqIeIUUi%`(Ze$I$LspjOcMyoJ$?8QccCs=yMRq=lc@vzdKj78+1f$h z0;pFj)r`it2y%Gw6i6#ytuF5jGX>H|h1E+`SMRchW7bX`l^BmBS4tb*?PnjaE{i`$ zWt-@FBaCG4xy#nC4PC#fyu4cuTv96G;1ChLID@P2yUv*g(Oe!nq)_5pxn5*~}^0Or3c{`)cayfY#^w{WD)YPC?apAB?*^yi3YZ zk8#i=yt|)vvBr2c_rr<~X25n0FyNUIl+`kSqED~y+^GYHmA6V2%_A{a`z=#DY12`A zG#C;g)wI-S)$UEAGcEbNrA<2aK*xp&@yw~Jg|{)acxAgY%6Vk_Aq#pH?U!d`6=VJH zkfzoxhgrpB%|GgyE(PyX2ELt-%_bpb_`w>cPdlHr#0OgWOL|Nn+_8Tj0|J-QeSB6r zp_|P?reuiHL(w)dmxF81+sCA5Nv+@V#n{{Es%XIDmTO05Vt!&U5)VT(ve>kY>~LWn zNxk;LWbnbsaCs0D-}l%h^d-x4@)5IXk3kopRp;A~WciT@v9&0j76WR+aV0+tlP;cb zl8*%^n$Ac@2a1k2>rxHHKubFgIKh?s4h1V#h_dp?J#@|$7G?(-v+E5X%3;LF{G@q* z`tU(S@PhI;wBR4DZycDlGXpCc9JZ~=Nn0Kr_M=4X?EF1OeFiNX(mwRU=H(mC8)O#q z((?{&xl&%MlVdwFsa6Y}M)D%0$|zwqG8^1%qR2z5i29ihMeTmV0O^_Q!JIH3jt&l~ z+DK-;N#orY_$JW=7lJ2WZ8nRIZtEFv!ujzR?x>KIfXzURF=V3j=txH)VuCIkhu~P( zT5siI6@GG}k<{2k&l7@=_kXndGc({Qg+{XSoRQs=7UvrVd*kUQ$Wv zac#w_jp>lW6m%mz#NlqS?Qw1#H~7F~+fCaUJda(EIN?1*s%CxC zNGvEbVxV)%K`3c|Jp>;jXj%RaapVdn`ZWEKWY{=qhQP0BkknI8`B!R#z=HVS&)a)J z`VPT+fqQYH6X+<+FeRC0hupLN!UJ(b;L@5cQ%9f2#TqxRe3PO`JO#xmMQI^mx9lJv zE2GeT-cY=ZI7R?m4fuAM zG1d(mM{3FIg(8f&PI=0XX0S+`D8w8~Ti+FKntafv79gGL@pULrw{zWbt;##Wo?=Ox zsPT`K{>v_p4U*X~?I!ku)bH11mHzNE>LX9}Ay78 zr9H>@0Bd@4jZ4`Z!#Cr&@>gZIbSF*2nMI$PUy%ZEJx-rF7r^pa)#=I4K`9a>{()6ZXxpDcvN~9D_SJ zZsSCD!+#E5Wj_qSfTF=`W-BZ!txkZEUBuy1;1d5OmBdQyx3g@enWZi_Swdw;DwWwn z1_T^w#ggjINpl=AcEXGHX?tc{Y$c0fRpemy{UG@oZBgaiQpqChp|&pUt)109>~SX$ zBb`GbEYq@~-u!^)o${BTezDykp^FdUX_dMPr=;eHD6cvCILm*CBvNNJy)M=IV6Ykn zx9B8(oYU>RRN#nE;;dtIBWmyfVG;u6#Q$ zef_a#4#do}+ytqUtt*;+7fyF+pW_148XQvhXO7dfNN(4itukH2#)g?y71;d&g>@eO zSoyVbWjR{Ipg2OQcrW?SETlcrGpL_9Sd|~n4xW~NOuY-gu{4OWX7}4!rRf)`xSEm5 zlxJ|~svI3<&Pe!8*fAjXLKHFns(kBH$~~FW|8`t_6me=KmA^|Xfa5PcA#wvt=^QtD zhtOD=E|W7-{A3@bxPoZUczKTj#2O3xyZW{z+B(gjQg^sw<)}N|W9C{Gms)Zho{57c4z8lofj;y0>K;Fyg_j0K@t9;XpdTt&|rg`<$-e_^+Ng`QR zUOL>#DTJDmuE1HwIWRv4S{d@UwnYW8sQ8M~{Ewi#`8=%#6Z><1hSWsD_?P%3TMewI zq%1(E-7d-3A z!Tp9KzXBKH&KSujvwgRj9jx%CW9K0fAdXvbZl=-{J4bxnjp98S)2U>RsbHG+eW^n^ zBaaSztF)lWrPcUEgpi|}En0P@LCXnNkS<_t=#0M~>2;0vo9})G+4ib_-F;va2@GyO+Xu@Z#-x0x$dFM;QuFe!l;;#X=-0wwuB zVEezWCKwP10}}-Li+lgi2o9l#Pwh*@Nrg_t_)qbFk@5dqkrMSkMTJC+f9{PEi6JHF z{*xpnVvzk?<9`A2|JyV-wKtLOpVjeSt6jJMtca8T^8-#Ih71e*CsE<~hp4M5LO^0e z{l9H$Pllf%^VEx=K%hNB5D4Xe!UlnYA-q0;K)#M(4^KZwE06!bX=?pn&k`}I(@6yX zt^Ai`icN(}Ci(9*hf8Fvr~kTJf(C*9dj6jg451gF+7(NjYMPAv&&)ZQ7}7W)wJ#0_ WS4|NH_MaZ=-{;HUhV?yGX>_7Z4RRRtJWTmS%o2-uHKLLETbkA5`##jXBupR~jP>h;W-MK7)+ zz+^~F%!J*;r`yt=r7s)iyFID zf$tycumib?_s6seMp6pvc7A+Y-2UHzcr?s1%%#C>euCnM|2HB)kMq#)DME;JpsDzE zAZ8%mQUz~Aqr=#@kmddn>14;IW9pb;@sPqDc|(IG3_P$tL=Z2gyqBOC)+d$}3@yTY zN-&n0SCc6RuY+)P0eH$>en|Q}xG#Qs3=5n|75OP%7cp%Kf%hr@W{9P?3f)+5v zYbE;gY8+o5mn+24_P{TD%jtm^L?I+$!Ij>G0wkKx^ivjRCj^^se|LCI^yU$;^&mDF zK(TH-P%l&(p)yWX7&M761=O!iC!9ahMv)RzO4q3nSCF}vpU!Op&yEr-Q4 z*^N@H;``#zCv1yKGD^S9fB%S*EledamG5Dr#*zH4mW;O4jb;hKst9yK3l3ly6}^6T z43x)z^@ zmU^A^e+&`7%CWL@q$WW)AVgvz7^0L`rwp#eb1LFi*6pEwBOWapjq4<#7_vCRlpY{s z5M^aO6R09YZ!{z-Dw7R(cvJk6mbLYQHt*o<UP7CAXU`U0$y`nhM`5f;w)i5H;L~(qYGySbL!>Js0K*UUgby9;h+EN@wd`frOvn;w<$vdP#B*J8+i`Zh#Bh#j$#aYkMm@;4%ls(PIJpAvW1npuBubYIq`IUZD(p)YmlIatXjhU0 zI+^Whtd<0oDo=6|;?)ntT}O@VrFPC4IoxVBpNMJWKn|jtuUigOrF^sQq%(}Y&xj;| z9a%$!VR-M&iN2Mu?Idt#$k+}jB9W8aHu{8KxKwFOttj2y7*Q!&K+mt)r!5#?u9S~? zlE@gkJSV_wE)lrRKQXlsdtE9OB73{Mx`dJt=eR2)zUqzT-Pz|J1>oVA^qc!5q+7eG z)PJ&I1kDmgAS<4*=zX1QHZ`e^@DP3FVG+8utTwjzT!l8X zJ6@f~om44_7Vr&+FGVB#YUOir$XgIFJS07p&rwk>C4YZ59;=!DypGKhTmCz^t+#p5 zPTO9~sfoF~B;>eqB&|4G=CGLPjGn_)t{vZDWe3ETZm zA5}>K08673aYUk&gd{jno}qy=sf^bf3mnuc>MmBKWOTCP8oZ5qx{VsOg!+ULoP;X3 z5$XM1nP7lx_wyK5&2PU3aZ19~@4^gM8bDut_qQmJT%Y^bF6)RB6rCWpX~l=CI>Gj* z>9VI7!|%rb+%x7rjF%68ZniwLrMpYixO%mN6@i|m9}PR_Tq!j`{&ChNwBR(4OvWpj zbpwM0>0pl`Ka+rgZ+Rmo0Y%LVUnD(=V`x#Ji3m(sOn#Yck!)87^1E9vGrfO!I*E~; zUg(rJk)iBgRuhTehi6taBm?Uj1^76FdY=8Cu~M$szJ1ZkukptpYn?8D)Vaq-TCf>B zz}7k!x5{KBkUm9&$O)L$a%KALKPR3X=^B+Gj6Yn*p8I`N)_4%|1h4n?@XgwOh3Sg# ztl)yE=rpox*_0Rghv#q(9F=rWF?wn0|M|8iwPC=XT;BTBsXxUx>D*vJjpJGPs<13q z|E=fYAm3#nZSv(#@z;;E!(DU?{>8w-cksr>%O4TWDQv~>Ang6lV3O=_H_zY)7TEQ5 zHr(}e^JgGLnFIo(aJ>VB?APLhBh&YG9l_Tf;pX4f&-_1KSBCE;m3iM`L5A?b-_!T5 z9l+s z5p$R4zzL0JhU|sLS_@7Bmm-NJcK0AcV_$th>nqkOm(FoHnnjUU>Qe+_DkN|& zrn2jdDm|((+kuZ0RDaZDP(-dbZIPxD_w>IDUVGe~a{O@?d$CR__P_`r&VVc^2@}8V zwen*VsQkF!Hmwv9W@N+{dsZc)>g7oZWDJK%A)ac{=z7Es);{9NxL%mL|5O^Fg;Uje z)yfb6YEPZ-WPw+%p^^Dj#3;qgF#d%Pn1kCG8O~wWsU)}}c~8HD<3Oa83tEySKGCGZ z$kuGgsaNUMji9X-cEd?er5Q5!H9}z}3BgM`?5l>x7SBe~DP>NlgAGc+<96N}Sgi8l zTI{69>+)$|EgHkHQwlU)x-V5oY0u&5jBHQ=a*Ubm;*hXius}{PI_CEdBUm~qgLv!Q$D0szV+gOO_7XCS%c}K6Ft3)XE>qH1Rv9mX(abnRV~AUhdo#rEmD?@!+y$+uIEh6g;3mC~b4Nuw^l%ihw| z(-TH`I#WA6hp6qhyUW&iiyRe087j>y(6y^r*4}mo!a=%*H-R^?R8EA8u7AX8doIr# zT<(vbiQ{ey68K%lQ9eNg!pOM<_lxR6b;H|9{KDUC?hi#!o#y|LH%YR|tNYEh()V6c zIWIpu$@L`Pj%vBGM@5Ivp5#1e*X|Z5>}aOr&5)PU;kb!K{Kji{T~1yvd_9j81Hoif z%2AENQa}1`hr84s_N&@lDLyhoerH{9Mdq!MdR^??Pyn+eG0ZXq&hETp?NWx8fV~kw zsVY~uN6-j-U#Z<9^W~~rNC~_o`AD3e((Zb_qt@PCiuYaD1-g4^4M}{q016%!816gj zRwB3G6)EDy$zblxG{7;e)iUK%hw;$<#5l-P*DG(_ChZ&NyD}63yj+n4c_B3_)UPTR z{GMs6Hs$j(K7k-?w;E{h#SrsDJ|3%ges3KY=?iuCTPSrC3EO0T=vaSCAO1+6bfd0c z%;KRqvRrB}vftYeXl$~;Gshh5jF$nlJU-};1fy>rkC4YD!~pqtZ@4!o_su;H1M72V zx8HtINGE|WWkjJzaKGq)N`ilfI(y`PE+T$4hEzQTdbpgA2$GU@_vIKT7~9Iw(F8?q zk5R7q`2f}sp>F~X83^eWH!V-brNb}G+`28!sm|PM{t2sofc3xyva=CmER<6qF}U*% zw#^@wVr4*M_#5I=xjUpNhl*N-)QYNFDkEL2s`X`zjP!J$p6wDub+&ZF@Wr)8VTJ*P zXq=KM8@I==l4mJN$SPKUXfc>f8Cv>}OPhEb&F-+9{CIqV^>ZBYa`TQ?+$q%=a<#AQ zdh59|YJBkPfq!%z!X%ri^o|6$IT@O?L_Bq8H@EP!x-aoj6SwYD&raVo?~qc zvv#L|Ek_%#^Xe5)_+*|^x!gD3h~_s(#@J92Uj^5W_mem83W1)BeF>6~vqPf< z`uo{tW}p>b!TaXMFCGMM4C{jj|6OuBY@Grhu4Px;7Y{M} zwbD{&MLn#bwV|KjwkaN!CZ2J1uKtQ`6)a8*zQW1QVRp`0U62LsJl_#tU4jUm^-qas z)9K$CZVZENEg(VQ_o|40aR3+|U9|Id)8DuSPZLWU@UJ{_0Pu8f3?S z1Cm~7Y*}^M`A7OLb~G9<%B)uPY#t_{>_uP;T+94&vG=Jm>(pp02%>uUk#>{4*j|AN z%&NA{MuoWN(WP=L7@olOW2gk+iNjpmQc#jTL=9L!m)ugp$*)XfEaF$Y%cd`;i&h?LtZk{2t%jtK>dDdFRGOM}&Y8 zT*Mkvcu^r@xFPJX5?hMWiTOAX+H zh&}oA3F6==*80Z7-$RjBp7#EcH%lFjJ&T}!j`k?>JBdIL>5er$m=Kd|*^?OYbHhL& z6+8Bh28P3pd75S1=NX-_0dmBVEG$?t1`cgfUhl6=GPSE#+{1|@N_p>ZSyj;)a|rQB zo%xLNX_0)F^BtA~bF?W(%O*c#B^Y&5@VBOfTwMaY8d(_8oK$BL%zM~&Muc}h$$Nn$ zv)!(>RkH~9@c*FVSApu zv2gQ2mDM)as*2wch9RKOH%)sU^SHY)^@tLaL68hfGHkEo<`PIGDP{pz38T@Jt(L)Q znbF9y>`zI$uA!sT;C#EC@4eoB^RobHke)ELsvjE-pp#_B?HKzUD3!Z^@o>B0O{X4e zk?jbhj#tU265YmiwtL%pX+gmXPn6)wpw==@e!H&_o8KD_y4czpu~4=s;aM99D?gm- z#??T}EU2PS3)Y1am*X*{gvV?WAsWtCoo49keoRcY{MaFySN)k`q2v#la%Ci_=?l)= zL5O8Nm77DRw>a)@0NVcNP^*TeHp$C#sQk`JoN?G+v(R2k{k4-M)M^YxQXR z!IT&bWeHwjdXZnw?^ARKy)M%2A};}_8l!u)zBkE|(-U@A2{%a1Z_ z;S>1-HqMIzq_XV6zAIx3wN!o}VP^lB)GMvh)eeqZ*_;R13=-!piRFrAbq1o}Qo?I% zA&Rf8h){|!1=%asZBF|87*GzPnH}obL=qU5!4Trl|(W()At!Pdxp3$&f;)NIcR|aP|jq6TR}j7bVm?iL+} zPn%PQ_*6{D@(kZx>+|ttPa^BLoWjRMRJ9!waVMPXjL4yKW1SXg#AO)gx{g1-d$ z9Q82T?uS>$awbZgA531=a`Q<=h5G(ntLEnC^$7QR$9;3oCkIc6<#W6_zAw(sXXGyN z`c)t^5BE#-=0d~HBvjpNMwxJK!@k|sK?e4Qri5fb*<1psSL5MCH-WNr z^m3cx3-GBQZ+lrifcL1dMlhf7E6k%L7_DR4z409p=2EbB&=v(wJ~FTTS`$}d@Wr+p zfy3K2d>g?Sz5i-PckOr$%o!jiaCY7j+87HD61QeL||2Z3KYj2}~GKhxS``NOYykbNB=_#${@r5ETQ+4a)V$i6MJdRf>#F-&0Uk1e-iX{X9*EfG{E zKVFgS-&wFgsC`R=?nmx=EV$-Dke&2-F(2Tf9X+Yl{n3e#_Lu6~hXQp``k(Q@9_GA+~ z_`r#}!If86*ckRXAdXI00YD*1noCpa3c*v_YB@)%1ntK5AQp?skw}DxjOpop&H3U| zG7V2>aVodk8{}7k9mbn>`E|*TP!|o=W1l{J>^Zr_%2Za06IIHVc9M5I8T;OZZW>`i zJWZf`Gnw^L?E-yyNpkgg&DZ9geR-+&se+{~wBu7U$#r1_Vy~oJZFWrikSCk*2{U^T zPq~o}FX(G(8=0Ld7L)(s7-8rYJ&l-Gke7_6y`3q%xsSRJ5_ULqV!X!QF4@JYhw}t} z;`P2zNBanQqUd@0YiO`A!6_;Dv*COh#}4)O*D+ME z1B9|-WzgvYgOhWiGIFsynB8>TaKtj+Nkf2$-#cJs5lYELYEq#uj>iWt;*`u(YWg8v zyQopTMGI}<`90+33ggj%M)`M6ER`W!!~U}}vDqEXJb!G2=c`yA=d&o@?C&>#*kcSU zP~Z=aX+Fs5#H;l)z3B31mBs1dW%AZ~W>;^dOD{OfF|~4VGHdt^Cyhdqo1i@W9YVg2 z&}v;KQ?EMT9@3kqHrxysgH_C0Pl<3AhMdlXI3!XFfdM{Oz&anx`DJTsI(* z>Na?)pBDA1?j;@S(7iR}6bONPQ#L)8?r2e3UJ~caF7Ux2^~QDKp8@-8b(RA`G!E!< z{Tw;;4r_8Ozl%omo9y6Q>%8o{t9>I^aYnCSx^#q&?kXp|O0LPC;zr_Lr2|sGTvu)y zadMcFsawWiz_rquI&+)t?^(|wX9>WCx_-s9m^UX+RQm;sufFG&7oXzMd8zA?EAqO0A8VIUtbg!!tLm(qo{J@ujgC z=P_Uqf@Ml2qhx-ndWLV_qHxf$qLvGIMxctSl12?avkryx~c6~WNh?WgQS8GyvNt%_*iD%tWm49WAJXsy>) z_E70Udi$EJaH8QPwAcB!FMq~*+;yG=G z`Bu4=r?dKZGqD;iB`01H_to~Im2XU2S3dZr;&pT07j92<4am3=U_ZEraE`!@TDzPH zbrQ(<0=VX}5k|urMt=b|!SU6xV<|F(ACYBv-0k8c>;(N+4@nTj5!epY!?99d~qa z!n}KrAXrlN=L0SD6?XTd9|H2t*&!U6VE9|ymr2(J`gI2~(5tP!sfA*h00YU6m743bGQRFf0E3O!pwqsZ^nICsGW#=ka zhA131{eWUY!@O^&2P4h8?LSF;?0iWZcV*qn!r77qBv;YfinSTvdoG03Krwx6sgq^# zTg2LNeYsGFy5g+l!?yT4#K61jp~WP)f-p$k^l>{yQ(~x1ps3IE-VOKZnE6s_fZs~{ zWwIsElmhwFShO|xa`+GAZB_Ivv8}g2g0r2WnvP@Ka`nhcJWB=c<2?jU2A7_h6Ct!S zvEMETgaX??3iBiA(BzshxQ23J?^kZQoL~?Gpl#jX)Mm~VN)=eFb$-wo!KGn5JRh%g zIm2D~l)Czn;170A4^WNOmW><|Lt}^*Ff{xzBTBw2&y*)uVj3fK*y-Cy|C(z~rv7cVG-9PjeMFFUn6HaHgB@J<3ll!qK>o?wR@NKRti z;I%z7c5Ri}Ih&VXIhT12e^ulq9G0>~YDw&Y@yWL@DQZ>d!Z2 zVN(dzR5ivlrIpTSuSIQXqVLgwe(y*PZ9`z^r@j)OS2`Z(+?hwLz0atY!;96nVY|d( z8@Da5$VQk~T5%J4riQs$Ir}?tKrPm`4PF6LwINfQ0o&dYm7Y86UIN@ADXCNXBv}OvOdCZ66_}~+ zOg@f?RS2;?IOXk3#?7W6FNkRml_-2p=sDW-GUeP)9VV=~{(20cHu0jPJSX=mk-!5K zBP0BQYMw*KxN^wV?nw4V8L!r)M?>?hsk>IzMvqtI{jia!h8kOP9nHrWF2bXo=J0DvQm1i@xRe_6E6V zVLXcbuBBEtyprC!15fp6sGyK&jgr@@vr4i7giyF-(d5f?WuNP-qMUHCCRg>Ee)7vSR5cr8=q{ zS0Tfx1W%hz+%Ab-l_4L)_lmBZJGCN1)D84`i&d7_a;-ntjrjAF>Y~`(ff-s+>R56c zl^UlXZaGOe+918Co8hstx100$VoF&uXgQ;-Vob*Nc6r=B4BhE~T9Kya72P!u?n70i zzvv-YRzd?3#W<`hFK0wVh+?i-p5m~!(@I!ER%Vg1u3VSCLvkwCDq3ziqj9DzUW-z! zr>xdDYw?i<*Z*wyV5=Cpk*46(_c?ExR9A|e((3U~dwi68y}!=j%I`Fz_V*08b#@eu z$cdRk$(kb2#rW2;+OxCX^G#1q2I2uzXJY% z0au8BF*srZ<{wN^2LJlIsqzlk#+QyU#o002X(|4(Fa`G-jV*O8+nQs{BN zf7M8m|86aZl2X64|{!TxtvRe*xVh51k0)1%=#4V)JM z8UUDp0{}4o'; + return; + } + var rows = list.map(function (p) { + var chars = (p.text || '').length; + return '' + + '' + fmtDate(p.from_date) + ' – ' + fmtDate(p.to_date) + '' + + '' + fmtDate(p.saved_at) + '' + + '' + chars.toLocaleString('pl-PL') + ' znaków' + + '' + + ''; + }).join(''); + host.innerHTML = + '

' + + '' + + '' + rows + '
OkresZapisanoObjętość
'; + + Array.prototype.forEach.call(host.querySelectorAll('[data-drop]'), function (btn) { + btn.addEventListener('click', function () { + remove(btn.getAttribute('data-drop')); + render(); + }); + }); + } + + function saveCurrent() { + var entry = capture(); + if (!entry) return; + var note = document.getElementById('predictionsNote'); + if (upsert(entry)) { + if (note) note.textContent = 'Zapamiętano predykcję ' + + entry.from_date + ' – ' + entry.to_date + '.'; + } else if (note) { + // Horoskopy bywają długie; gdy magazyn się przepełni, trzeba powiedzieć + // wprost, zamiast po cichu zgubić wynik. + note.textContent = 'Nie udało się zapamiętać — magazyn przeglądarki pełny. ' + + 'Usuń starsze predykcje i spróbuj ponownie.'; + } + render(); + } + + function ready(fn) { + if (document.readyState !== 'loading') fn(); + else document.addEventListener('DOMContentLoaded', fn); + } + + ready(function () { + if (!document.getElementById('savedPredictions')) return; // nie ta zakładka + render(); + + // 1) horoskop policzony przez okno postępu (strumień) + document.addEventListener('astrololo:horoscope', function (e) { + if (!e.detail || e.detail.profile === 'period') saveCurrent(); + }); + + // 2) wariant bez strumienia: strona wróciła z gotowym wynikiem po zwykłym + // POST. Zapis jest idempotentny (klucz = okres), więc odświeżenie strony + // niczego nie duplikuje. + if (document.querySelector('#horoscopeText')) saveCurrent(); + }); + + // Udostępniamy magazyn zakładce „Skompiluj" (PRE-23) — jedno źródło prawdy. + window.astrololoPredictions = { read: read, remove: remove, key: KEY }; +})(); diff --git a/services/presentation/app/static/progress.js b/services/presentation/app/static/progress.js index 197a55a..f692d06 100644 --- a/services/presentation/app/static/progress.js +++ b/services/presentation/app/static/progress.js @@ -113,6 +113,11 @@ document.addEventListener('DOMContentLoaded', function () { overlay.hidden = true; const host = document.getElementById('promptResult'); if (host) host.scrollIntoView({ behavior: 'smooth', block: 'start' }); + // Gotowy horoskop ogłaszamy zdarzeniem, zamiast zapisywać go tutaj: + // to okno postępu, a nie magazyn. Nasłuchuje predictions.js (PRE-22). + document.dispatchEvent(new CustomEvent('astrololo:horoscope', { + detail: { profile: profile } + })); return; } addLine(ev.message || ev.type, ev.type === 'error' ? 'err' diff --git a/services/presentation/app/templates/timeline.html b/services/presentation/app/templates/timeline.html index 630d92d..8c0fc59 100644 --- a/services/presentation/app/templates/timeline.html +++ b/services/presentation/app/templates/timeline.html @@ -85,8 +85,16 @@ {% endfor %} {% endif %} +{# Zapamiętane predykcje okresowe (PRE-22) — materiał dla zakładki „Skompiluj" #} +
+
+

+
+ +{# po progress.js — nasłuchuje zdarzenia, które tamten wysyła po gotowym horoskopie #} + {% endblock %} diff --git a/services/presentation/tests/test_predictions.py b/services/presentation/tests/test_predictions.py new file mode 100644 index 0000000..5f6c1c6 --- /dev/null +++ b/services/presentation/tests/test_predictions.py @@ -0,0 +1,91 @@ +"""Zapamiętane predykcje okresowe (PRE-22). + +Magazyn żyje w JS (localStorage), więc znów pilnujemy go strukturalnie. Sedno: +predykcje mają PRZEŻYĆ przejście na inną zakładkę i trafić do raportu, a lista +nie może puchnąć od powtórzonych prób na tym samym okresie. +""" +import pathlib + +APP = pathlib.Path(__file__).resolve().parents[1] / "app" +JS = (APP / "static" / "predictions.js").read_text(encoding="utf-8") +PROGRESS = (APP / "static" / "progress.js").read_text(encoding="utf-8") +TIMELINE = (APP / "templates" / "timeline.html").read_text(encoding="utf-8") + + +# ─────────────────────────────── podpięcie ─────────────────────────────── + +def test_script_loaded_on_calendar_tab(): + assert "predictions.js" in TIMELINE + + +def test_loaded_after_progress_script(): + """Nasłuchuje zdarzenia, które wysyła progress.js — musi być załadowany po nim, + inaczej przegapi pierwsze wywołanie.""" + assert TIMELINE.index("progress.js") < TIMELINE.index("predictions.js") + + +def test_calendar_has_container_for_the_list(): + assert 'id="savedPredictions"' in TIMELINE + + +def test_progress_announces_finished_horoscope(): + """Okno postępu ogłasza gotowy horoskop zdarzeniem, zamiast samo go zapisywać + — to okno postępu, a nie magazyn.""" + assert "astrololo:horoscope" in PROGRESS + assert "profile" in PROGRESS.split("astrololo:horoscope")[1][:200] + + +# ─────────────────────────────── zachowanie ────────────────────────────── + +def test_only_period_horoscopes_are_stored(): + """Interpretacja natalna ma NIE trafiać na listę predykcji okresowych.""" + assert "'period'" in JS + + +def test_same_period_replaces_instead_of_duplicating(): + """Ponowne policzenie tego samego zakresu podmienia wpis. Inaczej lista + puchłaby przy każdej próbie z innym modelem.""" + assert "function upsert" in JS + assert "p.from_date === entry.from_date" in JS + assert "p.to_date === entry.to_date" in JS + + +def test_entry_carries_period_and_text(): + for field in ("from_date", "to_date", "text", "saved_at"): + assert field in JS, f"wpis nie niesie pola {field}" + + +def test_incomplete_result_is_not_stored(): + """Bez tekstu albo bez dat nie ma czego zapamiętać — nie zapisujemy śmieci.""" + assert "if (!text || !from || !to) return null;" in JS + + +def test_list_can_be_pruned(): + assert "function remove" in JS + assert "data-drop" in JS + + +def test_full_storage_is_reported_not_swallowed(): + """Horoskopy bywają długie. Gdy magazyn się przepełni, użytkownik musi się + o tym dowiedzieć, zamiast po cichu stracić wynik.""" + assert "pełny" in JS or "pelny" in JS + assert "return false;" in JS + + +def test_corrupt_storage_does_not_break_the_page(): + assert "catch" in JS and "Array.isArray" in JS + + +def test_storage_is_exposed_for_the_compile_tab(): + """Zakładka „Skompiluj" (PRE-23) ma czytać to samo miejsce — jedno źródło prawdy.""" + assert "window.astrololoPredictions" in JS + + +def test_does_nothing_on_other_tabs(): + assert "if (!document.getElementById('savedPredictions')) return;" in JS + + +def test_reload_does_not_duplicate(): + """Wariant bez strumienia zapisuje przy wczytaniu strony — klucz to okres, + więc odświeżenie nie mnoży wpisów.""" + assert "id: from + '..' + to" in JS -- 2.52.0