From 8253ab3f0edded665a1c214bd70535eebb395e7e Mon Sep 17 00:00:00 2001 From: alientim Date: Sat, 22 Aug 2026 11:28:15 +0200 Subject: [PATCH] Verlauf: aktuelle live.log als Eintrag, Auswahl per Zeitraum statt Dateiname; RAW-Darstellung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _live_log_history_files() liefert jetzt zusätzlich generation 0 = die aktuelle, noch laufende live.log selbst (nicht nur die rotierten Kopien .1/.2/...) -- _resolve_log_history_file akzeptiert 'live.log' als gültige Auswahl. - Jeder Eintrag bekommt ein range_label ('von' - 'bis'): 'bis' ist das eigene mtime (Zeitpunkt der Rotation) bzw. 'laufend' bei der aktuellen Datei, 'von' ist das mtime der naechstaelteren Generation -- rename() bei einer Rotation aendert mtime nicht, jede Generation behaelt also dauerhaft ihren eigenen Entstehungszeitpunkt. Live verifiziert: die Zeitraeume reihen sich lueckenlos aneinander (Ende von Generation N == Anfang von Generation N-1). - Dropdown und Toolbar zeigen jetzt den Zeitraum statt des rohen Dateinamens. - logs_history.html: Farbcodierung (colorizeLine) entfernt, log-box durch ein
 ersetzt -- reine RAW-Darstellung
  ohne Client-seitiges Span-pro-Zeile-Aufbauen, auf Wunsch zur weiteren
  Reduktion der Ladezeit im Verlauf (Live-Ansicht selbst bleibt farbig).
- VERSION 1.0.6 -> 1.0.7.

Live getestet auf Testsystem-Update-Restart: 4 Eintraege (aktuell + 3
rotierte Generationen), Zeitraeume lueckenlos verkettet, RAW-Endpoint für
'file=live.log' funktioniert, Path-Traversal-Versuch weiterhin mit 404
abgewiesen.

Zusaetzlich untersucht (kein Code-Fix, siehe Konversation): der Verdacht
eines install.sh-Bugs, der tesm-check.service nach einem Update gestoppt
liesse. journalctl zeigt auf beiden Boxen durchgehend 'active (running)'
ueber den fraglichen Zeitraum -- keine Bestaetigung eines Defekts in der
Dienstverwaltung gefunden. Wahrscheinlichste Erklaerung: ein manuell zu
Testzwecken erzwungener logrotate-Lauf (logrotate -f) direkt nach dem
Update hat live.log geleert, wodurch die Live-Log-Seite und die
Naechste-Pruefung-Anzeige fuer die Dauer eines Pruefintervalls leer
wirkten, bis der naechste Zyklus neuen Inhalt schrieb.

Co-Authored-By: Claude Sonnet 5 
---
 srv/tesm/VERSION                     |  2 +-
 srv/tesm/app.py                      | 60 +++++++++++++++++++++-------
 srv/tesm/templates/logs_history.html | 44 +++++---------------
 3 files changed, 58 insertions(+), 48 deletions(-)

diff --git a/srv/tesm/VERSION b/srv/tesm/VERSION
index af0b7dd..238d6e8 100644
--- a/srv/tesm/VERSION
+++ b/srv/tesm/VERSION
@@ -1 +1 @@
-1.0.6
+1.0.7
diff --git a/srv/tesm/app.py b/srv/tesm/app.py
index 4b1b1d3..3cd869e 100644
--- a/srv/tesm/app.py
+++ b/srv/tesm/app.py
@@ -1904,18 +1904,39 @@ def _format_log_size(num_bytes):
 
 
 _LOG_HISTORY_FILE_RE = re.compile(r"^live\.log\.([1-9][0-9]*)$")
+_LOG_HISTORY_DATE_FMT = "%d.%m.%Y %H:%M"
 
 
 def _live_log_history_files():
-    """Ältere, von logrotate rotierte Kopien des Live-Logs (live.log.1 =
-    zuletzt rotiert, .2 der Lauf davor, usw. -- copytruncate ohne compress,
-    siehe _write_logrotate_config, daher reine Textdateien ohne .gz),
-    sortiert von neu (1) nach alt."""
+    """Alle über die Verlauf-Seite auswählbaren Stände des Live-Logs --
+    generation 0 ist die aktuelle, noch laufende live.log selbst, 1 die
+    zuletzt rotierte Kopie, 2 der Lauf davor usw. (copytruncate ohne
+    compress, siehe _write_logrotate_config, daher reine Textdateien ohne
+    .gz), sortiert von neu (0) nach alt.
+
+    Jede Kopie deckt den Zeitraum zwischen ZWEI Rotationen ab: ihr eigenes
+    mtime ist der Zeitpunkt, an dem ihr Inhalt eingefroren wurde (Ende des
+    Zeitraums), das mtime der NÄCHSTälteren Generation der Beginn -- ein
+    reines rename() bei jeder weiteren Rotation ändert mtime nicht, jede
+    Generation behält also dauerhaft den Zeitpunkt ihrer eigenen
+    Entstehung. Für die aktuelle live.log gibt es kein eigenes "Ende",
+    dort wird stattdessen "laufend" angezeigt."""
     results = []
