/* * Protokollseite: die Logdateien der Hintergrundprozesse ansehen. * * Die Seite haelt nichts vor - jede Anfrage holt frisch die letzten N Zeilen * vom Server. Das ist Absicht: ein Log wird von hinten gelesen, und wer * blaettert, will ohnehin filtern statt scrollen. Der Server liest die Datei * dafuer rueckwaerts, ein Blick in die 300-MB-Datei der Wallbox kostet * deshalb so viel wie einer in die kleine des Weckers. * * Gezeichnet wird aus der Antwort, nicht aus dem DOM. Die einzige Sache, die * ueber einen Aufruf hinaus bestehen bleibt, ist die zuletzt gewaehlte Datei * im localStorage - damit man nach dem Neuladen dort weitermacht, wo man war. */ (function () { "use strict"; const liste = document.querySelector(".log-liste"); if (!liste) return; // andere Seite const ausgabe = document.querySelector(".log-ausgabe"); const hinweis = document.querySelector(".log-hinweis"); const titel = document.querySelector(".log-titel"); const standTxt = document.querySelector(".log-stand"); const suche = document.querySelector(".log-suche"); const level = document.querySelector(".log-level"); const anzahl = document.querySelector(".log-anzahl"); const folgen = document.querySelector(".log-folgen"); const SPEICHER = "logDateiWahl"; // Direkt und nicht ueber window: die Seite deklariert den Ausgangsstand mit // const, und das legt nichts auf window ab. let dateien = (typeof logDateienStand !== "undefined" && Array.isArray(logDateienStand)) ? logDateienStand : []; let gewaehlt = localStorage.getItem(SPEICHER) || (dateien[0] && dateien[0].key) || "runner"; let uhr = null; let abbruch = null; // laufende Anfrage, siehe laden() // --- Kleinkram ---------------------------------------------------------- function escape(s) { return String(s === null || s === undefined ? "" : s) .replace(/&/g, "&").replace(//g, ">"); } /** 345351905 -> "329,4 MB" */ function groesse(bytes) { if (!bytes) return "leer"; const einheiten = ["B", "kB", "MB", "GB"]; let i = 0, z = bytes; while (z >= 1024 && i < einheiten.length - 1) { z /= 1024; i++; } return (i === 0 ? z : z.toFixed(1).replace(".", ",")) + " " + einheiten[i]; } /** * "vor 3 Minuten". Wichtiger als der Zeitstempel selbst: eine Datei, die * seit Tagen nicht gewachsen ist, gehoert angesehen. */ function alter(stand) { if (!stand) return ""; const sek = (Date.now() - new Date(stand.replace(" ", "T")).getTime()) / 1000; if (sek < 90) return "gerade eben"; if (sek < 5400) return "vor " + Math.round(sek / 60) + " min"; if (sek < 172800) return "vor " + Math.round(sek / 3600) + " h"; return "vor " + Math.round(sek / 86400) + " Tagen"; } /** Ab wann eine Datei als eingeschlafen gilt - grob, nur zum Einfaerben. */ function eingeschlafen(stand) { if (!stand) return true; return (Date.now() - new Date(stand.replace(" ", "T")).getTime()) > 24 * 3600 * 1000; } // --- Dateiliste --------------------------------------------------------- function listeZeichnen() { liste.innerHTML = dateien.map(function (d) { const aktiv = d.key === gewaehlt ? " active" : ""; let zustand; if (!d.da) { zustand = 'gibt es nicht'; } else if (!d.lesbar) { zustand = 'nicht lesbar'; } else { const farbe = eingeschlafen(d.stand) ? "text-warning" : "text-secondary"; zustand = '' + escape(alter(d.stand)) + "" + ' · ' + escape(groesse(d.groesse)) + ""; } return '"; }).join(""); } function zustandLaden() { return fetch("ajax/logs.php?action=list", { cache: "no-store" }) .then(function (r) { return r.json(); }) .then(function (a) { if (a.dateien) { dateien = a.dateien; listeZeichnen(); } }) .catch(function () { /* der naechste Versuch kommt bestimmt */ }); } // --- Zeilen ------------------------------------------------------------- const KLASSE = { DEBUG: "log-debug", INFO: "log-info", WARNING: "log-warning", ERROR: "log-error", CRITICAL: "log-error" }; function zeilenZeichnen(antwort) { const zeilen = antwort.zeilen || []; if (!zeilen.length) { // Eine leere Datei und ein Filter ohne Treffer sehen gleich aus, sind // aber zwei verschiedene Befunde: das eine heisst "der Prozess hat noch // nichts gesagt", das andere "such woanders". const leer = (antwort.zustand || {}).groesse === 0; ausgabe.innerHTML = '
' + (leer ? "Die Datei ist leer – seit dem letzten Start hat der Prozess nichts gemeldet." : "Keine Zeile passt.") + "
"; return; } let letztesDatum = ""; const stuecke = []; zeilen.forEach(function (z) { const zeit = z.t || ""; const datum = zeit.slice(0, 10); const uhrzeit = zeit.slice(11, 19); // Ein Tageswechsel bekommt eine eigene Zeile. Sonst steht das Datum // 300-mal untereinander und die Uhrzeit geht darin unter. if (datum && datum !== letztesDatum) { letztesDatum = datum; stuecke.push('
' + escape(datum.split("-").reverse().join(".")) + "
"); } const klasse = KLASSE[z.l] || "log-info"; stuecke.push( '
' + '' + escape(uhrzeit) + "" + '' + escape(z.fortsetzung ? "" : z.l) + "" + '' + escape(z.m) + "" + "
" ); }); ausgabe.innerHTML = stuecke.join(""); ausgabe.scrollTop = ausgabe.scrollHeight; // ans Ende, da steht das Neueste } function laden() { // Eine Suche, die nichts findet, laesst den Server vier Megabyte lesen - // das dauert spuerbar. Wer in der Zeit weitertippt, soll nicht auf die // alte Antwort warten: die laufende Anfrage wird abgebrochen, die neue // startet sofort. Eine blosse "laeuft gerade"-Sperre wuerde stattdessen // die letzte Eingabe verschlucken, und die ist die, die zaehlt. if (abbruch) abbruch.abort(); abbruch = new AbortController(); const meine = abbruch; hinweis.textContent = "sucht …"; const p = new URLSearchParams({ action: "read", datei: gewaehlt, anzahl: anzahl.value, suche: suche.value.trim(), level: level.value }); return fetch("ajax/logs.php?" + p.toString(), { cache: "no-store", signal: meine.signal }) .then(function (r) { return r.json(); }) .then(function (a) { if (a.error) { ausgabe.innerHTML = '
' + escape(a.error) + "
"; hinweis.textContent = ""; return; } const z = a.zustand || {}; titel.textContent = z.titel || "Protokoll"; standTxt.textContent = z.datei ? z.datei + " · " + groesse(z.groesse) + " · " + alter(z.stand) : ""; let text = (a.zeilen || []).length + " Zeilen"; if (a.abgeschnitten) { text += " · es wurden nur die letzten " + groesse(a.durchsucht) + " der Datei durchsucht, ältere Treffer fehlen also"; } hinweis.textContent = text; zeilenZeichnen(a); }) .catch(function (f) { // Ein Abbruch ist kein Fehler - da tippt jemand weiter. if (f.name === "AbortError") return; hinweis.textContent = ""; ausgabe.innerHTML = '
Nicht erreichbar: ' + escape(f.message) + "
"; }) .finally(function () { if (abbruch === meine) abbruch = null; }); } // --- Bedienung ---------------------------------------------------------- liste.addEventListener("click", function (e) { const knopf = e.target.closest(".log-wahl"); if (!knopf) return; gewaehlt = knopf.dataset.key; localStorage.setItem(SPEICHER, gewaehlt); listeZeichnen(); laden(); }); // Beim Tippen nicht bei jedem Anschlag fragen - das Lesen kostet auf der // NAS genug, dass man es merkt. let tippUhr = null; suche.addEventListener("input", function () { clearTimeout(tippUhr); tippUhr = setTimeout(laden, 400); }); level.addEventListener("change", laden); anzahl.addEventListener("change", laden); document.querySelector(".log-neu").addEventListener("click", function () { zustandLaden(); laden(); }); document.querySelector(".log-zustand-neu").addEventListener("click", zustandLaden); folgen.addEventListener("change", function () { clearInterval(uhr); if (folgen.checked) { uhr = setInterval(function () { // Im Hintergrundtab nicht nachladen: das Fenster sieht niemand, und // die Anfrage laeuft trotzdem ueber die Platte. if (!document.hidden) { zustandLaden(); laden(); } }, 10000); } }); listeZeichnen(); laden(); // Der eingebettete Stand ist beim Aufschlagen schon ein paar Sekunden alt. // Ein Nachfassen kostet nichts und macht aus "vor 18 min" die Wahrheit. zustandLaden(); })();