Benachrichtigungen: Geraet, Abos und der Reiter Meldungen

Der Geraetesuchlauf legt "Benachrichtigungen" an (benachrichtigung_module.py),
ein Kommando je Kanal. Damit kann jede Automatik melden, ohne dass Editor,
Zeitleiste oder Datenbank etwas davon wissen muessten.

Fuer die Meldung aufs Handy braucht es keine App: Der Browser meldet sich
unter Einstellungen -> Meldungen selbst an (sw.js, ajax/push.php), das Abo
steht in homeMesh.push_abos, verschickt wird vom Runner. Auf dem iPhone geht
es erst ab der Ablage auf dem Home-Bildschirm - der Reiter sagt das auch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 08:37:31 +02:00
co-authored by Claude Opus 5
parent 061dfa3fc5
commit df48432133
10 changed files with 720 additions and 1 deletions
+132
View File
@@ -1432,7 +1432,139 @@ function skodaBinden() {
if (neuLaden) neuLaden.addEventListener("click", skodaHolen);
}
/* ==========================================================================
Reiter "Meldungen": Web-Push-Abos dieses und der anderen Geräte.
Das Abo gehört dem Browser. Deshalb steht hier zweierlei nebeneinander:
dieses Gerät (an- und abmelden) und die Liste aller angemeldeten Geräte
(entfernen, wenn eines weg ist). Verschickt wird nichts von hier - die
Probe geht über MQTT an den Runner, genau wie eine Automatik.
========================================================================== */
let pushDiesesGeraet = null; // Abo dieses Browsers, oder null
async function pushZeichnen() {
const ziel = document.getElementById("pushListe");
if (!ziel) return;
if (!pushMoeglich()) {
ziel.innerHTML = '<div class="alert alert-secondary mb-0">Dieser Browser kann keine '
+ "Benachrichtigungen empfangen. Auf dem iPhone geht es erst, wenn die Seite über "
+ "„Teilen ▸ Zum Home-Bildschirm“ abgelegt und von dort geöffnet wird.</div>";
return;
}
if (!pushStand.schluessel) {
ziel.innerHTML = '<div class="alert alert-warning mb-0">Auf der NAS fehlt das '
+ "Schlüsselpaar. Einmalig <code>python3 vapid_erzeugen.py</code> im SolarManager "
+ "ausführen, dann diese Seite neu laden.</div>";
return;
}
const an = !!pushDiesesGeraet;
const blockiert = Notification.permission === "denied";
const zeilen = pushStand.abos.map(a => {
const stand = a.fehler
? '<span class="text-danger">' + textSchuetzen(a.fehler) + "</span>"
: a.zuletzt ? "zuletzt zugestellt " + textSchuetzen(a.zuletzt)
: "noch nichts zugestellt";
return '<div class="push-abo border-top py-2 d-flex align-items-center gap-2 flex-wrap">'
+ '<i class="bi bi-phone text-body-secondary"></i>'
+ "<b>" + textSchuetzen(a.name || "Gerät") + "</b>"
+ '<span class="small text-body-secondary">' + textSchuetzen(a.browser) + "</span>"
+ '<span class="small text-body-secondary ms-auto">' + stand + "</span>"
+ '<button type="button" class="btn btn-outline-danger btn-sm push-weg" data-id="' + a.id
+ '" title="Dieses Gerät entfernen"><i class="bi bi-trash3"></i></button></div>';
}).join("");
ziel.innerHTML =
'<div class="d-flex flex-wrap align-items-center gap-2 mb-3">'
+ '<button type="button" class="btn ' + (an ? "btn-outline-secondary" : "btn-primary")
+ '" id="pushSchalter"' + (blockiert && !an ? " disabled" : "") + ">"
+ '<i class="bi ' + (an ? "bi-bell-slash" : "bi-bell") + ' me-1"></i>'
+ (an ? "Auf diesem Gerät abmelden" : "Auf diesem Gerät anmelden") + "</button>"
+ '<button type="button" class="btn btn-outline-primary" id="pushProbe"'
+ (pushStand.abos.length ? "" : " disabled") + '><i class="bi bi-send me-1"></i>Probe schicken</button>'
+ '<span class="small" id="pushMeldung"></span>'
+ "</div>"
+ (blockiert && !an
? '<div class="alert alert-warning py-2 small">Der Browser hat Benachrichtigungen für '
+ "diese Seite blockiert. Das lässt sich nur in seinen eigenen Website-Einstellungen "
+ "wieder erlauben.</div>"
: "")
+ (zeilen
? '<div class="push-abos">' + zeilen + "</div>"
: '<p class="text-secondary small mb-0">Noch kein Gerät angemeldet.</p>');
document.getElementById("pushSchalter").addEventListener("click", pushSchalten);
document.getElementById("pushProbe").addEventListener("click", pushProbeSchicken);
ziel.querySelectorAll(".push-weg").forEach(k => k.addEventListener("click", async () => {
pushMeldung("wird entfernt …");
try {
await pushEntfernen(Number(k.dataset.id));
pushDiesesGeraet = await pushAbo();
await pushZeichnen();
} catch (fehler) {
pushMeldung(fehler.message, "text-danger");
}
}));
}
function pushMeldung(text, klasse) {
const el = document.getElementById("pushMeldung");
if (!el) return;
el.className = "small " + (klasse || "text-body-secondary");
el.textContent = text || "";
}
async function pushSchalten() {
const an = !!pushDiesesGeraet;
pushMeldung(an ? "wird abgemeldet …" : "wird angemeldet …");
try {
if (an) await pushAbmelden();
else await pushAnmelden();
pushDiesesGeraet = await pushAbo();
await pushZeichnen();
pushMeldung(an ? "Abgemeldet." : "Angemeldet die Probe kommt jetzt hier an.",
an ? "text-body-secondary" : "text-success");
} catch (fehler) {
pushMeldung(fehler.message, "text-danger");
}
}
async function pushProbeSchicken() {
pushMeldung("Probe unterwegs …");
try {
await pushProbe();
pushMeldung("Probe geschickt sie geht über den Runner auf der NAS.", "text-success");
} catch (fehler) {
pushMeldung(fehler.message, "text-danger");
}
}
async function pushLaden() {
try {
await pushStandLaden();
pushDiesesGeraet = await pushAbo();
} catch (fehler) {
const ziel = document.getElementById("pushListe");
if (ziel) ziel.innerHTML = '<div class="alert alert-danger mb-0">' + textSchuetzen(fehler.message) + "</div>";
return;
}
await pushZeichnen();
}
document.addEventListener("DOMContentLoaded", function () {
skodaBinden();
skodaHolen();
});
function pushBinden() {
const neu = document.getElementById("pushNeuLaden");
if (neu) neu.addEventListener("click", pushLaden);
}
document.addEventListener("DOMContentLoaded", function () {
pushBinden();
pushLaden();
});