Files
Smart-Dashboard/js/solar/logs.js
T
adminandClaude Opus 5 cbc9353f0a 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 <noreply@anthropic.com>
2026-09-03 19:26:56 +02:00

251 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
/** 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 = '<span class="text-danger">gibt es nicht</span>';
} else if (!d.lesbar) {
zustand = '<span class="text-danger">nicht lesbar</span>';
} else {
const farbe = eingeschlafen(d.stand) ? "text-warning" : "text-secondary";
zustand = '<span class="' + farbe + '">' + escape(alter(d.stand)) + "</span>"
+ '<span class="text-secondary"> · ' + escape(groesse(d.groesse)) + "</span>";
}
return '<button type="button" class="list-group-item list-group-item-action log-wahl' + aktiv + '"'
+ ' data-key="' + escape(d.key) + '" title="' + escape(d.hinweis) + '">'
+ '<div class="d-flex align-items-center">'
+ '<i class="bi ' + escape(d.symbol) + ' me-2"></i>'
+ '<span class="flex-grow-1">' + escape(d.titel) + "</span>"
+ "</div>"
+ '<div class="small mt-1">' + zustand + "</div>"
+ "</button>";
}).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 = '<div class="text-secondary p-3">'
+ (leer ? "Die Datei ist leer – seit dem letzten Start hat der Prozess nichts gemeldet."
: "Keine Zeile passt.")
+ "</div>";
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('<div class="log-tag">' + escape(datum.split("-").reverse().join(".")) + "</div>");
}
const klasse = KLASSE[z.l] || "log-info";
stuecke.push(
'<div class="log-zeile ' + klasse + (z.fortsetzung ? " log-fortsetzung" : "") + '">'
+ '<span class="log-zeit">' + escape(uhrzeit) + "</span>"
+ '<span class="log-level">' + escape(z.fortsetzung ? "" : z.l) + "</span>"
+ '<span class="log-text">' + escape(z.m) + "</span>"
+ "</div>"
);
});
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 = '<div class="alert alert-warning mb-0">' + escape(a.error) + "</div>";
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 = '<div class="alert alert-danger mb-0">Nicht erreichbar: ' + escape(f.message) + "</div>";
})
.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();
})();