+    try:
+        if os.path.isfile(TESM_LIVE_LOG_PATH):
+            mtime = os.path.getmtime(TESM_LIVE_LOG_PATH)
+            size = os.path.getsize(TESM_LIVE_LOG_PATH)
+            results.append({
+                "filename": "live.log", "generation": 0,
+                "mtime": mtime, "size_str": _format_log_size(size),
+            })
+    except OSError:
+        pass
+
     try:
         entries = os.listdir(TESM_LOG_DIR)
     except OSError:
-        return results
+        entries = []
     for entry in entries:
         m = _LOG_HISTORY_FILE_RE.match(entry)
         if not m:
@@ -1929,20 +1950,30 @@ def _live_log_history_files():
         except OSError:
             continue
         results.append({
-            "filename": entry,
-            "generation": int(m.group(1)),
-            "mtime_str": datetime.fromtimestamp(mtime).strftime("%d.%m.%Y %H:%M"),
-            "size_str": _format_log_size(size),
+            "filename": entry, "generation": int(m.group(1)),
+            "mtime": mtime, "size_str": _format_log_size(size),
         })
     results.sort(key=lambda r: r["generation"])
+
+    for i, entry in enumerate(results):
+        older = results[i + 1] if i + 1 < len(results) else None
+        start_str = datetime.fromtimestamp(older["mtime"]).strftime(_LOG_HISTORY_DATE_FMT) if older else None
+        if entry["generation"] == 0:
+            end_str = "laufend"
+        else:
+            end_str = datetime.fromtimestamp(entry["mtime"]).strftime(_LOG_HISTORY_DATE_FMT)
+        entry["range_label"] = f"{start_str or '?'} – {end_str}"
     return results
 
 
 def _resolve_log_history_file(filename):
     """Validiert einen vom Client übergebenen Dateinamen (?file=...) gegen
-    die tatsächlich vorhandenen rotierten Live-Log-Kopien in TESM_LOG_DIR --
-    verhindert Path-Traversal bzw. beliebiges Datei-Lesen über den
-    Query-Parameter. Gibt den vollen Pfad oder None zurück."""
+    die tatsächlich vorhandene aktuelle live.log bzw. rotierte Kopien davon
+    in TESM_LOG_DIR -- verhindert Path-Traversal bzw. beliebiges
+    Datei-Lesen über den Query-Parameter. Gibt den vollen Pfad oder None
+    zurück."""
+    if filename == "live.log":
+        return TESM_LIVE_LOG_PATH if os.path.isfile(TESM_LIVE_LOG_PATH) else None
     if not filename or not _LOG_HISTORY_FILE_RE.match(filename):
         return None
     full_path = os.path.join(TESM_LOG_DIR, filename)
@@ -5901,7 +5932,8 @@ def logs_history():
     selected_name = requested if selected_path else None
     if not selected_path and files:
         selected_name = files[0]["filename"]
-        selected_path = os.path.join(TESM_LOG_DIR, selected_name)
+        selected_path = _resolve_log_history_file(selected_name)
+    selected_file = next((f for f in files if f["filename"] == selected_name), None)
 
     log_content = None
     total_lines = None
@@ -5913,7 +5945,7 @@ def logs_history():
             log_content = f"Fehler beim Lesen des Logs: {e}"
 
     return render_template(
-        "logs_history.html", files=files, selected_name=selected_name,
+        "logs_history.html", files=files, selected_name=selected_name, selected_file=selected_file,
         log_content=log_content, total_lines=total_lines, tail_lines=LIVE_LOG_TAIL_LINES,
     )
 
diff --git a/srv/tesm/templates/logs_history.html b/srv/tesm/templates/logs_history.html
index 19149d1..9f2c2a5 100644
--- a/srv/tesm/templates/logs_history.html
+++ b/srv/tesm/templates/logs_history.html
@@ -1,14 +1,14 @@
 {% extends "base.html" %}
 {% set active_page = "logs" %}
 {% block page_title %}Verlauf{% endblock %}
-{% block page_sub %}
{{ selected_name or "kein Archiv-Log" }}
{% endblock %} +{% block page_sub %}
{{ selected_file.range_label if selected_file else "kein Log" }}
{% endblock %} {% block content %}

Log-Verlauf

-
Ältere, von logrotate rotierte Kopien des Live-Logs — {{ files|length }} Stand{{ 'e' if files|length != 1 else '' }} verfügbar.
+
Aktuelles Live-Log sowie ältere, von logrotate rotierte Kopien davon — {{ files|length }} Stand{{ 'e' if files|length != 1 else '' }} verfügbar, auswählbar nach Zeitraum. Aus Performance-Gründen unformatiert (RAW) dargestellt, ohne farbliche Aufbereitung.
@@ -16,7 +16,7 @@ Zurück zur Live-Ansicht