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:
+1
-1
@@ -1 +1 @@
|
||||
1.0.6
|
||||
1.0.7
|
||||
|
||||
+46
-14
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user