Version 2.8.27: Live-Protokoll mit Stufen, Auftraege fallen nicht mehr heraus
This commit is contained in:
1 parent
4c1d6387c0
commit
cf5cf8622b
12 files changed
+93
-17
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-license"
|
name = "tesm-license"
|
||||||
version = "2.8.3"
|
version = "2.8.4"
|
||||||
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ NAVIGATION = NavTree(
|
|||||||
children=(
|
children=(
|
||||||
NavItem(
|
NavItem(
|
||||||
key="logs_live",
|
key="logs_live",
|
||||||
label="Laufend",
|
label="Live",
|
||||||
icon="terminal",
|
icon="terminal",
|
||||||
endpoint="logs.live",
|
endpoint="logs.live",
|
||||||
permissions=("logs_live.read",),
|
permissions=("logs_live.read",),
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ class LiveLog:
|
|||||||
def info(self, scope: str, message: str) -> None:
|
def info(self, scope: str, message: str) -> None:
|
||||||
self.write("INFO", scope, message)
|
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:
|
def warning(self, scope: str, message: str) -> None:
|
||||||
self.write("WARNUNG", scope, message)
|
self.write("WARNUNG", scope, message)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
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"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ DIAGNOSTICS = Resource(
|
|||||||
|
|
||||||
LOGS_LIVE = Resource(
|
LOGS_LIVE = Resource(
|
||||||
key="logs_live",
|
key="logs_live",
|
||||||
label="Laufendes Protokoll",
|
label="Live-Protokoll",
|
||||||
actions=(A.READ,),
|
actions=(A.READ,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1133,6 +1133,16 @@
|
|||||||
}
|
}
|
||||||
.log-view mark { background: var(--warning-soft); color: var(--warning); border-radius: 3px; }
|
.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 { 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 dt { color: var(--text-faint); white-space: nowrap; }
|
||||||
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||||
|
|||||||
@@ -960,15 +960,22 @@ export function initFilePreview(wurzel = document) {
|
|||||||
const ersetzenFeld = stueck("ersetzen");
|
const ersetzenFeld = stueck("ersetzen");
|
||||||
const ersetzenKasten = stueck("ersetzenfeld");
|
const ersetzenKasten = stueck("ersetzenfeld");
|
||||||
const pruefzeile = stueck("pruefung");
|
const pruefzeile = stueck("pruefung");
|
||||||
// Die Ordner der geoeffneten Freigabe kommen mit der Seite; die einer
|
// Die Ordner der geoeffneten Freigabe stehen bereits in der Auswahl -- der
|
||||||
// anderen holt das Auswahlfeld bei Bedarf nach.
|
// Server hat sie dort hineingeschrieben. Gemerkt werden sie trotzdem, denn
|
||||||
const eigeneOrdner = () => {
|
// wer zwischendurch eine andere Freigabe waehlt, ersetzt die Liste; die
|
||||||
try {
|
// einer anderen Freigabe holt das Auswahlfeld dann bei Bedarf nach.
|
||||||
return JSON.parse(rumpf.dataset.viewerOrdnerliste || "[]");
|
//
|
||||||
} catch {
|
// Vorher stand dieselbe Liste ein zweites Mal daneben, als JSON in einem
|
||||||
return [];
|
// 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 jetzt = stueck("jetzt");
|
||||||
const abbrechen = stueck("abbrechen");
|
const abbrechen = stueck("abbrechen");
|
||||||
const modusFeld = stueck("modus");
|
const modusFeld = stueck("modus");
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{% if lines %}
|
{% if lines %}
|
||||||
<pre class="log-view" data-tail>{% for line in lines %}{{ line }}
|
{# Jede Zeile in einer Huelle, damit die Stufe eine Farbe tragen kann. Der
|
||||||
|
Zeilenumbruch steht ausserhalb: im <pre> ist er der Umbruch selbst, und
|
||||||
|
eine Huelle mit display:block wuerde daraus zwei machen. #}
|
||||||
|
<pre class="log-view" data-tail>{% for line in lines %}{% set z = line | protokollzeile %}<span class="log-view__zeile{% if z.stufe %} log-view__zeile--{{ z.stufe }}{% endif %}">{{ z.text }}</span>
|
||||||
{% endfor %}</pre>
|
{% endfor %}</pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty"><div class="empty__title">Keine Zeilen</div>
|
<div class="empty"><div class="empty__title">Keine Zeilen</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for('logs.history_file', name=entry.name) }}">{{ entry.name }}</a></td>
|
<td><a href="{{ url_for('logs.history_file', name=entry.name) }}">{{ entry.name }}</a></td>
|
||||||
<td class="numeric fs-sm">{{ entry.size | bytes }}</td>
|
<td class="numeric fs-sm">{{ entry.size | bytes }}</td>
|
||||||
<td class="fs-sm">{{ entry.modified | int | string }}</td>
|
<td class="fs-sm">{{ entry.modified | dt }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "tesm_core/base.html" %}
|
{% 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 page_sub %}{{ path }} · letzte {{ line_count }} Zeilen{% endblock %}
|
||||||
|
|
||||||
{% block topbar_extra %}
|
{% block topbar_extra %}
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ def format_datetime(value: Any, *, with_time: bool = True) -> str:
|
|||||||
return "-"
|
return "-"
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
moment = value
|
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:
|
else:
|
||||||
text = str(value).strip()
|
text = str(value).strip()
|
||||||
if text.endswith("Z"):
|
if text.endswith("Z"):
|
||||||
@@ -351,6 +359,38 @@ def audit_detail(text: Any) -> Markup:
|
|||||||
teile.append(str(escape(roh[ende:])))
|
teile.append(str(escape(roh[ende:])))
|
||||||
return Markup("".join(teile))
|
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:
|
def init_app(app: Flask) -> None:
|
||||||
app.jinja_env.globals.update(
|
app.jinja_env.globals.update(
|
||||||
icon=icon,
|
icon=icon,
|
||||||
@@ -371,6 +411,7 @@ def init_app(app: Flask) -> None:
|
|||||||
enumerate=enumerate,
|
enumerate=enumerate,
|
||||||
)
|
)
|
||||||
app.jinja_env.filters.update(
|
app.jinja_env.filters.update(
|
||||||
|
protokollzeile=protokollzeile,
|
||||||
bytes=format_bytes,
|
bytes=format_bytes,
|
||||||
dt=format_datetime,
|
dt=format_datetime,
|
||||||
date=format_date,
|
date=format_date,
|
||||||
|
|||||||
@@ -68,12 +68,17 @@ def live_fragment():
|
|||||||
def history():
|
def history():
|
||||||
extension = core()
|
extension = core()
|
||||||
rotated = []
|
rotated = []
|
||||||
for path in sorted(extension.config.log_dir.glob("*.log.*"), reverse=True):
|
for path in extension.config.log_dir.glob("*.log.*"):
|
||||||
try:
|
try:
|
||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
rotated.append({"name": path.name, "size": stat.st_size, "modified": stat.st_mtime})
|
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(
|
return render_template(
|
||||||
"tesm_core/logs_history.html",
|
"tesm_core/logs_history.html",
|
||||||
rotated=rotated,
|
rotated=rotated,
|
||||||
|
|||||||
Reference in new issue
Block a user