Anruf: zwei Minuten Zeit -- und ein verpasster Anruf bleibt sichtbar

Filipe: „das mit dem anruf klappt nicht … es geeeeeht einfach nicht."

NACHGEMESSEN WAR SERVERSEITIG ALLES IN ORDNUNG: Der Anruf kam an
(Protokoll), der Raum stimmte (Dogfather + VanVan), sie war angemeldet
(Sitzung bis morgen frueh), sie hat ein Geraet fuer
Benachrichtigungen, und der neue Code lief. Trotzdem ging es nicht --
und zwar aus zwei Gruenden, die beide nichts mit Technik zu tun haben,
sondern mit Zeit und Sichtbarkeit.

1. 45 SEKUNDEN SIND KEIN KLINGELN. Bei einem Telefon hebt man ab.
   Hier muss in dieselben Sekunden: Benachrichtigung zustellen,
   bemerken, Handy entsperren, antippen, App laden, Anmeldung pruefen.
   Das reicht, wenn man das Geraet in der Hand haelt -- und sonst nie.
   Jetzt zwei Minuten: lang genug, um aus der Tasche zu kommen, kurz
   genug, dass niemand vor einem Anruf sitzt, den es nicht mehr gibt.

2. UND DANACH WAR ES, ALS WAERE NIE ETWAS GEWESEN. Ein verpasster
   Anruf verschwand lautlos: keine Zeile, kein Hinweis, nicht einmal,
   DASS jemand angerufen hat. Genau das fuehlt sich an wie „geht
   nicht", auch wenn alles funktioniert hat. Jedes Telefon der Welt
   zeigt einen verpassten Anruf; jetzt steht er als Zeile im Gespraech,
   beim Anrufer, mit Uhrzeit.

Geprueft: pruef-anruf 95 -> 100. Der neue Abschnitt misst den echten
Ablauf (anrufen, niemand geht ran, Zeile erscheint) und dass sie NUR
EINMAL erscheint -- `aufraeumen()` laeuft bei jeder Anfrage mit.

Dafuer ist die Klingeldauer ueber die Umgebung einstellbar: Eine
Pruefung, die zwei Minuten wartet, wird abgeschaltet, und dann waere
der verpasste Anruf wieder ungeprueft. Im Betrieb steht die Variable
nirgends.

