Files
Smart-Dashboard/js/solar/benachrichtigungen.js
T
adminandClaude Opus 5 df48432133 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>
2026-09-21 08:37:31 +02:00

125 lines
4.5 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.
/*
* Benachrichtigungen auf diesem Gerät ein- und ausschalten.
*
* Web Push braucht drei Dinge, und alle drei gibt es hier schon: eine
* sichere Verbindung, einen Service Worker (sw.js im Wurzelverzeichnis) und
* ein Schlüsselpaar (VAPID, liegt auf der NAS). Eine eigene App braucht es
* nicht - unter iOS muss die Seite allerdings einmal über "Zum
* Home-Bildschirm" abgelegt werden, sonst verweigert Safari die Erlaubnis.
*
* Das Abo gehört dem Browser, nicht dem Konto: Jedes Gerät meldet sich
* einzeln an. Gespeichert wird es in homeMesh.push_abos (ajax/push.php),
* verschickt vom Runner auf der NAS.
*/
const PUSH_ENDPUNKT = "ajax/push.php";
let pushStand = { schluessel: "", abos: [] };
/** base64url aus der Datenbank in den Bytesalat, den der Browser will. */
function pushSchluesselBytes(text) {
const voll = (text + "=".repeat((4 - text.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
const roh = atob(voll);
return Uint8Array.from(roh, z => z.charCodeAt(0));
}
/** Was der Browser über sich verrät, auf das Nötige gekürzt. */
function pushBrowserName() {
const ua = navigator.userAgent;
const name = /Edg\//.test(ua) ? "Edge" : /OPR\//.test(ua) ? "Opera"
: /Chrome\//.test(ua) ? "Chrome" : /Safari\//.test(ua) ? "Safari"
: /Firefox\//.test(ua) ? "Firefox" : "Browser";
const os = /Android/.test(ua) ? "Android" : /iPhone|iPad/.test(ua) ? "iOS"
: /Windows/.test(ua) ? "Windows" : /Mac OS/.test(ua) ? "Mac" : "";
return os ? name + " · " + os : name;
}
function pushMoeglich() {
return "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
}
async function pushAnfrage(action, body) {
const antwort = await fetch(PUSH_ENDPUNKT + "?action=" + action, body ? {
method: "POST",
headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" },
body: JSON.stringify(body),
} : { cache: "no-store" });
const d = await antwort.json();
if (d.error) throw new Error(d.error);
return d;
}
/** Das Abo dieses Browsers, oder null. */
async function pushAbo() {
if (!pushMoeglich()) return null;
const reg = await navigator.serviceWorker.getRegistration();
return reg ? await reg.pushManager.getSubscription() : null;
}
/**
* Auf diesem Gerät anmelden.
*
* Die Reihenfolge ist wichtig: erst den Service Worker, dann die Erlaubnis,
* dann das Abo. Fragt man zuerst nach der Erlaubnis und der Service Worker
* scheitert danach, hat der Nutzer eine Erlaubnis vergeben, die zu nichts
* führt - und der Browser fragt kein zweites Mal.
*/
async function pushAnmelden(name) {
if (!pushMoeglich()) {
throw new Error("Dieser Browser kann keine Benachrichtigungen empfangen.");
}
if (!pushStand.schluessel) {
throw new Error("Auf der NAS fehlt der Schlüssel (push_vapid.json).");
}
const reg = await navigator.serviceWorker.register("sw.js");
await navigator.serviceWorker.ready;
const erlaubnis = await Notification.requestPermission();
if (erlaubnis !== "granted") {
throw new Error(erlaubnis === "denied"
? "Benachrichtigungen sind für diese Seite blockiert – im Browser unter „Website-Einstellungen“ wieder erlauben."
: "Ohne Erlaubnis geht es nicht.");
}
const abo = await reg.pushManager.getSubscription()
|| await reg.pushManager.subscribe({
// Ohne diese Zusage weigern sich die Browser: Jede Nachricht muss den
// Nutzer erreichen, stille Datenübertragung ist nicht erlaubt.
userVisibleOnly: true,
applicationServerKey: pushSchluesselBytes(pushStand.schluessel),
});
const d = await pushAnfrage("anmelden", {
abo: abo.toJSON(),
name: name || pushBrowserName(),
});
pushStand.abos = d.abos;
return d;
}
/** Auf diesem Gerät abmelden - Abo im Browser und Zeile in der Datenbank. */
async function pushAbmelden() {
const abo = await pushAbo();
if (!abo) return;
const endpoint = abo.endpoint;
await abo.unsubscribe();
const d = await pushAnfrage("abmelden", { endpoint: endpoint });
pushStand.abos = d.abos;
}
/** Ein fremdes Gerät aus der Liste werfen (Handy verkauft, Browser weg). */
async function pushEntfernen(id) {
const d = await pushAnfrage("entfernen", { id: id });
pushStand.abos = d.abos;
}
/** Probemeldung über den Runner an alle angemeldeten Geräte. */
async function pushProbe() {
await pushAnfrage("probe", { titel: "Probe", text: "Wenn das ankommt, stimmt der Weg." });
}
async function pushStandLaden() {
pushStand = await pushAnfrage("stand");
return pushStand;
}