diff --git a/services/presentation/README.md b/services/presentation/README.md index 7f64cfc..2d4a3f4 100644 --- a/services/presentation/README.md +++ b/services/presentation/README.md @@ -5,12 +5,21 @@ przekazuje dane z formularza do warstwy logicznej i renderuje opracowane wyniki. Brak logiki biznesowej i dostępu do danych. ## Trasy -- `GET /` — strona z formularzem -- `POST /` — wysłanie formularza → warstwa logiczna → render wyników +- `GET /` — **Horoskop**: formularz danych momentu (data, godzina, strefa, opcjonalnie lokalizacja) +- `POST /` — liczy pozycje obiektów (woła logic `/chart/positions`) i renderuje czytelną tabelę +- `GET /significators`, `POST /significators` — wyszukiwarka w bazach interpretacji (warstwa danych) - `GET /health` +Strona „Horoskop" to widok do **ręcznego testowania**: podstawowe dane wejściowe → +to, co policzyła warstwa logiczna (znak, pozycja w znaku, absolutna, kierunek, prędkość). +Przycisk „Tu i teraz" uzupełnia bieżącą datę/godzinę i strefę przeglądarki. + ## Zależności w dół -Zna wyłącznie `LOGIC_URL` (adres warstwy logicznej). +Zna wyłącznie `LOGIC_URL` (adres warstwy logicznej) i jej kontrakty `/chart/positions` +oraz `/api/query`. Nie wie nic o silniku ani bazach. + +> Uwaga: pełne liczenie pozycji wymaga warstwy logicznej z silnikiem efemeryd +> (gałąź `feat/logic-engine`). Bez niego strona „Horoskop" pokaże czytelny komunikat. ## Uruchomienie ```bash diff --git a/services/presentation/app/clients/logic_client.py b/services/presentation/app/clients/logic_client.py index 665f055..0071e4a 100644 --- a/services/presentation/app/clients/logic_client.py +++ b/services/presentation/app/clients/logic_client.py @@ -1,7 +1,7 @@ """Klient HTTP do warstwy logicznej. Jedyny punkt styku prezentacji w dół. Przekazuje dane z formularza i odbiera -opracowane wyniki. Prezentacja nie sięga bezpośrednio do bazy. +opracowane wyniki. Prezentacja nie sięga bezpośrednio do bazy ani do silnika. """ from __future__ import annotations @@ -22,3 +22,13 @@ class LogicClient: r = client.post(f"{self.base_url}/api/query", json=payload) r.raise_for_status() return r.json() + + def positions( + self, when_utc_iso: str, lat: float, lon: float, objects: list[str] | None = None + ) -> dict[str, Any]: + """Pozycje obiektów dla danego momentu — woła logic /chart/positions.""" + payload = {"when_utc": when_utc_iso, "lat": lat, "lon": lon, "objects": objects} + with httpx.Client(timeout=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 edc405b..e7948ea 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -2,9 +2,15 @@ W dół: przekazuje dane z formularza do warstwy logicznej i odbiera opracowane wyniki. Nie zawiera logiki biznesowej ani dostępu do danych — tylko UI. + +Strona główna „/" = wprowadzenie danych horoskopu i podgląd policzonych pozycji +(do ręcznego testowania aplikacji). Wyszukiwarka sygnifikatorów przeniesiona pod +„/significators". """ from __future__ import annotations +from datetime import datetime, timedelta, timezone + import httpx from fastapi import FastAPI, Form, Request from fastapi.responses import HTMLResponse @@ -19,13 +25,62 @@ templates = Jinja2Templates(directory="app/templates") logic = LogicClient() +def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]: + """Z lokalnej daty/godziny + przesunięcia strefy → moment UTC. + + Zwraca (iso_utc, etykieta_czytelna). UTC = czas lokalny − offset. + """ + local = datetime.fromisoformat(f"{date}T{time}") + utc = (local - timedelta(hours=tz_offset)).replace(tzinfo=timezone.utc) + label = utc.strftime("%Y-%m-%d %H:%M UTC") + return utc.isoformat(), label + + +def _logic_error(e: Exception) -> str: + if isinstance(e, httpx.HTTPStatusError) and e.response.status_code == 404: + return ( + "Warstwa logiczna działa, ale nie ma endpointu /chart/positions. " + "Uruchom warstwę logiczną z silnikiem efemeryd (gałąź feat/logic-engine)." + ) + return f"Warstwa logiczna niedostępna: {e}" + + +# ---------------- Horoskop: pozycje (strona główna) ---------------- @app.get("/", response_class=HTMLResponse) -def index(request: Request): - return templates.TemplateResponse(request, "index.html", {"result": None, "form": {}}) +def chart_form(request: Request): + return templates.TemplateResponse(request, "chart.html", {"result": None, "form": {}}) @app.post("/", response_class=HTMLResponse) -def search( +def chart_compute( + request: Request, + date: str = Form(...), + time: str = Form(...), + tz_offset: float = Form(0.0), + lat: float = Form(0.0), + lon: float = Form(0.0), +): + form = {"date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon} + 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) + 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, "chart.html", ctx) + + +# ---------------- Sygnifikatory (wyszukiwarka w bazach) ---------------- +@app.get("/significators", response_class=HTMLResponse) +def significators_form(request: Request): + return templates.TemplateResponse(request, "significators.html", {"result": None, "form": {}}) + + +@app.post("/significators", response_class=HTMLResponse) +def significators_search( request: Request, query: str = Form(...), field: str = Form("name"), @@ -37,8 +92,8 @@ def search( try: ctx["result"] = logic.query(query=query, field=field, exact=exact, limit=limit) except httpx.HTTPError as e: - ctx["error"] = f"Warstwa logiczna niedostępna: {e}" - return templates.TemplateResponse(request, "index.html", ctx) + ctx["error"] = _logic_error(e) + return templates.TemplateResponse(request, "significators.html", ctx) @app.get("/health") diff --git a/services/presentation/app/static/styles.css b/services/presentation/app/static/styles.css index 2b67cfb..a632745 100644 --- a/services/presentation/app/static/styles.css +++ b/services/presentation/app/static/styles.css @@ -15,6 +15,23 @@ body { main { width: 100%; max-width: 880px; } h1 { margin: 0; font-size: 2rem; letter-spacing: .5px; } .sub { color: var(--muted); margin: .25rem 0 2rem; } + +.topbar { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: .25rem; } +.topbar nav { display: flex; gap: .25rem; } +.topbar nav a { color: var(--muted); text-decoration: none; padding: .4rem .8rem; border-radius: 8px; border: 1px solid transparent; } +.topbar nav a:hover { color: var(--ink); } +.topbar nav a.active { color: #fff; background: var(--panel); border-color: var(--line); } +.foot { color: var(--muted); font-size: .8rem; margin-top: 2.5rem; opacity: .7; } + +.grid { display: flex; gap: .6rem; flex-wrap: wrap; } +.grid label { display: flex; flex-direction: column; gap: .25rem; color: var(--muted); font-size: .85rem; flex: 1; min-width: 9rem; } +.grid input { width: 100%; } +.loc { margin-top: .8rem; } +.loc summary { color: var(--muted); cursor: pointer; font-size: .85rem; margin-bottom: .6rem; } +.actions { display: flex; gap: .5rem; margin-top: 1rem; } +button.ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); } +.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; } +td.retro { color: #ff9b6a; font-weight: 600; } form { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; } .row { display: flex; gap: .5rem; } .row input[type=text] { flex: 1; } diff --git a/services/presentation/app/templates/base.html b/services/presentation/app/templates/base.html new file mode 100644 index 0000000..7142264 --- /dev/null +++ b/services/presentation/app/templates/base.html @@ -0,0 +1,24 @@ + + +
+ + +Wpisz podstawowe dane momentu — program policzy pozycje obiektów (silnik efemeryd warstwy logicznej).
+ + + +{% if error %} +| Obiekt | Znak | W znaku | Absolutna | Kier. | Prędkość °/d | +
|---|---|---|---|---|---|
| {{ p.name }} | +{{ p.sign }} | +{{ p.in_sign }} | +{{ p.absolute }} | +{{ p.direction }} | +{{ '%+.4f' | format(p.speed) }} | +
Warstwa prezentacji → logiczna → bazodanowa
- - - - {% if error %} -| {{ col }} | {% endfor %}
|---|
| {{ v }} | {% endfor %}
Brak wyników.
- {% endif %} - {% endif %} -Wyszukiwanie w bazach interpretacji (warstwa danych). Wymaga wgranych baz.
+ + + +{% if error %} +| {{ col }} | {% endfor %}
|---|
| {{ v }} | {% endfor %}
Brak wyników.
+ {% endif %} +{% endif %} +{% endblock %}