Live-Log: 300-Zeilen-Tail, RAW-Popup und Log-Verlauf (History) mit eigenem Recht
- /logs und /get_log liefern nur noch die letzten 300 Zeilen (_tail_lines, effizientes Rueckwaerts-Lesen ohne komplette Datei einzulesen) statt der kompletten live.log -- behebt Ladezeit-Probleme auf groesseren Umgebungen. - Neuer Endpoint /logs/live/raw liefert das komplette Live-Log fuer ein Popup im Stil des bestehenden 'Ungespeicherte Aenderungen'-Modals (window.openRawLogModal in app.js, lazy nachgeladen erst bei Klick). - Neue Unterseite 'Verlauf' (/logs/history, /logs/history/raw) fuer aeltere, von logrotate rotierte Kopien des Live-Logs (live.log.1, .2, ...), auswaehlbar per Dropdown, ebenfalls mit 300-Zeilen-Tail + RAW-Popup. - Eigenes Recht logs_history.view (Kind von logs_group, direkt unter Live einsortiert) statt an logs_live.view gekoppelt -- Live-Status sehen und durch alte Logs blaettern sind bewusst getrennte Rechte. - Dateiname-Validierung gegen Path-Traversal in _resolve_log_history_file (striktes Regex + Verzeichnis-Check + Existenzpruefung). - VERSION 1.0.4 -> 1.0.5. Live getestet auf Testsystem-Update-Restart (192.168.82.51): Tail-Anzeige, RAW-Popup, Verlauf-Auswahl inkl. echter live.log.1, Permission-Matrix (Gruppen-Seite) sowie Rechtetrennung (Nutzer mit nur logs_live.view sieht /logs, aber weder Verlauf-Link noch Zugriff auf /logs/history bzw. /logs/history/raw) und Path-Traversal-Abwehr allesamt verifiziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"key": '<circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M17 6l3 3M14 9l2 2"/>',
|
||||
"terminal": '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 9l4 3-4 3M13 15h5"/>',
|
||||
"history": '<path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
||||
"clock": '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/>',
|
||||
"sliders": '<path d="M4 6h9M17 6h3M4 12h3M11 12h9M4 18h13M20 18h0"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="17" cy="18" r="2"/>',
|
||||
"logout": '<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/>',
|
||||
"gear": '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>',
|
||||
|
||||
@@ -8,18 +8,31 @@
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Live-Log</h2>
|
||||
<div class="hint">Laufende Erreichbarkeitsprüfung von poe.sh, farblich markiert (online/offline).</div>
|
||||
<div class="hint">Laufende Erreichbarkeitsprüfung von poe.sh, farblich markiert (online/offline). Zeigt aus Performance-Gründen nur die letzten {{ tail_lines }} Zeilen.</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{% if current_user.can_view_log_history %}
|
||||
<a href="{{ url_for('logs_history') }}" class="btn btn-secondary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>
|
||||
Verlauf
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary" onclick="openRawLogModal('{{ url_for('get_log_raw') }}', 'Komplettes Live-Log (RAW)')">
|
||||
<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)
|
||||
</button>
|
||||
<button id="refresh-btn" class="btn btn-secondary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3.2-6.9M21 4v5h-5"/></svg>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<button id="refresh-btn" class="btn btn-secondary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3.2-6.9M21 4v5h-5"/></svg>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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;" data-log-name>{{ log_name or "" }}</span>
|
||||
<span class="text-faint mono" style="font-size:11.5px;" id="log-line-info">{% if total_lines %}letzte {{ tail_lines }} von {{ total_lines }} Zeilen{% endif %}</span>
|
||||
</div>
|
||||
<div id="log-box">{{ log_content or "Keine Logfiles gefunden." }}</div>
|
||||
</div>
|
||||
@@ -43,7 +56,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const intervalMinutes = {{ global_check_interval | int }};
|
||||
const intervalMilliseconds = intervalMinutes * 60 * 1000;
|
||||
|
||||
function renderLog(text, logName) {
|
||||
function renderLog(text, logName, totalLines) {
|
||||
const box = document.getElementById("log-box");
|
||||
box.innerHTML = "";
|
||||
const lines = text.split("\n");
|
||||
@@ -55,11 +68,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (logName) {
|
||||
document.querySelectorAll("[data-log-name]").forEach((el) => { el.textContent = logName; });
|
||||
}
|
||||
const info = document.getElementById("log-line-info");
|
||||
if (info && totalLines) {
|
||||
info.textContent = "letzte {{ tail_lines }} von " + totalLines + " Zeilen";
|
||||
}
|
||||
}
|
||||
|
||||
function fetchLog() {
|
||||
fetch("{{ url_for('get_log') }}")
|
||||
.then(r => r.text().then((text) => renderLog(text, r.headers.get("X-Log-Name"))))
|
||||
.then(r => r.text().then((text) => renderLog(text, r.headers.get("X-Log-Name"), r.headers.get("X-Total-Lines"))))
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
{% 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 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>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ url_for('logs') }}" class="btn btn-secondary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
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 '' }}')"
|
||||
{% 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)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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.
|
||||
</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;">
|
||||
{% 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 }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div id="log-box">{{ log_content or "" }}</div>
|
||||
</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;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user