generated from knso/webklar-preview-template
Korrekturen aus zwei Pruefrunden vor der MBE-Vorfuehrung
Demo-Seite: - Ampel in Kalendertagen, naechstes kommendes Datum, "Termin am"/"Frist bis" - nie "Deutsch | Deutsch"; Beraterin bekommt immer Deutsch (d=1), das Handy erkennt seine Sprache selbst und speichert die PC-Vorgabe nicht dauerhaft - Code-Feld ausserhalb von #eingabe; tolerante Code-Eingabe; Ablaufdatum im Banner; leeres/abgelaufenes Einrichtungs-Kontingent ohne Kaufangebot, mit Telefonnummer; nach Aufladen reicht ein Neuladen - Handy-Modus: endet nach dem ersten Brief, uebersteht ein Neuladen (sessionStorage), keine Sackgasse ohne Zugang, ehrlicher Hinweis + "Noch einmal senden", wenn das Ergebnis den PC nicht erreicht - PC wartet geduldig, solange verbunden; meldet langsame Analysen, fehlende Verbindung, fehlenden Zugang; Warnung beim Verlassen - Einwilligung pro Brief, nennt Gesundheit/Herkunft/Aufenthalt - localStorage nur ueber abgesicherten Helfer; Dateifeld wird geleert - RTL (bdi, dir=auto), Sprachliste mit deutschem Zusatz, groesserer QR Worker: KI-Aufruf und Router-catch liefern immer JSON mit CORS; Logs ohne Briefinhalt; [observability]; Nutzungszaehler je Analyse (Datum, Sprache, 390 Tage); normCode + 30 Fehlversuche; Uebergabe-TTL gleitend. Werkzeug: Auswertung pro Woche/Sprache, Eingabepruefung, --bis in der Hilfe. Datenschutz: SCC statt DPF, "in der Regel 30 Tage", Server-Protokolle, Nutzungszaehler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
91437d37b4
commit
2af4b7ed41
6 files changed
+447
-101
No files matched your search
+55
-11
@@ -245,7 +245,8 @@ async function uebersetzeAnalyse(env: Env, analyse: unknown, sprachName: string)
|
||||
if (d.stop_reason !== 'end_turn' || !text) return null;
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.log('Übersetzung fehlgeschlagen', String(e));
|
||||
// Nur den Fehlertyp loggen: JSON.parse-Meldungen zitieren Teile der Eingabe (= Briefinhalt)
|
||||
console.log('Übersetzung fehlgeschlagen', e instanceof Error ? e.name : 'unbekannt');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -392,7 +393,9 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
||||
? { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: bild } }
|
||||
: { type: 'image', source: { type: 'base64', media_type: mimeType, data: bild } };
|
||||
|
||||
const antwort = await fetch(ANTHROPIC_URL, {
|
||||
let antwort: Response;
|
||||
try {
|
||||
antwort = await fetch(ANTHROPIC_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
@@ -417,6 +420,10 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
||||
],
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Demo-KI nicht erreichbar', String(e));
|
||||
return demoFehler(503, 'ki', 'Der KI-Dienst ist gerade nicht erreichbar. Bitte versuchen Sie es in einer Minute erneut.', origin);
|
||||
}
|
||||
|
||||
if (!antwort.ok) {
|
||||
const status = antwort.status;
|
||||
@@ -433,7 +440,13 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
||||
return demoFehler(502, 'ki', 'Die Analyse ist fehlgeschlagen. Bitte versuchen Sie es mit einem neuen, gut beleuchteten Foto.', origin);
|
||||
}
|
||||
|
||||
const daten = (await antwort.json()) as {
|
||||
let datenRoh: unknown;
|
||||
try {
|
||||
datenRoh = await antwort.json();
|
||||
} catch {
|
||||
return demoFehler(502, 'ki', 'Die KI hat keine verwertbare Antwort geliefert. Bitte erneut versuchen.', origin);
|
||||
}
|
||||
const daten = datenRoh as {
|
||||
stop_reason?: string;
|
||||
content?: { type: string; text?: string }[];
|
||||
};
|
||||
@@ -444,6 +457,17 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
||||
if (bezahlt) {
|
||||
// Bezahlte Analyse: Guthaben um 1 verringern (kein TTL — bleibt bestehen)
|
||||
await env.RATE_LIMIT.put(`wallet:${walletId}`, String(walletRest - 1));
|
||||
if (einrichtungsName) {
|
||||
// Für die Auswertung mit der Beratungsstelle: nur Datum und Sprache,
|
||||
// kein Briefinhalt, kein Bezug zu einer Person. Ein Schlüssel pro
|
||||
// Analyse, damit gleichzeitige Analysen sich nicht überschreiben.
|
||||
// Lesen: node tools/einrichtungs-code.js stand <CODE>
|
||||
await env.RATE_LIMIT.put(
|
||||
`nutzung:${walletId}:${heute}:${sprachCode}:${crypto.randomUUID().slice(0, 8)}`,
|
||||
'1',
|
||||
{ expirationTtl: 60 * 60 * 24 * 390 }, // < 13 Monate (siehe Datenschutzerklärung 9a)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const schreiben: Promise<void>[] = [
|
||||
env.RATE_LIMIT.put(tagKey, String(tagZahl + 1), { expirationTtl: TAG_TTL }),
|
||||
@@ -476,11 +500,18 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
||||
|
||||
let analyseDe: unknown = undefined;
|
||||
let ausgabeSprache = sprachCode;
|
||||
let uebersetzungFehlgeschlagen = false;
|
||||
if (zweisprachig) {
|
||||
analyseDe = analyse;
|
||||
const uebersetzt = await uebersetzeAnalyse(env, analyse, sprachName);
|
||||
if (uebersetzt) analyse = uebersetzt;
|
||||
else ausgabeSprache = 'de'; // lieber Deutsch als gar nichts
|
||||
if (uebersetzt) {
|
||||
analyseDe = analyse;
|
||||
analyse = uebersetzt;
|
||||
} else {
|
||||
// lieber Deutsch als gar nichts — aber nur EINE Fassung schicken,
|
||||
// sonst zeigt die Seite zwei Knöpfe „Deutsch | Deutsch"
|
||||
ausgabeSprache = 'de';
|
||||
uebersetzungFehlgeschlagen = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
@@ -491,6 +522,7 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
||||
guthaben: bezahlt ? walletRest - 1 : undefined,
|
||||
einrichtung: einrichtungsName ?? undefined,
|
||||
sprache: ausgabeSprache,
|
||||
uebersetzung_fehlgeschlagen: uebersetzungFehlgeschlagen || undefined,
|
||||
},
|
||||
{ headers: demoCorsHeaders(origin) }
|
||||
);
|
||||
@@ -685,7 +717,15 @@ async function kaufStatusHandler(request: Request, env: Env): Promise<Response>
|
||||
// Der zufällige Teil im Code macht Raten aussichtslos; zusätzlich sind
|
||||
// Fehlversuche pro IP und Tag begrenzt.
|
||||
|
||||
const CODE_FEHLVERSUCHE_PRO_TAG = 10;
|
||||
// Großzügig, weil sich eine ganze Beratungsstelle eine IP teilt. Raten bleibt
|
||||
// trotzdem aussichtslos: 31^4 ≈ 920.000 Codes je Präfix.
|
||||
const CODE_FEHLVERSUCHE_PRO_TAG = 30;
|
||||
|
||||
/** „mbe k7qx", „MBEK7QX", „MBE-K7QX" → „MBE-K7QX" (Bindestrich vor den letzten 4 Zeichen). */
|
||||
function normCode(eingabe: string): string {
|
||||
const roh = eingabe.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
return roh.length > 4 ? `${roh.slice(0, -4)}-${roh.slice(-4)}` : roh;
|
||||
}
|
||||
|
||||
interface Einrichtung {
|
||||
name: string;
|
||||
@@ -716,7 +756,7 @@ async function codeHandler(request: Request, env: Env): Promise<Response> {
|
||||
} catch {
|
||||
return demoFehler(400, 'json', 'Ungültige Anfrage.', origin);
|
||||
}
|
||||
const code = typeof body.code === 'string' ? body.code.trim().toUpperCase().replace(/\s+/g, '') : '';
|
||||
const code = typeof body.code === 'string' ? normCode(body.code) : '';
|
||||
|
||||
const ip = request.headers.get('cf-connecting-ip') ?? 'unbekannt';
|
||||
const heute = new Date().toISOString().slice(0, 10);
|
||||
@@ -782,9 +822,8 @@ export class Uebergabe extends DurableObject<Env> {
|
||||
const zahl = (await this.ctx.storage.get<number>('zahl')) ?? 0;
|
||||
if (zahl >= UEBERGABE_MAX_SCHREIBVORGAENGE) return false;
|
||||
await this.ctx.storage.put({ [`fach:${fach}`]: wert, zahl: zahl + 1 });
|
||||
if ((await this.ctx.storage.getAlarm()) === null) {
|
||||
// gleitend: 15 Minuten nach dem LETZTEN Päckchen löschen, nicht nach dem ersten
|
||||
await this.ctx.storage.setAlarm(Date.now() + UEBERGABE_LEBENSDAUER_MS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -896,7 +935,12 @@ export default {
|
||||
const pfad = new URL(request.url).pathname;
|
||||
lokalTest = env.LOKAL_TEST === '1';
|
||||
// Web-Demo + Bezahl-Routen haben eigene Regeln
|
||||
if (pfad === '/demo') return demoHandler(request, env);
|
||||
if (pfad === '/demo') {
|
||||
return demoHandler(request, env).catch((e) => {
|
||||
console.log('Demo-Fehler', e instanceof Error ? e.name : 'unbekannt');
|
||||
return demoFehler(503, 'dienst', 'Der Dienst ist gerade nicht erreichbar. Bitte versuchen Sie es in einer Minute erneut.', request.headers.get('origin'));
|
||||
});
|
||||
}
|
||||
if (pfad === '/kaufen') return kaufenHandler(request, env);
|
||||
if (pfad === '/kauf-status') return kaufStatusHandler(request, env);
|
||||
if (pfad === '/code') return codeHandler(request, env);
|
||||
|
||||
@@ -2,6 +2,11 @@ name = "behoerdenklar-proxy"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-07-01"
|
||||
|
||||
# Worker-Logs im Cloudflare-Dashboard (und `npx wrangler tail`) — hilft,
|
||||
# wenn live etwas schiefgeht (z. B. die Übersetzung)
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
# Erlaubte Anfragen pro Tag: pro Gerät (normale Nutzung) und pro IP-Adresse
|
||||
# (dämmt Angreifer ein, die Geräte-IDs fälschen; höher, weil sich viele
|
||||
# Nutzer eine IP teilen können)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Einrichtungs-Codes für Beratungsstellen anlegen und verwalten.
|
||||
*
|
||||
* node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --bis 2026-12-18
|
||||
* node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --monate 1
|
||||
* node tools/einrichtungs-code.js stand MBE-K7QX
|
||||
* node tools/einrichtungs-code.js stand MBE-K7QX (mit Nutzung nach Woche und Sprache)
|
||||
* node tools/einrichtungs-code.js aufladen MBE-K7QX 100
|
||||
* node tools/einrichtungs-code.js verlaengern MBE-K7QX 2027-06-30
|
||||
*
|
||||
@@ -49,8 +50,26 @@ function plusMonate(monate) {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** Wie im Worker: „mbe k7qx" → „MBE-K7QX". */
|
||||
function normCode(c) {
|
||||
const roh = String(c || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
return roh.length > 4 ? `${roh.slice(0, -4)}-${roh.slice(-4)}` : roh;
|
||||
}
|
||||
|
||||
/** Nutzung je Analyse (vom Worker: nutzung:<walletId>:<datum>:<sprache>:<zufall>). */
|
||||
function nutzung(walletId) {
|
||||
let liste = [];
|
||||
try {
|
||||
liste = JSON.parse(kv('list', '--prefix', `nutzung:${walletId}:`));
|
||||
} catch { return null; }
|
||||
return liste.map((e) => {
|
||||
const [, , datum, sprache] = e.name.split(':');
|
||||
return { datum, sprache };
|
||||
});
|
||||
}
|
||||
|
||||
function ladeCode(code) {
|
||||
code = String(code || '').toUpperCase();
|
||||
code = normCode(code);
|
||||
const walletId = lies(`code:${code}`);
|
||||
if (!walletId) abbruch(`Code ${code} gibt es nicht.`);
|
||||
const einrichtung = JSON.parse(lies(`einrichtung:${walletId}`) || '{}');
|
||||
@@ -58,14 +77,31 @@ function ladeCode(code) {
|
||||
return { code, walletId, einrichtung, rest };
|
||||
}
|
||||
|
||||
function zeige({ code, einrichtung, rest }) {
|
||||
function zeige({ code, einrichtung, rest, walletId }) {
|
||||
const heute = new Date().toISOString().slice(0, 10);
|
||||
const n = walletId ? nutzung(walletId) : null;
|
||||
let auswertung = '';
|
||||
if (n && n.length) {
|
||||
const proWoche = {}, proSprache = {};
|
||||
for (const { datum, sprache } of n) {
|
||||
const d = new Date(`${datum}T00:00:00Z`);
|
||||
const montag = new Date(d.getTime() - ((d.getUTCDay() + 6) % 7) * 86400000).toISOString().slice(0, 10);
|
||||
proWoche[montag] = (proWoche[montag] || 0) + 1;
|
||||
proSprache[sprache] = (proSprache[sprache] || 0) + 1;
|
||||
}
|
||||
auswertung = `
|
||||
Genutzt: ${n.length} Briefe
|
||||
Pro Woche: ${Object.keys(proWoche).sort().map((w) => `ab ${w}: ${proWoche[w]}`).join(' · ')}
|
||||
Sprachen: ${Object.entries(proSprache).sort((a, b) => b[1] - a[1]).map(([s, z]) => `${s} ${z}`).join(' · ')}`;
|
||||
} else if (n) {
|
||||
auswertung = '\n Genutzt: noch keine Briefe';
|
||||
}
|
||||
console.log(`
|
||||
Einrichtung: ${einrichtung.name}
|
||||
Code: \x1b[1m${code}\x1b[0m
|
||||
Übrig: ${rest} Briefe
|
||||
Gültig bis: ${einrichtung.ablauf}${einrichtung.ablauf < heute ? ' \x1b[31m(abgelaufen)\x1b[0m' : ''}
|
||||
Link: https://behoerdenklar.de/demo?code=${code}
|
||||
Link: https://behoerdenklar.de/demo?code=${code}${auswertung}
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -78,7 +114,10 @@ if (befehl === 'neu') {
|
||||
abbruch('So geht es: node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --monate 1');
|
||||
}
|
||||
const briefe = parseInt(option('briefe', '100'), 10);
|
||||
const ablauf = option('bis', plusMonate(parseInt(option('monate', '1'), 10)));
|
||||
const monate = parseInt(option('monate', '1'), 10);
|
||||
const ablauf = option('bis', Number.isFinite(monate) && monate > 0 ? plusMonate(monate) : '');
|
||||
if (!(briefe > 0)) abbruch('--briefe braucht eine Zahl größer 0, z. B. --briefe 100');
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(ablauf)) abbruch('--bis braucht ein Datum wie 2026-12-18 (oder --monate 1)');
|
||||
const code = `${praefix}-${zufall(4)}`;
|
||||
if (lies(`code:${code}`)) abbruch('Zufällig schon vergeben — einfach nochmal ausführen.');
|
||||
const walletId = crypto.randomUUID();
|
||||
@@ -86,7 +125,7 @@ if (befehl === 'neu') {
|
||||
schreib(`einrichtung:${walletId}`, JSON.stringify({ name, ablauf }));
|
||||
schreib(`code:${code}`, walletId);
|
||||
console.log('\n \x1b[32m✓ Code angelegt\x1b[0m');
|
||||
zeige({ code, einrichtung: { name, ablauf }, rest: briefe });
|
||||
zeige({ code, einrichtung: { name, ablauf }, rest: briefe, walletId });
|
||||
} else if (befehl === 'stand') {
|
||||
zeige(ladeCode(a));
|
||||
} else if (befehl === 'aufladen') {
|
||||
|
||||
+16
-10
@@ -61,8 +61,9 @@
|
||||
<li>
|
||||
<strong>Anthropic PBC (USA)</strong> — den Betreiber des KI-Modells
|
||||
„Claude". Anthropic verarbeitet die Daten ausschließlich zur
|
||||
Erstellung der Analyse, speichert sie maximal 30 Tage (zur
|
||||
Missbrauchserkennung) und verwendet sie nicht zum Training der KI.
|
||||
Erstellung der Analyse, löscht sie in der Regel nach 30 Tagen
|
||||
(länger nur, wenn ein Verdacht auf Missbrauch geprüft werden muss)
|
||||
und verwendet sie nicht zum Training der KI.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
@@ -105,15 +106,16 @@
|
||||
<h2>4. Übermittlung in die USA</h2>
|
||||
<p>
|
||||
Anthropic PBC und Cloudflare, Inc. haben ihren Sitz in den USA. Die
|
||||
Übermittlung stützt sich auf den Angemessenheitsbeschluss der
|
||||
EU-Kommission zum EU-U.S. Data Privacy Framework bzw. auf
|
||||
EU-Standardvertragsklauseln (Art. 46 DSGVO).
|
||||
Übermittlung stützt sich auf EU-Standardvertragsklauseln (Art. 46
|
||||
Abs. 2 lit. c DSGVO), die Bestandteil der
|
||||
Auftragsverarbeitungsverträge beider Anbieter sind.
|
||||
</p>
|
||||
|
||||
<h2>5. Speicherdauer</h2>
|
||||
<ul>
|
||||
<li>Brieffotos bei Anthropic: maximal 30 Tage, danach automatische Löschung</li>
|
||||
<li>Brieffotos bei Anthropic: in der Regel 30 Tage, danach automatische Löschung (länger nur bei Prüfung eines Missbrauchsverdachts)</li>
|
||||
<li>Anonyme Geräte-Kennung + Tageszähler auf unserem Server: maximal 48 Stunden</li>
|
||||
<li>Technische Server-Protokolle (Cloudflare Workers Logs: Zeitpunkt, aufgerufene Adresse, Fehlerart, technische Metadaten, kein Briefinhalt) zur Fehlersuche: maximal 7 Tage (Art. 6 Abs. 1 lit. f DSGVO)</li>
|
||||
<li>Daten auf Ihrem Gerät: bis Sie sie löschen („Alle Daten löschen" in den Einstellungen oder Deinstallation der App)</li>
|
||||
</ul>
|
||||
|
||||
@@ -157,7 +159,7 @@
|
||||
(Häkchen vor dem Hochladen, Art. 6 Abs. 1 lit. a, Art. 9 Abs. 2
|
||||
lit. a DSGVO) — über unseren Server (Cloudflare) an Anthropic (USA)
|
||||
zur Analyse geschickt. Es wird auf unserem Server nicht gespeichert;
|
||||
bei Anthropic wird es nach spätestens 30 Tagen gelöscht (wie in
|
||||
bei Anthropic wird es in der Regel nach 30 Tagen gelöscht (wie in
|
||||
Abschnitt 3a beschrieben). Das Ergebnis sehen nur Sie in Ihrem
|
||||
Browser; wir speichern es nicht.
|
||||
</li>
|
||||
@@ -222,9 +224,13 @@
|
||||
Guthaben der Einrichtung, das wie in Abschnitt 9 beschrieben unter
|
||||
einer zufälligen Kennung verwaltet wird. Wir erfahren dabei nicht, wer
|
||||
den Code nutzt, und die Beratungsstelle sieht keine Briefe oder
|
||||
Ergebnisse, nur wie viele Analysen noch übrig sind. Gegen das Erraten
|
||||
von Codes zählen wir falsche Eingaben pro IP-Adresse (Speicherung
|
||||
max. 48 Stunden, Art. 6 Abs. 1 lit. f DSGVO).
|
||||
Ergebnisse, nur Zahlen: wie viele Analysen noch übrig sind und wie
|
||||
viele pro Woche und Sprache genutzt wurden. Für die Auswertung
|
||||
mit der Beratungsstelle zählen wir pro Analyse über einen Code nur das
|
||||
Datum und die gewählte Sprache, ohne Briefinhalt und ohne Bezug zu
|
||||
einer Person (Speicherung max. 13 Monate). Gegen das Erraten von
|
||||
Codes zählen wir falsche Eingaben pro IP-Adresse (Speicherung max.
|
||||
48 Stunden). Rechtsgrundlage ist jeweils Art. 6 Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
|
||||
<h2>9b. Übergabe zwischen Handy und Computer</h2>
|
||||
|
||||
+323
-73
@@ -183,7 +183,7 @@
|
||||
font-size: 16px; font-weight: 700;
|
||||
}
|
||||
.todo { padding: 8px 0 8px 2px; border-bottom: 1px dotted rgba(33,33,29,0.3); font-size: 16.5px; }
|
||||
.todo::before { content: '☐'; margin-right: 10px; color: var(--amtsblau); font-size: 18px; }
|
||||
.todo::before { content: '☐'; margin-inline-end: 10px; color: var(--amtsblau); font-size: 18px; }
|
||||
details { border-bottom: 1px solid rgba(33, 33, 29, 0.2); padding: 12px 4px; }
|
||||
summary { font-weight: 700; cursor: pointer; font-size: 16.5px; list-style: none; }
|
||||
summary::-webkit-details-marker { display: none; }
|
||||
@@ -215,7 +215,7 @@
|
||||
background: #eaf1fa; border: 2px solid var(--amtsblau); font-size: 16px;
|
||||
}
|
||||
.qr-box { display: none; margin-top: 16px; padding: 18px; background: #fff; border: 2px dashed var(--amtsblau); text-align: center; }
|
||||
.qr-box .qr { width: 220px; height: 220px; margin: 10px auto; }
|
||||
.qr-box .qr { width: 290px; max-width: 100%; aspect-ratio: 1; height: auto; margin: 10px auto; }
|
||||
.qr-box .qr svg { width: 100%; height: 100%; display: block; }
|
||||
.qr-box ol { text-align: start; margin: 8px auto 0; max-width: 420px; padding-inline-start: 22px; font-size: 15.5px; }
|
||||
.qr-box .zustand { margin-top: 12px; font-weight: 700; font-size: 16.5px; }
|
||||
@@ -306,11 +306,12 @@
|
||||
<label class="konsens">
|
||||
<input type="checkbox" id="konsens">
|
||||
<span>
|
||||
Ich bin einverstanden, dass mein Brief-Foto zur Analyse an den
|
||||
Server von BehördenKlar und an den KI-Dienst Anthropic (USA)
|
||||
geschickt wird. Es wird dort nicht dauerhaft gespeichert.
|
||||
Behördenbriefe enthalten persönliche Daten — laden Sie nur hoch,
|
||||
was Sie analysieren lassen möchten.
|
||||
Ich bin einverstanden, dass das Foto dieses Briefs zur Erklärung
|
||||
an den Server von BehördenKlar und an den KI-Dienst Anthropic
|
||||
(USA) geschickt wird. Briefe können sehr persönliche Angaben
|
||||
enthalten, zum Beispiel zu Gesundheit, Herkunft oder Aufenthalt.
|
||||
BehördenKlar speichert den Brief nicht. Anthropic löscht ihn in
|
||||
der Regel nach 30 Tagen und nutzt ihn nicht zum Training.
|
||||
<a href="datenschutz.html" target="_blank">Datenschutzerklärung</a>
|
||||
</span>
|
||||
</label>
|
||||
@@ -329,7 +330,7 @@
|
||||
</div>
|
||||
|
||||
<input type="file" id="datei" accept="image/jpeg,image/png,application/pdf" style="display: none;">
|
||||
<button class="cta" id="startKnopf" disabled>📷 Brief auswählen & erklären lassen</button>
|
||||
<button class="cta" id="startKnopf">📷 Brief auswählen & erklären lassen</button>
|
||||
<button class="cta cta-zwei" id="handyKnopf" type="button" style="display: none;">📱 Lieber mit dem Handy fotografieren</button>
|
||||
<div class="qr-box" id="handyBox">
|
||||
<strong>Mit dem Handy fotografieren</strong>
|
||||
@@ -341,7 +342,7 @@
|
||||
</ol>
|
||||
<label class="konsens" id="handyDeutschZeile" style="justify-content: center; margin: 14px 0 0;">
|
||||
<input type="checkbox" id="handyDeutsch" checked>
|
||||
<span>Hier am Bildschirm auf Deutsch zeigen, am Handy in der gewählten Sprache</span>
|
||||
<span>Hier am Bildschirm immer auf Deutsch zeigen, am Handy in der Sprache der Person</span>
|
||||
</label>
|
||||
<p class="zustand" id="handyZustand">⏳ Warte auf das Handy …</p>
|
||||
<p class="klein" style="margin-top: 6px;">Verschlüsselt: Den Schlüssel kennt nur dieser QR-Code. Der Code gilt 15 Minuten.</p>
|
||||
@@ -369,12 +370,6 @@
|
||||
Kamera noch auf Mikrofon zu.
|
||||
</p>
|
||||
|
||||
<details id="codeTeil" style="margin-top: 14px; border-top: 1px solid rgba(33, 33, 29, 0.2);">
|
||||
<summary>🤝 Sie haben einen Code von einer Beratungsstelle?</summary>
|
||||
<p>Dann ist BehördenKlar für Sie kostenlos. Geben Sie den Code hier ein:</p>
|
||||
<input class="wl-input" id="codeFeld" placeholder="z. B. MBE-K7QX" autocapitalize="characters" autocomplete="off" spellcheck="false" style="margin-top: 10px; text-transform: uppercase;">
|
||||
<button class="cta" id="codeKnopf" type="button">Code einlösen</button>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status">
|
||||
@@ -384,6 +379,13 @@
|
||||
</div>
|
||||
|
||||
<div class="fehlerbox" id="fehler"></div>
|
||||
|
||||
<details id="codeTeil" style="margin-top: 16px; border-top: 1px solid rgba(33, 33, 29, 0.2);">
|
||||
<summary>🤝 Sie haben einen Code von einer Beratungsstelle?</summary>
|
||||
<p>Dann ist BehördenKlar für Sie kostenlos. Geben Sie den Code hier ein:</p>
|
||||
<input class="wl-input" id="codeFeld" placeholder="z. B. MBE-K7QX" autocapitalize="characters" autocomplete="off" spellcheck="false" style="margin-top: 10px; text-transform: uppercase;">
|
||||
<button class="cta" id="codeKnopf" type="button">Code einlösen</button>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -437,7 +439,7 @@
|
||||
<div class="umschalter" id="sprachUmschalter" role="group" aria-label="Sprache der Erklärung"></div>
|
||||
|
||||
<div class="brief-karte">
|
||||
<span class="karten-label neutral" id="ergBrieftyp">Brief</span>
|
||||
<span class="karten-label neutral" id="ergBrieftyp" dir="auto" style="font-family: inherit; letter-spacing: 0; text-transform: none;">Brief</span>
|
||||
<p style="margin-top: 6px;"><strong id="ergKern" dir="auto"></strong></p>
|
||||
<p class="ampelzeile" id="ergAmpel" style="display: none;"></p>
|
||||
</div>
|
||||
@@ -551,8 +553,27 @@
|
||||
{ priceId: 'price_1U6BPRBsRA1AOdnemDUalzb8', titel: '15 Analysen', preis: '6,99 €', best: true },
|
||||
];
|
||||
|
||||
// Browser-Speicher abgesichert: Manche Browser (Safari „Alle Cookies
|
||||
// blockieren", App-interne Browser) werfen schon beim Zugriff einen Fehler.
|
||||
// Dann merken wir uns die Werte nur, solange die Seite offen ist.
|
||||
var speicher = (function () {
|
||||
var ersatz = {};
|
||||
return {
|
||||
get: function (k) { try { return window.localStorage.getItem(k); } catch (e) { return k in ersatz ? ersatz[k] : null; } },
|
||||
set: function (k, v) { try { window.localStorage.setItem(k, v); } catch (e) { ersatz[k] = String(v); } },
|
||||
remove: function (k) { try { window.localStorage.removeItem(k); } catch (e) { delete ersatz[k]; } },
|
||||
};
|
||||
})();
|
||||
|
||||
// Zielsprache: kommt als ?sprache=tr von den Sprachseiten, wird gemerkt.
|
||||
var SPRACH_SCHLUESSEL = 'bk_sprache';
|
||||
var SPRACH_DEUTSCH = {
|
||||
tr:'Türkisch', ar:'Arabisch', ru:'Russisch', uk:'Ukrainisch', pl:'Polnisch', ro:'Rumänisch',
|
||||
en:'Englisch', fa:'Farsi/Dari', ku:'Kurdisch', bg:'Bulgarisch', hr:'Kroatisch', sr:'Serbisch',
|
||||
bs:'Bosnisch', sq:'Albanisch', it:'Italienisch', el:'Griechisch', es:'Spanisch', pt:'Portugiesisch',
|
||||
fr:'Französisch', hu:'Ungarisch', vi:'Vietnamesisch', hi:'Hindi', ur:'Urdu', ps:'Paschtu',
|
||||
ti:'Tigrinya', so:'Somali', zh:'Chinesisch'
|
||||
};
|
||||
var SPRACH_NAMEN = {
|
||||
de:'Deutsch', tr:'Türkçe', ar:'العربية', ru:'Русский', uk:'Українська', pl:'Polski',
|
||||
ro:'Română', en:'English', fa:'فارسی', ku:'Kurdî', bg:'Български', hr:'Hrvatski',
|
||||
@@ -560,10 +581,21 @@
|
||||
pt:'Português', fr:'Français', hu:'Magyar', vi:'Tiếng Việt', hi:'हिन्दी', ur:'اردو',
|
||||
ps:'پښتو', ti:'ትግርኛ', so:'Soomaali', zh:'中文'
|
||||
};
|
||||
var sitzungsSprache = null; // vom PC per QR vorgegeben — gilt nur, solange die Seite offen ist
|
||||
function zielsprache() {
|
||||
if (sitzungsSprache) return sitzungsSprache;
|
||||
var p = new URLSearchParams(window.location.search).get('sprache');
|
||||
if (p && SPRACH_NAMEN[p]) { try { localStorage.setItem(SPRACH_SCHLUESSEL, p); } catch (e) {} return p; }
|
||||
try { var g = localStorage.getItem(SPRACH_SCHLUESSEL); if (g && SPRACH_NAMEN[g]) return g; } catch (e) {}
|
||||
if (p && SPRACH_NAMEN[p]) { speicher.set(SPRACH_SCHLUESSEL, p); return p; }
|
||||
var g = speicher.get(SPRACH_SCHLUESSEL); if (g && SPRACH_NAMEN[g]) return g;
|
||||
// RA4: Sprache des HANDYS vorschlagen (nicht speichern — eine Auswahl von Hand gewinnt).
|
||||
// Nicht am PC: Dort wählt die Beraterin die Sprache ihres Gegenübers.
|
||||
var amPc = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
var liste = amPc ? [] : (navigator.languages && navigator.languages.length) ? navigator.languages : [navigator.language || ''];
|
||||
for (var i = 0; i < liste.length; i++) {
|
||||
var kurz = String(liste[i] || '').toLowerCase().split('-')[0];
|
||||
if (kurz === 'de') break;
|
||||
if (SPRACH_NAMEN[kurz]) return kurz;
|
||||
}
|
||||
return 'de';
|
||||
}
|
||||
|
||||
@@ -591,17 +623,21 @@
|
||||
// Wallet (passwortloses Guthaben) — Token + zuletzt bekannter Stand lokal.
|
||||
var WALLET_SCHLUESSEL = 'bk_wallet';
|
||||
var GUTHABEN_SCHLUESSEL = 'bk_guthaben';
|
||||
function walletToken() { return localStorage.getItem(WALLET_SCHLUESSEL) || ''; }
|
||||
function setzeWallet(t) { localStorage.setItem(WALLET_SCHLUESSEL, t); }
|
||||
function guthaben() { return parseInt(localStorage.getItem(GUTHABEN_SCHLUESSEL) || '0', 10); }
|
||||
function setzeGuthaben(n) { localStorage.setItem(GUTHABEN_SCHLUESSEL, String(Math.max(0, n | 0))); }
|
||||
function walletToken() { return speicher.get(WALLET_SCHLUESSEL) || ''; }
|
||||
function setzeWallet(t) { speicher.set(WALLET_SCHLUESSEL, t); }
|
||||
function guthaben() { return parseInt(speicher.get(GUTHABEN_SCHLUESSEL) || '0', 10); }
|
||||
function setzeGuthaben(n) { speicher.set(GUTHABEN_SCHLUESSEL, String(Math.max(0, n | 0))); }
|
||||
function hatGuthaben() { return !!walletToken() && guthaben() > 0; }
|
||||
// Einrichtungs-Code: Name der Beratungsstelle, über die das Guthaben läuft
|
||||
var EINRICHTUNG_SCHLUESSEL = 'bk_einrichtung';
|
||||
function einrichtung() { return localStorage.getItem(EINRICHTUNG_SCHLUESSEL) || ''; }
|
||||
function einrichtung() { return speicher.get(EINRICHTUNG_SCHLUESSEL) || ''; }
|
||||
// BE3: Ablaufdatum des Einrichtungs-Zugangs (kommt beim Einlösen vom Server)
|
||||
var ABLAUF_SCHLUESSEL = 'bk_einrichtung_bis';
|
||||
var CODE_SCHLUESSEL = 'bk_einrichtung_code';
|
||||
function einrichtungBis() { return speicher.get(ABLAUF_SCHLUESSEL) || ''; }
|
||||
function setzeEinrichtung(name) {
|
||||
if (name) localStorage.setItem(EINRICHTUNG_SCHLUESSEL, name);
|
||||
else localStorage.removeItem(EINRICHTUNG_SCHLUESSEL);
|
||||
if (name) speicher.set(EINRICHTUNG_SCHLUESSEL, name);
|
||||
else { speicher.remove(EINRICHTUNG_SCHLUESSEL); speicher.remove(ABLAUF_SCHLUESSEL); speicher.remove(CODE_SCHLUESSEL); }
|
||||
}
|
||||
// Handy ↔ PC: Ist dieses Gerät gerade das Handy, das für einen PC
|
||||
// fotografiert? Ein Guthaben aus dem QR-Code (w) gilt nur für diese
|
||||
@@ -613,12 +649,25 @@
|
||||
if (handyModus && handyModus.w) return handyModus.w;
|
||||
return hatGuthaben() ? walletToken() : '';
|
||||
}
|
||||
/** An den PC melden. Liefert true/false, ob es geklappt hat (Fehler werden nicht geworfen). */
|
||||
function meldeAnPc(daten, fach) {
|
||||
if (!handyModus) return Promise.resolve();
|
||||
return BKUebergabe.senden(handyModus.ue, fach || 'status', daten).catch(function () {});
|
||||
if (!handyModus) return Promise.resolve(false);
|
||||
return BKUebergabe.senden(handyModus.ue, fach || 'status', daten)
|
||||
.then(function () { return true; }, function () { return false; });
|
||||
}
|
||||
/** Das Ergebnis ist das Wichtigste: bis zu 3 Versuche mit kurzer Pause. */
|
||||
async function ergebnisAnPc(paket) {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (await meldeAnPc(paket, 'ergebnis')) return true;
|
||||
await new Promise(function (r) { setTimeout(r, 1500); });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function zeigeGuthabenBanner(n) {
|
||||
guthabenText.textContent = einrichtung() ? '🤝 Kostenlos über ' + einrichtung() + ':' : '💳 Ihr Guthaben:';
|
||||
var bis = einrichtungBis();
|
||||
guthabenText.textContent = einrichtung()
|
||||
? '🤝 Kostenlos über ' + einrichtung() + (bis ? ' (bis ' + deDatum(bis) + ')' : '') + ':'
|
||||
: '💳 Ihr Guthaben:';
|
||||
guthabenZahl.textContent = n;
|
||||
guthabenBanner.style.display = 'block';
|
||||
}
|
||||
@@ -626,10 +675,43 @@
|
||||
// Schlüsselname bewusst 'v2': setzt evtl. falsch gezählte Stände (aus der
|
||||
// Testphase) bei bestehenden Besuchern automatisch einmalig zurück.
|
||||
var SPEICHER_SCHLUESSEL = 'bk_demo_v2';
|
||||
function benutzt() { return parseInt(localStorage.getItem(SPEICHER_SCHLUESSEL) || '0', 10); }
|
||||
function setzeBenutzt(n) { localStorage.setItem(SPEICHER_SCHLUESSEL, String(n)); }
|
||||
function benutzt() { return parseInt(speicher.get(SPEICHER_SCHLUESSEL) || '0', 10); }
|
||||
function setzeBenutzt(n) { speicher.set(SPEICHER_SCHLUESSEL, String(n)); }
|
||||
|
||||
var LEER_HINWEIS = 'Die kostenlosen Analysen sind aufgebraucht oder abgelaufen. Ratsuchende: Bitte fragen Sie bei Ihrer Beratungsstelle nach. Beratungsstelle: Ein Anruf genügt, Justin Klein, 0170 4969375. Nach dem Aufladen einfach diese Seite neu laden oder den Code unten noch einmal eingeben.';
|
||||
var KEIN_ZUGANG_HANDY = 'Auf diesem Handy sind die kostenlosen Analysen verbraucht. Bitte am Computer den Code der Beratungsstelle einlösen und dann einen neuen QR-Code erzeugen.';
|
||||
function zeigeStand() {
|
||||
if (handyModus && handyModus.fertig) return;
|
||||
// Handy-Modus: nie Kaufangebot oder Code-Feld (die Beratung ist dabei)
|
||||
if (handyModus && !handyModus.w) {
|
||||
kaufBlock.style.display = 'none';
|
||||
codeTeil.style.display = 'none';
|
||||
guthabenBanner.style.display = 'none';
|
||||
if (benutzt() >= 2) {
|
||||
eingabe.style.display = 'none';
|
||||
fehlerBox.style.display = 'block';
|
||||
fehlerBox.textContent = KEIN_ZUGANG_HANDY;
|
||||
meldeAnPc({ schritt: 'fehler', code: 'kein_zugang', meldung: 'Am Handy sind die kostenlosen Analysen verbraucht.' });
|
||||
} else {
|
||||
eingabe.style.display = 'block';
|
||||
emailTeil.style.display = benutzt() === 1 ? 'block' : 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Einrichtungs-Zugang leer: kein Kaufangebot (Ratsuchende zahlen nichts),
|
||||
// Code-Feld bleibt, damit nach dem Aufladen ein neues Öffnen reicht
|
||||
if (einrichtung() && walletToken() && guthaben() <= 0 && !(handyModus && handyModus.w)) {
|
||||
eingabe.style.display = 'none';
|
||||
guthabenBanner.style.display = 'none';
|
||||
kaufBlock.style.display = 'none';
|
||||
codeTeil.style.display = 'block';
|
||||
fehlerBox.style.display = 'block';
|
||||
fehlerBox.textContent = LEER_HINWEIS;
|
||||
return;
|
||||
}
|
||||
if (einrichtung() && hatGuthaben()) {
|
||||
document.querySelector('#demo .formular-kopf').textContent = 'Antrag auf Klartext · kostenlos über ' + einrichtung();
|
||||
}
|
||||
if (handyModus && handyModus.w) {
|
||||
eingabe.style.display = 'block';
|
||||
guthabenBanner.style.display = 'none';
|
||||
@@ -657,16 +739,25 @@
|
||||
if (benutzt() >= 2) {
|
||||
eingabe.style.display = 'none';
|
||||
fehlerBox.style.display = 'block';
|
||||
fehlerBox.innerHTML = 'Ihre 2 Gratis-Analysen sind verbraucht. Kaufen Sie unten <strong>Guthaben</strong>, um hier direkt weiterzumachen — oder tragen Sie sich für die App ein.';
|
||||
fehlerBox.innerHTML = 'Ihre 2 Gratis-Analysen sind verbraucht. Haben Sie einen <strong>Code von einer Beratungsstelle</strong>? Dann geben Sie ihn direkt hier unten ein. Sonst können Sie unten <strong>Guthaben</strong> kaufen.';
|
||||
werbung.style.display = 'block';
|
||||
} else if (benutzt() === 1) {
|
||||
emailTeil.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
konsens.addEventListener('change', function () { startKnopf.disabled = !konsens.checked; });
|
||||
konsens.addEventListener('change', function () {
|
||||
if (konsens.checked && fehlerBox.dataset.art === 'konsens') fehlerBox.style.display = 'none';
|
||||
});
|
||||
startKnopf.addEventListener('click', function () {
|
||||
fehlerBox.style.display = 'none';
|
||||
if (!konsens.checked) {
|
||||
zeigeFehler('Bitte zuerst oben das Häkchen bei der Einwilligung setzen.');
|
||||
fehlerBox.dataset.art = 'konsens';
|
||||
konsens.closest('label').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
konsens.focus();
|
||||
return;
|
||||
}
|
||||
if (!walletFuerAnalyse() && benutzt() >= 1 && !emailFeld.value.trim()) {
|
||||
emailTeil.style.display = 'block';
|
||||
emailFeld.focus();
|
||||
@@ -676,10 +767,39 @@
|
||||
dateiFeld.click();
|
||||
});
|
||||
dateiFeld.addEventListener('change', function () {
|
||||
if (dateiFeld.files && dateiFeld.files[0]) starteAnalyse(dateiFeld.files[0]);
|
||||
var datei = dateiFeld.files && dateiFeld.files[0];
|
||||
// Feld leeren: sonst meldet der Browser dieselbe Datei beim zweiten Versuch nicht
|
||||
dateiFeld.value = '';
|
||||
if (datei) starteAnalyse(datei);
|
||||
});
|
||||
|
||||
/** Freundlicher Hinweis (kein Fehler) — nutzt das Feld über dem Upload. */
|
||||
function zeigeHinweis(text) {
|
||||
kaufOk.textContent = 'ℹ️ ' + text;
|
||||
kaufOk.style.display = 'block';
|
||||
}
|
||||
|
||||
function zeigeFertigBanner(angekommen, ohneUebersetzung, paket) {
|
||||
verbundenBanner.innerHTML = '';
|
||||
verbundenBanner.appendChild(document.createTextNode(angekommen
|
||||
? '✅ Fertig. Die Erklärung steht hier und auf dem Computer. Für einen weiteren Brief tippt die Beratung am Computer noch einmal auf „Lieber mit dem Handy fotografieren".'
|
||||
: '⚠️ Die Erklärung steht hier, ist aber am Computer nicht angekommen.'));
|
||||
if (ohneUebersetzung) verbundenBanner.appendChild(document.createTextNode(' Die Übersetzung hat diesmal nicht geklappt, deshalb steht die Erklärung auf Deutsch.'));
|
||||
if (!angekommen) {
|
||||
var knopf = document.createElement('button');
|
||||
knopf.type = 'button'; knopf.className = 'cta cta-zwei';
|
||||
knopf.textContent = '🔁 Noch einmal an den Computer senden';
|
||||
knopf.addEventListener('click', async function () {
|
||||
knopf.disabled = true; knopf.textContent = 'Wird gesendet …';
|
||||
var ok = await ergebnisAnPc(paket);
|
||||
zeigeFertigBanner(ok, ohneUebersetzung, paket);
|
||||
});
|
||||
verbundenBanner.appendChild(knopf);
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeFehler(text) {
|
||||
fehlerBox.dataset.art = '';
|
||||
fehlerBox.style.display = 'block';
|
||||
fehlerBox.textContent = text;
|
||||
}
|
||||
@@ -761,12 +881,21 @@
|
||||
status.style.display = 'none';
|
||||
eingabe.style.display = 'block';
|
||||
if (window.turnstile) window.turnstile.reset();
|
||||
meldeAnPc({ schritt: 'fehler', meldung: daten.meldung || '' });
|
||||
meldeAnPc({ schritt: 'fehler', code: daten.code || '', meldung: daten.meldung || '' });
|
||||
// Guthaben vom PC: Fehler nur anzeigen, am Handy nichts umstellen
|
||||
if (handyModus && handyModus.w) {
|
||||
zeigeFehler(daten.meldung || 'Etwas ist schiefgelaufen. Bitte erneut versuchen.');
|
||||
return;
|
||||
}
|
||||
// Handy ohne Zugang vom PC: Kontingent-Fehler nicht als Kaufangebot zeigen
|
||||
if (handyModus && /^(aufgebraucht|kein_guthaben|budget)$/.test(daten.code || '')) {
|
||||
if (daten.code === 'aufgebraucht') setzeBenutzt(2);
|
||||
eingabe.style.display = 'none';
|
||||
fehlerBox.style.display = 'block';
|
||||
fehlerBox.textContent = KEIN_ZUGANG_HANDY;
|
||||
meldeAnPc({ schritt: 'fehler', code: 'kein_zugang', meldung: 'Am Handy sind die kostenlosen Analysen verbraucht.' });
|
||||
return;
|
||||
}
|
||||
if (daten.code === 'email_noetig') {
|
||||
// Nur das E-Mail-Feld zeigen — NICHT als verbraucht markieren.
|
||||
// (Sonst würde ein serverseitiges "E-Mail nötig", z. B. bei
|
||||
@@ -781,11 +910,9 @@
|
||||
return;
|
||||
}
|
||||
if (daten.code === 'kontingent_leer' || daten.code === 'kontingent_abgelaufen') {
|
||||
setzeWallet('');
|
||||
vergissHandySitzung();
|
||||
setzeGuthaben(0);
|
||||
setzeEinrichtung(null);
|
||||
zeigeStand();
|
||||
zeigeFehler(daten.meldung);
|
||||
return;
|
||||
}
|
||||
if (daten.code === 'kein_guthaben') {
|
||||
@@ -801,17 +928,32 @@
|
||||
|
||||
// Token erneuern — sonst würde die nächste Analyse ein "Seite neu laden" verlangen
|
||||
if (window.turnstile) window.turnstile.reset();
|
||||
// Die Einwilligung gilt für genau diesen Brief: für den nächsten neu fragen
|
||||
konsens.checked = false;
|
||||
|
||||
// Handy fotografiert für den PC: Ergebnis verschlüsselt hinüberschicken
|
||||
await meldeAnPc({
|
||||
var paketFuerPc = {
|
||||
analyse: daten.analyse, analyse_de: daten.analyse_de, sprache: daten.sprache,
|
||||
guthaben: daten.guthaben, einrichtung: daten.einrichtung,
|
||||
}, 'ergebnis');
|
||||
};
|
||||
var amPcAngekommen = handyModus ? await ergebnisAnPc(paketFuerPc) : false;
|
||||
var analyse = bereiteVersionen(daten, false);
|
||||
if (daten.uebersetzung_fehlgeschlagen) {
|
||||
zeigeHinweis('Die Übersetzung hat diesmal nicht geklappt. Die Erklärung steht hier auf Deutsch.');
|
||||
}
|
||||
|
||||
if (handyModus && handyModus.w) {
|
||||
// Handy hat für den PC fotografiert: Verbindung ist damit erledigt.
|
||||
// (Der PC wartet nicht mehr — ein zweiter Brief käme dort nie an.)
|
||||
if (handyModus) {
|
||||
if (!handyModus.w && typeof daten.versuch === 'number') setzeBenutzt(Math.max(benutzt(), daten.versuch));
|
||||
handyModus.fertig = true;
|
||||
zeigeAnalyse(analyse);
|
||||
eingabe.style.display = 'block';
|
||||
eingabe.style.display = 'none';
|
||||
kaufBlock.style.display = 'none';
|
||||
codeTeil.style.display = 'none';
|
||||
werbung.style.display = 'none';
|
||||
vergissHandySitzung();
|
||||
zeigeFertigBanner(amPcAngekommen, daten.uebersetzung_fehlgeschlagen, paketFuerPc);
|
||||
document.getElementById('ergebnis').scrollIntoView({ behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
@@ -829,25 +971,42 @@
|
||||
status.style.display = 'none';
|
||||
eingabe.style.display = 'block';
|
||||
if (window.turnstile) window.turnstile.reset();
|
||||
meldeAnPc({ schritt: 'fehler', code: 'verbindung', meldung: 'Verbindung fehlgeschlagen.' });
|
||||
zeigeFehler('Verbindung fehlgeschlagen. Bitte prüfen Sie Ihr Internet und versuchen Sie es erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
/** Ampel wie in der App: rot ≤ 7 Tage, gelb ≤ 21, sonst grün. */
|
||||
/** Ganze Kalendertage von heute bis zum ISO-Datum (wie tageBis() in der App).
|
||||
* Früher wurde bis 12 Uhr mittags gerechnet — dann zeigte ein Termin in
|
||||
* 7 Tagen vormittags „8 Tage" und Gelb statt Rot. */
|
||||
function kalenderTage(iso) {
|
||||
var t = String(iso).split('-');
|
||||
var heute = new Date();
|
||||
var a = Date.UTC(heute.getFullYear(), heute.getMonth(), heute.getDate());
|
||||
var b = Date.UTC(+t[0], +t[1] - 1, +t[2]);
|
||||
return Math.round((b - a) / 86400000);
|
||||
}
|
||||
function ampel(analyse) {
|
||||
var daten = [];
|
||||
if (analyse.frist && analyse.frist.datum) daten.push(analyse.frist.datum);
|
||||
if (analyse.termin && analyse.termin.datum) daten.push(analyse.termin.datum);
|
||||
if (analyse.frist && isoGueltig(analyse.frist.datum)) daten.push({ iso: analyse.frist.datum, art: 'Frist' });
|
||||
if (analyse.termin && isoGueltig(analyse.termin.datum)) daten.push({ iso: analyse.termin.datum, art: 'Termin' });
|
||||
if (!daten.length) return null;
|
||||
daten.sort();
|
||||
var tage = Math.ceil((new Date(daten[0] + 'T12:00:00') - new Date()) / 86400000);
|
||||
daten.sort(function (x, y) { return x.iso < y.iso ? -1 : x.iso > y.iso ? 1 : 0; });
|
||||
// Das nächste kommende Datum zählt; nur wenn alles vorbei ist, das zuletzt vergangene
|
||||
var kommend = daten.filter(function (d) { return kalenderTage(d.iso) >= 0; });
|
||||
var erstes = kommend.length ? kommend[0] : daten[daten.length - 1];
|
||||
var tage = kalenderTage(erstes.iso);
|
||||
if (isNaN(tage)) return null;
|
||||
var farbe = tage <= 7 ? 'var(--rot)' : tage <= 21 ? 'var(--gelb)' : 'var(--gruen)';
|
||||
var punkt = tage <= 7 ? '🔴' : tage <= 21 ? '🟡' : '🟢';
|
||||
var text = tage < 0 ? 'Die Frist ist womöglich schon vorbei!' :
|
||||
tage === 0 ? 'HEUTE ist der wichtige Tag!' :
|
||||
'Noch ' + tage + ' Tage Zeit (' + daten[0].split('-').reverse().join('.') + ')';
|
||||
return { farbe: farbe, text: punkt + ' ' + text };
|
||||
var datum = deDatum(erstes.iso);
|
||||
var wann = erstes.art === 'Termin' ? 'Termin am ' + datum : 'Frist bis ' + datum;
|
||||
var text = tage < 0 ? (erstes.art === 'Termin' ? 'Der Termin war am ' + datum + '!' : 'Die Frist ist womöglich schon vorbei (' + datum + ')!') :
|
||||
tage === 0 ? 'HEUTE: ' + wann + '!' :
|
||||
tage === 1 ? 'Morgen: ' + wann :
|
||||
'Noch ' + tage + ' Tage · ' + wann;
|
||||
return { farbe: farbe, text: punkt + ' ' + text, istFrist: erstes.art === 'Frist' };
|
||||
}
|
||||
|
||||
// Eine Analyse kann in zwei Sprachen vorliegen (PC Deutsch, Handy Muttersprache).
|
||||
@@ -857,7 +1016,10 @@
|
||||
function bereiteVersionen(d, deutschZuerst) {
|
||||
letzteDaten = { analyse: d.analyse, analyse_de: d.analyse_de, sprache: d.sprache || 'de' };
|
||||
versionen = [{ code: letzteDaten.sprache, analyse: d.analyse }];
|
||||
if (d.analyse_de) versionen[deutschZuerst ? 'unshift' : 'push']({ code: 'de', analyse: d.analyse_de });
|
||||
// Zweite Fassung nur, wenn sie wirklich eine andere Sprache ist
|
||||
if (d.analyse_de && letzteDaten.sprache !== 'de') {
|
||||
versionen[deutschZuerst ? 'unshift' : 'push']({ code: 'de', analyse: d.analyse_de });
|
||||
}
|
||||
aktiveVersion = 0;
|
||||
return versionen[0].analyse;
|
||||
}
|
||||
@@ -891,9 +1053,14 @@
|
||||
if (a) {
|
||||
ampelEl.style.display = 'block';
|
||||
ampelEl.style.color = a.farbe;
|
||||
var teile = [a.text];
|
||||
if (analyse.frist && analyse.frist.aktion) teile.push(analyse.frist.aktion);
|
||||
ampelEl.textContent = teile.join(' · ');
|
||||
ampelEl.textContent = a.text;
|
||||
// Die Aufgabe gehört zur Frist — nur anhängen, wenn die Frist das früheste Datum ist.
|
||||
// KI-Text in eigenes <bdi>, damit Arabisch/Farsi richtig herum läuft.
|
||||
if (a.istFrist && analyse.frist && analyse.frist.aktion) {
|
||||
ampelEl.appendChild(document.createTextNode(' · '));
|
||||
var bdi = document.createElement('bdi'); bdi.textContent = analyse.frist.aktion;
|
||||
ampelEl.appendChild(bdi);
|
||||
}
|
||||
}
|
||||
|
||||
if (analyse.checkliste && analyse.checkliste.length) {
|
||||
@@ -910,6 +1077,7 @@
|
||||
var sum = document.createElement('summary');
|
||||
sum.textContent = b.begriff;
|
||||
var p = document.createElement('p');
|
||||
p.setAttribute('dir', 'auto');
|
||||
p.textContent = b.erklaerung;
|
||||
det.appendChild(sum); det.appendChild(p);
|
||||
return det.outerHTML;
|
||||
@@ -938,6 +1106,8 @@
|
||||
zeigeGuthabenBanner(guthabenRest);
|
||||
emailTeil.style.display = 'none';
|
||||
kaufBlock.style.display = 'none';
|
||||
} else if (einrichtung()) {
|
||||
zeigeStand(); // Leer-Hinweis mit Telefonnummer — kein Kaufangebot für Ratsuchende
|
||||
} else {
|
||||
eingabe.style.display = 'none';
|
||||
guthabenBanner.style.display = 'none';
|
||||
@@ -1055,7 +1225,11 @@
|
||||
var termin = a.termin && isoGueltig(a.termin.datum) ? a.termin : null;
|
||||
var box = document.getElementById('ergKalenderText');
|
||||
box.innerHTML = '';
|
||||
function zeile(text) { var p = document.createElement('p'); p.setAttribute('dir', 'auto'); p.textContent = text; box.appendChild(p); }
|
||||
function zeile(text, kiText) {
|
||||
var p = document.createElement('p'); p.setAttribute('dir', 'auto'); p.textContent = text;
|
||||
if (kiText) { p.appendChild(document.createTextNode(' — ')); var b = document.createElement('bdi'); b.textContent = kiText; p.appendChild(b); }
|
||||
box.appendChild(p);
|
||||
}
|
||||
|
||||
// Kein Datum im Brief: trotzdem sichtbar sagen, dass gesucht wurde.
|
||||
// Sonst weiss niemand, ob nichts da ist oder etwas kaputt ist — und
|
||||
@@ -1080,8 +1254,8 @@
|
||||
knopf.style.display = '';
|
||||
hinweis.style.display = '';
|
||||
karte.style.border = '2px solid var(--amtsblau)';
|
||||
if (frist) zeile('⚠️ Frist: ' + deDatum(frist.datum) + (frist.aktion ? ' — ' + frist.aktion : ''));
|
||||
if (termin) zeile('📅 Termin: ' + deDatum(termin.datum) + (termin.uhrzeit ? ', ' + termin.uhrzeit + ' Uhr' : '') + (termin.ort ? ' — ' + termin.ort : ''));
|
||||
if (frist) zeile('⚠️ Frist: ' + deDatum(frist.datum), frist.aktion);
|
||||
if (termin) zeile('📅 Termin: ' + deDatum(termin.datum) + (termin.uhrzeit ? ', ' + termin.uhrzeit + ' Uhr' : ''), termin.ort);
|
||||
karte.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -1130,7 +1304,8 @@
|
||||
kopf.style.marginTop = '12px';
|
||||
kopf.innerHTML = '<strong>Das können Sie am Telefon sagen:</strong>';
|
||||
liste.appendChild(kopf);
|
||||
var mitBedeutung = zielsprache() !== 'de';
|
||||
var angezeigt = versionen.length ? versionen[aktiveVersion].code : zielsprache();
|
||||
var mitBedeutung = angezeigt !== 'de';
|
||||
saetze.forEach(function (s) {
|
||||
var d = document.createElement('div'); d.className = 'anruf-satz';
|
||||
var de = document.createElement('p'); de.className = 'anruf-de';
|
||||
@@ -1231,8 +1406,13 @@
|
||||
}
|
||||
|
||||
// ---- Einrichtungs-Code einlösen ------------------------------------
|
||||
/** „mbe k7qx", „MBEK7QX", „MBE-K7QX" → „MBE-K7QX" (wie im Worker). */
|
||||
function normCode(c) {
|
||||
var roh = String(c || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
return roh.length > 4 ? roh.slice(0, -4) + '-' + roh.slice(-4) : roh;
|
||||
}
|
||||
function loeseCodeEin(code) {
|
||||
code = String(code || '').trim().toUpperCase();
|
||||
code = normCode(code);
|
||||
if (!code) { codeFeld.focus(); return; }
|
||||
if (walletToken() && guthaben() > 0 && !einrichtung() &&
|
||||
!window.confirm('Sie haben noch gekauftes Guthaben. Wenn Sie den Code einlösen, nutzt diese Seite den Code. Ihr gekauftes Guthaben bleibt über den Link in Ihrer Kauf-E-Mail erhalten. Weiter?')) {
|
||||
@@ -1257,11 +1437,11 @@
|
||||
setzeWallet(res.d.token);
|
||||
setzeGuthaben(res.d.guthaben);
|
||||
setzeEinrichtung(res.d.einrichtung);
|
||||
if (res.d.ablauf) speicher.set(ABLAUF_SCHLUESSEL, res.d.ablauf);
|
||||
speicher.set(CODE_SCHLUESSEL, code);
|
||||
zeichneHandyQr();
|
||||
zeigeStand();
|
||||
if (res.d.guthaben <= 0) {
|
||||
zeigeFehler('Die Analysen von ' + res.d.einrichtung + ' sind aufgebraucht. Bitte fragen Sie dort nach.');
|
||||
return;
|
||||
}
|
||||
if (res.d.guthaben <= 0) return; // zeigeStand zeigt den Leer-Hinweis mit Telefonnummer
|
||||
kaufOk.textContent = '✅ Code eingelöst. BehördenKlar ist für Sie kostenlos, über ' + res.d.einrichtung + '.';
|
||||
kaufOk.style.display = 'block';
|
||||
})
|
||||
@@ -1294,13 +1474,19 @@
|
||||
function zeichneHandyQr() {
|
||||
if (!pcWarten) return;
|
||||
var sprache = zielsprache();
|
||||
handyDeutschZeile.style.display = sprache === 'de' ? 'none' : 'flex';
|
||||
// d=1 auch bei „Deutsch" am PC: Stellt die Person am Handy ihre eigene
|
||||
// Sprache ein, bekommt die Beraterin trotzdem die deutsche Fassung.
|
||||
zeigeQr(handyQr, BKUebergabe.adresse({
|
||||
a: 'k', s: pcWarten.ue.id, k: pcWarten.ue.k, l: sprache,
|
||||
d: sprache !== 'de' && handyDeutsch.checked ? '1' : '',
|
||||
// „Deutsch" am PC heißt nur: nichts vorgeben — dann erkennt das Handy seine Sprache selbst
|
||||
a: 'k', s: pcWarten.ue.id, k: pcWarten.ue.k, l: sprache !== 'de' ? sprache : '',
|
||||
d: handyDeutsch.checked ? '1' : '',
|
||||
w: hatGuthaben() ? walletToken() : '',
|
||||
e: hatGuthaben() ? einrichtung() : '',
|
||||
}));
|
||||
}
|
||||
window.addEventListener('beforeunload', function (ev) {
|
||||
if (pcWarten) { ev.preventDefault(); ev.returnValue = ''; }
|
||||
});
|
||||
function stoppeWarten() {
|
||||
if (pcWarten && pcWarten.timer) clearInterval(pcWarten.timer);
|
||||
pcWarten = null;
|
||||
@@ -1345,19 +1531,54 @@
|
||||
var ue = pcWarten.ue;
|
||||
var f = await BKUebergabe.holen(ue);
|
||||
if (!pcWarten || pcWarten.ue !== ue) return; // inzwischen abgebrochen
|
||||
if ((pcWarten.fehlversuche || 0) >= 5 && !f.status) handyZustand.textContent = '⏳ Warte auf das Handy …';
|
||||
pcWarten.fehlversuche = 0;
|
||||
// Jede neue Meldung vom Handy verlängert das Warten (nicht ab QR-Anzeige rechnen)
|
||||
var schritt = f.status ? f.status.schritt + '|' + (f.status.meldung || '') : '';
|
||||
if (schritt && schritt !== pcWarten.letzterSchritt) {
|
||||
pcWarten.letzterSchritt = schritt;
|
||||
// Verbunden: großzügiger warten (Gespräch vor dem Foto kann dauern)
|
||||
pcWarten.bis = Date.now() + 3 * UEBERGABE_MS;
|
||||
if (f.status.schritt === 'analyse') pcWarten.analyseSeit = Date.now();
|
||||
}
|
||||
if (f.status && f.status.schritt === 'fehler' && /^kontingent_/.test(f.status.code || '')) {
|
||||
// Kontingent leer/abgelaufen: Warten ist sinnlos
|
||||
stoppeWarten();
|
||||
BKUebergabe.loeschen(ue);
|
||||
schliesseHandyBox();
|
||||
setzeGuthaben(0);
|
||||
zeigeStand();
|
||||
return;
|
||||
}
|
||||
if (f.status && f.status.schritt === 'fehler' && f.status.code === 'kein_zugang') {
|
||||
stoppeWarten();
|
||||
BKUebergabe.loeschen(ue);
|
||||
schliesseHandyBox();
|
||||
zeigeFehler('Am Handy sind die kostenlosen Analysen verbraucht. Bitte hier unten den Code der Beratungsstelle einlösen und dann noch einmal auf „Lieber mit dem Handy fotografieren" tippen.');
|
||||
codeTeil.style.display = 'block';
|
||||
codeTeil.open = true;
|
||||
return;
|
||||
}
|
||||
if (f.ergebnis) {
|
||||
stoppeWarten();
|
||||
BKUebergabe.loeschen(ue);
|
||||
schliesseHandyBox();
|
||||
zeigeVomHandy(f.ergebnis);
|
||||
} else if (f.status) {
|
||||
var langsam = f.status.schritt === 'analyse' && pcWarten.analyseSeit && Date.now() - pcWarten.analyseSeit > 3 * 60 * 1000;
|
||||
handyZustand.textContent =
|
||||
f.status.schritt === 'analyse' ? '⏳ Das Handy hat fotografiert. Der Brief wird erklärt …' :
|
||||
langsam ? '⚠️ Das dauert ungewöhnlich lange. Bitte am Handy nachsehen: Steht die Erklärung dort schon, auf „Noch einmal an den Computer senden" tippen. Sonst hier abbrechen und neu verbinden.' :
|
||||
f.status.schritt === 'analyse' ? '⏳ Das Handy hat fotografiert. Der Brief wird erklärt (ein bis zwei Minuten) …' :
|
||||
f.status.schritt === 'fehler' ? '⚠️ Am Handy hat es nicht geklappt: ' + (f.status.meldung || '') + ' Bitte dort noch einmal versuchen.' :
|
||||
'✅ Handy verbunden. Jetzt am Handy den Brief fotografieren.';
|
||||
}
|
||||
} catch (e) {
|
||||
// kurz keine Verbindung — beim nächsten Durchlauf erneut versuchen
|
||||
// kurz keine Verbindung — beim nächsten Durchlauf erneut versuchen,
|
||||
// aber nach mehreren Fehlschlägen sichtbar sagen, woran es liegt
|
||||
if (pcWarten) {
|
||||
pcWarten.fehlversuche = (pcWarten.fehlversuche || 0) + 1;
|
||||
if (pcWarten.fehlversuche >= 5) handyZustand.textContent = '⚠️ Keine Verbindung zum Server. Bitte das Internet dieses Computers prüfen.';
|
||||
}
|
||||
} finally {
|
||||
pruefeLaeuft = false;
|
||||
}
|
||||
@@ -1409,16 +1630,22 @@
|
||||
}
|
||||
|
||||
if (h.a === 'k') {
|
||||
handyModus = { ue: ue, w: h.w || '', d: h.d === '1' };
|
||||
handyModus = { ue: ue, w: h.w || '', d: h.d === '1', e: h.e || '' };
|
||||
if (h.l && SPRACH_NAMEN[h.l]) {
|
||||
try { localStorage.setItem(SPRACH_SCHLUESSEL, h.l); } catch (e) {}
|
||||
// nicht dauerhaft speichern: Es ist das Handy der Person, nicht der Beratung
|
||||
sitzungsSprache = h.l;
|
||||
document.getElementById('sprachWahl').value = h.l;
|
||||
}
|
||||
verbundenBanner.textContent = '🔗 Mit dem Computer verbunden. Fotografieren Sie jetzt Ihren Brief. Die Erklärung erscheint hier und auf dem Bildschirm.';
|
||||
verbundenBanner.textContent = '🔗 Mit dem Computer verbunden. Setzen Sie das Häkchen und fotografieren Sie Ihren Brief. Die Erklärung erscheint hier und auf dem Bildschirm.';
|
||||
verbundenBanner.style.display = 'block';
|
||||
handyKnopf.style.display = 'none';
|
||||
if (handyModus.e) document.querySelector('#demo .formular-kopf').textContent = 'Antrag auf Klartext · kostenlos über ' + handyModus.e;
|
||||
// „verbunden" zuerst melden — zeigeStand meldet danach ggf. „kein Zugang",
|
||||
// und das darf nicht von „verbunden" überschrieben werden
|
||||
await meldeAnPc({ schritt: 'verbunden' });
|
||||
zeigeStand();
|
||||
meldeAnPc({ schritt: 'verbunden' });
|
||||
if (handyModus.w) { kaufBlock.style.display = 'none'; codeTeil.style.display = 'none'; }
|
||||
verbundenBanner.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1438,6 +1665,8 @@
|
||||
return;
|
||||
}
|
||||
BKUebergabe.loeschen(ue);
|
||||
codeTeil.style.display = 'none';
|
||||
kaufBlock.style.display = 'none';
|
||||
verbundenBanner.textContent = '📱 Vom Computer übernommen. Tragen Sie die Frist gleich in Ihren Kalender ein, dann vergessen Sie nichts.';
|
||||
verbundenBanner.style.display = 'block';
|
||||
zeigeAnalyse(bereiteVersionen(f.ergebnis, false));
|
||||
@@ -1459,6 +1688,8 @@
|
||||
zeigeStand();
|
||||
}
|
||||
|
||||
function vergissHandySitzung() { try { sessionStorage.removeItem('bk_handy'); } catch (e) {} }
|
||||
|
||||
function pruefeRueckkehr() {
|
||||
var p = new URLSearchParams(window.location.search);
|
||||
var w = p.get('wallet');
|
||||
@@ -1493,24 +1724,43 @@
|
||||
var aktiv = zielsprache();
|
||||
Object.keys(SPRACH_NAMEN).forEach(function (c) {
|
||||
var o = document.createElement('option');
|
||||
o.value = c; o.textContent = SPRACH_NAMEN[c];
|
||||
o.value = c; o.textContent = SPRACH_NAMEN[c] + (SPRACH_DEUTSCH[c] ? ' – ' + SPRACH_DEUTSCH[c] : '');
|
||||
if (c === aktiv) o.selected = true;
|
||||
feld.appendChild(o);
|
||||
});
|
||||
feld.addEventListener('change', function () {
|
||||
try { localStorage.setItem(SPRACH_SCHLUESSEL, feld.value); } catch (e) {}
|
||||
speicher.set(SPRACH_SCHLUESSEL, feld.value);
|
||||
sitzungsSprache = null;
|
||||
if (/[?&]sprache=/.test(window.location.search) && window.history.replaceState) {
|
||||
var u = new URL(window.location.href);
|
||||
u.searchParams.delete('sprache');
|
||||
window.history.replaceState(null, document.title, u.pathname + u.search + u.hash);
|
||||
}
|
||||
zeichneHandyQr();
|
||||
});
|
||||
})();
|
||||
|
||||
rendrePakete();
|
||||
var startHash = BKUebergabe.ausAdresse();
|
||||
var startSuche = window.location.search; // vor pruefeRueckkehr, das die Adresse aufräumt
|
||||
// Handy-Verbindung übersteht ein Neuladen (z. B. wenn Android den Tab beim
|
||||
// Kamera-Wechsel neu lädt). sessionStorage gilt nur für diesen einen Tab.
|
||||
try {
|
||||
if (startHash && startHash.a === 'k') sessionStorage.setItem('bk_handy', JSON.stringify(startHash));
|
||||
else if (!startHash) { var gemerkt = sessionStorage.getItem('bk_handy'); if (gemerkt) startHash = JSON.parse(gemerkt); }
|
||||
} catch (e) {}
|
||||
if (startHash && window.history.replaceState) {
|
||||
// Schlüssel nicht im Browser-Verlauf stehen lassen
|
||||
window.history.replaceState(null, document.title, window.location.pathname + window.location.search);
|
||||
}
|
||||
pruefeRueckkehr();
|
||||
zeigeStand();
|
||||
// Einrichtungs-Zugang leer? Einmal still nachfragen — vielleicht wurde
|
||||
// inzwischen aufgeladen oder verlängert (genau ein Aufruf pro Seitenaufruf)
|
||||
if (!startHash && einrichtung() && walletToken() && guthaben() <= 0 && speicher.get(CODE_SCHLUESSEL)
|
||||
&& !/[?&](code|wallet|kauf)=/.test(startSuche)) {
|
||||
loeseCodeEin(speicher.get(CODE_SCHLUESSEL));
|
||||
}
|
||||
handyKnopf.style.display = istPc ? 'block' : 'none';
|
||||
document.getElementById('guthabenHandyKnopf').style.display = istPc ? '' : 'none';
|
||||
pruefeUebergabe(startHash);
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
|
||||
/** Alle Fächer holen und entschlüsseln: { status: {...}, ergebnis: {...} }. */
|
||||
async function holen(ue) {
|
||||
var r = await fetch(BASIS + ue.id, { cache: 'no-store' });
|
||||
// Zeitlimit, damit eine hängende Anfrage die Abfrage am PC nicht blockiert
|
||||
var signal = (w.AbortSignal && w.AbortSignal.timeout) ? w.AbortSignal.timeout(10000) : undefined;
|
||||
var r = await fetch(BASIS + ue.id, { cache: 'no-store', signal: signal });
|
||||
if (!r.ok) throw new Error('Übergabe nicht erreichbar (' + r.status + ')');
|
||||
var faecher = (await r.json()).faecher || {};
|
||||
var aus = {};
|
||||
|
||||
Reference in new issue
Block a user