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]>
259 lines
12 KiB
JavaScript
259 lines
12 KiB
JavaScript
/* =====================================================================
|
||
pruef-anruf-klingelt.mjs — Klingelt es auch wirklich? (19.09.2026)
|
||
|
||
Filipe: „irgendwas klappt mit dem telefonieren nicht."
|
||
|
||
---------------------------------------------------------------------
|
||
WIE DER FEHLER GEFUNDEN WURDE — UND WARUM DAS DER WICHTIGE TEIL IST
|
||
|
||
Ich habe zuerst die Vermittlung verdächtigt: coturn geprüft, STUN
|
||
gemessen, eine echte Zuteilung von außen gemacht, den TCP-Weg
|
||
nachgerüstet. Alles richtig und alles nötig — aber es war NICHT die
|
||
Ursache.
|
||
|
||
Die Ursache stand im Protokoll des Servers, und sie stand dort die
|
||
ganze Zeit:
|
||
|
||
24× anruf_start, jeder nach 7–39 Sekunden beendet
|
||
im Chat danach jedes Mal: „📞 Verpasster Anruf"
|
||
die Gegenseite schrieb: „Hab keinen Anrufeingang gehabt
|
||
und keine Benachrichtigung"
|
||
|
||
Es ging nie um Ton oder Verbindung. Es hat bei der anderen Person
|
||
schlicht nicht geklingelt.
|
||
|
||
Das ist dieselbe Lehre wie am 06.09. („kommt keine Mail an" — erst
|
||
fragen, OB gesendet wurde) und am 14.09. (zwei Tage Audiowege
|
||
verfolgt, während die CPU bei 98 % stand): **Erst das Protokoll
|
||
lesen, dann suchen.** Eine halbe Minute in der richtigen Tabelle
|
||
hätte Stunden gespart.
|
||
|
||
---------------------------------------------------------------------
|
||
WAS WIRKLICH DEFEKT WAR
|
||
|
||
Die Benachrichtigung wurde zugestellt — `zuletzt_ok` am Gerät stand
|
||
auf genau die Anrufminute. Sie hat nur nicht geklingelt:
|
||
|
||
1. `Urgency: "normal"` stand FEST im Transport, für jede Meldung.
|
||
Auf Android entscheidet dieser Kopf, ob sofort zugestellt wird
|
||
oder bis zum nächsten Aufwachen gewartet. Im Stromsparmodus
|
||
werden daraus Minuten — bei einem Anruf, der 120 Sekunden
|
||
klingelt, ist das ein verpasster Anruf.
|
||
|
||
2. Im Service Worker war `renotify: false` und `tag: d.art`. Eine
|
||
zweite Meldung mit derselben Kennung ersetzt die erste LAUTLOS.
|
||
Wer ein zweites Mal anruft, WEIL nicht abgehoben wurde, löst
|
||
damit gar keinen Ton mehr aus.
|
||
|
||
3. Kein `vibrate`, kein `requireInteraction`. Ein stiller Kasten,
|
||
der von selbst verschwindet — in einer Hosentasche unsichtbar.
|
||
|
||
Für „Aufgabe überfällig" ist all das genau richtig. Für ein
|
||
klingelndes Telefon ist es das Gegenteil. Deshalb unterscheidet das
|
||
Haus jetzt zwischen beidem.
|
||
|
||
AUFRUF: node server/pruef-anruf-klingelt.mjs
|
||
Läuft in unter einer Sekunde, ohne Server, ohne Netz.
|
||
===================================================================== */
|
||
|
||
import { readFileSync } from "node:fs";
|
||
import { join, dirname } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||
const lies = (p) => readFileSync(join(REPO, p), "utf8");
|
||
|
||
let ok = 0, fehl = 0;
|
||
const pruef = (name, bedingung, zusatz = "") => {
|
||
if (bedingung) { ok++; console.log(` OK ${name}`); }
|
||
else { fehl++; console.log(` FEHL ${name}${zusatz ? " — " + zusatz : ""}`); }
|
||
};
|
||
|
||
console.log("\n=== Klingelt ein Anruf auch wirklich? ===\n");
|
||
|
||
/* =====================================================================
|
||
1. DER SERVICE WORKER — die Anzeige auf dem Gerät
|
||
===================================================================== */
|
||
const sw = lies("workspace/sw.js");
|
||
|
||
pruef("der Dienst unterscheidet einen Anruf von anderen Meldungen",
|
||
/istAnruf\s*=\s*d\.art\s*===\s*"anruf"/.test(sw));
|
||
|
||
pruef("ein Anruf bleibt stehen, bis jemand reagiert",
|
||
/requireInteraction:\s*istAnruf/.test(sw),
|
||
"ohne requireInteraction verschwindet er von selbst");
|
||
|
||
pruef("ein zweiter Anruf meldet sich hörbar (renotify)",
|
||
/renotify:\s*istAnruf/.test(sw),
|
||
"mit renotify:false ersetzt der zweite den ersten LAUTLOS — genau der Fehler");
|
||
|
||
pruef("das Handy vibriert beim Anruf",
|
||
/vibrate:\s*istAnruf\s*\?/.test(sw));
|
||
|
||
pruef("jeder Anruf bekommt eine eigene Kennung",
|
||
/tag:\s*istAnruf\s*\?\s*\("anruf-"/.test(sw),
|
||
"eine gemeinsame Kennung lässt den zweiten Anruf den ersten still ersetzen");
|
||
|
||
/* DIE ANDEREN MELDUNGEN BLEIBEN, WIE SIE WAREN. Das ist kein
|
||
Nebensatz: Würden alle Benachrichtigungen stehenbleiben und
|
||
vibrieren, schaltet sie nach drei Tagen jemand ab -- und dann
|
||
klingelt auch der Anruf nie wieder. */
|
||
pruef("und für alles andere bleibt es ruhig",
|
||
/\(d\.art \|\| "workspace"\)/.test(sw) && !/requireInteraction:\s*true\b/.test(sw));
|
||
|
||
/* =====================================================================
|
||
2. DER VERSAND — Dringlichkeit und Haltbarkeit
|
||
===================================================================== */
|
||
const krypto = lies("server/workspace-push-krypto.js");
|
||
const versand = lies("server/workspace-push.js");
|
||
|
||
pruef("der Transport kennt überhaupt eine Dringlichkeit",
|
||
/dringend\s*=\s*false\s*\}\s*=\s*\{\}/.test(krypto),
|
||
"vorher stand Urgency fest auf \"normal\"");
|
||
|
||
pruef("ein dringender Push geht mit Urgency: high hinaus",
|
||
/Urgency:\s*dringend\s*\?\s*"high"\s*:\s*"normal"/.test(krypto));
|
||
|
||
pruef("und verfällt, wenn der Anruf vorbei ist",
|
||
/TTL:\s*String\(dringend\s*\?\s*Math\.min\(ttl,\s*\d+\)/.test(krypto),
|
||
"sonst ploppt ein Anruf von vor einer Stunde nachträglich auf");
|
||
|
||
pruef("der Anruf wird als dringend verschickt",
|
||
/schicken\(a,\s*inhalt,\s*paar,\s*ABSENDER,\s*\{\s*dringend\s*\}\)/.test(versand));
|
||
|
||
/* DIESELBE BEDINGUNG WIE BEI DER RUHEZEIT. „Darf das nachts stören?"
|
||
und „darf das warten?" haben dieselbe Antwort -- zwei getrennte
|
||
Bedingungen wären die, die beim nächsten Nachschärfen auseinander
|
||
laufen. */
|
||
pruef("und zwar an derselben Bedingung wie die Nachtruhe",
|
||
/const dringend = art === "test" \|\| art === "anruf";/.test(versand));
|
||
|
||
/* =====================================================================
|
||
3. DER WEG DORTHIN — wird beim Anruf überhaupt geweckt?
|
||
===================================================================== */
|
||
const anruf = lies("server/workspace-anruf.js");
|
||
const chat = lies("server/workspace-chat.js");
|
||
|
||
pruef("ein Anruf weckt alle außer dem Anrufer",
|
||
/const wecken = leute\.map\(\(t\) => t\.id\)\.filter\(\(id\) => id !== req\.person\.id\)/.test(anruf));
|
||
|
||
pruef("und die Art „anruf\" geht bis zur Benachrichtigung durch",
|
||
/chatEreignis\(raumId,\s*\{[\s\S]*?art:\s*"klingelt"[\s\S]*?\},\s*wecken,\s*"anruf"\)/.test(anruf));
|
||
|
||
/* `.size` UND NICHT `.length` (berichtigt 19.09.2026).
|
||
|
||
Hier stand die alte Form -- und sie war grün, obwohl die Zeile
|
||
nichts tat: `zuschauer` hält Mengen, und eine Menge hat kein
|
||
`length`. Der Ausdruck war immer `undefined`, also immer falsch,
|
||
also wurde nie übersprungen: Wer die Seite offen hatte, bekam
|
||
zusätzlich eine Meldung aufs Handy.
|
||
|
||
Die Prüfung hat den alten Wortlaut bestätigt statt sein Verhalten.
|
||
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. */
|
||
/* ==== 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
|
||
|
||
Zu jeder Prüfung, die „in Ordnung" sagen kann, gehört der Beweis,
|
||
dass sie auch „nicht in Ordnung" sagen kann.
|
||
===================================================================== */
|
||
console.log("\n --- Gegenproben (jede MUSS anschlagen) ---");
|
||
|
||
pruef("Gegenprobe: das alte renotify:false würde auffallen",
|
||
!/renotify:\s*istAnruf/.test(' renotify: false,'));
|
||
|
||
pruef("Gegenprobe: ein festes Urgency würde auffallen",
|
||
!/Urgency:\s*dringend\s*\?/.test('headers: { ...kopf, TTL: String(ttl), Urgency: "normal" }'));
|
||
|
||
pruef("Gegenprobe: eine gemeinsame Kennung würde auffallen",
|
||
!/tag:\s*istAnruf\s*\?\s*\("anruf-"/.test(' tag: d.art || "workspace",'));
|
||
|
||
/* =====================================================================
|
||
4. DER ZWEITE WEG — selbst nachfragen (19.09.2026)
|
||
|
||
Filipe: „wenn vanvan rangeht und redet klingelt es immer noch bei
|
||
mir weiter."
|
||
|
||
Im Protokoll nachgemessen: Sie WAR im Gespräch (24 Sekunden, mit
|
||
eigenem `anruf_ende`). Das Ereignis „dabei" wurde also verschickt
|
||
und kam beim Anrufer nicht an.
|
||
|
||
Geprüft und ausgeschlossen: Raumzugehörigkeit, Ereignis-Verkabelung,
|
||
Tonsteuerung, Service Worker. Es geht auf einem Weg verloren, der
|
||
von hier aus nicht messbar ist.
|
||
|
||
Deshalb wird die Ursache nicht weiter gesucht, sondern die
|
||
ABHÄNGIGKEIT beseitigt: Solange es klingelt, fragt der Anrufer
|
||
selbst nach. Der Ereignisstrom bleibt der schnellere erste Weg --
|
||
dies ist das Netz darunter.
|
||
===================================================================== */
|
||
console.log("\n --- Der zweite Weg ---");
|
||
|
||
const anrufJs = lies("workspace/assets/js/anruf.js");
|
||
|
||
pruef("der Anrufer fragt selbst nach, ob schon jemand dran ist",
|
||
/function nachfrageStarten\(raumId\)/.test(anrufJs),
|
||
"ohne das hängt ein klingelndes Telefon an einem einzigen Kanal");
|
||
|
||
pruef("und zwar ab dem Moment des Wählens",
|
||
/klingelWache\(KLINGELT_MS\);[\s\S]{0,160}nachfrageStarten\(raumId\)/.test(anrufJs));
|
||
|
||
pruef("sie hält an, sobald die Verbindung steht",
|
||
/if \(anruf\.beginn\) return nachfrageStoppen\(\)/.test(anrufJs));
|
||
|
||
/* SIE MUSS AN JEDEM ENDE AUFHOEREN. Eine Schleife, die weiterläuft,
|
||
nachdem das Gespräch vorbei ist, fragt den Server alle zwei Sekunden
|
||
nach einem Anruf, den es nicht mehr gibt -- und das auf jedem Gerät,
|
||
das je telefoniert hat. */
|
||
for (const [wo, muster] of [
|
||
["beim Auflegen", /async function auflegen\(melden\) \{[\s\S]{0,160}nachfrageStoppen\(\)/],
|
||
["wenn die Verbindung steht", /nachfrageStoppen\(\);\s*\/\/ und die Nachfrage/],
|
||
["wenn das Ereignis doch ankommt", /d\.art === 'dabei'[\s\S]{0,260}nachfrageStoppen\(\)/],
|
||
]) {
|
||
pruef(`die Nachfrage hört auf: ${wo}`, muster.test(anrufJs));
|
||
}
|
||
|
||
/* =====================================================================
|
||
5. DAS PROTOKOLL MUSS DIE FRAGE BEANTWORTEN KOENNEN
|
||
|
||
Bis heute standen dort nur Anfang und Ende. Die wichtigste Frage bei
|
||
der Suche -- „ist sie ueberhaupt rangegangen?" -- war nur ueber
|
||
einen Umweg zu beantworten. Das ist eine Schlussfolgerung, keine
|
||
Auskunft.
|
||
===================================================================== */
|
||
pruef("das Rangehen steht im Protokoll",
|
||
/protokolliere\("anruf_dabei"/.test(lies("server/workspace-anruf.js")),
|
||
"sonst lässt sich später nicht sagen, OB jemand rangegangen ist");
|
||
|
||
console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen\n`);
|
||
process.exit(fehl ? 1 : 0);
|