Und noch ein eigener Messfehler: Die Pruefung las zuerst
/api/chat/raum/:id -- die Route heisst /api/chat/raeume/:id/nachrichten.
Sie meldete „0 Nachrichten, vorher wie nachher" und sah damit aus wie
ein Befund ueber den Code.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-18 23:30:52 +02:00
co-authored by Claude Opus 5
parent 4ea7b37333
commit e43fe97550
35 changed files with 536 additions and 404 deletions
+59
View File
@@ -28,6 +28,10 @@ process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
/* Zwei Sekunden statt zwei Minuten -- sonst muesste Abschnitt 5e so
lange warten, und eine Pruefung, die zwei Minuten braucht, wird
abgeschaltet. Im Betrieb steht die Variable nirgends. */
process.env.ANRUF_KLINGELT_SEKUNDEN = "2";
/* Der Vermittlungsserver holt sein Geheimnis aus einer Datei. Hier muss
der Pfad UMGEBOGEN werden, bevor index.js geladen wird -- sonst
zeigte er auf die echte Datei unter /etc, und die Pruefung wuerde
@@ -267,6 +271,61 @@ melde("=== 5. Auflegen ===");
jetzt in `pushTextFuer()` -- eine Zeile weniger Verschachtelung und
eine Frage, die sich beantworten laesst.
======================================================================= */
/* =======================================================================
5e. EIN VERPASSTER ANRUF HINTERLAESST EINE SPUR
Bisher verschwand er lautlos. Wer zu spaet kam, oeffnete den Chat
und sah -- nichts. Keine Zeile, kein Hinweis, nicht einmal, DASS
jemand angerufen hat.
Genau das fuehlt sich an wie „es geht einfach nicht", auch wenn
technisch alles funktioniert hat: Der Anruf kam an, es klingelte,
niemand war schnell genug, und danach war es, als waere nie etwas
gewesen. Jedes Telefon der Welt zeigt einen verpassten Anruf.
======================================================================= */
melde("");
melde("=== 5e. Ein verpasster Anruf ===");
{
const raum = json(await roh("/workspace/api/chat/raeume", HAUS,
{ cookie: kDogi })).raeume?.[0];
if (!raum) {
unklar("kein Gespraech vorhanden -- nicht nachgesehen");
} else {
const vorher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
{ cookie: kDogi })).nachrichten?.length ?? 0;
const start = await roh(`/workspace/api/anruf/${raum.id}/start`, HAUS,
{ cookie: kDogi }, { video: false });
ok(start.code === 200 || start.code === 201, `Anruf gestartet (${start.code})`);
/* Niemand geht ran. Nach ANRUF_KLINGELT_SEKUNDEN (hier 2) gilt er
als verpasst -- `aufraeumen()` laeuft bei der naechsten Anfrage
mit. */
await new Promise((r) => setTimeout(r, 2600));
await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand });
const nachher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
{ cookie: kDogi })).nachrichten || [];
ok(nachher.length === vorher + 1,
`es steht genau EINE neue Zeile im Gespraech (${vorher} -> ${nachher.length})`);
const letzte = nachher[nachher.length - 1];
ok(/[Vv]erpasst/.test(letzte?.text || ""),
`und sie sagt, was los war („${letzte?.text}")`);
ok(letzte?.von_id !== undefined,
"sie steht beim Anrufer, nicht als Systemtext ohne Absender");
/* UND NICHT ZWEIMAL. `aufraeumen()` laeuft bei JEDER Anfrage --
ohne das Loeschen aus `laufend` staende hier bei jedem Aufruf
eine weitere Zeile. */
await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand });
await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand });
const nochmal = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
{ cookie: kDogi })).nachrichten || [];
ok(nochmal.length === nachher.length,
`und bleibt bei einer, auch nach weiteren Abfragen (${nochmal.length})`);
}
}
melde("");
melde("=== 5d. Was in der Benachrichtigung steht ===");
{
+75 -2
View File
@@ -91,8 +91,33 @@ export const GRUPPE_MAX = 4;
*
* Ohne diese Grenze bliebe ein Anruf, bei dem der Anrufer die Seite
* schließt, für immer offen stehen -- und der Angerufene sähe eine
* Einladung zu einem Gespräch, an dem niemand mehr teilnimmt. */
const KLINGELT_SEKUNDEN = 45;
* Einladung zu einem Gespräch, an dem niemand mehr teilnimmt.
*
* ---------------------------------------------------------------------
* WARUM ZWEI MINUTEN UND NICHT 45 SEKUNDEN (18.09.2026)
*
* Filipe: „das mit dem anruf klappt nicht … es geeeeeht einfach
* nicht." Nachgemessen war serverseitig alles in Ordnung: Der Anruf
* kam an, der Raum stimmte, die Angerufene war angemeldet und hatte
* ein Gerät für Benachrichtigungen.
*
* Nur passte die Zeit nicht zum Weg. Bei einem Telefon hebt man ab --
* hier muss in dieselben Sekunden: Benachrichtigung zustellen, sie
* bemerken, Handy entsperren, antippen, App laden, Anmeldung prüfen.
* 45 Sekunden reichen dafür, wenn man das Gerät in der Hand hält, und
* sonst nie.
*
* Das ist der Unterschied zwischen „funktioniert" und „funktioniert
* im Alltag". Zwei Minuten sind lang genug, um aus der Tasche zu
* kommen, und kurz genug, dass niemand vor einem Anruf sitzt, den es
* längst nicht mehr gibt. */
const KLINGELT_SEKUNDEN = Number(process.env.ANRUF_KLINGELT_SEKUNDEN) || 120;
/* WARUM UEBER DIE UMGEBUNG UMSTELLBAR: Damit eine Pruefung den Ablauf
messen kann, ohne zwei Minuten zu warten. Eine Pruefung, die so
lange braucht, wird abgeschaltet -- und dann ist der verpasste
Anruf wieder ungeprueft. Im Betrieb steht die Variable nirgends;
dort gelten die 120 Sekunden. */
/* Wer gerade in welchem Raum telefoniert.
IM SPEICHER, NICHT IN DER DATENBANK: Ein laufender Anruf ist ein
@@ -112,10 +137,58 @@ function aufraeumen() {
ist keiner mehr -- der Angerufene ist nicht rangegangen. */
if (a.wer.size === 1 && jetzt - a.seit > KLINGELT_SEKUNDEN * 1000) {
laufend.delete(raumId);
/* ---- UND ER HINTERLAESST EINE SPUR (18.09.2026) -------------
Bisher verschwand ein verpasster Anruf lautlos. Wer zu spaet
kam, oeffnete den Chat und sah -- nichts. Keine Zeile, kein
Hinweis, nicht einmal, DASS jemand angerufen hat. Genau das
fuehlt sich an wie „es geht einfach nicht", auch wenn technisch
alles funktioniert hat.
Jedes Telefon der Welt zeigt einen verpassten Anruf. Hier
steht er als Zeile im Gespraech, mit Namen und Uhrzeit -- und
damit hat der andere etwas, worauf er antworten kann.
IN EINEM EIGENEN try: Ein Fehler beim Schreiben der Zeile darf
das Aufraeumen nicht anhalten -- sonst bliebe ein abgelaufener
Anruf stehen, und das waere schlimmer als eine fehlende
Notiz. */
try { verpasstVermerken(raumId, a); } catch (f) {
console.error("[anruf] verpasst:", f?.message);
}
}
}
}
/** Schreibt „Verpasster Anruf" in das Gespraech.
*
* Als ganz normale Nachricht des Anrufers: So steht sie an der
* richtigen Stelle im Verlauf, wird mitgezaehlt wie jede andere und
* braucht keine Sonderbehandlung in der Oberflaeche. */
function verpasstVermerken(raumId, a) {
const anrufer = [...a.wer.entries()][0];
if (!anrufer) return;
const [id, wer] = anrufer;
const d = db();
const n = new Date().toISOString();
const text = a.video ? "📞 Verpasster Videoanruf" : "📞 Verpasster Anruf";
d.prepare("INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)")
.run(raumId, id, text, n);
const neueId = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(n, raumId);
/* Die anderen im Raum bekommen sie wie jede Nachricht -- inklusive
Benachrichtigung, falls sie gerade nicht zusehen. Wer den Anruf
verpasst hat, sieht wenigstens, DASS er ihn verpasst hat. */
const andere = teilnehmerFuerAnruf(raumId)
.map((t) => t.id).filter((x) => x !== id);
chatEreignis(raumId, {
id: neueId, raum_id: raumId, von_id: id, von: wer.name,
text, erstellt: n, zurueckgenommen: false, antwort: null, anhang: null,
}, andere, "neu");
}
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });