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]
|
||||
name = "tesm-license"
|
||||
version = "2.8.3"
|
||||
version = "2.8.4"
|
||||
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -72,7 +72,7 @@ NAVIGATION = NavTree(
|
||||
children=(
|
||||
NavItem(
|
||||
key="logs_live",
|
||||
label="Laufend",
|
||||
label="Live",
|
||||
icon="terminal",
|
||||
endpoint="logs.live",
|
||||
permissions=("logs_live.read",),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -129,7 +129,7 @@ DIAGNOSTICS = Resource(
|
||||
|
||||
LOGS_LIVE = Resource(
|
||||
key="logs_live",
|
||||
label="Laufendes Protokoll",
|
||||
label="Live-Protokoll",
|
||||
actions=(A.READ,),
|
||||
)
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{% 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>
|
||||
{% else %}
|
||||
<div class="empty"><div class="empty__title">Keine Zeilen</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<tr>
|
||||
<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="fs-sm">{{ entry.modified | int | string }}</td>
|
||||
<td class="fs-sm">{{ entry.modified | dt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user