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 %}
+
+
Oś
Znak
W znaku
+
+ {% for key in ["Asc", "MC", "Dsc", "IC"] %}
+ {% set a = result.angles[key] %}
+
{{ a.name }}
{{ a.sign }}
{{ a.in_sign }}
+ {% endfor %}
+
+
+{% 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 }})
{{ 'antyscja' if r.type == 'antiscion' else 'kontrantyscja' }}
+
{{ r.obj2 }}
+
{{ '%.2f'|format(r.orb) }}°
+
+ {% endfor %}
+
+
+{% endif %}
+
+{% if result.tables %}
+ {% set tb = result.tables %}
+
Tabele dodatkowe (LOG-23)
+
+ {% set bal = tb.tally.with_modern_10_plus_asc %}
+
+
Bilans (10 planet + Asc)
Rozkład
+
+
Żywioły
+ {% for e, n in bal.elements.items() %}
+
{{ tb.tally.labels.elements[e] }}: {{ n }}
+ {% endfor %}
+
+
Jakości
+ {% for q, n in bal.qualities.items() %}
+
{{ tb.tally.labels.qualities[q] }}: {{ n }}
+ {% endfor %}
+
+
+
+
+ {% 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 result.cusps %}
+
+ Cusps domów ({{ result.house_system }})
+
+
Dom
Znak
Cusp
+
+ {% for c in result.cusps %}
+
{{ c.house }}
{{ c.sign }}
{{ c.in_sign }}
+ {% endfor %}
+
+
+
+{% 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:
+
+
+
+
-
-
Sym.
Obiekt
Znak
W znaku
Dom
Kier.
-
- {% for p in result.positions %}
-
-
{{ 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 }}
-
- {% endfor %}
-
-
-
- {% if result.angles %}
-
-
Oś
Znak
W znaku
-
- {% for key in ["Asc", "MC", "Dsc", "IC"] %}
- {% set a = result.angles[key] %}
-
{{ a.name }}
{{ a.sign }}
{{ a.in_sign }}
- {% endfor %}
-
-
- {% 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