Compare commits

..
3 Commits
Author SHA1 Message Date
alientimandClaude Opus 5 160675e368 upgrade.sh und update.sh genuegen als einzelne Datei
Auf einer 1.2.8 liegt nichts von dieser Fassung -- upgrade.sh verlangte aber
ein install.sh daneben und brach sonst ab. Das war die falsche Voraussetzung
fuer genau den Fall, fuer den das Skript gedacht ist. Beide Skripte holen sich
das Release jetzt selbst:

    curl -fsSLO .../raw/branch/main/deploy/upgrade.sh
    sudo bash upgrade.sh --app tesm

Liegt das Skript in einem entpackten Release, wird dessen install.sh benutzt
statt eines Downloads: wer ein Paket ausgepackt hat, will nicht, dass ungefragt
ein anderes geladen wird. Mit --tag laesst sich eine Version festlegen.

Der Bootstrap steht wortgleich in beiden Dateien -- zwei eigenstaendige
Skripte koennen sich keine Datei teilen, und beide sollen allein genuegen. Ein
Hygienetest vergleicht die Bloecke, ein zweiter prueft, dass upgrade.sh kein
entpacktes Release mehr verlangt, ein dritter fuehrt den Block mit leerem PATH
aus und erwartet, dass er das fehlende Werkzeug benennt.

update.sh ruft install.sh nicht mehr per exec auf: dadurch lief die
Aufraeumfalle nie, und jedes Update liess ein entpacktes Release unter /tmp
liegen.

Ausserdem "opus" aus allen Beispielen genommen. Das war der Name der
Testinstanz neben der Fassung 1.x; seit dem Umbenennen heissen die
Installationen wie ihre Anwendung, und in einer Hilfe, die jemand anders liest,
ist "opus" ein Raetsel. In BETRIEB.md stand dabei zweimal /opt statt /srv.

Geprueft am System, nicht behauptet: die einzelne upgrade.sh hat eine echte
1.2.8 (Kopie des Archivs) **in dasselbe Verzeichnis** auf 2.0.8 gebracht --
Release geladen, 4 Clients, 2 Zugangsdaten und die DHCP-Konfiguration
uebernommen, Dienst laeuft, /login 200. Die Sicherung enthaelt die 1.2.8 samt
sqlite.db und fernet.key (Verzeichnis 0700, Passphrase 0600), und /tmp bleibt
leer. Danach dieselbe Probe mit der einzelnen update.sh.

Version 2.0.9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:18:52 +02:00
alientimandClaude Opus 5 3bbddd7d29 upgrade.sh findet die Fassung 1.x auch im Archiv
Nach einem Umbenennen liegt unter /srv/<app> die laufende Installation und die
Fassung 1.x unter /srv/<app>-sonnet5 -- dorthin verschiebt rename.sh sie. Die
Vorgabe von upgrade.sh zeigte danach auf die neue Installation und brach ab
(sauber, aber unbrauchbar). Gesucht wird jetzt an beiden Orten; erkannt wird
eine 1.x an der sqlite.db im Wurzelverzeichnis.

Den Weg selbst habe ich gegen die echten Altdaten geprueft, nicht behauptet:
aus /srv/tesm-sonnet5 (Version 1.2.8) kamen 4 Clients, 2 Zugangsdaten und die
DHCP-Konfiguration in eine Probeinstanz, aus /srv/tesm-license-sonnet5
(Version 1.2.7) 2 Kunden, 2 Tickets und 2 neu signierte Lizenzen samt
uebernommenem Signaturschluessel und Anbieterblock. Die laufenden
Installationen blieben unberuehrt, die Probeinstanzen sind abgeraeumt.

Zwei Tests halten die beiden Skripte zusammen: derselbe Namenszusatz, und die
Suche laeuft ueber die echten Zeilen aus upgrade.sh.

Version 2.0.8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:53:36 +02:00
alientimandClaude Opus 5 7407f4bc00 Anmeldeseite: Wortmarke fuer beide, Karte ohne Wiederholung
* Die Wortmarke stand nur bei TESM -- sie hing an der Funktionszeile, und die
  hatte der Lizenzserver nicht. Jetzt zeigen beide die Marke; die Zeile
  darunter ist optional und beim Lizenzserver "LIZENZIEREN".
* Der Kartenkopf ist weg: eine Kachel mit zwei Buchstaben, die Ueberschrift
  "Anmelden" und der Anwendungsname darunter sagten dasselbe wie die Marke
  darueber und der Knopf darunter. Die Ueberschrift bleibt fuer die
  Titelzeile des Browsers.
* Marke und Karte stehen jetzt uebereinander im Fluss statt uebereinander
  gestapelt: auf einem flachen Fenster schnitt die Karte mitten durch den
  Schriftzug.
* Die Marke ist kuerzer und symmetrisch um die Mitte aufgebaut -- Schriftzug
  ueber text-anchor, Zacken spiegelbildlich, Linie links so lang wie der Punkt
  rechts. Volle Deckkraft, weil sie kein Wasserzeichen hinter der Karte mehr
  ist.
* Funktionszeile gekuerzt auf "UEBERWACHEN . BOOTEN . ANBINDEN" -- mit echtem
  Umlaut. "(PXE)" stand fuer etwas, das es noch nicht gibt.
* Der Farbverlauf im Hintergrund hat wieder Geometrie und Staerke der Fassung
  1.x. Der zweite Kreis lag auf --info und 10 %: ein kaltes Blau, das man auf
  dunklem Grund nicht sah und das nicht zur Marke gehoert.
