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,