astroklient: warstwa pośrednia — pełne astro, bez generowania i administracji (4/5)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m19s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Failing after 0s
Testy / Testy astroklient (pull_request) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 7s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 19s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m25s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m26s
Testy / Testy astrodemo (push) Failing after 0s
Testy / Testy astroklient (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m19s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Failing after 0s
Testy / Testy astroklient (pull_request) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 7s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 19s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m25s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m26s
Testy / Testy astrodemo (push) Failing after 0s
Testy / Testy astroklient (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Trzeci produkt drabiny: astrodemo (dwie funkcje) → astroklient → astrololo. Po trzech poprzednich krokach jest cienki, bo jest ZŁOŻENIEM, a nie kopią: własne main.py z ośmioma importami, a rdzeń — ekrany, szablony, zasoby — bierze z warstwy prezentacji przy budowaniu obrazu. Jedno źródło, dwa produkty; inaczej te same 2500 linii szablonów żyłyby w dwóch egzemplarzach i rozjechały się w ciągu tygodni, po cichu. CO MA: Horoskop, Interpretacje, Kalendarz, Synastria, Sygnifikatory, wgrywanie plików. Wyszukiwarka miejsca i strefa czasowa zgodnie z ustaleniem. CZEGO NIE MA I DLACZEGO NIE DA SIĘ WŁĄCZYĆ: plików usuniętych wg usun.txt nie ma w obrazie. Nie istnieje uprawnienie, którym dałoby się je odsłonić, bo katalog funkcji składa się ze ZGŁOSZEŃ ekranów obecnych w obrazie. To dlatego „każde konto dostaje wszystko, co ta usługa umie" jest tu bezpieczne i nie wymaga wypisywania listy: zbiór liczy się z katalogu, więc opisuje ten produkt. KONTA jak w astrodemo: z konfiguracji środowiska (ASTROKLIENT_USERS), jeden poziom dostępu, bez pliku kont i bez ekranu ich zakładania. Konta rozdziela się po to, żeby każde miało własną pulę plików. PULE PER KONTO — tu była realna dziura. Warstwa logiczna przenosiła pulę tylko przy raporcie i operacjach na plikach, więc Kalendarz i Sygnifikatory czytałyby CAŁY udział: jedno konto widziałoby pliki drugiego, mimo obietnicy izolacji. Domknięte: TimelineRequest i QueryRequest niosą teraz pulę, a QueryService buduje klienta danych na żądanie. Pula jedzie w każdym żądaniu w dół i bierze się z kontekstu ustawianego przy wejściu, nigdy z formularza. Test podstawia `tenant=ktos-inny` w POST i sprawdza, że w dół poszedł login zalogowanego. WARSTWA WSPÓLNA ROZDZIELONA OD POJĘCIA ADMINISTRATORA. base.html miał wpisany na sztywno warunek `can(request, 'admin')` i odsyłacz do ekranu kont — czyli w produkcie bez tego ekranu zostawał martwy link i nazwa czegoś, czego nie ma. Rejestr niesie teraz wymagane uprawnienie, a szablon dostaje gotową listę. Podstawa przestała też importować moduły służące jednemu ekranowi (konta, stany plików), bo produkt bez tego ekranu wlókł ich zależności. ZAPORA SŁOWNIKOWA NAD REALNYM DRZEWEM. Test buduje złożenie tak samo jak Dockerfile i szuka słów o funkcjach, których nie ma — w odpowiedziach ORAZ w plikach. Pierwsza wersja znalazła dziesięć trafień, w tym trzy moje własne docstringi WYLICZAJĄCE nieobecne funkcje: zdanie „nie ma tu generowania tekstu" mówi wprost, że coś takiego istnieje, więc jest takim samym śladem jak przycisk. Po poprawkach: zero. Test ma kontrolę negatywną — podrzucony plik ma go wywrócić. usun.txt jest DANYMI, nie tekstem w Dockerfile: czyta go też test pilnujący, żeby zgadzał się ze złożeniem w main.py. Rozjazd znaczyłby albo martwy kod w obrazie, albo błąd dopiero przy uruchomieniu. CI: astroklient buduje się z KORZENIA repozytorium (jego Dockerfile sięga po rdzeń), a zmiana w warstwie prezentacji też go przebudowuje — bez tego jego obraz zostawałby ze starymi ekranami, a różnicy nie byłoby widać do zgłoszenia użytkownika. Testy: astroklient 12, presentation 368, astrodemo 28, logic 342, data 42, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit was merged in pull request #83.
This commit is contained in:
@@ -4,8 +4,9 @@ from __future__ import annotations
|
||||
from fastapi import Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from app import accounts as accounts_store
|
||||
from app.features import Feature
|
||||
from app.podstawa import (app, templates, security, features, accounts_store)
|
||||
from app.podstawa import (app, templates, security, features)
|
||||
|
||||
# ---------------- Konta i uprawnienia (PRE-27) ----------------
|
||||
# Ochrona tych tras siedzi w features.ROUTES, nie w dekoratorze — jedna mapa
|
||||
@@ -94,7 +95,7 @@ def accounts_delete(request: Request, login: str = Form(...)):
|
||||
# Ekran wyłącznie administracyjny — nie ma pozycji w katalogu funkcji, bo ADMIN
|
||||
# nie jest uprawnieniem do nadania (patrz features.GRANTABLE).
|
||||
features.zarejestruj(
|
||||
nawigacja_admina=("accounts", "/accounts", "Konta"),
|
||||
nawigacja=("accounts", "/accounts", "Konta", features.ADMIN),
|
||||
trasy={("GET", "/accounts"): features.ADMIN,
|
||||
("POST", "/accounts/create"): features.ADMIN,
|
||||
("POST", "/accounts/update"): features.ADMIN,
|
||||
|
||||
@@ -6,8 +6,14 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
import base64
|
||||
import httpx
|
||||
|
||||
from app import files_state
|
||||
from app.features import Feature
|
||||
from app.podstawa import (app, templates, logic, features, files_state, _perms, _logic_error)
|
||||
from app.podstawa import (app, templates, logic, features, _perms, _logic_error)
|
||||
|
||||
# Etykiety stanów pliku dla szablonu. Rejestrowane TUTAJ, a nie w podstawie:
|
||||
# należą do tego ekranu, a produkt, który go nie ma, nie ma po co ich znać.
|
||||
templates.env.globals["STATUS_LABELS"] = files_state.LABELS
|
||||
|
||||
|
||||
# ---------------- Pliki baz (DAN-27) ----------------
|
||||
# Trzy poziomy dostępu, opisane w features.ROUTES:
|
||||
|
||||
@@ -45,10 +45,12 @@ class Feature:
|
||||
# `kolejnosc` jest jawna, bo o zawartości decyduje lista importów w `main.py`,
|
||||
# a kolejność importów nie może rządzić kolejnością zakładek w nawigacji.
|
||||
SCREENS: list[Feature] = []
|
||||
# Pozycje nawigacji dla administratora. Osobno od SCREENS, bo ADMIN nie jest
|
||||
# uprawnieniem do nadania — a wpisane na sztywno w szablonie zostawiały nazwę
|
||||
# ekranu (i martwy odsyłacz) w produkcie, który tego ekranu nie ma.
|
||||
NAWIGACJA_ADMINA: list[tuple[str, str, str]] = [] # (klucz, adres, etykieta)
|
||||
# Pozycje nawigacji dla uprawnień NIENADAWALNYCH — tych, których nie ma w
|
||||
# GRANTABLE, więc nie da się ich przyznać z żadnego ekranu. Osobno od SCREENS
|
||||
# i wraz z wymaganym uprawnieniem, żeby szablon nie musiał go wypowiadać: wpisane
|
||||
# w szablonie na sztywno zostawiało nazwę ekranu i martwy odsyłacz w produkcie,
|
||||
# który tego ekranu nie ma.
|
||||
NAWIGACJA_SPECJALNA: list[tuple[str, str, str, str]] = [] # (klucz, adres, etykieta, uprawnienie)
|
||||
EXTRAS: list[Feature] = []
|
||||
ALL: tuple[Feature, ...] = ()
|
||||
BY_KEY: dict[str, Feature] = {}
|
||||
@@ -70,7 +72,7 @@ def _przelicz() -> None:
|
||||
def zarejestruj(*, ekran: Feature | None = None, rozszerzenie: Feature | None = None,
|
||||
trasy: dict[tuple[str, str], str | None] | None = None,
|
||||
zasoby: dict | None = None,
|
||||
nawigacja_admina: tuple[str, str, str] | None = None) -> None:
|
||||
nawigacja: tuple[str, str, str, str] | None = None) -> None:
|
||||
"""Zgłoszenie funkcji, jej tras i jej zasobów.
|
||||
|
||||
Idempotentne po kluczu: `export` zgłaszają dwa ekrany (raport do PDF-a
|
||||
@@ -78,8 +80,8 @@ def zarejestruj(*, ekran: Feature | None = None, rozszerzenie: Feature | None =
|
||||
for pozycja, zbior in ((ekran, SCREENS), (rozszerzenie, EXTRAS)):
|
||||
if pozycja and pozycja.key not in {f.key for f in zbior}:
|
||||
zbior.append(pozycja)
|
||||
if nawigacja_admina and nawigacja_admina[0] not in {p[0] for p in NAWIGACJA_ADMINA}:
|
||||
NAWIGACJA_ADMINA.append(nawigacja_admina)
|
||||
if nawigacja and nawigacja[0] not in {p[0] for p in NAWIGACJA_SPECJALNA}:
|
||||
NAWIGACJA_SPECJALNA.append(nawigacja)
|
||||
ROUTES.update(trasy or {})
|
||||
STATIC.update(zasoby or {})
|
||||
_przelicz()
|
||||
|
||||
@@ -24,12 +24,10 @@ from fastapi.responses import Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
|
||||
from app import accounts as accounts_store
|
||||
from app import rozszerzenia
|
||||
from app import chartwheel as chartwheel_mod
|
||||
from app import features
|
||||
from app import session
|
||||
from app import files_state
|
||||
from app.house_systems import HOUSE_SYSTEMS, LIMITED as HOUSE_LIMITED, label as house_label
|
||||
|
||||
from app import geocode, security
|
||||
@@ -145,13 +143,16 @@ def static_file(nazwa: str):
|
||||
|
||||
|
||||
templates.env.globals["static"] = static_url
|
||||
templates.env.globals["nav_admin"] = lambda: features.NAWIGACJA_ADMINA
|
||||
# Pozycje specjalne przefiltrowane uprawnieniem — szablon dostaje gotową listę
|
||||
# i nie musi wiedzieć, czego która wymaga.
|
||||
templates.env.globals["nav_extra"] = lambda request: [
|
||||
(k, a, e) for k, a, e, u in features.NAWIGACJA_SPECJALNA
|
||||
if features.can(_perms(request), u)]
|
||||
# Katalog systemów domów — jedno źródło nazw dla wszystkich szablonów.
|
||||
templates.env.globals["HOUSE_SYSTEMS"] = HOUSE_SYSTEMS
|
||||
templates.env.globals["house_label"] = house_label
|
||||
templates.env.globals["HOUSE_LIMITED"] = HOUSE_LIMITED
|
||||
templates.env.globals["WHEEL_ORIENTATIONS"] = chartwheel_mod.ORIENTATIONS
|
||||
templates.env.globals["STATUS_LABELS"] = files_state.LABELS
|
||||
|
||||
|
||||
def _perms(request: Request) -> frozenset[str]:
|
||||
@@ -169,6 +170,10 @@ templates.env.globals["nav_screens"] = lambda request: features.screens_for(_per
|
||||
templates.env.globals["current_user"] = lambda request: getattr(request.state, "user", "") or ""
|
||||
templates.env.globals["auth_on"] = security.auth_enabled
|
||||
templates.env.globals["can"] = lambda request, key: key in _perms(request)
|
||||
# Czy to konto ma uprawnienie NIENADAWALNE — czyli takie, którego nie da się
|
||||
# przyznać z żadnego ekranu. Nazwa neutralna, bo szablon jest wspólny.
|
||||
templates.env.globals["widok_diagnostyczny"] = lambda request: features.can(
|
||||
_perms(request), features.ADMIN)
|
||||
|
||||
|
||||
def _limit_options(request: Request, opts: dict) -> dict:
|
||||
|
||||
@@ -175,7 +175,7 @@ def refresh(dane: dict, *, now: float | None = None) -> str | None:
|
||||
|
||||
|
||||
def cookie_params(secure: bool = True) -> dict:
|
||||
"""Ustawienia ciasteczka wspólne dla ustawiania i kasowania.
|
||||
"""Parametry ciasteczka wspólne dla ustawiania i kasowania.
|
||||
|
||||
httponly — JavaScript nie ma po co go czytać, a bez tego jeden wstrzyknięty
|
||||
skrypt wynosi sesję. samesite=strict — odcina większość ataków polegających
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{# Opcje obliczeń — WSPÓLNE dla „Horoskop" (/) i „Skompiluj" (/compile). Jeden plik,
|
||||
{# Opcje obliczeń — WSPÓLNE dla „Horoskop" (/) i raportu zbiorczego (/compile). Jeden plik,
|
||||
bo dodawaliśmy je już 3× w dwóch formularzach i groziło rozjechaniem (regresja
|
||||
podsumowania). Wszystkie synchronizowane między zakładkami przez formsync.js. #}
|
||||
{% if can(request, 'advanced_calc') %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{# Tabele wyniku horoskopu — używane przez „Skompiluj" (/compile), żeby
|
||||
{# Tabele wyniku horoskopu — używane przez raportu zbiorczego (/compile), żeby
|
||||
podsumowanie pokazywało dokładnie to, co policzono (regresja 2026-07-28:
|
||||
podsumowanie miało braki). Skopiowane 1:1 z sekcji tabel na „Horoskop" (/).
|
||||
TODO: przełączyć też chart.html na ten include, żeby oba widoki nie mogły się
|
||||
|
||||
@@ -28,14 +28,12 @@
|
||||
{% for tab in nav_screens(request) %}
|
||||
<a href="{{ tab.href }}" class="{{ 'active' if nav_active is defined and nav_active == tab.key else '' }}">{{ tab.label }}</a>
|
||||
{% endfor %}
|
||||
{# Pozycje administracyjne z REJESTRU, nie z listy w szablonie: wpisany tu
|
||||
na sztywno odsyłacz zostawał w produkcie, który tego ekranu nie ma —
|
||||
{# Pozycje specjalne z REJESTRU, już przefiltrowane uprawnieniem. Wpisany
|
||||
tu na sztywno odsyłacz zostawał w produkcie, który tego ekranu nie ma —
|
||||
razem z nazwą ekranu i martwym adresem. #}
|
||||
{% if can(request, 'admin') %}
|
||||
{% for klucz, adres, etykieta in nav_admin() %}
|
||||
{% for klucz, adres, etykieta in nav_extra(request) %}
|
||||
<a href="{{ adres }}" class="{{ 'active' if nav_active is defined and nav_active == klucz else '' }}">{{ etykieta }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{# Wyjście na końcu paska i odsunięte: to nie jest zakładka, tylko
|
||||
czynność — pomylenie go z zakładką kosztuje przypadkowe wylogowania.
|
||||
Pokazujemy też KTO jest zalogowany, bo bez tego przycisk wyjścia jest
|
||||
@@ -52,11 +50,11 @@
|
||||
</nav>
|
||||
</header>
|
||||
{% block content %}{% endblock %}
|
||||
{# Stopka wymieniała warstwy architektury i nazywała całość „widokiem
|
||||
testowym" — czyli mówiła, że to nie jest gotowy produkt i że za nim stoi
|
||||
łańcuch usług. Dla konta ograniczonego program ma wyglądać na kompletny,
|
||||
więc ten opis widzi wyłącznie administrator. #}
|
||||
{% if can(request, 'admin') %}
|
||||
{# Stopka wymienia warstwy architektury i nazywa całość „widokiem testowym" —
|
||||
czyli mówi, że to nie jest gotowy produkt i że za nim stoi łańcuch usług.
|
||||
Widzi ją wyłącznie konto z uprawnieniem nienadawalnym; dla pozostałych
|
||||
program ma wyglądać na kompletny. #}
|
||||
{% if widok_diagnostyczny(request) %}
|
||||
<footer class="foot">
|
||||
prezentacja → logika → dane · widok testowy
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user