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_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():
|
def _live_log_history_files():
|
||||||
"""Ältere, von logrotate rotierte Kopien des Live-Logs (live.log.1 =
|
"""Alle über die Verlauf-Seite auswählbaren Stände des Live-Logs --
|
||||||
zuletzt rotiert, .2 der Lauf davor, usw. -- copytruncate ohne compress,
|
generation 0 ist die aktuelle, noch laufende live.log selbst, 1 die
|
||||||
siehe _write_logrotate_config, daher reine Textdateien ohne .gz),
|
zuletzt rotierte Kopie, 2 der Lauf davor usw. (copytruncate ohne
|
||||||
sortiert von neu (1) nach alt."""
|
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 = []
|
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:
|
try:
|
||||||
entries = os.listdir(TESM_LOG_DIR)
|
entries = os.listdir(TESM_LOG_DIR)
|
||||||
except OSError:
|
except OSError:
|
||||||
return results
|
entries = []
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
m = _LOG_HISTORY_FILE_RE.match(entry)
|
m = _LOG_HISTORY_FILE_RE.match(entry)
|
||||||
if not m:
|
if not m:
|
||||||
@@ -1929,20 +1950,30 @@ def _live_log_history_files():
|
|||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
results.append({
|
results.append({
|
||||||
"filename": entry,
|
"filename": entry, "generation": int(m.group(1)),
|
||||||
"generation": int(m.group(1)),
|
"mtime": mtime, "size_str": _format_log_size(size),
|
||||||
"mtime_str": datetime.fromtimestamp(mtime).strftime("%d.%m.%Y %H:%M"),
|
|
||||||
"size_str": _format_log_size(size),
|
|
||||||
})
|
})
|
||||||
results.sort(key=lambda r: r["generation"])
|
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
|
return results
|
||||||
|
|
||||||
|
|
||||||
def _resolve_log_history_file(filename):
|
def _resolve_log_history_file(filename):
|
||||||
"""Validiert einen vom Client übergebenen Dateinamen (?file=...) gegen
|
"""Validiert einen vom Client übergebenen Dateinamen (?file=...) gegen
|
||||||
die tatsächlich vorhandenen rotierten Live-Log-Kopien in TESM_LOG_DIR --
|
die tatsächlich vorhandene aktuelle live.log bzw. rotierte Kopien davon
|
||||||
verhindert Path-Traversal bzw. beliebiges Datei-Lesen über den
|
in TESM_LOG_DIR -- verhindert Path-Traversal bzw. beliebiges
|
||||||
Query-Parameter. Gibt den vollen Pfad oder None zurück."""
|
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):
|
if not filename or not _LOG_HISTORY_FILE_RE.match(filename):
|
||||||
return None
|
return None
|
||||||
full_path = os.path.join(TESM_LOG_DIR, filename)
|
full_path = os.path.join(TESM_LOG_DIR, filename)
|
||||||
@@ -5901,7 +5932,8 @@ def logs_history():
|
|||||||
selected_name = requested if selected_path else None
|
selected_name = requested if selected_path else None
|
||||||
if not selected_path and files:
|
if not selected_path and files:
|
||||||
selected_name = files[0]["filename"]
|
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
|
log_content = None
|
||||||
total_lines = None
|
total_lines = None
|
||||||
@@ -5913,7 +5945,7 @@ def logs_history():
|
|||||||
log_content = f"Fehler beim Lesen des Logs: {e}"
|
log_content = f"Fehler beim Lesen des Logs: {e}"
|
||||||
|
|
||||||
return render_template(
|
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,
|
log_content=log_content, total_lines=total_lines, tail_lines=LIVE_LOG_TAIL_LINES,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "logs" %}
|
{% set active_page = "logs" %}
|
||||||
{% block page_title %}Verlauf{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<h2 style="font-size:16px;">Log-Verlauf</h2>
|
<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>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a href="{{ url_for('logs') }}" class="btn btn-secondary">
|
<a href="{{ url_for('logs') }}" class="btn btn-secondary">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
Zurück zur Live-Ansicht
|
Zurück zur Live-Ansicht
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-secondary"
|
<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 %}>
|
{% 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>
|
<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)
|
Komplettes Log (RAW)
|
||||||
@@ -26,18 +26,16 @@
|
|||||||
|
|
||||||
{% if not files %}
|
{% if not files %}
|
||||||
<p class="text-faint" style="font-size:12.5px;">
|
<p class="text-faint" style="font-size:12.5px;">
|
||||||
Noch keine rotierten Live-Logs vorhanden — sobald logrotate das aktuelle
|
Kein Live-Log gefunden.
|
||||||
<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.
|
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="flex gap-2" style="align-items:center; margin-bottom:14px;">
|
<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>
|
<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:420px;">
|
<select id="history-file-select" onchange="window.location.href=this.value;" class="mono" style="max-width:460px;">
|
||||||
{% for f in files %}
|
{% for f in files %}
|
||||||
<option value="{{ url_for('logs_history', file=f.filename) }}" {% if f.filename == selected_name %}selected{% endif %}>
|
<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>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@@ -46,39 +44,19 @@
|
|||||||
<div class="log-shell">
|
<div class="log-shell">
|
||||||
<div class="log-toolbar">
|
<div class="log-toolbar">
|
||||||
<div class="log-dots"><span></span><span></span><span></span></div>
|
<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;">{{ 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{% endif %}</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>
|
||||||
<div id="log-box">{{ log_content or "" }}</div>
|
<pre id="log-box" class="raw-log-content" style="height:100%;">{{ log_content or "" }}</pre>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<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", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const box = document.getElementById("log-box");
|
const box = document.getElementById("log-box");
|
||||||
if (!box) return;
|
if (box) box.scrollTop = box.scrollHeight;
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user