Benachrichtigungen: nicht "Verbindung offen", sondern "sieht jemand hin"

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 <[email protected]>
This commit is contained in:
2026-09-30 23:48:49 +02:00
co-authored by Claude Opus 5
parent d4a3e54a72
commit 4503b1a475
49 changed files with 956 additions and 676 deletions
+84 -3
View File
@@ -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;
}
/* =====================================================================