From cbc9353f0a33a7f58785337a842cf3695d1b5f2d Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Thu, 3 Sep 2026 19:26:56 +0200 Subject: [PATCH] Protokollseite: die Logs der Hintergrundprozesse im Dashboard Bisher kam man an die Ausgabe von solarManager.py, dem Runner und den uebrigen Dauerprozessen nur per SSH heran. Die neue Seite unter "Protokolle" zeigt sie: links die fuenf Dateien mit Groesse und Alter - eine, die seit Tagen nicht gewachsen ist, faellt so auf, ohne dass man sie oeffnet -, rechts die Zeilen mit Volltextsuche, Level-Filter und optionalem Nachladen. Nicht in die Datenbank geschrieben, obwohl das naheliegt: ein Log muss genau dann noch funktionieren, wenn die Datenbank es nicht tut. Ein Handler, der nach MySQL schreibt, verschluckt ausgerechnet die Meldung "Datenbank nicht erreichbar" - also die, wegen der man nachsieht. Was strukturiert ausgewertet werden soll, steht ohnehin in automation_log. Gelesen wird rueckwaerts in Bloecken: 5000 Zeilen aus der 329 MB grossen Datei der Wallbox kosten 46 ms, weil nur das Ende angefasst wird. Sucht man ins Leere, bricht der Server nach 4 MB ab und sagt es auch. Die beiden Prozesse formatieren unterschiedlich - der eine stellt das Level voran, der andere den Zeitstempel -, deshalb wird beides gesucht statt an fester Stelle erwartet. Zeilen ohne eigenes Level erben das der Zeile darueber, sonst filterte "nur Fehler" die Tracebacks weg, die den Fehler erklaeren. Co-Authored-By: Claude Opus 5 --- ajax/logs.php | 59 +++++++++ css/solar.css | 61 +++++++++ index.php | 2 + js/solar/logs.js | 250 +++++++++++++++++++++++++++++++++++ restricted/header.php | 10 +- restricted/logdateien.php | 271 ++++++++++++++++++++++++++++++++++++++ restricted/logs.php | 125 ++++++++++++++++++ 7 files changed, 777 insertions(+), 1 deletion(-) create mode 100644 ajax/logs.php create mode 100644 js/solar/logs.js create mode 100644 restricted/logdateien.php create mode 100644 restricted/logs.php diff --git a/ajax/logs.php b/ajax/logs.php new file mode 100644 index 0000000..b712523 --- /dev/null +++ b/ajax/logs.php @@ -0,0 +1,59 @@ + $liste]); + + case "read": + jsonAntwort(logLesen( + $_GET["datei"] ?? "", + $_GET["anzahl"] ?? 300, + trim((string) ($_GET["suche"] ?? "")), + strtoupper(trim((string) ($_GET["level"] ?? ""))) + )); + + default: + jsonAntwort(["error" => "Unbekannte Aktion."], 400); + } +} catch (InvalidArgumentException $e) { + jsonAntwort(["error" => $e->getMessage()], 400); +} catch (Throwable $e) { + jsonAntwort(["error" => $e->getMessage()], 500); +} diff --git a/css/solar.css b/css/solar.css index 20851b9..5be521c 100644 --- a/css/solar.css +++ b/css/solar.css @@ -906,3 +906,64 @@ input[type='range']::-webkit-slider-runnable-track { } .skoda-alter { font-size: .85rem; } + +/* === Protokollseite ====================================================== + Ein Log liest sich nur in einer Schrift mit fester Breite: die Zeitstempel + stehen dann untereinander, und das Auge findet die Stelle, an der etwas + anders wird. Die Zeilen umbrechen statt seitlich zu scrollen - ein + Traceback ist lang, und waagerecht zu blaettern verliert man sofort. */ +.log-ausgabe { + max-height: calc(100vh - 22rem); + min-height: 20rem; + overflow-y: auto; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: .78rem; + line-height: 1.45; + background: var(--bs-body-bg, #1b1e21); + border: 1px solid var(--bs-border-color, #2c3034); + border-radius: .375rem; +} + +.log-ausgabe .log-zeile { + display: flex; + gap: .6rem; + padding: .1rem .6rem; + border-left: 3px solid transparent; + white-space: pre-wrap; + word-break: break-word; +} +.log-ausgabe .log-zeile:hover { background: rgba(255, 255, 255, .04); } + +.log-ausgabe .log-zeit { flex: 0 0 5rem; color: var(--bs-secondary-color, #8b9198); } +.log-ausgabe .log-level { flex: 0 0 4.5rem; font-weight: 600; } +.log-ausgabe .log-text { flex: 1 1 auto; } + +/* Fortsetzungszeilen - die zweite bis letzte Zeile eines Tracebacks - tragen + kein eigenes Level. Sie bleiben in der Farbe ihrer Kopfzeile, ruecken aber + ein: so sieht man auf einen Blick, was zusammengehoert. */ +.log-ausgabe .log-fortsetzung .log-text { padding-left: 1.2rem; opacity: .85; } + +.log-ausgabe .log-debug { color: var(--bs-secondary-color, #8b9198); } +.log-ausgabe .log-info .log-level { color: #6ea8dc; } +.log-ausgabe .log-warning { border-left-color: #d9a441; background: rgba(217, 164, 65, .07); } +.log-ausgabe .log-warning .log-level { color: #d9a441; } +.log-ausgabe .log-error { border-left-color: #e06060; background: rgba(224, 96, 96, .09); } +.log-ausgabe .log-error .log-level { color: #f08a8a; } + +/* Tagestrenner: das Datum steht einmal da, nicht in jeder Zeile. */ +.log-ausgabe .log-tag { + position: sticky; + top: 0; + padding: .15rem .6rem; + font-size: .72rem; + letter-spacing: .04em; + color: var(--bs-secondary-color, #8b9198); + background: var(--bs-tertiary-bg, #23272b); + border-top: 1px solid var(--bs-border-color, #2c3034); + border-bottom: 1px solid var(--bs-border-color, #2c3034); +} + +/* Die Dateiliste links: der aktive Eintrag wird von AdminLTE eingefaerbt, + die Zustandszeile darunter soll trotzdem lesbar bleiben. */ +.log-liste .log-wahl.active .text-secondary, +.log-liste .log-wahl.active .text-warning { color: rgba(255, 255, 255, .75) !important; } diff --git a/index.php b/index.php index e6763e0..b7dd3ed 100644 --- a/index.php +++ b/index.php @@ -113,6 +113,8 @@ if (isset($_GET["addUser"])) { "scripts" => ["js/solar/weatherMQTT.js"]], "settings" => ["template" => "settings.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false, "scripts" => ["js/solar/settings.js"]], + "logs" => ["template" => "logs.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false, + "scripts" => ["js/solar/logs.js"]], ]; // Unbekannte oder fehlende Aktion faellt auf die Solar-Seite zurueck. diff --git a/js/solar/logs.js b/js/solar/logs.js new file mode 100644 index 0000000..5c44762 --- /dev/null +++ b/js/solar/logs.js @@ -0,0 +1,250 @@ +/* + * 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(); +})(); diff --git a/restricted/header.php b/restricted/header.php index 6482ab2..fe3cc64 100644 --- a/restricted/header.php +++ b/restricted/header.php @@ -183,13 +183,21 @@ if (!isset($_GET["floor"])) + sondern die Parameter dahinter. Genauso die + Protokolle - sie zeigen nicht das Haus, sondern + die Prozesse, die es steuern. --> + diff --git a/restricted/logdateien.php b/restricted/logdateien.php new file mode 100644 index 0000000..698f37b --- /dev/null +++ b/restricted/logdateien.php @@ -0,0 +1,271 @@ + [ + "titel" => "SolarManager", + "datei" => "solarOutput.log", + "symbol" => "bi-sun", + "hinweis" => "Messwerte einsammeln, Wallbox und Heizung regeln.", + ], + "runner" => [ + "titel" => "AutoAction-Runner", + "datei" => "autoActions.log", + "symbol" => "bi-diagram-3", + "hinweis" => "Wertet die Automatiken aus und schickt die Kommandos.", + ], + "wattpilot" => [ + "titel" => "Wattpilot", + "datei" => "wattpilotshell.log", + "symbol" => "bi-ev-station", + "hinweis" => "Bruecke zur Wallbox.", + ], + "mqtt" => [ + "titel" => "MQTT-Brücke", + "datei" => "wsMQTTbridge.log", + "symbol" => "bi-broadcast", + "hinweis" => "Reicht die Messwerte an den Browser weiter.", + ], + "wecker" => [ + "titel" => "Wecker", + "datei" => "wecker.log", + "symbol" => "bi-alarm", + "hinweis" => "Stellt morgens die Rollläden und das Licht.", + ], + ]; +} + +/** + * Beschreibung einer Datei samt Zustand: Groesse, Alter, ob lesbar. + * + * Die Seite zeigt das an, damit man eine stehengebliebene Datei erkennt, + * ohne sie erst zu oeffnen - ein Prozess, der seit Tagen nichts geschrieben + * hat, ist ein Befund fuer sich. + */ +function logZustand($key) +{ + $dateien = logDateien(); + if (!isset($dateien[$key])) { + throw new InvalidArgumentException("Unbekannte Logdatei: " . $key); + } + $eintrag = $dateien[$key]; + $pfad = LOG_BASIS . "/" . $eintrag["datei"]; + + $zustand = [ + "key" => $key, + "titel" => $eintrag["titel"], + "datei" => $eintrag["datei"], + "symbol" => $eintrag["symbol"], + "hinweis" => $eintrag["hinweis"], + "da" => false, + "lesbar" => false, + "groesse" => 0, + "stand" => null, + ]; + if (!is_file($pfad)) { + return $zustand; + } + $zustand["da"] = true; + $zustand["lesbar"] = is_readable($pfad); + $zustand["groesse"] = (int) filesize($pfad); + $zustand["stand"] = date("Y-m-d H:i:s", filemtime($pfad)); + return $zustand; +} + +/** + * Level einer Zeile, oder "" wenn keines drinsteht. + * + * Die beiden Prozesse formatieren unterschiedlich - der Runner schreibt + * "2026-09-03 18:34:25,108 - INFO - Text", der SolarManager stellt das Level + * nach vorn. Deshalb wird es gesucht und nicht an einer festen Stelle + * erwartet. Zeilen ohne Level sind meist Fortsetzungen: die zweite bis + * letzte Zeile eines Tracebacks. + */ +function logLevel($zeile) +{ + if (preg_match('/\b(DEBUG|INFO|WARNING|WARN|ERROR|CRITICAL|FATAL)\b/', $zeile, $t)) { + return $t[1] === "WARN" ? "WARNING" : ($t[1] === "FATAL" ? "CRITICAL" : $t[1]); + } + return ""; +} + +/** + * Eine Rohzeile in Zeitstempel, Level und Text zerlegen. + * + * Was nicht erkannt wird, bleibt im Text stehen - lieber eine Zeile, die + * roh aussieht, als eine, von der die Haelfte fehlt, weil ein Muster nicht + * gepasst hat. + */ +function logZerlegen($zeile) +{ + $zeit = null; + $text = $zeile; + + // Der SolarManager schreibt "INFO 2026-09-03 18:29:01,123 /langer/pfad/x.py x.py - Text". + // Der Pfad davor steht zweimal da und ist im Browser nur Ballast; der + // blosse Dateiname sagt dasselbe. + $text = preg_replace('#(\s)/\S+/(\S+\.py)(\s+\2\s+-\s)#', '$1$2 - ', $text, 1); + + if (preg_match('/(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?)/', $text, $t)) { + $zeit = str_replace(",", ".", $t[1]); + // Nur den Zeitstempel entfernen, nicht alles davor: beim + // SolarManager steht links davon das Level. + $text = trim(str_replace($t[1], "", $text)); + } + $level = logLevel($text); + if ($level !== "") { + // Das Level und die Trennzeichen drumherum wegnehmen, aber nur am + // Anfang - ein "ERROR" mitten im Satz bleibt stehen. + $text = preg_replace('/^[\s\-:]*' . preg_quote($level, '/') . '[\s\-:]*/', '', $text, 1); + } + return ["t" => $zeit, "l" => $level, "m" => $text]; +} + +/** + * Text so zurechtruecken, dass json_encode ihn nicht verweigert. + * + * In einem Log steht irgendwann eine Zeile, die kein gueltiges UTF-8 ist - + * ein abgeschnittener Umlaut, eine Fremdbibliothek in Latin-1. json_encode + * gibt dann fuer die ganze Antwort false zurueck, und die Seite bliebe leer. + */ +function logText($s) +{ + return mb_check_encoding($s, "UTF-8") ? $s : mb_convert_encoding($s, "UTF-8", "ISO-8859-1"); +} + +/** + * Die letzten Zeilen einer Logdatei, optional gefiltert. + * + * Gelesen wird rueckwaerts in Bloecken: erst der letzte, dann bei Bedarf der + * davor. So kostet ein Blick in eine 300-MB-Datei so viel wie einer in eine + * kleine, solange genug Treffer im letzten Stueck stehen. + * + * Innerhalb eines Blocks wird vorwaerts gearbeitet, damit Zeilen ohne + * eigenes Level - Tracebacks - das Level der Zeile darueber erben. Sonst + * verschwaende der Filter "nur Fehler" genau die Zeilen, die den Fehler + * erklaeren. + * + * @param string $key Schluessel aus logDateien() + * @param int $anzahl gewuenschte Zeilen + * @param string $suche Text, der vorkommen muss (leer = alles) + * @param string $level Mindestlevel: "", "INFO", "WARNING", "ERROR" + */ +function logLesen($key, $anzahl = 300, $suche = "", $level = "") +{ + $zustand = logZustand($key); + $pfad = LOG_BASIS . "/" . $zustand["datei"]; + + if (!$zustand["da"]) { + throw new RuntimeException("Die Datei " . $zustand["datei"] . " gibt es (noch) nicht."); + } + if (!$zustand["lesbar"]) { + throw new RuntimeException("Die Datei " . $zustand["datei"] . " ist für den Webserver nicht lesbar."); + } + + $anzahl = max(1, min(5000, (int) $anzahl)); + $rang = ["DEBUG" => 0, "" => 0, "INFO" => 1, "WARNING" => 2, "ERROR" => 3, "CRITICAL" => 4]; + $mindest = $rang[$level] ?? 0; + + $fh = fopen($pfad, "rb"); + if (!$fh) { + throw new RuntimeException("Die Datei " . $zustand["datei"] . " ließ sich nicht öffnen."); + } + + $groesse = $zustand["groesse"]; + $block = 128 * 1024; + $gelesen = 0; + $rest = ""; // angefangene Zeile am Blockanfang + $treffer = []; // gesammelt, neueste zuerst + $abgeschnitten = false; + + while ($gelesen < $groesse && count($treffer) < $anzahl) { + $laenge = (int) min($block, $groesse - $gelesen); + fseek($fh, $groesse - $gelesen - $laenge, SEEK_SET); + $roh = fread($fh, $laenge) . $rest; + $gelesen += $laenge; + + $zeilen = explode("\n", $roh); + // Die erste Zeile des Blocks ist angeschnitten, solange noch etwas + // davor liegt - sie wandert in den naechsten Durchgang. + $rest = ($gelesen < $groesse) ? array_shift($zeilen) : ""; + + // Vorwaerts: Level fortschreiben, damit Fortsetzungszeilen eins erben. + $vorbereitet = []; + $letztesLevel = ""; + foreach ($zeilen as $zeile) { + $zeile = rtrim($zeile, "\r"); + if ($zeile === "") { + continue; + } + $teil = logZerlegen(logText($zeile)); + if ($teil["l"] === "") { + $teil["l"] = $letztesLevel; + $teil["fortsetzung"] = true; + } else { + $letztesLevel = $teil["l"]; + $teil["fortsetzung"] = false; + } + $vorbereitet[] = $teil; + } + + // Rueckwaerts einsammeln: die juengsten Zeilen zuerst. + for ($i = count($vorbereitet) - 1; $i >= 0 && count($treffer) < $anzahl; $i--) { + $teil = $vorbereitet[$i]; + if (($rang[$teil["l"]] ?? 0) < $mindest) { + continue; + } + if ($suche !== "" && mb_stripos($teil["m"], $suche) === false + && mb_stripos((string) $teil["t"], $suche) === false) { + continue; + } + $treffer[] = $teil; + } + + if ($gelesen >= LOG_MAX_BYTES && count($treffer) < $anzahl && $gelesen < $groesse) { + $abgeschnitten = true; + break; + } + } + fclose($fh); + + return [ + "zustand" => $zustand, + "zeilen" => array_reverse($treffer), + "abgeschnitten" => $abgeschnitten, + "durchsucht" => $gelesen, + ]; +} diff --git a/restricted/logs.php b/restricted/logs.php new file mode 100644 index 0000000..5fcf521 --- /dev/null +++ b/restricted/logs.php @@ -0,0 +1,125 @@ +getMessage(); +} +?> + +
+ +
+
+
+
+

Protokolle

+
+
+
+
+ + +
+
+ +
Die Protokolle sind nicht erreichbar:
+ +
+ +
+
+
+

Prozesse

+
+ +
+
+
+
+
+
+
+ + +
+
+
+

+ Protokoll +

+
+ + +
+
+ +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+ +
+ +