From cf5cf8622bd92b9e54f1c26789e59b8ebea5565d Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Wed, 16 Sep 2026 11:33:13 +0200 Subject: [PATCH] Version 2.8.27: Live-Protokoll mit Stufen, Auftraege fallen nicht mehr heraus --- apps/tesm-license/pyproject.toml | 2 +- .../src/tesm_license/navigation.py | 2 +- .../src/tesm_license/services/monitorlog.py | 10 +++++ packages/tesm-core/pyproject.toml | 2 +- .../tesm-core/src/tesm_core/rbac/common.py | 2 +- .../static/tesm_core/css/components.css | 10 +++++ .../tesm_core/static/tesm_core/js/viewer.js | 25 +++++++---- .../templates/tesm_core/_log_lines.html | 5 ++- .../templates/tesm_core/logs_history.html | 2 +- .../templates/tesm_core/logs_live.html | 2 +- packages/tesm-core/src/tesm_core/ui.py | 41 +++++++++++++++++++ .../tesm-core/src/tesm_core/views/logs.py | 7 +++- 12 files changed, 93 insertions(+), 17 deletions(-) diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index a5c1d69..4aff720 100644 --- a/apps/tesm-license/pyproject.toml +++ b/apps/tesm-license/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-license" -version = "2.8.3" +version = "2.8.4" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm-license/src/tesm_license/navigation.py b/apps/tesm-license/src/tesm_license/navigation.py index dfe0a9b..feb2bef 100644 --- a/apps/tesm-license/src/tesm_license/navigation.py +++ b/apps/tesm-license/src/tesm_license/navigation.py @@ -72,7 +72,7 @@ NAVIGATION = NavTree( children=( NavItem( key="logs_live", - label="Laufend", + label="Live", icon="terminal", endpoint="logs.live", permissions=("logs_live.read",), diff --git a/apps/tesm-license/src/tesm_license/services/monitorlog.py b/apps/tesm-license/src/tesm_license/services/monitorlog.py index 3718935..08012ff 100644 --- a/apps/tesm-license/src/tesm_license/services/monitorlog.py +++ b/apps/tesm-license/src/tesm_license/services/monitorlog.py @@ -40,6 +40,16 @@ class LiveLog: def info(self, scope: str, message: str) -> None: self.write("INFO", scope, message) + def notice(self, scope: str, message: str) -> None: + """Etwas ist passiert, und es ist in Ordnung -- aber es gehoert gesehen. + + Zwischen "laeuft" und "stimmt etwas nicht" fehlte eine Stufe. Ein + Geraet, das neu gestartet wird, ist kein Fehler; es einfach als INFO + zwischen zweihundert Erreichbarkeitsmeldungen zu stellen heisst aber, + dass es niemand findet. + """ + self.write("HINWEIS", scope, message) + def warning(self, scope: str, message: str) -> None: self.write("WARNUNG", scope, message) diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index 3d38d4c..2fee753 100644 --- a/packages/tesm-core/pyproject.toml +++ b/packages/tesm-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-core" -version = "2.8.14" +version = "2.8.16" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/src/tesm_core/rbac/common.py b/packages/tesm-core/src/tesm_core/rbac/common.py index 12b89b1..7e96ffa 100644 --- a/packages/tesm-core/src/tesm_core/rbac/common.py +++ b/packages/tesm-core/src/tesm_core/rbac/common.py @@ -129,7 +129,7 @@ DIAGNOSTICS = Resource( LOGS_LIVE = Resource( key="logs_live", - label="Laufendes Protokoll", + label="Live-Protokoll", actions=(A.READ,), ) diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css index e147079..d766f71 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css @@ -1133,6 +1133,16 @@ } .log-view mark { background: var(--warning-soft); color: var(--warning); border-radius: 3px; } + /* Stufen im laufenden Protokoll. Die Zuordnung der Farbrollen ist Absicht: + --warning ist bernsteinfarben und traegt den HINWEIS, --danger ist rot und + traegt die WARNUNG. Wer das nach den Namen "aufraeumt", dreht die Bedeutung + um: ein Geraet, das nicht antwortet, waere dann orange und eines, das neu + startet, rot. */ + .log-view__zeile--info { color: var(--success); } + .log-view__zeile--hinweis { color: var(--warning); } + .log-view__zeile--warnung { color: var(--danger); } + .log-view__zeile--fehler { color: var(--danger); font-weight: 600; } + .kv { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2) var(--sp-4); font-size: var(--fs-base); } .kv dt { color: var(--text-faint); white-space: nowrap; } .kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; } diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js index 8b2c120..ee7dbe1 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js @@ -960,15 +960,22 @@ export function initFilePreview(wurzel = document) { const ersetzenFeld = stueck("ersetzen"); const ersetzenKasten = stueck("ersetzenfeld"); const pruefzeile = stueck("pruefung"); - // Die Ordner der geoeffneten Freigabe kommen mit der Seite; die einer - // anderen holt das Auswahlfeld bei Bedarf nach. - const eigeneOrdner = () => { - try { - return JSON.parse(rumpf.dataset.viewerOrdnerliste || "[]"); - } catch { - return []; - } - }; + // Die Ordner der geoeffneten Freigabe stehen bereits in der Auswahl -- der + // Server hat sie dort hineingeschrieben. Gemerkt werden sie trotzdem, denn + // wer zwischendurch eine andere Freigabe waehlt, ersetzt die Liste; die + // einer anderen Freigabe holt das Auswahlfeld dann bei Bedarf nach. + // + // Vorher stand dieselbe Liste ein zweites Mal daneben, als JSON in einem + // Attribut -- und kam dort nie an: ``tojson`` laesst doppelte + // Anfuehrungszeichen stehen, und in einem doppelt gequoteten Attribut endet + // der Wert damit nach der oeffnenden Klammer. Die Auswahl zeigte danach nur + // noch das Wurzelverzeichnis, und in einen Unterordner liess sich nichts + // mehr speichern. Zwei Fassungen derselben Liste waren die eigentliche + // Ursache; jetzt gibt es nur noch eine. + const gelieferteOrdner = ordnerFeld + ? [...ordnerFeld.options].map((eintrag) => eintrag.value).filter(Boolean) + : []; + const eigeneOrdner = () => gelieferteOrdner; const jetzt = stueck("jetzt"); const abbrechen = stueck("abbrechen"); const modusFeld = stueck("modus"); diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/_log_lines.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/_log_lines.html index f9aea87..4e25809 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/_log_lines.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/_log_lines.html @@ -1,5 +1,8 @@ {% if lines %} -
{% for line in lines %}{{ line }}
+{# Jede Zeile in einer Huelle, damit die Stufe eine Farbe tragen kann. Der
+   Zeilenumbruch steht ausserhalb: im 
 ist er der Umbruch selbst, und
+   eine Huelle mit display:block wuerde daraus zwei machen. #}
+
{% for line in lines %}{% set z = line | protokollzeile %}{{ z.text }}
 {% endfor %}
{% else %}
Keine Zeilen
diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_history.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_history.html index 4fdb452..fe58de4 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_history.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_history.html @@ -25,7 +25,7 @@ {{ entry.name }} {{ entry.size | bytes }} - {{ entry.modified | int | string }} + {{ entry.modified | dt }} {% endfor %} diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_live.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_live.html index a632454..41838be 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_live.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_live.html @@ -1,5 +1,5 @@ {% extends "tesm_core/base.html" %} -{% block page_title %}Laufendes Protokoll{% endblock %} +{% block page_title %}Live-Protokoll{% endblock %} {% block page_sub %}{{ path }} · letzte {{ line_count }} Zeilen{% endblock %} {% block topbar_extra %} diff --git a/packages/tesm-core/src/tesm_core/ui.py b/packages/tesm-core/src/tesm_core/ui.py index ad4709e..ce23362 100644 --- a/packages/tesm-core/src/tesm_core/ui.py +++ b/packages/tesm-core/src/tesm_core/ui.py @@ -113,6 +113,14 @@ def format_datetime(value: Any, *, with_time: bool = True) -> str: return "-" if isinstance(value, datetime): moment = value + elif isinstance(value, (int, float)) and not isinstance(value, bool): + # Sekunden seit 1970, wie sie aus ``stat()`` kommen. Ohne diesen Zweig + # stand im Protokollverlauf die nackte Zahl (1788472810) in der Spalte + # "Geändert" -- fuer die Datei sichtbar, fuer den Menschen nutzlos. + try: + moment = datetime.fromtimestamp(float(value), timezone.utc) + except (OSError, OverflowError, ValueError): + return str(value) else: text = str(value).strip() if text.endswith("Z"): @@ -351,6 +359,38 @@ def audit_detail(text: Any) -> Markup: teile.append(str(escape(roh[ende:]))) return Markup("".join(teile)) +#: Stufen im laufenden Protokoll und ihre Farbrolle in der Oberflaeche. +#: +#: Die Zuordnung ist Absicht und keine Verwechslung: die Farbrolle "warning" +#: ist bernsteinfarben und traegt hier den **Hinweis**, "danger" ist rot und +#: traegt die **Warnung**. Ein Geraet, das nicht antwortet, ist rot; ein +#: Geraet, das neu gestartet wird, ist orange; alles Gelungene ist gruen. +PROTOKOLLSTUFEN = { + "INFO": "info", + "HINWEIS": "hinweis", + "WARNUNG": "warnung", + "FEHLER": "fehler", +} + +#: ``2026-09-16 09:54:03 INFO ping HAP02 ist wieder erreichbar`` +_PROTOKOLLZEILE = re.compile( + r"^\d{4}-\d\d-\d\d[ T]\d\d:\d\d:\d\d\s+([A-ZÄÖÜ]+)\s" +) + + +def protokollzeile(zeile: str) -> dict[str, str]: + """Zerlegt eine Protokollzeile so weit, dass die Seite sie faerben kann. + + Bewusst nur so weit: der Text bleibt, wie er ist -- eine Protokollansicht, + die ihre Zeilen umbaut, ist keine mehr. Gebraucht wird allein die Stufe. + Was nicht ins Muster passt (fremde Protokolle wie das von Kea), bekommt + keine Farbe statt einer geratenen. + """ + treffer = _PROTOKOLLZEILE.match(str(zeile or "")) + stufe = PROTOKOLLSTUFEN.get(treffer.group(1), "") if treffer else "" + return {"text": str(zeile or ""), "stufe": stufe} + + def init_app(app: Flask) -> None: app.jinja_env.globals.update( icon=icon, @@ -371,6 +411,7 @@ def init_app(app: Flask) -> None: enumerate=enumerate, ) app.jinja_env.filters.update( + protokollzeile=protokollzeile, bytes=format_bytes, dt=format_datetime, date=format_date, diff --git a/packages/tesm-core/src/tesm_core/views/logs.py b/packages/tesm-core/src/tesm_core/views/logs.py index b3e29e6..9a2018b 100644 --- a/packages/tesm-core/src/tesm_core/views/logs.py +++ b/packages/tesm-core/src/tesm_core/views/logs.py @@ -68,12 +68,17 @@ def live_fragment(): def history(): extension = core() rotated = [] - for path in sorted(extension.config.log_dir.glob("*.log.*"), reverse=True): + for path in extension.config.log_dir.glob("*.log.*"): try: stat = path.stat() except OSError: continue rotated.append({"name": path.name, "size": stat.st_size, "modified": stat.st_mtime}) + # Nach Alter, nicht nach Namen: ``sorted(..., reverse=True)`` ueber die + # Dateinamen stellte "live.log.9" vor "live.log.27", weil "9" hinter "2" + # kommt. Gefragt ist aber die juengste Kopie zuerst, und das steht im + # Zeitstempel. + rotated.sort(key=lambda eintrag: eintrag["modified"], reverse=True) return render_template( "tesm_core/logs_history.html", rotated=rotated,