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`. -->
-
-
+
+
-
+