diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml
index 2d0f319..8d9d9b0 100644
--- a/.gitea/workflows/build.yaml
+++ b/.gitea/workflows/build.yaml
@@ -9,12 +9,12 @@ jobs:
- uses: actions/checkout@v4
- name: Login
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.czernobog.pl -u gitea --password-stdin
- # astroklient-demo dołącza do tej samej pętli: dzieli warstwę logiczną i łącze,
+ # astrodemo dołącza do tej samej pętli: dzieli warstwę logiczną i łącze,
# więc jego obraz ma powstawać z tego samego commita co reszta produktu.
- - name: Build & push (data, logic, presentation, astroklient-demo)
+ - name: Build & push (data, logic, presentation, astrodemo)
run: |
TAG=${GITHUB_SHA::8}
- for SVC in data logic presentation astroklient-demo; do
+ for SVC in data logic presentation astrodemo; do
docker build -t gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG ./services/$SVC
docker push gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG
done
diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml
index 2021e5a..c102699 100644
--- a/.gitea/workflows/tests.yml
+++ b/.gitea/workflows/tests.yml
@@ -91,8 +91,8 @@ jobs:
PYTHONPATH: .
run: pytest tests -q -rs
- astroklient-demo-tests:
- name: Testy astroklienta-demo
+ astrodemo-tests:
+ name: Testy astrodemo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -100,14 +100,14 @@ jobs:
with:
python-version: "3.12"
cache: pip
- cache-dependency-path: services/astroklient-demo/requirements-dev.txt
+ cache-dependency-path: services/astrodemo/requirements-dev.txt
- name: Instalacja zależności
- run: pip install -r services/astroklient-demo/requirements-dev.txt
+ run: pip install -r services/astrodemo/requirements-dev.txt
# Demo rozmawia z warstwą danych PRODUKCJI, więc jego powierzchnia musi być
# pilnowana tak samo jak reszty: testy sprawdzają m.in., że nie przybyła
# żadna trasa poza dwiema funkcjami.
- name: Testy (pytest)
- working-directory: services/astroklient-demo
+ working-directory: services/astrodemo
env:
PYTHONPATH: .
run: pytest tests -q -rs
diff --git a/services/astroklient-demo/Dockerfile b/services/astrodemo/Dockerfile
similarity index 100%
rename from services/astroklient-demo/Dockerfile
rename to services/astrodemo/Dockerfile
diff --git a/services/astroklient-demo/README.md b/services/astrodemo/README.md
similarity index 84%
rename from services/astroklient-demo/README.md
rename to services/astrodemo/README.md
index 9d381af..4a0999e 100644
--- a/services/astroklient-demo/README.md
+++ b/services/astrodemo/README.md
@@ -1,4 +1,4 @@
-# astroklient-demo — wersja demonstracyjna (PRE-28)
+# astrodemo — wersja demonstracyjna (PRE-28)
Osobna warstwa prezentacji o **dwóch funkcjach**:
@@ -18,7 +18,7 @@ okazji kodu reszty programu.
## Izolacja: własne dane, pula na konto
```
-astroklient-demo → logic-demo → data-demo → udział astrololo-demo (pusty na starcie)
+astrodemo → logic-demo → data-demo → udział astrololo-demo (pusty na starcie)
└── klientA/ ← pula konta
└── klientB/ ← pula konta
```
@@ -42,15 +42,15 @@ w nazwie katalogu: `../..` czy ukośnik wyprowadziłyby zapytanie poza udział.
**Pula klienta to jeden katalog**, więc przejście na pełną wersję nie oznacza
utraty wgrań — procedura importu jest w runbooku wdrożenia (repo `deploy`,
-`astrololo/README-astroklient-demo.md`).
+`astrololo/README-astrodemo.md`).
## Konfiguracja
| zmienna | znaczenie |
|---|---|
| `LOGIC_URL` | adres warstwy logicznej (domyślnie `http://localhost:8001`) |
-| `DEMO_USERS` | konta jako `login:sekret`, po przecinku. Sekret jawny albo hash `scrypt$sól$hash`. Każdy login = osobna pula plików |
-| `DEMO_USER` / `DEMO_PASSWORD` | pojedyncze konto — zgodność wstecz, działa gdy `DEMO_USERS` jest puste. **Brak obu = aplikacja stoi otwarta** i mówi o tym głośno przy starcie |
+| `ASTRODEMO_USERS` | konta jako `login:sekret`, po przecinku. Sekret jawny albo hash `scrypt$sól$hash`. Każdy login = osobna pula plików |
+| `ASTRODEMO_USER` / `ASTRODEMO_PASSWORD` | pojedyncze konto — zgodność wstecz, działa gdy `ASTRODEMO_USERS` jest puste. **Brak obu = aplikacja stoi otwarta** i mówi o tym głośno przy starcie |
| `INTERNAL_TOKEN` | token międzywarstwowy (LOG-32) — ten sam co reszta usług |
| `LINK_KEY_PRESENTATION_LOGIC` | klucz szyfrowanego łącza (PRE-16) — ten sam co prezentacja |
| `RATE_LIMIT_PER_MIN` | limit żądań na adres, domyślnie 60; `0` wyłącza |
@@ -66,16 +66,16 @@ cd services/presentation && python scripts/make_user.py demo
## Uruchomienie lokalne
```bash
-cd services/astroklient-demo
+cd services/astrodemo
pip install -r requirements-dev.txt
-DEMO_PASSWORD=tajne LOGIC_URL=http://localhost:8001 \
+ASTRODEMO_PASSWORD=tajne LOGIC_URL=http://localhost:8001 \
uvicorn app.main:app --port 8005
```
Testy:
```bash
-cd services/astroklient-demo && PYTHONPATH=. pytest tests -q
+cd services/astrodemo && PYTHONPATH=. pytest tests -q
```
## Co pilnują testy
diff --git a/services/astroklient-demo/app/__init__.py b/services/astrodemo/app/__init__.py
similarity index 100%
rename from services/astroklient-demo/app/__init__.py
rename to services/astrodemo/app/__init__.py
diff --git a/services/astroklient-demo/app/clients/__init__.py b/services/astrodemo/app/clients/__init__.py
similarity index 100%
rename from services/astroklient-demo/app/clients/__init__.py
rename to services/astrodemo/app/clients/__init__.py
diff --git a/services/astroklient-demo/app/clients/logic_client.py b/services/astrodemo/app/clients/logic_client.py
similarity index 89%
rename from services/astroklient-demo/app/clients/logic_client.py
rename to services/astrodemo/app/clients/logic_client.py
index bddb49a..13cdadf 100644
--- a/services/astroklient-demo/app/clients/logic_client.py
+++ b/services/astrodemo/app/clients/logic_client.py
@@ -1,8 +1,8 @@
"""Klient warstwy logicznej — CELOWO okrojony do dwóch funkcji (PRE-28).
-Astroklient nie dziedziczy klienta z pełnej prezentacji i nie importuje go: ma tu
-być wyłącznie to, czego naprawdę używa. Metoda, której nie ma, nie zostanie
-przypadkiem zawołana ani nie podpowie, że po drugiej stronie jest coś więcej.
+Ma tu być wyłącznie to, czego ta usługa naprawdę używa. Metoda, której nie ma,
+nie zostanie przypadkiem zawołana ani nie podpowie, że po drugiej stronie jest
+coś więcej.
Rozmowa idzie tym samym szyfrowanym łączem co reszta (PRE-16) i pod tym samym
tokenem międzywarstwowym (LOG-32) — demo nie jest furtką omijającą ochronę.
@@ -45,8 +45,8 @@ class LogicClient:
def report(self, when_utc_iso: str, lat: float, lon: float) -> dict[str, Any]:
"""Interpretacja urodzeniowa: sygnifikatory z obliczeń szukane w bazach.
- Jedyne pytanie, jakie astroklient-demo zadaje o treść — bez grupowania,
- bez promptów, bez modelu językowego."""
+ Jedyne pytanie, jakie astrodemo zadaje o treść — bez grupowania,
+ bez grupowania i bez żadnej obróbki treści."""
payload = {"when_utc": when_utc_iso, "lat": lat, "lon": lon,
"limit": settings.report_limit, "group": False,
"tenant": self.tenant}
diff --git a/services/astroklient-demo/app/config.py b/services/astrodemo/app/config.py
similarity index 79%
rename from services/astroklient-demo/app/config.py
rename to services/astrodemo/app/config.py
index b02ed6a..c88142a 100644
--- a/services/astroklient-demo/app/config.py
+++ b/services/astrodemo/app/config.py
@@ -1,7 +1,6 @@
-"""Konfiguracja astroklienta-demo — wersji demonstracyjnej (PRE-28).
+"""Konfiguracja astrodemo — wersji demonstracyjnej (PRE-28).
-Zna WYŁĄCZNIE adres warstwy logicznej, tak samo jak pełna prezentacja. Nie wie
-nic o bazach, Excelu ani SQL-u.
+Zna WYŁĄCZNIE adres warstwy logicznej. Nie wie nic o bazach, Excelu ani SQL-u.
"""
from __future__ import annotations
diff --git a/services/astroklient-demo/app/link_crypto.py b/services/astrodemo/app/link_crypto.py
similarity index 99%
rename from services/astroklient-demo/app/link_crypto.py
rename to services/astrodemo/app/link_crypto.py
index 89ed3d8..da99bc2 100644
--- a/services/astroklient-demo/app/link_crypto.py
+++ b/services/astrodemo/app/link_crypto.py
@@ -153,7 +153,7 @@ class Link:
def open(self, direction: bytes, path: str, stamp: str, seq: int,
frame: bytes) -> bytes:
if not frame.startswith(MAGIC):
- raise LinkError("ramka bez znacznika astrololo")
+ raise LinkError("ramka bez znacznika protokołu")
body = frame[len(MAGIC):]
if len(body) <= NONCE_BYTES:
raise LinkError("ramka za krótka")
diff --git a/services/astroklient-demo/app/main.py b/services/astrodemo/app/main.py
similarity index 61%
rename from services/astroklient-demo/app/main.py
rename to services/astrodemo/app/main.py
index 14a1d10..e90a9e7 100644
--- a/services/astroklient-demo/app/main.py
+++ b/services/astrodemo/app/main.py
@@ -1,42 +1,86 @@
-"""astroklient-demo — wersja DEMONSTRACYJNA (PRE-28).
+"""astrodemo — wersja DEMONSTRACYJNA (PRE-28).
DWIE FUNKCJE I ANI JEDNEJ WIĘCEJ:
1. dodanie pliku bazy (wgranie + włączenie do użytku),
2. zapytanie o interpretację urodzeniową dla podanych danych wejściowych.
-DLACZEGO OSOBNA USŁUGA, A NIE KONTO Z OGRANICZENIAMI. Mechanizm uprawnień z PRE-27
-umiałby to ukryć w pełnej aplikacji, ale ukrycie a nieobecność to dwie różne rzeczy.
-Tutaj pozostałych funkcji NIE MA W OBRAZIE: nie ma tras, nie ma szablonów, nie ma
-nawet metod w kliencie warstwy logicznej. Demo można komuś oddać, nie oddając
-przy okazji kodu reszty programu.
+DLACZEGO OSOBNA USŁUGA, A NIE KONTO Z OGRANICZENIAMI. Ukrycie a nieobecność to
+dwie różne rzeczy. Tutaj niczego poza tymi dwiema funkcjami NIE MA W OBRAZIE: nie
+ma tras, nie ma szablonów, nie ma nawet metod w kliencie warstwy logicznej. Obraz
+tej usługi można komuś oddać i nie oddaje się przy tym niczego ponad to, co widać
+na ekranie — łącznie z komentarzami w tym pliku, bo one też jadą razem z obrazem.
-UWAGA, ŚWIADOMA DECYZJA WŁAŚCICIELA: astroklient-demo pracuje na TEJ SAMEJ warstwie
-danych co produkcja. Kto ma do niego dostęp, czyta oryginalne bazy interpretacyjne,
-a pliki przez niego wgrane trafiają do produkcyjnego zbioru. Dlatego konto jest
-osobne (DEMO_USER/DEMO_PASSWORD) — żeby dało się odciąć demo jedną zmienną, bez
+PULE SĄ ROZDZIELONE (PRE-29). Każde konto ma własny, izolowany zbiór plików,
+niewidoczny dla pozostałych kont; pula bierze się WYŁĄCZNIE z loginu zalogowanej
+osoby, nigdy z pola formularza. Konta są osobne (ASTRODEMO_USERS) — żeby dało się
+odciąć tę usługę jedną zmienną, bez
ruszania kont głównej aplikacji.
"""
from __future__ import annotations
import base64
+import logging
+import re
+from functools import lru_cache
+from pathlib import Path
from datetime import datetime, timedelta, timezone
import httpx
-from fastapi import FastAPI, File, Form, Request, UploadFile
-from fastapi.responses import HTMLResponse, RedirectResponse
-from fastapi.staticfiles import StaticFiles
+from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
+from fastapi.responses import HTMLResponse, RedirectResponse, Response
from fastapi.templating import Jinja2Templates
from app import link_crypto, security, session
from app.clients.logic_client import LogicClient
-# Automatyczna dokumentacja WYŁĄCZONA — tak samo jak w pełnej aplikacji. /docs
-# wypisałoby komplet tras, a demo ma nie zdradzać nawet własnej powierzchni.
-app = FastAPI(title="astroklient-demo", docs_url=None, redoc_url=None, openapi_url=None)
+# Automatyczna dokumentacja WYŁĄCZONA. /docs wypisałoby komplet tras, a ta usługa
+# ma nie zdradzać nawet własnej powierzchni.
+app = FastAPI(title="astrodemo", docs_url=None, redoc_url=None, openapi_url=None)
security.install(app)
-app.mount("/static", StaticFiles(directory="app/static"), name="static")
+# ── zasoby (PRE-27) ────────────────────────────────────────────────────────
+# Katalog NIE jest montowany. Montowanie serwuje wszystko, co w nim leży, każdemu
+# i bez śladu w dzienniku — a komentarz w arkuszu stylów mówił wprost, że istnieje
+# więcej, niż widać na ekranie. Tu jest jawna lista: arkusz logowania jest
+# publiczny, bo
+# potrzebuje go strona logowania, a cokolwiek innego jest nieznane i nie istnieje.
+ZASOBY_PUBLICZNE = frozenset({"styles.css"})
+_KATALOG_ZASOBOW = Path("app/static")
+
+
+def _bez_komentarzy(tresc: str) -> str:
+ """Komentarz w pliku statycznym jedzie do przeglądarki bajt w bajt.
+ W repozytorium zostaje — do odpowiedzi nie ma po co trafiać."""
+ tresc = re.sub(r"/\*.*?\*/", "", tresc, flags=re.S)
+ return re.sub(r"\n{3,}", "\n\n", tresc).strip() + "\n"
+
+
+@lru_cache(maxsize=None)
+def _zasob(nazwa: str) -> bytes | None:
+ """Treść zasobu tak, jak pójdzie na drut — albo None, gdy go nie ma."""
+ if nazwa not in ZASOBY_PUBLICZNE:
+ return None
+ sciezka = (_KATALOG_ZASOBOW / nazwa).resolve()
+ # Wyjście poza katalog: `..` w ścieżce albo dowiązanie na zewnątrz.
+ if _KATALOG_ZASOBOW.resolve() not in sciezka.parents or not sciezka.is_file():
+ return None
+ if sciezka.suffix in (".css", ".js"):
+ return _bez_komentarzy(sciezka.read_text(encoding="utf-8")).encode("utf-8")
+ return sciezka.read_bytes()
+
+
+@app.get("/static/{nazwa:path}")
+def zasob(nazwa: str):
+ tresc = _zasob(nazwa)
+ if tresc is None:
+ raise HTTPException(status_code=404)
+ typ = "text/css; charset=utf-8" if nazwa.endswith(".css") else "application/octet-stream"
+ return Response(tresc, media_type=typ,
+ headers={"Cache-Control": "public, max-age=3600"})
+
templates = Jinja2Templates(directory="app/templates")
+# Szczegóły awarii idą TUTAJ, nie na ekran (patrz _blad).
+log = logging.getLogger("astrodemo")
templates.env.globals["current_user"] = lambda request: getattr(request.state, "user", "") or ""
templates.env.globals["auth_on"] = security.auth_enabled
@@ -50,7 +94,7 @@ def _logic(request: Request) -> LogicClient:
return LogicClient(tenant=getattr(request.state, "user", "") or "")
# Stan pliku „w użyciu" — ta sama nazwa, co w warstwie danych (DAN-27). Wpisana
-# tutaj wprost, bo astroklient-demo nie importuje tamtej warstwy; literówka objawiłaby
+# tutaj wprost, bo astrodemo nie importuje tamtej warstwy; literówka objawiłaby
# się dopiero na żywo, więc pilnuje jej test.
ACTIVE = "active"
@@ -68,15 +112,16 @@ def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]:
return moment.astimezone(timezone.utc).isoformat(), local.strftime("%Y-%m-%d %H:%M")
+# Jedno zdanie dla wszystkich awarii i CELOWO żadnego szczegółu. Treść wyjątku
+# niosła nazwę usługi, jej port i nazwy jej tras — czyli mapę tego, co stoi za tym
+# ekranem, podaną komuś, kto ma widzieć wyłącznie dwa formularze.
+_AWARIA = "Nie udało się teraz wykonać tej operacji. Spróbuj ponownie za chwilę."
+
+
def _blad(e: Exception) -> str:
- if isinstance(e, httpx.HTTPStatusError):
- try:
- return str(e.response.json().get("detail") or e)
- except Exception: # noqa: BLE001
- return str(e)
- if isinstance(e, link_crypto.LinkError):
- return f"Łącze do warstwy logicznej nie działa: {e}"
- return f"Warstwa logiczna niedostępna: {e}"
+ """Komunikat dla użytkownika. Diagnostyka idzie do dziennika, nie na ekran."""
+ log.warning("operacja nieudana: %s: %s", type(e).__name__, e)
+ return _AWARIA
def _ile_rekordow(report: dict) -> int:
@@ -113,13 +158,12 @@ def interpretacja(request: Request, date: str = Form(...), time: str = Form(...)
async def dodaj_plik(request: Request, upload: UploadFile = File(...)):
"""Dodanie pliku bazy: wgranie ORAZ włączenie do użytku.
- W pełnej aplikacji to dwie osobne decyzje (DAN-27), bo tam ktoś nad tym panuje.
- Tutaj „dodać do bazy" ma znaczyć, że plik od razu bierze udział w wyszukiwaniu —
- inaczej po wgraniu nic by się nie zmieniło i demo wyglądałoby na zepsute.
+ Jedna czynność, nie dwie: „dodać plik" ma tu znaczyć, że od razu bierze udział
+ w wyszukiwaniu — inaczej po wgraniu nic by się nie zmieniło i wyglądałoby to
+ na zepsute.
- Walidacja zostaje: plik o złym układzie NIE wchodzi do użytku, ale też NIE JEST
- tracony — leży dalej na udziale i decyzję o nim podejmuje administrator
- w pełnej aplikacji."""
+ Plik o złym układzie nie wchodzi do użytku, ale NIE JEST tracony — zostaje
+ zapisany i czeka."""
raw = await upload.read()
if not raw:
return RedirectResponse("/?error=Pusty+plik.", status_code=303)
@@ -153,10 +197,9 @@ def _wroc(error: str = "", done: str = "") -> RedirectResponse:
# ---------------- Logowanie i wylogowanie (LOG-34) ----------------
-# Ta sama konstrukcja co w pełnej aplikacji. Różnica jest jedna i wynika z tego,
-# że demo nie ma własnego wolumenu: nie ma licznika pokolenia sesji, więc
-# „wyrzuć zewsząd" robi się zmianą hasła w DEMO_USERS. Wylogowanie i tak działa
-# natychmiast, bo polega na skasowaniu ciasteczka.
+# Bez własnego wolumenu nie ma gdzie trzymać licznika pokolenia sesji, więc
+# „wyrzuć ze wszystkich urządzeń" robi się zmianą hasła w ASTRODEMO_USERS.
+# Wylogowanie działa natychmiast, bo polega na skasowaniu ciasteczka.
def _bezpieczne_dokad(dokad: str) -> str:
"""Adres powrotu przepuszczony przez sito — bez tego `?dokad=https://obcy.pl`
@@ -207,4 +250,6 @@ def wyloguj(request: Request):
@app.get("/health")
def health() -> dict:
- return {"status": "ok", "layer": "astroklient-demo"}
+ # Sama „ok". Nazwa warstwy mówiła, że warstw jest więcej i jak się nazywa;
+ # kubernetes i compose czytają wyłącznie kod odpowiedzi.
+ return {"status": "ok"}
diff --git a/services/astroklient-demo/app/security.py b/services/astrodemo/app/security.py
similarity index 88%
rename from services/astroklient-demo/app/security.py
rename to services/astrodemo/app/security.py
index 00f16db..a5639b1 100644
--- a/services/astroklient-demo/app/security.py
+++ b/services/astrodemo/app/security.py
@@ -1,4 +1,4 @@
-"""Logowanie do astroklienta-demo (PRE-28).
+"""Logowanie do astrodemo (PRE-28).
OSOBNE KONTO, NIE WSPÓŁDZIELONE Z GŁÓWNĄ APLIKACJĄ. Demo pracuje na tej samej
warstwie danych co produkcja, więc kto ma do niego dostęp, czyta oryginalne bazy
@@ -6,7 +6,7 @@ interpretacyjne. Własny login i hasło pozwalają odciąć demo jedną zmienną
środowiskową, bez ruszania kont głównej aplikacji i bez zmiany hasła komukolwiek.
Mechanizm jest CELOWO prosty: jedno konto z konfiguracji, bez ekranu kont
-i bez uprawnień. Astroklient ma dwie funkcje i obie są dla tego samego człowieka —
+i bez uprawnień. Ta usługa ma dwie funkcje i obie są dla tego samego człowieka —
model uprawnień z PRE-27 nie miałby tu czego dzielić.
Hasło może być jawne albo jako hash scrypt (`scrypt$sól$hash`), tym samym formatem
@@ -26,23 +26,27 @@ from collections import deque
from fastapi import Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
-log = logging.getLogger("astroklient-demo.security")
-audit_log = logging.getLogger("astroklient-demo.audit")
+log = logging.getLogger("astrodemo.security")
+audit_log = logging.getLogger("astrodemo.audit")
_SCRYPT_N, _SCRYPT_R, _SCRYPT_P, _SCRYPT_LEN = 2**14, 8, 1, 32
_HASH_PREFIX = "scrypt$"
# /logowanie i /wyloguj poza bramką: inaczej żeby się zalogować, trzeba by być
# zalogowanym, a wyjście nie działałoby po wygaśnięciu sesji.
-PUBLIC_PATHS = frozenset({"/health", "/logowanie", "/wyloguj"})
-PUBLIC_PREFIXES = ("/static/",)
+PUBLIC_PATHS = frozenset({"/health", "/logowanie", "/wyloguj", "/static/styles.css"})
+# Pusto CELOWO. Prefiks "/static/" przepuszczał CAŁY katalog obok bramki: każdy
+# plik pobierał ktokolwiek, także niezalogowany, i nie zostawiało to śladu ani
+# w dzienniku, ani w limicie żądań. Publiczny jest jeden arkusz, wymieniony
+# z nazwy, bo potrzebuje go ekran logowania.
+PUBLIC_PREFIXES: tuple[str, ...] = ()
_hits: dict[str, deque[float]] = {}
MAX_TRACKED_CLIENTS = 4096
def accounts() -> dict[str, str]:
- """Konta demo: `login:sekret`, po przecinku, ze zmiennej DEMO_USERS.
+ """Konta demo: `login:sekret`, po przecinku, ze zmiennej ASTRODEMO_USERS.
WIELE KONT, bo każde ma WŁASNĄ, niewidoczną dla pozostałych pulę baz (PRE-29).
Wspólne konto oznaczałoby wspólną pulę, czyli klientów oglądających nawzajem
@@ -51,10 +55,10 @@ def accounts() -> dict[str, str]:
Sekret najlepiej jako hash scrypt (`scrypt$sól$hash`) — ten sam format i ten
sam skrypt make_user.py, co w głównej aplikacji.
- Zgodność wstecz: pojedyncze DEMO_USER/DEMO_PASSWORD działa, gdy DEMO_USERS
+ Zgodność wstecz: pojedyncze ASTRODEMO_USER/ASTRODEMO_PASSWORD działa, gdy ASTRODEMO_USERS
jest puste."""
out: dict[str, str] = {}
- for entry in os.getenv("DEMO_USERS", "").split(","):
+ for entry in os.getenv("ASTRODEMO_USERS", "").split(","):
entry = entry.strip()
if not entry:
continue
@@ -63,8 +67,8 @@ def accounts() -> dict[str, str]:
out[user.strip()] = secret.strip()
if out:
return out
- single = os.getenv("DEMO_PASSWORD", "")
- return {os.getenv("DEMO_USER", "demo"): single} if single else {}
+ single = os.getenv("ASTRODEMO_PASSWORD", "")
+ return {os.getenv("ASTRODEMO_USER", "demo"): single} if single else {}
def rate_limit_per_min() -> int:
@@ -109,7 +113,7 @@ def issue_session(login: str) -> str:
"""Zawartość ciasteczka dla właśnie zalogowanego konta.
W ciasteczku ląduje ODCISK poświadczenia, więc zmiana hasła albo usunięcie
- konta z DEMO_USERS unieważnia jego sesje samo z siebie. To jest tu jedyny
+ konta z ASTRODEMO_USERS unieważnia jego sesje samo z siebie. To jest tu jedyny
sposób zdalnego unieważnienia: demo nie ma gdzie trwale zapisywać stanu,
bo nie ma własnego wolumenu — a stan sesji na udziale z bazami klientów
byłby pomieszaniem dwóch zupełnie różnych rzeczy."""
@@ -217,7 +221,7 @@ def install(app) -> None:
if not auth_enabled():
log.warning(
- "UWAGA: brak kont (DEMO_USERS / DEMO_PASSWORD) — astroklient-demo stoi OTWARTY. "
+ "UWAGA: brak kont (ASTRODEMO_USERS / ASTRODEMO_PASSWORD) — astrodemo stoi OTWARTY. "
"Bez logowania nie ma też PULI: wszyscy pracowaliby na jednym, wspólnym "
"zbiorze baz, widząc nawzajem swoje wgrania."
)
diff --git a/services/astroklient-demo/app/session.py b/services/astrodemo/app/session.py
similarity index 96%
rename from services/astroklient-demo/app/session.py
rename to services/astrodemo/app/session.py
index 47689d1..0ea7083 100644
--- a/services/astroklient-demo/app/session.py
+++ b/services/astrodemo/app/session.py
@@ -40,10 +40,10 @@ import json
import os
import time
-# Własna nazwa, nie ta z pełnej aplikacji: gdyby obie stały kiedyś pod jedną
-# domeną, ciasteczka o tej samej nazwie nadpisywałyby się nawzajem i człowiek
-# wypadałby z jednej aplikacji, logując się do drugiej.
-COOKIE = "astroklient_sesja"
+# Nazwa własna tej usługi. Gdyby dwie aplikacje pod jedną domeną używały
+# ciasteczka o tej samej nazwie, nadpisywałyby się nawzajem i człowiek wypadałby
+# z jednej, logując się do drugiej.
+COOKIE = "astrodemo_sesja"
# Twardy kres życia sesji i kres bezczynności. Oba w sekundach, oba z konfiguracji.
DEFAULT_MAX_AGE = 30 * 24 * 3600 # 30 dni — po tylu i tak każe się zalogować
diff --git a/services/astroklient-demo/app/static/styles.css b/services/astrodemo/app/static/styles.css
similarity index 92%
rename from services/astroklient-demo/app/static/styles.css
rename to services/astrodemo/app/static/styles.css
index 71a6bd7..2e8699d 100644
--- a/services/astroklient-demo/app/static/styles.css
+++ b/services/astrodemo/app/static/styles.css
@@ -1,6 +1,5 @@
-/* astroklient-demo — arkusz CELOWO minimalny. Nie kopiujemy stylów pełnej aplikacji:
- demo ma dwa formularze i tabelę wyników, a każda przeniesiona klasa niosłaby
- nazwy funkcji, których tu nie ma. */
+/* Arkusz CELOWO minimalny: dwa formularze i tabela wyników. Każda klasa ponad to
+ niosłaby nazwę funkcji, której tu nie ma. */
:root {
--bg: #11121b; --panel: #191b26; --line: #2b2e3d;
--fg: #e8e9f0; --muted: #9aa0b4; --accent: #8ab4ff;
diff --git a/services/astroklient-demo/app/templates/base.html b/services/astrodemo/app/templates/base.html
similarity index 90%
rename from services/astroklient-demo/app/templates/base.html
rename to services/astrodemo/app/templates/base.html
index ba0080f..3a54f58 100644
--- a/services/astroklient-demo/app/templates/base.html
+++ b/services/astrodemo/app/templates/base.html
@@ -3,13 +3,13 @@
- astroklient-demo · {% block title %}{% endblock %}
+ astrodemo · {% block title %}{% endblock %}
-
astroklient-demo
+
astrodemo
{# Kto jest zalogowany i wyjście. Demo bywa używane na cudzych komputerach,
więc bez tego pierwszy klient zostawia otwartą sesję drugiemu. #}
{% if auth_on() %}
diff --git a/services/astroklient-demo/app/templates/index.html b/services/astrodemo/app/templates/index.html
similarity index 100%
rename from services/astroklient-demo/app/templates/index.html
rename to services/astrodemo/app/templates/index.html
diff --git a/services/astroklient-demo/app/templates/logowanie.html b/services/astrodemo/app/templates/logowanie.html
similarity index 92%
rename from services/astroklient-demo/app/templates/logowanie.html
rename to services/astrodemo/app/templates/logowanie.html
index cc41f83..6af2388 100644
--- a/services/astroklient-demo/app/templates/logowanie.html
+++ b/services/astrodemo/app/templates/logowanie.html
@@ -3,12 +3,12 @@
- astroklient-demo · logowanie
+ astrodemo · logowanie
-
astroklient-demo
+
astrodemo
{% if request.query_params.get('wylogowano') %}
Wylogowano.
diff --git a/services/astroklient-demo/requirements-dev.txt b/services/astrodemo/requirements-dev.txt
similarity index 100%
rename from services/astroklient-demo/requirements-dev.txt
rename to services/astrodemo/requirements-dev.txt
diff --git a/services/astroklient-demo/requirements.txt b/services/astrodemo/requirements.txt
similarity index 100%
rename from services/astroklient-demo/requirements.txt
rename to services/astrodemo/requirements.txt
diff --git a/services/astroklient-demo/tests/test_astroklient_demo.py b/services/astrodemo/tests/test_astrodemo.py
similarity index 94%
rename from services/astroklient-demo/tests/test_astroklient_demo.py
rename to services/astrodemo/tests/test_astrodemo.py
index f2a2f97..f5b4ea4 100644
--- a/services/astroklient-demo/tests/test_astroklient_demo.py
+++ b/services/astrodemo/tests/test_astrodemo.py
@@ -1,4 +1,4 @@
-"""astroklient-demo — wersja demonstracyjna (PRE-28).
+"""astrodemo — wersja demonstracyjna (PRE-28).
DWIE WŁASNOŚCI, KTÓRYCH PILNUJE TEN PLIK
@@ -19,9 +19,9 @@ from app import security
@pytest.fixture()
def env(monkeypatch):
- monkeypatch.setenv("DEMO_USERS", "demo:tajne-demo,klientA:hasloA,klientB:hasloB")
- monkeypatch.delenv("DEMO_USER", raising=False)
- monkeypatch.delenv("DEMO_PASSWORD", raising=False)
+ monkeypatch.setenv("ASTRODEMO_USERS", "demo:tajne-demo,klientA:hasloA,klientB:hasloB")
+ monkeypatch.delenv("ASTRODEMO_USER", raising=False)
+ monkeypatch.delenv("ASTRODEMO_PASSWORD", raising=False)
monkeypatch.setenv("RATE_LIMIT_PER_MIN", "0")
# Bez klucza podpisu usługa celowo nie wstaje (fail-closed, LOG-34).
monkeypatch.setenv("SESSION_SECRET", "t" * 64)
@@ -118,7 +118,7 @@ def test_without_credentials_nothing_is_served(env, monkeypatch):
def test_demo_account_is_separate_from_the_main_application(env, monkeypatch):
- """Konto astroklienta-demo MUSI być własne: demo pracuje na tej samej warstwie
+ """Konto astrodemo MUSI być własne: demo pracuje na tej samej warstwie
danych co produkcja, więc odcięcie go nie może wymagać zmiany haseł
w głównej aplikacji."""
monkeypatch.setenv("APP_USER", "astrololo")
@@ -129,7 +129,7 @@ def test_demo_account_is_separate_from_the_main_application(env, monkeypatch):
def test_a_password_with_polish_letters_is_refused_not_crashed(env, monkeypatch):
- monkeypatch.setenv("DEMO_USERS", "demo:zażółć-gęślą")
+ monkeypatch.setenv("ASTRODEMO_USERS", "demo:zażółć-gęślą")
c, _ = _client(monkeypatch)
@@ -204,7 +204,11 @@ def test_a_broken_logic_layer_is_reported_not_crashed(env, monkeypatch):
monkeypatch.setattr(LogicClient, "report", lambda self, **kw: (_ for _ in ()).throw(
httpx.ConnectError("brak połączenia")))
r = c.post("/", headers=_auth(), data=FORM)
- assert r.status_code == 200 and "Warstwa logiczna niedostępna" in r.text
+ # Komunikat jest CELOWO nieinformacyjny: treść wyjątku niosła nazwę usługi,
+ # jej port i nazwy tras, czyli mapę tego, co stoi za tym ekranem.
+ assert r.status_code == 200
+ assert "Nie udało się teraz wykonać tej operacji" in r.text
+ assert "Warstwa logiczna" not in r.text and "8001" not in r.text
def test_the_page_never_offers_anything_it_cannot_do(env, monkeypatch):
@@ -280,12 +284,12 @@ def test_logging_in_and_out_of_the_demo(env, monkeypatch):
def test_removing_an_account_kills_its_open_session(env, monkeypatch):
"""Demo nie ma gdzie trwale zapisywać stanu, więc zdalne unieważnienie robi
- się przez DEMO_USERS — usunięcie albo zmiana hasła ubija sesję odciskiem."""
+ się przez ASTRODEMO_USERS — usunięcie albo zmiana hasła ubija sesję odciskiem."""
c, _ = _client(monkeypatch)
ciasteczko = _auth("klientA")
assert c.get("/", headers=ciasteczko, follow_redirects=False).status_code == 200
- monkeypatch.setenv("DEMO_USERS", "demo:tajne-demo,klientB:hasloB") # bez klientA
+ monkeypatch.setenv("ASTRODEMO_USERS", "demo:tajne-demo,klientB:hasloB") # bez klientA
assert c.get("/", headers=ciasteczko, follow_redirects=False).status_code == 303
diff --git a/services/astrodemo/tests/test_slownik_zakazany.py b/services/astrodemo/tests/test_slownik_zakazany.py
new file mode 100644
index 0000000..19cc612
--- /dev/null
+++ b/services/astrodemo/tests/test_slownik_zakazany.py
@@ -0,0 +1,177 @@
+"""Zapora słownikowa astrodemo: czego użytkownik demo nie może zobaczyć.
+
+astrodemo stoi najniżej w drabinie trzech produktów (astrodemo → astroklient →
+astrololo) i jest dawane najszerzej. Ma dwie funkcje i ma wyglądać, jakby to
+było wszystko, co program potrafi.
+
+Test nie sprawdza pojedynczych miejsc, tylko przechodzi WSZYSTKO, co da się
+pobrać, i szuka słów, które nie mają prawa paść. Sprawdza też DRZEWO PLIKÓW
+usługi — bo „nie ma śladu w kodzie" znaczy, że `grep` po obrazie ma nic nie
+znaleźć, a nie tylko że odpowiedź jest czysta.
+
+Powód, dla którego to powstało: komentarz w pierwszej linii styles.css mówił
+„Nie kopiujemy stylów pełnej aplikacji", a arkusz pobierał anonim.
+"""
+import pathlib
+import re
+
+import pytest
+
+APP = pathlib.Path(__file__).resolve().parents[1] / "app"
+
+# (a) że istnieje wersja obszerniejsza i jak się nazywa
+WIEKSZY_BRAT = ["pełna aplikacja", "pełnej aplikacji", "pełna wersja", "pełnej wersji",
+ "astrololo", "astroklient", "wersji podstawowej", "upgrade", "rozszerzon"]
+# (b) że tekst pisze model językowy
+# Bez samego „token": w tej usłudze znaczy token sesji i token międzywarstwowy,
+# a nie jednostkę rozliczeniową modelu. Zamiast tego zwroty jednoznaczne.
+MODEL = ["LLM", "anthropic", "openai", "gpt", "claude", "prompt",
+ "generowanie tekstu", "sztuczn", "okno kontekstu", "model językow"]
+# (c) funkcje, których astrodemo nie ma
+INNE_FUNKCJE = ["kosmogram", "aspektarian", "synastri", "kalendarz", "predykcj",
+ "skompiluj", "system domów", "systemy domów", "deklinacj", "antyscj"]
+# (d) że jest ktoś, kto widzi więcej, i że produkt jest w budowie
+MECHANIZMY = ["administrator", "uprawnieni", "walidacj", "kwarantann", "roadmap",
+ "w budowie", "widok testowy", "warstwa logiczna", "layer"]
+# Dwa poziomy, bo dwie różne drogi wycieku.
+#
+# WSZĘDZIE — także w kodzie serwera, którego przeglądarka nigdy nie zobaczy.
+# Obraz tej usługi się KOMUŚ ODDAJE, więc kto go dostanie, przeczyta też
+# komentarze i docstringi. Zdanie „w pełnej aplikacji to działa inaczej" mówi mu
+# wprost, że istnieje pełna aplikacja — a to jest dokładnie to, czego ma nie
+# wiedzieć. Komentarz ma tłumaczyć TĘ usługę, bez powoływania się na rodzeństwo.
+WSZEDZIE = WIEKSZY_BRAT + MODEL + INNE_FUNKCJE
+# TYLKO DO PRZEGLĄDARKI — słownictwo inżynierskie opisujące własne działanie tej
+# usługi. W komentarzu serwera jest na miejscu; w odpowiedzi HTTP nie ma czego
+# szukać, bo mówi o mechanizmach, o których użytkownik nie ma wiedzieć.
+DO_PRZEGLADARKI = WSZEDZIE + MECHANIZMY
+ZAKAZANE = DO_PRZEGLADARKI
+WZORCE = [(r"(?i)\b(PRE|LOG|DAN)-\d+\b", "identyfikator wymagania"),
+ (r"(?i)\bfeat/[\w-]+", "nazwa gałęzi rozwojowej"),
+ (r"(?i)\bAI\b", "model językowy")]
+
+
+def _grzechy(tresc: str, gdzie: str, slownik: list[str] | None = None) -> list[str]:
+ out = []
+ for igla in (slownik if slownik is not None else ZAKAZANE):
+ if re.search(rf"(?i)(? bytes:
if not frame.startswith(MAGIC):
- raise LinkError("ramka bez znacznika astrololo")
+ raise LinkError("ramka bez znacznika protokołu")
body = frame[len(MAGIC):]
if len(body) <= NONCE_BYTES:
raise LinkError("ramka za krótka")
diff --git a/services/logic/app/link_crypto.py b/services/logic/app/link_crypto.py
index 89ed3d8..da99bc2 100644
--- a/services/logic/app/link_crypto.py
+++ b/services/logic/app/link_crypto.py
@@ -153,7 +153,7 @@ class Link:
def open(self, direction: bytes, path: str, stamp: str, seq: int,
frame: bytes) -> bytes:
if not frame.startswith(MAGIC):
- raise LinkError("ramka bez znacznika astrololo")
+ raise LinkError("ramka bez znacznika protokołu")
body = frame[len(MAGIC):]
if len(body) <= NONCE_BYTES:
raise LinkError("ramka za krótka")
diff --git a/services/presentation/app/link_crypto.py b/services/presentation/app/link_crypto.py
index 89ed3d8..da99bc2 100644
--- a/services/presentation/app/link_crypto.py
+++ b/services/presentation/app/link_crypto.py
@@ -153,7 +153,7 @@ class Link:
def open(self, direction: bytes, path: str, stamp: str, seq: int,
frame: bytes) -> bytes:
if not frame.startswith(MAGIC):
- raise LinkError("ramka bez znacznika astrololo")
+ raise LinkError("ramka bez znacznika protokołu")
body = frame[len(MAGIC):]
if len(body) <= NONCE_BYTES:
raise LinkError("ramka za krótka")
diff --git a/services/render/app/link_crypto.py b/services/render/app/link_crypto.py
index 89ed3d8..da99bc2 100644
--- a/services/render/app/link_crypto.py
+++ b/services/render/app/link_crypto.py
@@ -153,7 +153,7 @@ class Link:
def open(self, direction: bytes, path: str, stamp: str, seq: int,
frame: bytes) -> bytes:
if not frame.startswith(MAGIC):
- raise LinkError("ramka bez znacznika astrololo")
+ raise LinkError("ramka bez znacznika protokołu")
body = frame[len(MAGIC):]
if len(body) <= NONCE_BYTES:
raise LinkError("ramka za krótka")