diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index 90962ee..5b38763 100644 --- a/apps/tesm-license/pyproject.toml +++ b/apps/tesm-license/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-license" -version = "2.1.2" +version = "2.2.0" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/deploy/install.sh b/deploy/install.sh index ea21824..e673898 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -190,7 +190,10 @@ fi # nicht ausdruecklich neu gesetzt wird. ENV_FILE="$ETC_TESM/$NAME.env" # Wo die Daten liegen, steht in der Umgebungsdatei; ohne sie beim Vorgabeort. -DATEN_DIR="$(sed -n "s/^${ENV_PREFIX}DATA_DIR=//p" "$ENV_FILE" 2>/dev/null | tail -1)" +# "|| true": bei einer Neuinstallation gibt es die Datei noch nicht, und ein +# scheiterndes sed in einer Zuweisung beendet unter "set -e" das ganze Skript. +DATEN_DIR="$(sed -n "s/^${ENV_PREFIX}DATA_DIR=//p" "$ENV_FILE" 2>/dev/null || true)" +DATEN_DIR="$(printf '%s' "$DATEN_DIR" | tail -1)" DATEN_DIR="${DATEN_DIR:-$ROOT/data}" if [[ -z "$PORT" && -f "$ENV_FILE" ]]; then PORT="$(sed -n "s/^${ENV_PREFIX}BIND=127\.0\.0\.1:\([0-9]\{1,5\}\)$/\1/p" "$ENV_FILE" | head -1)" diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index de32f0a..bbbe41e 100644 --- a/packages/tesm-core/pyproject.toml +++ b/packages/tesm-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-core" -version = "2.1.2" +version = "2.2.0" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/src/tesm_core/app_factory.py b/packages/tesm-core/src/tesm_core/app_factory.py index 9283ee5..b7d3cdf 100644 --- a/packages/tesm-core/src/tesm_core/app_factory.py +++ b/packages/tesm-core/src/tesm_core/app_factory.py @@ -31,6 +31,7 @@ from .security import csrf from .security import sessions as session_store from .security.sessions import SqliteSessionInterface from .settings_store import SettingSpec, SettingsStore +from . import uploads from .sysops import SysOps from .auth import directory from .auth import routes as auth_routes @@ -134,7 +135,7 @@ def create_base_app( #: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide #: Anwendungen. auth_tagline: str = "", - app_version: str = "2.1.2", + app_version: str = "2.2.0", built_in_trusted_keys: tuple[str, ...] = (), enable_license: bool = True, template_folder: str | None = None, @@ -163,6 +164,10 @@ def create_base_app( config.data_dir.mkdir(parents=True, exist_ok=True) config.log_dir.mkdir(parents=True, exist_ok=True) + # Die Groessengrenze einer Anfrage kommt aus der Einstellung, nicht aus + # der Konfiguration beim Start. + app.request_class = uploads.UploadLimitRequest + app.config.update( SECRET_KEY=load_or_create_secret_key(config.secret_key_file), # site_key statt app_key: Cookies unterscheiden keine Ports. Zwei @@ -172,6 +177,9 @@ def create_base_app( SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE=config.security.cookie_secure, SESSION_COOKIE_SAMESITE=config.security.cookie_samesite, + # Nur die aeussere Schranke aus der Umgebung. Die eigentliche Grenze + # steht in der Einstellung und wird pro Anfrage gelesen (uploads.py) -- + # sonst braeuchte jede Aenderung einen Neustart aller Arbeiter. MAX_CONTENT_LENGTH=config.security.max_content_length, JSON_SORT_KEYS=False, TRAP_HTTP_EXCEPTIONS=False, @@ -328,6 +336,16 @@ def create_base_app( # -- Fehlerseiten ------------------------------------------------------ # @app.errorhandler(HTTPException) def _http_error(error: HTTPException): # type: ignore[no-untyped-def] + if error.code == 413: + # "Request Entity Too Large" allein hilft niemandem weiter: die + # Zahl steht in einer Einstellung, und wer hochlaedt, soll sie + # sehen. Vorher stand hier nur der nackte Status. + grenze = uploads.limit_mb() + error.description = ( + f"Diese Datei ist groesser als erlaubt. Zurzeit sind {grenze} MB je Upload " + "eingestellt; aendern kann das ein Administrator unter " + '"Einstellungen -> Webserver und TLS".' + ) if _wants_json(): return { "error": error.name, diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css index 958efab..efc3762 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css @@ -685,7 +685,10 @@ .modal__sub { font-size: var(--fs-xs); color: var(--text-faint); margin-top: 2px; } /* Eine Datei-Vorschau soll die ganze Flaeche des Dialogs nutzen; der uebliche Innenabstand des Dialogkoerpers wuerde sie einrahmen. */ - .modal__body--flush { padding: 0; max-height: none; } + /* Zwei Klassen am selben Element: diese hier muss gewinnen, obwohl + .modal__body weiter unten steht. Mit der doppelten Klasse haengt das + nicht mehr an der Reihenfolge im Stylesheet. */ + .modal__body.modal__body--flush { padding: 0; max-height: none; overflow: visible; } .modal__caption { padding: 0 var(--sp-5) var(--sp-2); color: var(--text-muted); @@ -702,6 +705,74 @@ darunter. --surface-0 gibt es nicht, der Rahmen blieb durchsichtig. */ background: var(--surface-1); } + + /* -- Betrachter: Bild, PDF, Rahmen ------------------------------------- */ + /* Eine feste Hoehe fuer alle drei Anzeigen: sonst springt der Dialog beim + Wechsel zwischen Foto und PDF. */ + .viewer { + position: relative; + display: flex; + flex-direction: column; + /* Was der Dialog uebrig laesst: Kopf, Dateiname und Fusszeile brauchen + zusammen rund 190 Pixel. Ein fester Anteil der Fensterhoehe schob die + Zoomleiste auf kleinen Fenstern unter den Rand. */ + height: min(calc(100dvh - 240px), 760px); + min-height: 220px; + background: var(--surface-1); + } + .viewer > .file-preview { + flex: 1; + height: auto; + } + .viewer__stage { + position: relative; + flex: 1; + /* Ohne min-height waechst die Buehne mit dem Bild, und dessen + max-height:100% haengt dann an sich selbst: ein 4032x3024-Foto stand + 36 Pixel ueber den Rand hinaus. */ + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + /* Die Seite darf nicht mitwandern, wenn im Bild geschoben wird. */ + touch-action: none; + cursor: zoom-in; + } + .viewer__stage--gezogen { + cursor: grab; + } + .viewer__img { + /* Der Ausgangszustand ist "passt in die Flaeche" -- genau das fehlte: + ein Foto vom iPad ist 4032 Pixel breit und wurde im Rahmen 1:1 + gezeigt, also als Ausschnitt aus der Mitte. */ + max-width: 100%; + max-height: 100%; + /* Zoomen soll nicht unscharf wirken, solange nicht vergroessert wird. */ + transform-origin: center center; + will-change: transform; + user-select: none; + -webkit-user-drag: none; + } + /* pdf.js verlangt einen absolut positionierten, scrollenden Behaelter. */ + .viewer__pdf { + position: absolute; + inset: 0; + bottom: var(--sp-10); + overflow: auto; + -webkit-overflow-scrolling: touch; + } + .viewer__bar { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-2) var(--sp-4); + border-top: 1px solid var(--border); + background: var(--surface-2, var(--surface-1)); + } + .viewer__hint { + margin-inline-start: auto; + } /* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere unruhig. */ diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js index 419691c..41076f1 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js @@ -895,6 +895,16 @@ function boot() { initTileFilters(); initLogTails(); unsavedGuard.init(); + initViewer(); +} + +/* Der Dateibetrachter kommt nur auf Seiten mit Vorschau -- und er zieht bei + einem PDF noch pdf.js nach. Beides erst dann, wenn es gebraucht wird. */ +function initViewer() { + if (!document.querySelector("[data-viewer]")) return; + import("./viewer.js") + .then((modul) => modul.initFilePreview()) + .catch((fehler) => console.error("Dateibetrachter", fehler)); } if (document.readyState === "loading") { diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js new file mode 100644 index 0000000..0a568ae --- /dev/null +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js @@ -0,0 +1,388 @@ +/* ============================================================================= + * Dateivorschau: Bilder zoomen, PDF-Formulare ausfuellen. + * + * Vorher lief jede Vorschau durch einen