PRE-27: pełne ukrycie niedostępnych funkcji (paranoja)
build-render / build (push) Failing after 8s
build-swisseph / build (push) Successful in 9s
build / build (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 5s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 4s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Failing after 4s
build-render / build (push) Failing after 8s
build-swisseph / build (push) Successful in 9s
build / build (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 5s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 4s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Failing after 4s
Ukrywanie jest teraz nadrzędne wobec wygody i czytelności komunikatów. Persona: konto z uprawnieniami files + files_input, zatrudnione wyłącznie do wgrywania plików. Nie ma się dowiedzieć, po co je wgrywa ani co program będzie robił — bo to rozgada. Audyt sześciu kanałów wycieku (statyki, HTML, sondowanie HTTP, ekran plików, odpowiedzi JSON i błędy, pozostałe warstwy) potwierdził 26 wycieków, każdy odtworzony uruchomionym kodem i zweryfikowany adwersarialnie. Ani jeden nie był przyciskiem. ZASÓB JEST CZĘŚCIĄ FUNKCJI /static/ omijało CAŁĄ bramkę (PUBLIC_PREFIXES), więc każdy skrypt i arkusz pobierał ktokolwiek, także niezalogowany, pod zgadywalnym adresem — a ich treść wymienia ekrany, dostawców modeli i przeznaczenie plików. Ruch ten nie trafiał przy tym ani do dziennika, ani pod limit żądań, więc wyciek był niewidoczny. Zasoby idą teraz trasą z bramką; każdy ma w features.STATIC uprawnienie swojego ekranu. Publiczny został jeden base.css, bo potrzebuje go ekran logowania. KOMENTARZ NIE JEDZIE NA DRUT Komentarze w CSS/JS opisywały funkcje pełnymi zdaniami po polsku — łącznie z „Wstrzymane widzi tylko administrator", czyli i mechanizmem kwarantanny, i istnieniem konta o wyższych uprawnieniach. _asset_body() usuwa je przy serwowaniu; w repozytorium zostają. styles.css rozbity na base.css + arkusz na ekran + x-ai.css. Jeden plik z wszystkimi selektorami był spisem treści programu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żaden ekran nie stracił reguły. base.html ładował skrypty kosmogramu na KAŻDEJ stronie — konto mające wyłącznie Pliki pobierało je przy wejściu na swój jedyny ekran, razem ze wzmianką o „przyszłej zakładce". Teraz dokłada je ekran, który ich używa. RÓŻNICA JEST INFORMACJĄ Komunikat po wgraniu pliku różnił się zależnie od wyniku walidacji — czyli był wyrocznią do odgadywania reguł, które ma znać tylko administrator — i mówił wprost, że plik „musi zatwierdzić administrator". Teraz jest jeden, ten sam. _logic_error wypisywał na ekran nazwę trasy, nazwę podsystemu, nazwę gałęzi rozwojowej i wewnętrzny host:port. Jedno zdanie dla wszystkich awarii, szczegóły do dziennika. Odsiew w jednym punkcie, nie w siedemnastu wywołaniach. Ponadto: stopka nie ogłasza architektury, /health nie nazywa warstwy, konto bez ekranów dostaje 404 zamiast tłumaczenia, ekran plików mówi o plikach zamiast o „bazach interpretacyjnych", klasy .house-warning i .account-card przemianowane na neutralne, a logic/data/render/engine-swisseph nie wystawiają już /docs ani /openapi.json i nie publikują portów na hoście. ZAPORA SŁOWNIKOWA test_slownik_zakazany.py nie sprawdza miejsc, tylko przechodzi wszystko, co dane konto może pobrać, i szuka słów, które nie mają prawa paść (87 pozycji dla tej persony). Nazwy funkcji, adresy ekranów i nazwy zasobów biorą się wprost z katalogu, więc nowa funkcja obejmuje się sama. Kontrola pozytywna pilnuje, żeby test nie przechodził dlatego, że program jest pusty. Sprawdzone: zapora puszczona na treść sprzed poprawek daje 16 trafień na samym styles.css i łapie każdy ze zneutralizowanych komunikatów. 358 testów zielonych, ekrany obejrzane w przeglądarce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit was merged in pull request #78.
This commit is contained in:
@@ -118,17 +118,77 @@ ROUTES: dict[tuple[str, str], str | None] = {
|
||||
}
|
||||
|
||||
|
||||
def required(method: str, path: str) -> str | None:
|
||||
# ── zasoby statyczne ─────────────────────────────────────────────────────
|
||||
# Skrypt i arkusz stylów są CZĘŚCIĄ funkcji, nie jej dekoracją. Nazwa pliku jest
|
||||
# zgadywalna, a treść opowiada o funkcji dokładniej niż przycisk: `models.js`
|
||||
# wymienia dostawców modeli, `compile.js` — składanie raportu. Dlatego każdy
|
||||
# zasób przechodzi przez tę samą bramkę co ekrany.
|
||||
#
|
||||
# Wartość to ZBIÓR uprawnień, z których wystarczy JEDNO: wyszukiwarka lokalizacji
|
||||
# siedzi na trzech ekranach i konto z którymkolwiek z nich musi ją dostać.
|
||||
# `None` = każdy zalogowany. Zasób spoza mapy wymaga admina — jak trasa.
|
||||
_FORMULARZE = frozenset({"chart", "interpret", "timeline", "compile"})
|
||||
_LOKALIZACJA = frozenset({"chart", "interpret", "timeline"})
|
||||
_KOLO = frozenset({"chart", "compile"})
|
||||
|
||||
STATIC: dict[str, frozenset[str] | str | None] = {
|
||||
# arkusze ekranów — jeden ekran, jedno uprawnienie
|
||||
"s-chart.css": "chart",
|
||||
"s-interpret.css": "interpret",
|
||||
"s-timeline.css": "timeline",
|
||||
"s-synastry.css": "synastry",
|
||||
"s-significators.css": "significators",
|
||||
"s-compile.css": "compile",
|
||||
"s-files.css": "files",
|
||||
"s-settings.css": "settings",
|
||||
"s-accounts.css": ADMIN,
|
||||
"x-ai.css": "ai",
|
||||
# skrypty
|
||||
"formsync.js": _FORMULARZE,
|
||||
"now.js": _LOKALIZACJA,
|
||||
"geo.js": _LOKALIZACJA,
|
||||
"vendor/leaflet/leaflet.js": _LOKALIZACJA,
|
||||
"vendor/leaflet/leaflet.css": _LOKALIZACJA,
|
||||
"wheelzoom.js": _KOLO,
|
||||
"wheeltip.js": _KOLO,
|
||||
"copy.js": frozenset({"interpret", "timeline"}),
|
||||
"compile.js": "compile",
|
||||
"models.js": "ai",
|
||||
"progress.js": "ai",
|
||||
"natal.js": "ai",
|
||||
"predictions.js": "ai",
|
||||
# base.css celowo NIE MA tu wpisu: potrzebuje go ekran logowania, więc jest
|
||||
# publiczny (PUBLIC_PATHS w security.py). Dlatego nie wolno w nim trzymać
|
||||
# niczego, co nazywa jakąkolwiek funkcję.
|
||||
}
|
||||
|
||||
|
||||
def static_required(name: str) -> frozenset[str] | str | None:
|
||||
"""Uprawnienie dla zasobu. Nieznany zasób → admin, jak nieznana trasa."""
|
||||
return STATIC.get(name, ADMIN)
|
||||
|
||||
|
||||
def required(method: str, path: str) -> frozenset[str] | str | None:
|
||||
"""Uprawnienie wymagane przez trasę. Nieznana trasa → wymaga admina.
|
||||
|
||||
Domyślną odpowiedzią jest NAJWĘŻSZE uprawnienie, nie najszersze: dopisanie
|
||||
trasy bez wpisu w ROUTES ma ją zamknąć, a nie otworzyć. Odwrotny domyślny
|
||||
wybór oznaczałby, że każde przeoczenie tworzy dziurę."""
|
||||
if path.startswith("/static/"):
|
||||
return static_required(path[len("/static/"):])
|
||||
return ROUTES.get((method.upper(), path), ADMIN)
|
||||
|
||||
|
||||
def can(perms: frozenset[str], feature: str | None) -> bool:
|
||||
return feature is None or feature in perms
|
||||
def can(perms: frozenset[str], feature: frozenset[str] | str | None) -> bool:
|
||||
"""Czy te uprawnienia otwierają tę funkcję.
|
||||
|
||||
Zbiór znaczy „wystarczy którekolwiek": jeden zasób bywa wspólny dla kilku
|
||||
ekranów i nie ma powodu wymagać ich wszystkich naraz."""
|
||||
if feature is None:
|
||||
return True
|
||||
if isinstance(feature, str):
|
||||
return feature in perms
|
||||
return bool(perms & feature)
|
||||
|
||||
|
||||
def screens_for(perms: frozenset[str]) -> list[Feature]:
|
||||
|
||||
@@ -12,14 +12,15 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI, File, Form, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
import base64
|
||||
@@ -41,8 +42,9 @@ from app.config import DEFAULT_LOCATION_LABEL, default_form
|
||||
# gdyby obok leżał ich katalog. Znalezione testem przechodzącym po trasach aplikacji.
|
||||
app = FastAPI(title="astrololo · warstwa prezentacji",
|
||||
docs_url=None, redoc_url=None, openapi_url=None)
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
# Szczegóły awarii idą TUTAJ, nie na ekran (patrz _logic_error).
|
||||
log = logging.getLogger("astrololo.prezentacja")
|
||||
logic = LogicClient()
|
||||
security.install(app) # logowanie + limit żądań (LOG-32)
|
||||
|
||||
@@ -55,18 +57,86 @@ security.install(app) # logowanie + limit żądań (LOG-32)
|
||||
_STATIC_DIR = Path("app/static")
|
||||
|
||||
|
||||
_TYPY_ZASOBOW = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".js": "application/javascript; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".woff2": "font/woff2",
|
||||
}
|
||||
|
||||
|
||||
def _bez_komentarzy(tresc: str, sufiks: str) -> str:
|
||||
"""Usuwa komentarze z arkusza albo skryptu przed wysłaniem.
|
||||
|
||||
Komentarz w pliku statycznym jedzie do przeglądarki bajt w bajt, a te
|
||||
komentarze opisują funkcje pełnym zdaniem po polsku — łącznie z tym, czego
|
||||
konto ograniczone NIE widzi i kto to widzi. Zostają w repozytorium, bo tam
|
||||
są potrzebne; do odpowiedzi nie mają po co trafiać.
|
||||
|
||||
Kasujemy tylko komentarze blokowe i CAŁE wiersze `//`. Komentarz na końcu
|
||||
wiersza zostaje CELOWO: żeby go usunąć, trzeba rozpoznać, czy `//` nie jest
|
||||
w napisie albo w wyrażeniu regularnym — a pomyłka tutaj psuje skrypt po
|
||||
cichu. Test pilnuje słownika zakazanych słów, więc reszta by i tak wyszła.
|
||||
"""
|
||||
tresc = re.sub(r"/\*.*?\*/", "", tresc, flags=re.S)
|
||||
if sufiks == ".js":
|
||||
tresc = re.sub(r"(?m)^[ \t]*//.*$\n?", "", tresc)
|
||||
return re.sub(r"\n{3,}", "\n\n", tresc).strip() + "\n"
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _asset_body(name: str) -> bytes | None:
|
||||
"""Treść zasobu tak, jak pójdzie na drut — albo None, gdy go nie ma."""
|
||||
sciezka = (_STATIC_DIR / name).resolve()
|
||||
korzen = _STATIC_DIR.resolve()
|
||||
# Wyjście poza katalog zasobów: `..` w ścieżce albo dowiązanie na zewnątrz.
|
||||
if korzen not in sciezka.parents or not sciezka.is_file():
|
||||
return None
|
||||
surowe = sciezka.read_bytes()
|
||||
sufiks = sciezka.suffix.lower()
|
||||
# Kod obcy zostaje nietknięty: minifikowany plik potrafi mieć `//` w napisie
|
||||
# albo w wyrażeniu regularnym, a on i tak nie mówi nic o naszym programie.
|
||||
if sufiks in (".css", ".js") and not name.startswith("vendor/"):
|
||||
try:
|
||||
return _bez_komentarzy(surowe.decode("utf-8"), sufiks).encode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return surowe
|
||||
return surowe
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _asset_version(name: str) -> str:
|
||||
try:
|
||||
return hashlib.md5((_STATIC_DIR / name).read_bytes()).hexdigest()[:8]
|
||||
except OSError:
|
||||
return "0" # brak pliku nie może wywrócić strony
|
||||
tresc = _asset_body(name)
|
||||
# Hash liczony z treści WYSYŁANEJ, nie z pliku na dysku: inaczej zmiana
|
||||
# samego komentarza podbijałaby adres, choć odpowiedź jest ta sama.
|
||||
return hashlib.md5(tresc).hexdigest()[:8] if tresc is not None else "0"
|
||||
|
||||
|
||||
def static_url(name: str) -> str:
|
||||
return f"/static/{name}?v={_asset_version(name)}"
|
||||
|
||||
|
||||
@app.get("/static/{nazwa:path}")
|
||||
def static_file(nazwa: str):
|
||||
"""Zasoby idą przez bramkę uprawnień jak każda inna trasa.
|
||||
|
||||
Dopóki `/static/` było w PUBLIC_PREFIXES, katalog omijał całe sprawdzanie:
|
||||
ukrycie przycisku w szablonie nic nie dawało, bo obok leżał skrypt tej samej
|
||||
funkcji, pod nazwą, którą wystarczyło zgadnąć. Uprawnienie zasobu sprawdza
|
||||
middleware (features.static_required); tutaj zostaje samo wydanie pliku.
|
||||
"""
|
||||
tresc = _asset_body(nazwa)
|
||||
if tresc is None:
|
||||
raise HTTPException(status_code=404)
|
||||
return Response(
|
||||
tresc,
|
||||
media_type=_TYPY_ZASOBOW.get(Path(nazwa).suffix.lower(), "application/octet-stream"),
|
||||
# Adres niesie hash treści, więc wersja pod danym adresem jest niezmienna.
|
||||
headers={"Cache-Control": "public, max-age=31536000, immutable"},
|
||||
)
|
||||
|
||||
|
||||
templates.env.globals["static"] = static_url
|
||||
# Katalog systemów domów — jedno źródło nazw dla wszystkich szablonów.
|
||||
templates.env.globals["HOUSE_SYSTEMS"] = HOUSE_SYSTEMS
|
||||
@@ -179,13 +249,24 @@ def _report_records(report: dict | None) -> int:
|
||||
return total
|
||||
|
||||
|
||||
# Jedno zdanie dla wszystkich awarii — i CELOWO żadnego szczegółu.
|
||||
# Poprzednia treść wypisywała na ekran nazwę endpointu (/chart/positions), nazwę
|
||||
# podsystemu („silnik efemeryd"), nazwę gałęzi rozwojowej i — przez str(wyjątku
|
||||
# httpx) — wewnętrzny adres z portem sąsiedniej usługi. Dla konta, które ma
|
||||
# widzieć wyłącznie Pliki, to był komplet: co program liczy, że stoi za nim
|
||||
# łańcuch usług, i gdzie zapukać.
|
||||
_AWARIA = "Nie udało się teraz wykonać tej operacji. Spróbuj ponownie za chwilę."
|
||||
|
||||
|
||||
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}"
|
||||
"""Komunikat dla użytkownika. Diagnostyka idzie do dziennika, nie na ekran.
|
||||
|
||||
Odsiew siedzi TUTAJ, a nie w siedemnastu miejscach wywołania: gdyby zależał
|
||||
od tego, czy ktoś pamiętał przekazać uprawnienia, prędzej czy później jedno
|
||||
wywołanie zostałoby pominięte — i nie byłoby tego jak zauważyć.
|
||||
"""
|
||||
log.warning("warstwa logiczna: %s: %s", type(e).__name__, e)
|
||||
return _AWARIA
|
||||
|
||||
|
||||
# ---------------- Horoskop: pozycje (strona główna) ----------------
|
||||
@@ -198,9 +279,11 @@ def chart_form(request: Request):
|
||||
target = _landing(request)
|
||||
if target:
|
||||
return RedirectResponse(target, status_code=303)
|
||||
return HTMLResponse("<h1>Brak przypisanych funkcji</h1>"
|
||||
"<p>Skontaktuj się z osobą, która zakładała konto.</p>",
|
||||
status_code=200)
|
||||
# Ta sama odpowiedź co bramka dla trasy bez uprawnienia. Poprzednia treść
|
||||
# mówiła o „przypisanych funkcjach" i o „osobie, która zakładała konto",
|
||||
# czyli wprost: funkcje są komuś przydzielane, jest ktoś, kto to robi,
|
||||
# a twoje konto zostało okrojone.
|
||||
return HTMLResponse("<h1>404 — nie znaleziono</h1>", status_code=404)
|
||||
return templates.TemplateResponse(
|
||||
request, "chart.html",
|
||||
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL,
|
||||
@@ -767,7 +850,7 @@ def files_use(request: Request, path: str = Form(...), use: str = Form("")):
|
||||
except httpx.HTTPError as e:
|
||||
return _files_redirect(error=_logic_error(e))
|
||||
return _files_redirect(done="Zapisano." if want == files_state.READY
|
||||
else "Baza włączona do użytku.")
|
||||
else "Plik włączony do użytku.")
|
||||
|
||||
|
||||
@app.post("/files/upload")
|
||||
@@ -782,12 +865,13 @@ async def files_upload(request: Request, upload: UploadFile = File(...)):
|
||||
base64.b64encode(raw).decode("ascii"), by=_who(request))
|
||||
except httpx.HTTPError as e:
|
||||
return _files_redirect(error=_logic_error(e))
|
||||
if out.get("accepted"):
|
||||
return _files_redirect(done=f"Wgrano „{out.get('name')}”. Można ją włączyć do użytku.")
|
||||
# Bez powodu i bez słowa „walidacja" — poza administratorem nikt nie ma
|
||||
# skąd wiedzieć, że taki mechanizm istnieje.
|
||||
return _files_redirect(done=f"Wgrano „{out.get('name')}”. "
|
||||
f"Zanim trafi do użytku, musi ją zatwierdzić administrator.")
|
||||
# DOKŁADNIE ten sam komunikat niezależnie od wyniku sprawdzenia. Dwa różne
|
||||
# komunikaty były wyrocznią: wystarczyło wgrywać spreparowane pliki i czytać
|
||||
# odpowiedź, żeby odgadnąć reguły, które ma znać wyłącznie administrator.
|
||||
# Poprzednia treść mówiła przy tym wprost, że istnieje „administrator", który
|
||||
# musi plik „zatwierdzić" — czyli że konto wgrywającego jest ograniczone
|
||||
# i że plik przechodzi przez jakąś bramkę.
|
||||
return _files_redirect(done=f"Wgrano „{out.get('name')}”.")
|
||||
|
||||
|
||||
@app.post("/files/archive")
|
||||
@@ -995,4 +1079,6 @@ def wyloguj(request: Request):
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict:
|
||||
return {"status": "ok", "layer": "presentation"}
|
||||
# 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"}
|
||||
|
||||
@@ -133,8 +133,16 @@ def trust_proxy() -> bool:
|
||||
# żądaniu, więc handler i tak widziałby zalogowaną osobę.
|
||||
# /logowanie MUSI stać poza bramką — inaczej żeby się zalogować, trzeba by być
|
||||
# zalogowanym. /wyloguj też: ma działać także wtedy, gdy sesja już wygasła.
|
||||
PUBLIC_PATHS = frozenset({"/health", "/logowanie", "/wyloguj"})
|
||||
PUBLIC_PREFIXES = ("/static/",)
|
||||
# Ekran logowania musi się wyświetlić PRZED zalogowaniem, więc jego arkusz jest
|
||||
# jedynym publicznym zasobem — i dlatego nie wolno w nim trzymać niczego, co
|
||||
# nazywa funkcję (patrz features.STATIC).
|
||||
PUBLIC_PATHS = frozenset({"/health", "/logowanie", "/wyloguj", "/static/base.css"})
|
||||
# Pusto CELOWO. Dopóki był tu "/static/", cały katalog omijał bramkę: każdy plik
|
||||
# JS i arkusz stylów pobierał ktokolwiek, także niezalogowany, pod zgadywalnym
|
||||
# adresem — a ich treść wymienia ekrany, dostawców modeli i przeznaczenie baz.
|
||||
# Ruch do zasobów nie trafiał przy tym ani do dziennika, ani pod limit żądań,
|
||||
# więc wyciek był dla właściciela niewidoczny.
|
||||
PUBLIC_PREFIXES: tuple[str, ...] = ()
|
||||
|
||||
_hits: dict[str, deque[float]] = {}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* Powłoka strony i ekran logowania — JEDYNY arkusz publiczny.
|
||||
Nie wolno tu wstawić niczego, co nazywa funkcję: ten plik pobiera
|
||||
każdy, także niezalogowany, bo potrzebuje go ekran logowania. */
|
||||
|
||||
:root {
|
||||
--bg: #0f1020;
|
||||
--panel: #1a1c33;
|
||||
--ink: #e8e8f0;
|
||||
--muted: #9aa0c0;
|
||||
--accent: #8b7bf0;
|
||||
--line: #2a2d4a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; background: var(--bg); color: var(--ink);
|
||||
font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
display: flex; justify-content: center; padding: 3rem 1rem;
|
||||
}
|
||||
main { width: 100%; max-width: 880px; }
|
||||
h1 { margin: 0; font-size: 2rem; letter-spacing: .5px; }
|
||||
.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; }
|
||||
.actions { display: flex; gap: .5rem; margin-top: 1rem; }
|
||||
form { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; }
|
||||
input, select, button {
|
||||
font: inherit; padding: .6rem .75rem; border-radius: 8px;
|
||||
border: 1px solid var(--line); background: #12132a; color: var(--ink);
|
||||
}
|
||||
button { background: var(--accent); color: #fff; border: none; cursor: pointer; padding-inline: 1.25rem; }
|
||||
button:hover { filter: brightness(1.1); }
|
||||
.opts { display: flex; gap: 1.5rem; margin-top: .75rem; color: var(--muted); align-items: center; }
|
||||
.meta { color: var(--muted); margin: 1.5rem 0 .5rem; font-size: .9rem; }
|
||||
/* Fallback systemu domów. Celowo NIE jest to dyskretny „muted" — po fallbacku
|
||||
kosmogram wygląda bezbłędnie, więc ten akapit jest jedynym sygnałem, że
|
||||
podział domów pochodzi z innego systemu, niż zamówiono. */
|
||||
.notice-warn {
|
||||
margin: 1rem 0; padding: .75rem 1rem; font-size: .92rem; line-height: 1.45;
|
||||
border: 1px solid var(--warn-border, #b8860b);
|
||||
border-left-width: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--warn-bg, rgba(184, 134, 11, .10));
|
||||
color: var(--warn-fg, inherit);
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; margin-top: .5rem; background: var(--panel); border-radius: 12px; overflow: hidden; }
|
||||
th, td { text-align: left; padding: .6rem .8rem; border-bottom: 1px solid var(--line); }
|
||||
th { color: var(--accent); font-size: .8rem; text-transform: uppercase; letter-spacing: .5px; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
.muted { color: var(--muted); }
|
||||
.small { font-size: .85rem; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
/* ── ekran kont (PRE-26) ────────────────────────────────────────────── */
|
||||
.card {
|
||||
margin-top: 1rem; padding: 1rem 1.25rem;
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
||||
}
|
||||
.card .actions { margin-top: .9rem; }
|
||||
/* Potwierdzenie operacji. Odróżnia się od .notice-warn, bo tamto ostrzega,
|
||||
a to tylko potwierdza — pomylenie ich uczy ignorować oba. */
|
||||
.done-note {
|
||||
margin: 1rem 0; padding: .6rem .9rem; font-size: .9rem;
|
||||
border: 1px solid var(--line); border-left: 4px solid var(--accent);
|
||||
border-radius: 4px; background: rgba(255, 255, 255, .03);
|
||||
}
|
||||
/* ── wyjście z aplikacji (LOG-32) ───────────────────────────────────── */
|
||||
/* Odsunięte od zakładek i wyglądające inaczej niż one: to czynność, nie miejsce,
|
||||
a pomylenie go z zakładką kosztuje przypadkowe wylogowania. Odstęp robimy
|
||||
marginesem, nie rozpychaczem — `nav` ma szerokość treści, więc nie ma czego
|
||||
rozpychać. */
|
||||
.whoami { color: var(--muted); font-size: .85rem; align-self: center;
|
||||
margin-left: 1rem; padding-left: 1rem; border-left: 1px solid var(--line); }
|
||||
/* Wylogowanie idzie POST-em, więc jest przyciskiem — ale ma wyglądać jak
|
||||
reszta paska, nie jak przycisk akcji w formularzu. */
|
||||
.wyloguj-form { display: inline; margin: 0; padding: 0; background: none; border: none; }
|
||||
button.wyloguj {
|
||||
background: none; border: 1px solid transparent; color: var(--muted);
|
||||
font: inherit; padding: .4rem .8rem; border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
button.wyloguj:hover { color: var(--ink); }
|
||||
/* Ekran logowania — wąski, bez nawigacji: nie ma dokąd iść przed zalogowaniem. */
|
||||
.login-main { max-width: 26rem; }
|
||||
.login-card label { display: block; margin-top: .75rem; }
|
||||
.login-card input { width: 100%; }
|
||||
.login-note { margin-top: 1.25rem; }
|
||||
@@ -0,0 +1,24 @@
|
||||
/* Arkusz ekranu „admin” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.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; }
|
||||
button.ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); }
|
||||
.row { display: flex; gap: .5rem; }
|
||||
.row input[type=text] { flex: 1; }
|
||||
/* Lista systemów domów urosła z 3 do 13 pozycji. Na flexie zawijały się w cztery
|
||||
poszarpane wiersze o różnej długości; siatka o stałej szerokości kolumny ustawia
|
||||
nazwy w pionie i daje się przeczytać wzrokiem. auto-fill, więc na wąskim ekranie
|
||||
kolumn jest po prostu mniej. */
|
||||
.opts.systems { display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
|
||||
gap: .35rem 1rem; align-items: start; }
|
||||
.opts.systems > .opts-label { grid-column: 1 / -1; margin-bottom: .1rem; }
|
||||
.opts.systems label { display: flex; align-items: center; gap: .35rem; }
|
||||
.opts .hint { font-style: normal; opacity: .75; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
.account-head { display: flex; align-items: baseline; gap: .75rem; margin-bottom: .5rem; }
|
||||
.account-note input { width: 100%; }
|
||||
button.danger { background: #8b2f2f; }
|
||||
@@ -0,0 +1,119 @@
|
||||
/* Arkusz ekranu „chart” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.sub { color: var(--muted); margin: .25rem 0 2rem; }
|
||||
.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; }
|
||||
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; }
|
||||
/* Glify astrologiczne (LOG-22). Trzy warstwy obrony przed prezentacją emoji
|
||||
(DAN-18: kolorowe kwadraty są nieczytelne i niesterowalne kolorem):
|
||||
1) selektor wariantu U+FE0E w samych danych (glyphs.py),
|
||||
2) font-variant-emoji:text — nowsza własność CSS,
|
||||
3) font-family celujący w MONOCHROMATYCZNE fonty symboli PRZED jakimkolwiek
|
||||
fontem emoji. Bez tego macOS/Chromium i tak sięga po Apple Color Emoji dla
|
||||
znaków zodiaku (♈–♓), mimo VS15 — potwierdzone wizualnie. „Apple Symbols",
|
||||
„Segoe UI Symbol", „Noto Sans Symbols2" mają te glify w wersji tekstowej. */
|
||||
.glyph { font-size: 1.15em; line-height: 1; font-variant-emoji: text;
|
||||
font-family: "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols2",
|
||||
"DejaVu Sans", sans-serif; }
|
||||
td.glyph { color: var(--accent); white-space: nowrap; }
|
||||
/* Kosmogram (PRE-12) — SVG skaluje się do szerokości, ale nie rośnie w nieskończoność. */
|
||||
.wheel-fig { margin: 1.5rem 0 .5rem; text-align: center; }
|
||||
.wheel-fig svg.wheel { width: 100%; max-width: 460px; height: auto; }
|
||||
.wheel-fig figcaption { margin-top: .4rem; }
|
||||
/* glify wewnątrz SVG dziedziczą monochromatyczny stack fontów (DAN-18) */
|
||||
svg.wheel text.glyph { font-size: inherit; }
|
||||
/* Aspektarian (PRE-18) — trójkątna siatka aspektów, węższa od koła. */
|
||||
.aspectarian-fig { margin: .25rem 0 1.25rem; text-align: center; }
|
||||
.aspectarian-fig svg.aspectarian { width: 100%; max-width: 380px; height: auto; }
|
||||
.aspectarian-fig figcaption { margin-top: .4rem; }
|
||||
svg.aspectarian text.glyph { font-size: inherit; }
|
||||
/* Wykres deklinacji i oś antyscji (LOG-07, etap 6) — szersze, pełna szerokość. */
|
||||
.declination-fig, .antiscia-fig { margin: 1.25rem 0 .75rem; text-align: center; }
|
||||
.declination-fig svg.declination { width: 100%; max-width: 560px; height: auto; }
|
||||
.antiscia-fig svg.antiscia { width: 100%; max-width: 560px; height: auto; }
|
||||
.declination-fig figcaption, .antiscia-fig figcaption { margin-top: .4rem; }
|
||||
svg.declination text.glyph, svg.antiscia text.glyph { font-size: inherit; }
|
||||
/* Powiększenie na pełne okno (PRE-25). Kliknięcie rozciąga wykres na cały ekran,
|
||||
ponowne wraca. z-index 1100 CELOWO pomiędzy: ponad kontrolki Leafleta (1000),
|
||||
ale PONIŻEJ okna postępu (1200) — gdy trwa pisanie horoskopu, log operacji ma
|
||||
zostać na wierzchu. */
|
||||
.wheel-fig svg.wheel { cursor: zoom-in; }
|
||||
.wheel-fig svg.wheel:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
||||
.wheel-fig.zoomed {
|
||||
position: fixed; inset: 0; z-index: 1100; margin: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: .5rem; padding: 1rem; background: var(--bg);
|
||||
}
|
||||
/* min() po obu wymiarach: koło jest kwadratowe, więc ograniczamy je krótszym
|
||||
bokiem okna — inaczej na szerokim ekranie wystawałoby w pionie. */
|
||||
.wheel-fig.zoomed svg.wheel {
|
||||
max-width: min(96vw, 92vh); width: min(96vw, 92vh); height: auto; cursor: zoom-out;
|
||||
}
|
||||
body.wheel-zoom-open { overflow: hidden; }
|
||||
/* nie przewijaj treści pod spodem */
|
||||
/* Dymek kosmogramu. z-index 1150 CELOWO pomiędzy: ponad nakładką powiększonego
|
||||
koła (1100), żeby działał także po powiększeniu, ale PONIŻEJ okna postępu
|
||||
(1200), które ma zostać na wierzchu. pointer-events: none — dymek chodzi za
|
||||
kursorem i nie może sam siebie zasłonić ani przechwycić najechania. */
|
||||
.wheel-tip {
|
||||
position: fixed; z-index: 1150; pointer-events: none;
|
||||
max-width: 22rem; padding: .4rem .6rem;
|
||||
background: var(--panel); color: var(--fg, inherit);
|
||||
border: 1px solid var(--line); border-radius: 8px;
|
||||
font-size: .85rem; line-height: 1.35;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
|
||||
}
|
||||
.wheel-tip[hidden] { display: none; }
|
||||
/* Lista systemów domów urosła z 3 do 13 pozycji. Na flexie zawijały się w cztery
|
||||
poszarpane wiersze o różnej długości; siatka o stałej szerokości kolumny ustawia
|
||||
nazwy w pionie i daje się przeczytać wzrokiem. auto-fill, więc na wąskim ekranie
|
||||
kolumn jest po prostu mniej. */
|
||||
.opts.systems { display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
|
||||
gap: .35rem 1rem; align-items: start; }
|
||||
.opts.systems > .opts-label { grid-column: 1 / -1; margin-bottom: .1rem; }
|
||||
.opts.systems label { display: flex; align-items: center; gap: .35rem; }
|
||||
.opts .hint { font-style: normal; opacity: .75; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
/* Tabela porównania systemów domów ma tyle kolumn, ile zaznaczono systemów —
|
||||
przy komplecie trzynastu robi się 1520 px i ROZPYCHAŁA CAŁĄ STRONĘ (dokument
|
||||
1713 px przy oknie 1280 px, poziomy pasek przewijania na body). Przewijać ma
|
||||
się sama tabela, nigdy strona. */
|
||||
.table-scroll { overflow-x: auto; margin-top: .5rem; border-radius: 12px; }
|
||||
.table-scroll > table { margin-top: 0; border-radius: 0; }
|
||||
.table-scroll th, .table-scroll td { white-space: nowrap; }
|
||||
/* Numer domu przyklejony do lewej krawędzi: bez tego po przewinięciu w prawo
|
||||
nie wiadomo, który to wiersz. */
|
||||
.table-scroll th:first-child, .table-scroll td:first-child {
|
||||
position: sticky; left: 0; background: var(--panel); z-index: 1;
|
||||
box-shadow: 1px 0 0 var(--line);
|
||||
}
|
||||
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
|
||||
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
|
||||
/* Wyszukiwarka lokalizacji + mapa (OSM/Leaflet) */
|
||||
.geo { margin-top: .9rem; }
|
||||
.geo-search { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||||
.geo-search input[type=search] { flex: 1; min-width: 12rem; }
|
||||
.geo-results { list-style: none; margin: .4rem 0 0; padding: 0; border: 1px solid var(--line);
|
||||
border-radius: 8px; overflow: hidden; background: #12132a; max-height: 14rem; overflow-y: auto; }
|
||||
.geo-results li { padding: .5rem .75rem; cursor: pointer; border-bottom: 1px solid var(--line); font-size: .9rem; }
|
||||
.geo-results li:last-child { border-bottom: none; }
|
||||
.geo-results li:hover { background: var(--panel); color: #fff; }
|
||||
.geo-map { height: 320px; margin-top: .6rem; border: 1px solid var(--line); border-radius: 10px; }
|
||||
.geo-map[hidden] { display: none; }
|
||||
.geo-attrib { margin: .4rem 0 0; }
|
||||
.geo-attrib a { color: var(--accent); }
|
||||
.geo-pin { font-size: 20px; line-height: 24px; text-align: center; }
|
||||
/* kafelki OSM są jasne — trzymamy kontrolki czytelne na ciemnym tle */
|
||||
.leaflet-control-attribution { font-size: .68rem; }
|
||||
button.toggle {
|
||||
background: none; border: 1px solid var(--line); border-radius: 50%;
|
||||
width: 1.9rem; height: 1.9rem; padding: 0; font-size: 1rem; line-height: 1;
|
||||
}
|
||||
button.toggle.on { color: var(--accent); border-color: var(--accent); }
|
||||
button.toggle.off { color: var(--muted); }
|
||||
@@ -0,0 +1,114 @@
|
||||
/* Arkusz ekranu „compile” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.sub { color: var(--muted); margin: .25rem 0 2rem; }
|
||||
.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; }
|
||||
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; }
|
||||
/* Glify astrologiczne (LOG-22). Trzy warstwy obrony przed prezentacją emoji
|
||||
(DAN-18: kolorowe kwadraty są nieczytelne i niesterowalne kolorem):
|
||||
1) selektor wariantu U+FE0E w samych danych (glyphs.py),
|
||||
2) font-variant-emoji:text — nowsza własność CSS,
|
||||
3) font-family celujący w MONOCHROMATYCZNE fonty symboli PRZED jakimkolwiek
|
||||
fontem emoji. Bez tego macOS/Chromium i tak sięga po Apple Color Emoji dla
|
||||
znaków zodiaku (♈–♓), mimo VS15 — potwierdzone wizualnie. „Apple Symbols",
|
||||
„Segoe UI Symbol", „Noto Sans Symbols2" mają te glify w wersji tekstowej. */
|
||||
.glyph { font-size: 1.15em; line-height: 1; font-variant-emoji: text;
|
||||
font-family: "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols2",
|
||||
"DejaVu Sans", sans-serif; }
|
||||
td.glyph { color: var(--accent); white-space: nowrap; }
|
||||
/* Kosmogram (PRE-12) — SVG skaluje się do szerokości, ale nie rośnie w nieskończoność. */
|
||||
.wheel-fig { margin: 1.5rem 0 .5rem; text-align: center; }
|
||||
.wheel-fig svg.wheel { width: 100%; max-width: 460px; height: auto; }
|
||||
.wheel-fig figcaption { margin-top: .4rem; }
|
||||
/* glify wewnątrz SVG dziedziczą monochromatyczny stack fontów (DAN-18) */
|
||||
svg.wheel text.glyph { font-size: inherit; }
|
||||
/* Aspektarian (PRE-18) — trójkątna siatka aspektów, węższa od koła. */
|
||||
.aspectarian-fig { margin: .25rem 0 1.25rem; text-align: center; }
|
||||
.aspectarian-fig svg.aspectarian { width: 100%; max-width: 380px; height: auto; }
|
||||
.aspectarian-fig figcaption { margin-top: .4rem; }
|
||||
svg.aspectarian text.glyph { font-size: inherit; }
|
||||
/* Wykres deklinacji i oś antyscji (LOG-07, etap 6) — szersze, pełna szerokość. */
|
||||
.declination-fig, .antiscia-fig { margin: 1.25rem 0 .75rem; text-align: center; }
|
||||
.declination-fig svg.declination { width: 100%; max-width: 560px; height: auto; }
|
||||
.antiscia-fig svg.antiscia { width: 100%; max-width: 560px; height: auto; }
|
||||
.declination-fig figcaption, .antiscia-fig figcaption { margin-top: .4rem; }
|
||||
svg.declination text.glyph, svg.antiscia text.glyph { font-size: inherit; }
|
||||
/* Powiększenie na pełne okno (PRE-25). Kliknięcie rozciąga wykres na cały ekran,
|
||||
ponowne wraca. z-index 1100 CELOWO pomiędzy: ponad kontrolki Leafleta (1000),
|
||||
ale PONIŻEJ okna postępu (1200) — gdy trwa pisanie horoskopu, log operacji ma
|
||||
zostać na wierzchu. */
|
||||
.wheel-fig svg.wheel { cursor: zoom-in; }
|
||||
.wheel-fig svg.wheel:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
||||
.wheel-fig.zoomed {
|
||||
position: fixed; inset: 0; z-index: 1100; margin: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: .5rem; padding: 1rem; background: var(--bg);
|
||||
}
|
||||
/* min() po obu wymiarach: koło jest kwadratowe, więc ograniczamy je krótszym
|
||||
bokiem okna — inaczej na szerokim ekranie wystawałoby w pionie. */
|
||||
.wheel-fig.zoomed svg.wheel {
|
||||
max-width: min(96vw, 92vh); width: min(96vw, 92vh); height: auto; cursor: zoom-out;
|
||||
}
|
||||
body.wheel-zoom-open { overflow: hidden; }
|
||||
/* nie przewijaj treści pod spodem */
|
||||
/* Dymek kosmogramu. z-index 1150 CELOWO pomiędzy: ponad nakładką powiększonego
|
||||
koła (1100), żeby działał także po powiększeniu, ale PONIŻEJ okna postępu
|
||||
(1200), które ma zostać na wierzchu. pointer-events: none — dymek chodzi za
|
||||
kursorem i nie może sam siebie zasłonić ani przechwycić najechania. */
|
||||
.wheel-tip {
|
||||
position: fixed; z-index: 1150; pointer-events: none;
|
||||
max-width: 22rem; padding: .4rem .6rem;
|
||||
background: var(--panel); color: var(--fg, inherit);
|
||||
border: 1px solid var(--line); border-radius: 8px;
|
||||
font-size: .85rem; line-height: 1.35;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
|
||||
}
|
||||
.wheel-tip[hidden] { display: none; }
|
||||
/* Lista systemów domów urosła z 3 do 13 pozycji. Na flexie zawijały się w cztery
|
||||
poszarpane wiersze o różnej długości; siatka o stałej szerokości kolumny ustawia
|
||||
nazwy w pionie i daje się przeczytać wzrokiem. auto-fill, więc na wąskim ekranie
|
||||
kolumn jest po prostu mniej. */
|
||||
.opts.systems { display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
|
||||
gap: .35rem 1rem; align-items: start; }
|
||||
.opts.systems > .opts-label { grid-column: 1 / -1; margin-bottom: .1rem; }
|
||||
.opts.systems label { display: flex; align-items: center; gap: .35rem; }
|
||||
.opts .hint { font-style: normal; opacity: .75; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
/* Tabela porównania systemów domów ma tyle kolumn, ile zaznaczono systemów —
|
||||
przy komplecie trzynastu robi się 1520 px i ROZPYCHAŁA CAŁĄ STRONĘ (dokument
|
||||
1713 px przy oknie 1280 px, poziomy pasek przewijania na body). Przewijać ma
|
||||
się sama tabela, nigdy strona. */
|
||||
.table-scroll { overflow-x: auto; margin-top: .5rem; border-radius: 12px; }
|
||||
.table-scroll > table { margin-top: 0; border-radius: 0; }
|
||||
.table-scroll th, .table-scroll td { white-space: nowrap; }
|
||||
/* Numer domu przyklejony do lewej krawędzi: bez tego po przewinięciu w prawo
|
||||
nie wiadomo, który to wiersz. */
|
||||
.table-scroll th:first-child, .table-scroll td:first-child {
|
||||
position: sticky; left: 0; background: var(--panel); z-index: 1;
|
||||
box-shadow: 1px 0 0 var(--line);
|
||||
}
|
||||
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
|
||||
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
|
||||
/* Zakładka „Skompiluj" (PRE-23) — składanie raportu z trzech źródeł. */
|
||||
.readiness { list-style: none; margin: .3rem 0 1rem; padding: 0; }
|
||||
.readiness li { padding: .2rem 0; }
|
||||
.readiness .rdy-ok { color: #7fd18b; }
|
||||
.readiness .rdy-missing { color: #e0c060; }
|
||||
.report-head { margin: 1.5rem 0 .5rem; }
|
||||
.report-head h2 { margin: 0; font-size: 1.4rem; }
|
||||
/* Treść od AI bywa długa — white-space:pre-wrap zachowuje akapity modelu,
|
||||
bez przepisywania ich na HTML. */
|
||||
.report-text { white-space: pre-wrap; background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: .8rem 1rem; margin-top: .4rem; line-height: 1.6; }
|
||||
button.toggle {
|
||||
background: none; border: 1px solid var(--line); border-radius: 50%;
|
||||
width: 1.9rem; height: 1.9rem; padding: 0; font-size: 1rem; line-height: 1;
|
||||
}
|
||||
button.toggle.on { color: var(--accent); border-color: var(--accent); }
|
||||
button.toggle.off { color: var(--muted); }
|
||||
@@ -0,0 +1,36 @@
|
||||
/* Arkusz ekranu „files” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.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%; }
|
||||
button.ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
/* Tabela porównania systemów domów ma tyle kolumn, ile zaznaczono systemów —
|
||||
przy komplecie trzynastu robi się 1520 px i ROZPYCHAŁA CAŁĄ STRONĘ (dokument
|
||||
1713 px przy oknie 1280 px, poziomy pasek przewijania na body). Przewijać ma
|
||||
się sama tabela, nigdy strona. */
|
||||
.table-scroll { overflow-x: auto; margin-top: .5rem; border-radius: 12px; }
|
||||
.table-scroll > table { margin-top: 0; border-radius: 0; }
|
||||
.table-scroll th, .table-scroll td { white-space: nowrap; }
|
||||
/* Numer domu przyklejony do lewej krawędzi: bez tego po przewinięciu w prawo
|
||||
nie wiadomo, który to wiersz. */
|
||||
.table-scroll th:first-child, .table-scroll td:first-child {
|
||||
position: sticky; left: 0; background: var(--panel); z-index: 1;
|
||||
box-shadow: 1px 0 0 var(--line);
|
||||
}
|
||||
button.danger { background: #8b2f2f; }
|
||||
/* ── ekran plików (DAN-27) ──────────────────────────────────────────── */
|
||||
form.inline { display: inline; }
|
||||
td.ops form.inline + form.inline { margin-left: .4rem; }
|
||||
button.toggle {
|
||||
background: none; border: 1px solid var(--line); border-radius: 50%;
|
||||
width: 1.9rem; height: 1.9rem; padding: 0; font-size: 1rem; line-height: 1;
|
||||
}
|
||||
button.toggle.on { color: var(--accent); border-color: var(--accent); }
|
||||
button.toggle.off { color: var(--muted); }
|
||||
tr.row-archived td { opacity: .55; }
|
||||
/* Wstrzymane widzi tylko administrator — dla reszty tych wierszy nie ma
|
||||
w ogóle w odpowiedzi, więc ten styl nigdy nie dotyczy ich strony. */
|
||||
tr.row-held td { background: rgba(184, 134, 11, .07); }
|
||||
@@ -0,0 +1,42 @@
|
||||
/* Arkusz ekranu „interpret” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.sub { color: var(--muted); margin: .25rem 0 2rem; }
|
||||
.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%; }
|
||||
button.ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); }
|
||||
td.retro { color: #ff9b6a; font-weight: 600; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
.sig-item { margin-top: 1.25rem; }
|
||||
.sig-head { padding: .5rem 0; }
|
||||
.samples { margin-top: .3rem; }
|
||||
.samples td { font-size: .92rem; vertical-align: top; }
|
||||
.samples td.nowrap { color: var(--accent); padding-right: 1rem; }
|
||||
.facet { margin: .4rem 0 .6rem 1rem; }
|
||||
.facet-head { color: var(--ink); font-size: .9rem; }
|
||||
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
|
||||
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
|
||||
.samples td.sig { color: var(--accent); padding-right: 1rem; cursor: help; }
|
||||
/* Wyszukiwarka lokalizacji + mapa (OSM/Leaflet) */
|
||||
.geo { margin-top: .9rem; }
|
||||
.geo-search { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||||
.geo-search input[type=search] { flex: 1; min-width: 12rem; }
|
||||
.geo-results { list-style: none; margin: .4rem 0 0; padding: 0; border: 1px solid var(--line);
|
||||
border-radius: 8px; overflow: hidden; background: #12132a; max-height: 14rem; overflow-y: auto; }
|
||||
.geo-results li { padding: .5rem .75rem; cursor: pointer; border-bottom: 1px solid var(--line); font-size: .9rem; }
|
||||
.geo-results li:last-child { border-bottom: none; }
|
||||
.geo-results li:hover { background: var(--panel); color: #fff; }
|
||||
.geo-map { height: 320px; margin-top: .6rem; border: 1px solid var(--line); border-radius: 10px; }
|
||||
.geo-map[hidden] { display: none; }
|
||||
.geo-attrib { margin: .4rem 0 0; }
|
||||
.geo-attrib a { color: var(--accent); }
|
||||
.geo-pin { font-size: 20px; line-height: 24px; text-align: center; }
|
||||
/* kafelki OSM są jasne — trzymamy kontrolki czytelne na ciemnym tle */
|
||||
.leaflet-control-attribution { font-size: .68rem; }
|
||||
/* Generator promptu do LLM (LOG-29/30) */
|
||||
textarea.prompt { width: 100%; margin-top: .5rem; padding: .7rem .8rem; box-sizing: border-box;
|
||||
background: #12132a; color: var(--ink); border: 1px solid var(--line);
|
||||
border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: .82rem; line-height: 1.45; resize: vertical; white-space: pre; }
|
||||
textarea.prompt:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Arkusz ekranu „settings” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.sub { color: var(--muted); margin: .25rem 0 2rem; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
|
||||
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Arkusz ekranu „significators” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.sub { color: var(--muted); margin: .25rem 0 2rem; }
|
||||
.row { display: flex; gap: .5rem; }
|
||||
.row input[type=text] { flex: 1; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
.empty { color: var(--muted); }
|
||||
@@ -0,0 +1,23 @@
|
||||
/* Arkusz ekranu „synastry” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.sub { color: var(--muted); margin: .25rem 0 2rem; }
|
||||
.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%; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; }
|
||||
td.retro { color: #ff9b6a; font-weight: 600; }
|
||||
/* Glify astrologiczne (LOG-22). Trzy warstwy obrony przed prezentacją emoji
|
||||
(DAN-18: kolorowe kwadraty są nieczytelne i niesterowalne kolorem):
|
||||
1) selektor wariantu U+FE0E w samych danych (glyphs.py),
|
||||
2) font-variant-emoji:text — nowsza własność CSS,
|
||||
3) font-family celujący w MONOCHROMATYCZNE fonty symboli PRZED jakimkolwiek
|
||||
fontem emoji. Bez tego macOS/Chromium i tak sięga po Apple Color Emoji dla
|
||||
znaków zodiaku (♈–♓), mimo VS15 — potwierdzone wizualnie. „Apple Symbols",
|
||||
„Segoe UI Symbol", „Noto Sans Symbols2" mają te glify w wersji tekstowej. */
|
||||
.glyph { font-size: 1.15em; line-height: 1; font-variant-emoji: text;
|
||||
font-family: "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols2",
|
||||
"DejaVu Sans", sans-serif; }
|
||||
td.glyph { color: var(--accent); white-space: nowrap; }
|
||||
svg.aspectarian text.glyph { font-size: inherit; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
@@ -0,0 +1,39 @@
|
||||
/* Arkusz ekranu „timeline” — dokładany tylko dla konta z tym uprawnieniem. */
|
||||
|
||||
.sub { color: var(--muted); margin: .25rem 0 2rem; }
|
||||
.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%; }
|
||||
button.ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); }
|
||||
td.retro { color: #ff9b6a; font-weight: 600; }
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
.sig-item { margin-top: 1.25rem; }
|
||||
.sig-head { padding: .5rem 0; }
|
||||
.samples { margin-top: .3rem; }
|
||||
.samples td { font-size: .92rem; vertical-align: top; }
|
||||
.samples td.nowrap { color: var(--accent); padding-right: 1rem; }
|
||||
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
|
||||
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
|
||||
.samples td.sig { color: var(--accent); padding-right: 1rem; cursor: help; }
|
||||
/* Wyszukiwarka lokalizacji + mapa (OSM/Leaflet) */
|
||||
.geo { margin-top: .9rem; }
|
||||
.geo-search { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||||
.geo-search input[type=search] { flex: 1; min-width: 12rem; }
|
||||
.geo-results { list-style: none; margin: .4rem 0 0; padding: 0; border: 1px solid var(--line);
|
||||
border-radius: 8px; overflow: hidden; background: #12132a; max-height: 14rem; overflow-y: auto; }
|
||||
.geo-results li { padding: .5rem .75rem; cursor: pointer; border-bottom: 1px solid var(--line); font-size: .9rem; }
|
||||
.geo-results li:last-child { border-bottom: none; }
|
||||
.geo-results li:hover { background: var(--panel); color: #fff; }
|
||||
.geo-map { height: 320px; margin-top: .6rem; border: 1px solid var(--line); border-radius: 10px; }
|
||||
.geo-map[hidden] { display: none; }
|
||||
.geo-attrib { margin: .4rem 0 0; }
|
||||
.geo-attrib a { color: var(--accent); }
|
||||
.geo-pin { font-size: 20px; line-height: 24px; text-align: center; }
|
||||
/* kafelki OSM są jasne — trzymamy kontrolki czytelne na ciemnym tle */
|
||||
.leaflet-control-attribution { font-size: .68rem; }
|
||||
/* Generator promptu do LLM (LOG-29/30) */
|
||||
textarea.prompt { width: 100%; margin-top: .5rem; padding: .7rem .8rem; box-sizing: border-box;
|
||||
background: #12132a; color: var(--ink); border: 1px solid var(--line);
|
||||
border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: .82rem; line-height: 1.45; resize: vertical; white-space: pre; }
|
||||
textarea.prompt:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
@@ -1,287 +0,0 @@
|
||||
:root {
|
||||
--bg: #0f1020;
|
||||
--panel: #1a1c33;
|
||||
--ink: #e8e8f0;
|
||||
--muted: #9aa0c0;
|
||||
--accent: #8b7bf0;
|
||||
--line: #2a2d4a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; background: var(--bg); color: var(--ink);
|
||||
font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
display: flex; justify-content: center; padding: 3rem 1rem;
|
||||
}
|
||||
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; }
|
||||
/* Glify astrologiczne (LOG-22). Trzy warstwy obrony przed prezentacją emoji
|
||||
(DAN-18: kolorowe kwadraty są nieczytelne i niesterowalne kolorem):
|
||||
1) selektor wariantu U+FE0E w samych danych (glyphs.py),
|
||||
2) font-variant-emoji:text — nowsza własność CSS,
|
||||
3) font-family celujący w MONOCHROMATYCZNE fonty symboli PRZED jakimkolwiek
|
||||
fontem emoji. Bez tego macOS/Chromium i tak sięga po Apple Color Emoji dla
|
||||
znaków zodiaku (♈–♓), mimo VS15 — potwierdzone wizualnie. „Apple Symbols",
|
||||
„Segoe UI Symbol", „Noto Sans Symbols2" mają te glify w wersji tekstowej. */
|
||||
.glyph { font-size: 1.15em; line-height: 1; font-variant-emoji: text;
|
||||
font-family: "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols2",
|
||||
"DejaVu Sans", sans-serif; }
|
||||
td.glyph { color: var(--accent); white-space: nowrap; }
|
||||
/* Kosmogram (PRE-12) — SVG skaluje się do szerokości, ale nie rośnie w nieskończoność. */
|
||||
.wheel-fig { margin: 1.5rem 0 .5rem; text-align: center; }
|
||||
.wheel-fig svg.wheel { width: 100%; max-width: 460px; height: auto; }
|
||||
.wheel-fig figcaption { margin-top: .4rem; }
|
||||
/* glify wewnątrz SVG dziedziczą monochromatyczny stack fontów (DAN-18) */
|
||||
svg.wheel text.glyph { font-size: inherit; }
|
||||
|
||||
/* Aspektarian (PRE-18) — trójkątna siatka aspektów, węższa od koła. */
|
||||
.aspectarian-fig { margin: .25rem 0 1.25rem; text-align: center; }
|
||||
.aspectarian-fig svg.aspectarian { width: 100%; max-width: 380px; height: auto; }
|
||||
.aspectarian-fig figcaption { margin-top: .4rem; }
|
||||
svg.aspectarian text.glyph { font-size: inherit; }
|
||||
|
||||
/* Wykres deklinacji i oś antyscji (LOG-07, etap 6) — szersze, pełna szerokość. */
|
||||
.declination-fig, .antiscia-fig { margin: 1.25rem 0 .75rem; text-align: center; }
|
||||
.declination-fig svg.declination { width: 100%; max-width: 560px; height: auto; }
|
||||
.antiscia-fig svg.antiscia { width: 100%; max-width: 560px; height: auto; }
|
||||
.declination-fig figcaption, .antiscia-fig figcaption { margin-top: .4rem; }
|
||||
svg.declination text.glyph, svg.antiscia text.glyph { font-size: inherit; }
|
||||
|
||||
/* Powiększenie na pełne okno (PRE-25). Kliknięcie rozciąga wykres na cały ekran,
|
||||
ponowne wraca. z-index 1100 CELOWO pomiędzy: ponad kontrolki Leafleta (1000),
|
||||
ale PONIŻEJ okna postępu (1200) — gdy trwa pisanie horoskopu, log operacji ma
|
||||
zostać na wierzchu. */
|
||||
.wheel-fig svg.wheel { cursor: zoom-in; }
|
||||
.wheel-fig svg.wheel:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
||||
.wheel-fig.zoomed {
|
||||
position: fixed; inset: 0; z-index: 1100; margin: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: .5rem; padding: 1rem; background: var(--bg);
|
||||
}
|
||||
/* min() po obu wymiarach: koło jest kwadratowe, więc ograniczamy je krótszym
|
||||
bokiem okna — inaczej na szerokim ekranie wystawałoby w pionie. */
|
||||
.wheel-fig.zoomed svg.wheel {
|
||||
max-width: min(96vw, 92vh); width: min(96vw, 92vh); height: auto; cursor: zoom-out;
|
||||
}
|
||||
body.wheel-zoom-open { overflow: hidden; } /* nie przewijaj treści pod spodem */
|
||||
|
||||
/* Dymek kosmogramu. z-index 1150 CELOWO pomiędzy: ponad nakładką powiększonego
|
||||
koła (1100), żeby działał także po powiększeniu, ale PONIŻEJ okna postępu
|
||||
(1200), które ma zostać na wierzchu. pointer-events: none — dymek chodzi za
|
||||
kursorem i nie może sam siebie zasłonić ani przechwycić najechania. */
|
||||
.wheel-tip {
|
||||
position: fixed; z-index: 1150; pointer-events: none;
|
||||
max-width: 22rem; padding: .4rem .6rem;
|
||||
background: var(--panel); color: var(--fg, inherit);
|
||||
border: 1px solid var(--line); border-radius: 8px;
|
||||
font-size: .85rem; line-height: 1.35;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
|
||||
}
|
||||
.wheel-tip[hidden] { display: none; }
|
||||
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; }
|
||||
input, select, button {
|
||||
font: inherit; padding: .6rem .75rem; border-radius: 8px;
|
||||
border: 1px solid var(--line); background: #12132a; color: var(--ink);
|
||||
}
|
||||
button { background: var(--accent); color: #fff; border: none; cursor: pointer; padding-inline: 1.25rem; }
|
||||
button:hover { filter: brightness(1.1); }
|
||||
.opts { display: flex; gap: 1.5rem; margin-top: .75rem; color: var(--muted); align-items: center; }
|
||||
|
||||
/* Lista systemów domów urosła z 3 do 13 pozycji. Na flexie zawijały się w cztery
|
||||
poszarpane wiersze o różnej długości; siatka o stałej szerokości kolumny ustawia
|
||||
nazwy w pionie i daje się przeczytać wzrokiem. auto-fill, więc na wąskim ekranie
|
||||
kolumn jest po prostu mniej. */
|
||||
.opts.systems { display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
|
||||
gap: .35rem 1rem; align-items: start; }
|
||||
.opts.systems > .opts-label { grid-column: 1 / -1; margin-bottom: .1rem; }
|
||||
.opts.systems label { display: flex; align-items: center; gap: .35rem; }
|
||||
.opts .hint { font-style: normal; opacity: .75; }
|
||||
.opts input[type=number] { width: 5rem; }
|
||||
.meta { color: var(--muted); margin: 1.5rem 0 .5rem; font-size: .9rem; }
|
||||
|
||||
/* Fallback systemu domów. Celowo NIE jest to dyskretny „muted" — po fallbacku
|
||||
kosmogram wygląda bezbłędnie, więc ten akapit jest jedynym sygnałem, że
|
||||
podział domów pochodzi z innego systemu, niż zamówiono. */
|
||||
.house-warning {
|
||||
margin: 1rem 0; padding: .75rem 1rem; font-size: .92rem; line-height: 1.45;
|
||||
border: 1px solid var(--warn-border, #b8860b);
|
||||
border-left-width: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--warn-bg, rgba(184, 134, 11, .10));
|
||||
color: var(--warn-fg, inherit);
|
||||
}
|
||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||
.empty { color: var(--muted); }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: .5rem; background: var(--panel); border-radius: 12px; overflow: hidden; }
|
||||
|
||||
/* Tabela porównania systemów domów ma tyle kolumn, ile zaznaczono systemów —
|
||||
przy komplecie trzynastu robi się 1520 px i ROZPYCHAŁA CAŁĄ STRONĘ (dokument
|
||||
1713 px przy oknie 1280 px, poziomy pasek przewijania na body). Przewijać ma
|
||||
się sama tabela, nigdy strona. */
|
||||
.table-scroll { overflow-x: auto; margin-top: .5rem; border-radius: 12px; }
|
||||
.table-scroll > table { margin-top: 0; border-radius: 0; }
|
||||
.table-scroll th, .table-scroll td { white-space: nowrap; }
|
||||
|
||||
/* Numer domu przyklejony do lewej krawędzi: bez tego po przewinięciu w prawo
|
||||
nie wiadomo, który to wiersz. */
|
||||
.table-scroll th:first-child, .table-scroll td:first-child {
|
||||
position: sticky; left: 0; background: var(--panel); z-index: 1;
|
||||
box-shadow: 1px 0 0 var(--line);
|
||||
}
|
||||
th, td { text-align: left; padding: .6rem .8rem; border-bottom: 1px solid var(--line); }
|
||||
th { color: var(--accent); font-size: .8rem; text-transform: uppercase; letter-spacing: .5px; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
|
||||
.muted { color: var(--muted); }
|
||||
.small { font-size: .85rem; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
.sig-item { margin-top: 1.25rem; }
|
||||
.sig-head { padding: .5rem 0; }
|
||||
.samples { margin-top: .3rem; }
|
||||
.samples td { font-size: .92rem; vertical-align: top; }
|
||||
.samples td.nowrap { color: var(--accent); padding-right: 1rem; }
|
||||
.facet { margin: .4rem 0 .6rem 1rem; }
|
||||
.facet-head { color: var(--ink); font-size: .9rem; }
|
||||
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
|
||||
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
|
||||
.samples td.sig { color: var(--accent); padding-right: 1rem; cursor: help; }
|
||||
|
||||
/* Wyszukiwarka lokalizacji + mapa (OSM/Leaflet) */
|
||||
.geo { margin-top: .9rem; }
|
||||
.geo-search { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||||
.geo-search input[type=search] { flex: 1; min-width: 12rem; }
|
||||
.geo-results { list-style: none; margin: .4rem 0 0; padding: 0; border: 1px solid var(--line);
|
||||
border-radius: 8px; overflow: hidden; background: #12132a; max-height: 14rem; overflow-y: auto; }
|
||||
.geo-results li { padding: .5rem .75rem; cursor: pointer; border-bottom: 1px solid var(--line); font-size: .9rem; }
|
||||
.geo-results li:last-child { border-bottom: none; }
|
||||
.geo-results li:hover { background: var(--panel); color: #fff; }
|
||||
.geo-map { height: 320px; margin-top: .6rem; border: 1px solid var(--line); border-radius: 10px; }
|
||||
.geo-map[hidden] { display: none; }
|
||||
.geo-attrib { margin: .4rem 0 0; }
|
||||
.geo-attrib a { color: var(--accent); }
|
||||
.geo-pin { font-size: 20px; line-height: 24px; text-align: center; }
|
||||
/* kafelki OSM są jasne — trzymamy kontrolki czytelne na ciemnym tle */
|
||||
.leaflet-control-attribution { font-size: .68rem; }
|
||||
|
||||
/* Generator promptu do LLM (LOG-29/30) */
|
||||
textarea.prompt { width: 100%; margin-top: .5rem; padding: .7rem .8rem; box-sizing: border-box;
|
||||
background: #12132a; color: var(--ink); border: 1px solid var(--line);
|
||||
border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: .82rem; line-height: 1.45; resize: vertical; white-space: pre; }
|
||||
textarea.prompt:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
|
||||
/* Okno postępu przy generowaniu horoskopu */
|
||||
/* z-index ponad Leaflet: jego kontrolki (.leaflet-top/.leaflet-bottom — zoom,
|
||||
atrybucja) siedzą na z-index:1000 i — bo .leaflet-container nie tworzy własnego
|
||||
kontekstu stackowania — trafiają wprost do korzenia. Przy z-index:50 mapa
|
||||
wychodziła NA WIERZCH modala. 1200 daje zapas nad 1000. */
|
||||
.progress-overlay { position: fixed; inset: 0; background: rgba(8,9,20,.72);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 1200; padding: 1rem; }
|
||||
.progress-overlay[hidden] { display: none; }
|
||||
.progress-box { background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
|
||||
padding: 1rem 1.1rem; width: min(38rem, 100%); box-shadow: 0 18px 48px rgba(0,0,0,.45); }
|
||||
.progress-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; }
|
||||
.progress-clock { margin-left: auto; font-family: ui-monospace, Menlo, monospace;
|
||||
color: var(--muted); font-size: .9rem; }
|
||||
.progress-spinner { width: 14px; height: 14px; border-radius: 50%; flex: none;
|
||||
border: 2px solid var(--line); border-top-color: var(--accent);
|
||||
animation: spin .8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .progress-spinner { animation: none; } }
|
||||
.progress-log { list-style: none; margin: 0 0 .6rem; padding: .6rem .7rem;
|
||||
background: #12132a; border: 1px solid var(--line); border-radius: 10px;
|
||||
max-height: 15rem; overflow-y: auto;
|
||||
font-family: ui-monospace, Menlo, monospace; font-size: .78rem; line-height: 1.6; }
|
||||
.progress-log li { color: var(--ink); white-space: pre-wrap; }
|
||||
.progress-log li.log-ok { color: #7fd18b; }
|
||||
.progress-log li.log-warn { color: #e0c060; }
|
||||
.progress-log li.log-err { color: #ef6b6b; }
|
||||
|
||||
/* Zakładka „Skompiluj" (PRE-23) — składanie raportu z trzech źródeł. */
|
||||
.readiness { list-style: none; margin: .3rem 0 1rem; padding: 0; }
|
||||
.readiness li { padding: .2rem 0; }
|
||||
.readiness .rdy-ok { color: #7fd18b; }
|
||||
.readiness .rdy-missing { color: #e0c060; }
|
||||
.report-head { margin: 1.5rem 0 .5rem; }
|
||||
.report-head h2 { margin: 0; font-size: 1.4rem; }
|
||||
/* Treść od AI bywa długa — white-space:pre-wrap zachowuje akapity modelu,
|
||||
bez przepisywania ich na HTML. */
|
||||
.report-text { white-space: pre-wrap; background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: .8rem 1rem; margin-top: .4rem; line-height: 1.6; }
|
||||
|
||||
|
||||
/* ── ekran kont (PRE-26) ────────────────────────────────────────────── */
|
||||
.account-card {
|
||||
margin-top: 1rem; padding: 1rem 1.25rem;
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
||||
}
|
||||
.account-head { display: flex; align-items: baseline; gap: .75rem; margin-bottom: .5rem; }
|
||||
.account-note input { width: 100%; }
|
||||
.account-card .actions { margin-top: .9rem; }
|
||||
button.danger { background: #8b2f2f; }
|
||||
|
||||
/* Potwierdzenie operacji. Odróżnia się od .house-warning, bo tamto ostrzega,
|
||||
a to tylko potwierdza — pomylenie ich uczy ignorować oba. */
|
||||
.done-note {
|
||||
margin: 1rem 0; padding: .6rem .9rem; font-size: .9rem;
|
||||
border: 1px solid var(--line); border-left: 4px solid var(--accent);
|
||||
border-radius: 4px; background: rgba(255, 255, 255, .03);
|
||||
}
|
||||
|
||||
|
||||
/* ── ekran plików (DAN-27) ──────────────────────────────────────────── */
|
||||
form.inline { display: inline; }
|
||||
td.ops form.inline + form.inline { margin-left: .4rem; }
|
||||
button.toggle {
|
||||
background: none; border: 1px solid var(--line); border-radius: 50%;
|
||||
width: 1.9rem; height: 1.9rem; padding: 0; font-size: 1rem; line-height: 1;
|
||||
}
|
||||
button.toggle.on { color: var(--accent); border-color: var(--accent); }
|
||||
button.toggle.off { color: var(--muted); }
|
||||
tr.row-archived td { opacity: .55; }
|
||||
/* Wstrzymane widzi tylko administrator — dla reszty tych wierszy nie ma
|
||||
w ogóle w odpowiedzi, więc ten styl nigdy nie dotyczy ich strony. */
|
||||
tr.row-held td { background: rgba(184, 134, 11, .07); }
|
||||
|
||||
|
||||
/* ── wyjście z aplikacji (LOG-32) ───────────────────────────────────── */
|
||||
/* Odsunięte od zakładek i wyglądające inaczej niż one: to czynność, nie miejsce,
|
||||
a pomylenie go z zakładką kosztuje przypadkowe wylogowania. Odstęp robimy
|
||||
marginesem, nie rozpychaczem — `nav` ma szerokość treści, więc nie ma czego
|
||||
rozpychać. */
|
||||
.whoami { color: var(--muted); font-size: .85rem; align-self: center;
|
||||
margin-left: 1rem; padding-left: 1rem; border-left: 1px solid var(--line); }
|
||||
/* Wylogowanie idzie POST-em, więc jest przyciskiem — ale ma wyglądać jak
|
||||
reszta paska, nie jak przycisk akcji w formularzu. */
|
||||
.wyloguj-form { display: inline; margin: 0; padding: 0; background: none; border: none; }
|
||||
button.wyloguj {
|
||||
background: none; border: 1px solid transparent; color: var(--muted);
|
||||
font: inherit; padding: .4rem .8rem; border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
button.wyloguj:hover { color: var(--ink); }
|
||||
|
||||
/* Ekran logowania — wąski, bez nawigacji: nie ma dokąd iść przed zalogowaniem. */
|
||||
.login-main { max-width: 26rem; }
|
||||
.login-card label { display: block; margin-top: .75rem; }
|
||||
.login-card input { width: 100%; }
|
||||
.login-note { margin-top: 1.25rem; }
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Generowanie tekstu przez model — dokładany wyłącznie, gdy konto ma
|
||||
uprawnienie „ai”. Selektory nazywają funkcję, więc arkusz nie może
|
||||
wchodzić do arkuszy ekranów, na których ta funkcja bywa wyłączona. */
|
||||
|
||||
/* Okno postępu przy generowaniu horoskopu */
|
||||
/* z-index ponad Leaflet: jego kontrolki (.leaflet-top/.leaflet-bottom — zoom,
|
||||
atrybucja) siedzą na z-index:1000 i — bo .leaflet-container nie tworzy własnego
|
||||
kontekstu stackowania — trafiają wprost do korzenia. Przy z-index:50 mapa
|
||||
wychodziła NA WIERZCH modala. 1200 daje zapas nad 1000. */
|
||||
.progress-overlay { position: fixed; inset: 0; background: rgba(8,9,20,.72);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 1200; padding: 1rem; }
|
||||
.progress-overlay[hidden] { display: none; }
|
||||
.progress-box { background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
|
||||
padding: 1rem 1.1rem; width: min(38rem, 100%); box-shadow: 0 18px 48px rgba(0,0,0,.45); }
|
||||
.progress-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; }
|
||||
.progress-clock { margin-left: auto; font-family: ui-monospace, Menlo, monospace;
|
||||
color: var(--muted); font-size: .9rem; }
|
||||
.progress-spinner { width: 14px; height: 14px; border-radius: 50%; flex: none;
|
||||
border: 2px solid var(--line); border-top-color: var(--accent);
|
||||
animation: spin .8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .progress-spinner { animation: none; } }
|
||||
.progress-log { list-style: none; margin: 0 0 .6rem; padding: .6rem .7rem;
|
||||
background: #12132a; border: 1px solid var(--line); border-radius: 10px;
|
||||
max-height: 15rem; overflow-y: auto;
|
||||
font-family: ui-monospace, Menlo, monospace; font-size: .78rem; line-height: 1.6; }
|
||||
.progress-log li { color: var(--ink); white-space: pre-wrap; }
|
||||
.progress-log li.log-ok { color: #7fd18b; }
|
||||
.progress-log li.log-warn { color: #e0c060; }
|
||||
.progress-log li.log-err { color: #ef6b6b; }
|
||||
@@ -10,7 +10,7 @@
|
||||
inny. Musi być WIDOCZNE, bo z samego wykresu tego nie da się poznać. #}
|
||||
{% if result.house_warnings %}
|
||||
{% for w in result.house_warnings %}
|
||||
<p class="house-warning">{{ w }}</p>
|
||||
<p class="notice-warn">{{ w }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
{% set nav_active = "accounts" %}
|
||||
{% block title %}Konta{% endblock %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-accounts.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="muted">
|
||||
Konta i ich dostęp do funkcji programu. Konto <strong>{{ admin_login }}</strong>
|
||||
@@ -15,7 +18,7 @@
|
||||
odpowiadają tak samo jak nieistniejące.
|
||||
</p>
|
||||
|
||||
{% if error %}<p class="house-warning">{{ error }}</p>{% endif %}
|
||||
{% if error %}<p class="notice-warn">{{ error }}</p>{% endif %}
|
||||
{% if done %}<p class="done-note">{{ done }}</p>{% endif %}
|
||||
|
||||
<div class="meta">Konta ({{ users | length }})</div>
|
||||
@@ -24,7 +27,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% for login, row in users | dictsort %}
|
||||
<form method="post" action="/accounts/update" class="account-card">
|
||||
<form method="post" action="/accounts/update" class="card">
|
||||
<input type="hidden" name="login" value="{{ login }}">
|
||||
<div class="account-head">
|
||||
<strong>{{ login }}</strong>
|
||||
@@ -66,7 +69,7 @@
|
||||
{% endfor %}
|
||||
|
||||
<div class="meta">Nowe konto</div>
|
||||
<form method="post" action="/accounts/create" class="account-card">
|
||||
<form method="post" action="/accounts/create" class="card">
|
||||
<div class="grid">
|
||||
<label>Login <input type="text" name="login" required autocomplete="off"></label>
|
||||
<label>Hasło <input type="password" name="password" required autocomplete="new-password"></label>
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>astrololo · {% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ static('styles.css') }}">
|
||||
{# Wspólne dane formularza między zakładkami (PRE-21). W <head> z `defer`
|
||||
CELOWO: skrypty defer wykonują się w kolejności dokumentu, więc ten zdąży
|
||||
odtworzyć współrzędne, ZANIM geo.js zbuduje mapę — mapa startuje od razu
|
||||
we właściwym miejscu, bez przestawiania. #}
|
||||
<script defer src="{{ static('formsync.js') }}"></script>
|
||||
{# Powiększanie kosmogramu (PRE-25) — globalnie, bo koło pojawi się też na
|
||||
zakładce „Skompiluj"; skrypt sam sprawdza, czy jest co powiększać. #}
|
||||
<script defer src="{{ static('wheelzoom.js') }}"></script>
|
||||
<script defer src="{{ static('wheeltip.js') }}"></script>
|
||||
{# Arkusz wspólny niesie WYŁĄCZNIE powłokę i logowanie — wszystko, co nazywa
|
||||
jakąkolwiek funkcję, mieszka w arkuszu tej funkcji i dokłada je `styles`.
|
||||
Inaczej jeden plik, pobierany też przed zalogowaniem, byłby spisem treści
|
||||
programu: nazwa selektora opowiada o funkcji tak samo dobrze jak przycisk. #}
|
||||
<link rel="stylesheet" href="{{ static('base.css') }}">
|
||||
{% block styles %}{% endblock %}
|
||||
{# Skrypty dokłada ekran, który ich potrzebuje. Ładowane stąd docierały na
|
||||
KAŻDĄ stronę — także do konta, które ma wyłącznie Pliki — a ich treść
|
||||
wymienia z nazwy ekrany, których takie konto nie ma prawa znać.
|
||||
Blok jest w <head>, bo `defer` wykonuje skrypty w kolejności dokumentu:
|
||||
formsync musi odtworzyć współrzędne, ZANIM geo.js zbuduje mapę. #}
|
||||
{% block head_scripts %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
@@ -45,9 +47,15 @@
|
||||
</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') %}
|
||||
<footer class="foot">
|
||||
prezentacja → logika → dane · widok testowy
|
||||
</footer>
|
||||
{% endif %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
{% block title %}Horoskop{% endblock %}
|
||||
{% set nav_active = "chart" %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-chart.css') }}">
|
||||
{% endblock %}
|
||||
{% block head_scripts %}
|
||||
<script defer src="{{ static('formsync.js') }}"></script>
|
||||
<script defer src="{{ static('wheelzoom.js') }}"></script>
|
||||
<script defer src="{{ static('wheeltip.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="sub">Wpisz dane momentu i miejsca — program policzy pozycje obiektów, osie i domy (silnik efemeryd warstwy logicznej).</p>
|
||||
|
||||
@@ -71,7 +79,7 @@
|
||||
{% if result %}
|
||||
{# Fallback systemu domów — z kosmogramu nie da się go poznać, więc mówimy wprost #}
|
||||
{% for w in result.house_warnings or [] %}
|
||||
<p class="house-warning">{{ w }}</p>
|
||||
<p class="notice-warn">{{ w }}</p>
|
||||
{% endfor %}
|
||||
<div class="meta">
|
||||
Silnik: <strong>{{ result.engine }}</strong> ·
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
{% block title %}Skompiluj{% endblock %}
|
||||
{% set nav_active = "compile" %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-compile.css') }}">
|
||||
{% if can(request, 'ai') %}<link rel="stylesheet" href="{{ static('x-ai.css') }}">{% endif %}
|
||||
{% endblock %}
|
||||
{% block head_scripts %}
|
||||
<script defer src="{{ static('formsync.js') }}"></script>
|
||||
<script defer src="{{ static('wheelzoom.js') }}"></script>
|
||||
<script defer src="{{ static('wheeltip.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# Opis raportu zależy od tego, z czego konto może go złożyć. Wersja z AI
|
||||
wymieniałaby funkcje, których część kont nie ma — a wymienienie ich w zdaniu
|
||||
|
||||
@@ -2,18 +2,25 @@
|
||||
{% set nav_active = "files" %}
|
||||
{% block title %}Pliki{% endblock %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-files.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# Opis czysto operacyjny: co robi zaznaczenie, i nic o tym, do CZEGO te dane
|
||||
służą. Poprzednia treść nazywała je „bazami interpretacyjnymi" i mówiła
|
||||
o „wyszukiwaniu" — czyli zdradzała i dziedzinę programu, i istnienie ekranu
|
||||
wyszukiwarki. To jest jedyny ekran, który ogląda konto wgrywające dane, więc
|
||||
każde zdanie na nim waży najwięcej. #}
|
||||
<p class="muted">
|
||||
Bazy interpretacyjne, z których korzysta program. Zaznaczona baza bierze udział
|
||||
w wyszukiwaniu; odznaczona zostaje na dysku, ale program jej nie używa.
|
||||
Zaznaczony plik jest w użyciu. Odznaczony zostaje na dysku, ale nie jest używany.
|
||||
</p>
|
||||
|
||||
{% if error %}<p class="house-warning">{{ error }}</p>{% endif %}
|
||||
{% if error %}<p class="notice-warn">{{ error }}</p>{% endif %}
|
||||
{% if done %}<p class="done-note">{{ done }}</p>{% endif %}
|
||||
|
||||
<div class="meta">Bazy ({{ files | length }})</div>
|
||||
<div class="meta">Pliki ({{ files | length }})</div>
|
||||
{% if not files %}
|
||||
<p class="muted">Nie ma jeszcze żadnego pliku bazy.</p>
|
||||
<p class="muted">Nie ma jeszcze żadnego pliku.</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -35,7 +42,7 @@
|
||||
<input type="hidden" name="path" value="{{ f.path }}">
|
||||
<input type="hidden" name="use" value="{{ '0' if f.in_use else '1' }}">
|
||||
<button type="submit" class="toggle {{ 'on' if f.in_use else 'off' }}"
|
||||
title="{{ 'Odstaw tę bazę' if f.in_use else 'Włącz tę bazę do użytku' }}">
|
||||
title="{{ 'Odstaw ten plik' if f.in_use else 'Włącz ten plik do użytku' }}">
|
||||
{{ '●' if f.in_use else '○' }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -84,8 +91,8 @@
|
||||
</div>
|
||||
|
||||
{% if can(request, 'files_input') %}
|
||||
<div class="meta">Wgraj nową bazę</div>
|
||||
<form method="post" action="/files/upload" enctype="multipart/form-data" class="account-card">
|
||||
<div class="meta">Wgraj plik</div>
|
||||
<form method="post" action="/files/upload" enctype="multipart/form-data" class="card">
|
||||
<div class="grid">
|
||||
<label>Plik <input type="file" name="upload" required></label>
|
||||
</div>
|
||||
@@ -101,7 +108,7 @@
|
||||
Plik musi je spełnić, żeby dało się go włączyć do użytku. Plik, który ich nie
|
||||
spełnia, <strong>nie jest kasowany</strong> — czeka na Twoją decyzję.
|
||||
</p>
|
||||
<form method="post" action="/files/rules" class="account-card">
|
||||
<form method="post" action="/files/rules" class="card">
|
||||
<div class="grid">
|
||||
<label>Dozwolone rozszerzenia (po przecinku)
|
||||
<input type="text" name="extensions" value="{{ (rules.extensions or []) | join(', ') }}"></label>
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
{% block title %}Interpretacje{% endblock %}
|
||||
{% set nav_active = "interpret" %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-interpret.css') }}">
|
||||
{% if can(request, 'ai') %}<link rel="stylesheet" href="{{ static('x-ai.css') }}">{% endif %}
|
||||
{% endblock %}
|
||||
{% block head_scripts %}
|
||||
<script defer src="{{ static('formsync.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="sub">Program policzy horoskop i wyszuka w bazie interpretacje pasujące do obliczeń (pierwsza wersja: planeta w swoim znaku).</p>
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>astrololo · logowanie</title>
|
||||
<link rel="stylesheet" href="{{ static('styles.css') }}">
|
||||
{# Ekran logowania nie dziedziczy z base.html (nie ma paska zakładek), więc
|
||||
linkuje arkusz sam. To JEDYNY arkusz publiczny — patrz features.STATIC. #}
|
||||
<link rel="stylesheet" href="{{ static('base.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<main class="login-main">
|
||||
@@ -13,9 +15,9 @@
|
||||
{% if request.query_params.get('wylogowano') %}
|
||||
<p class="done-note">Wylogowano.</p>
|
||||
{% endif %}
|
||||
{% if blad %}<p class="house-warning">{{ blad }}</p>{% endif %}
|
||||
{% if blad %}<p class="notice-warn">{{ blad }}</p>{% endif %}
|
||||
|
||||
<form method="post" action="/logowanie" class="account-card login-card">
|
||||
<form method="post" action="/logowanie" class="card login-card">
|
||||
{# Adres powrotu jedzie w ukrytym polu, ale i tak przechodzi przez sito
|
||||
po stronie serwera — pole formularza jest wejściem od użytkownika. #}
|
||||
<input type="hidden" name="dokad" value="{{ dokad }}">
|
||||
@@ -28,8 +30,9 @@
|
||||
</form>
|
||||
|
||||
<p class="muted small login-note">
|
||||
Nie masz konta albo nie pamiętasz hasła? Zgłoś się do osoby, która zakładała
|
||||
Ci dostęp — kont nie da się odzyskać samodzielnie.
|
||||
{# Bez wskazywania, że dostęp ktoś „zakłada": to mówi wprost, że istnieje
|
||||
konto o wyższych uprawnieniach i że własne jest okrojone. #}
|
||||
Nie pamiętasz hasła? Skontaktuj się z osobą, od której masz dane logowania.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
{% block title %}Ustawienia{% endblock %}
|
||||
{% set nav_active = "settings" %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-settings.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="sub">Bazy interpretacyjne dostępne na udziale sieciowym — i to, które biorą udział w interpretacji.</p>
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
{% block title %}Sygnifikatory{% endblock %}
|
||||
{% set nav_active = "significators" %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-significators.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="sub">Wyszukiwanie w bazach interpretacji (warstwa danych). Wymaga wgranych baz.</p>
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
{% block title %}Synastria{% endblock %}
|
||||
{% set nav_active = "synastry" %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-synastry.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="sub">Dwie osoby → aspekty <strong>między</strong> ich horoskopami (planeta jednej osoby do planety drugiej). Bez interpretacji z bazy — sama siatka aspektów.</p>
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
{% block title %}Kalendarz{% endblock %}
|
||||
{% set nav_active = "timeline" %}
|
||||
|
||||
{% block styles %}<link rel="stylesheet" href="{{ static('s-timeline.css') }}">
|
||||
{% if can(request, 'ai') %}<link rel="stylesheet" href="{{ static('x-ai.css') }}">{% endif %}
|
||||
{% endblock %}
|
||||
{% block head_scripts %}
|
||||
<script defer src="{{ static('formsync.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="sub">Zbiorcza oś czasu technik predykcyjnych (profekcje, solariusze, dyrekcje solar-arc) z interpretacjami z bazy dla dat.</p>
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ TEMPLATES = pathlib.Path(__file__).resolve().parents[1] / "app" / "templates"
|
||||
|
||||
|
||||
def test_static_url_carries_content_hash():
|
||||
assert re.fullmatch(r"/static/styles\.css\?v=[0-9a-f]{8}", static_url("styles.css"))
|
||||
assert re.fullmatch(r"/static/base\.css\?v=[0-9a-f]{8}", static_url("base.css"))
|
||||
|
||||
|
||||
def test_version_is_content_based_so_differs_between_files():
|
||||
assert _asset_version("styles.css") != _asset_version("formsync.js")
|
||||
assert _asset_version("base.css") != _asset_version("formsync.js")
|
||||
|
||||
|
||||
def test_missing_file_does_not_crash():
|
||||
@@ -35,7 +35,17 @@ def test_no_template_serves_raw_static_paths():
|
||||
assert 'href="/static/' not in txt, f"{f.name}: surowy href bez cache-bustingu"
|
||||
|
||||
|
||||
def test_base_uses_helper_for_stylesheet_and_scripts():
|
||||
def test_base_uses_helper_for_the_shared_stylesheet():
|
||||
base = (TEMPLATES / "base.html").read_text(encoding="utf-8")
|
||||
assert "static('styles.css')" in base
|
||||
assert "static('formsync.js')" in base
|
||||
assert "static('base.css')" in base
|
||||
|
||||
|
||||
def test_version_follows_what_is_sent_not_what_is_on_disk():
|
||||
"""Adres niesie hash treści WYSYŁANEJ, bo komentarze są przy serwowaniu
|
||||
usuwane. Gdyby hash szedł z pliku na dysku, poprawka w samym komentarzu
|
||||
podbijałaby adres i kasowała cache przeglądarki bez żadnej zmiany dla niej."""
|
||||
import hashlib
|
||||
|
||||
from app.main import _asset_body
|
||||
|
||||
assert _asset_version("base.css") == hashlib.md5(_asset_body("base.css")).hexdigest()[:8]
|
||||
|
||||
@@ -65,12 +65,19 @@ def test_every_shared_field_is_synced(key):
|
||||
assert f"'{key}'" in SYNC_JS, f"pole {key} nie jest synchronizowane"
|
||||
|
||||
|
||||
def test_sync_script_loaded_before_map_script():
|
||||
"""KOLEJNOŚĆ MA ZNACZENIE: formsync w <head> z `defer` wykonuje się przed
|
||||
geo.js (skrypty defer idą w kolejności dokumentu), więc mapa startuje już
|
||||
z odtworzonymi współrzędnymi zamiast skakać po chwili."""
|
||||
assert "formsync.js" in BASE_HTML, "formsync.js nie jest ładowany globalnie"
|
||||
assert "defer" in BASE_HTML.split("formsync.js")[0].rsplit("<script", 1)[-1] + "defer"
|
||||
@pytest.mark.parametrize("name", WITH_FORM)
|
||||
def test_sync_script_loaded_before_map_script(name):
|
||||
"""KOLEJNOŚĆ MA ZNACZENIE: formsync z `defer` wykonuje się przed geo.js
|
||||
(skrypty defer idą w kolejności dokumentu), więc mapa startuje już
|
||||
z odtworzonymi współrzędnymi zamiast skakać po chwili.
|
||||
|
||||
Skrypt siedzi teraz w bloku `head_scripts` EKRANU, nie w base.html. Ładowany
|
||||
globalnie docierał także do konta, które ma wyłącznie Pliki — a jego treść
|
||||
wymienia z nazwy zakładki, których takie konto nie ma."""
|
||||
html = (TEMPLATES / name).read_text(encoding="utf-8")
|
||||
assert "head_scripts" in html and "formsync.js" in html
|
||||
przed_geo = html.split("formsync.js")[0]
|
||||
assert "defer" in przed_geo.rsplit("<script", 1)[-1] + "defer"
|
||||
|
||||
|
||||
def test_sync_reacts_to_other_browser_tab():
|
||||
|
||||
@@ -113,7 +113,10 @@ def test_fallback_notice_is_shown_prominently():
|
||||
sygnałem, że podział domów jest z innego systemu."""
|
||||
html = _render("_result_tables.html",
|
||||
result={"house_warnings": ["UWAGA: system domów nie ma definicji"]})
|
||||
assert "house-warning" in html
|
||||
# Klasa nazywa się neutralnie: `house-warning` siedziała też na ekranie
|
||||
# logowania i na ekranie plików, czyli nazwa z astrologii lądowała tam,
|
||||
# gdzie astrologii ma nie być widać.
|
||||
assert "notice-warn" in html
|
||||
assert "UWAGA: system domów nie ma definicji" in html
|
||||
|
||||
|
||||
@@ -184,7 +187,12 @@ def test_wheel_orientation_reaches_the_renderer(monkeypatch, orientation):
|
||||
# overflow-x, ROZPYCHAŁA CAŁY DOKUMENT — 1713 px przy oknie 1280 px, czyli
|
||||
# poziomy pasek przewijania na body i rozjechany układ.
|
||||
|
||||
CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
# Arkusz jest teraz rozbity per uprawnienie (arkusz wspólny + arkusze ekranów),
|
||||
# bo jeden plik ze wszystkimi selektorami był spisem funkcji programu dla
|
||||
# każdego, kto go pobrał. Reguły szukamy w całości — który arkusz ją niesie,
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "static").glob("*.css")))
|
||||
|
||||
|
||||
def test_comparison_table_scrolls_instead_of_widening_the_page():
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Zapora słownikowa: czego konto NIE MOŻE zobaczyć w żadnej swojej odpowiedzi.
|
||||
|
||||
Poprzednie testy pilnowały pojedynczych miejsc — tego przycisku, tamtego bloku.
|
||||
Wyciek prawie nigdy nie siedzi w miejscu, o którym się pamięta: katalog modeli
|
||||
jechał blokiem JSON, nazwy ekranów komentarzem w arkuszu stylów, mechanizm
|
||||
kwarantanny zdaniem po polsku w komentarzu CSS, a nazwa gałęzi rozwojowej
|
||||
w komunikacie o błędzie. Wspólne mają jedno — pewne SŁOWA nie mają prawa paść.
|
||||
|
||||
Więc test nie sprawdza miejsc, tylko przechodzi WSZYSTKO, co dane konto może
|
||||
pobrać (strony i zasoby), i szuka słów zakazanych. Nowy ekran, nowy skrypt,
|
||||
nowy komunikat wchodzą pod tę samą zaporę bez dopisywania czegokolwiek.
|
||||
|
||||
Dwie z trzech list biorą się wprost z katalogu funkcji, więc nie rozjadą się
|
||||
z programem: dopisanie funkcji automatycznie dopisuje jej nazwę i jej adres do
|
||||
tego, czego konto bez niej nie ma prawa zobaczyć.
|
||||
"""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from app import accounts as store
|
||||
from app import features
|
||||
|
||||
# ── persona ──────────────────────────────────────────────────────────────
|
||||
# Osoba zatrudniona wyłącznie do wgrywania plików. Ma widzieć swój ekran i nic
|
||||
# poza nim — ani co program robi, ani co będzie robił, ani że ktoś widzi więcej.
|
||||
WGRYWACZ = ["files", "files_input"]
|
||||
|
||||
# ── słowa zakazane, których NIE DA SIĘ wyprowadzić z katalogu ────────────
|
||||
# Katalog zna nazwy funkcji, ale nie zna dziedziny programu ani słownictwa jego
|
||||
# mechanizmów. Te trzeba wypisać, i każde ma tu swój powód.
|
||||
DZIEDZINA = [
|
||||
# (c) po co są wgrywane pliki
|
||||
"astrolog", "horoskop", "kosmogram", "zodiak", "efemeryd", "sygnifikator",
|
||||
"interpretacyjn", "aspektarian", "antyscj", "deklinacj",
|
||||
# „baza" mówi, że to nie są zwykłe pliki, tylko zbiory czegoś. Na ekranie
|
||||
# wgrywającego wszystko nazywa się plikiem; w częściach administracyjnych
|
||||
# słowo zostaje, bo tam jest właściwe (i tam ten test nie zagląda).
|
||||
"baz",
|
||||
# (b) że tekst pisze model językowy i u kogo
|
||||
"LLM", "anthropic", "openai", "gpt", "claude", "prompt", "token",
|
||||
# (e)/(f) mechanizmy i to, że istnieje ktoś, kto widzi więcej
|
||||
"administrator", "uprawnieni", "walidacj", "kwarantann", "wstrzyman",
|
||||
"zatwierdzi", "przypisan", "zakłada", "zakładał",
|
||||
# „konto" w obu przypadkach: pierwsza wersja listy miała samo „konto",
|
||||
# więc przepuszczała zdanie „Nie masz konta" — literówka, która kosztowałaby
|
||||
# cały sens testu, gdyby nie wyszła przy oglądaniu strony.
|
||||
"konto", "konta", "kontem",
|
||||
# (d) że produkt jest w budowie i ma ponumerowany backlog
|
||||
"roadmap", "przyszł", "planowan", "w budowie", "widok testowy",
|
||||
# topologia: gdzie pukać, żeby ominąć logowanie
|
||||
"warstwa logiczna", "warstwie logicznej", "openapi", "swagger",
|
||||
# architektura: „to jedna z warstw" mówi, że warstw jest więcej
|
||||
"layer", "presentation", "prezentacj",
|
||||
"localhost", "http://logic", "8001", "8002", "8003",
|
||||
]
|
||||
WZORCE = [
|
||||
(r"(?i)\b(PRE|LOG|DAN)-\d+\b", "identyfikator wymagania (ujawnia backlog)"),
|
||||
(r"(?i)\bfeat/[\w-]+", "nazwa gałęzi rozwojowej"),
|
||||
(r"(?i)\bmodel(u|em|e|i|ach|owi)?\b", "generowanie tekstu przez model"),
|
||||
(r"(?i)\bAI\b", "generowanie tekstu przez model"),
|
||||
]
|
||||
|
||||
|
||||
def _zakazane_dla(perms: set[str]) -> list[tuple[str, str]]:
|
||||
"""(igła, powód) — wyprowadzone z katalogu plus lista dziedzinowa."""
|
||||
out = [(s, "słownik dziedziny/mechanizmu") for s in DZIEDZINA]
|
||||
for f in features.ALL:
|
||||
if f.key in perms:
|
||||
continue
|
||||
out.append((f.label, f"nazwa funkcji, której to konto nie ma ({f.key})"))
|
||||
if f.href and f.href != "/":
|
||||
out.append((f.href, f"adres ekranu, którego to konto nie ma ({f.key})"))
|
||||
# Nazwa zasobu też jest wyciekiem: `<script src=".../wheelzoom.js">` w źródle
|
||||
# mówi, że istnieje coś do powiększania, nawet jeśli sam plik zwróci 404.
|
||||
# Wykaz bierze się z mapy zasobów, więc nowy skrypt obejmuje sam z siebie.
|
||||
for nazwa, need in features.STATIC.items():
|
||||
if not features.can(frozenset(perms), need):
|
||||
out.append((nazwa, f"nazwa zasobu, którego to konto nie może pobrać"))
|
||||
return out
|
||||
|
||||
|
||||
# Ekran logowania MUSI mówić o koncie — inaczej nie da się go opisać. To jedyne
|
||||
# odstępstwo i jest wąskie: dotyczy tej jednej strony i tych dwóch słów.
|
||||
WYJATKI = {"/logowanie": {"konto", "konta", "kontem"}}
|
||||
|
||||
|
||||
def _przeskanuj(tresc: str, perms: set[str], gdzie: str) -> list[str]:
|
||||
wolno = WYJATKI.get(gdzie, set())
|
||||
grzechy = []
|
||||
for igla, powod in _zakazane_dla(perms):
|
||||
if igla in wolno:
|
||||
continue
|
||||
if re.search(rf"(?i)(?<![\w-]){re.escape(igla)}", tresc):
|
||||
grzechy.append(f"{gdzie}: „{igla}” — {powod}")
|
||||
for wzor, powod in WZORCE:
|
||||
m = re.search(wzor, tresc)
|
||||
if m:
|
||||
grzechy.append(f"{gdzie}: „{m.group(0)}” — {powod}")
|
||||
return grzechy
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def env(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("ACCOUNTS_FILE", str(tmp_path / "accounts.json"))
|
||||
monkeypatch.setenv("APP_USER", "szef")
|
||||
monkeypatch.setenv("APP_PASSWORD", "tajne-szefa")
|
||||
monkeypatch.delenv("APP_USERS", raising=False)
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_MIN", "0")
|
||||
monkeypatch.setenv("SESSION_SECRET", "t" * 64)
|
||||
monkeypatch.setenv("COOKIES_INSECURE", "1")
|
||||
|
||||
|
||||
def _klient(monkeypatch):
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app.main import app, logic
|
||||
|
||||
# Warstwa logiczna niedostępna CELOWO: ekran plików idzie wtedy ścieżką
|
||||
# błędu, a to właśnie komunikaty o błędach wypisywały nazwy tras, nazwę
|
||||
# gałęzi i wewnętrzny adres z portem.
|
||||
import httpx
|
||||
|
||||
def pada(*a, **k):
|
||||
raise httpx.ConnectError("[Errno 111] connect to http://logic:8001/files failed")
|
||||
|
||||
for nazwa in ("files_list", "report", "timeline", "prompt", "horoscope", "positions"):
|
||||
if hasattr(logic, nazwa):
|
||||
monkeypatch.setattr(logic, nazwa, pada, raising=False)
|
||||
return TestClient(app, follow_redirects=False)
|
||||
|
||||
|
||||
def _co_moze_pobrac(perms: set[str]) -> list[str]:
|
||||
"""Wszystko, co to konto ma prawo dostać: strony i zasoby."""
|
||||
strony = [p for (m, p), need in features.ROUTES.items()
|
||||
if m == "GET" and features.can(frozenset(perms), need)]
|
||||
zasoby = ["/static/base.css"] + [
|
||||
f"/static/{n}" for n, need in features.STATIC.items()
|
||||
if features.can(frozenset(perms), need)]
|
||||
return sorted(set(strony + zasoby))
|
||||
|
||||
|
||||
def test_the_uploader_never_reads_a_forbidden_word(env, monkeypatch):
|
||||
store.create("wgrywacz", "x", WGRYWACZ)
|
||||
from app import security, session
|
||||
|
||||
c = _klient(monkeypatch)
|
||||
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('wgrywacz')}"}
|
||||
perms = set(WGRYWACZ)
|
||||
|
||||
grzechy, obejrzane = [], 0
|
||||
for adres in _co_moze_pobrac(perms):
|
||||
r = c.get(adres, headers=naglowki)
|
||||
if r.status_code != 200:
|
||||
continue
|
||||
obejrzane += 1
|
||||
grzechy += _przeskanuj(r.text, perms, adres)
|
||||
assert obejrzane >= 3, f"test niczego nie obejrzał ({obejrzane}) — zapora byłaby pozorna"
|
||||
assert not grzechy, "\n" + "\n".join(grzechy)
|
||||
|
||||
|
||||
def test_an_anonymous_visitor_never_reads_a_forbidden_word(env, monkeypatch):
|
||||
"""Przed zalogowaniem widać ekran logowania i jeden arkusz. Nic więcej,
|
||||
i nic w nich nie może nazywać funkcji."""
|
||||
store.create("wgrywacz", "x", WGRYWACZ)
|
||||
c = _klient(monkeypatch)
|
||||
grzechy, obejrzane = [], 0
|
||||
for adres in ("/logowanie", "/health", "/static/base.css"):
|
||||
r = c.get(adres)
|
||||
assert r.status_code == 200, f"{adres}: {r.status_code}"
|
||||
obejrzane += 1
|
||||
grzechy += _przeskanuj(r.text, set(), adres)
|
||||
assert obejrzane == 3
|
||||
assert not grzechy, "\n" + "\n".join(grzechy)
|
||||
|
||||
|
||||
def test_every_other_asset_is_denied_to_the_uploader(env, monkeypatch):
|
||||
"""Zasób spoza jego zestawu ma być NIE DO ODRÓŻNIENIA od nieistniejącego."""
|
||||
store.create("wgrywacz", "x", WGRYWACZ)
|
||||
from app import security, session
|
||||
|
||||
c = _klient(monkeypatch)
|
||||
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('wgrywacz')}"}
|
||||
wolno = set(_co_moze_pobrac(set(WGRYWACZ)))
|
||||
nieistniejacy = c.get("/static/nie-ma-takiego-pliku.js", headers=naglowki)
|
||||
for nazwa in features.STATIC:
|
||||
adres = f"/static/{nazwa}"
|
||||
if adres in wolno:
|
||||
continue
|
||||
r = c.get(adres, headers=naglowki)
|
||||
assert r.status_code == nieistniejacy.status_code == 404, f"{adres}: {r.status_code}"
|
||||
assert r.text == nieistniejacy.text, f"{adres}: odpowiedź różni się od „nie ma takiego pliku”"
|
||||
|
||||
|
||||
def test_the_catalogue_drives_the_list_so_it_cannot_go_stale(env):
|
||||
"""Zapora ma się uczyć z katalogu, nie z pamięci tego, kto ją pisał."""
|
||||
zakazane = {i for i, _ in _zakazane_dla(set(WGRYWACZ))}
|
||||
assert "Sygnifikatory" in zakazane and "/interpret" in zakazane
|
||||
# a to, co konto MA, nie może trafić na listę zakazanych
|
||||
assert "Pliki" not in zakazane
|
||||
|
||||
|
||||
def test_the_owner_sees_everything(env, monkeypatch):
|
||||
"""Kontrola pozytywna: dla administratora te słowa MAJĄ się pojawiać.
|
||||
Bez niej zapora przechodziłaby także wtedy, gdyby program był pusty."""
|
||||
from app import security, session
|
||||
|
||||
c = _klient(monkeypatch)
|
||||
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('szef')}"}
|
||||
html = c.get("/accounts", headers=naglowki).text
|
||||
assert "Sygnifikatory" in html and "Generowanie tekstu przez model" in html
|
||||
@@ -15,13 +15,31 @@ import re
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
JS = (APP / "static" / "wheeltip.js").read_text(encoding="utf-8")
|
||||
CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
# Arkusz jest teraz rozbity per uprawnienie (arkusz wspólny + arkusze ekranów),
|
||||
# bo jeden plik ze wszystkimi selektorami był spisem funkcji programu dla
|
||||
# każdego, kto go pobrał. Reguły szukamy w całości — który arkusz ją niesie,
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "static").glob("*.css")))
|
||||
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_script_is_loaded_globally():
|
||||
"""W base.html, bo koło jest też na zakładce „Skompiluj"."""
|
||||
assert "wheeltip.js" in BASE
|
||||
def test_script_is_loaded_only_where_there_is_a_wheel():
|
||||
"""Koło jest na Horoskopie i na Skompiluj — i tylko tam ten skrypt ma prawo
|
||||
się pojawić.
|
||||
|
||||
Wcześniej ładowany był z base.html, czyli na KAŻDEJ stronie. Konto, które ma
|
||||
wyłącznie Pliki, pobierało go przy zwykłym wejściu na swój jedyny ekran,
|
||||
a jego treść wymienia z nazwy zakładki, o których takie konto nie ma prawa
|
||||
wiedzieć — łącznie z tą, której jeszcze nie ma."""
|
||||
from pathlib import Path
|
||||
|
||||
szablony = Path(__file__).resolve().parents[1] / "app" / "templates"
|
||||
z_kolem, bez_kola = set(), set()
|
||||
for f in szablony.glob("*.html"):
|
||||
(z_kolem if "wheeltip.js" in f.read_text(encoding="utf-8") else bez_kola).add(f.name)
|
||||
assert z_kolem == {"chart.html", "compile.html"}, f"ładowany też przez: {z_kolem}"
|
||||
assert "files.html" in bez_kola and "logowanie.html" in bez_kola
|
||||
|
||||
|
||||
def test_tooltip_sits_above_the_zoom_overlay_but_below_the_progress_window():
|
||||
|
||||
@@ -9,13 +9,31 @@ import re
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
JS = (APP / "static" / "wheelzoom.js").read_text(encoding="utf-8")
|
||||
CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
# Arkusz jest teraz rozbity per uprawnienie (arkusz wspólny + arkusze ekranów),
|
||||
# bo jeden plik ze wszystkimi selektorami był spisem funkcji programu dla
|
||||
# każdego, kto go pobrał. Reguły szukamy w całości — który arkusz ją niesie,
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "static").glob("*.css")))
|
||||
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_script_is_loaded_globally():
|
||||
"""Ładowane w base.html, bo koło pojawi się też na zakładce Skompiluj."""
|
||||
assert "wheelzoom.js" in BASE
|
||||
def test_script_is_loaded_only_where_there_is_a_wheel():
|
||||
"""Koło jest na Horoskopie i na Skompiluj — i tylko tam ten skrypt ma prawo
|
||||
się pojawić.
|
||||
|
||||
Wcześniej ładowany był z base.html, czyli na KAŻDEJ stronie. Konto, które ma
|
||||
wyłącznie Pliki, pobierało go przy zwykłym wejściu na swój jedyny ekran,
|
||||
a jego treść wymienia z nazwy zakładki, o których takie konto nie ma prawa
|
||||
wiedzieć — łącznie z tą, której jeszcze nie ma."""
|
||||
from pathlib import Path
|
||||
|
||||
szablony = Path(__file__).resolve().parents[1] / "app" / "templates"
|
||||
z_kolem, bez_kola = set(), set()
|
||||
for f in szablony.glob("*.html"):
|
||||
(z_kolem if "wheelzoom.js" in f.read_text(encoding="utf-8") else bez_kola).add(f.name)
|
||||
assert z_kolem == {"chart.html", "compile.html"}, f"ładowany też przez: {z_kolem}"
|
||||
assert "files.html" in bez_kola and "logowanie.html" in bez_kola
|
||||
|
||||
|
||||
def test_toggles_on_click():
|
||||
|
||||
Reference in New Issue
Block a user