diff --git a/server/pruef-anruf-klingelt.mjs b/server/pruef-anruf-klingelt.mjs
new file mode 100644
index 00000000..cc2d5b2b
--- /dev/null
+++ b/server/pruef-anruf-klingelt.mjs
@@ -0,0 +1,165 @@
+/* =====================================================================
+ 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));
+
+pruef("wer die Seite offen hat, bekommt KEINE zusätzliche Meldung",
+ /if \(\(zuschauer\.get\(personId\) \|\| \[\]\)\.length\) 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",'));
+
+console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen\n`);
+process.exit(fehl ? 1 : 0);
diff --git a/server/workspace-push-krypto.js b/server/workspace-push-krypto.js
index a2b69525..dc99b4fe 100644
--- a/server/workspace-push-krypto.js
+++ b/server/workspace-push-krypto.js
@@ -208,14 +208,33 @@ export function vapidKopf(endpunkt, paar, absender, stunden = 12) {
* Berechtigung entzogen) und gehoert geloescht. Der Push-Dienst sagt
* das mit 404 oder 410.
*/
-export async function schicken(anmeldung, text, paar, absender, { ttl = 3600 } = {}) {
+/* DRINGLICHKEIT GEHT MIT (19.09.2026).
+ *
+ * Hier stand fest `Urgency: "normal"` -- fuer JEDE Benachrichtigung,
+ * auch fuer einen Anruf.
+ *
+ * Auf Android entscheidet dieser Kopf, ob der Push-Dienst sofort
+ * zustellt oder bis zum naechsten Aufwachen des Geraets wartet. Im
+ * Stromsparmodus koennen daraus Minuten werden. Ein Anruf klingelt
+ * 120 Sekunden -- eine Benachrichtigung, die danach ankommt, ist ein
+ * verpasster Anruf mit Zeitstempel.
+ *
+ * Und die Haltbarkeit gehoert dazu: Ein Anruf von vor einer Stunde
+ * soll NICHT nachtraeglich aufploppen. Deshalb bekommt er eine kurze
+ * TTL -- ist er vorbei, verfaellt auch die Meldung. */
+export async function schicken(anmeldung, text, paar, absender,
+ { ttl = 3600, dringend = false } = {}) {
try {
const koerper = verschluesseln(text, anmeldung.p256dh, anmeldung.auth);
const kopf = vapidKopf(anmeldung.endpunkt, paar, absender);
const a = await fetch(anmeldung.endpunkt, {
method: "POST",
- headers: { ...kopf, TTL: String(ttl), Urgency: "normal" },
+ headers: {
+ ...kopf,
+ TTL: String(dringend ? Math.min(ttl, 150) : ttl),
+ Urgency: dringend ? "high" : "normal",
+ },
body: koerper,
/* Ein haengender Push-Dienst darf den Lauf nicht aufhalten. */
signal: AbortSignal.timeout(15_000),
diff --git a/server/workspace-push.js b/server/workspace-push.js
index f93a46fd..fc36cd79 100644
--- a/server/workspace-push.js
+++ b/server/workspace-push.js
@@ -380,7 +380,14 @@ export async function benachrichtige(personId, art, { titel, text, ziel, merkmal
let raus = 0;
for (const a of anmeldungen) {
- const e = await schicken(a, inhalt, paar, ABSENDER);
+ /* EIN ANRUF IST DRINGEND, ALLES ANDERE NICHT (19.09.2026).
+
+ `dringend` ist hier dasselbe `art === "anruf"`, das zwanzig
+ Zeilen weiter oben schon ueber die Ruhezeit entscheidet -- die
+ Frage „darf das nachts stoeren?" und „darf das warten?" haben
+ dieselbe Antwort. Eine zweite Bedingung waere die, die beim
+ naechsten Nachschaerfen auseinanderlaeuft. */
+ const e = await schicken(a, inhalt, paar, ABSENDER, { dringend });
if (e.ok) {
raus++;
db().prepare("UPDATE push_anmeldungen SET zuletzt_ok = ?, fehler = 0 WHERE endpunkt = ?")
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index e27aa28d..062f023d 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+