From 3195d9b003db720ef3be09d6539a2d0856c482c0 Mon Sep 17 00:00:00 2001 From: migatu Date: Sat, 27 Jun 2026 20:08:43 +0200 Subject: [PATCH] =?UTF-8?q?Warstwa=20prezentacji:=20widok=20horoskopu=20do?= =?UTF-8?q?=20r=C4=99cznego=20testowania?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strona główna "/" = formularz podstawowych danych momentu (data, godzina, strefa, opcjonalnie lokalizacja) → tabela policzonych pozycji w formie human-readable (znak, pozycja w znaku, absolutna, kierunek, prędkość). Woła logic /chart/positions; przelicza czas lokalny + offset na UTC. Przycisk "Tu i teraz" uzupełnia bieżącą datę/godzinę i strefę przeglądarki. Retrogradacja wyróżniona w tabeli. Wyszukiwarkę sygnifikatorów przeniesiono pod "/significants" -> /significators, dodano nawigację (base.html). Czytelny komunikat, gdy logika nie ma jeszcze endpointu silnika. Zweryfikowano end-to-end: formularz → przeliczenie UTC → render tabeli (przez stub kontraktu /chart/positions). Co-Authored-By: Claude Opus 4.8 --- services/presentation/README.md | 15 +++- .../presentation/app/clients/logic_client.py | 12 ++- services/presentation/app/main.py | 65 +++++++++++++-- services/presentation/app/static/styles.css | 17 ++++ services/presentation/app/templates/base.html | 24 ++++++ .../presentation/app/templates/chart.html | 79 +++++++++++++++++++ .../presentation/app/templates/index.html | 62 --------------- .../app/templates/significators.html | 54 +++++++++++++ 8 files changed, 257 insertions(+), 71 deletions(-) create mode 100644 services/presentation/app/templates/base.html create mode 100644 services/presentation/app/templates/chart.html delete mode 100644 services/presentation/app/templates/index.html create mode 100644 services/presentation/app/templates/significators.html 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 @@ + + + + + + astrololo · {% block title %}{% endblock %} + + + +
+
+

astrololo

+ +
+ {% block content %}{% endblock %} +
+ prezentacja → logika → dane · widok testowy +
+
+ + diff --git a/services/presentation/app/templates/chart.html b/services/presentation/app/templates/chart.html new file mode 100644 index 0000000..8084181 --- /dev/null +++ b/services/presentation/app/templates/chart.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% block title %}Horoskop{% endblock %} +{% block nav_chart %}active{% endblock %} + +{% block content %} +

Wpisz podstawowe dane momentu — program policzy pozycje obiektów (silnik efemeryd warstwy logicznej).

+ +
+
+ + + +
+
+ Lokalizacja (opcjonalnie — przyda się później do osi i domów) +
+ + +
+
+
+ + +
+
+ +{% if error %} +
{{ error }}
+{% endif %} + +{% if result %} +
+ Silnik: {{ result.engine }} · + obiektów: {{ result.positions | length }} + {% if moment %}· moment: {{ moment }}{% endif %} +
+ + + + + + + + {% for p in result.positions %} + + + + + + + + + {% endfor %} + +
ObiektZnakW znakuAbsolutnaKier.Prędkość °/d
{{ p.name }}{{ p.sign }}{{ p.in_sign }}{{ p.absolute }}{{ p.direction }}{{ '%+.4f' | format(p.speed) }}
+{% endif %} + + +{% endblock %} diff --git a/services/presentation/app/templates/index.html b/services/presentation/app/templates/index.html deleted file mode 100644 index aa40966..0000000 --- a/services/presentation/app/templates/index.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - astrololo - - - -
-

astrololo

-

Warstwa prezentacji → logiczna → bazodanowa

- -
-
- - - -
-
- - -
-
- - {% if error %} -
{{ error }}
- {% endif %} - - {% if result %} -
- Znaleziono {{ result.count }} · - provider: {{ result.meta.data_provider }} · - cache: {{ result.meta.data_cache }} · - {{ result.meta.data_elapsed_ms }} ms -
- {% if result.results %} - - - {% for col in result.results[0].keys() %}{% endfor %} - - - {% for row in result.results %} - {% for v in row.values() %}{% endfor %} - {% endfor %} - -
{{ col }}
{{ v }}
- {% else %} -

Brak wyników.

- {% endif %} - {% endif %} -
- - diff --git a/services/presentation/app/templates/significators.html b/services/presentation/app/templates/significators.html new file mode 100644 index 0000000..89c9e44 --- /dev/null +++ b/services/presentation/app/templates/significators.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% block title %}Sygnifikatory{% endblock %} +{% block nav_sig %}active{% endblock %} + +{% block content %} +

Wyszukiwanie w bazach interpretacji (warstwa danych). Wymaga wgranych baz.

+ +
+
+ + + +
+
+ + +
+
+ +{% if error %} +
{{ error }}
+{% endif %} + +{% if result %} +
+ Znaleziono {{ result.count }} · + provider: {{ result.meta.data_provider }} · + cache: {{ result.meta.data_cache }} · + {{ result.meta.data_elapsed_ms }} ms +
+ {% if result.results %} + + + {% for col in result.results[0].keys() %}{% endfor %} + + + {% for row in result.results %} + {% for v in row.values() %}{% endfor %} + {% endfor %} + +
{{ col }}
{{ v }}
+ {% else %} +

Brak wyników.

+ {% endif %} +{% endif %} +{% endblock %}