feat: Notruf — Telefon in SMS, Geo-Diagnose + Geräteerkennung; Sprach-UI-Fix

Notruf (Punkte 1+2):
- SMS enthält jetzt die Telefonnummer(n) der Person (Mobil/Festnetz).
- Geo-Diagnose (P1): captureGeo liefert bei Fehlschlag einen Grund
  (Permission/insecure/Timeout/Code) statt still null; geht als geo_error an
  /api/emergency, landet in E-Mail ("Standort nicht verfügbar (<Grund>)") und
  im Server-Log (logger.info) — Fehlversuche sind jetzt erklärbar.
- Geräteerkennung (P2): device {mobile, platform} via User-Agent Client Hints
  (Fallback UA-String); E-Mail zeigt "Auslösegerät: Mobilgerät/Festgerät (…)".
- schemas: DeviceInfo + EmergencyRequest.geo_error/device; chat reicht durch.
- Tests: Telefon in SMS, geo_error + Geräte-Zeile in E-Mail (240 grün).

Sprach-Bug (Punkt 3): allowed_languages wurde im Menü nur via <option hidden>
gefiltert, was manche Browser bei <select> ignorieren -> Sprachen blieben
wählbar. Jetzt zusätzlich opt.disabled (robust, nicht wählbar); Backend klemmt
ohnehin serverseitig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-25 17:24:36 +02:00
commit 5d05e081d4
5 changed files with 104 additions and 34 deletions

View file

@ -337,7 +337,9 @@ function applyAllowedLanguages(prefs) {
const allowed = String((prefs || {}).allowed_languages || "")
.split(",").map((s) => s.trim()).filter(Boolean);
Array.from(langSel.options).forEach((opt) => {
opt.hidden = allowed.length > 0 && !allowed.includes(opt.value);
const blocked = allowed.length > 0 && !allowed.includes(opt.value);
opt.hidden = blocked; // versteckt (moderne Browser)
opt.disabled = blocked; // robust: auch wo <option hidden> ignoriert wird, nicht wählbar
});
let val = (prefs || {}).language || "de";
if (allowed.length && !allowed.includes(val)) val = allowed[0];
@ -2091,39 +2093,54 @@ $("#load-settings").addEventListener("click", loadSettings);
// später; vorerst zeigt der Server einen Hinweis in der Nutzersprache.
const emergencyBtn = $("#emergency-btn");
// Standort (One-Shot) für den Notruf erfassen — nur mit Freigabe (location_consent)
// und mit Timeout, damit ein hängendes GPS den Alarm nicht blockiert. null = kein Fix.
// Standort (One-Shot) für den Notruf erfassen — nur mit Freigabe (location_consent),
// mit Timeout (hängendes GPS blockiert den Alarm nie). Liefert {ok:true,…} oder
// {ok:false, error:"…"} mit erklärbarem Grund (für Diagnose in Meldung/Log).
function captureGeo(timeoutMs = 8000) {
return new Promise((resolve) => {
if (!myPrefs.location_consent || !navigator.geolocation) return resolve(null);
if (!myPrefs.location_consent) return resolve({ ok: false, error: "Standort-Freigabe aus" });
if (!window.isSecureContext) return resolve({ ok: false, error: "kein HTTPS (insecure context)" });
if (!navigator.geolocation) return resolve({ ok: false, error: "Browser ohne Geolocation" });
let done = false;
const finish = (v) => { if (!done) { done = true; resolve(v); } };
const timer = setTimeout(() => finish(null), timeoutMs);
const timer = setTimeout(() => finish({ ok: false, error: "Timeout" }), timeoutMs);
navigator.geolocation.getCurrentPosition(
(pos) => {
clearTimeout(timer);
finish({
ok: true,
lat: pos.coords.latitude,
lon: pos.coords.longitude,
accuracy: pos.coords.accuracy,
ts: new Date(pos.timestamp).toISOString(),
});
},
() => { clearTimeout(timer); finish(null); },
(err) => { clearTimeout(timer); finish({ ok: false, error: "code " + err.code + ": " + err.message }); },
{ enableHighAccuracy: true, timeout: timeoutMs, maximumAge: 60000 }
);
});
}
// Geräteart/Plattform aus Browser-Daten (User-Agent Client Hints, Fallback UA-String).
function deviceInfo() {
try {
const uad = navigator.userAgentData;
if (uad) return { mobile: !!uad.mobile, platform: uad.platform || "" };
} catch (e) { /* ignore */ }
const ua = navigator.userAgent || "";
return { mobile: /Mobi|Android|iPhone|iPad|iPod/i.test(ua), platform: ua.slice(0, 120) };
}
if (emergencyBtn) {
emergencyBtn.addEventListener("click", async () => {
if (!confirm("Wirklich Hilfe rufen?")) return;
const lang = (langSel && langSel.value) || "de";
emergencyBtn.disabled = true;
try {
const geo = await captureGeo(); // null, wenn keine Freigabe/kein Fix
const body = { language: lang };
if (geo) body.geo = geo;
const g = await captureGeo();
const body = { language: lang, device: deviceInfo() };
if (g.ok) body.geo = { lat: g.lat, lon: g.lon, accuracy: g.accuracy, ts: g.ts };
else body.geo_error = g.error;
const res = await fetch("/api/emergency", {
method: "POST",
headers: { "Content-Type": "application/json" },