Verlauf: aktuelle live.log als Eintrag, Auswahl per Zeitraum statt Dateiname; RAW-Darstellung

- _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 <pre class="raw-log-content"> 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 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 11:28:15 +02:00
co-authored by Claude Sonnet 5
parent befc76819e
commit 8253ab3f0e
3 changed files with 58 additions and 48 deletions
+1 -1
View File
@@ -1 +1 @@
1.0.6
1.0.7
+46 -14
View File
@@ -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,
)
+11 -33
View File
@@ -1,14 +1,14 @@
{% extends "base.html" %}
{% set active_page = "logs" %}
{% block page_title %}Verlauf{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ selected_name or "kein Archiv-Log" }}</div>{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ selected_file.range_label if selected_file else "kein Log" }}</div>{% endblock %}
{% block content %}
<div class="section-head">
<div>
<h2 style="font-size:16px;">Log-Verlauf</h2>
<div class="hint">Ältere, von logrotate rotierte Kopien des Live-Logs — {{ files|length }} Stand{{ 'e' if files|length != 1 else '' }} verfügbar.</div>
<div class="hint">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.</div>
</div>
<div class="flex gap-2">
<a href="{{ url_for('logs') }}" class="btn btn-secondary">
@@ -16,7 +16,7 @@
Zurück zur Live-Ansicht
</a>
<button type="button" class="btn btn-secondary"
onclick="openRawLogModal('{{ url_for('logs_history_raw', file=selected_name) if selected_name else '' }}', 'Komplettes Log (RAW) — {{ selected_name or '' }}')"
onclick="openRawLogModal('{{ url_for('logs_history_raw', file=selected_name) if selected_name else '' }}', 'Komplettes Log (RAW) — {{ selected_file.range_label if selected_file else '' }}')"
{% if not selected_name %}disabled{% endif %}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
Komplettes Log (RAW)
@@ -26,18 +26,16 @@
{% if not files %}
<p class="text-faint" style="font-size:12.5px;">
Noch keine rotierten Live-Logs vorhanden — sobald logrotate das aktuelle
<a href="{{ url_for('logs') }}" style="color:var(--accent-strong); font-weight:600;">Live-Log</a>
zum ersten Mal rotiert (siehe Rotations-Intervall unter Systemeinstellungen), erscheinen ältere Stände hier.
Kein Live-Log gefunden.
</p>
{% else %}
<div class="flex gap-2" style="align-items:center; margin-bottom:14px;">
<label for="history-file-select" class="text-faint" style="font-size:12.5px; font-weight:600;">Archiv-Stand:</label>
<select id="history-file-select" onchange="window.location.href=this.value;" class="mono" style="max-width:420px;">
<label for="history-file-select" class="text-faint" style="font-size:12.5px; font-weight:600;">Zeitraum:</label>
<select id="history-file-select" onchange="window.location.href=this.value;" class="mono" style="max-width:460px;">
{% for f in files %}
<option value="{{ url_for('logs_history', file=f.filename) }}" {% if f.filename == selected_name %}selected{% endif %}>
{{ f.filename }} — rotiert am {{ f.mtime_str }} ({{ f.size_str }})
{% if f.generation == 0 %}Aktuell — {{ f.range_label }}{% else %}{{ f.range_label }}{% endif %} ({{ f.size_str }})
</option>
{% endfor %}
</select>
@@ -46,39 +44,19 @@
<div class="log-shell">
<div class="log-toolbar">
<div class="log-dots"><span></span><span></span><span></span></div>
<span class="text-faint mono" style="font-size:11.5px;">{{ selected_name }}</span>
<span class="text-faint mono" style="font-size:11.5px;">{% if total_lines %}letzte {{ tail_lines }} von {{ total_lines }} Zeilen{% endif %}</span>
<span class="text-faint mono" style="font-size:11.5px;">{{ selected_file.range_label if selected_file else "" }}</span>
<span class="text-faint mono" style="font-size:11.5px;">{% if total_lines %}letzte {{ tail_lines }} von {{ total_lines }} Zeilen — RAW{% endif %}</span>
</div>
<div id="log-box">{{ log_content or "" }}</div>
<pre id="log-box" class="raw-log-content" style="height:100%;">{{ log_content or "" }}</pre>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
function colorizeLine(line) {
let cls = "";
if (line.includes(" ist erreichbar!")) cls = "online";
else if (line.includes(" ist nicht erreichbar!")) cls = "offline";
else if (line.startsWith("----")) cls = "sep";
else if (line.toLowerCase().includes("manuell") || line.includes("PoE")) cls = "restart";
const span = document.createElement("span");
span.className = "log-line" + (cls ? " " + cls : "");
span.textContent = line;
return span;
}
document.addEventListener("DOMContentLoaded", () => {
const box = document.getElementById("log-box");
if (!box) return;
const text = box.textContent;
box.innerHTML = "";
const lines = text.split("\n");
lines.forEach((line, i) => {
box.appendChild(colorizeLine(line));
if (i < lines.length - 1) box.appendChild(document.createElement("br"));
});
box.scrollTop = box.scrollHeight;
if (box) box.scrollTop = box.scrollHeight;
});
</script>
{% endblock %}