diff --git a/server/rechte.js b/server/rechte.js
index 8d8a2bd4..d7230d2b 100644
--- a/server/rechte.js
+++ b/server/rechte.js
@@ -280,6 +280,12 @@ export const SEITEN = {
haben mit dem Treff nichts zu tun, und die Seite nennt Fristen,
Stufen und die Bedingungen des Ausschlusses. */
"/workspace/treff-regeln.html": ["gast", "modi", "hand", "admin"],
+ /* DIE ANRUF-PROBE (19.09.2026). Sie sagt einem Menschen in seinem
+ eigenen Browser, warum es nicht klingelt -- die Frage, die sich
+ von aussen nicht beantworten laesst. Sie ruft niemanden an und
+ zeigt nichts ueber andere; deshalb steht sie allen offen, die
+ ueberhaupt telefonieren koennen. */
+ "/workspace/anruf-probe.html": ["admin", "hand", "modi"],
/* DIE ANFRAGE FUER MODI (17.09.2026).
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
new file mode 100644
index 00000000..9bcfb3aa
--- /dev/null
+++ b/workspace/anruf-probe.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+Anruf-Probe · Team Dogi
+
+
+
+
+
+
+
+
+ ‹ Zum Chat
+
Anruf-Probe
+
+
+
+
+
Warum klingelt es nicht?
+
+ Diese Seite geht der Reihe nach durch, was ein Anruf braucht – und sagt,
+ an welcher Stelle es hakt. Sie ruft niemanden an.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest
index 19706be4..e1cedd1a 100644
--- a/workspace/app.webmanifest
+++ b/workspace/app.webmanifest
@@ -22,25 +22,25 @@
},
"icons": [
{
- "src": "/assets/img/app-symbole/workspace-192.png?v=202609182358",
+ "src": "/assets/img/app-symbole/workspace-192.png?v=202609190013",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
- "src": "/assets/img/app-symbole/workspace-512.png?v=202609182358",
+ "src": "/assets/img/app-symbole/workspace-512.png?v=202609190013",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
- "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609182358",
+ "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609190013",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
- "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609182358",
+ "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609190013",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
diff --git a/workspace/assets/js/anruf-probe.js b/workspace/assets/js/anruf-probe.js
new file mode 100644
index 00000000..e093d320
--- /dev/null
+++ b/workspace/assets/js/anruf-probe.js
@@ -0,0 +1,240 @@
+/* =====================================================================
+ DIE ANRUF-PROBE (19.09.2026)
+
+ Filipe, nach einem langen Abend: „funktioniert nichts davon was
+ machst du?"
+
+ Zu Recht. Ich habe an diesem Abend fünf Dinge repariert, jedes
+ einzeln nachgewiesen -- und trotzdem klingelte bei ihm nichts. Der
+ Grund ist immer derselbe: Ich kann von hier aus messen, was der
+ SERVER tut, aber nicht, was in seinem Browser ankommt. Und genau
+ dort lag es jedes Mal.
+
+ Diese Seite dreht das um. Sie läuft in SEINEM Browser und beantwortet
+ der Reihe nach die Fragen, die ich ihm sonst stellen müsste:
+
+ 1. Ist die neue Fassung überhaupt geladen? (Der häufigste Fall:
+ eine alte Seite aus dem Zwischenspeicher, die alte Dateien
+ nachlädt. Dann ist jede Reparatur unsichtbar.)
+ 2. Darf der Browser überhaupt Ton abspielen?
+ 3. Sind Benachrichtigungen erlaubt -- und ist ein Gerät angemeldet?
+ 4. Steht die Verbindung zum Server, über die das Klingeln kommt?
+
+ Sie ruft niemanden an. Man kann sie so oft aufmachen, wie man will.
+ ===================================================================== */
+(() => {
+ const $ = (id) => document.getElementById(id);
+ const ziel = $('ergebnis');
+ if (!ziel) return;
+
+ const punkte = [];
+ function punkt(stand, titel, satz) {
+ punkte.push({ stand, titel, satz });
+ const k = document.createElement('div');
+ k.className = 'punkt';
+ k.dataset.stand = stand;
+ const z = document.createElement('span');
+ z.className = 'punkt__zeichen';
+ z.textContent = stand === 'gut' ? '✓' : stand === 'warn' ? '!' : '✗';
+ const t = document.createElement('div');
+ t.className = 'punkt__text';
+ const h = document.createElement('p');
+ h.className = 'punkt__titel';
+ h.textContent = titel;
+ const s = document.createElement('p');
+ s.className = 'punkt__satz';
+ s.textContent = satz;
+ t.append(h, s);
+ k.append(z, t);
+ ziel.append(k);
+ }
+
+ /* ---- 1. WELCHE FASSUNG IST GELADEN? -----------------------------
+
+ Der wichtigste Punkt, und der am häufigsten übersehene. Die
+ Anruf-Datei trägt einen Stempel im Namen (anruf.js?v=…). Steht in
+ der geladenen Seite ein alter Stempel, kommt aus dem
+ Zwischenspeicher eine alte Datei -- und jede Reparatur am Server
+ ist unsichtbar, egal wie oft man neu lädt. */
+ async function fassungPruefen() {
+ const skript = [...document.querySelectorAll('script[src]')]
+ .map((s) => s.getAttribute('src'))
+ .find((s) => /anruf-probe\.js/.test(s || ''));
+ const stempel = (skript || '').split('v=')[1] || '(keiner)';
+
+ try {
+ const a = await fetch(`assets/js/anruf.js?v=${stempel}`, { cache: 'no-store' });
+ const text = await a.text();
+ const hatTon = text.includes('klingelTon');
+ const hatOffen = text.includes('anruf/offen');
+ if (hatTon && hatOffen) {
+ punkt('gut', 'Die neue Fassung ist geladen',
+ `Klingelton und Nachklingeln sind da (Stand ${stempel}).`);
+ } else {
+ punkt('schlecht', 'Es läuft eine ALTE Fassung',
+ `Der Klingelton fehlt${hatOffen ? '' : ', das Nachklingeln auch'}. `
+ + 'Die Seite kommt aus dem Zwischenspeicher. '
+ + 'Hilft: die Seite ganz schließen (in der App aus dem Wechsler wischen) und neu öffnen.');
+ }
+ } catch {
+ punkt('warn', 'Fassung nicht prüfbar',
+ 'Die Datei ließ sich nicht laden – bist du gerade offline?');
+ }
+ }
+
+ /* ---- 2. DARF DER BROWSER TON ABSPIELEN? -------------------------
+
+ Browser erlauben Ton erst, wenn jemand auf der Seite etwas
+ angeklickt hat. Wer über eine Benachrichtigung hereinkommt und
+ nichts berührt, hört deshalb unter Umständen nichts -- und hält
+ das Telefon für kaputt. */
+ function tonPruefen() {
+ const K = window.AudioContext || window.webkitAudioContext;
+ if (!K) {
+ punkt('schlecht', 'Dieser Browser kann keinen Ton erzeugen',
+ 'Sehr selten. Ein anderer Browser hilft.');
+ return null;
+ }
+ const raum = new K();
+ if (raum.state === 'running') {
+ punkt('gut', 'Ton ist erlaubt', 'Der Klingelton kann abgespielt werden.');
+ } else {
+ punkt('warn', 'Ton ist noch gesperrt',
+ 'Der Browser lässt Ton erst zu, nachdem man einmal etwas angetippt hat. '
+ + 'Drück unten auf „Klingelton anhören" – danach ist er frei, '
+ + 'solange die Seite offen bleibt.');
+ }
+ return raum;
+ }
+
+ /* ---- 3. BENACHRICHTIGUNGEN ---------------------------------------- */
+ async function meldungPruefen() {
+ if (!('Notification' in window)) {
+ punkt('warn', 'Benachrichtigungen kennt dieser Browser nicht',
+ 'Anrufe klingeln dann nur, wenn die Seite offen ist.');
+ return;
+ }
+ const erlaubt = Notification.permission;
+ if (erlaubt === 'granted') punkt('gut', 'Benachrichtigungen sind erlaubt', 'Gut.');
+ else if (erlaubt === 'denied') {
+ punkt('schlecht', 'Benachrichtigungen sind blockiert',
+ 'Dann kommt kein Anruf an, solange die Seite zu ist. '
+ + 'In den Browsereinstellungen für diese Seite wieder erlauben.');
+ } else {
+ punkt('warn', 'Benachrichtigungen sind noch nicht erlaubt',
+ 'Im Chat oben auf die Glocke tippen und zustimmen.');
+ }
+
+ /* Ist auch wirklich ein Gerät beim Server angemeldet? Erlaubnis im
+ Browser und Anmeldung beim Server sind zwei verschiedene Dinge --
+ genau diese Lücke sieht man sonst nirgends. */
+ try {
+ const a = await fetch('/workspace/api/push/stand', { credentials: 'same-origin' });
+ if (a.ok) {
+ const d = await a.json();
+ /* `geraete` ist eine LISTE, keine Zahl -- nachgesehen in
+ workspace-push.js statt geraten. */
+ const n = Array.isArray(d.geraete) ? d.geraete.length : 0;
+ if (n > 0) punkt('gut', 'Dieses Konto hat ein Gerät angemeldet', `${n} Gerät(e).`);
+ else {
+ punkt('schlecht', 'Kein Gerät beim Server angemeldet',
+ 'Der Browser darf zwar, aber es ist nichts hinterlegt. '
+ + 'Im Chat auf die Glocke tippen und neu zustimmen.');
+ }
+ /* UND DIE RUHEZEIT. Sie hat gestern Abend den ganzen Anruf
+ verschluckt; seit heute sind Anrufe ausgenommen. Wer das
+ hier liest, soll es sehen, statt es zu vermuten. */
+ if (d.ruhe) {
+ const anrufArt = (d.arten || []).find((x) => x.schluessel === 'anruf');
+ if (anrufArt && anrufArt.an === false) {
+ punkt('schlecht', 'Anrufe sind bei dir abgeschaltet',
+ 'In den Benachrichtigungen die Art „Jemand ruft an" wieder einschalten.');
+ } else if (d.ruhe.jetzt) {
+ punkt('gut', `Es ist Ruhezeit (${d.ruhe.ab}–${d.ruhe.bis} Uhr)`,
+ 'Erinnerungen bleiben jetzt liegen – Anrufe kommen trotzdem durch.');
+ }
+ }
+ }
+ } catch { /* die Route gibt es vielleicht nicht -- kein Grund zur Panik */ }
+ }
+
+ /* ---- 4. KOMMT ETWAS VOM SERVER AN? ------------------------------- */
+ async function serverPruefen() {
+ try {
+ const a = await fetch('/workspace/api/anruf/offen', { credentials: 'same-origin' });
+ if (a.status === 401) {
+ punkt('schlecht', 'Du bist nicht angemeldet',
+ 'Dann kann auch nichts klingeln. Melde dich neu an.');
+ return;
+ }
+ if (!a.ok) {
+ punkt('warn', 'Der Server antwortet ungewöhnlich', `Antwort ${a.status}.`);
+ return;
+ }
+ const d = await a.json();
+ punkt('gut', 'Die Verbindung zum Server steht',
+ (d.anrufe?.length
+ ? `Gerade klingelt sogar etwas (${d.anrufe.length}).`
+ : 'Gerade klingelt nichts – das ist normal.'));
+ } catch {
+ punkt('schlecht', 'Keine Verbindung zum Server',
+ 'Kein Netz? Ohne sie kommt kein Anruf an.');
+ }
+ }
+
+ /* ---- Der Rat am Ende --------------------------------------------- */
+ function ratGeben() {
+ const schlecht = punkte.filter((p) => p.stand === 'schlecht');
+ const warn = punkte.filter((p) => p.stand === 'warn');
+ const r = $('rat');
+ if (!r) return;
+ if (schlecht.length) {
+ r.textContent = 'Das Wichtigste zuerst: ' + schlecht[0].titel
+ + '. ' + schlecht[0].satz;
+ } else if (warn.length) {
+ r.textContent = 'Fast alles steht. Offen ist: ' + warn[0].titel
+ + '. ' + warn[0].satz;
+ } else {
+ r.textContent = 'Alles, was von hier aus prüfbar ist, steht. '
+ + 'Wenn es jetzt noch klemmt, liegt es an der Verbindung zwischen '
+ + 'den beiden Geräten – dann hilft nur ein echter Anruf zu zweit.';
+ }
+ }
+
+ /* ---- Der Knopf: Ton wirklich hören -------------------------------- */
+ let tonRaum = null;
+ $('ton-probe')?.addEventListener('click', () => {
+ try {
+ const K = window.AudioContext || window.webkitAudioContext;
+ tonRaum = tonRaum || new K();
+ if (tonRaum.state === 'suspended') tonRaum.resume();
+ const spiel = (hz, ab, lang) => {
+ const o = tonRaum.createOscillator();
+ const v = tonRaum.createGain();
+ o.type = 'sine'; o.frequency.value = hz;
+ const t = tonRaum.currentTime + ab;
+ v.gain.setValueAtTime(0, t);
+ v.gain.linearRampToValueAtTime(0.16, t + 0.03);
+ v.gain.linearRampToValueAtTime(0, t + lang);
+ o.connect(v).connect(tonRaum.destination);
+ o.start(t); o.stop(t + lang + 0.05);
+ };
+ /* Genau der Klingelton aus dem Anruf -- damit man hört, was man
+ hören würde. */
+ spiel(880, 0, 0.22);
+ spiel(660, 0.3, 0.26);
+ try { navigator.vibrate?.([250, 200, 250]); } catch { /* egal */ }
+ $('ton-probe').textContent = 'Nochmal anhören';
+ } catch {
+ $('ton-probe').textContent = 'Ton ging nicht – siehe oben';
+ }
+ });
+
+ (async () => {
+ await fassungPruefen();
+ tonPruefen();
+ await meldungPruefen();
+ await serverPruefen();
+ ratGeben();
+ })();
+})();
diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html
index 710efe72..e588a78c 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 @@
-
-
-
-
-
+
+
+
+
+