Files
dogfather-universe/server/pruef-anruf-klingelt.mjs
T
DogFatherGitandClaude Opus 5 26937dda16 Ein klingelndes Telefon haengt nicht mehr an einem einzigen Kanal
Filipe: "wenn vanvan rangeht und redet klingelt es immer noch bei mir
weiter, der anruf verbindet nicht richtig."

=== WAS DAS PROTOKOLL SAGT ===

    17:04:52  anruf_start  Person 1 (Filipe)
    17:05:25  anruf_ende   Person 4 (VanVan)  24 s
    17:05:31  anruf_ende   Person 1 (Filipe)  39 s

Sie WAR im Gespraech -- der Server hat sie 24 Sekunden als
Teilnehmerin gefuehrt. Das Ereignis "dabei" ist also verschickt
worden. Bei Filipe kam es nicht an: `tonAus()` ist das Erste im
`dabei`-Zweig, noch vor jeder Pruefung, und das Tuten lief weiter.

=== GEPRUEFT UND AUSGESCHLOSSEN ===

  Raumzugehoerigkeit   beide in Raum 1, bei keinem `raus_am` gesetzt
  Verkabelung          Server sendet mit `art: "anruf"`, chat.js reicht
                       an window.anrufEreignis weiter, anruf.js nimmt
                       entgegen -- alle drei Stellen stimmen
  Tonsteuerung         ein einziger Taktgeber, `tonAus` raeumt ihn;
                       kein zweiter Weg, der ihn neu startet
  Ereignisstrom        Keep-alive vorhanden, Kopfzeilen richtig
                       (no-transform, X-Accel-Buffering: no)
  Service Worker       hat gar keinen fetch-Handler, kann also kein
                       altes Skript ausliefern
  teilnehmerVon vs.
  teilnehmerFuerAnruf  reicht nur durch, dieselbe Abfrage

Es geht unterwegs verloren, auf einem Weg, der von hier aus nicht
messbar ist: Ereignisstrom ueber Cloudflare, ein schlafender Reiter,
ein Neustart im falschen Moment.

=== ALSO NICHT WEITERSUCHEN, SONDERN DIE ABHAENGIGKEIT BESEITIGEN ===

Ein klingelndes Telefon darf nicht an einem einzigen, zerbrechlichen
Kanal haengen. Solange es klingelt, fragt der Anrufer jetzt SELBST
nach: "ist schon jemand dran?" -- alle zwei Sekunden an
`/workspace/api/anruf/:raum`, das es laengst gibt.

Der Ereignisstrom bleibt der erste Weg, er ist schneller. Das hier ist
das Netz darunter. Kommt das Ereignis an, hat die Nachfrage nichts
mehr zu tun und haelt von selbst an (sie prueft `anruf.beginn` und die
bekannten Teilnehmer).

Sie hoert an JEDEM Ende auf: beim Auflegen, wenn die Verbindung steht,
wenn das Ereignis doch ankommt, wenn der Anruf vorbei ist. Eine
Schleife, die weiterlaeuft, fragt sonst auf jedem Geraet, das je
telefoniert hat, alle zwei Sekunden nach einem Anruf, den es nicht
mehr gibt.

Alle zwei Sekunden und nicht jede halbe: Es klingelt hoechstens zwei
Minuten, das sind sechzig Abrufe.

=== ZWEI DINGE, DIE DIESE SUCHE ERST SO MUEHSAM GEMACHT HABEN ===

DAS PROTOKOLL KANNTE ANFANG UND ENDE, ABER NICHT DEN MOMENT DAZWISCHEN.
Die wichtigste Frage -- "ist sie ueberhaupt rangegangen?" -- war nur
ueber einen Umweg zu beantworten (ein `anruf_ende` mit ihrer Nummer).
Das ist eine Schlussfolgerung, keine Auskunft. `anruf_dabei` steht
jetzt drin, mit der Zahl der Beteiligten.

UND EINE PRUEFUNG WAR GRUEN, OHNE ETWAS ZU PRUEFEN. In chatEreignis
stand `(zuschauer.get(personId) || []).length` -- `zuschauer` haelt
aber Mengen, und eine Menge hat kein `length`. Der Ausdruck war IMMER
undefined, also immer falsch, also wurde nie uebersprungen: Wer die
Seite offen hatte, bekam zusaetzlich zur Nachricht auf dem Bildschirm
noch eine Meldung aufs Handy.

Der Kommentar drei Zeilen darueber warnt woertlich davor ("der
schnellste Weg, dass er Benachrichtigungen abschaltet"), und
`siehtZu()` weiter unten macht es mit `.size` richtig. Die Absicht
stand da, die Zeile tat das Gegenteil.

Meine eigene Pruefung hat das mitgetragen: Sie bestaetigte den alten
WORTLAUT statt sein VERHALTEN und war deshalb gruen. Genau die
Hausregel vom 01.09. -- ein gruener Haken sagt nur, dass die Bedingung
erfuellt war, nicht dass sie das Richtige geprueft hat. Jetzt prueft
sie auf `.size`.

GEMESSEN: pruef-anruf-klingelt 24/0 (vorher 17), pruef-anruf 114/0,
pruef-turn-wege 15/0, pruef-meldungen 8/0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 19:15:51 +02:00

235 lines
10 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.
/* =====================================================================
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. */
pruef("wer die Seite offen hat, bekommt KEINE zusätzliche Meldung",
/\(zuschauer\.get\(personId\) \|\| new Set\(\)\)\.size\) continue;/.test(chat),
"sonst meldet es sich doppelt — der schnellste Weg, dass jemand alles abschaltet");
/* =====================================================================
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);