From a8c3072e62f64dddadf359a76d78a91a435d6f21 Mon Sep 17 00:00:00 2001 From: migatu Date: Wed, 8 Jul 2026 10:51:29 +0200 Subject: [PATCH] =?UTF-8?q?W=C4=99z=C5=82y=20ksi=C4=99=C5=BCycowe,=20mean?= =?UTF-8?q?=20Lilith=20i=20wykrywanie=20stacji=20(LOG-02/03)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Punkty wirtualne (LOG-02): - engine/points.py: mean Node (Ω) i mean Lilith (apogeum) wzorami Meeusa; prędkości numerycznie. SN = NN + 180° (ta sama prędkość), zawsze Rx. - DEFAULT_OBJECTS + North Node / South Node / Lilith — automatycznie dostają domy, aspekty i A/S. Parzystość silnika B: swe.MEAN_NODE / swe.MEAN_APOG (uwaga: stała pyswisseph to MEAN_APOG, nie MEAN_APOGEE). - significators: tokeny [NN / [SN / [Lilith (zgodne z SIGNIFICATORS KEY). Stacje (LOG-03): - engine/stations.py: skan prędkości (krok 4 dni, okno ±800 dni — pokrywa najdłuższe przerwy Marsa/Wenus) + bisekcja; klasyfikacja SD/SR; poprzednia/ następna stacja (dni, data, stopień w znaku) + flaga station_soon (<7 dni). - /chart/positions: opt-in stations:true; UI: checkbox + tabela stacji. Walidacja: - mean NN vs astro-seek (Gem 8°09'24"): Δ=0,3'; vs swisseph: Δ=17"; mean Lilith vs swisseph: Δ=1,5'. NN dom 12 / SN dom 6 zgodnie z astro-seek. - Stacje Marsa 1984 trafiają w historię: SR 5.04.1984, SD 19.06.1984; samospójność |speed|<0,01°/d w znalezionych momentach; flaga "blisko" działa (Merkury +5,3d, Jowisz -0,6d). - E2E na realnej bazie: [SN 134 rekordy, trafienie w 6. domu. 54 testy przechodzą. Co-Authored-By: Claude Opus 4.8 --- services/engine-swisseph/app/main.py | 13 ++- services/logic/README.md | 2 +- services/logic/app/engine/models.py | 4 +- services/logic/app/engine/points.py | 43 ++++++++ services/logic/app/engine/skyfield_engine.py | 24 +++++ services/logic/app/engine/stations.py | 100 ++++++++++++++++++ services/logic/app/main.py | 14 ++- services/logic/app/significators.py | 2 + services/logic/tests/test_points.py | 50 +++++++++ services/logic/tests/test_stations.py | 55 ++++++++++ .../presentation/app/clients/logic_client.py | 5 +- services/presentation/app/main.py | 6 +- .../presentation/app/templates/chart.html | 21 ++++ 13 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 services/logic/app/engine/points.py create mode 100644 services/logic/app/engine/stations.py create mode 100644 services/logic/tests/test_points.py create mode 100644 services/logic/tests/test_stations.py diff --git a/services/engine-swisseph/app/main.py b/services/engine-swisseph/app/main.py index 34fa73c..9a3f79b 100644 --- a/services/engine-swisseph/app/main.py +++ b/services/engine-swisseph/app/main.py @@ -29,8 +29,14 @@ _PLANETS = { "Sun": swe.SUN, "Moon": swe.MOON, "Mercury": swe.MERCURY, "Venus": swe.VENUS, "Mars": swe.MARS, "Jupiter": swe.JUPITER, "Saturn": swe.SATURN, "Uranus": swe.URANUS, "Neptune": swe.NEPTUNE, "Pluto": swe.PLUTO, + # punkty wirtualne — mean, jak w silniku własnym (parzystość LOG-28) + "North Node": swe.MEAN_NODE, "Lilith": swe.MEAN_APOG, + # "South Node" obsługiwany pochodnie w /positions: NN + 180° } -DEFAULT_OBJECTS = list(_PLANETS) +DEFAULT_OBJECTS = [ + "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", + "Uranus", "Neptune", "Pluto", "North Node", "South Node", "Lilith", +] class PositionsRequest(BaseModel): @@ -48,8 +54,11 @@ def positions(req: PositionsRequest) -> dict: rows = [] for name in (req.objects or DEFAULT_OBJECTS): - xx, _retflag = swe.calc_ut(jd, _PLANETS[name], _FLAGS) + lookup = "North Node" if name == "South Node" else name + xx, _retflag = swe.calc_ut(jd, _PLANETS[lookup], _FLAGS) lon, lat, _dist, lon_speed = xx[0], xx[1], xx[2], xx[3] + if name == "South Node": + lon, lat = lon + 180.0, -lat rows.append({ "name": name, "longitude": lon % 360.0, diff --git a/services/logic/README.md b/services/logic/README.md index 2eb9c54..938a39c 100644 --- a/services/logic/README.md +++ b/services/logic/README.md @@ -6,7 +6,7 @@ i nie w bazie. ## API - `POST /api/query` → `QueryRequest` → `QueryResponse` -- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`. +- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06) + opcjonalnie stacje planet (`stations:true`, LOG-03). Obiekty: 10 planet + mean NN/SN/Lilith (LOG-02). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`. - `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/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/models.py b/services/logic/app/engine/models.py index b11a1ac..0892935 100644 --- a/services/logic/app/engine/models.py +++ b/services/logic/app/engine/models.py @@ -13,10 +13,12 @@ from typing import Any from app.engine import formats -# kanoniczny zestaw i kolejność obiektów (LOG-02: światła + 7 klasycznych + 3 nowożytne) +# kanoniczny zestaw i kolejność obiektów (LOG-02: światła + 7 klasycznych + +# 3 nowożytne + punkty wirtualne: węzły mean i mean Lilith) DEFAULT_OBJECTS = [ "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", + "North Node", "South Node", "Lilith", ] diff --git a/services/logic/app/engine/points.py b/services/logic/app/engine/points.py new file mode 100644 index 0000000..b172e02 --- /dev/null +++ b/services/logic/app/engine/points.py @@ -0,0 +1,43 @@ +"""Punkty wirtualne liczone analitycznie (LOG-02): mean Node i mean Lilith. + +Wzory Meeusa (Astronomical Algorithms) w stuleciach juliańskich od J2000 (TT): +- Ω — średni węzeł wstępujący orbity Księżyca (mean ascending node). Porusza się + zawsze wstecz (~−0,053°/dobę) — stąd węzły są wiecznie Rx. +- średnie perygeum orbity Księżyca; mean Lilith (Black Moon) = średnie APOGEUM + = perygeum + 180° (~+0,111°/dobę). + +Wersje TRUE (oskulacyjne) — osobny, późniejszy krok (notatki: mean to +historyczny standard i domyślne ustawienie programów). +""" +from __future__ import annotations + +from app.engine.formats import norm360 + +_DAYS_PER_CENTURY = 36525.0 + + +def _t(tt_jd: float) -> float: + return (tt_jd - 2451545.0) / _DAYS_PER_CENTURY + + +def mean_lunar_node(tt_jd: float) -> float: + """Długość ekliptyczna średniego Węzła Północnego (Ω) [°].""" + t = _t(tt_jd) + omega = (125.0445479 - 1934.1362891 * t + 0.0020754 * t * t + + t ** 3 / 467441.0 - t ** 4 / 60616000.0) + return norm360(omega) + + +def mean_lilith(tt_jd: float) -> float: + """Długość ekliptyczna mean Lilith (średnie apogeum Księżyca) [°].""" + t = _t(tt_jd) + perigee = (83.3532465 + 4069.0137287 * t - 0.0103200 * t * t + - t ** 3 / 80053.0 + t ** 4 / 18999000.0) + return norm360(perigee + 180.0) + + +def point_speed(fn, tt_jd: float, dt_days: float = 0.1) -> float: + """Prędkość [°/dobę] punktu analitycznego — różnica po małym kroku.""" + a = fn(tt_jd) + b = fn(tt_jd + dt_days) + return (((b - a + 180.0) % 360.0) - 180.0) / dt_days diff --git a/services/logic/app/engine/skyfield_engine.py b/services/logic/app/engine/skyfield_engine.py index 7e00ba3..f17f3f8 100644 --- a/services/logic/app/engine/skyfield_engine.py +++ b/services/logic/app/engine/skyfield_engine.py @@ -60,6 +60,27 @@ class SkyfieldEngine(EphemerisEngine): lat, lon, _dist = astrometric.ecliptic_latlon(epoch="date") return lon.degrees, lat.degrees + def _virtual_point(self, name: str, tt_jd: float) -> ObjectPosition: + """Punkty analityczne (LOG-02): mean Node (NN/SN) i mean Lilith. + + Liczone wzorami Meeusa, nie z jądra JPL. SN = NN + 180° (ta sama prędkość). + Punkty leżą na ekliptyce (latitude = 0). + """ + from app.engine.points import mean_lilith, mean_lunar_node, point_speed + + if name in ("North Node", "South Node"): + lon = mean_lunar_node(tt_jd) + if name == "South Node": + lon = norm360(lon + 180.0) + speed = point_speed(mean_lunar_node, tt_jd) + else: # Lilith + lon = mean_lilith(tt_jd) + speed = point_speed(mean_lilith, tt_jd) + return ObjectPosition( + name=name, longitude=float(lon), latitude=0.0, + speed=float(speed), retrograde=bool(speed < 0), + ) + def positions( self, moment: ChartMoment, objects: list[str] | None = None ) -> list[ObjectPosition]: @@ -70,6 +91,9 @@ class SkyfieldEngine(EphemerisEngine): out: list[ObjectPosition] = [] for name in names: + if name not in _TARGETS: # punkt wirtualny (NN/SN/Lilith) + out.append(self._virtual_point(name, t.tt)) + continue target = self.eph[_TARGETS[name]] lon, lat = self._ecliptic_lon_lat(target, t) lon2, _ = self._ecliptic_lon_lat(target, t2) diff --git a/services/logic/app/engine/stations.py b/services/logic/app/engine/stations.py new file mode 100644 index 0000000..105c010 --- /dev/null +++ b/services/logic/app/engine/stations.py @@ -0,0 +1,100 @@ +"""Wykrywanie stacji planet (LOG-03): poprzednia/następna stacja, SD/SR, flaga <7 dni. + +Stacja ścisła = moment, w którym prędkość zodiakalna przechodzi przez zero. +Metoda: próbki prędkości co 1 dzień w oknie ± SEARCH_DAYS → zmiana znaku → +bisekcja do dokładności ~1 minuty. Klasyfikacja: prędkość przed<0 i po>0 → SD +(stationary direct), odwrotnie → SR (stationary retrograde). + +Pomijamy Słońce/Księżyc (nigdy Rx) i punkty mean (NN/SN/Lilith — ruch jednostajny). +""" +from __future__ import annotations + +from datetime import timedelta + +from app.engine.formats import in_sign +from app.engine.models import ChartMoment + +# obiekty bez stacji +NO_STATIONS = {"Sun", "Moon", "North Node", "South Node", "Lilith"} + +# Okno musi pokryć najdłuższą przerwę między stacjami (Mars/Wenus: ~700 dni), +# a krok skanu musi być krótszy niż najkrótsza retrogradacja (Merkury ~21 dni). +SEARCH_DAYS = 800 # okno poszukiwań w każdą stronę +SCAN_STEP_DAYS = 4.0 # krok zgrubnego skanu (potem bisekcja) +STATION_SOON_DAYS = 7.0 # próg flagi "stacja blisko" (konfigurowalny, notes2) + + +def _speed_fn(engine, name: str): + """Zwraca funkcję: dni_od_momentu_bazowego -> prędkość [°/dobę].""" + def speed(base_moment: ChartMoment, offset_days: float) -> float: + m = ChartMoment( + when_utc=base_moment.when_utc + timedelta(days=offset_days), + lat=base_moment.lat, lon=base_moment.lon, + ) + return engine.positions(m, [name])[0].speed + return speed + + +def _bisect_zero(speed, moment: ChartMoment, lo: float, hi: float, iters: int = 20) -> float: + """Bisekcja miejsca zerowego prędkości między dniami lo i hi.""" + s_lo = speed(moment, lo) + for _ in range(iters): + mid = (lo + hi) / 2.0 + s_mid = speed(moment, mid) + if (s_lo < 0) == (s_mid < 0): + lo, s_lo = mid, s_mid + else: + hi = mid + return (lo + hi) / 2.0 + + +def _station_info(engine, moment: ChartMoment, name: str, day: float, speed) -> dict: + """Opis stacji w danym dniu (offset od momentu bazowego).""" + before = speed(moment, day - 0.5) + kind = "SD" if before < 0 else "SR" + when = moment.when_utc + timedelta(days=day) + m = ChartMoment(when_utc=when, lat=moment.lat, lon=moment.lon) + lon = engine.positions(m, [name])[0].longitude + return { + "type": kind, + "date": when.strftime("%Y-%m-%d %H:%M"), + "days": round(day, 1), # ujemne = w przeszłości + "degree": in_sign(lon), + } + + +def find_stations(engine, moment: ChartMoment, name: str, step_days: float = SCAN_STEP_DAYS) -> dict | None: + """Poprzednia i następna stacja obiektu względem momentu horoskopu.""" + if name in NO_STATIONS: + return None + speed = _speed_fn(engine, name) + + prev_day = next_day = None + # w przeszłość + s_right = speed(moment, 0.0) + d = 0.0 + while d > -SEARCH_DAYS: + s_left = speed(moment, d - step_days) + if (s_left < 0) != (s_right < 0): + prev_day = _bisect_zero(speed, moment, d - step_days, d) + break + d, s_right = d - step_days, s_left + # w przyszłość + s_left = speed(moment, 0.0) + d = 0.0 + while d < SEARCH_DAYS: + s_right = speed(moment, d + step_days) + if (s_left < 0) != (s_right < 0): + next_day = _bisect_zero(speed, moment, d, d + step_days) + break + d, s_left = d + step_days, s_right + + result: dict = {} + if prev_day is not None: + result["prev"] = _station_info(engine, moment, name, prev_day, speed) + if next_day is not None: + result["next"] = _station_info(engine, moment, name, next_day, speed) + result["station_soon"] = any( + abs(x["days"]) < STATION_SOON_DAYS for x in result.values() if isinstance(x, dict) + ) + return result or None diff --git a/services/logic/app/main.py b/services/logic/app/main.py index 208a0f6..aa4fb7a 100644 --- a/services/logic/app/main.py +++ b/services/logic/app/main.py @@ -38,6 +38,7 @@ class PositionsRequest(BaseModel): lon: float = 0.0 objects: list[str] | None = None house_system: str = "whole_sign" # whole_sign | equal | porphyry + stations: bool = False # licz stacje (LOG-03; wolniejsze — root-findy) @app.post("/api/query", response_model=QueryResponse) @@ -50,13 +51,22 @@ def query(req: QueryRequest) -> QueryResponse: @app.post("/chart/positions") def chart_positions(req: PositionsRequest) -> dict: - """Pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05), aktywnym silnikiem.""" + """Pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty (LOG-06); + opcjonalnie stacje planet (LOG-03, stations=true).""" from app.engine.chart import build_chart from app.engine.models import ChartMoment engine = get_engine() moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon) - return build_chart(engine, moment, req.house_system) + chart = build_chart(engine, moment, req.house_system) + if req.stations: + from app.engine.stations import find_stations + + for p in chart["positions"]: + st = find_stations(engine, moment, p["name"]) + if st: + p["stations"] = st + return chart @app.post("/chart/compare") diff --git a/services/logic/app/significators.py b/services/logic/app/significators.py index 9f3d64d..76544e3 100644 --- a/services/logic/app/significators.py +++ b/services/logic/app/significators.py @@ -20,6 +20,8 @@ from app.scoring import score_facet PLANET_ABBR = { "Sun": "Su", "Moon": "Mo", "Mercury": "Me", "Venus": "Ve", "Mars": "Ma", "Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl", + # punkty wirtualne — tokeny wg SIGNIFICATORS KEY ([NN, [SN, [Lilith) + "North Node": "NN", "South Node": "SN", "Lilith": "Lilith", } SIGN_TO_ABBR = dict(zip(SIGNS, SIGN_ABBR)) diff --git a/services/logic/tests/test_points.py b/services/logic/tests/test_points.py new file mode 100644 index 0000000..440bce6 --- /dev/null +++ b/services/logic/tests/test_points.py @@ -0,0 +1,50 @@ +"""Testy punktów wirtualnych (LOG-02): mean Node, mean Lilith. + +Referencje dla 30.04.1984 07:35 UT: +- astro-seek (notes3): North Node (M) = Gem 8°09'24" = 68.1567° +- wyrocznia swisseph (MEAN_NODE / MEAN_APOG, tryb Moshiera): + NN = 68.1569°, Lilith = 345.6840° +""" +import pytest + +from app.engine.formats import norm360 + + +def _delta_arcmin(a: float, b: float) -> float: + return abs(((a - b + 180.0) % 360.0) - 180.0) * 60.0 + + +@pytest.fixture(scope="module") +def by_name(own_engine, reference_moment): + return {p.name: p for p in own_engine.positions(reference_moment)} + + +def test_mean_node_matches_references(by_name): + nn = by_name["North Node"] + assert _delta_arcmin(nn.longitude, 68.1567) < 2.0 # astro-seek + assert _delta_arcmin(nn.longitude, 68.1569) < 2.0 # swisseph + assert nn.sign == "Gemini" + + +def test_nodes_always_retrograde_and_opposed(by_name): + nn, sn = by_name["North Node"], by_name["South Node"] + assert nn.retrograde and sn.retrograde # mean node zawsze Rx + assert _delta_arcmin(sn.longitude, norm360(nn.longitude + 180.0)) < 0.01 + assert abs(nn.speed - sn.speed) < 1e-9 # ta sama prędkość + + +def test_mean_lilith_matches_swisseph(by_name): + li = by_name["Lilith"] + assert _delta_arcmin(li.longitude, 345.6840) < 3.0 # wyrocznia swisseph + assert li.sign == "Pisces" + assert li.speed > 0 and not li.retrograde # mean Lilith zawsze direct + + +def test_points_join_houses_and_chart(own_engine, reference_moment): + from app.engine.chart import build_chart + + chart = build_chart(own_engine, reference_moment) + by = {p["name"]: p for p in chart["positions"]} + # NN w Gem -> 12. dom Whole Sign (Asc w Raku); zgodnie z tabelą astro-seek w notes3 + assert by["North Node"]["house"] == 12 + assert by["Lilith"]["house"] == 9 # Pis -> 9. dom diff --git a/services/logic/tests/test_stations.py b/services/logic/tests/test_stations.py new file mode 100644 index 0000000..f9a2c80 --- /dev/null +++ b/services/logic/tests/test_stations.py @@ -0,0 +1,55 @@ +"""Testy wykrywania stacji (LOG-03). + +Fakt historyczny: Mars w horoskopie referencyjnym (30.04.1984) jest w środku +retrogradacji — stacja SR ~5.04.1984 (ok. 25 dni wstecz), stacja SD ~19.06.1984 +(ok. 50 dni w przód). Testy sprawdzają strukturę, klasyfikację SD/SR, przedziały +dat i samospójność (prędkość w znalezionym momencie ~0). +""" +import datetime as dt + +import pytest + +from app.engine.models import ChartMoment +from app.engine.stations import STATION_SOON_DAYS, find_stations + + +@pytest.fixture(scope="module") +def mars_stations(own_engine, reference_moment): + return find_stations(own_engine, reference_moment, "Mars") + + +def test_no_stations_for_sun_moon_and_points(own_engine, reference_moment): + for name in ("Sun", "Moon", "North Node", "South Node", "Lilith"): + assert find_stations(own_engine, reference_moment, name) is None + + +def test_mars_prev_is_sr_next_is_sd(mars_stations): + assert mars_stations["prev"]["type"] == "SR" # wszedł w retrogradację + assert mars_stations["next"]["type"] == "SD" # wróci do ruchu prostego + + +def test_mars_station_windows_match_history(mars_stations): + # SR ~5.04.1984 -> ok. -25 dni; SD ~19/20.06.1984 -> ok. +50 dni + assert -35 < mars_stations["prev"]["days"] < -15 + assert 40 < mars_stations["next"]["days"] < 60 + assert mars_stations["prev"]["date"].startswith("1984-04") + assert mars_stations["next"]["date"].startswith("1984-06") + + +def test_station_speed_is_near_zero(own_engine, reference_moment, mars_stations): + """Samospójność: w znalezionym momencie stacji prędkość Marsa ~0.""" + for key in ("prev", "next"): + when = dt.datetime.strptime(mars_stations[key]["date"], "%Y-%m-%d %H:%M").replace( + tzinfo=dt.timezone.utc + ) + m = ChartMoment(when_utc=when, lat=reference_moment.lat, lon=reference_moment.lon) + speed = own_engine.positions(m, ["Mars"])[0].speed + assert abs(speed) < 0.01, f"{key}: speed={speed}" + + +def test_station_soon_flag_consistent(mars_stations): + expected = any( + abs(mars_stations[k]["days"]) < STATION_SOON_DAYS + for k in ("prev", "next") if k in mars_stations + ) + assert mars_stations["station_soon"] == expected diff --git a/services/presentation/app/clients/logic_client.py b/services/presentation/app/clients/logic_client.py index bc8c0f0..243f6dc 100644 --- a/services/presentation/app/clients/logic_client.py +++ b/services/presentation/app/clients/logic_client.py @@ -30,6 +30,7 @@ class LogicClient: lon: float, objects: list[str] | None = None, house_system: str = "whole_sign", + stations: bool = False, ) -> dict[str, Any]: """Pełny horoskop dla danego momentu — woła logic /chart/positions.""" payload = { @@ -38,8 +39,10 @@ class LogicClient: "lon": lon, "objects": objects, "house_system": house_system, + "stations": stations, } - with httpx.Client(timeout=settings.http_timeout) as client: + # stacje wymagają root-findów — dłuższy timeout + with httpx.Client(timeout=max(settings.http_timeout, 60.0) if stations else settings.http_timeout) as client: r = client.post(f"{self.base_url}/chart/positions", json=payload) r.raise_for_status() return r.json() diff --git a/services/presentation/app/main.py b/services/presentation/app/main.py index 1b5a91c..4b17cf2 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -60,15 +60,17 @@ def chart_compute( lat: float = Form(0.0), lon: float = Form(0.0), house_system: str = Form("whole_sign"), + stations: bool = Form(False), ): form = {"date": date, "time": time, "tz_offset": tz_offset, - "lat": lat, "lon": lon, "house_system": house_system} + "lat": lat, "lon": lon, "house_system": house_system, "stations": stations} 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 + when_utc_iso=iso_utc, lat=lat, lon=lon, + house_system=house_system, stations=stations, ) except (httpx.HTTPError,) as e: ctx["error"] = _logic_error(e) diff --git a/services/presentation/app/templates/chart.html b/services/presentation/app/templates/chart.html index 9264432..7d1ae20 100644 --- a/services/presentation/app/templates/chart.html +++ b/services/presentation/app/templates/chart.html @@ -33,6 +33,10 @@ +
+ +
@@ -84,6 +88,23 @@ + {% 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 }})