From e96e4837f19e6a15fb9cb9c2672d3232a8684b86 Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Wed, 9 Sep 2026 21:48:07 +0200 Subject: [PATCH] Version 2.2.0: Vorschau mit Zoom und PDF-Formularen, eine Uploadgrenze --- apps/tesm-license/pyproject.toml | 2 +- deploy/install.sh | 5 +- packages/tesm-core/pyproject.toml | 2 +- .../tesm-core/src/tesm_core/app_factory.py | 20 +- .../static/tesm_core/css/components.css | 73 +- .../src/tesm_core/static/tesm_core/js/app.js | 10 + .../tesm_core/static/tesm_core/js/viewer.js | 388 + .../tesm_core/vendor/pdfjs/HERKUNFT.txt | 6 + .../static/tesm_core/vendor/pdfjs/LICENSE | 177 + .../static/tesm_core/vendor/pdfjs/VERSION | 1 + .../vendor/pdfjs/images/altText_add.svg | 3 + .../pdfjs/images/altText_disclaimer.svg | 3 + .../vendor/pdfjs/images/altText_done.svg | 3 + .../vendor/pdfjs/images/altText_spinner.svg | 30 + .../vendor/pdfjs/images/altText_warning.svg | 3 + .../vendor/pdfjs/images/annotation-check.svg | 11 + .../pdfjs/images/annotation-comment.svg | 16 + .../vendor/pdfjs/images/annotation-help.svg | 26 + .../vendor/pdfjs/images/annotation-insert.svg | 10 + .../vendor/pdfjs/images/annotation-key.svg | 11 + .../pdfjs/images/annotation-newparagraph.svg | 11 + .../vendor/pdfjs/images/annotation-noicon.svg | 7 + .../vendor/pdfjs/images/annotation-note.svg | 42 + .../pdfjs/images/annotation-paperclip.svg | 6 + .../pdfjs/images/annotation-paragraph.svg | 16 + .../pdfjs/images/annotation-pushpin.svg | 7 + .../images/cursor-editorFreeHighlight.svg | 6 + .../pdfjs/images/cursor-editorFreeText.svg | 3 + .../vendor/pdfjs/images/cursor-editorInk.svg | 4 + .../images/cursor-editorTextHighlight.svg | 8 + .../pdfjs/images/editor-toolbar-delete.svg | 5 + .../vendor/pdfjs/images/loading-icon.gif | Bin 0 -> 2545 bytes .../pdfjs/images/messageBar_closingButton.svg | 3 + .../pdfjs/images/messageBar_warning.svg | 3 + ...ondaryToolbarButton-documentProperties.svg | 3 + .../images/toolbarButton-editorHighlight.svg | 6 + .../pdfjs/images/toolbarButton-menuArrow.svg | 3 + .../static/tesm_core/vendor/pdfjs/pdf.min.mjs | 21 + .../tesm_core/vendor/pdfjs/pdf.worker.min.mjs | 21 + .../tesm_core/vendor/pdfjs/pdf_viewer.css | 3274 +++++++ .../tesm_core/vendor/pdfjs/pdf_viewer.mjs | 8435 +++++++++++++++++ .../pdfjs/standard_fonts/FoxitDingbats.pfb | Bin 0 -> 29513 bytes .../pdfjs/standard_fonts/FoxitFixed.pfb | Bin 0 -> 17597 bytes .../pdfjs/standard_fonts/FoxitFixedBold.pfb | Bin 0 -> 18055 bytes .../standard_fonts/FoxitFixedBoldItalic.pfb | Bin 0 -> 19151 bytes .../pdfjs/standard_fonts/FoxitFixedItalic.pfb | Bin 0 -> 18746 bytes .../pdfjs/standard_fonts/FoxitSerif.pfb | Bin 0 -> 19469 bytes .../pdfjs/standard_fonts/FoxitSerifBold.pfb | Bin 0 -> 19395 bytes .../standard_fonts/FoxitSerifBoldItalic.pfb | Bin 0 -> 20733 bytes .../pdfjs/standard_fonts/FoxitSerifItalic.pfb | Bin 0 -> 21227 bytes .../pdfjs/standard_fonts/FoxitSymbol.pfb | Bin 0 -> 16729 bytes .../vendor/pdfjs/standard_fonts/LICENSE_FOXIT | 27 + .../pdfjs/standard_fonts/LICENSE_LIBERATION | 102 + .../standard_fonts/LiberationSans-Bold.ttf | Bin 0 -> 137052 bytes .../LiberationSans-BoldItalic.ttf | Bin 0 -> 135124 bytes .../standard_fonts/LiberationSans-Italic.ttf | Bin 0 -> 162036 bytes .../standard_fonts/LiberationSans-Regular.ttf | Bin 0 -> 139512 bytes .../src/tesm_core/system/webserver.py | 11 +- .../templates/tesm_core/settings_web.html | 9 + packages/tesm-core/src/tesm_core/ui.py | 3 + packages/tesm-core/src/tesm_core/uploads.py | 80 + .../src/tesm_core/views/hostsettings.py | 27 +- packages/tesm-licensing-server/pyproject.toml | 4 +- packages/tesm-licensing/pyproject.toml | 2 +- 64 files changed, 12936 insertions(+), 13 deletions(-) create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/HERKUNFT.txt create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/LICENSE create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/VERSION create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_add.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_disclaimer.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_done.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_spinner.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_warning.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-check.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-comment.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-help.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-insert.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-key.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-newparagraph.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-noicon.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-note.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-paperclip.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-paragraph.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-pushpin.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorFreeHighlight.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorFreeText.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorInk.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorTextHighlight.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/editor-toolbar-delete.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/loading-icon.gif create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/messageBar_closingButton.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/messageBar_warning.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/secondaryToolbarButton-documentProperties.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/toolbarButton-editorHighlight.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/toolbarButton-menuArrow.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf.min.mjs create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf.worker.min.mjs create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf_viewer.css create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf_viewer.mjs create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixed.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerif.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LICENSE_FOXIT create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf create mode 100644 packages/tesm-core/src/tesm_core/uploads.py 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