* "Bitte melden Sie sich an." stand auf einer Seite aus Anmeldefeld,
  Passwortfeld und einem Knopf "Anmelden". Statt dessen erfaehrt jetzt der,
  dessen Sitzung gerade ablief, warum er wieder hier ist.

Dabei drei Dinge gefunden, die lautlos nichts taten:

* __version__ stand fest verdrahtet in drei __init__.py. Die Fusszeile zeigte
  vier Releases lang 2.0.2, waehrend die Anwendung 2.0.6 war. Die Nummer kommt
  jetzt aus den Metadaten des installierten Pakets; Quelle ist pyproject.toml.
* "gap: var(--sp-7)" -- die Skala kennt kein --sp-7 (1 bis 6, dann 8, 10, 12,
  16). Eine undefinierte Variable verwirft die ganze Deklaration ohne Meldung:
  der Abstand war "normal", und Marke und Karte stiessen auf den Pixel
  aneinander.
* Ein Test dagegen fand sofort ein zweites: --surface-0 wird dreimal benutzt
  und nirgends definiert. Die Schrift auf der Kachel "kritisch" blieb
  dunkelgrau auf Rot, die Dateivorschau hatte keinen Grund, und das Feld
  "Recht nicht vorhanden" in der Rechtematrix war durchsichtig -- also so hell
  wie die Karte, obwohl es dunkler sein sollte.

Version 2.0.7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:44:32 +02:00
19 changed files with 451 additions and 148 deletions
+16
View File
@@ -152,3 +152,19 @@ anders gelöst ist. Die wichtigsten:
* **Zwei Installationen stören sich nicht** -- eigener `SITE_KEY` für
nginx-Site, netplan, logrotate, Zertifikate und Sitzungscookie.
* **Verschachtelte Datenbankverbindungen sind ein Fehler**, kein Deadlock.
---
## Von einer Installation der Fassung 1.x
Eine einzelne Datei genügt -- sie lädt das Release selbst:
```bash
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
sudo bash upgrade.sh --app tesm
```
Sichert die alte Installation, schaltet sie ab, installiert diese Fassung und
übernimmt die Daten. Einzelheiten und was **nicht** mitkommt:
[docs/BETRIEB.md](docs/BETRIEB.md).
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "tesm"
version = "2.0.6"
version = "2.0.9"
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
requires-python = ">=3.11"
dependencies = [
+20 -5
View File
@@ -23,7 +23,20 @@ from .permissions import PERMISSIONS, ROLE_PRESETS
from .schema import TESM_MIGRATIONS
from .settings_spec import TESM_SETTINGS
__version__ = "2.0.2"
#: Version aus den Metadaten des installierten Pakets -- die einzige Quelle
#: ist ``pyproject.toml``. Vorher stand hier eine feste Zahl, und die
#: Fusszeile zeigte vier Releases lang 2.0.2.
#:
#: Im Quellbaum ohne ``pip install`` gibt es keine Metadaten; dann steht dort
#: "unbekannt" statt einer erfundenen Nummer. In einem editierbaren Install
#: zeigt sie den Stand des letzten ``pip install -e`` -- also die Version, die
#: tatsaechlich installiert ist.
try: # pragma: no cover - haengt an der Installationsart
from importlib.metadata import PackageNotFoundError, version as _paketversion
__version__ = _paketversion("tesm")
except (ImportError, PackageNotFoundError): # pragma: no cover
__version__ = "unbekannt"
APP_KEY = "tesm"
APP_NAME = "TESM"
@@ -32,10 +45,12 @@ APP_EDITION = "Geraeteverwaltung"
#: Funktionszeile unter der Wortmarke auf der Anmeldeseite. Uebernommen vom
#: Vorgaenger, wo sie Teil des Hintergrundmotivs war -- sie sagt in einer Zeile,
#: was die Anwendung tut, und das gehoert auf die erste Seite.
AUTH_TAGLINE = (
"UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · "
"BETRIEBSSYSTEM GEBEN (PXE)"
)
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Kurz gehalten: sie ist
#: eine Marke, keine Funktionsliste. "(PXE)" stand ausserdem fuer etwas, das
#: es noch nicht gibt -- das Anmeldebanner hat es aus demselben Grund nie
#: gezeigt. Wortgleich in ``deploy/tesm-motd``; ein Hygienetest haelt beides
#: zusammen.
AUTH_TAGLINE = "ÜBERWACHEN · BOOTEN · ANBINDEN"
ENV_PREFIX = "TESM_"
logger = logging.getLogger(__name__)
+3 -3
View File
@@ -3,8 +3,8 @@
# Benennt eine Installation um -- aus einer Instanz wird die Hauptinstallation
# (oder umgekehrt).
#
# sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus \
# sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test \
# --to tesm-license
#
# Der Name steckt an mehr Stellen, als man beim Verschieben eines Verzeichnisses
@@ -51,7 +51,7 @@ usage() {
Verwendung: rename.sh --app <tesm|tesm-license> --from <alt> --to <neu> [Optionen]
--app <name> Welche Anwendung (Pflicht)
--from <name> Bisheriger Installationsname, z.B. tesm-opus (Pflicht)
--from <name> Bisheriger Installationsname, z.B. tesm-test (Pflicht)
--to <name> Neuer Installationsname, z.B. tesm (Pflicht)
--suffix <text> Namenszusatz fuer eine beiseite geschobene Installation
der Fassung 1.x (Vorgabe: sonnet5)
+1 -1
View File
@@ -47,7 +47,7 @@ check_in_list() {
}
# Name einer Installation auf diesem Host: entweder die Hauptinstallation
# ("tesm") oder eine benannte Instanz daneben ("tesm-opus"). Mehr als diese
# ("tesm") oder eine benannte Instanz daneben ("tesm-test"). Mehr als diese
# beiden Formen gibt es nicht -- so kann keine erfundene Zeichenkette einen
# Pfad ausserhalb des vorgesehenen Namensraums erzeugen.
check_app_key() {
+5 -8
View File
@@ -34,15 +34,12 @@ RESET = "\033[0m"
#: Dieselbe Datei liest die Anwendung (``services/kea.py``: ``LEASE_FILE``).
LEASE_FILE = "/var/lib/kea/kea-leases4.csv"
#: Zeile unter der Wortmarke. Wortlaut wie in der Anwendung
#: (``apps/tesm/src/tesm/__init__.py``: ``AUTH_TAGLINE``) -- ein Hygienetest
#: haelt beides zusammen.
#:
#: Der Lizenzserver hat bewusst keine: die Funktionsliste von TESM waere dort
#: falsch, und eine erfundene waere schlimmer als keine.
#: Zeile unter der Wortmarke. Wortlaut wie in der jeweiligen Anwendung
#: (``AUTH_TAGLINE`` in deren ``__init__.py``) -- ein Hygienetest haelt beides
#: zusammen.
TAGLINE = {
"tesm": "UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · BETRIEBSSYSTEM GEBEN (PXE)",
"tesm-license": "",
"tesm": "ÜBERWACHEN · BOOTEN · ANBINDEN",
"tesm-license": "LIZENZIEREN",
}
TITEL = {"tesm": "TimEShepManager", "tesm-license": "Lizenzserver"}
+92 -54
View File
@@ -1,89 +1,127 @@
#!/usr/bin/env bash
# =============================================================================
# Bootstrapper: laedt ein Release herunter und ruft dessen install.sh auf.
# Aktualisierung: laedt das aktuelle Release und ruft dessen install.sh auf.
# Enthaelt selbst keinerlei Installationslogik.
#
# Zwei Fallen des Vorgaengers sind hier bewusst vermieden:
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Assets.
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert -- faengt
# denselben Fehler ein zweites Mal ab.
# Eine einzelne Datei genuegt -- sie holt sich alles Weitere:
#
# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
# sudo bash update.sh tesm
#
# Auf einer bestehenden Installation liegt sie schon bereit:
#
# sudo bash /srv/tesm/deploy/update.sh tesm
# =============================================================================
set -euo pipefail
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel
# unbrauchbar ("bad interpreter"). Die Pruefung darf dabei nicht selbst
# von einem Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken
# Server fehlt -- endete sie in "command not found", grep bekam nichts zu
# sehen, und das Skript machte weiter, als waere alles in Ordnung.
hat_crlf() {
local cr
cr="$(printf '\r')"
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer Windows-Buildmaschine
# aber entscheidend: dort entfernt grep den Wagenruecklauf beim Lesen und
# findet deshalb nie einen.
LC_ALL=C grep -qU "$cr" "$1"
}
APP="${1:-}"
TAG="${2:-latest}"
# Alles nach dem Tag geht unveraendert an install.sh weiter. Ohne das liesse
# sich eine benannte Instanz nicht aktualisieren: install.sh leitet den Namen
# aus "--instance" ab, und ohne den Schalter entstuende neben "tesm-opus" eine
# aus "--instance" ab, und ohne den Schalter entstuende neben "tesm-test" eine
# zweite Installation namens "tesm" -- mit eigener Datenbank, eigener
# nginx-Site und demselben Port.
shift $(( $# > 2 ? 2 : $# ))
EXTRA=("$@")
# Bezugsquelle. Die Vorgaben zeigen dorthin, wo die Releases tatsaechlich
# liegen -- ein Platzhalter wie "gitea.example.invalid" macht das Skript auf
# jedem Host unbrauchbar, und genau das war hier der Fall.
#
# Das Repository heisst wie die Anwendung; getrennte Repositorien, getrennte
# Releases. Der Anhang heisst "<repo>-<tag>.tar.gz", siehe unten.
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
OWNER="${TESM_RELEASE_OWNER:-alientim}"
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || {
echo "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
echo "Beispiel : update.sh tesm latest --instance opus" >&2
echo "Beispiel : update.sh tesm latest --instance test" >&2
exit 2
}
REPO="${TESM_RELEASE_REPO:-$APP}"
# --8<-- Bootstrap: Release holen --------------------------------------------
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
#
# Erwartet: $APP und $TAG. Setzt: $TAG (aufgeloest) und $SRC (entpacktes
# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf.
#
# Zwei Fallen des Vorgaengers sind hier vermieden:
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs.
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert.
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
OWNER="${TESM_RELEASE_OWNER:-alientim}"
WORK="$(mktemp -d /tmp/tesm-update-XXXXXX)"
trap 'rm -rf "$WORK"' EXIT # anders als im Vorgaenger wird aufgeraeumt
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
# endete sie in "command not found", grep bekam nichts zu sehen, und das Skript
# machte weiter, als waere alles in Ordnung.
hat_crlf() {
local cr
cr="$(printf '\r')"
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer
# Windows-Buildmaschine aber entscheidend: dort entfernt grep den
# Wagenruecklauf beim Lesen und findet deshalb nie einen.
LC_ALL=C grep -qU "$cr" "$1"
}
if [[ "$TAG" == "latest" ]]; then
BOOTSTRAP_WORK=""
bootstrap_aufraeumen() { [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK"; }
trap bootstrap_aufraeumen EXIT
release_holen() {
local repo="${TESM_RELEASE_REPO:-$APP}" asset
for werkzeug in curl tar python3 find; do
command -v "$werkzeug" >/dev/null 2>&1 || {
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
return 1
}
done
BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)"
if [[ "$TAG" == "latest" ]]; then
echo "==> Ermittle aktuellen Release-Tag ..."
TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$REPO/releases/latest" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')"
TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2
echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
return 1
}
echo " $TAG"
fi
fi
ASSET="$REPO-$TAG.tar.gz"
echo "==> Lade $ASSET ..."
curl -fsSL -o "$WORK/$ASSET" "$BASE/$OWNER/$REPO/releases/download/$TAG/$ASSET"
asset="$repo-$TAG.tar.gz"
echo "==> Lade $asset ..."
curl -fsSL -o "$BOOTSTRAP_WORK/$asset" \
"$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
return 1
}
echo "==> Entpacke ..."
tar -xzf "$WORK/$ASSET" -C "$WORK"
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
SRC="$(find "$WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' -printf '%h/..\n' | head -1)"
[[ -n "$SRC" ]] || { echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2; exit 1; }
SRC="$(cd "$SRC" && pwd)"
echo "==> Entpacke ..."
tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK"
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \
-printf '%h/..\n' | head -1)"
[[ -n "$SRC" ]] || {
echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2
return 1
}
SRC="$(cd "$SRC" && pwd)"
echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..."
if hat_crlf "$SRC/deploy/install.sh"; then
echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..."
if hat_crlf "$SRC/deploy/install.sh"; then
echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2
echo "'git -c core.autocrlf=false archive'." >&2
exit 1
fi
return 1
fi
}
# --8<-- Ende Bootstrap ------------------------------------------------------
release_holen
read -r -p "Version $TAG fuer $APP${EXTRA[0]+ (${EXTRA[*]})} installieren? [j/N] " answer
[[ "$answer" =~ ^[jJyY]$ ]] || exit 0
exec sudo bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}
# Kein exec: sonst laeuft die Aufraeumfalle nie, und jedes Update liesse ein
# entpacktes Release unter /tmp liegen.
bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}
+151 -9
View File
@@ -2,10 +2,17 @@
# =============================================================================
# Upgrade vom Stand SONNET5 (Version 1.x) auf diese Fassung -- mit Datenuebernahme.
#
# Aufruf (aus dem entpackten Release-Verzeichnis):
# Eine einzelne Datei genuegt -- sie holt sich das Release selbst:
#
# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
# sudo bash upgrade.sh --app tesm
#
# Auf einer 1.2.8 liegt nichts von diesem Projekt; alles andere waere die
# falsche Voraussetzung. Liegt das Skript in einem entpackten Release, wird
# dessen install.sh benutzt statt eines Downloads:
#
# sudo bash deploy/upgrade.sh --app tesm
# sudo bash deploy/upgrade.sh --app tesm-license
# sudo bash deploy/upgrade.sh --app tesm --instance opus --source /srv/tesm
# sudo bash deploy/upgrade.sh --app tesm --instance test --source /srv/tesm
#
# Der Ablauf ist die Vorgabe und braucht keine Schalter:
#
@@ -44,6 +51,19 @@ DRY_RUN=0
WITH_DATA=1
STOP_OLD=1
ADOPT_KEY=1
TAG="latest"
#: Namenszusatz, unter dem ``rename.sh`` eine verdraengte Installation der
#: Fassung 1.x ablegt. Beide Skripte muessen denselben Wert benutzen; ein
#: Hygienetest vergleicht sie.
#:
#: Auf einer echten 1.2.8 gibt es das **nicht** -- dort liegt die Installation
#: unter ihrem Hauptnamen. Der Zusatz entsteht erst, wenn ``rename.sh`` den
#: Hauptnamen fuer die neue Fassung braucht. Deshalb ist er der zweite Ort, an
#: dem gesucht wird, nicht der erste.
ARCHIV_SUFFIX="sonnet5"
#: Nur fuer die Ausgabe: wurde die Quelle selbst gefunden oder angegeben?
info_quelle=""
EXTRA=()
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -73,6 +93,8 @@ Verwendung: upgrade.sh --app <tesm|tesm-license> [Optionen]
erzeugen, statt den alten zu uebernehmen. Dann muessen
alle Kunden den Ausstellerwechsel bestaetigen -- die
Vorgabe ist deshalb die Uebernahme.
--tag <vX.Y.Z> Bestimmte Version installieren (Vorgabe: latest).
Wird nur beachtet, wenn das Release geladen wird.
--dry-run Nur zeigen, was geschehen wuerde
--yes Keine Rueckfrage
-h, --help Diese Hilfe
@@ -85,7 +107,7 @@ Beispiele:
sudo bash deploy/upgrade.sh --app tesm
# Daneben, zum Vergleich, alte Installation bleibt unangetastet:
sudo bash deploy/upgrade.sh --app tesm --instance opus --keep-old-running
sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running
USAGE
}
@@ -99,6 +121,7 @@ while [[ $# -gt 0 ]]; do
--keep-old-running) STOP_OLD=0; shift ;;
--new-key) ADOPT_KEY=0; shift ;;
--adopt-key) ADOPT_KEY=1; shift ;; # Vorgabe, hier nur zur Deutlichkeit
--tag) TAG="${2:?}"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
--yes|-y) ASSUME_YES=1; shift ;;
-h|--help) usage; exit 0 ;;
@@ -108,11 +131,129 @@ done
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; }
[[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren"
[[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript"
[[ -n "$SOURCE" ]] || SOURCE="/srv/$APP"
# --8<-- Bootstrap: Release holen --------------------------------------------
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
#
# Erwartet: $APP und $TAG. Setzt: $TAG (aufgeloest) und $SRC (entpacktes
# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf.
#
# Zwei Fallen des Vorgaengers sind hier vermieden:
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs.
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert.
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
OWNER="${TESM_RELEASE_OWNER:-alientim}"
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
# endete sie in "command not found", grep bekam nichts zu sehen, und das Skript
# machte weiter, als waere alles in Ordnung.
hat_crlf() {
local cr
cr="$(printf '\r')"
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer
# Windows-Buildmaschine aber entscheidend: dort entfernt grep den
# Wagenruecklauf beim Lesen und findet deshalb nie einen.
LC_ALL=C grep -qU "$cr" "$1"
}
BOOTSTRAP_WORK=""
bootstrap_aufraeumen() { [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK"; }
trap bootstrap_aufraeumen EXIT
release_holen() {
local repo="${TESM_RELEASE_REPO:-$APP}" asset
for werkzeug in curl tar python3 find; do
command -v "$werkzeug" >/dev/null 2>&1 || {
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
return 1
}
done
BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)"
if [[ "$TAG" == "latest" ]]; then
echo "==> Ermittle aktuellen Release-Tag ..."
TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2
echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
return 1
}
echo " $TAG"
fi
asset="$repo-$TAG.tar.gz"
echo "==> Lade $asset ..."
curl -fsSL -o "$BOOTSTRAP_WORK/$asset" \
"$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
return 1
}
echo "==> Entpacke ..."
tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK"
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \
-printf '%h/..\n' | head -1)"
[[ -n "$SRC" ]] || {
echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2
return 1
}
SRC="$(cd "$SRC" && pwd)"
echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..."
if hat_crlf "$SRC/deploy/install.sh"; then
echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2
echo "'git -c core.autocrlf=false archive'." >&2
return 1
fi
}
# --8<-- Ende Bootstrap ------------------------------------------------------
# Woher kommt install.sh? Liegt dieses Skript in einem entpackten Release,
# nimm dessen Verzeichnis -- wer ein Paket ausgepackt hat, will nicht, dass
# ungefragt ein anderes geladen wird. Sonst hol das Release.
if [[ -f "$SCRIPT_DIR/install.sh" ]]; then
SRC="$REPO_DIR"
RELEASE_HERKUNFT="$SRC (entpacktes Release neben diesem Skript)"
else
release_holen || fail "Das Release liess sich nicht holen."
RELEASE_HERKUNFT="$TAG von $BASE/$OWNER"
fi
# -- Wo liegt die alte Fassung? ---------------------------------------------- #
# Zwei Orte kommen in Frage, und der zweite ist der Grund fuer diesen Block:
# ``rename.sh`` schiebt eine Installation der Fassung 1.x nach
# ``/srv/<app>-sonnet5``, sobald der Hauptname fuer die neue gebraucht wird.
# Danach liegt unter ``/srv/<app>`` die **laufende** Installation, und die
# Vorgabe zeigte ins Leere. Erkannt wird an ``sqlite.db`` im Wurzelverzeichnis
# -- die neue Fassung legt ihre Datenbank unter ``data/app.db`` ab.
ist_alte_fassung() { [[ -f "$1/sqlite.db" ]]; }
if [[ -z "$SOURCE" ]]; then
for kandidat in "/srv/$APP" "/srv/$APP-$ARCHIV_SUFFIX"; do
if ist_alte_fassung "$kandidat"; then SOURCE="$kandidat"; break; fi
done
if [[ -z "$SOURCE" ]]; then
warn "Keine Installation der Fassung 1.x gefunden. Gesucht wurde in:"
warn " /srv/$APP (dort liegt sie normalerweise)"
warn " /srv/$APP-$ARCHIV_SUFFIX (dorthin verschiebt rename.sh sie, falls"
warn " der Hauptname schon vergeben war)"
[[ -f "/srv/$APP/data/app.db" ]] && warn "Unter /srv/$APP liegt die laufende Installation dieser Fassung."
fail "Bitte --source setzen."
fi
info_quelle="gefunden: $SOURCE"
fi
[[ -d "$SOURCE" ]] || fail "Die alte Installation $SOURCE gibt es nicht (--source setzen)"
[[ -f "$SOURCE/sqlite.db" ]] || fail "In $SOURCE liegt keine sqlite.db -- das ist keine Installation der Vorgaengerfassung"
ist_alte_fassung "$SOURCE" || fail "In $SOURCE liegt keine sqlite.db -- das ist keine Installation der Vorgaengerfassung"
NAME="$APP"
[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE"
@@ -138,7 +279,8 @@ fi
step "Vorhaben"
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
info "Alt : $SOURCE"
info "Neue Fassung : $RELEASE_HERKUNFT"
info "Alt : $SOURCE${info_quelle:+ (automatisch gefunden)}"
info "Neu : $NEW_ROOT$([[ $IN_PLACE -eq 1 ]] && echo ' << dasselbe Verzeichnis')"
info "Sicherung : $ALT_KOPIE"
info "Datenuebernahme: $([[ $WITH_DATA -eq 1 ]] && echo ja || echo nein)"
@@ -186,7 +328,7 @@ fi
step "Neu installieren"
INSTALL_ARGS=(--app "$APP" --yes)
[[ -n "$INSTANCE" ]] && INSTALL_ARGS+=(--instance "$INSTANCE")
bash "$SCRIPT_DIR/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"}
bash "$SRC/deploy/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"}
if [[ $WITH_DATA -eq 0 ]]; then
step "Fertig -- ohne Datenuebernahme"
+38 -9
View File
@@ -105,19 +105,26 @@ Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne
### Ohne Quellbaum: `update.sh`
Liegt kein entpacktes Release vor, holt `update.sh` es selbst -- es lädt das
Release-Asset, entpackt es und ruft dessen `install.sh` auf:
`update.sh` genügt als **einzelne Datei**. Auf einem System, auf dem noch nichts
von diesem Projekt liegt:
```bash
sudo bash /opt/tesm/deploy/update.sh tesm
sudo bash /opt/tesm-license/deploy/update.sh tesm-license
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
sudo bash update.sh tesm
```
Auf einer bestehenden Installation liegt sie schon bereit:
```bash
sudo bash /srv/tesm/deploy/update.sh tesm
sudo bash /srv/tesm-license/deploy/update.sh tesm-license
```
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
eine **benannte Instanz** zu aktualisieren:
```bash
sudo bash /opt/tesm-opus/deploy/update.sh tesm latest --instance opus
sudo bash /srv/tesm/deploy/update.sh tesm
```
Ohne `--instance` entstünde daneben eine zweite Installation namens `tesm`, mit
@@ -147,11 +154,33 @@ Ein eigenes Skript, weil dabei mehr passiert als ein Dateiabgleich: die alte
Fassung hat ein anderes Schema, andere Rechteschlüssel und ein anderes
Exportformat.
Auch `upgrade.sh` genügt als **einzelne Datei** -- und das ist hier der
Regelfall: auf einer 1.2.8 liegt nichts von dieser Fassung.
```bash
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
sudo bash upgrade.sh --app tesm
```
Sie lädt das aktuelle Release, entpackt es und benutzt dessen `install.sh`.
Liegt das Skript in einem entpackten Release, wird dieses benutzt statt eines
Downloads -- wer ein Paket ausgepackt hat, will nicht, dass ungefragt ein
anderes geladen wird:
```bash
sudo bash deploy/upgrade.sh --app tesm
sudo bash deploy/upgrade.sh --app tesm-license
```
Eine bestimmte Version statt `latest`: `--tag v2.0.8`.
Ohne `--source` wird die alte Fassung an zwei Stellen gesucht: unter
`/srv/<app>` und unter `/srv/<app>-sonnet5`. Der zweite Ort ist der, an den
`rename.sh` sie verschiebt, sobald der Hauptname für die neue Installation
gebraucht wird -- danach liegt unter `/srv/<app>` die **laufende** Installation.
Erkannt wird eine 1.x an der `sqlite.db` im Wurzelverzeichnis; die neue Fassung
legt ihre Datenbank unter `data/app.db` ab.
Der Ablauf ist die Vorgabe und braucht keine Schalter:
| | |
@@ -177,7 +206,7 @@ Export des Vorgängers und taugt deshalb auch als Rückweg.
und die alte bleibt vollständig liegen:
```bash
sudo bash deploy/upgrade.sh --app tesm --instance opus --keep-old-running --port 5100 --http-port 8080
sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running --port 5100 --http-port 8080
```
**Was nicht mitkommt** -- und warum:
@@ -273,7 +302,7 @@ die niemand mehr füllte.
Von Hand prüfen:
```bash
sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-opus
sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-test
```
Der Aufruf im Banner ist mit `timeout 3` und `|| true` abgesichert: ein
@@ -288,8 +317,8 @@ entfernt -- nur dieses, nichts anderes in dem Verzeichnis.
Aus einer Instanz die Hauptinstallation machen (oder umgekehrt):
```bash
sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus --to tesm-license
sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test --to tesm-license
```
Der Name steckt an mehr Stellen als im Verzeichnisnamen, und drei davon sind
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "tesm-core"
version = "2.0.6"
version = "2.0.9"
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11"
dependencies = [
+14 -1
View File
@@ -8,7 +8,20 @@ Design-System.
from __future__ import annotations
__version__ = "2.0.2"
#: Version aus den Metadaten des installierten Pakets -- die einzige Quelle
#: ist ``pyproject.toml``. Vorher stand hier eine feste Zahl, und die
#: Fusszeile zeigte vier Releases lang 2.0.2.
#:
#: Im Quellbaum ohne ``pip install`` gibt es keine Metadaten; dann steht dort
#: "unbekannt" statt einer erfundenen Nummer. In einem editierbaren Install
#: zeigt sie den Stand des letzten ``pip install -e`` -- also die Version, die
#: tatsaechlich installiert ist.
try: # pragma: no cover - haengt an der Installationsart
from importlib.metadata import PackageNotFoundError, version as _paketversion
__version__ = _paketversion("tesm-core")
except (ImportError, PackageNotFoundError): # pragma: no cover
__version__ = "unbekannt"
from .app_factory import create_base_app
from .config import CoreConfig, SecurityConfig, load_core_config
@@ -130,11 +130,11 @@ def create_base_app(
import_name: str = "tesm_core",
app_short: str = "TESM",
app_edition: str = "",
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Leer = kein
#: Seitenhintergrund. Der Lizenzserver setzt sie bewusst nicht: die
#: Funktionsliste von TESM waere dort schlicht falsch.
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Leer = nur die
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
#: Anwendungen.
auth_tagline: str = "",
app_version: str = "2.0.6",
app_version: str = "2.0.9",
built_in_trusted_keys: tuple[str, ...] = (),
enable_license: bool = True,
template_folder: str | None = None,
@@ -200,7 +200,12 @@ def _deny(message: str) -> Any:
abort(401, description="Sitzung abgelaufen.")
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
abort(401, description="Anmeldung erforderlich.")
flash("Bitte melden Sie sich an.", "info")
# "Bitte melden Sie sich an." stand auf einer Seite, die aus einem
# Anmeldefeld, einem Passwortfeld und einem Knopf "Anmelden" besteht --
# eine Meldung, die nichts hinzufuegt. Wer dagegen gerade noch
# angemeldet war, soll erfahren, warum er es nicht mehr ist.
if getattr(g, "session_expired", False):
flash("Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", "info")
return redirect(url_for("auth.login"))
abort(403, description=message)
+1 -1
View File
@@ -76,7 +76,7 @@ class CoreConfig:
"""Technischer Kurzname, z. B. ``tesm`` -- Namensraum fuer Pfade und Units."""
site_key: str
"""Name *dieser Installation* auf dem Host, z. B. ``tesm-opus``.
"""Name *dieser Installation* auf dem Host, z. B. ``tesm-test``.
Weicht vom ``app_key`` ab, sobald neben der Hauptinstallation eine zweite
Instanz laeuft. Alles, was ausserhalb von ``/srv/<name>`` liegt und damit
@@ -26,7 +26,7 @@ import sqlite3
from datetime import datetime, timedelta, timezone
from typing import Any
from flask import Flask, Request, Response
from flask import Flask, Request, Response, g
from flask import request as aktuelle_anfrage
from flask.sessions import SessionInterface, SessionMixin
from werkzeug.datastructures import CallbackDict
@@ -209,6 +209,11 @@ class SqliteSessionInterface(SessionInterface):
"WHERE id=?",
(_iso(now), sid),
)
# Fuer die Anmeldeseite vermerken, **warum** sie erscheint. Ohne
# das steht dort entweder nichts -- dann wundert sich, wer
# gerade noch angemeldet war -- oder eine Meldung, die jeder
# Besucher zu sehen bekommt und die deshalb nichts sagt.
g.session_expired = True
return self.session_class(new=True)
try:
@@ -412,7 +412,10 @@
und beide sind rot. */
.pill--critical {
background: var(--danger);
color: var(--surface-0);
/* Weiss wie bei .btn--danger. Vorher stand hier ein Token, das es nicht
gibt -- die Deklaration war damit unwirksam, und die Schrift blieb in
der Textfarbe: dunkelgrau auf Rot. */
color: #fff;
font-weight: 700;
}
.pill--critical::before { background: currentColor; }
@@ -695,7 +698,9 @@
width: 100%;
height: min(70dvh, 760px);
border: 0;
background: var(--surface-0);
/* Die Vorschau zeigt fremde Dokumente; ein neutraler Grund gehoert
darunter. --surface-0 gibt es nicht, der Rahmen blieb durchsichtig. */
background: var(--surface-1);
}
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
@@ -849,7 +854,10 @@
width: 17px;
height: 17px;
border-radius: 5px;
background: var(--surface-0);
/* Dunkler als die Karte -- genau das war die Absicht: "dunkler heisst,
es gibt das Recht nicht". Mit dem nicht existierenden --surface-0 war
das Feld durchsichtig und damit so hell wie die Karte. */
background: var(--canvas);
border: 1px solid var(--border-subtle);
box-shadow: inset 0 1px 2px rgb(0 0 0 / 28%);
vertical-align: -3px;
@@ -943,9 +951,15 @@
display: grid;
place-items: center;
padding: var(--sp-6);
/* Zwei warme Kreise, Geometrie und Staerke aus der Fassung 1.x
uebernommen: oben links deutlich, unten rechts angedeutet. Die vorige
Fassung setzte den zweiten Kreis auf --info und 10 % -- ein kaltes
Blau, das man auf dunklem Grund nicht sah und das nicht zur Marke
gehoert. Ueber color-mix statt festem rgba, damit es im hellen Thema
nicht schmutzig wird. */
background:
radial-gradient(1100px 520px at 12% -10%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%),
radial-gradient(900px 480px at 100% 110%, color-mix(in srgb, var(--info) 10%, transparent), transparent 70%),
radial-gradient(circle at 15% 15%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 45%),
radial-gradient(circle at 85% 85%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 40%),
var(--canvas);
}
/* Die Wortmarke liegt hinter allem (z-index 0), die Anmeldebox davor.
@@ -1026,24 +1040,35 @@
color: var(--text-faint);
}
.auth-page { position: relative; overflow: hidden; align-content: center; }
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein
Stapel kann nicht kollidieren, und die Seite darf notfalls scrollen. */
.auth-stack {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-8);
width: 100%;
}
.auth-backdrop {
position: absolute;
top: 4vh;
left: 50%;
transform: translateX(-50%);
width: min(72vw, 880px);
pointer-events: none;
z-index: 0;
opacity: 0.5;
/* Volle Deckung: solange die Marke hinter der Karte lag, war sie ein
Wasserzeichen und musste zurueckhaltend sein. Jetzt steht sie ueber der
Karte und ist das Erste, was man sieht -- halbtransparent sah sie dort
nur blass aus. Die Farben kommen ohnehin aus zurueckhaltenden Token. */
opacity: 1;
}
.auth-backdrop svg { width: 100%; height: auto; display: block; }
@media (max-width: 640px) {
.auth-backdrop { width: min(90vw, 460px); opacity: 0.32; }
.auth-backdrop { width: min(90vw, 460px); }
}
/* Wer wenig Bewegung/Reize moechte, bekommt das Motiv dezenter. */
/* Wer weniger Reize moechte, bekommt die Marke ruhiger -- aber nicht
unsichtbar: sie ist die Ueberschrift der Seite. */
@media (prefers-contrast: more) {
.auth-backdrop { opacity: 0.18; }
.auth-backdrop { opacity: 0.75; }
}
.auth-card {
@@ -1,6 +1,9 @@
{% extends "tesm_core/standalone.html" %}
{# Die Ueberschrift bleibt fuer die Titelzeile des Browsers -- sichtbar ist sie
hier nicht: der leere ``brand``-Block laesst Kachel, Ueberschrift und
Unterzeile weg. Die Wortmarke darueber sagt schon, wo man ist. #}
{% block heading %}Anmelden{% endblock %}
{% block subheading %}{{ app_name }} &ndash; {{ app_edition }}{% endblock %}
{% block brand %}{% endblock %}
{% block content %}
<form method="post" class="stack" data-no-guard autocomplete="on">
@@ -22,37 +22,51 @@
</head>
<body data-csrf="{{ csrf_token() }}">
<div class="auth-page">
{% if auth_tagline %}
{# Wortmarke mit Funktionszeile als tatsächlicher Seitenhintergrund --
übernommen vom Vorgänger. Bewusst inline und mit Themen-Tokens statt
zweier Dateien für hell und dunkel: bei der Einstellung "System" und
beim Umschalten von Hand wäre die versteckte Variante die falsche.
``aria-hidden``, weil der Name schon in der Überschrift steht. #}
<div class="auth-stack">
{# Wortmarke als Seitenhintergrund -- fuer beide Anwendungen. Bewusst inline
und mit Themen-Tokens statt zweier Dateien fuer hell und dunkel: bei der
Einstellung "System" und beim Umschalten von Hand waere die versteckte
Variante die falsche. ``aria-hidden``, weil der Name in der Titelzeile
steht.
Die Geometrie ist symmetrisch um die Mitte (x=260) aufgebaut, damit die
Marke ohne Nachrechnen mittig sitzt: der Schriftzug haengt an
``text-anchor="middle"``, die beiden Zacken liegen spiegelbildlich, und
die Linie beginnt links so weit vor der Mitte, wie der Punkt rechts nach
ihr endet. Wer die Linie verlaengert, muss beide Enden anfassen.
Ohne Funktionszeile (Lizenzserver) ist der Kasten kuerzer -- sonst
stuende unter der Marke ein leerer Streifen. #}
<div class="auth-backdrop" aria-hidden="true">
<svg viewBox="0 0 620 190" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(150,15)">
<circle cx="298" cy="70" r="15" fill="var(--logo-mark)" opacity="0.22"/>
<polyline points="0,70 90,70 100,70 108,20 116,120 124,70 134,70 200,70 210,70
218,35 226,105 234,70 244,70 300,70"
<svg viewBox="0 0 520 {{ 150 if auth_tagline else 116 }}" xmlns="http://www.w3.org/2000/svg">
<circle cx="452" cy="62" r="15" fill="var(--logo-mark)" opacity="0.22"/>
<polyline points="60,62 163,62 171,12 179,112 187,62 333,62 341,12 349,112 357,62 452,62"
fill="none" stroke="var(--logo-mark)" stroke-width="3.2"
stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
<text x="14" y="98" font-family="var(--font-mono)" font-weight="800"
font-size="82" letter-spacing="2" fill="var(--logo-text)">TESM</text>
<circle cx="298" cy="70" r="8" fill="var(--logo-mark)"/>
</g>
<text x="310" y="178" text-anchor="middle" font-family="var(--font-sans)"
<text x="260" y="90" text-anchor="middle" font-family="var(--font-mono)"
font-weight="800" font-size="82" letter-spacing="2"
fill="var(--logo-text)">TESM</text>
<circle cx="452" cy="62" r="8" fill="var(--logo-mark)"/>
{% if auth_tagline %}
<text x="260" y="140" text-anchor="middle" font-family="var(--font-sans)"
font-weight="600" font-size="13" letter-spacing="1.5"
fill="var(--logo-mark)">{{ auth_tagline }}</text>
{% endif %}
</svg>
</div>
{% endif %}
<div class="auth-card {% block card_class %}{% endblock %}">
{# Die Anmeldeseite laesst diesen Kopf weg: die Wortmarke darueber sagt
schon, wo man ist, und "Anmelden" steht auf dem Knopf. Die uebrigen
Standalone-Seiten behalten ihn -- "Zweiter Faktor" oder der Kundenname
im Portal sind dort die Auskunft, um die es geht. #}
{% block brand %}
<div class="auth-brand">
<span class="auth-brand__mark" aria-hidden="true">{{ app_short }}</span>
<h1>{% block heading %}{{ app_name }}{% endblock %}</h1>
{% set sub %}{% block subheading %}{% endblock %}{% endset %}
{% if sub | trim %}<p>{{ sub }}</p>{% endif %}
</div>
{% endblock %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
@@ -73,6 +87,7 @@
<p class="auth-footnote">{{ app_name }} {{ app_version }} &middot; &copy; {{ current_year }} {{ copyright_notice }}</p>
</div>
</div>
</div>
<script type="module" src="{{ asset_url('js/app.js') }}" nonce="{{ csp_nonce }}"></script>
{% block scripts %}{% endblock %}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "tesm-licensing"
version = "2.0.6"
version = "2.0.9"
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11"
dependencies = ["cryptography>=42"]