feat: przegląd baz na udziale + globalne włączanie/wyłączanie (DAN-15/PRE-09)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m30s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 12s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 40s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m3s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m35s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 14s
Testy / Kontrola składni wszystkich warstw (push) Successful in 10s

Wymaganie przedefiniowane pod model serwerowy (#47): nie wybiera się folderu —
pliki leżą na stałym NFS. Potrzeba za to WIDZIEĆ, jakie bazy są dostępne i móc
zdecydować, które biorą udział w interpretacji.

Warstwa danych: `bases.py` (lista plików + metaopis: nazwa, ścieżka, rozmiar,
data, stan) i endpoint `/bases`. Wyłączone bazy są ODSIEWANE z kandydatów przy
wyszukiwaniu, więc naprawdę nie biorą udziału w interpretacji — nie tylko znikają
z listy. Lista wyłączonych wchodzi do klucza cache zapytań: bez tego zmiana
ustawień oddawałaby wynik sprzed zmiany, czyli treść bazy uznanej za wyłączoną.
`list_bases()` doszło do interfejsu dostawcy jako OPCJONALNE (SQL nie operuje na
plikach → pusto, zamiast wywrotki).

Przelot logika → prezentacja i ekran „Ustawienia" z tabelą baz. Przez łącze idą
SAME METADANE — podgląd listy nie jest kolejną drogą do wyniesienia treści.

Stan przełączników jest DEKLARATYWNY (`DISABLED_BASES`), nie klikalny — i to jest
świadome: udział z bazami montujemy read-only, a katalog cache to `emptyDir`, więc
zapisany przełącznik ginąłby przy restarcie poda i po cichu włączał z powrotem
wyłączoną bazę. Ekran mówi wprost, jak wyłączyć bazę i dlaczego nie klikaniem.
Tryb klikalny wymagałby dołożenia trwałego wolumenu.

Weryfikacja na żywym łańcuchu: `/bases` przechodzi przez SZYFROWANE łącze
(logic→data), pokazuje 3 bazy z metaopisem i stanem; wyszukiwanie daje 3 → 2 → 0
wierszy w miarę wyłączania baz. Testy: dane +6, prezentacja +6. Dane 13,
logika 277, prezentacja 249.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #55.
This commit is contained in:
2026-08-03 22:09:27 +02:00
parent d3d9b365fe
commit 4c1e7f8808
12 changed files with 288 additions and 1 deletions
@@ -152,6 +152,12 @@ class LogicClient:
client, f"{self.base_url}/chart/horoscope/stream",
payload=payload, headers=_auth_headers(), link=_link())
def bases(self) -> dict[str, Any]:
"""Bazy dostępne na udziale + metaopis i stan włączenia (DAN-15/PRE-09)."""
with httpx.Client(timeout=settings.http_timeout) as client:
return link_crypto.call_json(client, "GET", f"{self.base_url}/bases",
headers=_auth_headers(), link=_link())
def llm_models(self) -> dict[str, Any]:
"""Katalog modeli per dostawca (podpowiedzi do pola wyboru w UI)."""
with httpx.Client(timeout=settings.http_timeout) as client:
+17
View File
@@ -289,6 +289,23 @@ def compile_pdf(payload: dict):
headers={"Content-Disposition": 'attachment; filename="raport.pdf"'})
# ---------------- Ustawienia: bazy na udziale (DAN-15/PRE-09) ----------------
@app.get("/settings", response_class=HTMLResponse)
def settings_view(request: Request):
"""Podgląd baz dostępnych na udziale + które biorą udział w interpretacji.
Same metadane (nazwa, rozmiar, data) — bez treści baz. Przełączniki są
DEKLARATYWNE (`DISABLED_BASES`), bo warstwa danych nie ma trwałego zapisu:
udział jest read-only, a cache to emptyDir. Zapis do pliku ginąłby po
restarcie, po cichu włączając z powrotem wyłączoną bazę."""
ctx: dict = {"result": None, "error": None}
try:
ctx["result"] = logic.bases()
except httpx.HTTPError as e:
ctx["error"] = _logic_error(e)
return templates.TemplateResponse(request, "settings.html", ctx)
# ---------------- Synastria (technika relacyjna) ----------------
@app.get("/synastry", response_class=HTMLResponse)
def synastry_form(request: Request):
@@ -25,6 +25,7 @@
<a href="/synastry" class="{% block nav_synastry %}{% endblock %}">Synastria</a>
<a href="/significators" class="{% block nav_sig %}{% endblock %}">Sygnifikatory</a>
<a href="/compile" class="{% block nav_compile %}{% endblock %}">Skompiluj</a>
<a href="/settings" class="{% block nav_settings %}{% endblock %}">Ustawienia</a>
</nav>
</header>
{% block content %}{% endblock %}
@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}Ustawienia{% endblock %}
{% block nav_settings %}active{% endblock %}
{% block content %}
<p class="sub">Bazy interpretacyjne dostępne na udziale sieciowym — i to, które biorą udział w interpretacji.</p>
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% if result %}
<div class="meta">Bazy na udziale: <strong>{{ result.total }}</strong> ·
biorą udział w interpretacji: <strong>{{ result.enabled }}</strong>
{% if result.total and result.enabled < result.total %}
· wyłączonych: <strong>{{ result.total - result.enabled }}</strong>
{% endif %}
</div>
{% if result.bases %}
<table>
<thead><tr><th>Baza</th><th>Ścieżka</th><th>Rozmiar</th><th>Zmieniono</th><th>W interpretacji</th></tr></thead>
<tbody>
{% for b in result.bases %}
<tr>
<td>{{ b.name }}</td>
<td class="muted small">{{ b.path }}</td>
<td class="mono">{% if b.size_mb is not none %}{{ b.size_mb }} MB{% else %}—{% endif %}</td>
<td class="mono">{{ b.modified or '—' }}</td>
<td>{% if b.enabled %}tak{% else %}<span class="badge">wyłączona</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="muted">Na udziale nie widać żadnych baz (albo aktywny dostawca nie operuje na plikach).</p>
{% endif %}
<div class="meta">Jak wyłączyć bazę</div>
<p class="muted small">
Lista wyłączonych jest <strong>deklaratywna</strong> — trzyma ją zmienna
<span class="mono">DISABLED_BASES</span> usługi danych (nazwy plików po przecinku,
np. <span class="mono">stara_baza.xlsx,archiwum/testowa.xlsx</span>). Po zmianie
zrestartuj warstwę danych.
</p>
<p class="muted small">
Dlaczego nie klikaniem: udział z bazami jest montowany tylko do odczytu, a katalog
cache to <span class="mono">emptyDir</span> — zapisany przełącznik ginąłby przy
restarcie poda i po cichu włączał z powrotem wyłączoną bazę. Przełączanie z tego
ekranu wymagałoby dołożenia trwałego wolumenu.
</p>
{% endif %}
{% endblock %}
@@ -0,0 +1,36 @@
"""Ekran ustawień: bazy na udziale (PRE-09)."""
import pathlib
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
TPL = (APP / "templates" / "settings.html").read_text(encoding="utf-8")
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
MAIN = (APP / "main.py").read_text(encoding="utf-8")
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
def test_tab_is_in_the_menu():
assert 'href="/settings"' in BASE and "Ustawienia" in BASE
def test_route_and_client_exist():
assert '@app.get("/settings"' in MAIN and "logic.bases()" in MAIN
assert 'f"{self.base_url}/bases"' in CLIENT
def test_listing_shows_name_and_metadata():
for col in ("b.name", "b.path", "b.size_mb", "b.modified"):
assert col in TPL, f"brak kolumny {col}"
def test_listing_shows_whether_base_counts_in_interpretation():
assert "b.enabled" in TPL and "wyłączona" in TPL
def test_page_explains_how_to_disable_and_why_not_clickable():
"""Ekran ma mówić, JAK wyłączyć bazę — inaczej podgląd bez sprawczości myli."""
assert "DISABLED_BASES" in TPL
assert "emptyDir" in TPL # uzasadnienie: brak trwałego zapisu
def test_error_from_logic_does_not_break_the_page():
assert "_logic_error(e)" in MAIN[MAIN.index("def settings_view"):MAIN.index("def settings_view") + 900]