Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73f39e7df8 | |||
| c4a181b810 | |||
| 97ad21d2e4 | |||
| 2dbd3410e9 | |||
| ca458fd741 | |||
| b013831492 | |||
| d14a77360a |
@@ -0,0 +1,19 @@
|
|||||||
|
name: build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Login
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.czernobog.pl -u gitea --password-stdin
|
||||||
|
- name: Build & push (data, logic, presentation)
|
||||||
|
run: |
|
||||||
|
TAG=${GITHUB_SHA::8}
|
||||||
|
for SVC in data logic presentation; do
|
||||||
|
docker build -t gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG ./services/$SVC
|
||||||
|
docker push gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG
|
||||||
|
done
|
||||||
|
echo "Tag: $TAG"
|
||||||
@@ -8,6 +8,9 @@ i nie w bazie.
|
|||||||
- `POST /api/query` → `QueryRequest` → `QueryResponse`
|
- `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) + 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/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/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); 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)
|
- `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
|
||||||
- `GET /health` (sprawdza też warstwę bazodanową)
|
- `GET /health` (sprawdza też warstwę bazodanową)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""Profekcje roczne (LOG-10) — hellenistyczna technika time-lord.
|
||||||
|
|
||||||
|
Zasada (Whole Sign): co każde urodziny profektowany Ascendent przeskakuje o jeden
|
||||||
|
znak do przodu (wiek mod 12). Władca Roku (Lord of Year) = władca domicylowy
|
||||||
|
znaku profektowanego Asc. Profektować można każdy punkt natalny (MC, Słońce…) —
|
||||||
|
wszystkie przeskakują o tyle samo znaków.
|
||||||
|
|
||||||
|
Referencja: tabela profekcji w notes3 (astro-seek) dla horoskopu 30.04.1984
|
||||||
|
(wiek 0: Can/Moon, 1: Leo/Sun, …, 42: Cap/Saturn).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.engine.formats import SIGNS, sign_index
|
||||||
|
|
||||||
|
# władcy domicylowi (tradycyjni) — zgodni z tabelą referencyjną notes3
|
||||||
|
DOMICILE_RULERS = {
|
||||||
|
"Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon",
|
||||||
|
"Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars",
|
||||||
|
"Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn",
|
||||||
|
"Pisces": "Jupiter",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def age_at(birth_utc: datetime, when_utc: datetime) -> int:
|
||||||
|
"""Pełne lata między urodzeniem a danym momentem (wiek profekcyjny)."""
|
||||||
|
age = when_utc.year - birth_utc.year
|
||||||
|
if (when_utc.month, when_utc.day) < (birth_utc.month, birth_utc.day):
|
||||||
|
age -= 1
|
||||||
|
return max(age, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def profected_sign(natal_lon: float, age: int) -> str:
|
||||||
|
"""Znak, do którego profektował punkt natalny po `age` latach."""
|
||||||
|
return SIGNS[(sign_index(natal_lon) + age) % 12]
|
||||||
|
|
||||||
|
|
||||||
|
def profection_rows(
|
||||||
|
natal_points: dict[str, float],
|
||||||
|
birth_utc: datetime,
|
||||||
|
start_age: int,
|
||||||
|
count: int,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Tabela profekcji dla zakresu lat życia.
|
||||||
|
|
||||||
|
natal_points: nazwa -> natalna długość ekliptyczna (musi zawierać 'Asc').
|
||||||
|
Każdy wiersz: wiek, data początku roku profekcyjnego (urodziny), znak
|
||||||
|
profektowanego Asc, Władca Roku oraz profekcje pozostałych punktów.
|
||||||
|
"""
|
||||||
|
def _birthday(year: int) -> datetime:
|
||||||
|
try:
|
||||||
|
return birth_utc.replace(year=year)
|
||||||
|
except ValueError: # 29 lutego w roku nieprzestępnym
|
||||||
|
return birth_utc.replace(year=year, day=28)
|
||||||
|
|
||||||
|
rows: list[dict] = []
|
||||||
|
for age in range(start_age, start_age + count):
|
||||||
|
asc_sign = profected_sign(natal_points["Asc"], age)
|
||||||
|
row = {
|
||||||
|
"age": age,
|
||||||
|
"from": _birthday(birth_utc.year + age).strftime("%Y-%m-%d"),
|
||||||
|
"profected_asc": asc_sign,
|
||||||
|
"lord_of_year": DOMICILE_RULERS[asc_sign],
|
||||||
|
}
|
||||||
|
for name, lon in natal_points.items():
|
||||||
|
if name != "Asc":
|
||||||
|
row[name] = profected_sign(lon, age)
|
||||||
|
rows.append(row)
|
||||||
|
return rows
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Solar / Lunar Return (LOG-12) — moment powrotu do pozycji natalnej.
|
||||||
|
|
||||||
|
Solar Return (solariusz): moment, w którym Słońce wraca dokładnie do natalnej
|
||||||
|
długości ekliptycznej (raz na rok, w okolicy urodzin). Lunar Return: to samo
|
||||||
|
dla Księżyca (raz na ~27,3 dnia). Dwa warianty użycia (osobny horoskop vs
|
||||||
|
tranzyt do natalu) obsługujemy zwracając pełny horoskop na znaleziony moment —
|
||||||
|
interpretacja pozostaje po stronie technik wyżej.
|
||||||
|
|
||||||
|
Metoda: podpisana różnica długości Δ = lon − natal (zawinięta do ±180°) rośnie
|
||||||
|
monotonicznie i przechodzi przez 0 dokładnie w momencie powrotu. Skan dobowy
|
||||||
|
wykrywa przejście −→+ (skok +180→−180 to artefakt zawinięcia — pomijany,
|
||||||
|
warunek d_hi − d_lo < 180), potem bisekcja do ~sekundy.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from app.engine.models import ChartMoment
|
||||||
|
|
||||||
|
# szerokość okna skanu wokół `around` [dni]: solar kotwiczymy przy urodzinach,
|
||||||
|
# lunar musi objąć cały okres syderyczny Księżyca (27,3 d)
|
||||||
|
SCAN_WINDOW = {"solar": 6.0, "lunar": 15.0}
|
||||||
|
|
||||||
|
|
||||||
|
def _lon_delta(engine, body: str, natal_lon: float, when: datetime) -> float:
|
||||||
|
m = ChartMoment(when_utc=when)
|
||||||
|
lon = engine.positions(m, [body])[0].longitude
|
||||||
|
return ((lon - natal_lon + 180.0) % 360.0) - 180.0
|
||||||
|
|
||||||
|
|
||||||
|
def find_return(
|
||||||
|
engine, kind: str, natal_moment: ChartMoment, around: datetime
|
||||||
|
) -> datetime | None:
|
||||||
|
"""Moment powrotu (kind: 'solar'/'lunar') najbliższy dacie `around`."""
|
||||||
|
body = "Sun" if kind == "solar" else "Moon"
|
||||||
|
natal_lon = engine.positions(natal_moment, [body])[0].longitude
|
||||||
|
if around.tzinfo is None:
|
||||||
|
around = around.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
window = SCAN_WINDOW[kind]
|
||||||
|
step = timedelta(days=1.0)
|
||||||
|
t = around - timedelta(days=window)
|
||||||
|
end = around + timedelta(days=window)
|
||||||
|
|
||||||
|
candidates: list[datetime] = []
|
||||||
|
d_prev = _lon_delta(engine, body, natal_lon, t)
|
||||||
|
while t < end:
|
||||||
|
t_next = t + step
|
||||||
|
d_next = _lon_delta(engine, body, natal_lon, t_next)
|
||||||
|
# prawdziwe przejście przez zero: − -> + bez skoku zawinięcia
|
||||||
|
if d_prev < 0 <= d_next and (d_next - d_prev) < 180.0:
|
||||||
|
lo, hi, d_lo = t, t_next, d_prev
|
||||||
|
for _ in range(40): # bisekcja do ułamka sekundy
|
||||||
|
mid = lo + (hi - lo) / 2
|
||||||
|
if (_lon_delta(engine, body, natal_lon, mid) < 0) == (d_lo < 0):
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
candidates.append(lo + (hi - lo) / 2)
|
||||||
|
t, d_prev = t_next, d_next
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
return min(candidates, key=lambda c: abs(c - around))
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""Zbiorcza tabela dat z technik (LOG-14).
|
||||||
|
|
||||||
|
Spina w jedną, posortowaną oś czasu daty z kilku technik:
|
||||||
|
- profekcje roczne (LOG-10) — rok życia,
|
||||||
|
- Solar Return (LOG-12) — moment powrotu Słońca,
|
||||||
|
- dyrekcje solar-arc — daty dokładnych aspektów kierowanych planet do punktów
|
||||||
|
natalnych (wzorzec z notes3: „Profection planet | Aspect | Birth planet |
|
||||||
|
Exact Date"). Klucz łuku konfigurowalny; domyślnie Naiboda (0°59'08"/rok).
|
||||||
|
|
||||||
|
Każdy wiersz ma kształt z notes2: technique | significator | start | exact | end.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from app.engine.aspects import MAJOR, PL_NAME
|
||||||
|
from app.engine.profections import DOMICILE_RULERS, profected_sign
|
||||||
|
from app.engine.returns import find_return
|
||||||
|
|
||||||
|
NAIBOD_KEY = 0.9856472 # °/rok (0°59'08") — domyślny klucz solar-arc
|
||||||
|
DAYS_PER_YEAR = 365.2422
|
||||||
|
DIRECTED = ["Sun", "Moon", "Mercury", "Venus", "Mars",
|
||||||
|
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"]
|
||||||
|
|
||||||
|
|
||||||
|
def _add_years(birth: datetime, years: float) -> datetime:
|
||||||
|
return birth + timedelta(days=years * DAYS_PER_YEAR)
|
||||||
|
|
||||||
|
|
||||||
|
def _row(technique, significator, start, exact, end) -> dict:
|
||||||
|
def iso(x):
|
||||||
|
return x.date().isoformat() if isinstance(x, datetime) else x
|
||||||
|
return {"technique": technique, "significator": significator,
|
||||||
|
"start": iso(start), "exact": iso(exact), "end": iso(end)}
|
||||||
|
|
||||||
|
|
||||||
|
def solar_arc_directions(
|
||||||
|
natal: dict[str, float], birth: datetime, lo: datetime, hi: datetime,
|
||||||
|
key: float = NAIBOD_KEY, orb_years: float = 1.0,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Daty dyrekcji solar-arc w oknie [lo, hi].
|
||||||
|
|
||||||
|
natal: nazwa punktu -> długość natalna (planety + Asc/MC). Kierowane są planety
|
||||||
|
(DIRECTED), celem każdy punkt natalny. Aspekt dokładny gdy łuk = odległość
|
||||||
|
kątowa (mod 360). Wiek = łuk/klucz; data = urodziny + wiek.
|
||||||
|
"""
|
||||||
|
out: list[dict] = []
|
||||||
|
lo_age = (lo - birth).days / DAYS_PER_YEAR - orb_years
|
||||||
|
hi_age = (hi - birth).days / DAYS_PER_YEAR + orb_years
|
||||||
|
for p in DIRECTED:
|
||||||
|
if p not in natal:
|
||||||
|
continue
|
||||||
|
for q, q_lon in natal.items():
|
||||||
|
for asp, angle in MAJOR.items():
|
||||||
|
for target in ({angle, (360.0 - angle) % 360.0}):
|
||||||
|
arc = (q_lon + target - natal[p]) % 360.0
|
||||||
|
age = arc / key
|
||||||
|
if not (lo_age <= age <= hi_age) or (p == q and arc < 1e-6):
|
||||||
|
continue
|
||||||
|
exact = _add_years(birth, age)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def profection_events(natal_asc: float, birth: datetime, lo: datetime, hi: datetime) -> list[dict]:
|
||||||
|
"""Lata profekcyjne (LOG-10) nachodzące na okno."""
|
||||||
|
out: list[dict] = []
|
||||||
|
for age in range((lo.year - birth.year) - 1, (hi.year - birth.year) + 1):
|
||||||
|
if age < 0:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
start = birth.replace(year=birth.year + age)
|
||||||
|
end = birth.replace(year=birth.year + age + 1)
|
||||||
|
except ValueError: # 29 lutego
|
||||||
|
start = birth.replace(year=birth.year + age, day=28)
|
||||||
|
end = birth.replace(year=birth.year + age + 1, day=28)
|
||||||
|
if end < lo or start > hi:
|
||||||
|
continue
|
||||||
|
sign = profected_sign(natal_asc, 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
|
||||||
|
|
||||||
|
|
||||||
|
def solar_return_events(engine, natal_moment, birth: datetime, lo: datetime, hi: datetime) -> list[dict]:
|
||||||
|
"""Solariusze w oknie (LOG-12) — jeden na rok."""
|
||||||
|
out: list[dict] = []
|
||||||
|
for year in range(lo.year, hi.year + 1):
|
||||||
|
try:
|
||||||
|
around = birth.replace(year=year)
|
||||||
|
except ValueError:
|
||||||
|
around = birth.replace(year=year, day=28)
|
||||||
|
hit = find_return(engine, "solar", natal_moment, around)
|
||||||
|
if hit and lo <= hit <= hi:
|
||||||
|
out.append(_row("solar_return", "Solar Return", hit, hit, _add_years(hit, 1)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _as_dt(d) -> datetime:
|
||||||
|
if isinstance(d, datetime):
|
||||||
|
return d if d.tzinfo else d.replace(tzinfo=timezone.utc)
|
||||||
|
if isinstance(d, date):
|
||||||
|
return datetime(d.year, d.month, d.day, tzinfo=timezone.utc)
|
||||||
|
return datetime.fromisoformat(str(d)).replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def build_timeline(
|
||||||
|
engine, natal_moment, natal_points: dict[str, float],
|
||||||
|
from_d, to_d, techniques: list[str] | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Scala wybrane techniki w jedną oś czasu, posortowaną po dacie dokładnej."""
|
||||||
|
lo, hi = _as_dt(from_d), _as_dt(to_d)
|
||||||
|
birth = natal_moment.when_utc
|
||||||
|
want = set(techniques or ["profection", "solar_return", "solar_arc"])
|
||||||
|
events: list[dict] = []
|
||||||
|
if "profection" in want:
|
||||||
|
events += profection_events(natal_points["Asc"], birth, lo, hi)
|
||||||
|
if "solar_return" in want:
|
||||||
|
events += solar_return_events(engine, natal_moment, birth, lo, hi)
|
||||||
|
if "solar_arc" in want:
|
||||||
|
events += solar_arc_directions(natal_points, birth, lo, hi)
|
||||||
|
events.sort(key=lambda e: e["exact"])
|
||||||
|
return events
|
||||||
@@ -117,6 +117,101 @@ def chart_report(req: ReportRequest) -> dict:
|
|||||||
return {"engine": engine.name, **report}
|
return {"engine": engine.name, **report}
|
||||||
|
|
||||||
|
|
||||||
|
class ProfectionsRequest(BaseModel):
|
||||||
|
when_utc: datetime # moment urodzenia (UTC)
|
||||||
|
lat: float = 0.0
|
||||||
|
lon: float = 0.0
|
||||||
|
start_age: int = 0
|
||||||
|
count: int = 13 # domyślnie pełny cykl 12 lat + rok startowy
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/chart/profections")
|
||||||
|
def chart_profections(req: ProfectionsRequest) -> dict:
|
||||||
|
"""Profekcje roczne (LOG-10): wiek, profektowany Asc, Władca Roku (+MC/Su/Mo)."""
|
||||||
|
from app.engine import houses as H
|
||||||
|
from app.engine.models import ChartMoment
|
||||||
|
from app.engine.profections import profection_rows
|
||||||
|
|
||||||
|
engine = get_engine()
|
||||||
|
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||||
|
ramc, eps = engine.sidereal(natal)
|
||||||
|
points = {
|
||||||
|
"Asc": H.compute_asc(ramc, eps, natal.lat),
|
||||||
|
"MC": H.compute_mc(ramc, eps),
|
||||||
|
}
|
||||||
|
for p in engine.positions(natal, ["Sun", "Moon"]):
|
||||||
|
points[p.name] = p.longitude
|
||||||
|
rows = profection_rows(points, req.when_utc, req.start_age, req.count)
|
||||||
|
return {"engine": engine.name, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
class ReturnRequest(BaseModel):
|
||||||
|
when_utc: datetime # moment urodzenia (UTC)
|
||||||
|
lat: float = 0.0
|
||||||
|
lon: float = 0.0
|
||||||
|
kind: str = "solar" # solar | lunar
|
||||||
|
around: datetime | None = None # data, wokół której szukać powrotu
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/chart/return")
|
||||||
|
def chart_return(req: ReturnRequest) -> dict:
|
||||||
|
"""Solar/Lunar Return (LOG-12): moment powrotu + pełny horoskop na ten moment."""
|
||||||
|
from app.engine.chart import build_chart
|
||||||
|
from app.engine.models import ChartMoment
|
||||||
|
from app.engine.returns import find_return
|
||||||
|
|
||||||
|
if req.kind not in ("solar", "lunar"):
|
||||||
|
raise HTTPException(status_code=422, detail="kind: solar albo lunar")
|
||||||
|
engine = get_engine()
|
||||||
|
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||||
|
around = req.around or req.when_utc
|
||||||
|
hit = find_return(engine, req.kind, natal, around)
|
||||||
|
if hit is None:
|
||||||
|
raise HTTPException(status_code=404, detail="nie znaleziono powrotu w oknie skanu")
|
||||||
|
chart = build_chart(engine, ChartMoment(when_utc=hit, lat=req.lat, lon=req.lon))
|
||||||
|
return {"engine": engine.name, "kind": req.kind,
|
||||||
|
"return_utc": hit.isoformat(), **chart}
|
||||||
|
|
||||||
|
|
||||||
|
class TimelineRequest(BaseModel):
|
||||||
|
when_utc: datetime # moment urodzenia (UTC)
|
||||||
|
lat: float = 0.0
|
||||||
|
lon: float = 0.0
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
engine = get_engine()
|
||||||
|
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||||
|
ramc, eps = engine.sidereal(natal)
|
||||||
|
points = {"Asc": H.compute_asc(ramc, eps, natal.lat), "MC": H.compute_mc(ramc, eps)}
|
||||||
|
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)
|
||||||
|
|
||||||
|
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")
|
@app.get("/health")
|
||||||
def health() -> dict:
|
def health() -> dict:
|
||||||
info = {"status": "ok", "layer": "logic"}
|
info = {"status": "ok", "layer": "logic"}
|
||||||
|
|||||||
@@ -195,3 +195,52 @@ def build_report(
|
|||||||
"facets": facets,
|
"facets": facets,
|
||||||
})
|
})
|
||||||
return {"provider": provider, "objects": items}
|
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
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Profekcje (LOG-10) — walidacja względem tabeli astro-seek z notes3."""
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from app.engine.profections import DOMICILE_RULERS, age_at, profected_sign, profection_rows
|
||||||
|
|
||||||
|
BIRTH = dt.datetime(1984, 4, 30, 7, 35, tzinfo=dt.timezone.utc)
|
||||||
|
NATAL = {"Asc": 112.18, "MC": 352.59, "Sun": 40.14, "Moon": 30.55}
|
||||||
|
|
||||||
|
# wiek -> (profektowany Asc, Władca Roku) — tabela referencyjna notes3
|
||||||
|
REFERENCE = {
|
||||||
|
0: ("Cancer", "Moon"), 1: ("Leo", "Sun"), 2: ("Virgo", "Mercury"),
|
||||||
|
3: ("Libra", "Venus"), 4: ("Scorpio", "Mars"), 5: ("Sagittarius", "Jupiter"),
|
||||||
|
6: ("Capricorn", "Saturn"), 7: ("Aquarius", "Saturn"), 8: ("Pisces", "Jupiter"),
|
||||||
|
9: ("Aries", "Mars"), 10: ("Taurus", "Venus"), 11: ("Gemini", "Mercury"),
|
||||||
|
12: ("Cancer", "Moon"), 40: ("Scorpio", "Mars"), 41: ("Sagittarius", "Jupiter"),
|
||||||
|
42: ("Capricorn", "Saturn"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_profections_match_astroseek_table():
|
||||||
|
rows = {r["age"]: r for r in profection_rows(NATAL, BIRTH, 0, 43)}
|
||||||
|
for age, (asc, lord) in REFERENCE.items():
|
||||||
|
assert rows[age]["profected_asc"] == asc, f"wiek {age}"
|
||||||
|
assert rows[age]["lord_of_year"] == lord, f"wiek {age}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_profected_secondary_points_match_reference():
|
||||||
|
rows = {r["age"]: r for r in profection_rows(NATAL, BIRTH, 0, 3)}
|
||||||
|
# notes3: wiek 0 -> MC Pis, Sun Tau, Moon Tau; wiek 1 -> MC Ari, Sun Gem
|
||||||
|
assert rows[0]["MC"] == "Pisces" and rows[0]["Sun"] == "Taurus"
|
||||||
|
assert rows[1]["MC"] == "Aries" and rows[1]["Sun"] == "Gemini"
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_dates_are_birthdays():
|
||||||
|
rows = profection_rows(NATAL, BIRTH, 40, 3)
|
||||||
|
assert [r["from"] for r in rows] == ["2024-04-30", "2025-04-30", "2026-04-30"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_age_at_boundaries():
|
||||||
|
assert age_at(BIRTH, dt.datetime(2026, 4, 29, tzinfo=dt.timezone.utc)) == 41
|
||||||
|
assert age_at(BIRTH, dt.datetime(2026, 4, 30, tzinfo=dt.timezone.utc)) == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_rulers_cover_all_signs():
|
||||||
|
assert len(DOMICILE_RULERS) == 12
|
||||||
|
assert profected_sign(112.18, 12) == "Cancer" # pełny cykl wraca
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Solar / Lunar Return (LOG-12) — samospójność i sensowność dat."""
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.engine.models import ChartMoment
|
||||||
|
from app.engine.returns import find_return
|
||||||
|
|
||||||
|
|
||||||
|
def _lon(engine, body, when):
|
||||||
|
return engine.positions(ChartMoment(when_utc=when), [body])[0].longitude
|
||||||
|
|
||||||
|
|
||||||
|
def _delta_arcmin(a, b):
|
||||||
|
return abs(((a - b + 180.0) % 360.0) - 180.0) * 60.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_solar_return_hits_natal_sun(own_engine, reference_moment):
|
||||||
|
natal_sun = _lon(own_engine, "Sun", reference_moment.when_utc)
|
||||||
|
# solariusz na 42. urodziny (2026)
|
||||||
|
hit = find_return(own_engine, "solar", reference_moment,
|
||||||
|
dt.datetime(2026, 4, 30, tzinfo=dt.timezone.utc))
|
||||||
|
assert hit is not None
|
||||||
|
assert _delta_arcmin(_lon(own_engine, "Sun", hit), natal_sun) < 0.5
|
||||||
|
assert hit.month == 4 and hit.year == 2026 # w okolicy urodzin
|
||||||
|
|
||||||
|
|
||||||
|
def test_lunar_return_hits_natal_moon(own_engine, reference_moment):
|
||||||
|
natal_moon = _lon(own_engine, "Moon", reference_moment.when_utc)
|
||||||
|
hit = find_return(own_engine, "lunar", reference_moment,
|
||||||
|
dt.datetime(1984, 5, 27, tzinfo=dt.timezone.utc))
|
||||||
|
assert hit is not None
|
||||||
|
assert _delta_arcmin(_lon(own_engine, "Moon", hit), natal_moon) < 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_solar_return_near_birth_is_close_to_birth(own_engine, reference_moment):
|
||||||
|
# powrót szukany wokół samych urodzin = ~moment urodzenia
|
||||||
|
hit = find_return(own_engine, "solar", reference_moment, reference_moment.when_utc)
|
||||||
|
assert hit is not None
|
||||||
|
assert abs(hit - reference_moment.when_utc) < dt.timedelta(days=1)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""Zbiorcza oś czasu z technik (LOG-14)."""
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from app.engine.timeline import (
|
||||||
|
NAIBOD_KEY,
|
||||||
|
build_timeline,
|
||||||
|
profection_events,
|
||||||
|
solar_arc_directions,
|
||||||
|
)
|
||||||
|
|
||||||
|
BIRTH = dt.datetime(1984, 4, 30, 7, 35, tzinfo=dt.timezone.utc)
|
||||||
|
# natalne długości (z horoskopu referencyjnego)
|
||||||
|
NATAL = {
|
||||||
|
"Asc": 112.18, "MC": 352.59, "Sun": 40.14, "Moon": 30.55, "Mercury": 27.38,
|
||||||
|
"Venus": 27.68, "Mars": 234.54, "Jupiter": 282.96, "Saturn": 223.31,
|
||||||
|
"Uranus": 252.81, "Neptune": 271.22, "Pluto": 210.48,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _win(y0, y1):
|
||||||
|
return (dt.datetime(y0, 1, 1, tzinfo=dt.timezone.utc),
|
||||||
|
dt.datetime(y1, 12, 31, tzinfo=dt.timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
|
def test_profection_events_in_window():
|
||||||
|
lo, hi = _win(2024, 2026)
|
||||||
|
rows = profection_events(NATAL["Asc"], BIRTH, lo, hi)
|
||||||
|
# rok profekcyjny wiek 42 zaczyna się 30.04.2026 -> Asc Capricorn
|
||||||
|
ages = [r["significator"] for r in rows]
|
||||||
|
assert any("Asc Capricorn" in a and "wiek 42" in a for a in ages)
|
||||||
|
|
||||||
|
|
||||||
|
def test_solar_arc_exact_matches_arc_over_key():
|
||||||
|
lo, hi = _win(2020, 2030)
|
||||||
|
rows = solar_arc_directions(NATAL, BIRTH, lo, hi, key=NAIBOD_KEY)
|
||||||
|
assert rows, "brak dyrekcji w oknie"
|
||||||
|
# dla każdej dyrekcji: łuk = (wiek * klucz), a data = urodziny + wiek -> spójne
|
||||||
|
for r in rows[:5]:
|
||||||
|
exact = dt.date.fromisoformat(r["exact"])
|
||||||
|
age_years = (dt.datetime(exact.year, exact.month, exact.day, tzinfo=dt.timezone.utc)
|
||||||
|
- BIRTH).days / 365.2422
|
||||||
|
assert 36 <= age_years <= 47 # okno 2020-2030 = wiek ~36-46
|
||||||
|
assert r["technique"] == "solar_arc" and "dyr." in r["significator"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_directed_sun_conjunct_natal_mc_date():
|
||||||
|
# Sun natal 40.14 -> MC natal 352.59: łuk koniunkcji = (352.59-40.14)%360 = 312.45
|
||||||
|
# to > lifespan przy Naibod (~317 lat) -> NIE powinno być w oknie życia
|
||||||
|
lo, hi = _win(1984, 2084)
|
||||||
|
rows = solar_arc_directions(NATAL, BIRTH, lo, hi)
|
||||||
|
sun_mc = [r for r in rows if r["significator"] == "dyr. Sun koniunkcja MC"]
|
||||||
|
assert not sun_mc # łuk 312° = poza życiem
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_timeline_sorted_and_merged():
|
||||||
|
events = build_timeline(_FakeEngine(), _FakeNatal(), NATAL,
|
||||||
|
"2025-01-01", "2027-01-01",
|
||||||
|
techniques=["profection", "solar_arc"])
|
||||||
|
assert events
|
||||||
|
dates = [e["exact"] for e in events]
|
||||||
|
assert dates == sorted(dates) # posortowane po dacie dokładnej
|
||||||
|
techs = {e["technique"] for e in events}
|
||||||
|
assert "profection" in techs and "solar_arc" in techs
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeNatal:
|
||||||
|
when_utc = BIRTH
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeEngine:
|
||||||
|
"""Silnik-atrapa — build_timeline z solar_return by go użył, tu go pomijamy."""
|
||||||
@@ -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
|
||||||
@@ -56,3 +56,17 @@ class LogicClient:
|
|||||||
r = client.post(f"{self.base_url}/chart/report", json=payload)
|
r = client.post(f"{self.base_url}/chart/report", json=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
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()
|
||||||
|
|||||||
@@ -132,6 +132,40 @@ def interpret_run(
|
|||||||
return templates.TemplateResponse(request, "interpret.html", ctx)
|
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")
|
@app.get("/health")
|
||||||
def health() -> dict:
|
def health() -> dict:
|
||||||
return {"status": "ok", "layer": "presentation"}
|
return {"status": "ok", "layer": "presentation"}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<a href="/" class="{% block nav_chart %}{% endblock %}">Horoskop</a>
|
<a href="/" class="{% block nav_chart %}{% endblock %}">Horoskop</a>
|
||||||
<a href="/interpret" class="{% block nav_interp %}{% endblock %}">Interpretacje</a>
|
<a href="/interpret" class="{% block nav_interp %}{% endblock %}">Interpretacje</a>
|
||||||
|
<a href="/timeline" class="{% block nav_timeline %}{% endblock %}">Kalendarz</a>
|
||||||
<a href="/significators" class="{% block nav_sig %}{% endblock %}">Sygnifikatory</a>
|
<a href="/significators" class="{% block nav_sig %}{% endblock %}">Sygnifikatory</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Kalendarz{% endblock %}
|
||||||
|
{% block nav_timeline %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p class="sub">Zbiorcza oś czasu technik predykcyjnych (profekcje, solariusze, dyrekcje solar-arc) z interpretacjami z bazy dla dat.</p>
|
||||||
|
|
||||||
|
<form method="post" action="/timeline">
|
||||||
|
<div class="grid">
|
||||||
|
<label>Data urodzenia
|
||||||
|
<input type="date" name="date" value="{{ form.date or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label>Godzina (lokalna)
|
||||||
|
<input type="time" name="time" value="{{ form.time or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label>Strefa (offset h)
|
||||||
|
<input type="number" name="tz_offset" step="0.25" value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
<label>Szerokość (lat)
|
||||||
|
<input type="number" name="lat" step="0.0001" value="{{ form.lat if form.lat is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
<label>Długość (lon)
|
||||||
|
<input type="number" name="lon" step="0.0001" value="{{ form.lon if form.lon is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<label>Zakres od
|
||||||
|
<input type="date" name="from_date" value="{{ form.from_date or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label>Zakres do
|
||||||
|
<input type="date" name="to_date" value="{{ form.to_date or '' }}" required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||||
|
<button type="submit">Pokaż kalendarz</button>
|
||||||
|
<span id="geoNote" class="muted small"></span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
||||||
|
|
||||||
|
{% if result %}
|
||||||
|
{% if result.data_error %}<div class="error">{{ result.data_error }}</div>{% endif %}
|
||||||
|
<div class="meta">
|
||||||
|
Silnik: <strong>{{ result.engine }}</strong> ·
|
||||||
|
zdarzeń: {{ result.count }} · zakres {{ result.from }} → {{ result.to }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for e in result.events %}
|
||||||
|
<div class="sig-item">
|
||||||
|
<div class="sig-head">
|
||||||
|
<span class="badge">{{ e.exact }}</span>
|
||||||
|
<span class="badge">{{ e.technique }}</span>
|
||||||
|
<strong>{{ e.significator }}</strong>
|
||||||
|
<span class="muted small">(okno {{ e.start }} → {{ e.end }})</span>
|
||||||
|
</div>
|
||||||
|
{% if e.interpretations %}
|
||||||
|
<table class="samples">
|
||||||
|
<tbody>
|
||||||
|
{% for s in e.interpretations %}
|
||||||
|
<tr><td class="sig nowrap" title="{{ s.significator }}">{{ s.expanded }}</td><td>{{ s.effect }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if e.interpretations_count > e.interpretations | length %}
|
||||||
|
<div class="muted small">…i {{ e.interpretations_count - (e.interpretations | length) }} więcej</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif e.technique != 'solar_return' %}
|
||||||
|
<div class="muted small">brak dopasowań w bazie</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script src="/static/now.js"></script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user