diff --git a/services/logic/README.md b/services/logic/README.md index 6c19cf7..f8115a2 100644 --- a/services/logic/README.md +++ b/services/logic/README.md @@ -10,7 +10,7 @@ i nie w bazie. - `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów, odsiewaniem duplikatów (ten sam sygnifikator i opis), rankingiem siły (LOG-21) oraz opcją group (grupowanie identycznych opisów) - `POST /chart/profections` → `{when_utc, lat, lon, start_age?, count?}` → profekcje roczne: wiek, profektowany Asc, Władca Roku (+MC/Su/Mo) (LOG-10) - `POST /chart/return` → `{when_utc, lat, lon, kind, around?}` → Solar/Lunar Return: moment powrotu + pełny horoskop na ten moment (LOG-12) -- `POST /chart/timeline` → `{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc, posortowane (technique | significator | start | exact | end) (LOG-14) +- `POST /chart/timeline` → `{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc, posortowane (technique | significator | start | exact | end); z interpret=true dopina interpretacje z bazy do dat (LOG-14, 1B->2B) - `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B) - `GET /health` (sprawdza też warstwę bazodanową) diff --git a/services/logic/app/engine/timeline.py b/services/logic/app/engine/timeline.py index 1e2983b..2eac9a0 100644 --- a/services/logic/app/engine/timeline.py +++ b/services/logic/app/engine/timeline.py @@ -58,13 +58,15 @@ def solar_arc_directions( if not (lo_age <= age <= hi_age) or (p == q and arc < 1e-6): continue exact = _add_years(birth, age) - out.append(_row( + row = _row( "solar_arc", f"dyr. {p} {PL_NAME[asp]} {q}", _add_years(birth, age - orb_years), exact, _add_years(birth, age + orb_years), - )) + ) + row.update(directed=p, aspect=asp, target=q) # do budowy tokenów (1B->2B) + out.append(row) return out @@ -83,10 +85,13 @@ def profection_events(natal_asc: float, birth: datetime, lo: datetime, hi: datet if end < lo or start > hi: continue sign = profected_sign(natal_asc, age) - out.append(_row( - "profection", f"Władca Roku: {DOMICILE_RULERS[sign]} (Asc {sign}, wiek {age})", + lord = DOMICILE_RULERS[sign] + row = _row( + "profection", f"Władca Roku: {lord} (Asc {sign}, wiek {age})", start, start, end, - )) + ) + row.update(lord=lord, sign=sign) # do budowy tokenów (1B->2B) + out.append(row) return out diff --git a/services/logic/app/main.py b/services/logic/app/main.py index 665d939..b3a5c50 100644 --- a/services/logic/app/main.py +++ b/services/logic/app/main.py @@ -180,11 +180,15 @@ class TimelineRequest(BaseModel): from_date: str # zakres: YYYY-MM-DD to_date: str techniques: list[str] | None = None # profection | solar_return | solar_arc + interpret: bool = False # dopnij interpretacje z bazy (1B->2B) @app.post("/chart/timeline") def chart_timeline(req: TimelineRequest) -> dict: - """Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end.""" + """Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end. + + Z interpret=true dopina do zdarzeń interpretacje z warstwy danych (LOG-19, 1B->2B). + """ from app.engine import houses as H from app.engine.models import ChartMoment from app.engine.timeline import build_timeline @@ -196,8 +200,16 @@ def chart_timeline(req: TimelineRequest) -> dict: for p in engine.positions(natal): points[p.name] = p.longitude events = build_timeline(engine, natal, points, req.from_date, req.to_date, req.techniques) - return {"engine": engine.name, "from": req.from_date, "to": req.to_date, - "count": len(events), "events": events} + + out = {"engine": engine.name, "from": req.from_date, "to": req.to_date} + if req.interpret: + from app.significators import interpret_events + try: + interpret_events(events, DataClient()) + except httpx.HTTPError as e: + out["data_error"] = f"Warstwa danych niedostępna: {e}" + out.update(count=len(events), events=events) + return out @app.get("/health") diff --git a/services/logic/app/significators.py b/services/logic/app/significators.py index 76544e3..cdc5dad 100644 --- a/services/logic/app/significators.py +++ b/services/logic/app/significators.py @@ -195,3 +195,52 @@ def build_report( "facets": facets, }) return {"provider": provider, "objects": items} + + +def _event_tokens(event: dict) -> list[str]: + """Tokeny bazy dla zdarzenia osi czasu (spięcie 1B→2B). Pierwszy = planeta.""" + from app.engine.aspects import DB_TOKEN + + if event.get("technique") == "solar_arc": + p = PLANET_ABBR.get(event.get("directed")) + a = DB_TOKEN.get(event.get("aspect")) + q = PLANET_ABBR.get(event.get("target")) # None dla Asc/MC (nie ma tokenu) + toks = [] + if p: + toks.append("[" + p) + if a: + toks.append(a) + if q: + toks.append("[" + q) + return toks if p else [] + if event.get("technique") == "profection": + lord = PLANET_ABBR.get(event.get("lord")) + sign = SIGN_TO_ABBR.get(event.get("sign")) + toks = [] + if lord: + toks.append("[" + lord) + if sign: + toks.append("[" + sign) + return toks if lord else [] + return [] + + +def interpret_events(events: list[dict], data: DataSource, limit: int = 4, + per_object_limit: int = 5000) -> list[dict]: + """Dopina interpretacje z bazy do zdarzeń osi czasu (predykcyjne 1B → 2B). + + Wyszukuje po tokenie planety zdarzenia i zawęża do wszystkich tokenów + (aspekt/druga planeta lub znak), z odsiewaniem szumu i duplikatów. + """ + for ev in events: + tokens = _event_tokens(ev) + if not tokens: + continue + raw = data.search( + key="significator", value=tokens[0], exact=False, limit=per_object_limit, + fields=["significator", "actioneffect", "topicresult", "bodypart"], + ) + samples = _facet_samples(raw.get("rows", []), tokens) + ev["interpretations"] = samples[:limit] + ev["interpretations_count"] = len(samples) + return events diff --git a/services/logic/tests/test_timeline_interpret.py b/services/logic/tests/test_timeline_interpret.py new file mode 100644 index 0000000..40f0c01 --- /dev/null +++ b/services/logic/tests/test_timeline_interpret.py @@ -0,0 +1,46 @@ +"""Spięcie osi czasu z bazą interpretacji (LOG-14 → 1B→2B).""" +from app.significators import _event_tokens, interpret_events + + +class FakeData: + def __init__(self, rows_by_value): + self.rows_by_value = rows_by_value + + def search(self, key, value, exact, limit, fields=None): + rows = self.rows_by_value.get(value, []) + return {"provider": "fake", "total": len(rows), "rows": rows} + + +def test_event_tokens_solar_arc(): + ev = {"technique": "solar_arc", "directed": "Venus", "aspect": "conjunction", "target": "North Node"} + assert _event_tokens(ev) == ["[Ve", "[conj", "[NN"] + + +def test_event_tokens_solar_arc_to_angle_has_no_target_token(): + ev = {"technique": "solar_arc", "directed": "Sun", "aspect": "square", "target": "MC"} + assert _event_tokens(ev) == ["[Su", "[sq"] # MC nie ma tokenu planety + + +def test_event_tokens_profection(): + ev = {"technique": "profection", "lord": "Saturn", "sign": "Capricorn"} + assert _event_tokens(ev) == ["[Sa", "[Cap"] + + +def test_solar_return_has_no_tokens(): + assert _event_tokens({"technique": "solar_return"}) == [] + + +def test_interpret_attaches_matches_with_all_tokens(): + events = [ + {"technique": "solar_arc", "directed": "Venus", "aspect": "conjunction", "target": "North Node"}, + {"technique": "solar_return"}, + ] + data = FakeData({"[Ve": [ + {"significator": "[Ve [conj [NN", "actioneffect": "spotkanie losowe"}, # wszystkie tokeny + {"significator": "[Ve [conj [Mo", "actioneffect": "inny"}, # brak [NN + {"significator": "[Ve [conj [NN", "actioneffect": "spotkanie losowe"}, # duplikat + ]}) + interpret_events(events, data) + assert events[0]["interpretations_count"] == 1 # duplikat odsiany, tylko z [NN + assert events[0]["interpretations"][0]["expanded"] == "Venus conjunction North Node" + assert "interpretations" not in events[1] # solar_return pominięty diff --git a/services/presentation/app/clients/logic_client.py b/services/presentation/app/clients/logic_client.py index 243f6dc..80c40cd 100644 --- a/services/presentation/app/clients/logic_client.py +++ b/services/presentation/app/clients/logic_client.py @@ -56,3 +56,17 @@ class LogicClient: r = client.post(f"{self.base_url}/chart/report", json=payload) r.raise_for_status() return r.json() + + def timeline( + self, when_utc_iso: str, lat: float, lon: float, + from_date: str, to_date: str, interpret: bool = True, + ) -> dict[str, Any]: + """Oś czasu z technik (+interpretacje z bazy) — woła logic /chart/timeline.""" + payload = { + "when_utc": when_utc_iso, "lat": lat, "lon": lon, + "from_date": from_date, "to_date": to_date, "interpret": interpret, + } + with httpx.Client(timeout=max(settings.http_timeout, 60.0)) as client: + r = client.post(f"{self.base_url}/chart/timeline", json=payload) + r.raise_for_status() + return r.json() diff --git a/services/presentation/app/main.py b/services/presentation/app/main.py index 4b17cf2..6a3744b 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -132,6 +132,40 @@ def interpret_run( return templates.TemplateResponse(request, "interpret.html", ctx) +# ---------------- Kalendarz (oś czasu z technik + interpretacje) ---------------- +@app.get("/timeline", response_class=HTMLResponse) +def timeline_form(request: Request): + return templates.TemplateResponse(request, "timeline.html", {"result": None, "form": {}}) + + +@app.post("/timeline", response_class=HTMLResponse) +def timeline_run( + request: Request, + date: str = Form(...), + time: str = Form(...), + tz_offset: float = Form(0.0), + lat: float = Form(0.0), + lon: float = Form(0.0), + from_date: str = Form(...), + to_date: str = Form(...), +): + form = {"date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon, + "from_date": from_date, "to_date": to_date} + 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.timeline( + when_utc_iso=iso_utc, lat=lat, lon=lon, + from_date=from_date, to_date=to_date, interpret=True, + ) + except httpx.HTTPError as e: + ctx["error"] = _logic_error(e) + except ValueError as e: + ctx["error"] = f"Niepoprawne dane wejściowe: {e}" + return templates.TemplateResponse(request, "timeline.html", ctx) + + @app.get("/health") def health() -> dict: return {"status": "ok", "layer": "presentation"} diff --git a/services/presentation/app/templates/base.html b/services/presentation/app/templates/base.html index d2791e8..8c4dde7 100644 --- a/services/presentation/app/templates/base.html +++ b/services/presentation/app/templates/base.html @@ -13,6 +13,7 @@ diff --git a/services/presentation/app/templates/timeline.html b/services/presentation/app/templates/timeline.html new file mode 100644 index 0000000..5200eec --- /dev/null +++ b/services/presentation/app/templates/timeline.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} +{% block title %}Kalendarz{% endblock %} +{% block nav_timeline %}active{% endblock %} + +{% block content %} +

Zbiorcza oś czasu technik predykcyjnych (profekcje, solariusze, dyrekcje solar-arc) z interpretacjami z bazy dla dat.

+ +
+
+ + + + + +
+
+ + +
+
+ + + +
+
+ +{% if error %}
{{ error }}
{% endif %} + +{% if result %} + {% if result.data_error %}
{{ result.data_error }}
{% endif %} +
+ Silnik: {{ result.engine }} · + zdarzeń: {{ result.count }} · zakres {{ result.from }} → {{ result.to }} +
+ + {% for e in result.events %} +
+
+ {{ e.exact }} + {{ e.technique }} + {{ e.significator }} + (okno {{ e.start }} → {{ e.end }}) +
+ {% if e.interpretations %} + + + {% for s in e.interpretations %} + + {% endfor %} + +
{{ s.expanded }}{{ s.effect }}
+ {% if e.interpretations_count > e.interpretations | length %} +
…i {{ e.interpretations_count - (e.interpretations | length) }} więcej
+ {% endif %} + {% elif e.technique != 'solar_return' %} +
brak dopasowań w bazie
+ {% endif %} +
+ {% endfor %} +{% endif %} + + +{% endblock %}