From 4503b1a475acd9212bd1549ae1e8ff2401b9a824 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 30 Sep 2026 23:48:49 +0200 Subject: [PATCH] Benachrichtigungen: nicht "Verbindung offen", sondern "sieht jemand hin" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diene im Support (vor 5 Tagen): „Die Benachrichtigungen werden nicht angezeigt, wenn neue Nachrichten reinkommen. Erst, wenn man die App öffnet." ERST GEMESSEN, WAS NICHT DAS PROBLEM IST. Am echten Bestand nachgesehen: Diene HAT ein angemeldetes Geraet (Android, Chrome, seit dem 29.09.), und alle zehn Geraete im Haus melden `fehler = 0`. An der Zustellung liegt es nicht. DANN NACHGESTELLT (pruef-abzeichen, Abschnitt 8): Verbindung offen -> KEINE Benachrichtigung Verbindung zu -> sie kommt Genau sein Befund. DER GEDANKE WAR RICHTIG, DIE FRAGE FALSCH. Im Quelltext stand: if ((zuschauer.get(personId) || new Set()).size) continue; und daneben die Begruendung -- „wer die Seite offen hat, sieht die Nachricht ohnehin; ihm auch noch eine Meldung aufs Handy zu schicken ist der schnellste Weg, dass er Benachrichtigungen abschaltet." Das stimmt. Nur beantwortet `zuschauer` eine ANDERE Frage: ob eine VERBINDUNG offen ist. Ein Handy mit der App im Hintergrund haelt sie weiter -- und der Server hielt Diene fuer anwesend, waehrend sein Bildschirm schwarz war. DIE SEITE WEISS ES, DER SERVER NICHT. `document.visibilityState` ist die einzige Stelle, die den Unterschied kennt. Also sagt sie es -- ueber einen winzigen Weg (`/api/chat/sicht`), beim Aufbau, bei jedem Wechsel und mit `keepalive` beim Weggehen. MIT VERFALL, und das ist der wichtige Teil: Ein Geraet, das abstuerzt, im Funkloch steht oder eingefroren wird, sagt gar nichts mehr. Ohne Verfall bliebe es fuer immer „sichtbar" und fuer immer still. Wer nicht widerspricht, gilt nach zweieinhalb Minuten als weg -- eine Meldung zu viel ist laestig, eine zu wenig ist genau der Fehler, den Diene gemeldet hat. Dazu alle Minute ein Lebenszeichen, solange die App vorn liegt. EINE STELLE FUER DIE FRAGE. Sie wurde an zwei Orten gestellt: `siehtZu()` und eine Abschrift mitten in `chatEreignis`. Die Abschrift war die kaputte. Jetzt fragen beide dieselbe Funktion. GEPRUEFT -- vier Lagen, und die zweite ist die wichtigere Gegenprobe: App liegt hinten -> Meldung kommt (war: nichts) sieht wirklich hin -> KEINE Meldung (Absicht bleibt) App weggelegt -> Meldung kommt wieder gar keine Verbindung -> Meldung kommt Ohne die zweite Zeile hiesse die Reparatur nur „jetzt kommt immer eine", und das waere der schnellste Weg, dass jemand Benachrichtigungen abschaltet. UND EINE PRUEFUNG HAT DEN FEHLER MITGETRAGEN. pruef-anruf-klingelt hielt den WORTLAUT der kaputten Zeile fest -- genau das, wovor ihr eigener Kommentar drei Zeilen darueber warnt („Die Pruefung hat den alten Wortlaut bestaetigt statt sein Verhalten"). Sie prueft jetzt beides: dass gefragt wird, und dass die Frage die richtige ist. pruef-abzeichen 26/0, pruef-chat, pruef-anruf 132/0, pruef-chat-kanaele 81/0, pruef-anruf-klingelt 25/0, pruef-push-ziel 38/0, pruef-arten 28/0, pruef-reaktion 421/0, pruef-struktur, pruef-zwischenspeicher 34/0. Co-Authored-By: Claude Opus 5 --- server/pruef-abzeichen.mjs | 126 ++++++++++++++++++++++++++++++++ server/pruef-anruf-klingelt.mjs | 28 ++++++- server/workspace-chat.js | 87 +++++++++++++++++++++- workspace/anruf-probe.html | 10 +-- workspace/app.webmanifest | 8 +- workspace/assets/js/kopf.js | 51 ++++++++++++- workspace/aufgaben.html | 38 +++++----- workspace/automation.html | 32 ++++---- workspace/befinden.html | 34 ++++----- workspace/bereich.html | 44 +++++------ workspace/bewerben.html | 38 +++++----- workspace/bewerbungen.html | 36 ++++----- workspace/buehne.html | 6 +- workspace/calls.html | 34 ++++----- workspace/chat.html | 40 +++++----- workspace/content.html | 34 ++++----- workspace/crew-index.html | 12 +-- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 ++++----- workspace/entwicklung.html | 36 ++++----- workspace/hilfe.html | 30 ++++---- workspace/index.html | 10 +-- workspace/kalender.html | 34 ++++----- workspace/leistung.html | 32 ++++---- workspace/material.html | 32 ++++---- workspace/notizen.html | 28 +++---- workspace/personen.html | 32 ++++---- workspace/profil.html | 36 ++++----- workspace/reaktion.html | 38 +++++----- workspace/rechte.html | 34 ++++----- workspace/report.html | 36 ++++----- workspace/scouting.html | 32 ++++---- workspace/start.html | 32 ++++---- workspace/startcheck.html | 34 ++++----- workspace/steckbrief.html | 36 ++++----- workspace/support.html | 28 +++---- workspace/tafel.html | 6 +- workspace/talente.html | 36 ++++----- workspace/team.html | 32 ++++---- workspace/teamlage.html | 34 ++++----- workspace/teilen.html | 30 ++++---- workspace/treff-moderation.html | 32 ++++---- workspace/treff-regeln.html | 32 ++++---- workspace/uebersicht.html | 32 ++++---- workspace/unsere-seiten.html | 30 ++++---- workspace/unterstuetzen.html | 32 ++++---- workspace/werdegang.html | 32 ++++---- workspace/willkommen.html | 30 ++++---- workspace/wissen.html | 34 ++++----- 49 files changed, 956 insertions(+), 676 deletions(-) diff --git a/server/pruef-abzeichen.mjs b/server/pruef-abzeichen.mjs index cdb939da..214112f9 100644 --- a/server/pruef-abzeichen.mjs +++ b/server/pruef-abzeichen.mjs @@ -532,6 +532,132 @@ console.log("=== 7. Jede Adresse zaehlt nur ihr eigenes Haus ==="); } } +/* ======================================================================= + 8. DIENES MELDUNG: DIE APP IM HINTERGRUND (30.09.2026) + + „Die Benachrichtigungen werden nicht angezeigt, wenn neue + Nachrichten reinkommen. Erst, wenn man die App öffnet." + + Am echten Bestand nachgesehen: Diene HAT ein angemeldetes Geraet + (Android, Chrome, seit dem 29.09.), und alle zehn Geraete im Haus + melden `fehler = 0`. An der Zustellung liegt es nicht. + + DER VERDACHT steht in workspace-chat.js: + + if ((zuschauer.get(personId) || new Set()).size) continue; + + Wer eine offene Verbindung hat, bekommt KEINE Benachrichtigung. + Der Gedanke daneben ist richtig -- „wer die Seite offen hat, sieht + die Nachricht ohnehin, und ihm auch noch eine Meldung aufs Handy zu + schicken ist der schnellste Weg, dass er Benachrichtigungen + abschaltet." + + NUR HEISST EINE OFFENE VERBINDUNG NICHT, DASS JEMAND HINSIEHT. + Ein Handy mit der App im Hintergrund haelt sie weiter; der Server + haelt ihn dann fuer anwesend und schweigt. + + HERGESTELLT UND NICHT ANGENOMMEN: Der Strom wird geoeffnet und + offen gelassen -- genau das, was ein Telefon im Hintergrund tut. + ======================================================================= */ +console.log(""); +console.log("=== 8. Wenn die App im Hintergrund liegt ==="); +{ + const { request: httpRoh } = await import("node:http"); + /* EINE ROHE VERBINDUNG, DIE OFFEN BLEIBT. `fetch` wartet auf das + Ende; ein Ereignisstrom endet nie. Genau daran ist im September + schon einmal eine Pruefung haengengeblieben. */ + const stromAuf = (keks) => new Promise((fertig) => { + const a = httpRoh({ + host: "127.0.0.1", port: PORT, path: "/workspace/api/chat/strom", + method: "GET", + headers: { Cookie: keks, Host: "crew.dogfather-universe.com", Accept: "text/event-stream" }, + }, (antwort) => { + antwort.on("data", () => { }); /* mitlesen, nichts tun */ + fertig({ anfrage: a, code: antwort.statusCode }); + }); + a.on("error", () => fertig({ anfrage: a, code: 0 })); + a.end(); + }); + + const strom = await stromAuf(ayla.keks); + ok(strom.code === 200, `Aylas Handy haelt die Verbindung (HTTP ${strom.code})`); + /* Kurz warten, damit der Server sie wirklich eingetragen hat. */ + await new Promise((r) => setTimeout(r, 400)); + + const vorher = pakete.length; + const g = await rufe(dogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`, + { text: "Kommt das an, wenn die App hinten liegt?" }, "POST"); + ok(g.ok, `DogFather schreibt ihr (HTTP ${g.status})`); + /* Auf das Ergebnis warten, nicht auf die Uhr -- aber hier ist das + AUSBLEIBEN das Ergebnis, also braucht es eine Frist. Drei + Sekunden sind grosszuegig: Der Weg zum Push-Dienst dauert + oertlich Millisekunden. */ + const bis = Date.now() + 3000; + while (pakete.length === vorher && Date.now() < bis) { + await new Promise((r) => setTimeout(r, 60)); + } + const kam = pakete.length > vorher; + console.log(` Benachrichtigung verschickt: ${kam ? "ja" : "NEIN"}`); + ok(kam, + kam + ? "sie bekommt die Meldung, obwohl eine Verbindung offen ist " + + "(die App liegt hinten)" + /* BACKTICKS, WEIL EINE DEUTSCHE ANFUEHRUNG DARIN STEHT. In + einem "..."-String beendet das ASCII-Zeichen nach dem Wort + den String -- mir heute zum fuenften Mal passiert. Wo eine + deutsche Anfuehrung hinein soll, gehoeren Backticks darum. */ + : `KEINE MELDUNG — der Server haelt eine offene Verbindung fuer ` + + `„sie sieht ja hin". Genau Dienes Befund.`); + + /* ==== DIE GEGENPROBE, OHNE DIE DIE ZEILE OBEN NICHTS BEWEIST ==== + + Wer WIRKLICH hinsieht, soll KEINE Meldung bekommen -- das war + der richtige Gedanke hinter der alten Zeile, und er bleibt. + Ohne diesen Teil hiesse die Reparatur nur „jetzt kommt immer + eine", und das waere der schnellste Weg, dass jemand + Benachrichtigungen abschaltet. + + GEMELDET WIRD, WAS DIE SEITE SAGEN WUERDE: `sichtbar: true` -- + genau das schickt die Kopfleiste, wenn die App vorn liegt. */ + const sicht = await rufe(ayla, "/workspace/api/chat/sicht", + { sichtbar: true }, "POST"); + ok(sicht.ok, `Ayla meldet: ich sehe hin (HTTP ${sicht.status})`); + const vorherS = pakete.length; + await rufe(dogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`, + { text: "Und wenn sie wirklich hinsieht?" }, "POST"); + await new Promise((r) => setTimeout(r, 2500)); + ok(pakete.length === vorherS, + `wer hinsieht, bekommt KEINE Meldung aufs Handy ` + + `(${pakete.length - vorherS} verschickt)`); + + /* Und wenn sie die App weglegt, kommt sie wieder. */ + await rufe(ayla, "/workspace/api/chat/sicht", { sichtbar: false }, "POST"); + const vorherW = pakete.length; + await rufe(dogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`, + { text: "App weggelegt." }, "POST"); + const bisW = Date.now() + 5000; + while (pakete.length === vorherW && Date.now() < bisW) { + await new Promise((r) => setTimeout(r, 60)); + } + ok(pakete.length > vorherW, + `legt sie die App weg, kommt die Meldung wieder ` + + `(${pakete.length - vorherW})`); + + /* GEGENPROBE ZUM SCHLUSS: Ohne offene Verbindung erst recht. */ + strom.anfrage.destroy(); + await new Promise((r) => setTimeout(r, 500)); + const vorher2 = pakete.length; + await rufe(dogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`, + { text: "Und jetzt, mit geschlossener App?" }, "POST"); + const bis2 = Date.now() + 5000; + while (pakete.length === vorher2 && Date.now() < bis2) { + await new Promise((r) => setTimeout(r, 60)); + } + ok(pakete.length > vorher2, + `Gegenprobe: ohne offene Verbindung kommt sie sehr wohl ` + + `(${pakete.length - vorher2})`); +} + /* ------------------------------------------------------------------- */ await schliessen(); d.close(); diff --git a/server/pruef-anruf-klingelt.mjs b/server/pruef-anruf-klingelt.mjs index a6d5c0d9..8e129e6e 100644 --- a/server/pruef-anruf-klingelt.mjs +++ b/server/pruef-anruf-klingelt.mjs @@ -152,10 +152,34 @@ pruef("und die Art „anruf\" geht bis zur Benachrichtigung durch", Genau davor warnt die Hausregel vom 01.09.: Ein grüner Haken sagt nur, dass die Bedingung erfüllt war -- nicht, dass sie das Richtige geprüft hat. */ -pruef("wer die Seite offen hat, bekommt KEINE zusätzliche Meldung", - /\(zuschauer\.get\(personId\) \|\| new Set\(\)\)\.size\) continue;/.test(chat), +/* ==== UND SEIT DEM 30.09.2026 IST ES EINE ANDERE FRAGE ============ + + Diene im Support: „Die Benachrichtigungen werden nicht angezeigt, + wenn neue Nachrichten reinkommen. Erst, wenn man die App öffnet." + + Nachgestellt (pruef-abzeichen, Abschnitt 8): Der Server hat die + Meldung unterdrückt, solange eine VERBINDUNG offen war. Ein Handy + mit der App im Hintergrund hält sie weiter — also galt Diene als + anwesend, während sein Bildschirm schwarz war. + + Die Absicht der Zeile darüber bleibt richtig und wird weiter + geprüft: Wer WIRKLICH hinsieht, bekommt keine zweite Meldung. + Gefragt wird nur nicht mehr nach der Verbindung, sondern nach + `siehtZu()` — und das verlangt zusätzlich eine frische Sichtmeldung + der Seite. + + UND DIESE PRÜFUNG HAT DEN FEHLER MITGETRAGEN. Sie hielt den + WORTLAUT fest, nicht das Verhalten — genau das, wovor ihr eigener + Kommentar drei Zeilen weiter oben warnt. Deshalb steht hier jetzt + BEIDES: dass gefragt wird, und dass die Frage die richtige ist. */ +pruef("wer wirklich hinsieht, bekommt KEINE zusätzliche Meldung", + /if \(siehtZu\(personId\)\) continue;/.test(chat), "sonst meldet es sich doppelt — der schnellste Weg, dass jemand alles abschaltet"); +pruef("und „hinsehen\" heißt mehr als „eine Verbindung offen haben\"", + /function siehtZu[\s\S]{0,400}?sichtbarSeit\.get\(id\)[\s\S]{0,200}?SICHT_FRIST/.test(chat), + "sonst wäre es wieder die alte Frage, nur mit neuem Namen"); + /* ===================================================================== DIE GEGENPROBEN diff --git a/server/workspace-chat.js b/server/workspace-chat.js index 1e40ec30..67383958 100644 --- a/server/workspace-chat.js +++ b/server/workspace-chat.js @@ -3435,6 +3435,32 @@ chatRouter.delete("/workspace/api/chat/nachrichten/:id", gleicheHerkunft, (req, * Eine Person kann an zwei Geräten sitzen. */ const zuschauer = new Map(); +/* ==== SIEHT JEMAND HIN? (30.09.2026) ================================ + + Diene im Support: „Die Benachrichtigungen werden nicht angezeigt, + wenn neue Nachrichten reinkommen. Erst, wenn man die App öffnet." + + Nachgestellt (pruef-abzeichen, Abschnitt 8): Mit offener Verbindung + kam KEINE Meldung, ohne sie sehr wohl. + + Hier stand `zuschauer.size > 0`. Der Gedanke dahinter ist richtig + -- wer die Seite offen hat, sieht die Nachricht ohnehin, und eine + Meldung obendrauf ist der schnellste Weg, dass er sie abschaltet. + Nur beantwortet `zuschauer` eine ANDERE Frage: ob eine VERBINDUNG + offen ist. Ein Handy mit der App im Hintergrund haelt sie weiter. + + Die Seite weiss es (`document.visibilityState`), der Server nicht. + Also sagt sie es ihm -- ueber `/workspace/api/chat/sicht`. + + MIT VERFALL, und das ist der wichtige Teil: Ein Geraet, das + abstuerzt, im Funkloch steht oder eingefroren wird, sagt nichts + mehr. Ohne Verfall bliebe es fuer immer „sichtbar" und fuer immer + still. Wer nicht widerspricht, gilt nach zweieinhalb Minuten als + weg -- eine Meldung zu viel ist laestig, eine zu wenig ist genau + der Fehler, den Diene gemeldet hat. */ +const sichtbarSeit = new Map(); +const SICHT_FRIST = 150_000; + /** * @param erwaehnte Wer in DIESER Nachricht mit "@" angesprochen wurde. * Diese Menge bekommt eine ANDERE Meldung, nicht eine zweite @@ -3472,7 +3498,18 @@ function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu", Absicht stand da, die Zeile tat das Gegenteil -- und `siehtZu()` weiter unten macht es mit `.size` schon richtig. Gefunden beim Nachverfolgen eines ganz anderen Fehlers. */ - if ((zuschauer.get(personId) || new Set()).size) continue; + /* ==== GEFRAGT WIRD, OB JEMAND HINSIEHT (berichtigt 30.09.2026) + + Hier stand `zuschauer.get(personId).size` -- also „ist eine + Verbindung offen". Ein Handy mit der App im Hintergrund haelt + sie weiter, und damit blieb Diene ohne Meldung, solange die App + nur im Hintergrund lag. Gemessen in pruef-abzeichen, + Abschnitt 8. + + Jetzt entscheidet `siehtZu()` -- dieselbe Funktion, die auch + andere Module fragen. Eine Abschrift daneben war genau der + Fehler. */ + if (siehtZu(personId)) continue; /* DIE ART GEHT MIT (18.09.2026). Sie stand als Parameter oben und endete im Ereignisstrom -- die Benachrichtigung bekam sie nie zu sehen und behandelte deshalb JEDES Ereignis als Chatnachricht. @@ -3660,6 +3697,29 @@ async function meldeNeueNachricht(personId, raumId, nachricht, art = "neu") { } } +/* ==== DIE SEITE SAGT, OB SIE VORN LIEGT (30.09.2026) =============== + + Ein winziger Weg, und er traegt die ganze Reparatur: Ohne ihn + koennte der Server „Verbindung offen" nicht von „sieht hin" + unterscheiden. + + OHNE RUMPF IST ES EIN „NEIN". Wer beim Weggehen noch schnell etwas + schickt (`keepalive`), soll nicht darauf angewiesen sein, dass ein + JSON-Koerper heil ankommt -- und „weg" ist die sichere Vorgabe: Sie + fuehrt zu einer Meldung zu viel, nicht zu einer zu wenig. */ +chatRouter.post("/workspace/api/chat/sicht", gleicheHerkunft, + express.json({ limit: "1kb" }), (req, res) => { + try { + const id = req.person.id; + if (req.body?.sichtbar === true) sichtbarSeit.set(id, Date.now()); + else sichtbarSeit.delete(id); + res.json({ ok: true }); + } catch (fehler) { + console.error("[chat] Sicht:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + chatRouter.get("/workspace/api/chat/strom", (req, res) => { res.set({ "Content-Type": "text/event-stream", @@ -3691,7 +3751,14 @@ chatRouter.get("/workspace/api/chat/strom", (req, res) => { const menge = zuschauer.get(personId); if (menge) { menge.delete(res); - if (!menge.size) zuschauer.delete(personId); + if (!menge.size) { + zuschauer.delete(personId); + /* UND DIE SICHTMELDUNG GEHT MIT. Sonst bliebe sie stehen, + bis sie verfaellt -- und beim naechsten Verbindungsaufbau + gaelte die Person sofort als „sieht hin", ohne dass sie es + gesagt haette. */ + sichtbarSeit.delete(personId); + } } }; req.on("close", aufraeumen); @@ -3847,8 +3914,22 @@ export function teilnehmerFuerAnruf(raumId) { /** Der Ereigniskanal -- der Hinweg fuer Klingeln und Verbindungsdaten. */ export { chatEreignis }; +/** Sieht diese Person gerade hin? + * + * ZWEI BEDINGUNGEN, ZWEI GRUENDE: + * eine offene Verbindung -- die Seite ist ueberhaupt geladen + * eine frische Sichtmeldung -- und sie liegt vorn, nicht hinten + * + * DIE EINE STELLE FUER DIESE FRAGE (30.09.2026). Sie wurde an zwei + * Orten gestellt: hier und als eigene Zeile in `chatEreignis`. Die + * dortige Abschrift war es, die Dienes Meldung verursacht hat -- + * sie fragte nur nach der Verbindung. Zwei Rechnungen fuer dieselbe + * Frage laufen auseinander; jetzt gibt es nur noch eine. */ export function siehtZu(personId) { - return (zuschauer.get(Number(personId)) || []).size > 0; + const id = Number(personId); + if (!(zuschauer.get(id) || []).size) return false; + const wann = sichtbarSeit.get(id); + return !!wann && (Date.now() - wann) < SICHT_FRIST; } /* ===================================================================== diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 39c44b6b..0685f0ee 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +