generated from knso/webklar-preview-template
Web-Demo: 2 Gratis-Analysen direkt auf der Webseite
Worker (/demo auf behoerdenklar-proxy): - Turnstile-Verifikation serverseitig (Bot-Mauer) - Analyse 1 anonym (1x pro IP/30 Tage), Analyse 2 nur mit E-Mail (SHA-256-Hash mit Pepper, dauerhaft — nie Klartext gespeichert) - IP-Gesamtlimit 3/30 Tage, globales Tagesbudget 100 (~2,50 EUR Deckel) - Wegwerf-Mail-Blockliste, 5-MB-Limit, CORS nur eigene Domain - Prompt/Schema identisch zur App (ohne Antwort-Generator) Webseite: - demo.html im Papier-Design: Einwilligung, Upload mit Client-Downscaling (2000px/JPEG wie App), Ampel-Logik, Ergebnis-Karten, Warteliste-CTA mit vorbelegter E-Mail, gestaffelter Ablauf 1. anonym / 2. mit E-Mail - Startseite verlinkt Demo im Hero - Datenschutz: neuer Abschnitt 8 Gratis-Demo (Einwilligung, Turnstile, E-Mail-Hash), Folgeabschnitte neu nummeriert; Sitemap + /demo Ende-zu-Ende getestet: echte Analyse (Jobcenter-Testbrief korrekt erklärt, Ampel erkennt fruehere Absagefrist), Schummler-Szenarien (Browser-Loeschen, doppelte E-Mail) serverseitig abgefangen. Dabei entdeckt: Anthropic-Key war ungueltig -> erneuert (App-Analyse funktionierte wieder). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
9
.claude/agents/.claude.agent.md
Normal file
9
.claude/agents/.claude.agent.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: .claude
|
||||
description: Describe what this custom agent does and when to use it.
|
||||
tools: Read, Grep, Glob, Bash # specify the tools this agent can use. If not set, all enabled tools are allowed.
|
||||
---
|
||||
|
||||
<!-- Tip: Use /create-agent in chat to generate content with agent assistance -->
|
||||
|
||||
Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.
|
||||
9
.claude/agents/claude.agent.md
Normal file
9
.claude/agents/claude.agent.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: claude
|
||||
description: Describe what this custom agent does and when to use it.
|
||||
tools: Read, Grep, Glob, Bash # specify the tools this agent can use. If not set, all enabled tools are allowed.
|
||||
---
|
||||
|
||||
<!-- Tip: Use /create-agent in chat to generate content with agent assistance -->
|
||||
|
||||
Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.
|
||||
@@ -16,12 +16,18 @@
|
||||
interface Env {
|
||||
/** Geheimnis: `wrangler secret put ANTHROPIC_API_KEY` */
|
||||
ANTHROPIC_API_KEY: string;
|
||||
/** Geheimnis: `wrangler secret put TURNSTILE_SECRET_KEY` (Web-Demo) */
|
||||
TURNSTILE_SECRET_KEY: string;
|
||||
/** KV-Namespace für das Rate-Limit (Binding in wrangler.toml) */
|
||||
RATE_LIMIT: KVNamespace;
|
||||
/** Erlaubte Anfragen pro Gerät und Tag (in wrangler.toml unter [vars]) */
|
||||
TAGES_LIMIT: string;
|
||||
/** Erlaubte Anfragen pro IP-Adresse und Tag (in wrangler.toml unter [vars]) */
|
||||
TAGES_LIMIT_IP: string;
|
||||
/** Web-Demo: hartes Tagesbudget über alle Nutzer (Kosten-Deckel) */
|
||||
DEMO_TAGES_BUDGET: string;
|
||||
/** Web-Demo: max. Analysen pro IP innerhalb von 30 Tagen */
|
||||
DEMO_LIMIT_IP: string;
|
||||
}
|
||||
|
||||
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
||||
@@ -44,8 +50,290 @@ function fehler(status: number, typ: string, meldung: string): Response {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Web-Demo (/demo): 2 Gratis-Analysen direkt auf der Webseite
|
||||
// ============================================================
|
||||
//
|
||||
// Schutzschichten (jede allein wäre umgehbar, zusammen dicht genug):
|
||||
// 1. Turnstile-Token (Bot-Mauer, wird serverseitig verifiziert)
|
||||
// 2. Analyse 1 anonym: max. 1 pro IP / 30 Tage
|
||||
// 3. Analyse 2 nur mit E-Mail: pro E-Mail (Hash) genau 1 — dauerhaft
|
||||
// 4. IP-Gesamtlimit über 30 Tage (bremst Verlauf-Löscher)
|
||||
// 5. Globales Tagesbudget (harter Kosten-Deckel für den Betreiber)
|
||||
|
||||
/** Nur die eigene Webseite darf den Demo-Endpunkt aufrufen (CORS). */
|
||||
const DEMO_ORIGIN_MUSTER = /^https:\/\/([a-z0-9-]+\.)?behoerdenklar\.pages\.dev$/;
|
||||
|
||||
/** ~5 MB Datei entsprechen ~6,7 Mio. Base64-Zeichen. */
|
||||
const DEMO_MAX_BASE64 = 7_000_000;
|
||||
|
||||
/** Bekannte Wegwerf-E-Mail-Domains (kleine, pragmatische Liste). */
|
||||
const WEGWERF_DOMAINS = new Set([
|
||||
'mailinator.com', 'guerrillamail.com', '10minutemail.com', 'temp-mail.org',
|
||||
'tempmail.com', 'trashmail.com', 'yopmail.com', 'sharklasers.com',
|
||||
'getnada.com', 'dispostable.com', 'maildrop.cc', 'throwawaymail.com',
|
||||
]);
|
||||
|
||||
/** Identisch zur App (src/services/analyse.ts) — gleiche Qualität in der Demo. */
|
||||
const DEMO_SYSTEM_PROMPT = `Du bist ein Assistent, der deutschen Behördenbriefe für Privatpersonen verständlich macht. Die Nutzer sind Deutsche, die Amtsdeutsch schwer verstehen, oder Menschen mit Deutsch als Fremdsprache.
|
||||
|
||||
Deine Aufgabe:
|
||||
1. Lies den fotografierten/hochgeladenen Brief vollständig.
|
||||
2. Erkläre ihn in einfacher Alltagssprache (Sprachniveau A2/B1): kurze Sätze, keine Schachtelsätze, keine unerklärten Fachbegriffe.
|
||||
3. Extrahiere Fristen und Termine exakt. Datumsangaben immer als ISO-Format (JJJJ-MM-TT). Wenn du ein Datum nicht sicher lesen kannst, lass das Feld null — erfinde niemals Daten.
|
||||
4. Erstelle eine konkrete Checkliste, was der Nutzer tun muss.
|
||||
5. Sei sachlich und beruhigend, nicht alarmierend.
|
||||
|
||||
Wichtig: Wenn das Bild kein Behördenbrief ist oder unlesbar ist, schreibe das klar in kernaussage und erklaerung_einfach und lasse frist/termin null.`;
|
||||
|
||||
/** Schlanke Demo-Variante des Analyse-Schemas (ohne Antwort-Optionen —
|
||||
* der Antwort-Generator bleibt der App vorbehalten). */
|
||||
const DEMO_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['brieftyp', 'absender', 'kernaussage', 'erklaerung_einfach', 'fachbegriffe', 'frist', 'termin', 'checkliste'],
|
||||
properties: {
|
||||
brieftyp: { type: 'string', description: 'Kurze Kategorie des Briefs, z. B. "Einladung Jobcenter".' },
|
||||
absender: { type: 'string', description: 'Die Behörde, die den Brief geschickt hat.' },
|
||||
kernaussage: { type: 'string', description: 'Antwort auf "Was will das Amt von mir?" in 2-3 kurzen Sätzen, Sprachniveau A2.' },
|
||||
erklaerung_einfach: { type: 'string', description: 'Erklärung des gesamten Briefs in einfacher Alltagssprache (A2/B1). Kurze Sätze.' },
|
||||
fachbegriffe: {
|
||||
type: 'array',
|
||||
description: 'Fachbegriffe aus dem Brief mit einfacher Erklärung.',
|
||||
items: {
|
||||
type: 'object', additionalProperties: false, required: ['begriff', 'erklaerung'],
|
||||
properties: { begriff: { type: 'string' }, erklaerung: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
frist: {
|
||||
description: 'Frist, bis wann reagiert werden muss. null wenn keine Frist im Brief steht.',
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['datum', 'aktion'],
|
||||
properties: { datum: { type: 'string', description: 'ISO JJJJ-MM-TT' }, aktion: { type: 'string' } },
|
||||
},
|
||||
{ type: 'null' },
|
||||
],
|
||||
},
|
||||
termin: {
|
||||
description: 'Persönlicher Termin. null wenn keiner im Brief steht.',
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['datum', 'uhrzeit', 'ort'],
|
||||
properties: {
|
||||
datum: { type: 'string', description: 'ISO JJJJ-MM-TT' },
|
||||
uhrzeit: { anyOf: [{ type: 'string' }, { type: 'null' }] },
|
||||
ort: { anyOf: [{ type: 'string' }, { type: 'null' }] },
|
||||
},
|
||||
},
|
||||
{ type: 'null' },
|
||||
],
|
||||
},
|
||||
checkliste: { type: 'array', description: 'To-do-Liste. Leer wenn nichts zu tun ist.', items: { type: 'string' } },
|
||||
},
|
||||
};
|
||||
|
||||
function demoCorsHeaders(origin: string | null): Record<string, string> {
|
||||
const erlaubt = origin && DEMO_ORIGIN_MUSTER.test(origin) ? origin : 'https://behoerdenklar.pages.dev';
|
||||
return {
|
||||
'access-control-allow-origin': erlaubt,
|
||||
'access-control-allow-methods': 'POST, OPTIONS',
|
||||
'access-control-allow-headers': 'content-type',
|
||||
'access-control-max-age': '86400',
|
||||
};
|
||||
}
|
||||
|
||||
/** Fehler für die Demo-Seite: { code, meldung } + CORS. */
|
||||
function demoFehler(status: number, code: string, meldung: string, origin: string | null): Response {
|
||||
return Response.json({ code, meldung }, { status, headers: demoCorsHeaders(origin) });
|
||||
}
|
||||
|
||||
/** SHA-256-Hash der E-Mail (mit Pepper) — es wird nie Klartext gespeichert. */
|
||||
async function emailHash(email: string, pepper: string): Promise<string> {
|
||||
const daten = new TextEncoder().encode(`bk-demo|${pepper}|${email}`);
|
||||
const digest = await crypto.subtle.digest('SHA-256', daten);
|
||||
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function demoHandler(request: Request, env: Env): Promise<Response> {
|
||||
const origin = request.headers.get('origin');
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, { status: 204, headers: demoCorsHeaders(origin) });
|
||||
}
|
||||
if (request.method !== 'POST') {
|
||||
return demoFehler(405, 'methode', 'Nur POST erlaubt.', origin);
|
||||
}
|
||||
if (!origin || !DEMO_ORIGIN_MUSTER.test(origin)) {
|
||||
return demoFehler(403, 'origin', 'Aufruf nur von der BehördenKlar-Webseite erlaubt.', origin);
|
||||
}
|
||||
|
||||
let body: { bild?: string; mimeType?: string; turnstileToken?: string; email?: string };
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return demoFehler(400, 'json', 'Ungültige Anfrage.', origin);
|
||||
}
|
||||
|
||||
const { bild, mimeType, turnstileToken } = body;
|
||||
const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : null;
|
||||
|
||||
if (typeof bild !== 'string' || bild.length === 0 || bild.length > DEMO_MAX_BASE64) {
|
||||
return demoFehler(400, 'datei', 'Die Datei fehlt oder ist zu groß (max. 5 MB).', origin);
|
||||
}
|
||||
if (mimeType !== 'image/jpeg' && mimeType !== 'image/png' && mimeType !== 'application/pdf') {
|
||||
return demoFehler(400, 'datei', 'Nur Fotos (JPG/PNG) oder PDF sind möglich.', origin);
|
||||
}
|
||||
if (typeof turnstileToken !== 'string' || !turnstileToken) {
|
||||
return demoFehler(403, 'turnstile', 'Sicherheitsprüfung fehlt. Bitte Seite neu laden.', origin);
|
||||
}
|
||||
|
||||
// Schicht 1: Turnstile serverseitig verifizieren (niemals nur im Browser!)
|
||||
const ip = request.headers.get('cf-connecting-ip') ?? 'unbekannt';
|
||||
const pruefung = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ secret: env.TURNSTILE_SECRET_KEY, response: turnstileToken, remoteip: ip }),
|
||||
});
|
||||
const pruefungJson = (await pruefung.json()) as { success?: boolean };
|
||||
if (!pruefungJson.success) {
|
||||
return demoFehler(403, 'turnstile', 'Sicherheitsprüfung fehlgeschlagen. Bitte Seite neu laden und erneut versuchen.', origin);
|
||||
}
|
||||
|
||||
// E-Mail prüfen (nur für die 2. Analyse nötig)
|
||||
let mailKey: string | null = null;
|
||||
if (email) {
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) {
|
||||
return demoFehler(400, 'email', 'Bitte geben Sie eine gültige E-Mail-Adresse ein.', origin);
|
||||
}
|
||||
const domain = email.split('@')[1];
|
||||
if (WEGWERF_DOMAINS.has(domain)) {
|
||||
return demoFehler(400, 'email', 'Wegwerf-E-Mail-Adressen sind nicht möglich. Bitte nutzen Sie Ihre normale Adresse.', origin);
|
||||
}
|
||||
mailKey = `demo:mail:${await emailHash(email, env.TURNSTILE_SECRET_KEY)}`;
|
||||
}
|
||||
|
||||
// Schichten 2-5: Limits lesen
|
||||
const heute = new Date().toISOString().slice(0, 10);
|
||||
const tagKey = `demo:tag:${heute}`;
|
||||
const ipKey = `demo:ip:${ip}`;
|
||||
const anonKey = `demo:anon:${ip}`;
|
||||
const budget = parseInt(env.DEMO_TAGES_BUDGET || '100', 10);
|
||||
const ipLimit = parseInt(env.DEMO_LIMIT_IP || '3', 10);
|
||||
|
||||
const [tagWert, ipWert, anonWert, mailWert] = await Promise.all([
|
||||
env.RATE_LIMIT.get(tagKey),
|
||||
env.RATE_LIMIT.get(ipKey),
|
||||
env.RATE_LIMIT.get(anonKey),
|
||||
mailKey ? env.RATE_LIMIT.get(mailKey) : Promise.resolve(null),
|
||||
]);
|
||||
const tagZahl = parseInt(tagWert ?? '0', 10);
|
||||
const ipZahl = parseInt(ipWert ?? '0', 10);
|
||||
const anonZahl = parseInt(anonWert ?? '0', 10);
|
||||
|
||||
// Schicht 5: globales Tagesbudget (Kosten-Deckel)
|
||||
if (tagZahl >= budget) {
|
||||
return demoFehler(429, 'budget', 'Die Gratis-Demo ist für heute ausgebucht. Kommen Sie morgen wieder — oder tragen Sie sich in die Warteliste ein.', origin);
|
||||
}
|
||||
// Schicht 4: IP-Gesamtlimit (30 Tage)
|
||||
if (ipZahl >= ipLimit) {
|
||||
return demoFehler(429, 'aufgebraucht', 'Die Gratis-Analysen für diesen Anschluss sind aufgebraucht. In der App gibt es 3 weitere gratis.', origin);
|
||||
}
|
||||
// Schicht 2/3: ohne E-Mail nur 1x, pro E-Mail nur 1x (dauerhaft)
|
||||
if (!email && anonZahl >= 1) {
|
||||
return demoFehler(403, 'email_noetig', 'Ihre erste Gratis-Analyse ist verbraucht. Für die zweite geben Sie bitte Ihre E-Mail-Adresse ein.', origin);
|
||||
}
|
||||
if (mailKey && mailWert !== null) {
|
||||
return demoFehler(429, 'aufgebraucht', 'Mit dieser E-Mail-Adresse wurde die Gratis-Analyse schon genutzt. In der App gibt es 3 weitere gratis.', origin);
|
||||
}
|
||||
|
||||
// KI-Analyse — identischer Aufbau wie in der App
|
||||
const dateiBlock =
|
||||
mimeType === 'application/pdf'
|
||||
? { 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, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': env.ANTHROPIC_API_KEY,
|
||||
'anthropic-version': ANTHROPIC_VERSION,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-5',
|
||||
max_tokens: MAX_TOKENS_OBERGRENZE,
|
||||
system: DEMO_SYSTEM_PROMPT,
|
||||
output_config: { format: { type: 'json_schema', schema: DEMO_SCHEMA } },
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
dateiBlock,
|
||||
{ type: 'text', text: `Analysiere diesen Behördenbrief. Heute ist der ${heute} (wichtig für relative Datumsangaben wie "innerhalb von 14 Tagen").` },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!antwort.ok) {
|
||||
const status = antwort.status;
|
||||
// Fehlerdetails ins Worker-Log (wrangler tail) — hilft bei der Diagnose
|
||||
console.log('Demo-KI-Fehler', status, (await antwort.text()).slice(0, 500));
|
||||
if (status === 429 || status >= 500) {
|
||||
return demoFehler(503, 'ki', 'Der KI-Dienst ist gerade ausgelastet. Bitte versuchen Sie es in einer Minute erneut.', origin);
|
||||
}
|
||||
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 {
|
||||
stop_reason?: string;
|
||||
content?: { type: string; text?: string }[];
|
||||
};
|
||||
|
||||
// Erst NACH erfolgreichem KI-Aufruf zählen (Fehler kosten kein Kontingent)
|
||||
const MONAT_TTL = 60 * 60 * 24 * 30;
|
||||
const schreiben: Promise<void>[] = [
|
||||
env.RATE_LIMIT.put(tagKey, String(tagZahl + 1), { expirationTtl: 60 * 60 * 48 }),
|
||||
env.RATE_LIMIT.put(ipKey, String(ipZahl + 1), { expirationTtl: MONAT_TTL }),
|
||||
];
|
||||
if (!email) {
|
||||
schreiben.push(env.RATE_LIMIT.put(anonKey, String(anonZahl + 1), { expirationTtl: MONAT_TTL }));
|
||||
}
|
||||
if (mailKey) {
|
||||
// bewusst OHNE TTL: „pro E-Mail für immer nur 1" — es liegt nur der Hash
|
||||
schreiben.push(env.RATE_LIMIT.put(mailKey, heute));
|
||||
}
|
||||
await Promise.all(schreiben);
|
||||
|
||||
if (daten.stop_reason === 'refusal') {
|
||||
return demoFehler(422, 'inhalt', 'Die KI konnte diesen Inhalt nicht verarbeiten. Bitte prüfen Sie, ob das Foto wirklich einen Behördenbrief zeigt.', origin);
|
||||
}
|
||||
const textBlock = [...(daten.content ?? [])].reverse().find((b) => b.type === 'text');
|
||||
if (!textBlock?.text) {
|
||||
return demoFehler(502, 'ki', 'Die KI hat keine verwertbare Antwort geliefert. Bitte erneut versuchen.', origin);
|
||||
}
|
||||
|
||||
let analyse: unknown;
|
||||
try {
|
||||
analyse = JSON.parse(textBlock.text);
|
||||
} catch {
|
||||
return demoFehler(502, 'ki', 'Die Antwort war unlesbar. Bitte erneut versuchen.', origin);
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{ analyse, versuch: email ? 2 : 1 },
|
||||
{ headers: demoCorsHeaders(origin) }
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Web-Demo hat einen eigenen Pfad mit eigenen Regeln
|
||||
if (new URL(request.url).pathname === '/demo') {
|
||||
return demoHandler(request, env);
|
||||
}
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return fehler(405, 'invalid_request_error', 'Nur POST erlaubt.');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ compatibility_date = "2026-07-01"
|
||||
[vars]
|
||||
TAGES_LIMIT = "20"
|
||||
TAGES_LIMIT_IP = "100"
|
||||
# Web-Demo (/demo): hartes Tagesbudget über alle Nutzer (Kosten-Deckel,
|
||||
# 100 Analysen ≈ 2,50 €/Tag Maximalschaden) + IP-Limit über 30 Tage
|
||||
DEMO_TAGES_BUDGET = "100"
|
||||
DEMO_LIMIT_IP = "3"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "RATE_LIMIT"
|
||||
|
||||
@@ -145,7 +145,38 @@
|
||||
Ihre Adresse wird dann gelöscht.
|
||||
</p>
|
||||
|
||||
<h2>8. Reichweitenmessung (Webseite)</h2>
|
||||
<h2>8. Gratis-Demo (Webseite)</h2>
|
||||
<p>
|
||||
Auf unserer Webseite können Sie zwei Brief-Analysen kostenlos
|
||||
ausprobieren, ohne die App zu installieren. Dabei gilt:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Ihr Brief-Foto</strong> wird — nur mit Ihrer Einwilligung
|
||||
(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
|
||||
Abschnitt 3a beschrieben). Das Ergebnis sehen nur Sie in Ihrem
|
||||
Browser; wir speichern es nicht.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Zum Schutz vor Missbrauch</strong> (Art. 6 Abs. 1 lit. f
|
||||
DSGVO) prüfen wir Anfragen mit Cloudflare Turnstile (Bot-Erkennung
|
||||
durch Cloudflare, Inc., USA), zählen Analysen pro IP-Adresse
|
||||
(Speicherung max. 30 Tage) und führen ein tägliches Gesamtlimit.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Für die zweite Gratis-Analyse</strong> geben Sie Ihre
|
||||
E-Mail-Adresse an. Wir speichern sie ausschließlich als
|
||||
nicht umkehrbare Prüfsumme (Hash), um das Kontingent von zwei
|
||||
Analysen durchzusetzen — die Adresse selbst wird nicht gespeichert
|
||||
und nicht für E-Mails verwendet. In die Warteliste (Abschnitt 7)
|
||||
kommen Sie nur, wenn Sie sich dort separat eintragen.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>9. Reichweitenmessung (Webseite)</h2>
|
||||
<p>
|
||||
Auf dieser Webseite nutzen wir Cloudflare Web Analytics (Cloudflare,
|
||||
Inc., USA) — eine datenschutzfreundliche Besucherstatistik
|
||||
@@ -157,14 +188,14 @@
|
||||
DSGVO).
|
||||
</p>
|
||||
|
||||
<h2>9. Keine Rechtsberatung</h2>
|
||||
<h2>10. Keine Rechtsberatung</h2>
|
||||
<p>
|
||||
BehördenKlar erklärt Briefe verständlich, ersetzt aber keine
|
||||
Rechtsberatung. Bei rechtlich wichtigen Entscheidungen wenden Sie sich
|
||||
an eine Beratungsstelle oder eine Anwältin/einen Anwalt.
|
||||
</p>
|
||||
|
||||
<h2>10. Änderungen</h2>
|
||||
<h2>11. Änderungen</h2>
|
||||
<p>
|
||||
Wir passen diese Datenschutzerklärung an, wenn sich die App oder die
|
||||
Rechtslage ändert. Die aktuelle Fassung finden Sie immer auf dieser
|
||||
|
||||
550
webseite/demo.html
Normal file
550
webseite/demo.html
Normal file
@@ -0,0 +1,550 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gratis-Demo: Behördenbrief online erklären lassen | BehördenKlar</title>
|
||||
<meta name="description" content="Testen Sie BehördenKlar direkt im Browser: Brief fotografieren, hochladen, in einfacher Sprache erklärt bekommen. 2 Analysen gratis, ohne Konto.">
|
||||
<link rel="canonical" href="https://behoerdenklar.pages.dev/demo">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<meta property="og:site_name" content="BehördenKlar">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:locale" content="de_DE">
|
||||
<meta property="og:url" content="https://behoerdenklar.pages.dev/demo">
|
||||
<meta property="og:title" content="Probieren Sie es aus: Ihr Behördenbrief, einfach erklärt">
|
||||
<meta property="og:description" content="Direkt im Browser testen — 2 Gratis-Analysen, ohne Konto. Brief fotografieren, Klartext lesen.">
|
||||
<meta property="og:image" content="https://behoerdenklar.pages.dev/vorschau.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<style>
|
||||
:root {
|
||||
--papier: #faf7f0;
|
||||
--papier-dunkel: #f1ecdf;
|
||||
--tinte: #21211d;
|
||||
--grau: #6b675e;
|
||||
--amtsblau: #1d4e89;
|
||||
--stempelrot: #b3372b;
|
||||
--marker: #ffe66b;
|
||||
--gruen: #2e7d4f;
|
||||
--rot: #b3372b;
|
||||
--gelb: #a06b00;
|
||||
color-scheme: light;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33, 33, 29, 0.025) 34px 35px),
|
||||
var(--papier);
|
||||
color: var(--tinte);
|
||||
line-height: 1.7;
|
||||
font-size: 17px;
|
||||
}
|
||||
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
|
||||
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
|
||||
|
||||
.briefkopf {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte);
|
||||
font-size: 14px;
|
||||
}
|
||||
.briefkopf .absender { font-weight: 700; font-size: 16px; }
|
||||
.briefkopf .absender a { color: inherit; text-decoration: none; }
|
||||
.briefkopf .az { color: var(--grau); font-size: 13px; }
|
||||
|
||||
header { padding: 40px 0 8px; }
|
||||
.betreff-label {
|
||||
font-size: 13px; color: var(--grau); text-transform: uppercase;
|
||||
letter-spacing: 0.14em; margin-bottom: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: clamp(30px, 6vw, 44px);
|
||||
line-height: 1.15; font-weight: 700; letter-spacing: -0.01em;
|
||||
}
|
||||
.markiert {
|
||||
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
|
||||
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
|
||||
}
|
||||
.anrede { margin-top: 20px; font-size: 18px; max-width: 580px; }
|
||||
|
||||
section { margin-top: 44px; }
|
||||
h2 {
|
||||
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
|
||||
color: var(--grau); font-weight: 400; margin-bottom: 20px;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
}
|
||||
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
|
||||
h2 .nr { font-family: 'Courier New', ui-monospace, monospace; }
|
||||
|
||||
.antrag {
|
||||
background: #fff; border: 2px solid var(--tinte);
|
||||
box-shadow: 7px 8px 0 rgba(33, 33, 29, 0.9);
|
||||
padding: 28px 26px; position: relative;
|
||||
}
|
||||
.formular-kopf {
|
||||
font-family: 'Courier New', ui-monospace, monospace;
|
||||
font-size: 13px; text-transform: uppercase; letter-spacing: 0.1em;
|
||||
color: var(--grau); margin-bottom: 10px;
|
||||
}
|
||||
.cta {
|
||||
display: inline-block; width: 100%; padding: 17px 30px;
|
||||
background: var(--amtsblau); color: #fff; text-decoration: none;
|
||||
font-family: inherit; font-size: 18px; font-weight: 700; text-align: center;
|
||||
border: 2px solid var(--tinte);
|
||||
box-shadow: 5px 5px 0 var(--tinte);
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cta:hover:not(:disabled) { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
|
||||
.cta:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
.konsens {
|
||||
display: flex; gap: 12px; align-items: flex-start;
|
||||
font-size: 15px; color: var(--grau); margin: 14px 0 18px;
|
||||
}
|
||||
.konsens input { width: 22px; height: 22px; margin-top: 4px; flex-shrink: 0; accent-color: var(--amtsblau); }
|
||||
.konsens a { color: var(--amtsblau); }
|
||||
|
||||
.klein { font-size: 13.5px; color: var(--grau); margin-top: 12px; }
|
||||
.klein a { color: var(--grau); }
|
||||
|
||||
.status { text-align: center; padding: 26px 10px; display: none; }
|
||||
.status .gross { font-size: 20px; font-weight: 700; }
|
||||
.status .sub { color: var(--grau); margin-top: 6px; font-size: 15px; }
|
||||
.sanduhr {
|
||||
display: inline-block; font-size: 34px;
|
||||
animation: drehen 2.4s linear infinite;
|
||||
}
|
||||
@keyframes drehen { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
|
||||
.fehlerbox {
|
||||
display: none; margin-top: 16px;
|
||||
border: 2px solid var(--stempelrot); background: #fdf1ef;
|
||||
color: #7c2018; padding: 14px 16px; font-size: 16px;
|
||||
}
|
||||
|
||||
.email-teil { display: none; margin-top: 16px; }
|
||||
.wl-input {
|
||||
width: 100%; padding: 15px 16px; font-size: 17px; font-family: inherit;
|
||||
border: 2px solid var(--tinte); background: #fff; color: var(--tinte);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.wl-input::placeholder { color: #9a958a; }
|
||||
|
||||
/* Ergebnis */
|
||||
#ergebnis { display: none; }
|
||||
.brief-karte {
|
||||
background: #fff; border: 1px solid rgba(33, 33, 29, 0.28);
|
||||
padding: 22px 24px; position: relative; margin-top: 22px;
|
||||
box-shadow: 4px 5px 0 rgba(33, 33, 29, 0.08);
|
||||
}
|
||||
.karten-label {
|
||||
position: absolute; top: -12px; left: 16px;
|
||||
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
|
||||
text-transform: uppercase; letter-spacing: 0.1em;
|
||||
background: var(--gruen); color: var(--papier); padding: 3px 10px;
|
||||
}
|
||||
.karten-label.neutral { background: var(--tinte); }
|
||||
.ampelzeile {
|
||||
margin-top: 14px; padding-top: 12px;
|
||||
border-top: 1px dashed rgba(33, 33, 29, 0.3);
|
||||
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; }
|
||||
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; }
|
||||
details p { margin-top: 8px; color: var(--grau); font-size: 15.5px; }
|
||||
|
||||
.stempel {
|
||||
display: inline-block; padding: 7px 14px;
|
||||
border: 2.5px solid var(--stempelrot); color: var(--stempelrot);
|
||||
font-family: 'Courier New', ui-monospace, monospace; font-weight: 700;
|
||||
font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase;
|
||||
transform: rotate(-4deg); opacity: 0.9;
|
||||
}
|
||||
.hinweis-recht {
|
||||
margin-top: 18px; font-size: 14px; color: var(--grau);
|
||||
border-top: 1px dashed rgba(33,33,29,0.3); padding-top: 12px;
|
||||
}
|
||||
|
||||
/* Warteliste-Formular (wie Startseite) */
|
||||
.wl-form { display: flex; flex-direction: column; gap: 12px; margin-top: 4px; }
|
||||
.bonus-zeile {
|
||||
margin: 14px 0; padding: 12px 16px;
|
||||
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
|
||||
font-size: 16px;
|
||||
}
|
||||
footer { margin-top: 48px; font-size: 14.5px; color: var(--grau); }
|
||||
footer a { color: var(--grau); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<div class="briefkopf">
|
||||
<span class="absender"><a href="index.html">📬 BehördenKlar</a></span>
|
||||
<span class="az mono">Formular D-01 · Gratis-Demo</span>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<p class="betreff-label">Betreff</p>
|
||||
<h1>Probieren Sie es aus —<br><span class="markiert">mit Ihrem eigenen Brief.</span></h1>
|
||||
<p class="anrede">
|
||||
Kein Konto. Keine App-Installation. Laden Sie ein Foto Ihres
|
||||
Behördenbriefs hoch — in etwa einer Minute lesen Sie ihn in einfacher
|
||||
Sprache. <strong>2 Analysen gratis.</strong>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section id="demo">
|
||||
<h2><span class="nr">§ 1</span> Brief hochladen</h2>
|
||||
<div class="antrag">
|
||||
<p class="formular-kopf">Antrag auf Klartext · 2 Gratis-Analysen</p>
|
||||
|
||||
<div id="eingabe">
|
||||
<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.
|
||||
<a href="datenschutz.html" target="_blank">Datenschutzerklärung</a>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div id="emailTeil" class="email-teil">
|
||||
<p style="margin-bottom: 10px;">
|
||||
🎉 <strong>Ihre erste Analyse ist verbraucht.</strong> Für die
|
||||
zweite geben Sie bitte Ihre E-Mail-Adresse ein — so stellen wir
|
||||
sicher, dass jeder genau 2 Gratis-Analysen bekommt.
|
||||
</p>
|
||||
<input class="wl-input" type="email" id="email" placeholder="Ihre E-Mail-Adresse" autocomplete="email">
|
||||
<p class="klein" style="margin: 0 0 12px;">
|
||||
Die Adresse wird nur verschlüsselt (als Prüfsumme) gespeichert,
|
||||
um mitzuzählen — Sie bekommen dadurch keine E-Mails von uns.
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<div class="cf-turnstile" data-sitekey="0x4AAAAAAD8vfEq7X2EX-Krk" data-action="turnstile-spin-v1" style="margin-top: 14px;"></div>
|
||||
|
||||
<p class="klein">
|
||||
Geht mit Foto (JPG/PNG) oder PDF, max. 5 MB. Tipp: gutes Licht,
|
||||
Brief von oben, ganzer Text im Bild.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status">
|
||||
<span class="sanduhr">⏳</span>
|
||||
<p class="gross">Ihr Brief wird analysiert…</p>
|
||||
<p class="sub">Das dauert bis zu einer Minute. Bitte lassen Sie die Seite geöffnet.</p>
|
||||
</div>
|
||||
|
||||
<div class="fehlerbox" id="fehler"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="ergebnis">
|
||||
<h2><span class="nr">§ 2</span> Ihr Brief in Klartext</h2>
|
||||
|
||||
<div class="brief-karte">
|
||||
<span class="karten-label neutral" id="ergBrieftyp">Brief</span>
|
||||
<p style="margin-top: 6px;"><strong id="ergKern"></strong></p>
|
||||
<p class="ampelzeile" id="ergAmpel" style="display: none;"></p>
|
||||
</div>
|
||||
|
||||
<div class="brief-karte">
|
||||
<span class="karten-label">Einfach erklärt</span>
|
||||
<p id="ergErklaerung" style="white-space: pre-line;"></p>
|
||||
</div>
|
||||
|
||||
<div class="brief-karte" id="ergChecklisteKarte" style="display: none;">
|
||||
<span class="karten-label neutral">Das müssen Sie tun</span>
|
||||
<div id="ergCheckliste"></div>
|
||||
</div>
|
||||
|
||||
<div class="brief-karte" id="ergBegriffeKarte" style="display: none;">
|
||||
<span class="karten-label neutral">Schwere Wörter erklärt</span>
|
||||
<div id="ergBegriffe"></div>
|
||||
</div>
|
||||
|
||||
<p class="hinweis-recht">
|
||||
⚖️ BehördenKlar erklärt Briefe, ersetzt aber keine Rechtsberatung. Bei
|
||||
wichtigen Entscheidungen hilft eine Beratungsstelle oder ein Anwalt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="werbung" style="display: none;">
|
||||
<h2><span class="nr">§ 3</span> Wie geht es weiter?</h2>
|
||||
<div class="antrag">
|
||||
<p class="formular-kopf" id="restZeile">Warteliste</p>
|
||||
<h3 id="wlTitel" style="font-size: 24px; line-height: 1.3;">Das hat Ihnen geholfen?</h3>
|
||||
<p style="margin-top: 10px;">
|
||||
Die App kann noch mehr: Briefe <strong>übersetzen in 9 Sprachen</strong>,
|
||||
Fristen mit <strong>Erinnerung</strong>, fertige
|
||||
<strong>Antwort-Entwürfe</strong> — und Ihre Briefe bleiben
|
||||
verschlüsselt auf Ihrem Handy gespeichert.
|
||||
</p>
|
||||
<p class="bonus-zeile">
|
||||
🎁 Warteliste = <strong>5 Gratis-Analysen</strong> in der App statt 3.
|
||||
</p>
|
||||
<p id="nochmalZeile" style="display: none; margin-bottom: 14px;">
|
||||
<a href="#demo" style="color: var(--amtsblau); font-weight: 700;">↑ Noch einen
|
||||
Brief testen? Ihre zweite Gratis-Analyse wartet oben.</a>
|
||||
</p>
|
||||
<form class="wl-form" action="https://86550585.sibforms.com/serve/MUIFAAvOQCpFg148k-xD2Ze2PDVKKaFEIaLECCCMe9fGKd_uIurPljyn_IhTrkR0EkM_pQp_8VBzcHZ6N9sKDX5GBZi9elYKN6isXGPUUxcAgO37OKhTFZyMlJiC9ikRmPI8Q2tU2GvqP6esJ5ReGfv1htZV_OfXRIgW59P6pkeUsYKxqdXA7nWOaWxNTYz4MzzoGg7czJMJ2fxuEg==" method="POST">
|
||||
<input class="wl-input" style="margin-bottom: 0;" type="email" name="EMAIL" id="wlEmail" placeholder="Ihre E-Mail-Adresse" required autocomplete="email">
|
||||
<input type="text" name="email_address_check" value="" style="position:absolute; left:-5000px;" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||
<input type="hidden" name="locale" value="de">
|
||||
<input type="hidden" name="html_type" value="simple">
|
||||
<button class="cta" type="submit">Auf die Warteliste eintragen →</button>
|
||||
</form>
|
||||
<p class="klein">
|
||||
Eine E-Mail zum Start, keine Werbung, jederzeit abmeldbar —
|
||||
<a href="datenschutz.html">Datenschutzerklärung</a>.
|
||||
</p>
|
||||
<div style="margin-top: 16px;"><span class="stempel">Bald im App Store</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<a href="index.html">← Zur Startseite</a> ·
|
||||
<a href="impressum.html">Impressum</a> ·
|
||||
<a href="datenschutz.html">Datenschutzerklärung</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
var ENDPUNKT = 'https://behoerdenklar-proxy.behoerdenbrief.workers.dev/demo';
|
||||
var MAX_KANTE = 2000; // wie in der App: reicht für A4, spart Datenvolumen
|
||||
|
||||
var konsens = document.getElementById('konsens');
|
||||
var startKnopf = document.getElementById('startKnopf');
|
||||
var dateiFeld = document.getElementById('datei');
|
||||
var emailTeil = document.getElementById('emailTeil');
|
||||
var emailFeld = document.getElementById('email');
|
||||
var status = document.getElementById('status');
|
||||
var eingabe = document.getElementById('eingabe');
|
||||
var fehlerBox = document.getElementById('fehler');
|
||||
|
||||
function benutzt() { return parseInt(localStorage.getItem('bk_demo_benutzt') || '0', 10); }
|
||||
function setzeBenutzt(n) { localStorage.setItem('bk_demo_benutzt', String(n)); }
|
||||
|
||||
function zeigeStand() {
|
||||
// 2. Versuch: E-Mail-Feld einblenden; alles verbraucht: Eingabe sperren
|
||||
if (benutzt() >= 2) {
|
||||
eingabe.style.display = 'none';
|
||||
fehlerBox.style.display = 'block';
|
||||
fehlerBox.innerHTML = 'Ihre 2 Gratis-Analysen sind verbraucht. 🎁 In der <strong>App</strong> gibt es 3 weitere gratis — mit Warteliste sogar 5. Tragen Sie sich unten ein!';
|
||||
document.getElementById('werbung').style.display = 'block';
|
||||
} else if (benutzt() === 1) {
|
||||
emailTeil.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
konsens.addEventListener('change', function () { startKnopf.disabled = !konsens.checked; });
|
||||
startKnopf.addEventListener('click', function () {
|
||||
fehlerBox.style.display = 'none';
|
||||
if (benutzt() >= 1 && !emailFeld.value.trim()) {
|
||||
emailTeil.style.display = 'block';
|
||||
emailFeld.focus();
|
||||
zeigeFehler('Bitte geben Sie zuerst Ihre E-Mail-Adresse ein — dann wählen Sie den Brief aus.');
|
||||
return;
|
||||
}
|
||||
dateiFeld.click();
|
||||
});
|
||||
dateiFeld.addEventListener('change', function () {
|
||||
if (dateiFeld.files && dateiFeld.files[0]) starteAnalyse(dateiFeld.files[0]);
|
||||
});
|
||||
|
||||
function zeigeFehler(text) {
|
||||
fehlerBox.style.display = 'block';
|
||||
fehlerBox.textContent = text;
|
||||
}
|
||||
|
||||
function turnstileToken() {
|
||||
var feld = document.querySelector('[name="cf-turnstile-response"]');
|
||||
return feld ? feld.value : '';
|
||||
}
|
||||
|
||||
/** Foto verkleinern (max 2000px, JPEG 60%) — wie in der App. */
|
||||
function bildVerkleinern(datei) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var img = new Image();
|
||||
var url = URL.createObjectURL(datei);
|
||||
img.onload = function () {
|
||||
URL.revokeObjectURL(url);
|
||||
var f = Math.min(1, MAX_KANTE / Math.max(img.width, img.height));
|
||||
var c = document.createElement('canvas');
|
||||
c.width = Math.round(img.width * f);
|
||||
c.height = Math.round(img.height * f);
|
||||
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
|
||||
resolve(c.toDataURL('image/jpeg', 0.6).split(',')[1]);
|
||||
};
|
||||
img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('Bild unlesbar')); };
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function pdfAlsBase64(datei) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var leser = new FileReader();
|
||||
leser.onload = function () { resolve(String(leser.result).split(',')[1]); };
|
||||
leser.onerror = function () { reject(new Error('Datei unlesbar')); };
|
||||
leser.readAsDataURL(datei);
|
||||
});
|
||||
}
|
||||
|
||||
async function starteAnalyse(datei) {
|
||||
fehlerBox.style.display = 'none';
|
||||
if (datei.size > 5 * 1024 * 1024 && datei.type === 'application/pdf') {
|
||||
zeigeFehler('Die PDF-Datei ist zu groß (max. 5 MB). Tipp: Fotografieren Sie den Brief stattdessen.');
|
||||
return;
|
||||
}
|
||||
var token = turnstileToken();
|
||||
if (!token) {
|
||||
zeigeFehler('Die Sicherheitsprüfung lädt noch — bitte warten Sie 2 Sekunden und versuchen Sie es erneut.');
|
||||
return;
|
||||
}
|
||||
|
||||
eingabe.style.display = 'none';
|
||||
status.style.display = 'block';
|
||||
|
||||
try {
|
||||
var istPdf = datei.type === 'application/pdf';
|
||||
var base64 = istPdf ? await pdfAlsBase64(datei) : await bildVerkleinern(datei);
|
||||
var body = {
|
||||
bild: base64,
|
||||
mimeType: istPdf ? 'application/pdf' : 'image/jpeg',
|
||||
turnstileToken: token,
|
||||
};
|
||||
var email = emailFeld.value.trim();
|
||||
if (benutzt() >= 1 && email) body.email = email;
|
||||
|
||||
var antwort = await fetch(ENDPUNKT, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
var daten = await antwort.json();
|
||||
|
||||
if (!antwort.ok) {
|
||||
status.style.display = 'none';
|
||||
eingabe.style.display = 'block';
|
||||
if (window.turnstile) window.turnstile.reset();
|
||||
if (daten.code === 'email_noetig') {
|
||||
setzeBenutzt(Math.max(benutzt(), 1));
|
||||
emailTeil.style.display = 'block';
|
||||
emailFeld.focus();
|
||||
}
|
||||
if (daten.code === 'aufgebraucht') {
|
||||
setzeBenutzt(2);
|
||||
zeigeStand();
|
||||
return;
|
||||
}
|
||||
zeigeFehler(daten.meldung || 'Etwas ist schiefgelaufen. Bitte erneut versuchen.');
|
||||
return;
|
||||
}
|
||||
|
||||
setzeBenutzt(Math.max(benutzt(), daten.versuch));
|
||||
if (email) document.getElementById('wlEmail').value = email;
|
||||
// Token erneuern — sonst würde die 2. Analyse ein "Seite neu laden" verlangen
|
||||
if (window.turnstile) window.turnstile.reset();
|
||||
zeigeErgebnis(daten.analyse, daten.versuch);
|
||||
} catch (e) {
|
||||
status.style.display = 'none';
|
||||
eingabe.style.display = 'block';
|
||||
if (window.turnstile) window.turnstile.reset();
|
||||
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. */
|
||||
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 (!daten.length) return null;
|
||||
daten.sort();
|
||||
var tage = Math.ceil((new Date(daten[0] + 'T12:00:00') - new Date()) / 86400000);
|
||||
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 };
|
||||
}
|
||||
|
||||
function zeigeErgebnis(analyse, versuch) {
|
||||
status.style.display = 'none';
|
||||
document.getElementById('ergBrieftyp').textContent = analyse.brieftyp || 'Ihr Brief';
|
||||
document.getElementById('ergKern').textContent = analyse.kernaussage || '';
|
||||
document.getElementById('ergErklaerung').textContent = analyse.erklaerung_einfach || '';
|
||||
|
||||
var a = ampel(analyse);
|
||||
var ampelEl = document.getElementById('ergAmpel');
|
||||
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(' · ');
|
||||
}
|
||||
|
||||
if (analyse.checkliste && analyse.checkliste.length) {
|
||||
document.getElementById('ergChecklisteKarte').style.display = 'block';
|
||||
document.getElementById('ergCheckliste').innerHTML = analyse.checkliste
|
||||
.map(function (t) { var d = document.createElement('div'); d.className = 'todo'; d.textContent = t; return d.outerHTML; })
|
||||
.join('');
|
||||
}
|
||||
if (analyse.fachbegriffe && analyse.fachbegriffe.length) {
|
||||
document.getElementById('ergBegriffeKarte').style.display = 'block';
|
||||
document.getElementById('ergBegriffe').innerHTML = analyse.fachbegriffe
|
||||
.map(function (b) {
|
||||
var det = document.createElement('details');
|
||||
var sum = document.createElement('summary');
|
||||
sum.textContent = b.begriff;
|
||||
var p = document.createElement('p');
|
||||
p.textContent = b.erklaerung;
|
||||
det.appendChild(sum); det.appendChild(p);
|
||||
return det.outerHTML;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
document.getElementById('ergebnis').style.display = 'block';
|
||||
var rest = 2 - benutzt();
|
||||
document.getElementById('restZeile').textContent =
|
||||
rest > 0 ? 'Noch ' + rest + ' Gratis-Analyse übrig' : 'Ihre 2 Gratis-Analysen sind verbraucht';
|
||||
document.getElementById('wlTitel').textContent =
|
||||
rest > 0 ? 'Das hat Ihnen geholfen?' : 'Weiter geht es in der App';
|
||||
document.getElementById('werbung').style.display = 'block';
|
||||
if (rest > 0) {
|
||||
// Upload-Bereich für die 2. Analyse wieder anbieten (mit E-Mail-Feld)
|
||||
eingabe.style.display = 'block';
|
||||
emailTeil.style.display = 'block';
|
||||
document.getElementById('nochmalZeile').style.display = 'block';
|
||||
}
|
||||
document.getElementById('ergebnis').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
zeigeStand();
|
||||
})();
|
||||
</script>
|
||||
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
|
||||
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "11f017e9a707466b9151921db6125dde"}'></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -322,6 +322,10 @@
|
||||
<a class="cta" href="#anmelden">Jetzt anmelden →</a>
|
||||
<span class="stempel">Bald im App Store</span>
|
||||
</div>
|
||||
<p style="margin-top: 18px; font-size: 17px;">
|
||||
Erst ausprobieren? <a href="demo.html" style="color: var(--amtsblau); font-weight: 700;">Testen
|
||||
Sie es direkt im Browser</a> — mit Ihrem eigenen Brief, 2 Analysen gratis.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
<url><loc>https://behoerdenklar.pages.dev/uk</loc><lastmod>2026-07-23</lastmod></url>
|
||||
<url><loc>https://behoerdenklar.pages.dev/ru</loc><lastmod>2026-07-23</lastmod></url>
|
||||
<url><loc>https://behoerdenklar.pages.dev/pl</loc><lastmod>2026-07-23</lastmod></url>
|
||||
<url><loc>https://behoerdenklar.pages.dev/demo</loc><lastmod>2026-07-24</lastmod></url>
|
||||
</urlset>
|
||||
|
||||
Reference in New Issue
Block a user