diff --git a/services/presentation/app/main.py b/services/presentation/app/main.py index 5421fb8..bbbd611 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -128,24 +128,32 @@ def compile_build( lat: float = Form(0.0), lon: float = Form(0.0), house_system: str = Form("whole_sign"), + house_systems: list[str] = Form([]), # PRE-05 — porównanie domów w podsumowaniu + stations: bool = Form(False), # LOG-03 — stacje też w podsumowaniu zodiac: str = Form("tropical"), + tables: bool = Form(False), # LOG-23 — żywioły/faza/godziny w podsumowaniu ): """Składa raport: horoskop liczymy TU NA NOWO, a części od AI (interpretacja natalna i predykcje okresowe) dokłada przeglądarka z magazynu (PRE-22/23). - Dlaczego horoskop liczymy ponownie, zamiast go zapamiętywać: to czysta funkcja - danych wejściowych — tanio ją powtórzyć, a odpada trzymanie w przeglądarce - dużego wyniku, który mógłby się rozjechać z aktualnym formularzem. + Horoskop liczymy ponownie (czysta funkcja wejścia — tanio powtórzyć, bez + trzymania dużego wyniku w przeglądarce), ale z TYMI SAMYMI opcjami, które + wybrano przy horoskopie (stacje, tabele, porównanie domów) — inaczej + podsumowanie miałoby braki względem tego, co policzono. Opcje wędrują między + zakładkami przez formsync. """ form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset, - "lat": lat, "lon": lon, "house_system": house_system, "zodiac": zodiac} + "lat": lat, "lon": lon, "house_system": house_system, + "house_systems": house_systems, "stations": stations, + "zodiac": zodiac, "tables": tables} ctx: dict = {"form": form, "result": None, "error": None, "moment": None} try: iso_utc, label = _build_utc(date, time, tz_offset) ctx["moment"] = label ctx["result"] = logic.positions( when_utc_iso=iso_utc, lat=lat, lon=lon, - house_system=house_system, zodiac=zodiac, + house_system=house_system, house_systems=house_systems, + stations=stations, zodiac=zodiac, tables=tables, ) from app import chartwheel ctx["wheel_svg"] = chartwheel.render(ctx["result"]) @@ -189,6 +197,8 @@ def compile_pdf(payload: dict): when_utc_iso=iso_utc, lat=float(data.get("lat") or 0.0), lon=float(data.get("lon") or 0.0), house_system=str(data.get("house_system") or "whole_sign"), + house_systems=[s for s in (data.get("house_systems") or []) if s], + stations=bool(data.get("stations")), tables=bool(data.get("tables")), zodiac=str(data.get("zodiac") or "tropical"), ) from app import chartwheel diff --git a/services/presentation/app/static/compile.js b/services/presentation/app/static/compile.js index d4c131c..7f89797 100644 --- a/services/presentation/app/static/compile.js +++ b/services/presentation/app/static/compile.js @@ -93,10 +93,23 @@ var el = document.querySelector('[name=' + name + ']'); return el ? el.value : ''; } + function checked(name) { + var el = document.querySelector('[name=' + name + ']'); + return !!(el && el.checked); + } + function multi(name) { + var out = []; + document.querySelectorAll('[name=' + name + ']:checked').forEach(function (el) { out.push(el.value); }); + return out; + } + // Opcje (stacje, tabele, porównanie domów) MUSZĄ jechać do PDF-a — inaczej + // PDF liczyłby okrojony horoskop i miałby braki względem podsumowania. return { date: v('date'), time: v('time'), tz_offset: v('tz_offset'), lat: v('lat'), lon: v('lon'), house_system: v('house_system'), zodiac: v('zodiac'), + house_systems: multi('house_systems'), + stations: checked('stations'), tables: checked('tables'), place: (document.querySelector('#geoSearch') || {}).value || '' }; } diff --git a/services/presentation/app/static/formsync.js b/services/presentation/app/static/formsync.js index 6551af4..c1bdf51 100644 --- a/services/presentation/app/static/formsync.js +++ b/services/presentation/app/static/formsync.js @@ -20,18 +20,31 @@ // Pola wspólne dla zakładek. Nazwa pola formularza -> jak je znaleźć. // `geoSearch` (nazwa miejsca) nie ma atrybutu name — szukamy po id. + // type: brak = zwykła wartość (.value); 'check' = pojedynczy checkbox (.checked); + // 'multi' = kilka checkboxów o tej samej nazwie (lista zaznaczonych wartości). + // Opcje (stacje, tabele, porównanie domów) są tu, żeby wybór z „Horoskopu" + // wędrował na „Skompiluj" — inaczej podsumowanie liczyłoby się bez nich. var FIELDS = [ - { key: 'person', sel: 'input[name=person]' }, - { key: 'date', sel: 'input[name=date]' }, - { key: 'time', sel: 'input[name=time]' }, - { key: 'tz_offset', sel: 'input[name=tz_offset]' }, - { key: 'lat', sel: 'input[name=lat]' }, - { key: 'lon', sel: 'input[name=lon]' }, - { key: 'house_system', sel: 'select[name=house_system]' }, - { key: 'zodiac', sel: 'select[name=zodiac]' }, - { key: 'place', sel: '#geoSearch' } + { key: 'person', sel: 'input[name=person]' }, + { key: 'date', sel: 'input[name=date]' }, + { key: 'time', sel: 'input[name=time]' }, + { key: 'tz_offset', sel: 'input[name=tz_offset]' }, + { key: 'lat', sel: 'input[name=lat]' }, + { key: 'lon', sel: 'input[name=lon]' }, + { key: 'house_system', sel: 'select[name=house_system]' }, + { key: 'zodiac', sel: 'select[name=zodiac]' }, + { key: 'place', sel: '#geoSearch' }, + { key: 'stations', sel: 'input[name=stations]', type: 'check' }, + { key: 'tables', sel: 'input[name=tables]', type: 'check' }, + { key: 'house_systems', name: 'house_systems', type: 'multi' } ]; + function elementsOf(f) { + if (f.type === 'multi') return Array.prototype.slice.call(document.querySelectorAll('[name=' + f.name + ']')); + var el = document.querySelector(f.sel); + return el ? [el] : []; + } + function load() { try { return JSON.parse(localStorage.getItem(KEY) || '{}') || {}; @@ -50,21 +63,35 @@ function apply(state) { FIELDS.forEach(function (f) { - var el = document.querySelector(f.sel); - if (!el) return; + var els = elementsOf(f); + if (!els.length) return; var v = state[f.key]; - if (v === undefined || v === null || v === '') return; - if (el.value !== v) el.value = v; + if (v === undefined || v === null) return; + if (f.type === 'multi') { + if (!Array.isArray(v)) return; + els.forEach(function (el) { el.checked = v.indexOf(el.value) !== -1; }); + } else if (f.type === 'check') { + els[0].checked = !!v; + } else { + if (v === '') return; + if (els[0].value !== v) els[0].value = v; + } }); } function collect() { var state = load(); FIELDS.forEach(function (f) { - var el = document.querySelector(f.sel); - if (!el) return; // pola nieobecnego na tej zakładce nie kasujemy - if (el.value === '' || el.value === null) return; - state[f.key] = el.value; + var els = elementsOf(f); + if (!els.length) return; // pola nieobecnego na tej zakładce nie kasujemy + if (f.type === 'multi') { + state[f.key] = els.filter(function (el) { return el.checked; }).map(function (el) { return el.value; }); + } else if (f.type === 'check') { + state[f.key] = els[0].checked; + } else { + if (els[0].value === '' || els[0].value === null) return; + state[f.key] = els[0].value; + } }); return state; } @@ -72,12 +99,12 @@ // 1) odtwórz to, co użytkownik wpisał gdzie indziej apply(load()); - // 2) zapisuj każdą zmianę (input dla wpisywania, change dla list i kalendarzy) + // 2) zapisuj każdą zmianę (input dla wpisywania, change dla list, checkboxów, kalendarzy) FIELDS.forEach(function (f) { - var el = document.querySelector(f.sel); - if (!el) return; - ['input', 'change'].forEach(function (ev) { - el.addEventListener(ev, function () { save(collect()); }); + elementsOf(f).forEach(function (el) { + ['input', 'change'].forEach(function (ev) { + el.addEventListener(ev, function () { save(collect()); }); + }); }); }); diff --git a/services/presentation/app/templates/_result_tables.html b/services/presentation/app/templates/_result_tables.html new file mode 100644 index 0000000..738c73e --- /dev/null +++ b/services/presentation/app/templates/_result_tables.html @@ -0,0 +1,232 @@ +{# Tabele wyniku horoskopu — używane przez „Skompiluj" (/compile), żeby + podsumowanie pokazywało dokładnie to, co policzono (regresja 2026-07-28: + podsumowanie miało braki). Skopiowane 1:1 z sekcji tabel na „Horoskop" (/). + TODO: przełączyć też chart.html na ten include, żeby oba widoki nie mogły się + rozjechać — na razie pilnować ręcznie, że są zgodne. Rysunki (koło/aspektarian/ + deklinacja/antyscja) renderuje rodzic; tu są same tabele. Każda sekcja pokazuje + się tylko, gdy jej dane są w wyniku (opcja policzona → sekcja jest). #} + +{% if result.angles %} + + + + {% for key in ["Asc", "MC", "Dsc", "IC"] %} + {% set a = result.angles[key] %} + + {% endfor %} + +
ZnakW znaku
{{ a.name }}{{ a.sign }}{{ a.in_sign }}
+{% endif %} + +{# Porównanie systemów domów (PRE-05) — te same osie, inny podział na domy #} +{% if result.house_systems and result.house_systems | length > 1 %} +{% set HSN = {'whole_sign': 'Whole Sign', 'equal': 'Equal', 'porphyry': 'Porphyry'} %} +
Porównanie systemów domów ({{ result.house_systems | length }})
+ + {% for hs in result.house_systems %}{% endfor %} + + {% for i in range(12) %} + + {% for hs in result.house_systems %} + + {% endfor %} + + {% endfor %} + +
Dom{{ HSN.get(hs.system, hs.system) }}
{{ i + 1 }}{{ hs.cusps[i].sign_glyph }} {{ hs.cusps[i].in_sign }}
+{% endif %} + + + + + + + + + + + {% for p in result.positions %} + + + + + + + + + + + {% endfor %} + +
Sym.ObiektZnakW znakuDomKier.Prędkość °/dDekl.
{{ p.glyph or '' }}{% if p.sign_glyph %} {{ p.sign_glyph }}{% endif %}{{ p.name }}{{ p.sign }}{{ p.in_sign }}{{ p.house if p.house is defined else '—' }}{{ p.direction }}{{ '%+.4f' | format(p.speed) }}{% if p.declination is defined %}{{ '%+.2f°' | format(p.declination) }}{% if p.out_of_bounds %} OOB{% endif %}{% else %}—{% endif %}
+ +{% if result.lots %} +
Lots hermetyczne ({{ result.lots | length }}) · sekta: {{ result.sect }}
+ + + + {% for l in result.lots %} + + {% endfor %} + +
LotZnakW znakuDomFormuła
{{ l.name }}{{ l.sign }}{{ l.in_sign }}{{ l.house }}{{ l.formula }}
+{% endif %} + +{% set with_stations = result.positions | selectattr('stations', 'defined') | list %} +{% if with_stations %} +
Stacje planet (poprzednia / następna; blisko = mniej niż 7 dni)
+ + + + {% for p in with_stations %} + + + + + + {% endfor %} + +
PlanetaPoprzedniaNastępna
{{ p.name }}{% if p.stations.station_soon %} blisko{% endif %}{% if p.stations.prev %}{{ p.stations.prev.type }} · {{ p.stations.prev.date }} · {{ p.stations.prev.degree }} ({{ p.stations.prev.days }} d){% else %}—{% endif %}{% if p.stations.next %}{{ p.stations.next.type }} · {{ p.stations.next.date }} · {{ p.stations.next.degree }} (+{{ p.stations.next.days }} d){% else %}—{% endif %}
+{% endif %} + +{% if result.aspects %} +
Aspekty główne ({{ result.aspects | length }})
+ + + + {% for a in result.aspects %} + + {% endfor %} + +
Obiekt 1AspektObiekt 2OrbA/S
{{ a.obj1 }}{% if a.glyph %}{{ a.glyph }} {% endif %}{{ a.aspect }}{{ a.obj2 }}{{ '%.2f'|format(a.orb) }}°{{ a['as'] if a['as'] is defined else '—' }}
+{% endif %} + +{% if result.parallels %} +
Paralele deklinacji ({{ result.parallels | length }})
+ + + + {% for r in result.parallels %} + + + + + + + + {% endfor %} + +
Obiekt 1RodzajObiekt 2OrbDekl.
{{ r.obj1 }}{{ 'paralela' if r.type == 'parallel' else 'kontrparalela' }}{{ r.obj2 }}{{ '%.2f'|format(r.orb) }}°{{ '%+.2f'|format(r.dec1) }} / {{ '%+.2f'|format(r.dec2) }}
+{% endif %} + +{% if result.antiscia %} +
Antyscja ({{ result.antiscia | length }})
+ + + + {% for r in result.antiscia %} + + + + + + + {% endfor %} + +
Obiekt 1RodzajObiekt 2Orb
{{ r.obj1 }}{{ 'antyscja' if r.type == 'antiscion' else 'kontrantyscja' }}{{ r.obj2 }}{{ '%.2f'|format(r.orb) }}°
+{% endif %} + +{% if result.tables %} + {% set tb = result.tables %} +
Tabele dodatkowe (LOG-23)
+ + {% set bal = tb.tally.with_modern_10_plus_asc %} + + + + + {% for e, n in bal.elements.items() %} + + {% endfor %} + + + {% for q, n in bal.qualities.items() %} + + {% endfor %} + + + +
Bilans (10 planet + Asc)Rozkład
Żywioły{{ tb.tally.labels.elements[e] }}: {{ n }}
Jakości{{ tb.tally.labels.qualities[q] }}: {{ n }}
+ {% if tb.tally.missing_elements or tb.tally.missing_qualities %} +

Brak: + {% for e in tb.tally.missing_elements %}{{ tb.tally.labels.elements[e] }}{{ ", " if not loop.last }}{% endfor %} + {% for q in tb.tally.missing_qualities %}{{ tb.tally.labels.qualities[q] }}{{ ", " if not loop.last }}{% endfor %} +

+ {% endif %} + + {% if tb.moon_phase %} +

+ Faza Księżyca: {{ tb.moon_phase.phase_pl }} · + elongacja {{ '%.2f'|format(tb.moon_phase.angle) }}° · + oświetlenie {{ '%.1f'|format(tb.moon_phase.illumination * 100) }}% · + {{ 'przybywa' if tb.moon_phase.waxing else 'ubywa' }} +

+ {% endif %} + + {% if tb.planetary_hours %} + {% set ph = tb.planetary_hours %} +

+ Godziny planetarne: władca dnia {{ ph.day_ruler }} · + {{ ph.hour_number }}. godzina ({{ ph.period }}), władca {{ ph.hour_ruler }} · + godzina trwa {{ ph.hour_length_minutes }} min +

+ {% endif %} + + {% if tb.prenatal_syzygy %} + {% set s = tb.prenatal_syzygy %} +

+ Syzygia prenatalna: {{ s.type_pl }} {{ s.when_utc }} + ({{ s.days_before_birth }} dni przed) w {{ s.in_sign }} +

+ {% endif %} + + {% if tb.critical_degrees %} +
Stopnie krytyczne
+ + + + {% for c in tb.critical_degrees %} + + + {% endfor %} + +
ObiektPozycjaUwaga
{{ c.name }}{{ c.in_sign }}{{ c.flags | join('; ') }}
+ {% endif %} + +
+ Podziały: dwunastniki (D12) i nawamsa (D9) + + + + {% for d in tb.divisional %} + + + {% endfor %} + +
ObiektD12D9
{{ d.name }}{{ d.d12_in_sign }}{{ d.d9_in_sign }}
+
+{% endif %} + +{% if result.cusps %} +
+ Cusps domów ({{ result.house_system }}) + + + + {% for c in result.cusps %} + + {% endfor %} + +
DomZnakCusp
{{ c.house }}{{ c.sign }}{{ c.in_sign }}
+
+{% endif %} diff --git a/services/presentation/app/templates/compile.html b/services/presentation/app/templates/compile.html index bf46d35..a8dfb57 100644 --- a/services/presentation/app/templates/compile.html +++ b/services/presentation/app/templates/compile.html @@ -55,6 +55,21 @@ zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie t + {# Te same opcje co na „Horoskop" — synchronizowane między zakładkami (formsync), + żeby podsumowanie liczyło się z tym, co wybrano przy horoskopie (nie okrojone). #} +
+ + +
+
+ {% set chosen = form.house_systems or [] %} + Porównaj systemy domów: + + + +
- - - - {% for p in result.positions %} - - - - - - - {% endfor %} - -
Sym.ObiektZnakW znakuDomKier.
{{ p.glyph or '' }}{% if p.sign_glyph %} {{ p.sign_glyph }}{% endif %}{{ p.name }}{{ p.sign }}{{ p.in_sign }}{{ p.house if p.house is defined else '—' }}{{ p.direction }}
- - {% if result.angles %} - - - - {% for key in ["Asc", "MC", "Dsc", "IC"] %} - {% set a = result.angles[key] %} - - {% endfor %} - -
ZnakW znaku
{{ a.name }}{{ a.sign }}{{ a.in_sign }}
- {% endif %} + {% include "_result_tables.html" %} {# ── 4. i 5. Części od AI — wstawia przeglądarka z magazynu ───────── #}
diff --git a/services/presentation/tests/test_compile.py b/services/presentation/tests/test_compile.py index 349dc9a..cfb9e50 100644 --- a/services/presentation/tests/test_compile.py +++ b/services/presentation/tests/test_compile.py @@ -159,3 +159,36 @@ def test_ai_text_is_escaped_before_injection(): def test_does_nothing_on_other_tabs(): assert "if (!document.getElementById('readiness')) return;" in JS + + +# ─────── podsumowanie NIE gubi opcji policzonych przy horoskopie ────────── +# Regresja 2026-07-28: „Skompiluj" przeliczało horoskop bez stacji/tabel/ +# porównania domów, więc podsumowanie miało braki względem tego, co policzono. + +def test_compile_form_carries_the_options(): + """Te same opcje co „Horoskop" (synchronizowane) — inaczej nie da się ich przekazać.""" + for f in ('name="stations"', 'name="tables"', 'name="house_systems"'): + assert f in TPL, f"brak opcji w formularzu podsumowania: {f}" + + +def test_compile_handler_recomputes_with_all_options(): + assert "house_systems: list[str] = Form([])" in MAIN + assert "stations: bool = Form(False)" in MAIN + assert "house_systems=house_systems" in MAIN # przekazane do logiki + assert "stations=stations, zodiac=zodiac, tables=tables" in MAIN + + +def test_compile_pdf_recomputes_with_all_options(): + assert 'stations=bool(data.get("stations"))' in MAIN + assert 'house_systems=[s for s in (data.get("house_systems")' in MAIN + + +def test_compile_pdf_payload_includes_options(): + assert "house_systems: multi('house_systems')" in JS + assert "stations: checked('stations')" in JS + + +def test_summary_uses_shared_result_tables(): + """Podsumowanie pokazuje te same tabele co horoskop — jeden wspólny plik, + żeby nie mogły znowu się rozjechać.""" + assert '{% include "_result_tables.html" %}' in TPL diff --git a/services/presentation/tests/test_formsync.py b/services/presentation/tests/test_formsync.py index 2d16390..49c2665 100644 --- a/services/presentation/tests/test_formsync.py +++ b/services/presentation/tests/test_formsync.py @@ -20,7 +20,15 @@ WITH_FORM = ("chart.html", "interpret.html", "timeline.html") # Pola, które mają wędrować między zakładkami. Gdy dojdzie nowe wspólne pole, # ten wykaz ma się nie rozjechać z formsync.js. SHARED = ("person", "date", "time", "tz_offset", "lat", "lon", - "house_system", "zodiac", "place") + "house_system", "zodiac", "place", + "stations", "tables", "house_systems") # opcje też wędrują (regresja: podsumowanie liczyło się bez nich) + + +def test_option_checkboxes_are_synced_by_type(): + """Stacje/tabele to checkboxy (.checked), a porównanie domów to wiele checkboxów + tej samej nazwy (lista) — formsync musi umieć oba, nie tylko .value.""" + assert "'check'" in SYNC_JS and "'multi'" in SYNC_JS + assert "el.checked" in SYNC_JS # ─────────────────────────────── pole imienia ──────────────────────────── @@ -90,5 +98,5 @@ def test_map_changes_are_reported_to_sync(): def test_sync_does_not_wipe_fields_missing_on_a_tab(): """Zakładka bez danego pola (np. Sygnifikatory) nie może wyczyścić wartości - zapamiętanej z innej zakładki.""" - assert "if (!el) return;" in SYNC_JS + zapamiętanej z innej zakładki. `collect` pomija pole, którego nie ma na stronie.""" + assert "if (!els.length) return;" in SYNC_JS