From 6c9029c49609f91c7804194142643ce56b8fede9 Mon Sep 17 00:00:00 2001 From: migatu Date: Fri, 24 Jul 2026 18:09:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(prezentacja):=20zakladka=20=E2=80=9ESkompi?= =?UTF-8?q?luj"=20=E2=80=94=20zbiorczy=20raport=20(PRE-23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Czwarta wskazowka partnerow. Nowa zakladka sklada w jedno trzy kawalki liczone w roznych miejscach: policzony horoskop, interpretacje natalna z AI i wszystkie zapamietane predykcje okresowe. Uklad sekcji dokladnie wg prosby: imie i nazwisko -> wprowadzone dane -> RYSUNEK kosmogramu -> interpretacja natalna -> predykcje okresowe. Test pilnuje tej kolejnosci, bo to jedyna rzecz, ktora latwo przestawic przy refaktorze, a partnerzy podali ja wprost. Skad material: - HOROSKOP liczymy TU NA NOWO z danych formularza, zamiast go zapamietywac. To czysta funkcja wejscia — tanio powtorzyc, a odpada trzymanie w przegladarce duzego wyniku, ktory moglby sie rozjechac z aktualnym formularzem. - INTERPRETACJA NATALNA — nowy natal.js, odpowiednik predictions.js. Natalna jest JEDNA (dotyczy momentu urodzenia, nie okresu), wiec ponowne wygenerowanie podmienia slot zamiast dokladac wpis. - PREDYKCJE — z magazynu z PRE-22. compile.js czyta magazyny przez ICH API (window.astrololoNatal / astrololoPredictions), a nie siegajac wprost do localStorage — format danych ma jednego wlasciciela: modul, ktory je zapisuje. Test tego pilnuje. Zakladka MOWI, CZEGO BRAKUJE: panel gotowosci z trzema pozycjami i podpowiedzia, na ktorej zakladce uzupelnic. Bez tego uzytkownik zlozylby niekompletny raport i dowiedzialby sie o tym dopiero po otwarciu PDF-a. Tresc od modelu jest ESKEJPOWANA przed wstawieniem do DOM — to tekst z zewnatrz, wiec bez tego mielibysmy wektor wstrzykniecia. Weryfikacja na zywej aplikacji: dwie predykcje zapisane w Kalendarzu, natalna w Interpretacjach, obie odczytane na Skompiluj (panel: brak horoskopu na zolto, dwa pozostale na zielono). Po zlozeniu wszystkie trzy zielone, a raport zaczyna sie od „Jan Kowalski" i danych wejsciowych. Kolejnosc sekcji sprawdzona na wyrenderowanym HTML: naglowek 2931 < dane 3033 < kosmogram 3158 < natalna 26544 < predykcje 26575. Testy: 16 nowych. Poprawione tez trzy wlasne testy, ktore lapaly nazwy plikow w KOMENTARZACH zamiast w tagach skryptow. Calosc: prezentacja 117 passed. Co-Authored-By: Claude Opus 4.8 --- docs/astrololo_wymagania.xlsx | Bin 37744 -> 37748 bytes services/presentation/app/main.py | 47 ++++++ services/presentation/app/static/compile.js | 95 +++++++++++++ services/presentation/app/static/natal.js | 97 +++++++++++++ services/presentation/app/static/styles.css | 12 ++ services/presentation/app/templates/base.html | 1 + .../presentation/app/templates/compile.html | 124 ++++++++++++++++ .../presentation/app/templates/interpret.html | 3 + services/presentation/tests/test_compile.py | 134 ++++++++++++++++++ services/presentation/tests/test_formsync.py | 9 +- 10 files changed, 518 insertions(+), 4 deletions(-) create mode 100644 services/presentation/app/static/compile.js create mode 100644 services/presentation/app/static/natal.js create mode 100644 services/presentation/app/templates/compile.html create mode 100644 services/presentation/tests/test_compile.py diff --git a/docs/astrololo_wymagania.xlsx b/docs/astrololo_wymagania.xlsx index 707ecd134445c4bafae6da0e1d637c2a9146292f..053664742592514f50f8eba03eae93d082dd9e99 100644 GIT binary patch delta 2166 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/g, '>'); + } + + function readNatal() { + return (window.astrololoNatal && window.astrololoNatal.read()) || null; + } + + function readPredictions() { + return (window.astrololoPredictions && window.astrololoPredictions.read()) || []; + } + + function item(ok, label, hint) { + return '
  • ' + + (ok ? '✓ ' : '✗ ') + esc(label) + + (ok ? '' : ' — ' + esc(hint) + '') + '
  • '; + } + + function renderReadiness(hasChart) { + var host = document.getElementById('readiness'); + if (!host) return; + var natal = readNatal(); + var preds = readPredictions(); + var parts = [ + item(hasChart, 'Policzony horoskop', 'wypełnij dane i kliknij „Złóż podsumowanie"'), + item(!!natal, 'Interpretacja natalna', + 'wygeneruj ją na zakładce Interpretacje — zapamięta się sama'), + item(preds.length > 0, + 'Predykcje okresowe' + (preds.length ? ' (' + preds.length + ')' : ''), + 'policz horoskop na wybrany okres w Kalendarzu') + ]; + host.innerHTML = '
    Materiał do raportu
      ' + + parts.join('') + '
    '; + } + + function renderNatal() { + var host = document.getElementById('reportNatal'); + if (!host) return; + var natal = readNatal(); + if (!natal) { + host.innerHTML = '
    Interpretacja natalna
    ' + + '

    Brak — wygeneruj ją na zakładce Interpretacje.

    '; + return; + } + host.innerHTML = '
    Interpretacja natalna ' + + '· zapamiętana ' + esc(String(natal.saved_at).slice(0, 10)) + + '
    ' + esc(natal.text) + '
    '; + } + + function renderPredictions() { + var host = document.getElementById('reportPredictions'); + if (!host) return; + var preds = readPredictions(); + if (!preds.length) { + host.innerHTML = '
    Predykcje okresowe
    ' + + '

    Brak — policz horoskop na wybrany okres w Kalendarzu.

    '; + return; + } + var blocks = preds.map(function (p) { + return '
    ' + esc(p.from_date) + ' – ' + esc(p.to_date) + '
    ' + + '
    ' + esc(p.text) + '
    '; + }).join(''); + host.innerHTML = '
    Predykcje okresowe (' + preds.length + ')
    ' + blocks; + } + + function ready(fn) { + if (document.readyState !== 'loading') fn(); + else document.addEventListener('DOMContentLoaded', fn); + } + + ready(function () { + if (!document.getElementById('readiness')) return; // nie ta zakładka + // Horoskop jest w dokumencie tylko po wysłaniu formularza. + renderReadiness(!!document.getElementById('reportPerson')); + renderNatal(); + renderPredictions(); + }); +})(); diff --git a/services/presentation/app/static/natal.js b/services/presentation/app/static/natal.js new file mode 100644 index 0000000..10abacb --- /dev/null +++ b/services/presentation/app/static/natal.js @@ -0,0 +1,97 @@ +// Zapamiętana interpretacja natalna (PRE-23). +// +// Odpowiednik predictions.js, tylko że natalna jest JEDNA — dotyczy momentu +// urodzenia, nie wybranego okresu. Ponowne wygenerowanie podmienia poprzednią. +// +// Po co magazyn: zakładka „Skompiluj" składa raport z trzech kawałków liczonych +// w różnych miejscach (horoskop, interpretacja natalna, predykcje okresowe). +// Bez zapamiętania trzeba by je generować od nowa na jednej stronie. +// +// localStorage, tak jak reszta stanu (PRE-21/22) — serwer zostaje bezstanowy. +(function () { + 'use strict'; + + var KEY = 'astrololo.natal.v1'; + + function read() { + try { + var v = JSON.parse(localStorage.getItem(KEY) || 'null'); + return (v && typeof v === 'object' && v.text) ? v : null; + } catch (e) { + return null; // uszkodzony wpis nie może zablokować strony + } + } + + function clear() { + try { localStorage.removeItem(KEY); } catch (e) { /* trudno */ } + } + + function val(sel) { + var el = document.querySelector(sel); + return el ? el.value.trim() : ''; + } + + function save() { + var text = val('#horoscopeText'); + if (!text) return false; // nie ma czego zapamiętać + var entry = { + text: text, + person: val('input[name=person]'), + date: val('input[name=date]'), + time: val('input[name=time]'), + tz_offset: val('input[name=tz_offset]'), + lat: val('input[name=lat]'), + lon: val('input[name=lon]'), + saved_at: new Date().toISOString() + }; + try { + localStorage.setItem(KEY, JSON.stringify(entry)); + return true; + } catch (e) { + return false; // przepełniony magazyn — zgłaszamy wyżej + } + } + + function note(msg) { + var el = document.getElementById('natalNote'); + if (el) el.textContent = msg; + } + + function saveCurrent() { + var text = val('#horoscopeText'); + if (!text) return; + if (save()) { + note('Interpretacja natalna zapamiętana — trafi do raportu (zakładka Skompiluj).'); + } else { + // Interpretacje bywają długie; cicha strata byłaby najgorsza. + note('Nie udało się zapamiętać — magazyn przeglądarki pełny. ' + + 'Usuń starsze predykcje i spróbuj ponownie.'); + } + } + + function ready(fn) { + if (document.readyState !== 'loading') fn(); + else document.addEventListener('DOMContentLoaded', fn); + } + + ready(function () { + if (!document.getElementById('natalNote')) return; // nie ta zakładka + + // 1) interpretacja policzona przez okno postępu (strumień) + document.addEventListener('astrololo:horoscope', function (e) { + if (!e.detail || e.detail.profile === 'natal') saveCurrent(); + }); + + // 2) wariant bez strumienia: strona wróciła z gotowym wynikiem po zwykłym + // POST. Zapis nadpisuje jeden slot, więc odświeżenie niczego nie mnoży. + if (document.querySelector('#horoscopeText')) saveCurrent(); + + var status = read(); + if (status) { + note('W raporcie jest zapamiętana interpretacja natalna z ' + + String(status.saved_at).slice(0, 10) + '.'); + } + }); + + window.astrololoNatal = { read: read, clear: clear, key: KEY }; +})(); diff --git a/services/presentation/app/static/styles.css b/services/presentation/app/static/styles.css index 85ad0f0..467aac5 100644 --- a/services/presentation/app/static/styles.css +++ b/services/presentation/app/static/styles.css @@ -152,3 +152,15 @@ textarea.prompt:focus { outline: 2px solid var(--accent); outline-offset: 1px; } .progress-log li.log-ok { color: #7fd18b; } .progress-log li.log-warn { color: #e0c060; } .progress-log li.log-err { color: #ef6b6b; } + +/* Zakładka „Skompiluj" (PRE-23) — składanie raportu z trzech źródeł. */ +.readiness { list-style: none; margin: .3rem 0 1rem; padding: 0; } +.readiness li { padding: .2rem 0; } +.readiness .rdy-ok { color: #7fd18b; } +.readiness .rdy-missing { color: #e0c060; } +.report-head { margin: 1.5rem 0 .5rem; } +.report-head h2 { margin: 0; font-size: 1.4rem; } +/* Treść od AI bywa długa — white-space:pre-wrap zachowuje akapity modelu, + bez przepisywania ich na HTML. */ +.report-text { white-space: pre-wrap; background: var(--panel); border: 1px solid var(--line); + border-radius: 10px; padding: .8rem 1rem; margin-top: .4rem; line-height: 1.6; } diff --git a/services/presentation/app/templates/base.html b/services/presentation/app/templates/base.html index 3bfb405..71ec1e2 100644 --- a/services/presentation/app/templates/base.html +++ b/services/presentation/app/templates/base.html @@ -23,6 +23,7 @@ Interpretacje Kalendarz Sygnifikatory + Skompiluj {% block content %}{% endblock %} diff --git a/services/presentation/app/templates/compile.html b/services/presentation/app/templates/compile.html new file mode 100644 index 0000000..45a18f3 --- /dev/null +++ b/services/presentation/app/templates/compile.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} +{% block title %}Skompiluj{% endblock %} +{% block nav_compile %}active{% endblock %} + +{% block content %} +

    Składa w jedno: policzony horoskop, interpretację natalną z AI oraz wszystkie +zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie trzeba nic przepisywać.

    + +{# Gotowość materiału — wypełniana po stronie przeglądarki (compile.js), bo część + kawałków (AI) mieszka w magazynie lokalnym, nie na serwerze. #} +
    + +
    +
    + +
    +
    + + + +
    +
    + + + + +
    +
    + +
    +
    + +{% if error %}
    {{ error }}
    {% endif %} + +{% if result %} + {# ── 1. Nagłówek: kto ──────────────────────────────────────────────── #} +
    +

    {{ form.person or 'Bez podanego imienia' }}

    +

    + {{ form.date }} {{ form.time }} · offset {{ form.tz_offset }} h względem GMT · + {{ '%.4f'|format(form.lat|float) }}, {{ '%.4f'|format(form.lon|float) }} + {% if moment %}· {{ moment }}{% endif %} +

    +
    + + {# ── 2. Kosmogram ─────────────────────────────────────────────────── #} + {% if wheel_svg %} +
    + {{ wheel_svg | safe }} +
    Kosmogram — kliknij, aby powiększyć.
    +
    + {% endif %} + + {# ── 3. Dane policzone ────────────────────────────────────────────── #} +
    Horoskop · silnik {{ result.engine }} + {% if result.house_system %}· domy {{ result.house_system }}{% endif %} + {% if result.zodiac %}· zodiak {{ result.zodiac }}{% endif %}
    + + + + {% 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 %} + + {# ── 4. i 5. Części od AI — wstawia przeglądarka z magazynu ───────── #} +
    +
    +{% endif %} + +{# Magazyny wczytujemy dla ich API odczytu. Ich własne UI samo się wyłącza — + każdy sprawdza, czy jego kontener jest na stronie (tu go nie ma). #} + + + +{% endblock %} diff --git a/services/presentation/app/templates/interpret.html b/services/presentation/app/templates/interpret.html index 620b8ed..07506b6 100644 --- a/services/presentation/app/templates/interpret.html +++ b/services/presentation/app/templates/interpret.html @@ -36,6 +36,7 @@ {% if location_label %}

    Wstępnie wpisano lokalizację: {{ location_label }} ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".

    {% endif %} {% include "_prompt_block.html" %} +

    @@ -99,4 +100,6 @@ +{# po progress.js — nasłuchuje zdarzenia o gotowej interpretacji (PRE-23) #} + {% endblock %} diff --git a/services/presentation/tests/test_compile.py b/services/presentation/tests/test_compile.py new file mode 100644 index 0000000..1992c44 --- /dev/null +++ b/services/presentation/tests/test_compile.py @@ -0,0 +1,134 @@ +"""Zakładka „Skompiluj" — zbiorczy raport (PRE-23). + +Raport składa się z trzech kawałków liczonych w różnych miejscach: horoskopu +(serwer), interpretacji natalnej i predykcji okresowych (magazyn przeglądarki). +Pilnujemy, żeby zakładka umiała je zebrać, a przede wszystkim — żeby POWIEDZIAŁA, +czego brakuje. Niekompletny raport, o którym dowiadujesz się dopiero z PDF-a, +byłby najgorszym wynikiem. +""" +import pathlib + +from starlette.testclient import TestClient + +APP = pathlib.Path(__file__).resolve().parents[1] / "app" +TPL = (APP / "templates" / "compile.html").read_text(encoding="utf-8") +BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8") +JS = (APP / "static" / "compile.js").read_text(encoding="utf-8") +NATAL_JS = (APP / "static" / "natal.js").read_text(encoding="utf-8") +INTERPRET = (APP / "templates" / "interpret.html").read_text(encoding="utf-8") +MAIN = (APP / "main.py").read_text(encoding="utf-8") + + +def _client(): + import os + + os.environ.pop("APP_PASSWORD", None) # bez bramki logowania w teście + from app.main import app + + return TestClient(app) + + +# ───────────────────────────────── nawigacja i trasa ───────────────────── + +def test_tab_is_in_the_menu(): + assert 'href="/compile"' in BASE and "Skompiluj" in BASE + + +def test_get_renders_empty_form(): + r = _client().get("/compile") + assert r.status_code == 200 + assert "Złóż podsumowanie" in r.text + + +def test_routes_exist(): + assert '@app.get("/compile"' in MAIN and '@app.post("/compile"' in MAIN + + +# ─────────────────────────── kolejność w raporcie ──────────────────────── + +def test_report_order_matches_the_brief(): + """Prośba partnerów: najpierw imię i nazwisko, potem wprowadzone dane, potem + RYSUNEK kosmogramu, a dopiero po nim interpretacja natalna i predykcje.""" + person = TPL.index('id="reportPerson"') + wheel = TPL.index("wheel_svg") + natal = TPL.index('id="reportNatal"') + preds = TPL.index('id="reportPredictions"') + assert person < wheel < natal < preds + + +def test_person_name_heads_the_report(): + assert 'id="reportPerson"' in TPL + assert "form.person" in TPL + + +# ──────────────────────── zbieranie materiału z magazynów ──────────────── + +def test_reads_stores_through_their_api_not_raw_storage(): + """Format danych ma jednego właściciela — moduł, który je zapisuje. + Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie.""" + assert "window.astrololoNatal" in JS + assert "window.astrololoPredictions" in JS + # chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu) + assert "localStorage.getItem" not in JS + assert "localStorage.setItem" not in JS + + +def test_stores_are_loaded_on_the_tab(): + for src in ("natal.js", "predictions.js", "compile.js"): + assert src in TPL, f"brak {src} na zakładce" + + +def test_stores_load_before_the_assembler(): + """Szukamy TAGÓW skryptów, nie samych nazw — te padają też w komentarzach.""" + def tag(name): + return TPL.index('src="/static/' + name + '"') + + assert tag("natal.js") < tag("compile.js") + assert tag("predictions.js") < tag("compile.js") + + +# ─────────────────────────── mówi, czego brakuje ───────────────────────── + +def test_shows_what_is_missing(): + assert 'id="readiness"' in TPL + assert "rdy-missing" in JS + for hint in ("Interpretacje", "Kalendarz"): + assert hint in JS, f"brak podpowiedzi, gdzie uzupełnić: {hint}" + + +def test_all_three_parts_are_checked(): + for label in ("Policzony horoskop", "Interpretacja natalna", "Predykcje okresowe"): + assert label in JS + + +# ───────────────────────────── natalna zapamiętywana ───────────────────── + +def test_natal_is_captured_on_interpret_tab(): + assert "natal.js" in INTERPRET + assert 'id="natalNote"' in INTERPRET + + +def test_natal_store_keeps_a_single_entry(): + """Natalna jest JEDNA — dotyczy momentu urodzenia, nie okresu. Ponowne + wygenerowanie ma podmienić, nie dokładać.""" + assert "setItem" in NATAL_JS and "push" not in NATAL_JS + + +def test_natal_only_stores_natal_profile(): + assert "'natal'" in NATAL_JS + + +def test_full_storage_is_reported(): + assert "pełny" in NATAL_JS + + +# ─────────────────────────────── bezpieczeństwo ────────────────────────── + +def test_ai_text_is_escaped_before_injection(): + """Treść od modelu wstawiamy do DOM — bez ucieczki byłby to wektor wstrzyknięcia.""" + assert "function esc" in JS + assert "replace(/