diff --git a/server/pruef-anruf.mjs b/server/pruef-anruf.mjs index 027a2300..6146f8e1 100644 --- a/server/pruef-anruf.mjs +++ b/server/pruef-anruf.mjs @@ -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 ==="); { diff --git a/server/workspace-anruf.js b/server/workspace-anruf.js index b48d5e9a..f9368289 100644 --- a/server/workspace-anruf.js +++ b/server/workspace-anruf.js @@ -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" }); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 51957fe9..020af340 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609182254", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609182330", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609182254", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609182330", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609182254", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609182330", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609182254", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609182330", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index a51e7dae..087df9b7 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -291,10 +291,10 @@ - - - - - + + + + +