From fbbf8eb87b0022540686b422a94e1356d6e7168d Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Wed, 2 Sep 2026 23:54:33 +0200 Subject: [PATCH] Umlaute in der Oberflaeche, Sitzungskopplung, Statusanzeigen Echte Umlaute in allen Texten der Oberflaeche und in den Markdown-Dateien. Die Einbindung von Dateifreigaben haengt jetzt an der Sitzung: wird sie widerrufen oder laeuft sie ab, verschwinden die Freigaben mit ihr. Neu einstellbar ist die automatische Abmeldung bei Leerlauf (Vorgabe 30 Minuten). Ein Geraet, das nicht antwortet, bietet nur noch "Einschalten (PoE)" an -- Neustart ueber SSH oder RPC braucht ein erreichbares Geraet. Der Zeitgeber der naechsten Pruefung und die Restlaufzeit der Lizenz stehen jetzt oben in der Kopfzeile; der doppelte Knopf darunter ist weg. Millisekunden werden ab einer Sekunde als Sekunden und ab einer Minute als m:ss dargestellt. Ausserdem: Release-Tarball nicht mehr im Repository -- er haengt am Release. Co-Authored-By: Claude Opus 5 --- README.md | 68 ++++----- apps/tesm/pyproject.toml | 2 +- apps/tesm/src/tesm/__init__.py | 40 +++++- apps/tesm/src/tesm/blueprints/devices.py | 6 + apps/tesm/src/tesm/blueprints/fileshare.py | 35 +++++ apps/tesm/src/tesm/blueprints/maintenance.py | 5 +- apps/tesm/src/tesm/blueprints/switches.py | 1 - apps/tesm/src/tesm/navigation.py | 2 +- apps/tesm/src/tesm/services/fileshare.py | 90 +++++++++++- apps/tesm/src/tesm/services/monitor.py | 65 ++++++++- apps/tesm/src/tesm/services/restart.py | 21 ++- .../tesm/templates/tesm/_dashboard_tiles.html | 20 +-- apps/tesm/src/tesm/templates/tesm/backup.html | 46 +++---- .../tesm/src/tesm/templates/tesm/console.html | 16 +-- .../templates/tesm/credential_reveal.html | 4 +- .../src/tesm/templates/tesm/credentials.html | 20 +-- .../src/tesm/templates/tesm/dashboard.html | 19 +-- .../tesm/templates/tesm/device_detail.html | 48 ++++--- .../tesm/src/tesm/templates/tesm/devices.html | 51 +++---- apps/tesm/src/tesm/templates/tesm/dhcp.html | 52 +++---- .../src/tesm/templates/tesm/fileshare.html | 96 +++++++++++-- .../templates/tesm/fileshare_mappings.html | 41 +++++- .../src/tesm/templates/tesm/hostkeys.html | 44 +++--- .../tesm/src/tesm/templates/tesm/license.html | 54 ++++---- .../tesm/templates/tesm/logs_restarts.html | 18 +-- .../src/tesm/templates/tesm/maintenance.html | 68 ++++----- .../tesm/templates/tesm/maintenance_job.html | 8 +- .../tesm/templates/tesm/settings_system.html | 37 ++--- .../tesm/templates/tesm/switch_command.html | 2 +- .../tesm/templates/tesm/switch_detail.html | 36 ++--- .../src/tesm/templates/tesm/switches.html | 46 ++----- docs/BETRIEB.md | 130 +++++++++--------- docs/SICHERHEIT.md | 112 +++++++-------- packages/tesm-core/pyproject.toml | 2 +- packages/tesm-core/src/tesm_core/__init__.py | 2 +- .../tesm-core/src/tesm_core/app_factory.py | 34 ++++- .../tesm-core/src/tesm_core/auth/directory.py | 6 +- packages/tesm-core/src/tesm_core/config.py | 4 +- .../src/tesm_core/licensing_client.py | 52 +++++-- .../src/tesm_core/security/sessions.py | 17 ++- .../static/tesm_core/css/components.css | 66 +++++++++ .../src/tesm_core/static/tesm_core/js/app.js | 14 +- .../templates/tesm_core/_group_picker.html | 14 +- .../templates/tesm_core/account.html | 4 +- .../templates/tesm_core/account_security.html | 20 +-- .../tesm_core/templates/tesm_core/base.html | 36 +++-- .../templates/tesm_core/diagnostics.html | 40 +++--- .../tesm_core/templates/tesm_core/error.html | 4 +- .../templates/tesm_core/group_detail.html | 10 +- .../tesm_core/templates/tesm_core/groups.html | 16 +-- .../tesm_core/templates/tesm_core/login.html | 2 +- .../templates/tesm_core/logs_archive.html | 2 +- .../templates/tesm_core/logs_audit.html | 29 ++-- .../templates/tesm_core/logs_history.html | 12 +- .../tesm_core/templates/tesm_core/macros.html | 23 +++- .../tesm_core/templates/tesm_core/reauth.html | 8 +- .../templates/tesm_core/second_factor.html | 6 +- .../templates/tesm_core/sessions.html | 6 +- .../tesm_core/settings_directory.html | 26 ++-- .../templates/tesm_core/settings_network.html | 32 ++--- .../templates/tesm_core/settings_web.html | 44 +++--- .../templates/tesm_core/standalone.html | 18 +-- .../tesm_core/templates/tesm_core/trash.html | 18 +-- .../tesm_core/templates/tesm_core/users.html | 20 +-- packages/tesm-core/src/tesm_core/ui.py | 32 +++++ packages/tesm-licensing/pyproject.toml | 2 +- .../src/tesm_licensing/__init__.py | 2 +- tesm-v2.0.1.tar.gz | Bin 690813 -> 0 bytes 68 files changed, 1235 insertions(+), 691 deletions(-) delete mode 100644 tesm-v2.0.1.tar.gz diff --git a/README.md b/README.md index 4a6cc8a..ee25c19 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # TESM -Verwaltung von PoE-Geraeten und Aruba-Switchen: Inventar, Zustandsueberwachung, -Portsteuerung, Neustart (PoE, SSH, RPC), DHCP ueber Kea, Dateifreigaben, -Wartung ueber SSH, Protokolle, Papierkorb, verschluesselte Ein- und Ausfuhr und +Verwaltung von PoE-Geräten und Aruba-Switchen: Inventar, Zustandsüberwachung, +Portsteuerung, Neustart (PoE, SSH, RPC), DHCP über Kea, Dateifreigaben, +Wartung über SSH, Protokolle, Papierkorb, verschlüsselte Ein- und Ausfuhr und ein SSH-Terminal im Browser. -**Dieses Repository enthaelt genau das, was zum Installieren und Betreiben von -TESM gebraucht wird** -- nichts darueber hinaus: +**Dieses Repository enthält genau das, was zum Installieren und Betreiben von +TESM gebraucht wird** -- nichts darüber hinaus: ``` apps/tesm die Anwendung @@ -16,7 +16,7 @@ deploy/ install.sh, update.sh, release.sh, systemd, privileg docs/ Betrieb und Sicherheit ``` -Die Serverhaelfte des Lizenzprotokolls (`tesm-licensing-server`: ausstellen, +Die Serverhälfte des Lizenzprotokolls (`tesm-licensing-server`: ausstellen, erneuern, Antworten signieren) liegt **nicht** hier. TESM verifiziert Lizenzen und stellt Anfragen; den Code zum Ausstellen soll ein Client nicht einmal mitbringen. @@ -31,7 +31,7 @@ mitbringen. | Zweig | Inhalt | |---|---| | `main` | dieser Neubau (Version 2.0.0) | -| `SONNET5` | der Stand davor, unveraendert aufbewahrt | +| `SONNET5` | der Stand davor, unverändert aufbewahrt | --- @@ -43,9 +43,9 @@ sudo -u tesm tesm-admin create-admin ``` Danach `https:///`. Ohne `--domain` entsteht ein selbstsigniertes -Zertifikat; der Browser warnt einmal, die Verbindung ist verschluesselt. +Zertifikat; der Browser warnt einmal, die Verbindung ist verschlüsselt. -Mit oeffentlichem Namen und Let's Encrypt stattdessen: +Mit öffentlichem Namen und Let's Encrypt stattdessen: ```bash sudo bash deploy/install.sh --app tesm --https --domain tesm.firma.de --acme-email it@firma.de @@ -54,16 +54,16 @@ sudo bash deploy/install.sh --app tesm --https --domain tesm.firma.de --acme-ema Zielzustand: **Port 443 mit TLS, Port 80 leitet dorthin um** -- ausser dem ACME-Pfad, der erreichbar bleiben muss, sonst kann Let's Encrypt nie erneuern. -Sitzt ein eigener Reverse Proxy davor, der TLS beendet, laeuft TESM ohne +Sitzt ein eigener Reverse Proxy davor, der TLS beendet, läuft TESM ohne `--https` schlicht auf Port 80. -Eine zweite Installation zum Testen stoert die laufende nicht: +Eine zweite Installation zum Testen stört die laufende nicht: ```bash sudo bash deploy/install.sh --app tesm --instance test --port 5100 --http-port 8080 ``` -Jede Instanz bekommt eigene Datenbank, eigene Schluessel, eigene nginx-Site und +Jede Instanz bekommt eigene Datenbank, eigene Schlüssel, eigene nginx-Site und ein eigenes Sitzungscookie. ### Aktualisieren @@ -72,9 +72,9 @@ ein eigenes Sitzungscookie. sudo bash deploy/update.sh --app tesm ``` -Das laedt ein Release, entpackt es und ruft dessen `install.sh` auf. Vor jedem -Update entsteht ein vollstaendiges Backup -- unabhaengig davon, ob sich das -Schema geaendert hat -- und danach laeuft ein Health-Check mit automatischem +Das lädt ein Release, entpackt es und ruft dessen `install.sh` auf. Vor jedem +Update entsteht ein vollständiges Backup -- unabhängig davon, ob sich das +Schema geändert hat -- und danach läuft ein Health-Check mit automatischem Rollback. --- @@ -83,15 +83,15 @@ Rollback. ```bash sudo -u tesm tesm-admin status # Zustand der Installation -sudo -u tesm tesm-admin create-admin # Konto anlegen oder zuruecksetzen -sudo -u tesm tesm-admin check-now # Ueberwachungsdurchlauf sofort -sudo -u tesm tesm-admin housekeeping # Sitzungen und Protokolle aufraeumen +sudo -u tesm tesm-admin create-admin # Konto anlegen oder zurücksetzen +sudo -u tesm tesm-admin check-now # Überwachungsdurchlauf sofort +sudo -u tesm tesm-admin housekeeping # Sitzungen und Protokolle aufräumen sudo -u tesm tesm-admin web-setup --https # nginx und TLS einrichten ``` ### Umzug aus dem alten TESM -Ein Export des Vorgaengers laesst sich uebersetzen und dann ueber den normalen +Ein Export des Vorgängers lässt sich übersetzen und dann über den normalen Import einlesen -- Clients, Switche, Zugangsdaten und die DHCP-Konfiguration: ```bash @@ -100,11 +100,11 @@ sudo -u tesm tesm-admin convert-legacy tesm_export_20260813_195455.json --dry-ru `--dry-run` zeigt nur den Bericht. Ohne die Option entsteht eine Datei im neuen Format, die unter *Verwaltung -> Sicherung* eingelesen wird. Der Bericht -nennt ausdruecklich, was **nicht** uebersetzt werden kann (lokale Konten, +nennt ausdrücklich, was **nicht** übersetzt werden kann (lokale Konten, Rechte, Verzeichnisdienst, Webserver) -- das ist von Hand nachzuziehen. -Die Passphrase gehoert an die verdeckte Eingabeaufforderung. `--passphrase` -existiert nur fuer Automatisierung: es schreibt sie in Shell-Historie und +Die Passphrase gehört an die verdeckte Eingabeaufforderung. `--passphrase` +existiert nur für Automatisierung: es schreibt sie in Shell-Historie und Prozessliste. --- @@ -135,20 +135,20 @@ Entwicklungsrepository, ebenso die Tests. ## Was diese Fassung anders macht -Ein Neubau. Die Fehler des Vorgaengers sind bekannt; an den Stellen, an denen -sie entstanden sind, steht jetzt ein Kommentar, der erklaert, warum es hier -anders geloest ist. Die wichtigsten: +Ein Neubau. Die Fehler des Vorgängers sind bekannt; an den Stellen, an denen +sie entstanden sind, steht jetzt ein Kommentar, der erklärt, warum es hier +anders gelöst ist. Die wichtigsten: -* **Kein root fuer die Anwendung.** Ein einziger allowlisted Helfer mit - Argumentpruefung statt eines Webprozesses als `root`. -* **CSRF-Schutz ueberhaupt.** Der Vorgaenger hatte keinen. -* **Rechte werden serverseitig geprueft**, nicht nur im Template ausgeblendet. +* **Kein root für die Anwendung.** Ein einziger allowlisted Helfer mit + Argumentprüfung statt eines Webprozesses als `root`. +* **CSRF-Schutz überhaupt.** Der Vorgänger hatte keinen. +* **Rechte werden serverseitig geprüft**, nicht nur im Template ausgeblendet. * **Die nginx-Site wird bei jedem Lauf inhaltlich verglichen und erneuert.** Ein - falscher `alias`-Pfad ueberlebte im Vorgaenger jahrelang jedes Update. -* **Jede erzeugte Konfiguration wird vor dem Uebernehmen geprueft** -- nginx wie - Kea. Eine fehlerhafte Datei faellt beim Schreiben auf, nicht erst beim - naechsten Start des Dienstes. + falscher `alias`-Pfad überlebte im Vorgänger jahrelang jedes Update. +* **Jede erzeugte Konfiguration wird vor dem Übernehmen geprüft** -- nginx wie + Kea. Eine fehlerhafte Datei fällt beim Schreiben auf, nicht erst beim + nächsten Start des Dienstes. * **Kein Hostname im Lizenz-Fingerabdruck.** Umbenennen brach die Bindung. -* **Zwei Installationen stoeren sich nicht** -- eigener `SITE_KEY` fuer +* **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. diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index b639514..42aa607 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.0.0" +version = "2.0.2" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/__init__.py b/apps/tesm/src/tesm/__init__.py index 51c0693..ea5cd7f 100644 --- a/apps/tesm/src/tesm/__init__.py +++ b/apps/tesm/src/tesm/__init__.py @@ -23,7 +23,7 @@ from .permissions import PERMISSIONS, ROLE_PRESETS from .schema import TESM_MIGRATIONS from .settings_spec import TESM_SETTINGS -__version__ = "2.0.0" +__version__ = "2.0.2" APP_KEY = "tesm" APP_NAME = "TESM" @@ -181,7 +181,31 @@ def _status_pills(user: Any) -> list[dict[str, str]]: extension = core() pills: list[dict[str, str]] = [] - counts = monitor.dashboard_counts(db()) + verbindung = db() + + # Timer bis zum naechsten Durchlauf, mit Knopf fuer "sofort pruefen". Der + # Knopf gehoert in die Pille, weil er genau die Angabe daneben zuruecksetzt. + if user.has_any(("devices.view", "monitoring.view")): + intervall = int(extension.settings.get(verbindung, "monitor_interval_seconds") or 300) + faellig = monitor.next_check_at(verbindung, intervall) + if faellig is not None: + eintrag = { + "label": "Naechste Pruefung in", + "countdown_to": faellig.isoformat(), + "countdown_done": "jetzt", + "css_class": "", + "dot": "1", + "dot_class": "", + "title": f"Der Ueberwachungsdienst prueft alle {intervall} Sekunden.", + } + if user.has("settings_system.execute"): + from flask import url_for + + eintrag["action"] = url_for("settings.run_check_now") + eintrag["action_title"] = "Alle Clients sofort pruefen" + pills.append(eintrag) + + counts = monitor.dashboard_counts(verbindung) if counts["offline"]: pills.append( { @@ -275,12 +299,22 @@ def _startup_tasks(app: Flask) -> None: ) return - from .services import maintenance + from .services import fileshare, maintenance with extension.database.session() as conn, extension.database.transaction(conn): orphaned = maintenance.mark_orphaned_jobs(conn) + # Einbindungen leben im Mount-Namensraum dieses Prozesses und sind mit + # dem vorherigen verschwunden. Die Zeilen dazu muessen weg, sonst zeigt + # die Oberflaeche Freigaben, die es nicht gibt. + vergessen = fileshare.forget_all_mounts(conn) if orphaned: logger.warning("%s unterbrochene Wartungsauftraege als unklar markiert", orphaned) + if vergessen: + logger.info( + "%s Freigabe-Einbindungen eines vorherigen Prozesses vergessen " + "-- sie bestehen mit dessen Namensraum nicht mehr", + vergessen, + ) if extension.license is not None: import socket diff --git a/apps/tesm/src/tesm/blueprints/devices.py b/apps/tesm/src/tesm/blueprints/devices.py index 86a57b5..c16a727 100644 --- a/apps/tesm/src/tesm/blueprints/devices.py +++ b/apps/tesm/src/tesm/blueprints/devices.py @@ -298,6 +298,12 @@ def _restart_as_job(extension: Any, conn: Any, device: Any, method: Any): # typ severity="notice", ) + # Der Auftrag laeuft im Hintergrund und meldet sein Ergebnis erst, wenn das + # Geraet zurueck ist. Der Zustand muss aber sofort umschlagen -- sonst sieht + # die Uebersicht aus wie eine, in der nichts passiert ist. + with transaction(): + monitor.mark_restarting(conn, int(device["id"])) + maintenance_service.start_background( maintenance_service.run_reboot, database=extension.database, diff --git a/apps/tesm/src/tesm/blueprints/fileshare.py b/apps/tesm/src/tesm/blueprints/fileshare.py index 81d9bf8..076da6e 100644 --- a/apps/tesm/src/tesm/blueprints/fileshare.py +++ b/apps/tesm/src/tesm/blueprints/fileshare.py @@ -119,6 +119,21 @@ def preview(token: str): headers={ "Content-Disposition": f'inline; filename="{path.name}"', "X-Content-Type-Options": "nosniff", + # Die Vorschau laeuft in einem Rahmen auf der Freigabeseite. Die + # anwendungsweite CSP verbietet mit "frame-ancestors 'none'" jedes + # Einrahmen -- auch das durch die eigene Seite. Beides wird hier + # deshalb ausdruecklich auf "gleicher Ursprung" gesetzt; die Header + # der Anwendung werden per setdefault gesetzt und treten damit + # zurueck. + # + # Bewusst nur die Rahmen-Direktiven und nicht die ganze strenge CSP: + # was hier ausgeliefert wird, ist eine Datei mit engem, erlaubtem + # MIME-Typ (kein HTML, kein SVG) und "nosniff" -- Skript kann darin + # nicht ausgefuehrt werden. Eine vollstaendige CSP auf einer + # PDF-Antwort riskiert dagegen, die eingebaute Anzeige des Browsers + # zu blockieren. + "Content-Security-Policy": "frame-ancestors 'self'", + "X-Frame-Options": "SAMEORIGIN", }, ) @@ -255,6 +270,26 @@ def mapping_add(): return redirect(url_for("fileshare.mappings")) +@bp.post("/zuordnungen/") +@permission_required("fileshare.edit") +def mapping_update(mapping_id: int): + try: + with transaction() as conn: + service.update_mapping( + conn, + mapping_id, + ad_group_dn=request.form.get("ad_group_dn", ""), + ad_group_name=request.form.get("ad_group_name", ""), + label=request.form.get("label", ""), + unc=request.form.get("unc", ""), + ) + _audit("fileshare.mapping_updated", request.form.get("label", ""), severity="notice") + flash("Zuordnung geaendert.", "success") + except service.FileshareError as exc: + flash(str(exc), "danger") + return redirect(url_for("fileshare.mappings")) + + @bp.post("/zuordnungen//loeschen") @permission_required("fileshare.edit") def mapping_delete(mapping_id: int): diff --git a/apps/tesm/src/tesm/blueprints/maintenance.py b/apps/tesm/src/tesm/blueprints/maintenance.py index b7be785..56c8d44 100644 --- a/apps/tesm/src/tesm/blueprints/maintenance.py +++ b/apps/tesm/src/tesm/blueprints/maintenance.py @@ -22,7 +22,7 @@ from tesm_core.auth.service import client_ip from tesm_core.db import Database from tesm_core.extension import core, db, transaction -from ..services import inventory, maintenance as service, ssh, windowsops +from ..services import inventory, maintenance as service, ssh, windowsops, monitor bp = Blueprint("maintenance", __name__, url_prefix="/wartung") @@ -246,6 +246,9 @@ def _start(kind: str) -> None: timeout=int(extension.settings.get(conn, "maintenance_command_timeout") or 900), ) else: + # Sofort sichtbar, nicht erst wenn das Geraet zurueck ist. + with transaction() as verbindung: + monitor.mark_restarting(verbindung, int(device["id"])) service.start_background( service.run_reboot, database=extension.database, diff --git a/apps/tesm/src/tesm/blueprints/switches.py b/apps/tesm/src/tesm/blueprints/switches.py index a7cf8cf..d4f076e 100644 --- a/apps/tesm/src/tesm/blueprints/switches.py +++ b/apps/tesm/src/tesm/blueprints/switches.py @@ -54,7 +54,6 @@ def overview(): "tesm/switches.html", switches=inventory.list_switches(conn), credentials=inventory.list_credentials(conn), - host_keys=Database.all(conn, "SELECT * FROM switch_host_keys ORDER BY host"), terminal_enabled=bool(core().settings.get(conn, "terminal_enabled")), ) diff --git a/apps/tesm/src/tesm/navigation.py b/apps/tesm/src/tesm/navigation.py index bdd61f6..4125f3e 100644 --- a/apps/tesm/src/tesm/navigation.py +++ b/apps/tesm/src/tesm/navigation.py @@ -140,7 +140,7 @@ NAVIGATION = NavTree( ), NavItem( key="logs_audit", - label="Aenderungen", + label="Audit", icon="history", endpoint="logs.audit_view", permissions=("logs_audit.view", "logs_audit.export"), diff --git a/apps/tesm/src/tesm/services/fileshare.py b/apps/tesm/src/tesm/services/fileshare.py index 758db02..79b1834 100644 --- a/apps/tesm/src/tesm/services/fileshare.py +++ b/apps/tesm/src/tesm/services/fileshare.py @@ -157,7 +157,7 @@ def search_suffixes(conn: sqlite3.Connection) -> list[str]: def resolve_unc_host(unc: str, suffixes: Iterable[str]) -> tuple[str, str]: - """Ersetzt den Hostnamen einer UNC durch einen auflösbaren. + """Ersetzt den Hostnamen einer UNC durch einen aufloesbaren. Rueckgabe: ``(unc, hinweis)``. Der Hinweis ist leer, wenn nichts geaendert wurde; laesst sich der Name gar nicht aufloesen, bleibt die UNC unveraendert @@ -176,9 +176,9 @@ def resolve_unc_host(unc: str, suffixes: Iterable[str]) -> tuple[str, str]: neu = chr(92) * 2 + kandidat + (chr(92) + rest if rest else "") return neu, f"Name {host} ueber {kandidat} aufgeloest" return unc, ( - f"Der Name {host} ist auf diesem Server nicht auflösbar. Bitte den " + f"Der Name {host} ist auf diesem Server nicht aufloesbar. Bitte den " "vollqualifizierten Namen oder die IP-Adresse eintragen -- oder unter " - "Netzwerk eine Suchdomäne setzen." + "Netzwerk eine Suchdomaene setzen." ) @@ -207,6 +207,52 @@ def add_mapping( return int(cursor.lastrowid or 0) +def update_mapping( + conn: sqlite3.Connection, + mapping_id: int, + *, + ad_group_dn: str, + ad_group_name: str, + label: str, + unc: str, +) -> None: + """Aendert eine bestehende Zuordnung. + + Dieselben Pruefungen wie beim Anlegen -- eine Zuordnung, die man nur neu + anlegen statt korrigieren kann, verleitet dazu, den Tippfehler stehen zu + lassen und daneben einen zweiten Eintrag zu machen. + """ + clean_unc = validate_unc(unc) + clean_label = (label or "").strip() + clean_dn = (ad_group_dn or "").strip() + if not clean_label: + raise FileshareError("Der Anzeigename darf nicht leer sein.") + if not clean_dn: + raise FileshareError("Es ist keine Verzeichnisgruppe ausgewaehlt.") + + vorhanden = Database.one( + conn, "SELECT id FROM fileshare_mappings WHERE id=?", (mapping_id,) + ) + if vorhanden is None: + raise FileshareError("Diese Zuordnung existiert nicht mehr.") + + # Die Eindeutigkeit (ad_group_dn, unc) gilt weiterhin -- nur nicht gegen + # sich selbst. + kollision = Database.one( + conn, + "SELECT id FROM fileshare_mappings WHERE ad_group_dn=? AND unc=? AND id<>?", + (clean_dn, clean_unc, mapping_id), + ) + if kollision is not None: + raise FileshareError("Diese Zuordnung existiert bereits.") + + conn.execute( + "UPDATE fileshare_mappings SET ad_group_dn=?, ad_group_name=?, label=?, unc=? " + "WHERE id=?", + (clean_dn, (ad_group_name or "").strip(), clean_label, clean_unc, mapping_id), + ) + + def delete_mapping(conn: sqlite3.Connection, mapping_id: int) -> None: conn.execute("DELETE FROM fileshare_mappings WHERE id=?", (mapping_id,)) @@ -372,6 +418,24 @@ def release_mount(conn: sqlite3.Connection, mount_id: int) -> None: ) +def forget_all_mounts(conn: sqlite3.Connection) -> int: + """Vergisst alle Einbindungen -- beim Start des Webprozesses. + + Eingebunden wird im Mount-Namensraum dieses Dienstes. Mit dem Dienst + verschwindet der Namensraum und mit ihm jede Einbindung; die Zeilen in der + Datenbank ueberleben aber. Ohne dieses Aufraeumen zeigte die Oberflaeche + nach jedem Neustart Freigaben an, die es nicht mehr gibt -- und ein Klick + darauf fuehrte in ein leeres Verzeichnis. + + Bewusst **kein** ``umount``: es gibt nichts zu loesen. Der Aufruf ginge in + einen Namensraum, der nicht mehr existiert. + """ + cursor = conn.execute( + "UPDATE fileshare_mounts SET released_at=? WHERE released_at IS NULL", (_iso(_now()),) + ) + return int(cursor.rowcount or 0) + + def release_all_for_user(conn: sqlite3.Connection, user_id: int) -> int: count = 0 for mount in active_mounts(conn, user_id): @@ -389,10 +453,26 @@ def sweep_expired(database: Any) -> int: """ released = 0 with database.session() as conn: + # Eine Einbindung lebt genau so lange wie die Sitzung, aus der sie + # entstanden ist. Vorher hing sie an einer eigenen Frist von zwoelf + # Stunden: nach dem Abmelden -- oder nach der automatischen Abmeldung + # bei Leerlauf -- blieb die Freigabe mit den Zugangsdaten des Benutzers + # weiter eingebunden. Die eigene Frist bleibt als Obergrenze bestehen; + # eine Sitzung kann durch Aktivitaet beliebig lange leben, eine + # Einbindung soll trotzdem irgendwann neu entstehen muessen. rows = Database.all( conn, - "SELECT id FROM fileshare_mounts WHERE released_at IS NULL AND expires_at <= ?", - (_iso(_now()),), + """ + SELECT m.id FROM fileshare_mounts m + LEFT JOIN user_sessions s ON s.id = m.session_id + WHERE m.released_at IS NULL + AND (m.expires_at <= ? + OR s.id IS NULL + OR s.revoked_at IS NOT NULL + OR s.idle_expires_at <= ? + OR s.absolute_expires_at <= ?) + """, + (_iso(_now()), _iso(_now()), _iso(_now())), ) if not rows: return 0 diff --git a/apps/tesm/src/tesm/services/monitor.py b/apps/tesm/src/tesm/services/monitor.py index 0fa669b..773c84c 100644 --- a/apps/tesm/src/tesm/services/monitor.py +++ b/apps/tesm/src/tesm/services/monitor.py @@ -33,7 +33,7 @@ import threading import time from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Callable @@ -313,9 +313,10 @@ def record_restart( (device_id, _now(), trigger, actor, action, result, duration_ms, detail[:8000], method), ) if result == "ok": - # Der Zustand wird hier gesetzt, nicht in den einzelnen Neustartwegen: - # PoE, SSH und RPC laufen alle durch diese Funktion. Eine zweite Stelle - # waere eine, die beim naechsten Verfahren vergessen wird. + # Hier faellt nur noch der Zaehler und der Zeitstempel an. Den Zustand + # setzt ``mark_restarting`` schon beim Ausloesen -- dieses Ergebnis + # kommt bei SSH und RPC erst, wenn das Geraet zurueck ist, und bis + # dahin stand die Kachel weiter auf "online". conn.execute( "UPDATE device_status SET last_restart_at=?, restart_count=restart_count+1, " "state=?, last_change_at=?, fail_count=0 WHERE device_id=?", @@ -323,6 +324,33 @@ def record_restart( ) +def mark_restarting(conn: sqlite3.Connection, device_id: int) -> None: + """Setzt den Zustand auf "startet neu" -- beim **Ausloesen**, nicht am Ende. + + Ein Neustart ueber SSH oder RPC laeuft als Auftrag: Kommando schicken, dann + auf die Rueckkehr warten. Das Ergebnis steht erst Minuten spaeter fest. Wer + den Knopf gedrueckt hat, will die Wirkung aber sofort sehen -- sonst sieht + die Uebersicht aus wie eine, in der nichts passiert ist. + + Bewusst ohne ``restart_count``: gezaehlt wird das Ereignis, nicht die + Absicht. Und ohne ``last_restart_at``: das Kulanzfenster laeuft ab dem + Zeitpunkt, an dem der Neustart tatsaechlich ausgeloest wurde + (``record_restart``). + """ + conn.execute( + """ + INSERT INTO device_status (device_id, state, fail_count, last_check_at, last_change_at) + VALUES (?,?,0,?,?) + ON CONFLICT(device_id) DO UPDATE SET + state=excluded.state, + fail_count=0, + last_change_at=CASE WHEN device_status.state <> excluded.state + THEN excluded.last_change_at ELSE device_status.last_change_at END + """, + (device_id, STATE_RESTARTING, _now(), _now()), + ) + + # --------------------------------------------------------------------------- # # PoE-Neustart # --------------------------------------------------------------------------- # @@ -363,6 +391,13 @@ def restart_device_poe( switch_ip = str(device.get("switch_ip") or "") credential_id = device.get("switch_credential_id") + # Sofort sichtbar machen, dass etwas passiert -- der Portzyklus dauert + # Sekunden, das Warten auf die Rueckkehr laenger. Gilt fuer den manuellen + # wie fuer den automatischen Neustart, denn beide laufen hier durch. + if port and switch_ip: + with write(): + mark_restarting(conn, int(device["id"])) + if not port or not switch_ip: message = ( "Kein Switch oder kein Port zugeordnet -- ein PoE-Neustart ist nicht moeglich." @@ -573,6 +608,28 @@ def _in_cooldown(last_restart_at: Any) -> bool: return (datetime.now(timezone.utc) - moment).total_seconds() < RESTART_COOLDOWN_SECONDS +def next_check_at(conn: sqlite3.Connection, interval_seconds: int) -> datetime | None: + """Wann der naechste Ueberwachungsdurchlauf faellig ist. + + Anker ist die letzte tatsaechliche Pruefung, nicht der Prozessstart: nach + einem Neustart des Dienstes soll der Zaehler dort weiterlaufen, wo der + Durchlauf steht, und nicht bei voller Laenge beginnen. + """ + zeile = Database.one( + conn, "SELECT MAX(last_check_at) AS letzte FROM device_status", () + ) + letzte = (zeile or {}).get("letzte") + if not letzte: + return None + try: + stamp = datetime.fromisoformat(str(letzte)) + except ValueError: + return None + if stamp.tzinfo is None: + stamp = stamp.replace(tzinfo=timezone.utc) + return stamp + timedelta(seconds=max(1, int(interval_seconds))) + + def dashboard_counts(conn: sqlite3.Connection) -> dict[str, int]: row = Database.one( conn, diff --git a/apps/tesm/src/tesm/services/restart.py b/apps/tesm/src/tesm/services/restart.py index 83a7ec6..d1cd805 100644 --- a/apps/tesm/src/tesm/services/restart.py +++ b/apps/tesm/src/tesm/services/restart.py @@ -45,7 +45,18 @@ class Method: return self.key == METHOD_POE +#: Derselbe Weg wie PoE, nur anders benannt: bei einem Geraet, das nicht +#: antwortet, ist "Neustart" das falsche Wort. Der Schluessel bleibt ``poe``, +#: damit Protokoll und Auswertung unveraendert bleiben. +POWER_ON = "poe-einschalten" + DEFINITIONS: dict[str, Method] = { + POWER_ON: Method( + key=METHOD_POE, + label="Einschalten (PoE)", + hint="Schaltet den Switch-Port aus und wieder ein. Bei einem Geraet, das nicht " + "antwortet, ist das der einzige Weg, der noch etwas bewirkt.", + ), METHOD_POE: Method( key=METHOD_POE, label="PoE-Port", @@ -82,9 +93,17 @@ def available(device: Any, *, maintenance_active: bool = True) -> list[Method]: return None methods: list[Method] = [] - if field("switch_id") and str(field("port") or "").strip(): + hat_poe = bool(field("switch_id")) and bool(str(field("port") or "").strip()) + if hat_poe: methods.append(DEFINITIONS[METHOD_POE]) + # Ein Geraet, das nicht antwortet, laesst sich nicht ueber SSH oder RPC neu + # starten -- dafuer muesste es erreichbar sein. Angeboten wird dann nur noch + # der PoE-Weg, und der heisst dort "Einschalten": er schaltet den Port + # wieder ein, statt ein laufendes System neu zu starten. + if str(field("state") or "") == "offline": + return [DEFINITIONS[POWER_ON]] if hat_poe else [] + category = str(field("credential_category") or "") has_login = bool(field("credential_id")) if has_login and maintenance_active: diff --git a/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html b/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html index 7f9da98..0efecd2 100644 --- a/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html +++ b/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html @@ -1,8 +1,8 @@ -{# Teilbereich der Statusuebersicht -- wird auch einzeln per fetch geladen. #} +{# Teilbereich der Statusübersicht -- wird auch einzeln per fetch geladen. #} {% from "tesm_core/macros.html" import pill, empty_state %} -{# Beide Bloecke in einen Stapel: sonst klebt die erste Gruppe an den - Filterkacheln, waehrend die Gruppen untereinander Abstand haben. #} +{# Beide Blöcke in einen Stapel: sonst klebt die erste Gruppe an den + Filterkacheln, während die Gruppen untereinander Abstand haben. #}
{% else %}
- Noch nicht geprueft + Noch nicht geprüft {{ counts.unknown }}
{% endif %} @@ -49,9 +49,9 @@
{% for tile in section.tiles %} {# Kopf zuerst Kennzeichen, dann Name: das Auge findet die Kachel - ueber Farbe und Symbol, den genauen Namen liest es danach. + über Farbe und Symbol, den genauen Namen liest es danach. Die IP steht gross darunter statt in der Merkmalliste -- sie ist - die Angabe, nach der man auf dieser Seite tatsaechlich sucht. #} + die Angabe, nach der man auf dieser Seite tatsächlich sucht. #}
@@ -73,7 +73,7 @@
Switch
{{ tile.switch }} / {{ tile.port }}
{% endif %} {% if tile.latency_ms %} -
Antwortzeit
{{ '%.1f' | format(tile.latency_ms) }} ms
+
Antwortzeit
{{ tile.latency_ms | millis }}
{% endif %} {% if tile.last_change_at %}
Seit
@@ -92,7 +92,7 @@
diff --git a/apps/tesm/src/tesm/templates/tesm/backup.html b/apps/tesm/src/tesm/templates/tesm/backup.html index 32a6a00..c6a23a1 100644 --- a/apps/tesm/src/tesm/templates/tesm/backup.html +++ b/apps/tesm/src/tesm/templates/tesm/backup.html @@ -1,14 +1,14 @@ {% extends "tesm_core/base.html" %} {% from "tesm_core/macros.html" import field, checkbox, alert, pill %} -{% block page_title %}Sicherung und Uebertragung{% endblock %} -{% block page_sub %}Verschluesselter Export · zweistufiger Import{% endblock %} +{% block page_title %}Sicherung und Übertragung{% endblock %} +{% block page_sub %}Verschlüsselter Export · zweistufiger Import{% endblock %} {% block content %} {% if license_hint %}{{ alert(license_hint, 'warning') }}{% endif %} -{{ alert('Der Export wird mit Argon2id aus Ihrer Passphrase verschluesselt (AES-256-GCM). Ohne -diese Passphrase ist die Datei nicht wiederherstellbar -- es gibt keine Hintertuer. Bewahren Sie +{{ alert('Der Export wird mit Argon2id aus Ihrer Passphrase verschlüsselt (AES-256-GCM). Ohne +diese Passphrase ist die Datei nicht wiederherstellbar -- es gibt keine Hintertür. Bewahren Sie sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }}
@@ -16,7 +16,7 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }}
{{ icon('download', size=17) }} Sicherung erstellen
-
Auswahl je Kategorie · verlangt eine frische Passwortbestaetigung
+
Auswahl je Kategorie · verlangt eine frische Passwortbestätigung
@@ -37,7 +37,7 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }} {% if category.admin_only %}{{ pill('nur Admin', 'accent') }}{% endif %}
{{ category.hint }}
{% if not allowed %} -
Fuer diese Kategorie fehlt Ihnen das Recht.
+
Für diese Kategorie fehlt Ihnen das Recht.
{% endif %} @@ -46,9 +46,9 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }} {% if can_secrets %} - {{ checkbox('include_secrets', 'Passwoerter der Zugangsdaten mit exportieren', - hint_text='Sie liegen dann im Klartext innerhalb der verschluesselten - Datei. Ohne dieses Kennzeichen enthaelt die Sicherung nur Namen und + {{ checkbox('include_secrets', 'Passwörter der Zugangsdaten mit exportieren', + hint_text='Sie liegen dann im Klartext innerhalb der verschlüsselten + Datei. Ohne dieses Kennzeichen enthält die Sicherung nur Namen und Benutzer.') }} {% endif %} @@ -73,9 +73,9 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }}
-
{{ icon('upload', size=17) }} Geoeffnete Sicherung
+
{{ icon('upload', size=17) }} Geöffnete Sicherung
- Waehlen Sie, was uebernommen werden soll. Gueltig fuer {{ preview_ttl }} Minuten. + Wählen Sie, was übernommen werden soll. Gültig für {{ preview_ttl }} Minuten.
@@ -84,7 +84,7 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }}
- + {% for entry in preview.summary %} @@ -96,7 +96,7 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }} @@ -104,17 +104,17 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }}
KategorieEintraege
KategorieEinträge
{{ entry.label }} - {% if entry.has_secrets %}{{ pill('mit Passwoertern', 'danger') }}{% endif %} + {% if entry.has_secrets %}{{ pill('mit Passwörtern', 'danger') }}{% endif %} {{ entry.count }}
- {{ alert('Bestehende Eintraege werden nach ihrem natuerlichen Schluessel abgeglichen - (Name, Hostname, MAC, Benutzername) und aktualisiert. Eintraege, die im Papierkorb + {{ alert('Bestehende Einträge werden nach ihrem natürlichen Schlüssel abgeglichen + (Name, Hostname, MAC, Benutzername) und aktualisiert. Einträge, die im Papierkorb liegen, werden nicht wiederbelebt.', 'info') }}
@@ -123,7 +123,7 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }}
{{ icon('upload', size=17) }} Sicherung einspielen
-
Erst oeffnen und pruefen, dann uebernehmen
+
Erst öffnen und prüfen, dann übernehmen
@@ -150,8 +150,8 @@ sie getrennt von der Sicherung auf.', 'warning', 'Passphrase') }}
  • Verzeichniskonten werden nie exportiert -- sie entstehen am Ziel bei der ersten Anmeldung neu.
  • -
  • Lizenz- und Aktivierungszustand bleiben ausgenommen: sie gehoeren zur Maschine, nicht zur Sicherung.
  • -
  • Sitzungen, Ratenbegrenzung und das Aenderungsprotokoll sind nicht Teil der Sicherung.
  • +
  • Lizenz- und Aktivierungszustand bleiben ausgenommen: sie gehören zur Maschine, nicht zur Sicherung.
  • +
  • Sitzungen, Ratenbegrenzung und das Änderungsprotokoll sind nicht Teil der Sicherung.
  • Der Zwischenstand des Imports liegt in der Datenbank -- ein Dienst-Neustall verliert ihn nicht.
diff --git a/apps/tesm/src/tesm/templates/tesm/console.html b/apps/tesm/src/tesm/templates/tesm/console.html index 760274b..ca4429d 100644 --- a/apps/tesm/src/tesm/templates/tesm/console.html +++ b/apps/tesm/src/tesm/templates/tesm/console.html @@ -18,21 +18,21 @@ {% block content %} {% if not host_key_ready %} - {{ alert('Fuer dieses Ziel ist kein SSH-Host-Schluessel freigegeben. Die Konsole verweigert + {{ alert('Für dieses Ziel ist kein SSH-Host-Schlüssel freigegeben. Die Konsole verweigert den Aufbau, bis das geschehen ist -- die Freigabe erfolgt auf der Detailseite.', - 'danger', 'Host-Schluessel fehlt') }} -

Zur Detailseite und Schluessel freigeben

+ 'danger', 'Host-Schlüssel fehlt') }} +

Zur Detailseite und Schlüssel freigeben

{% endif %} {{ alert('Die Konsole ist zeilenorientiert: ein Kommando pro Zeile, Antwort darunter. Jede -Eingabe und jede Ausgabe wird auf dem Server mitgeschrieben und im Aenderungsprotokoll -vermerkt. Bei Untaetigkeit trennt die Verbindung nach ' ~ idle_timeout ~ ' Sekunden.', +Eingabe und jede Ausgabe wird auf dem Server mitgeschrieben und im Änderungsprotokoll +vermerkt. Bei Untätigkeit trennt die Verbindung nach ' ~ idle_timeout ~ ' Sekunden.', 'warning', 'Mitschrift') }} {% if not is_switch %} - {{ alert('Jedes Kommando laeuft in einer eigenen Sitzung. Das heisst: ein "cd" wirkt nicht - auf das naechste Kommando, und interaktive Programme (Editoren, Passwortabfragen) laufen - hier nicht. Fuer solche Arbeiten einen richtigen SSH-Client nehmen.', 'info', + {{ alert('Jedes Kommando läuft in einer eigenen Sitzung. Das heisst: ein "cd" wirkt nicht + auf das nächste Kommando, und interaktive Programme (Editoren, Passwortabfragen) laufen + hier nicht. Für solche Arbeiten einen richtigen SSH-Client nehmen.', 'info', 'Ein Kommando, eine Sitzung') }} {% endif %} diff --git a/apps/tesm/src/tesm/templates/tesm/credential_reveal.html b/apps/tesm/src/tesm/templates/tesm/credential_reveal.html index e9d8765..68c74ca 100644 --- a/apps/tesm/src/tesm/templates/tesm/credential_reveal.html +++ b/apps/tesm/src/tesm/templates/tesm/credential_reveal.html @@ -8,8 +8,8 @@ {% endblock %} {% block content %} -{{ alert('Dieser Aufruf steht mit Zeitpunkt, Benutzer und IP im Aenderungsprotokoll. -Schliessen Sie die Seite, sobald Sie den Wert uebernommen haben.', 'warning') }} +{{ alert('Dieser Aufruf steht mit Zeitpunkt, Benutzer und IP im Änderungsprotokoll. +Schliessen Sie die Seite, sobald Sie den Wert übernommen haben.', 'warning') }}
{{ icon('key', size=17) }} {{ entry.name }}
diff --git a/apps/tesm/src/tesm/templates/tesm/credentials.html b/apps/tesm/src/tesm/templates/tesm/credentials.html index 676b6ec..5d8e47a 100644 --- a/apps/tesm/src/tesm/templates/tesm/credentials.html +++ b/apps/tesm/src/tesm/templates/tesm/credentials.html @@ -5,9 +5,9 @@ {% macro credential_form(entry=None) %}
{{ field('name', 'Name', value=entry.name if entry else '', required=True, - hint_text='Frei waehlbar, z. B. "Switch-Admin" oder "Linux-Wartung".') }} + hint_text='Frei wählbar, z. B. "Switch-Admin" oder "Linux-Wartung".') }} {{ field('username', 'Benutzer', value=entry.username if entry else '', autocomplete='off') }} - {{ field('secret', 'Passwort' ~ (' (leer lassen = unveraendert)' if entry else ''), + {{ field('secret', 'Passwort' ~ (' (leer lassen = unverändert)' if entry else ''), type='password', autocomplete='new-password', required=False if entry else True) }}
- {{ checkbox('is_active', 'Geraet wird ueberwacht', checked=device.is_active if device else True) }} + {{ checkbox('is_active', 'Gerät wird überwacht', checked=device.is_active if device else True) }} {{ checkbox('auto_restart', 'Automatischer PoE-Neustart bei Ausfall', checked=device.auto_restart if device else True, - hint_text='Greift nur mit gueltiger Lizenz und zugeordnetem Switch-Port.') }} + hint_text='Greift nur mit gültiger Lizenz und zugeordnetem Switch-Port.') }}
{% endmacro %} {% block page_title %}Clients{% endblock %} {% block page_sub %} - {{ devices | length }} Geraete · {{ counts.online }} erreichbar, {{ counts.offline }} nicht erreichbar + {{ devices | length }} Geräte · {{ counts.online }} erreichbar, {{ counts.offline }} nicht erreichbar {% endblock %} {% block topbar_extra %} {{ search_box('devices-table', 'Name, IP, MAC, Switch ...', count_id='devices-visible') }} {% if current_user.has('devices.create') %} {% endif %} {% endblock %} @@ -155,8 +155,8 @@ MAC Zugangsdaten Switch / Port - Letzte Pruefung - Host-Schluessel + Letzte Prüfung + Host-Schlüssel Aktion @@ -167,13 +167,14 @@ {{ device.name }} {% if not device.is_active %}{{ pill('deaktiviert', '') }}{% endif %} {% if not device.auto_restart %} - ! + ! {% endif %} {% if device.location %}
{{ device.location }}
{% endif %} {% if not device.is_active %}{{ pill('aus', '') }} {% elif device.state == 'online' %}{{ pill('online', 'online') }} + {% elif device.state == 'restarting' %}{{ pill('startet neu', 'warning') }} {% elif device.state == 'offline' %}{{ pill('offline', 'offline') }} {% else %}{{ pill('unbekannt', 'warning') }}{% endif %} {% if device.fail_count %}({{ device.fail_count }}×){% endif %} @@ -200,8 +201,8 @@ {% if device.credential_category == 'windows' %} - - nicht noetig + + nicht nötig {% elif device.host_key_known %}{{ pill('freigegeben', 'online') }} {% else %} @@ -225,9 +226,9 @@
-
{% endif %} @@ -238,19 +239,19 @@
{% if not devices %} - {{ empty_state('Keine Geraete', 'Legen Sie das erste ueberwachte Geraet an.', 'cpu') }} + {{ empty_state('Keine Geräte', 'Legen Sie das erste überwachte Gerät an.', 'cpu') }} {% endif %}
-{{ alert('Ein PoE-Neustart setzt Switch und Port voraus, ein Neustart ueber SSH oder -RPC setzt passende Zugangsdaten voraus. Ist keine Methode moeglich, erscheint der Knopf -nicht -- damit kein Neustart vorgetaeuscht wird, der nichts bewirkt.', 'info') }} +{{ alert('Ein PoE-Neustart setzt Switch und Port voraus, ein Neustart über SSH oder +RPC setzt passende Zugangsdaten voraus. Ist keine Methode möglich, erscheint der Knopf +nicht -- damit kein Neustart vorgetäuscht wird, der nichts bewirkt.', 'info') }} {% endblock %} {% block dialogs %} {% if current_user.has('devices.create') %} - {% call modal('device-new', 'Geraet anlegen', 'MAC und IP sind Pflicht', 'wide') %} + {% call modal('device-new', 'Gerät anlegen', 'MAC und IP sind Pflicht', 'wide') %}
diff --git a/apps/tesm/src/tesm/templates/tesm/dhcp.html b/apps/tesm/src/tesm/templates/tesm/dhcp.html index 585fec2..f624765 100644 --- a/apps/tesm/src/tesm/templates/tesm/dhcp.html +++ b/apps/tesm/src/tesm/templates/tesm/dhcp.html @@ -9,7 +9,7 @@ {% block content %} {% for warning in warnings %}{{ alert(warning, 'warning') }}{% endfor %} {% if not sysops_available %} - {{ alert('Systemhelfer nicht verfuegbar -- Konfiguration und Dienstzustand koennen hier nur + {{ alert('Systemhelfer nicht verfügbar -- Konfiguration und Dienstzustand können hier nur angezeigt, nicht geschrieben werden.', 'warning') }} {% endif %} @@ -27,13 +27,13 @@
Reservierungen {{ reservations | length }} - automatisch + manuell + automatisch + manüll
Leases {{ leases | rejectattr('is_expired') | list | length }} - gueltig{% if leases | selectattr('is_expired') | list %}, + gültig{% if leases | selectattr('is_expired') | list %}, {{ leases | selectattr('is_expired') | list | length }} abgelaufen{% endif %}
@@ -58,7 +58,7 @@
{% endfor %} @@ -164,8 +164,8 @@
{{ icon('puzzle', size=17) }} DHCP-Optionen
- {{ options | length }} Optionen · Standardoptionen sind nicht loeschbar, ihr Wert - laesst sich leeren + {{ options | length }} Optionen · Standardoptionen sind nicht löschbar, ihr Wert + lässt sich leeren
{{ search_box('options-table', 'Option suchen ...') }} @@ -202,8 +202,8 @@ data-no-guard> {% endif %} @@ -232,16 +232,16 @@ - {# Der Wert einer Option gilt sonst fuer alle. Genau dafuer gibt es - ``dhcp_option_values.device_id``: ein eigener Wert je Geraet, der in + {# Der Wert einer Option gilt sonst für alle. Genau dafür gibt es + ``dhcp_option_values.device_id``: ein eigener Wert je Gerät, der in dessen Reservierung geschrieben wird -- etwa eine Terminal-URL, die je - Geraet anders lautet. Auswertbar war das von Anfang an, eingeben liess + Gerät anders lautet. Auswertbar war das von Anfang an, eingeben liess es sich nicht. #}
-
{{ icon('sliders', size=17) }} Option nur fuer ein Geraet
+
{{ icon('sliders', size=17) }} Option nur für ein Gerät
- Der Wert landet in der Reservierung dieses Geraets und ueberschreibt dort den + Der Wert landet in der Reservierung dieses Geräts und überschreibt dort den globalen Wert. Leeres Feld entfernt ihn wieder.
@@ -251,7 +251,7 @@
{{ field('value', 'Wert', extra_class='field--full', - hint_text='Leer = Wert fuer dieses Geraet entfernen') }} + hint_text='Leer = Wert für dieses Gerät entfernen') }}
{% endif %} @@ -286,16 +286,16 @@
{{ icon('credit-card', size=17) }} Reservierungen
- Automatisch aus dem Geraetebestand; bei MAC-Kollision gewinnt der Geraetebestand. + Automatisch aus dem Gerätebestand; bei MAC-Kollision gewinnt der Gerätebestand. Nur Adressen aus eingerichteten Bereichen.
{% if skipped_reservations %} - {# Nicht verschweigen, sondern benennen: vorher standen diese Eintraege in + {# Nicht verschweigen, sondern benennen: vorher standen diese Einträge in der Liste, obwohl Kea sie nie ausgeliefert hat. #}
- {{ alert(skipped_reservations | length ~ ' Geraet(e) liegen in keinem eingerichteten + {{ alert(skipped_reservations | length ~ ' Gerät(e) liegen in keinem eingerichteten Bereich und bekommen keine Reservierung. Legen Sie den passenden Bereich an, wenn sie eine bekommen sollen.', 'warning') }}
@@ -339,7 +339,7 @@ {% endif %} - {% if reservation.source == 'manuell' and current_user.has('dhcp.edit') %} + {% if reservation.source == 'manüll' and current_user.has('dhcp.edit') %}
@@ -381,20 +381,20 @@
{% if leases and service.get('active') != 'active' %} - {# Sonst sieht man alte Eintraege und haelt sie fuer den aktuellen Stand: + {# Sonst sieht man alte Einträge und hält sie für den aktüllen Stand: ohne laufenden Dienst wird keine Lease erneuert, und die Datei veraltet still. #}
- {{ alert('Der DHCP-Dienst laeuft nicht (' ~ service.get('active', 'unbekannt') ~ '). + {{ alert('Der DHCP-Dienst läuft nicht (' ~ service.get('active', 'unbekannt') ~ '). Es wird keine Lease erneuert -- die Liste zeigt den letzten Stand der Datei, nicht die - aktuelle Lage. Der Grund steht im Kea-Protokoll.', 'warning') }} + aktülle Lage. Der Grund steht im Kea-Protokoll.', 'warning') }}
{% endif %} {% if leases %}
- + {% for lease in leases %} @@ -407,7 +407,7 @@ {% if current_user.has('fileshare.delete') %}{% endif %} - - + + @@ -69,9 +69,9 @@ {% if current_user.has('fileshare.delete') %}{% endif %} @@ -79,7 +79,7 @@ {% for entry in listing.directories %} {% if current_user.has('fileshare.delete') %} - + {% endif %} {% if current_user.has('fileshare.delete') %} - + {% endif %} - + {# Der Name ist die grösste Fläche der Zeile -- er soll auch + das Naheliegende tun. Was sich anzeigen lässt, öffnet die + Vorschau; alles andere lädt herunter. #} + - + @@ -112,7 +112,7 @@
{{ icon('server', size=17) }} Systemzugriff - {% if not sysops_available %}{{ pill('nicht verfuegbar', 'warning') }} + {% if not sysops_available %}{{ pill('nicht verfügbar', 'warning') }} {% elif sysops_selftest and sysops_selftest.ok %}{{ pill('Helfer antwortet', 'online') }} {% else %}{{ pill('Helfer antwortet nicht', 'danger') }}{% endif %}
@@ -121,7 +121,7 @@

- Der Webprozess laeuft unprivilegiert. Alles, was Root-Rechte braucht, geht durch genau + Der Webprozess läuft unprivilegiert. Alles, was Root-Rechte braucht, geht durch genau ein Helferskript mit Positivliste – nicht durch die Anwendung selbst.

{% if not sysops_available %} @@ -129,10 +129,10 @@ Alles andere funktioniert normal -- das ist der erwartete Zustand in einer Entwicklungsumgebung.', 'info') }} {% elif sysops_selftest and not sysops_selftest.ok %} - {# Geprueft wird ein echter Aufruf, nicht die blosse Existenz der Datei. - Genau daran lag es einmal: die Seite meldete "erreichbar", waehrend + {# Geprüft wird ein echter Aufruf, nicht die blosse Existenz der Datei. + Genau daran lag es einmal: die Seite meldete "erreichbar", während jeder Aufruf scheiterte. #} - {{ alert(sysops_selftest.message or 'Der Helfer laesst sich nicht aufrufen.', + {{ alert(sysops_selftest.message or 'Der Helfer lässt sich nicht aufrufen.', 'danger', 'Systemaktionen funktionieren nicht') }} {% endif %}
@@ -144,7 +144,7 @@ {% if current_user.is_admin %} - + {% endif %} @@ -167,14 +167,14 @@ {{ icon('key', size=17) }} Lizenzzustand {% if license_state.valid and license_state.activated %}{{ pill('aktiv', 'online') }} {% elif license_state.valid %}{{ pill('nicht aktiviert', 'warning') }} - {% else %}{{ pill('ungueltig', 'danger') }}{% endif %} + {% else %}{{ pill('ungültig', 'danger') }}{% endif %}
Datei vorhanden
{{ license_state.present | yes_no }}
-
Signatur gueltig
{{ license_state.valid | yes_no }}
+
Signatur gültig
{{ license_state.valid | yes_no }}
Aussteller gepinnt
{{ license_state.issuer_pinned | yes_no }}
Rolle
{{ license_state.role }}
Module
{{ license_state.modules | sort | join(', ') or '–' | safe }}
diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html index ebca1bb..1d0b9f7 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html @@ -7,11 +7,11 @@
{{ description }}
{% if request_id %} -

Vorgangsnummer fuer die Fehlersuche: {{ request_id }}

+

Vorgangsnummer für die Fehlersuche: {{ request_id }}

{% endif %} {% endblock %} diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html index aacb4d3..2f1e572 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html @@ -20,8 +20,8 @@ {{ icon('lock', size=18) }}
Systemgruppe
- Name und Rechte sind gesperrt. Anders als im Vorgaenger werden die Rechte - nicht bei jedem Neustart auf die Vorlage zurueckgesetzt – + Name und Rechte sind gesperrt. Anders als im Vorgänger werden die Rechte + nicht bei jedem Neustart auf die Vorlage zurückgesetzt – Anpassungen bleiben erhalten. {% if current_user.has('groups.edit') %} @@ -49,8 +49,8 @@ {% if group.preset_key and current_user.is_admin %} {% endif %} @@ -58,7 +58,7 @@
{{ field('name', 'Name', value=group.name, required=True, disabled=group.is_system, - hint_text='Systemgruppen koennen nicht umbenannt werden.' if group.is_system else '') }} + hint_text='Systemgruppen können nicht umbenannt werden.' if group.is_system else '') }} {{ textarea_field('description', 'Beschreibung', value=group.description, rows=2, extra_class='field--full') }}
diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html index e4204d9..0945367 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html @@ -2,7 +2,7 @@ {% from "tesm_core/macros.html" import field, textarea_field, pill, modal, alert, permission_matrix, empty_state %} {% block page_title %}Gruppen und Rechte{% endblock %} -{% block page_sub %}{{ groups | length }} Gruppen · Rechte addieren sich ueber alle Gruppen eines Benutzers{% endblock %} +{% block page_sub %}{{ groups | length }} Gruppen · Rechte addieren sich über alle Gruppen eines Benutzers{% endblock %} {% block topbar_extra %} {% if current_user.has('groups.create') %} @@ -13,9 +13,9 @@ {% endblock %} {% block content %} -{{ alert('Wer selbst ein Recht nicht besitzt, kann es auch nicht vergeben. Diese Regel gilt fuer -alle ausser echten Administratoren und verhindert, dass sich ein Gruppenverwalter ueber eine -neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }} +{{ alert('Wer selbst ein Recht nicht besitzt, kann es auch nicht vergeben. Diese Regel gilt für +alle ausser echten Administratoren und verhindert, dass sich ein Gruppenverwalter über eine +neue Gruppe selbst höherstuft.', 'info', 'Eskalationsschutz') }}
{% for group in groups %} @@ -37,9 +37,9 @@ neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }}
+ data-confirm="Gruppe {{ group.name }} löschen? Sie landet im Papierkorb; Mitglieder verlieren die zugehörigen Rechte." + data-confirm-title="Gruppe löschen" data-confirm-ok="Löschen" + aria-label="Löschen">{{ icon('trash', size=15) }} {% endif %}
@@ -67,7 +67,7 @@ neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }} {% block dialogs %} {% if current_user.has('groups.create') %} - {% call modal('group-new', 'Gruppe anlegen', 'Rechte koennen anschliessend feiner eingestellt werden', 'wide') %} + {% call modal('group-new', 'Gruppe anlegen', 'Rechte können anschliessend feiner eingestellt werden', 'wide') %}
@@ -77,13 +82,13 @@
IPMACHostnameArtZustandLaeuft ab
Läuft ab
{% if lease.is_expired %}{{ pill('abgelaufen', 'warning') }} - {% elif lease.is_active %}{{ pill('gueltig', 'online') }} + {% elif lease.is_active %}{{ pill('gültig', 'online') }} {% else %}{{ pill('inaktiv', '') }}{% endif %} {{ lease.expires_text }} @@ -429,7 +429,7 @@
{{ icon('file-text', size=17) }} Erzeugte Kea-Konfiguration
-
Vorschau – so wuerde die Datei geschrieben
+
Vorschau – so würde die Datei geschrieben
{{ icon('chevron-down', size=16) }}
diff --git a/apps/tesm/src/tesm/templates/tesm/fileshare.html b/apps/tesm/src/tesm/templates/tesm/fileshare.html index 532c402..faf9463 100644 --- a/apps/tesm/src/tesm/templates/tesm/fileshare.html +++ b/apps/tesm/src/tesm/templates/tesm/fileshare.html @@ -59,8 +59,8 @@
NameGroesseGeaendertGrösseGeändert Aktion
- - {{ icon('chevron-right', size=14) }} eine Ebene hoeher + {{ icon('chevron-right', size=14) }} eine Ebene höher
{{ icon('folder', size=15) }} @@ -93,16 +93,44 @@ {% for entry in listing.files %}
{{ icon('file-text', size=15) }} {{ entry.name }} + {% if entry.previewable %} + + {% else %} + + {{ icon('file-text', size=15) }} {{ entry.name }} + + {% endif %} + {{ entry.size | bytes }} {{ entry.modified | dt }} {% if entry.previewable %} - {{ icon('eye', size=15) }} + + {% endif %} + {% if current_user.has('fileshare.edit') %} + {% endif %} {% endif %} @@ -142,7 +170,7 @@
- +
@@ -165,4 +193,44 @@ {% endcall %} {% endif %} + +{% if listing %} + {# Vorschau im Rahmen statt in einem neuen Tab: der Zusammenhang bleibt + erhalten, und man ist mit Escape wieder in der Dateiliste. Titel, Quelle + und Download-Ziel werden beim Öffnen aus dem Auslöser gefüllt -- ein + Dialog für alle Dateien statt einer je Zeile. #} + {% call modal('file-preview', 'Vorschau', '', 'wide') %} + + +
+ {% endcall %} +{% endif %} + +{% if listing and current_user.has('fileshare.edit') %} + {% call modal('file-rename', 'Umbenennen', '', 'narrow') %} +
+ + + + + +
+ {% endcall %} +{% endif %} {% endblock %} diff --git a/apps/tesm/src/tesm/templates/tesm/fileshare_mappings.html b/apps/tesm/src/tesm/templates/tesm/fileshare_mappings.html index 28ce10a..2515da9 100644 --- a/apps/tesm/src/tesm/templates/tesm/fileshare_mappings.html +++ b/apps/tesm/src/tesm/templates/tesm/fileshare_mappings.html @@ -1,13 +1,13 @@ {% extends "tesm_core/base.html" %} {% from "tesm_core/_group_picker.html" import group_picker %} -{% from "tesm_core/macros.html" import field, empty_state, alert %} +{% from "tesm_core/macros.html" import field, empty_state, alert, modal %} {% block page_title %}Freigaben einrichten{% endblock %} {% block page_sub %}{{ mappings | length }} Zuordnungen · Wurzel {{ mount_root }}{% endblock %} {% block content %} {{ alert('Eine Freigabe wird bei der Anmeldung eingebunden, wenn der Benutzer Mitglied der zugeordneten Verzeichnisgruppe ist. Eingebunden wird mit dem Konto dieses Benutzers -- deshalb -ist die Verzeichnisgruppe das einzige moegliche Ziel, und lokale Konten bekommen keine +ist die Verzeichnisgruppe das einzige mögliche Ziel, und lokale Konten bekommen keine Einbindungen.', 'info') }} {% if not directory_enabled %} @@ -33,6 +33,11 @@ Einbindungen.', 'info') }}
{{ entry.ad_group_name }}
{{ entry.ad_group_dn }}
+ {% if current_user.has('fileshare.edit') %} + + {% endif %}
+ + + + {% endcall %} + {% endfor %} +{% endif %} +{% endblock %} diff --git a/apps/tesm/src/tesm/templates/tesm/hostkeys.html b/apps/tesm/src/tesm/templates/tesm/hostkeys.html index 4033373..174515d 100644 --- a/apps/tesm/src/tesm/templates/tesm/hostkeys.html +++ b/apps/tesm/src/tesm/templates/tesm/hostkeys.html @@ -1,40 +1,44 @@ {% extends "tesm_core/base.html" %} {% from "tesm_core/macros.html" import pill, empty_state, alert, search_box %} -{% block page_title %}Host-Schluessel{% endblock %} +{% block page_title %}Host-Schlüssel{% endblock %} {% block page_sub %} {{ targets | length }} Ziele mit SSH-Zugang {%- if open_count %} · {{ open_count }} ohne Freigabe{% endif %} {% endblock %} -{% block topbar_extra %}{{ search_box('hostkey-table', 'Geraet, IP, Fingerprint ...') }}{% endblock %} +{% block topbar_extra %}{{ search_box('hostkey-table', 'Gerät, IP, Fingerprint ...') }}{% endblock %} {% block content %} {% if open_count %} - {{ alert('Fuer ' ~ open_count ~ ' Ziel(e) ist noch kein Schluessel freigegeben. Wartung, - Konsole und PoE-Neustart brechen dort mit "Schluessel unbekannt" ab -- absichtlich: ein - Hintergrundauftrag darf keinen fremden Schluessel annehmen.', 'warning', - 'Freigaben offen') }} + {# Der PoE-Neustart hängt am Schlüssel des *Switches*, nicht am Schlüssel des + Clients: geschaltet wird der Port auf dem Switch. Der alte Satz warf beides + zusammen und liess vermuten, ein Client ohne freigegebenen Schlüssel sei + nicht per PoE neu startbar. #} + {{ alert('Für ' ~ open_count ~ ' Ziel(e) ist noch kein Schlüssel freigegeben. Wartung und + Konsole brechen dort mit "Schlüssel unbekannt" ab -- absichtlich: ein Hintergrundauftrag + darf keinen fremden Schlüssel annehmen. Ein PoE-Neustart braucht dagegen den Schlüssel + des Switches, nicht den des Clients.', 'warning', 'Freigaben offen') }} {% else %} - {{ alert('Fuer jedes Ziel mit SSH-Zugang liegt ein freigegebener Schluessel vor.', - 'success', 'Vollstaendig') }} + {{ alert('Für jedes Ziel mit SSH-Zugang liegt ein freigegebener Schlüssel vor.', + 'success', 'Vollständig') }} {% endif %} -{{ alert('Ein Schluessel wird in zwei Schritten uebernommen: erst auslesen, dann -- nach -Vergleich mit einer unabhaengigen Quelle -- freigeben. Gespeichert wird nach IP und Port; -aendert sich die Adresse eines Geraets, ist erneut freizugeben. Ein *geaenderter* Schluessel -wird nie stillschweigend uebernommen: das ist der Fall, der bei einem Angriff auftritt.', +{{ alert('Ein Schlüssel wird in zwei Schritten übernommen: erst auslesen, dann -- nach +Vergleich mit einer unabhängigen Quelle -- freigeben. Gespeichert wird nach IP und Port; +ändert sich die Adresse eines Geräts, ist erneut freizugeben. Ein *geänderter* Schlüssel +wird nie stillschweigend übernommen: das ist der Fall, der bei einem Angriff auftritt.', 'info', 'So funktioniert die Freigabe') }} {% if probed_fingerprint %}
{{ icon('alert', size=18) }}
-
Gelesener Schluessel von {{ probed_host }}:{{ probed_port }}
+
Gelesener Schlüssel von {{ probed_host }}:{{ probed_port }}
{{ probed_type }} {{ probed_fingerprint }}

- Vergleichen Sie den Fingerprint mit einer unabhaengigen Quelle -- Konsolenzugang am - Geraet, Inventardokumentation --, bevor Sie ihn unten freigeben. + Vergleichen Sie den Fingerprint mit einer unabhängigen Quelle -- Konsolenzugang am + Gerät, Inventardokumentation --, bevor Sie ihn unten freigeben.

@@ -102,8 +106,8 @@ wird nie stillschweigend uebernommen: das ist der Fall, der bei einem Angriff au @@ -114,7 +118,7 @@ wird nie stillschweigend uebernommen: das ist der Fall, der bei einem Angriff au action="{{ url_for('hostkeys.forget', kind=row.kind, target_id=row.id) }}"> @@ -141,7 +145,7 @@ wird nie stillschweigend uebernommen: das ist der Fall, der bei einem Angriff au
{{ icon('alert', size=17) }} Verwaiste Freigaben
- Zu diesen Eintraegen gibt es kein Ziel mehr -- geloescht oder mit neuer IP. + Zu diesen Einträgen gibt es kein Ziel mehr -- gelöscht oder mit neuer IP.
@@ -162,7 +166,7 @@ wird nie stillschweigend uebernommen: das ist der Fall, der bei einem Angriff au diff --git a/apps/tesm/src/tesm/templates/tesm/license.html b/apps/tesm/src/tesm/templates/tesm/license.html index a85b66e..49622d5 100644 --- a/apps/tesm/src/tesm/templates/tesm/license.html +++ b/apps/tesm/src/tesm/templates/tesm/license.html @@ -4,8 +4,8 @@ {% block page_title %}Lizenz{% endblock %} {% block page_sub %} {% if state and state.valid and state.activated %}aktiv - {% elif state and state.valid %}gueltig, noch nicht aktiviert - {% elif state and state.present %}ungueltig + {% elif state and state.valid %}gültig, noch nicht aktiviert + {% elif state and state.present %}ungültig {% else %}keine Lizenz installiert{% endif %} {% endblock %} @@ -37,7 +37,7 @@
Lizenz-ID
{{ state.status.license_id }}
Revision
{{ state.status.seq }}
Ausgestellt
{{ state.status.issued_at | dt }}
-
Laeuft ab
+
Läuft ab
{% if state.status.lifetime %}{{ pill('unbefristet', 'online') }} {% else %} @@ -46,7 +46,7 @@ {{ pill('abgelaufen seit ' ~ state.status.days_since_expiry ~ ' Tagen', 'danger') }} {% if state.status.grace_active %}
- Kulanzfrist laeuft ({{ grace_days }} Tage) – Funktionen bleiben vorerst nutzbar. + Kulanzfrist läuft ({{ grace_days }} Tage) – Funktionen bleiben vorerst nutzbar.
{% endif %} {% elif state.status.expiring_soon %} @@ -88,7 +88,7 @@ {% else %}
- +
@@ -118,14 +118,14 @@ {% if pending.reason %} {{ alert(pending.reason, 'warning', 'Warum der Online-Weg nicht ging') }} {% endif %} - {{ alert('Beide Codes sind signiert -- sie lassen sich unterwegs nicht veraendern. - Der Anfragecode gehoert zu genau dieser Maschine und genau dieser Anfrage; eine + {{ alert('Beide Codes sind signiert -- sie lassen sich unterwegs nicht verändern. + Der Anfragecode gehört zu genau dieser Maschine und genau dieser Anfrage; eine Antwort auf einen anderen Code wird abgelehnt.', 'info') }} {% if pending.code %}
- Anfragecode -- an den Anbieter uebermitteln + Anfragecode -- an den Anbieter übermitteln
{{ pending.code }}
@@ -135,8 +135,8 @@
{% else %} - {{ alert('Zu dieser Anfrage liegt kein Code vor -- sie stammt aus einer aelteren - Fassung. Loesen Sie die Aktion erneut aus, dann entsteht ein neuer Code.', + {{ alert('Zu dieser Anfrage liegt kein Code vor -- sie stammt aus einer älteren + Fassung. Lösen Sie die Aktion erneut aus, dann entsteht ein neuer Code.', 'warning') }} {% endif %} @@ -145,10 +145,10 @@
- +
@@ -161,15 +161,15 @@
{{ icon('shield', size=17) }} Zertifikat des Lizenzservers
- Nur noetig, wenn der Lizenzserver ein eigenes Zertifikat benutzt + Nur nötig, wenn der Lizenzserver ein eigenes Zertifikat benutzt
{{ alert('Ein Lizenzserver im eigenen Netz hat meist ein selbstsigniertes - Zertifikat. Ohne Anker scheitert die Pruefung, und jeder Versuch faellt auf den - Offline-Weg zurueck. Die Pruefung abzuschalten waere der falsche Weg -- hinterlegen - Sie stattdessen das Zertifikat, dann wird gegen genau dieses geprueft.', 'info') }} + Zertifikat. Ohne Anker scheitert die Prüfung, und jeder Versuch fällt auf den + Offline-Weg zurück. Die Prüfung abzuschalten wäre der falsche Weg -- hinterlegen + Sie stattdessen das Zertifikat, dann wird gegen genau dieses geprüft.', 'info') }}
@@ -196,7 +196,7 @@
{{ icon('upload', size=17) }} Lizenzdatei einspielen
- Die Datei enthaelt das Lizenzdokument und den zugehoerigen Client-Schluessel + Die Datei enthält das Lizenzdokument und den zugehörigen Client-Schlüssel
@@ -206,7 +206,7 @@
{% if trusted_keys and current_user.is_admin %} - {{ checkbox('allow_new_issuer', 'Wechsel des Ausstellers ausdruecklich zulassen', + {{ checkbox('allow_new_issuer', 'Wechsel des Ausstellers ausdrücklich zulassen', hint_text='Nur ankreuzen, wenn die neue Lizenz bewusst von einem anderen Lizenzserver kommt.') }} {% endif %} @@ -233,8 +233,8 @@
Fingerprint
{{ fingerprint }}

- Der Fingerprint stammt aus der stabilen Maschinenidentitaet. Eine Umbenennung des Hosts - aendert ihn nicht mehr -- im Vorgaenger fuehrte genau das zu + Der Fingerprint stammt aus der stabilen Maschinenidentität. Eine Umbenennung des Hosts + ändert ihn nicht mehr -- im Vorgänger führte genau das zu „bereits anderweitig gebunden“.

@@ -243,14 +243,14 @@
-
{{ icon('shield', size=17) }} Vertrauenswuerdige Aussteller
-
{{ trusted_keys | length }} gepinnte Schluessel
+
{{ icon('shield', size=17) }} Vertrauenswürdige Aussteller
+
{{ trusted_keys | length }} gepinnte Schlüssel
{% if trusted_keys %}
- + {% for key in trusted_keys %} @@ -268,7 +268,7 @@ action="{{ url_for('license.remove_trusted_key', key_id=key.key_id) }}"> @@ -281,7 +281,7 @@ {% else %} {{ empty_state('Kein Aussteller gepinnt', - 'Beim ersten Import wird der Schluessel des Lizenzservers uebernommen und danach + 'Beim ersten Import wird der Schlüssel des Lizenzservers übernommen und danach erwartet.', 'shield') }} {% endif %} @@ -307,7 +307,7 @@

Diese Liste ist die einzige Wahrheit: was hier steht, wird serverseitig durchgesetzt -- - nicht nur in der Oberflaeche ausgegraut. + nicht nur in der Oberfläche ausgegraut.

diff --git a/apps/tesm/src/tesm/templates/tesm/logs_restarts.html b/apps/tesm/src/tesm/templates/tesm/logs_restarts.html index c38330d..6d6f616 100644 --- a/apps/tesm/src/tesm/templates/tesm/logs_restarts.html +++ b/apps/tesm/src/tesm/templates/tesm/logs_restarts.html @@ -2,11 +2,11 @@ {% from "tesm_core/macros.html" import pill, empty_state, alert, search_box %} {% block page_title %}Neustarts{% endblock %} {% block page_sub %}{{ rows | length }} Neustarts (neueste zuerst){% endblock %} -{% block topbar_extra %}{{ search_box('restart-table', 'Geraet, Ergebnis, Methode ...') }}{% endblock %} +{% block topbar_extra %}{{ search_box('restart-table', 'Gerät, Ergebnis, Methode ...') }}{% endblock %} {% block content %} -{{ alert('Jeder Neustart wird mit Methode, Ergebnis, Dauer und Ausloeser festgehalten -- automatisch -wie manuell, ueber den PoE-Port des Switches ebenso wie per Neustart-Befehl aus der Wartung. -Der Vorgaenger protokollierte Neustarts ausdruecklich nicht.', 'info') }} +{{ alert('Jeder Neustart wird mit Methode, Ergebnis, Dauer und Auslöser festgehalten -- automatisch +wie manüll, über den PoE-Port des Switches ebenso wie per Neustart-Befehl aus der Wartung. +Der Vorgänger protokollierte Neustarts ausdrücklich nicht.', 'info') }}
@@ -25,8 +25,8 @@ Der Vorgaenger protokollierte Neustarts ausdruecklich nicht.', 'info') }}
Schluessel-IDBezeichnungQuelle
Schlüssel-IDBezeichnungQuelle
- - + + @@ -41,17 +41,17 @@ Der Vorgaenger protokollierte Neustarts ausdruecklich nicht.', 'info') }} jahrelang als PoE ausgegeben. #} {% if row.method == 'ssh' %}{{ pill('SSH', 'info') }} {% elif row.method == 'rpc' %}{{ pill('RPC', 'accent') }} - {% elif row.method == 'poe' %}{{ pill('PoE-Port', '') }} + {% elif row.method == 'pö' %}{{ pill('PoE-Port', '') }} {% else %}{{ pill(row.method or 'unbekannt', 'warning') }}{% endif %} - + {% endfor %} diff --git a/apps/tesm/src/tesm/templates/tesm/maintenance.html b/apps/tesm/src/tesm/templates/tesm/maintenance.html index 9feac70..56f8c1e 100644 --- a/apps/tesm/src/tesm/templates/tesm/maintenance.html +++ b/apps/tesm/src/tesm/templates/tesm/maintenance.html @@ -3,29 +3,29 @@ {% block page_title %}Wartung{% endblock %} {% block page_sub %} - {{ devices | length }} Geraete mit Zugangsdaten der Kategorie + {{ devices | length }} Geräte mit Zugangsdaten der Kategorie „Linux-Client“ oder „Windows-Client“ {% endblock %} -{% block topbar_extra %}{{ search_box('maintenance-table', 'Geraet suchen ...') }}{% endblock %} +{% block topbar_extra %}{{ search_box('maintenance-table', 'Gerät suchen ...') }}{% endblock %} {% block content %} {% if devices | rejectattr('host_key_ready') | list %} - {{ alert('Fuer mindestens ein Geraet ist der SSH-Host-Schluessel noch nicht freigegeben. - Die Wartung bricht dort mit "Schluessel unbekannt" ab. Die Freigabe erfolgt auf der - Detailseite des Geraets -- erst auslesen, Fingerprint pruefen, dann freigeben.', - 'warning', 'Host-Schluessel offen') }} + {{ alert('Für mindestens ein Gerät ist der SSH-Host-Schlüssel noch nicht freigegeben. + Die Wartung bricht dort mit "Schlüssel unbekannt" ab. Die Freigabe erfolgt auf der + Detailseite des Geräts -- erst auslesen, Fingerprint prüfen, dann freigeben.', + 'warning', 'Host-Schlüssel offen') }} {% endif %} {% if windows_tool_missing %} - {# Die Meldung im Auftrag nennt das fehlende Paket -- ohne diesen Knopf waere sie + {# Die Meldung im Auftrag nennt das fehlende Paket -- ohne diesen Knopf wäre sie eine Sackgasse. #}
{{ icon('alert', size=18) }}
- Windows-Neustart nicht moeglich + Windows-Neustart nicht möglich Auf diesem Server fehlt das Paket {{ windows_package }}. Es bringt - net mit, ueber das der Neustart eines Windows-Geraetes laeuft. + net mit, über das der Neustart eines Windows-Gerätes läuft.
@@ -38,10 +38,10 @@
{% endif %} -{{ alert('Welche Geraete hier erscheinen und was mit ihnen moeglich ist, entscheidet die -Kategorie ihrer Zugangsdaten. Linux-Clients werden ueber SSH aktualisiert und neu gestartet. -Windows-Clients werden nur neu gestartet, und zwar ueber RPC (wie "shutdown /m") -- sie -brauchen dafuer weder SSH noch einen Host-Schluessel, sondern die offene Datei- und +{{ alert('Welche Geräte hier erscheinen und was mit ihnen möglich ist, entscheidet die +Kategorie ihrer Zugangsdaten. Linux-Clients werden über SSH aktualisiert und neu gestartet. +Windows-Clients werden nur neu gestartet, und zwar über RPC (wie "shutdown /m") -- sie +brauchen dafür weder SSH noch einen Host-Schlüssel, sondern die offene Datei- und Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Port.', 'info') }} @@ -51,20 +51,20 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
{% if current_user.has('maintenance.execute') %} @@ -76,11 +76,11 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
- + - + @@ -92,7 +92,7 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por - {{ device.name }} auswaehlen + {{ device.name }} auswählen @@ -108,13 +108,13 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
ZeitGeraetMethodeAusloeserZeitGerätMethodeAuslöser BenutzerErgebnis DauerDetail
{{ row.trigger }} {{ row.actor }} {% if row.result == 'ok' %}{{ pill('erfolgreich', 'online') }} - {% elif row.result == 'skipped' %}{{ pill('uebersprungen', 'warning') }} + {% elif row.result == 'skipped' %}{{ pill('übersprungen', 'warning') }} {% else %}{{ pill('fehlgeschlagen', 'danger') }}{% endif %} {{ row.duration_ms or 0 }} ms{{ row.duration_ms | millis }} {{ row.detail }}
GeraetGerät IP Zustand ZugangsdatenHost-SchluesselHost-Schlüssel
{{ device.name }} {% if not device.ssh_managed %} - - nicht noetig + + nicht nötig {% elif device.host_key_ready %}{{ pill('freigegeben', 'online') }} {% else %} + title="Ohne freigegebenen Schlüssel bricht die Wartung ab"> {{ pill('offen', 'danger') }} {% endif %} @@ -134,9 +134,9 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
{% if not devices %} - {{ empty_state('Keine passenden Geraete', + {{ empty_state('Keine passenden Geräte', 'Setzen Sie bei den Zugangsdaten die Kategorie "Linux-Client" und ordnen Sie - sie einem Geraet zu.', 'wrench') }} + sie einem Gerät zu.', 'wrench') }} {% endif %}
@@ -144,12 +144,12 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por {# Kein data-refresh-url: die Schleife in app.js schreibt die Antwort in innerHTML. Hier zeigte sie auf die Seite selbst -- die komplette Wartungsseite landete damit in dieser Karte, mitsamt Seitenleiste und - Suchfeld. Aktualisiert wird ueber die Abfrage unten, die bei laufenden - Auftraegen die ganze Seite neu laedt. #} + Suchfeld. Aktualisiert wird über die Abfrage unten, die bei laufenden + Aufträgen die ganze Seite neu lädt. #}
-
{{ icon('clock', size=17) }} Auftraege
+
{{ icon('clock', size=17) }} Aufträge
Zustand liegt in der Datenbank -- ein Dienst-Neustart verliert ihn nicht mehr
@@ -158,7 +158,7 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por {% if jobs %}
- + {% for job in jobs %} @@ -170,7 +170,7 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por @@ -186,16 +186,16 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
GestartetGeraetArtZustandFortschrittMeldung
GestartetGerätArtZustandFortschrittMeldung
{% if job.state == 'succeeded' %}{{ pill('erfolgreich', 'online') }} {% elif job.state == 'failed' %}{{ pill('fehlgeschlagen', 'danger') }} - {% elif job.state == 'running' %}{{ pill('laeuft', 'info') }} + {% elif job.state == 'running' %}{{ pill('läuft', 'info') }} {% elif job.state == 'unknown' %}{{ pill('unklar', 'warning') }} {% else %}{{ pill('wartet', '') }}{% endif %}
{% else %} - {{ empty_state('Keine Auftraege', 'Es wurde noch keine Wartung gestartet.', 'clock') }} + {{ empty_state('Keine Aufträge', 'Es wurde noch keine Wartung gestartet.', 'clock') }} {% endif %}
{% endblock %} {% block scripts %} -{# -- Bestaetigungsdialog (ersetzt window.confirm) ------------------------- #} +{# -- Bestätigungsdialog (ersetzt window.confirm) ------------------------- #}
Komponente Version MigrationPruefsummePrüfsumme Angewendet
{{ row.target }} {{ row.detail }} - {% if row.severity == 'critical' %}{{ pill('kritisch', 'danger') }} - {% elif row.severity == 'warning' %}{{ pill('Warnung', 'warning') }} - {% elif row.severity == 'notice' %}{{ pill('Hinweis', 'info') }} - {% else %}{{ pill('Info', '') }}{% endif %} + {# Ampel: Info grün, Hinweis gelb, Warnung rot. Vorher war Info + grau und Hinweis blau -- dabei war nicht zu sehen, was harmlos + ist und was Aufmerksamkeit braucht. "kritisch" bekommt eine + eigene, gefüllte Darstellung, sonst wäre es von "Warnung" + nicht zu unterscheiden. #} + {% if row.severity == 'critical' %}{{ pill('kritisch', 'critical') }} + {% elif row.severity == 'warning' %}{{ pill('Warnung', 'danger') }} + {% elif row.severity == 'notice' %}{{ pill('Hinweis', 'warning') }} + {% else %}{{ pill('Info', 'online') }}{% endif %} {{ row.ip or '–' | safe }}
- {% if not rows %}{{ empty_state('Keine Eintraege', 'Zu diesem Filter gibt es nichts.', 'history') }}{% endif %} + {% if not rows %}{{ empty_state('Keine Einträge', 'Zu diesem Filter gibt es nichts.', 'history') }}{% endif %} diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/settings_web.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/settings_web.html index 837b1ac..2199d32 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/settings_web.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/settings_web.html @@ -10,14 +10,14 @@ {% block content %} {% if license_hint %}{{ alert(license_hint, 'warning') }}{% endif %} {% if not sysops_available %} - {{ alert('Systemhelfer nicht verfuegbar -- die Konfiguration kann hier erzeugt und angesehen, + {{ alert('Systemhelfer nicht verfügbar -- die Konfiguration kann hier erzeugt und angesehen, aber nicht angewandt werden.', 'info') }} {% endif %} -{{ alert('Die Site-Datei wird bei jeder Uebernahme vollstaendig neu geschrieben. -Alles, was ein Administrator sonst per Hand hineinschreiben wuerde, ist hier Einstellung. Der -Vorgaenger schrieb die Datei nur, wenn sie noch nicht existierte -- ein einmal falscher Pfad -blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.', 'info', +{{ alert('Die Site-Datei wird bei jeder Übernahme vollständig neu geschrieben. +Alles, was ein Administrator sonst per Hand hineinschreiben würde, ist hier Einstellung. Der +Vorgänger schrieb die Datei nur, wenn sie noch nicht existierte -- ein einmal falscher Pfad +blieb dadurch über Jahre bestehen und lieferte alle statischen Dateien als 404.', 'info', 'Warum die Datei immer neu geschrieben wird') }}
@@ -32,12 +32,12 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
{{ field('web_domain', 'Domain', value=config.domain, placeholder='tesm.firma.de', disabled=bool(license_hint), - hint_text='Leer = Zugriff ueber IP-Adresse.') }} - {{ field('web_acme_email', 'E-Mail fuer Let\'s Encrypt', value=acme_email, type='email', + hint_text='Leer = Zugriff über IP-Adresse.') }} + {{ field('web_acme_email', 'E-Mail für Let\'s Encrypt', value=acme_email, type='email', disabled=bool(license_hint)) }} {{ field('web_cert_path', 'Zertifikat (Pfad)', value=config.cert_path, disabled=bool(license_hint), extra_class='field--full') }} - {{ field('web_key_path', 'Privater Schluessel (Pfad)', value=config.key_path, + {{ field('web_key_path', 'Privater Schlüssel (Pfad)', value=config.key_path, disabled=bool(license_hint), extra_class='field--full') }} {{ field('web_acme_webroot', 'ACME-Webroot', value=config.acme_webroot, disabled=bool(license_hint), extra_class='field--full') }} @@ -47,7 +47,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404. checked=config.https_enabled, disabled=bool(license_hint)) }} {{ checkbox('web_redirect_http', 'HTTP (Port 80) auf HTTPS umleiten', checked=config.redirect_http, disabled=bool(license_hint), - hint_text='Der ACME-Pfad bleibt dabei erreichbar -- sonst koennte Let\'s + hint_text='Der ACME-Pfad bleibt dabei erreichbar -- sonst könnte Let\'s Encrypt nie erneuern.') }} {{ checkbox('web_hsts_enabled', 'HSTS-Header senden', checked=config.hsts_enabled, disabled=bool(license_hint), @@ -67,7 +67,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
{{ icon('file-text', size=17) }} Erzeugte nginx-Konfiguration
-
Vorschau der Datei, die geschrieben wuerde
+
Vorschau der Datei, die geschrieben würde
{{ icon('chevron-down', size=16) }}
@@ -89,8 +89,8 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404. {% if certificate.present and not certificate.get('error') %}
Inhaber
{{ certificate.subject }}
-
Aussteller
{{ certificate.issuer }}
-
Gueltig bis
{{ certificate.not_valid_after | dt }}
+
Aussteller
{{ certificate.issür }}
+
Gültig bis
{{ certificate.not_valid_after | dt }}
Seriennummer
{{ certificate.serial }}
{% elif certificate.get('error') %} @@ -107,7 +107,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/standalone.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/standalone.html index 5bd2f55..04ef814 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/standalone.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/standalone.html @@ -1,11 +1,11 @@ {# ========================================================================== - Grundgeruest fuer Seiten ohne Anwendungsrahmen: Anmeldung, zweiter Faktor, + Grundgerüst für Seiten ohne Anwendungsrahmen: Anmeldung, zweiter Faktor, Kundenportal, Self-Service. Wichtiger Unterschied zu base.html: Flash-Meldungen werden hier **sichtbar - serverseitig** gerendert, nicht ueber die JSON-Bruecke. Der Vorgaenger hatte + serverseitig** gerendert, nicht über die JSON-Brücke. Der Vorgänger hatte dieselbe Trennung; sie ist wichtig, weil diese Seiten auch ohne JavaScript - nutzbar bleiben muessen (und weil Tests hier direkt auf Text pruefen). + nutzbar bleiben müssen (und weil Tests hier direkt auf Text prüfen). ========================================================================== #} @@ -23,11 +23,11 @@
{% if auth_tagline %} - {# Wortmarke mit Funktionszeile als tatsaechlicher Seitenhintergrund -- - uebernommen vom Vorgaenger. 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 schon in der Ueberschrift steht. #} + {# 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. #}
diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/trash.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/trash.html index ecec0f3..67d0c22 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/trash.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/trash.html @@ -2,11 +2,11 @@ {% from "tesm_core/macros.html" import empty_state, alert, pill %} {% block page_title %}Papierkorb{% endblock %} -{% block page_sub %}{{ total }} Eintraege · Aufbewahrung {{ retention_days }} Tage{% endblock %} +{% block page_sub %}{{ total }} Einträge · Aufbewahrung {{ retention_days }} Tage{% endblock %} {% block content %} -{{ alert('Ansehen ist immer moeglich. Wiederherstellen und endgueltiges Loeschen erfordern eine -gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Seite entfernt.', +{{ alert('Ansehen ist immer möglich. Wiederherstellen und endgültiges Löschen erfordern eine +gültige, aktivierte Lizenz. Abgelaufene Einträge werden beim Aufruf dieser Seite entfernt.', 'info') }} {% for section in sections %} @@ -23,8 +23,8 @@ gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Sei @@ -33,7 +33,7 @@ gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Sei {% if section.entries %}
- + {% for entry in section.entries %} @@ -53,9 +53,9 @@ gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Sei action="{{ url_for('trash.purge', kind=section.key, ident=entry.id) }}"> {% endif %} diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/users.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/users.html index cc6bc6e..422ddb0 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/users.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/users.html @@ -20,7 +20,7 @@ {% if directory_enabled and current_user.has('users.create') %} {% endif %} {% if current_user.is_admin %} @@ -90,9 +90,9 @@ style="display:inline" data-no-guard> + data-confirm="Konto {{ user.username }} wirklich löschen?{% if user.auth_source == 'ldap' %} Verzeichniskonten werden sofort endgültig entfernt.{% else %} Es landet im Papierkorb.{% endif %}" + data-confirm-title="Konto löschen" data-confirm-ok="Löschen" + aria-label="Löschen">{{ icon('trash', size=15) }} {% endif %} @@ -106,8 +106,8 @@ {% endif %} -{{ alert('Rechte werden ausschliesslich ueber Gruppen vergeben. Das Admin-Kennzeichen ist davon -unabhaengig und kann nur von Administratoren gesetzt werden; mindestens ein entsperrtes +{{ alert('Rechte werden ausschliesslich über Gruppen vergeben. Das Admin-Kennzeichen ist davon +unabhängig und kann nur von Administratoren gesetzt werden; mindestens ein entsperrtes Administratorkonto muss immer bestehen bleiben.', 'info') }} {% endblock %} @@ -137,7 +137,7 @@ Administratorkonto muss immer bestehen bleiben.', 'info') }} {% if current_user.is_admin %} - {{ checkbox('is_admin', 'Administrator (darf alles, unabhaengig von Gruppen)') }} + {{ checkbox('is_admin', 'Administrator (darf alles, unabhängig von Gruppen)') }} {% endif %} {{ checkbox('must_change_password', 'Passwortwechsel bei der ersten Anmeldung erzwingen', checked=True) }} @@ -149,7 +149,7 @@ Administratorkonto muss immer bestehen bleiben.', 'info') }} {% endcall %} {% if directory_enabled %} - {% call modal('user-directory', 'Aus Verzeichnis uebernehmen', + {% call modal('user-directory', 'Aus Verzeichnis übernehmen', 'Konto vorbereiten, damit Gruppen vor der ersten Anmeldung sitzen') %} @@ -169,7 +169,7 @@ Administratorkonto muss immer bestehen bleiben.', 'info') }} {{ alert('Name und E-Mail werden bei der ersten echten Anmeldung aus dem Verzeichnis - uebernommen und ueberschrieben.', 'info') }} + übernommen und überschrieben.', 'info') }}
EintragDetailGeloeschtAktion
EintragDetailGelöschtAktion