Compare commits

...
15 Commits
Author SHA1 Message Date
JUSNandClaude Opus 5.5 a5d237eca5 STAND: Sprach-Durchlauf aller 27 Sprachen dokumentiert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:36:54 +02:00
JUSNandClaude Opus 5.5 808d0dbbfe STAND: zweiter Deploy live
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:32:04 +02:00
JUSNandClaude Opus 5.5 f5eea8ca66 Praesentation MBE: Datenschutz-Blatt (Traeger), Kurzanleitung Beratungs-PC, Spickzettel, STAND
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:30:33 +02:00
JUSNandClaude Opus 5.5 82235dc529 Handy-Bedienung in 27 Sprachen, Kopf im Einrichtungs-Modus, schmale Handys
- ui-texte.js: 74 Bedientexte je Sprache (uebersetzt + lektoriert), t() in
  demo.html; am PC bleibt alles deutsch, am Handy Sprache der Person,
  RTL fuer ar/fa/ur/ps, verbundene Schriften ohne Sperrsatz
- Servermeldungen am Handy nach Code in der Bediensprache
- Seitenkopf "Fuer Sie kostenlos ueber ..." statt "2 Analysen gratis"
- Turnstile passt auf schmale Handys (Abstaende, compact unter 350 px)
- Knopf fuer Mitarbeitende eindeutig: "Zugang dauerhaft auf ein Handy geben"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:30:33 +02:00
JUSNandClaude Opus 5.5 6dc0207716 Praesentation MBE: STAND.md, Handzettel/Spickzettel/Musterbrief aktualisiert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:34:15 +02:00
JUSNandClaude Opus 5.5 220e54b815 Startseite, 27 Sprachseiten, App: "in der Regel nach 30 Tagen geloescht" statt "spaetestens"
Je Sprache nur der eine Satzteil umformuliert (Uebersetzer + Gegenpruefung),
Sprachseiten mit tools/sprachseiten/build.js neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:34:15 +02:00
JUSNandClaude Opus 5.5 2af4b7ed41 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>
2026-10-22 20:34:15 +02:00
JUSNandClaude Opus 5.5 91437d37b4 Praesentation MBE: gepruefter Plan bis 29.10. (6 Perspektiven, 61 Funde)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:53:59 +02:00
JUSNandClaude Opus 5.5 a2748957ac Praesentation MBE: Ablaufplan / Spickzettel (2 Seiten A4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:27:02 +02:00
JUSNandClaude Opus 5.5 0571fc01eb App-Design wie die Webseite: Papier, Tinte, Amtsblau, Textmarker, Georgia
- theme.ts nimmt die Werte der Webseite (--papier, --tinte, --amtsblau,
  --marker, --stempelrot) statt Gold/Bronze; neue Bausteine schriftart
  (Georgia / Courier), ecke, kartenSchatten, formularRahmen, etikett
- alle Texte in Georgia, Versal-Etiketten in Schreibmaschinenschrift (grau
  wie "BETREFF"), Karten als Papierblaetter mit feiner Linie + flachem Schatten
- Knoepfe wie .cta: Amtsblau bzw. weisses Blatt, Tinten-Rahmen, harter
  Versatz-Schatten, beim Druecken rutscht der Knopf in den Schatten
- Startseite: Briefkopf "BehoerdenKlar by Webklar" mit Tinten-Linie,
  Textmarker auf "Fristen", dringendste Frist als Formular
- Tab-Leiste: Papier mit Tinten-Linie, Scan-Knopf mit hartem Schatten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:27:02 +02:00
JUSNandClaude Opus 5.5 6a7f35eb19 Praesentation MBE 29.10.: Handzettel, Musterbrief, Doku
- marketing/praesentation-mbe/handzettel: A4 zum Dalassen (Testzugang
  1 Monat, bis 100 Briefe), QR-Code wird nach Anlegen des Codes eingetragen
- musterbrief-auslaenderbehoerde: erfundene Behoerde/Person, zum Ausdrucken
  und Live-Fotografieren (Frist + Termin + Telefon + Fachbegriffe)
- Testphase im Preisblatt auf 1 Monat + Auswertungsgespraech
- FEATURES.md: Handy<->PC, Einrichtungs-Codes, lokal testen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:46:29 +02:00
JUSNandClaude Opus 5.5 cf188af080 App: QR-Uebergabe mit dem Beratungs-PC
- Brief -> "Am Computer zeigen": schickt den Brief verschluesselt auf den
  Bildschirm der Beratung (zweisprachig, wenn eine Uebersetzung da ist)
- Archiv -> "Vom Computer uebernehmen": holt das Ergebnis vom PC ins
  verschluesselte Archiv, mit Erinnerungen
- Gleiches Paeckchen-Format wie die Webseite (expo-crypto combined =
  IV||Chiffrat||Tag); Test prueft beide Richtungen gegen WebCrypto
- Link-Einfuegen als Ersatz, wenn keine Kamera da ist

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:46:29 +02:00
JUSNandClaude Opus 5.5 014737bd95 Demo-Seite: Einrichtungs-Code, Handy als Kamera fuer den PC, Mitnehmen per QR
- ?code=... oder Eingabefeld: kostenlos ueber die Beratungsstelle
- PC: "Lieber mit dem Handy fotografieren" - QR scannen, am Handy
  fotografieren, Ergebnis erscheint am PC auf Deutsch und am Handy in der
  Muttersprache (Umschalter zwischen beiden Sprachen)
- PC: "Ergebnis aufs Handy mitnehmen" und "Guthaben auch auf dem Handy"
- uebergabe.js: AES-256-GCM per WebCrypto, Schluessel nur hinter dem #
- qrcode.js (MIT, Kazuhiko Arase) selbst gehostet, keine Drittanfragen
- Datenschutzerklaerung: Abschnitte 9a (Einrichtungs-Code) und 9b (Uebergabe)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:46:29 +02:00
JUSNandClaude Opus 5.5 0eab2a0ee9 Worker: Einrichtungs-Codes, verschluesselte Uebergabe Handy<->PC, zweisprachige Analyse
- /code loest Einrichtungs-Codes fuer Beratungsstellen ein (Guthaben mit
  Name + Ablaufdatum, max. 10 Fehlversuche pro IP und Tag). Leeres oder
  abgelaufenes Kontingent sagt "fragen Sie bei <Stelle> nach" statt "kaufen".
- /uebergabe/<id>: Durable Object haelt nur Chiffrat (AES-GCM, Schluessel
  nur im QR-Code), loescht nach 15 Minuten, 50 Schreibvorgaenge, Rate-Limit.
  Durable Object statt KV, weil KV fehlende Schluessel zwischenspeichert.
- /demo mit auchDeutsch: deutsch analysieren (Sonnet), dann mit Haiku in die
  Zielsprache uebersetzen - PC zeigt Deutsch, Handy die Muttersprache.
- LOKAL_TEST=1 (.dev.vars) erlaubt localhost fuer Tests mit wrangler dev.
- tools/einrichtungs-code.js: neu | stand | aufladen | verlaengern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:46:29 +02:00
JUSNandClaude Opus 5.5 7621dd054a Outreach: Stand der Beratungsstellen-Mails aktualisiert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:13:47 +02:00
73 changed files with 8553 additions and 456 deletions

No files matched your search

+12 -12
View File
@@ -18,7 +18,7 @@ import { initialisiereBenachrichtigungen } from './src/services/erinnerungen';
import { holeConsent } from './src/services/storage';
import { AppSchutz } from './src/components/AppSchutz';
import { Ikone } from './src/components/Ikone';
import { farben, schrift } from './src/theme';
import { farben, schrift, schriftart } from './src/theme';
import { FristenScreen } from './src/screens/FristenScreen';
import { ArchivScreen } from './src/screens/ArchivScreen';
import { ConsentScreen } from './src/screens/ConsentScreen';
@@ -27,6 +27,7 @@ import { AnalyseScreen } from './src/screens/AnalyseScreen';
import { AntwortScreen } from './src/screens/AntwortScreen';
import { GlossarScreen } from './src/screens/GlossarScreen';
import { EinstellungenScreen } from './src/screens/EinstellungenScreen';
import { VerbindenScreen } from './src/screens/VerbindenScreen';
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<HauptTabParamList>();
@@ -66,10 +67,11 @@ function HauptTabs() {
headerShown: false,
tabBarActiveTintColor: farben.primaer,
tabBarInactiveTintColor: farben.textTertiaer,
tabBarLabelStyle: { fontSize: 11, fontWeight: '600' },
tabBarLabelStyle: { fontFamily: schriftart.serif, fontSize: 12, fontWeight: '700' },
tabBarStyle: {
backgroundColor: farben.flaeche,
borderTopColor: farben.rand,
backgroundColor: farben.hintergrund,
borderTopColor: farben.tinte,
borderTopWidth: 2,
height: 56 + insets.bottom,
paddingTop: 6,
paddingBottom: insets.bottom,
@@ -119,7 +121,7 @@ export default function App() {
headerStyle: { backgroundColor: farben.hintergrund },
headerShadowVisible: false,
headerTintColor: farben.primaer,
headerTitleStyle: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
headerTitleStyle: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text },
headerBackTitle: 'Zurück',
contentStyle: { backgroundColor: farben.hintergrund },
}}
@@ -131,6 +133,7 @@ export default function App() {
<Stack.Screen name="Antwort" component={AntwortScreen} options={{ title: 'Antwort erstellen' }} />
<Stack.Screen name="Glossar" component={GlossarScreen} options={{ title: 'Behörden-Glossar' }} />
<Stack.Screen name="Einstellungen" component={EinstellungenScreen} options={{ title: 'Einstellungen' }} />
<Stack.Screen name="Verbinden" component={VerbindenScreen} options={{ title: 'Verbinden' }} />
</Stack.Navigator>
</NavigationContainer>
</AppSchutz>
@@ -147,12 +150,9 @@ const styles = StyleSheet.create({
backgroundColor: farben.primaerFuellung,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 3,
borderColor: farben.flaeche,
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.2,
shadowRadius: 5,
elevation: 5,
// Wie die Knöpfe der Webseite: Amtsblau, Tinten-Rahmen, harter Schatten
borderWidth: 2,
borderColor: farben.tinte,
boxShadow: '3px 4px 0px #21211D',
},
});
+33 -2
View File
@@ -1,6 +1,6 @@
# BehördenKlar — Überblick für Claude
> Stand: 23.09.2026. Diese Datei beschreibt, **was es gibt**.
> Stand: 22.10.2026. Diese Datei beschreibt, **was es gibt**.
> Für Release-Stand und offene Aufgaben siehe die Projekt-Erinnerung.
> Für Arbeitsregeln siehe `AGENTS.md` (Expo-Docs vor Code lesen!).
@@ -92,6 +92,30 @@ Sprechzeiten — und schreibt einen **Spickzettel** mit 3–5 Sätzen für das T
**Regel im Prompt:** Kontaktdaten nur, wenn sie **wörtlich** im Brief stehen.
Getestet mit einem Brief ohne Telefonnummer → `telefon: null`, nichts erfunden.
Eine falsche Nummer wäre schlimmer als keine.
### 3c. Handy ↔ PC — für die Beratung (seit 22.10.)
Ein QR-Code verbindet zwei Geräte, **Ende-zu-Ende verschlüsselt** (AES-256-GCM).
Schlüssel und Übergabe-ID stehen hinter dem `#` der Adresse, gehen also nie an
einen Server. Der Worker hält nur Chiffrat, höchstens 15 Minuten
(Durable Object `Uebergabe`, Route `/uebergabe/<id>[/<fach>]`).
| Art (`a=`) | Wer zeigt den QR | Was passiert |
|---|---|---|
| `k` Kamera | PC: „Lieber mit dem Handy fotografieren" | Handy fotografiert, Ergebnis erscheint am PC **auf Deutsch** und am Handy **in der Muttersprache** (`auchDeutsch` → Worker analysiert deutsch und übersetzt mit Haiku, kostet 1 Analyse). Guthaben des PCs gilt am Handy nur für diese Sitzung. |
| `m` Mitnehmen | PC: „Ergebnis aufs Handy mitnehmen" | Ergebnis landet auf dem Handy (Browser) bzw. im App-Archiv, einmal abholbar |
| `g` Guthaben | PC: „Auch auf dem Handy nutzen" | Guthaben/Einrichtungs-Zugang dauerhaft aufs Handy (Browser) |
Gleiches Päckchen-Format in `webseite/uebergabe.js` und `src/services/uebergabe.ts`
(IV‖Chiffrat‖Tag, base64url) — Test: `src/utils/__tests__/uebergabe.test.ts`.
**App:** Brief → „Am Computer zeigen" (`a=k`), Archiv → „Vom Computer übernehmen"
(`a=m`), Bildschirm `VerbindenScreen` mit QR-Scanner und Link-Einfügen als Ersatz.
### 3d. Einrichtungs-Codes für Beratungsstellen (seit 22.10.)
Die Einrichtung zahlt, Ratsuchende nicht. Ein Code (z. B. `MBE-K7QX`) zeigt auf
ein Guthaben wie beim Kauf, mit Name und Ablaufdatum. Einlösen per Link
`behoerdenklar.de/demo?code=…` oder Eingabefeld; Banner „Kostenlos über …".
Verwalten mit `node tools/einrichtungs-code.js neu|stand|aufladen|verlaengern`.
Falsche Codes: max. 10 pro IP und Tag.
### 4. Ampel statt Datum
Rot ≤ 7 Tage, gelb ≤ 21, sonst grün. Die Startseite sortiert nach
**Dringlichkeit**, nicht nach Eingang — man sieht sofort, was brennt.
@@ -207,6 +231,8 @@ und Sprachwähler in der handgemachten `index.html` nach und schreibt die
| `/kaufen` | Stripe-Bezahlseite erzeugen |
| `/kauf-status` | Nach Rückkehr: Zahlung prüfen, Guthaben freischalten |
| `/stripe-webhook` | Sicherungsnetz + Wiederherstellungs-Mail |
| `/code` | Einrichtungs-Code einlösen → signiertes Guthaben-Token |
| `/uebergabe/<id>` | Handy ↔ PC: verschlüsselte Päckchen (Durable Object, 15 Min.) |
### Bezahlung (LIVE, echtes Geld)
- **Guthaben statt Abo:** 5 Analysen 2,99 € · 15 Analysen 6,99 €
@@ -247,11 +273,16 @@ rotiert werden.
```bash
npm run pruefung # Gesundheits-Check — nach JEDEM Deploy
npm test # 35 Tests
npm test # 43 Tests
npx tsc --noEmit # Typen
node tools/sprachseiten/build.js # Sprachseiten neu bauen
```
**Lokal testen** (ohne live zu gehen): in `/Users/homefolder/Behörden_App/.claude/launch.json`
`proxy-worker` (Port 8799, `.dev.vars` mit `LOKAL_TEST=1` und Turnstile-Testschlüssel)
+ `webseite` (Port 4173) starten, dann `http://localhost:4173/demo.html`. Auf
localhost nimmt die Seite automatisch den lokalen Worker und den Test-Sitekey.
**Deploy:**
```bash
cd proxy && npx wrangler deploy # Worker
+183 -40
View File
@@ -20,39 +20,59 @@ selbst ausprobieren. Das überzeugt mehr als jedes PDF.
| **MBE Donnersberg** (Migrationsberatung, Kirchheimbolanden) | mbe@fid-donnersberg.de | 06352 7897955 | Erstmail 17.07. · Nachfass: ____ | ____ | |
| **Diakonie Donnersbergkreis** (Soziale Lebensberatung, Kirchheimbolanden) | slb.kibo@diakonie-pfalz.de | *auf diakonie-pfalz.de nachsehen* | ____ | ____ | |
| **Caritas-Zentrum Kaiserslautern** (Migrationsberatung, Engelsgasse 1) | Migration.Kaiserslautern@caritas-speyer.de | 0631 36120-222 | ____ | ____ | |
| **DRK Kaiserslautern-Stadt** (Migrationsberatung, Barbarossastraße 27) — Birgit Steinmann, Leitung · nutzt schon mbeon | b.steinmann@kv-kls.drk.de | 0631 80093-140 | ____ | ____ | |
---
## Mail 1 — MBE Donnersberg *(Nachfass-Mail — die haben im Juli schon eine bekommen)*
**Von:** justin@webklar.com *(direkt aus dem Webklar-Postfach senden)*
**An:** mbe@fid-donnersberg.de
**Betreff:** Nachtrag zu meiner Mail vom Juli: BehördenKlar jetzt im Browser nutzbar
**Betreff:** Nachtrag zu meiner Mail vom Juli: BehördenKlar jetzt ausprobierbar
> Bewusst kürzer als die Erstmail. Die Juli-Mail nannte noch „iPhone-App", ein Abo
> (2,99 €/Monat) und Anhänge — beides stimmt so nicht mehr, deshalb Preis ehrlich
> korrigieren (Pakete statt Abo). Nach Absenden: ANRUFEN (06352 7897955), die Mail
> ist schon 2½ Monate her.
Sehr geehrtes Team der Migrationsberatung,
im Juli hatte ich Ihnen von BehördenKlar geschrieben, einem Werkzeug, das
Behördenbriefe in einfacher Sprache erklärt. Seitdem hat sich viel getan, deshalb
melde ich mich noch einmal kurz.
im Juli hatte ich Ihnen von BehördenKlar geschrieben. Seitdem hat sich viel getan,
deshalb melde ich mich noch einmal kurz.
Man kann es jetzt direkt im Browser nutzen — auf jedem Handy, ohne App und ohne Konto:
**https://behoerdenklar.de/demo**
Schwierige Briefe vom Amt kenne ich von klein auf: Meine Mutter ist selbst nach
Deutschland gekommen, und solche Briefe gehörten bei uns zum Alltag. Deshalb arbeite
ich an einer App, die Behördenbriefe in einfacher Sprache erklärt, auf Wunsch in
einer von 27 Sprachen, darunter Arabisch, Farsi, Paschtu, Kurdisch, Tigrinya, Türkisch
und Ukrainisch.
- Brief fotografieren → Erklärung in einfachem Deutsch **oder in einer von 27
Sprachen**, u. a. Arabisch, Farsi, Kurdisch, Tigrinya und Ukrainisch. Die deutschen
Fachbegriffe bleiben stehen, damit man sie im Brief wiederfindet.
- **Frist und Termin mit einem Tipp in den Handy-Kalender**, mit Erinnerung vorher.
- **Anruf-Hilfe:** Telefonnummer und Aktenzeichen aus dem Brief, dazu ein kurzer
Spickzettel, was man am Telefon sagen kann.
Die App ist noch in Arbeit, aber die Browser-Version läuft schon, auf jedem Handy,
ohne Konto und ohne Installation: https://behoerdenklar.de/demo
Es soll Ihre Beratung nicht ersetzen — eher dafür sorgen, dass Menschen vorbereitet
zu Ihnen kommen und keine Frist verpassen.
- Die deutschen Fachbegriffe bleiben sichtbar, damit man sie im Brief wiederfindet.
- Fristen und Termine kommen mit einem Tipp in den Handy-Kalender, damit keine Frist
untergeht.
- Für Anrufe beim Amt gibt es einen Spickzettel mit der Bedeutung in der eigenen
Sprache.
Dürfte ich es Ihnen einmal 10 Minuten zeigen? Gern bei Ihnen in Kirchheimbolanden
oder am Telefon — ich richte mich nach Ihrer Zeit.
Es soll Ihre Beratung nicht ersetzen, sondern dafür sorgen, dass Menschen vorbereitet
zu Ihnen kommen.
Eine Korrektur zu meiner Juli-Mail: Es wird kein Abo geben. Zwei Briefe sind gratis,
danach gibt es kleine Pakete ab 2,99 €, weil die KI pro Brief Geld kostet. Für
Beratungsstellen möchte ich eine eigene, deutlich günstigere Lösung anbieten, bei der
die Ratsuchenden selbst nichts bezahlen. Ums große Geld geht es mir dabei nicht. Wie
das für Sie aussehen könnte, würde ich gern gemeinsam mit Ihnen überlegen.
Dürfte ich es Ihnen 10 Minuten zeigen, gern bei Ihnen in Kirchheimbolanden oder am
Telefon? Mich interessiert vor allem Ihre ehrliche Einschätzung, solange die App noch
entsteht: Was würde Ihren Ratsuchenden wirklich helfen, und was fehlt noch?
Mit freundlichen Grüßen
Justin Klein
Webklar GbR · Winnweiler · behoerdenbriefhelfer@gmail.com
Webklar GbR · Winnweiler
Tel. 0170 4969375
justin@webklar.com
---
@@ -92,35 +112,136 @@ Webklar GbR · Winnweiler · behoerdenbriefhelfer@gmail.com
## Mail 3 — Caritas Kaiserslautern *(Erstmail)*
**Von:** justin@webklar.com *(direkt aus dem Webklar-Postfach senden)*
**An:** Migration.Kaiserslautern@caritas-speyer.de
**Betreff:** Behördenbriefe in 27 Sprachen erklärt — 10 Minuten für Ihre Einschätzung?
**Betreff:** Behördenbriefe in 27 Sprachen erklärt – als Ergänzung zu Ihrer Beratung
> Aufbau wie Mail 4 (DRK). Kein mbeon-Absatz, weil unbekannt ist, ob Caritas es nutzt.
> Steht auf caritas.de oder der Zentrum-Seite ein Name der Leitung: Anrede
> persönlich machen („Sehr geehrte Frau …") — wirkt deutlich besser als „Team".
Sehr geehrtes Team der Migrationsberatung,
mein Name ist Justin Klein, ich komme aus Winnweiler. Ich habe BehördenKlar
entwickelt: eine Webseite, die Behördenbriefe in einfacher Sprache erklärt — auf
Wunsch in einer von 27 Sprachen, darunter Arabisch, Farsi/Dari, Paschtu, Kurdisch,
Tigrinya, Somali und Ukrainisch.
mein Name ist Justin Klein, ich komme aus Winnweiler. Schwierige Briefe vom Amt kenne
ich von klein auf: Meine Mutter ist selbst nach Deutschland gekommen, und solche
Briefe gehörten bei uns zum Alltag. Ich weiß, wie viel Sorge ein Brief machen kann,
den man nicht versteht.
Sie funktioniert direkt im Browser, auf jedem Handy, ohne App und ohne Konto:
**https://behoerdenklar.de/demo**
Deshalb arbeite ich an BehördenKlar: einer App, die Behördenbriefe in einfacher
Sprache erklärt, auf Wunsch in einer von 27 Sprachen, darunter Arabisch,
Farsi, Paschtu, Kurdisch, Tigrinya, Türkisch und Ukrainisch.
- Brief fotografieren → Erklärung in der eigenen Sprache. Die **deutschen
Fachbegriffe bleiben sichtbar**, damit man sie im Brief wiederfindet.
- **Frist und Termin mit einem Tipp in den Handy-Kalender**, mit Erinnerung vorher.
- **Anruf-Hilfe:** Telefonnummer und Aktenzeichen aus dem Brief und ein Spickzettel
für das Telefonat — jeder Satz auf Deutsch, darunter die Bedeutung in der eigenen
Sprache.
Ich habe mir dabei viele Gedanken gemacht, was den Menschen, die zu Ihnen kommen,
wirklich hilft:
Es ersetzt Ihre Beratung nicht. Es soll helfen, dass Ihre Klientinnen und Klienten
vorbereitet zu Ihnen kommen und zwischen den Terminen keine Frist verpassen.
- Es braucht kein Konto, damit niemand schon an einer Anmeldung scheitert, und die
Browser-Version auch keine Installation.
- Die deutschen Fachbegriffe bleiben sichtbar, damit man sie im Brief wiederfindet
und gezielt nachfragen kann.
- Fristen und Termine kommen mit einem Tipp in den Handy-Kalender, damit keine Frist
untergeht.
- Für Anrufe beim Amt gibt es einen Spickzettel: jeder Satz auf Deutsch, darunter die
Bedeutung in der eigenen Sprache.
- Unter jeder Erklärung steht der Hinweis, dass bei wichtigen Entscheidungen eine
Beratungsstelle hilft.
- Die Briefe werden nicht dauerhaft gespeichert.
Die App ist noch in Arbeit, aber die Browser-Version können Sie schon jetzt direkt
selbst ausprobieren: https://behoerdenklar.de/demo
BehördenKlar soll Ihre Beratung nicht ersetzen, sondern davor ansetzen: Wer einen
Brief bekommt, versteht sofort, worum es geht und ob es eilt, auch abends oder am
Wochenende. Mit den wichtigen Fragen kommt man dann vorbereitet zu Ihnen und
verpasst zwischen den Terminen keine Frist.
Ums große Geld geht es mir dabei nicht, aber die KI, die die Briefe erklärt, kostet
mich pro Brief Geld. Deshalb ist die Browser-Version für Privatleute nicht ganz kostenlos: Zwei
Briefe sind gratis, danach gibt es kleine Pakete ab 2,99 €. Für Beratungsstellen
möchte ich eine eigene, deutlich günstigere Lösung anbieten, bei der die Ratsuchenden
selbst nichts bezahlen. Wie das für Sie aussehen könnte, würde ich gern gemeinsam mit
Ihnen überlegen.
Dürfte ich es Ihnen 10 Minuten zeigen, gern bei Ihnen in der Engelsgasse oder am
Telefon? Ich richte mich nach Ihrer Zeit.
Telefon? Mich interessiert vor allem Ihre ehrliche Einschätzung, solange die App noch
entsteht: Was würde Ihren Ratsuchenden wirklich helfen, und was fehlt noch?
Mit freundlichen Grüßen
Justin Klein
Webklar GbR · Winnweiler · behoerdenbriefhelfer@gmail.com
Webklar GbR · Winnweiler
Tel. 0170 4969375
justin@webklar.com
---
## Mail 4 — DRK Kaiserslautern-Stadt *(Erstmail, persönlich an die Leitung)*
**Von:** justin@webklar.com *(direkt aus dem Webklar-Postfach senden, nicht über
Gmail „Senden als" — webklar.com hat DMARC `p=quarantine`, sonst droht Spam-Ordner)*
**An:** b.steinmann@kv-kls.drk.de
**Betreff:** Behördenbriefe in 27 Sprachen erklärt – als Ergänzung zu mbeon
> Hintergrund: Das DRK KL berät schon über **mbeon** (Online-Beratung der MBE, für
> Ratsuchende kostenlos). Deshalb als *Ergänzung* positionieren, nie als Konkurrenz.
> Ton: helfend, persönlich (Mutter), Gedanken zeigen statt behaupten (jeder Punkt
> mit „damit …"). „Ums große Geld geht es mir nicht" nur EINMAL — öfter wirkt es wie
> das Gegenteil. Preise: Webseite = Privatleute, Beratungsstellen deutlich günstiger,
> Ratsuchende zahlen nichts, KEINE Zahlen (siehe „Preise für Beratungsstellen").
> Alle Funktions-Aussagen am 30.09. gegen demo.html + Demo-Prompt geprüft.
> KI + Datenschutz MÜSSEN in der Mail stehen (kurz, Details erst im Gespräch): Die Demo-Seite nennt vor dem Hochladen
> „KI-Dienst Anthropic (USA)" — fehlt das in der Mail, wirkt es verschwiegen.
> Fragt sie „Was kostet Sie ein Brief?": ehrlich antworten (ein paar Cent KI +
> Zahlungsgebühren + Server + Arbeitszeit), nicht ausweichen.
Sehr geehrte Frau Steinmann,
mein Name ist Justin Klein, ich komme aus Winnweiler. Schwierige Briefe vom Amt kenne
ich von klein auf: Meine Mutter ist selbst nach Deutschland gekommen, und solche
Briefe gehörten bei uns zum Alltag. Ich weiß, wie viel Sorge ein Brief machen kann,
den man nicht versteht.
Deshalb arbeite ich an BehördenKlar: einer App, die Behördenbriefe in einfacher
Sprache erklärt, auf Wunsch in einer von 27 Sprachen, darunter Arabisch,
Farsi, Paschtu, Kurdisch, Tigrinya, Türkisch und Ukrainisch.
Ich habe mir dabei viele Gedanken gemacht, was den Menschen, die zu Ihnen kommen,
wirklich hilft:
- Es braucht kein Konto, damit niemand schon an einer Anmeldung scheitert, und die
Browser-Version auch keine Installation.
- Die deutschen Fachbegriffe bleiben sichtbar, damit man sie im Brief wiederfindet
und gezielt nachfragen kann.
- Fristen und Termine kommen mit einem Tipp in den Handy-Kalender, damit keine Frist
untergeht.
- Für Anrufe beim Amt gibt es einen Spickzettel: jeder Satz auf Deutsch, darunter die
Bedeutung in der eigenen Sprache.
- Unter jeder Erklärung steht der Hinweis, dass bei wichtigen Entscheidungen eine
Beratungsstelle hilft.
- Die Briefe werden nicht dauerhaft gespeichert.
Die App ist noch in Arbeit, aber die Browser-Version können Sie schon jetzt direkt
selbst ausprobieren: https://behoerdenklar.de/demo
Ich habe gesehen, dass Sie auch über mbeon beraten. BehördenKlar soll das nicht
ersetzen, sondern davor ansetzen: Wer einen Brief bekommt, versteht sofort, worum es
geht und ob es eilt, auch abends oder am Wochenende. Mit den wirklich wichtigen
Fragen kommt man dann vorbereitet zu Ihnen, persönlich oder über mbeon.
Ums große Geld geht es mir dabei nicht, aber die KI, die die Briefe erklärt, kostet
mich pro Brief Geld.
Deshalb ist die Browser-Version für Privatleute nicht ganz kostenlos: Zwei Briefe sind gratis,
danach gibt es kleine Pakete ab 2,99 €. Für Beratungsstellen möchte ich eine eigene,
deutlich günstigere Lösung anbieten, bei der die Ratsuchenden selbst nichts bezahlen.
Wie das für Sie aussehen könnte, würde ich gern gemeinsam mit Ihnen überlegen.
Dürfte ich es Ihnen 10 Minuten zeigen, gern bei Ihnen in der Barbarossastraße oder am
Telefon? Mich interessiert vor allem Ihre ehrliche Einschätzung, solange die App noch
entsteht: Was würde Ihren Ratsuchenden wirklich helfen, und was fehlt noch?
Mit freundlichen Grüßen
Justin Klein
Webklar GbR · Winnweiler
Tel. 0170 4969375
justin@webklar.com
---
@@ -164,9 +285,10 @@ fehlt noch" ist für dich schon ein Erfolg.
> erfinden wir nicht: Steht nichts im Brief, sagen wir das.
**„Was kostet das für unsere Klienten?"**
> Zwei Briefe sind kostenlos. Danach 2,99 € für fünf Briefe oder 6,99 € für
> fünfzehn — kein Abo. Wenn Sie eine Idee haben, wie das für Ihre Einrichtung
> einfacher geht, bin ich offen dafür.
> Für Privatleute: zwei Briefe kostenlos, danach 2,99 € für fünf oder 6,99 € für
> fünfzehn — kein Abo. Für Beratungsstellen soll es anders laufen: Die Einrichtung
> bekommt ein Briefkontingent zu einem deutlich günstigeren Preis, und die
> Ratsuchenden zahlen gar nichts. *(Zahlen: siehe „Preise für Beratungsstellen".)*
**„Ersetzt das unsere Beratung?"**
> Nein. Bei rechtlich wichtigen Entscheidungen verweisen wir ausdrücklich auf
@@ -187,6 +309,27 @@ fehlt noch" ist für dich schon ein Erfolg.
4. Darf ich Ihre Stelle auf meiner Seite als Beratungsstelle nennen?
*(Und umgekehrt: Würden Sie auf BehördenKlar verlinken? — Das hilft enorm bei Google.)*
**Nicht in die Mail, erst im Gespräch:** die Idee eines Guthabens, das die
Einrichtung für ihre Klienten bereitstellt. Das gibt es technisch noch nicht —
also nichts versprechen, nur fragen, ob das interessant wäre.
---
## Preise für Beratungsstellen — nur im Gespräch, nie in die Mail *(Vorschlag 30.09.)*
**Grundidee:** Die Einrichtung bezahlt, die Ratsuchenden zahlen nichts. Die
Einrichtung kauft vorher ein **Briefkontingent** — jeder Brief ist bezahlt, bevor er
genutzt wird. Mehr Nutzer heißt mehr Einnahmen, nie mehr Verlust.
| | Preis | pro Brief |
|---|---|---|
| Deine Kosten (eigene Rechnung ~2,5 ct, mit Puffer für fremde Sprachen) | — | ~5 ct |
| Privatleute (Webseite) | 2,99 € für 5 · 6,99 € für 15 | 47–60 ct |
| **Test** für eine Beratungsstelle, 1 Monat (verlängerbar) + Auswertungsgespräch | bis 100 Briefe gratis | kostet dich max. ~5 € |
| **Kleines Paket** | 100 Briefe · 20 € | 20 ct |
| **Großes Paket** | 500 Briefe · 75 € | 15 ct |
| **Untergrenze beim Verhandeln** | — | 10 ct |
- **Nie „unbegrenzt"** anbieten — immer eine feste Briefzahl. Wenn sie „Lizenz"
sagen: „Jahreslizenz mit X Briefen" ist dasselbe.
- **Keine Mehrwertsteuer** (Kleinunternehmer §19) — der Preis ist der Endpreis.
- Echte Kosten einmal in der Anthropic-Konsole unter „Usage" nachsehen, dann ist
die Untergrenze sicher.
- Den **Einrichtungs-Code gibt es technisch noch nicht** — nichts mit Datum
versprechen. Bauen, sobald eine Stelle ernsthaft Ja sagt.
+146
View File
@@ -0,0 +1,146 @@
# Stand: Präsentation MBE Kirchheimbolanden (Do 29.10.2026)
> Diese Datei halte ich (Claude) bei jedem Schritt aktuell.
> Zeichen: ✅ erledigt · 🔄 läuft gerade · ⏳ als Nächstes · 👤 **machst du** · 💤 nach dem Termin
>
> Ausführlicher Prüfbericht mit allen 61 Funden: [plan-bis-29-10.md](plan-bis-29-10.md)
**Zuletzt aktualisiert:** Do 22.10.2026, nachts: **alles live**, inklusive Handy-Bedienung in 27 Sprachen. Nächster Schritt: deine Generalprobe.
---
## Wo wir stehen (in einem Satz)
**Live auf behoerdenklar.de** (Gesundheits-Check 10/10 grün). Dein Vorführ-Code: **`VORF-2EDF`**
(30 Briefe, bis 30.11.). Morgen: Generalprobe 1 mit deinem Laptop und Handy.
---
## 1. Fertig gebaut (heute, 22.10.)
### Für die Beratungsstellen
- ✅ **Einrichtungs-Code:** Die MBE bekommt einen Code wie `MBE-K7QX`, damit ist es für ihre Ratsuchenden kostenlos.
Das Einlösen geht per QR-Code oder über ein Eingabefeld, auch mit Tippfehlern wie „mbe k7qx“.
- ✅ **Werkzeug für Codes:** anlegen, Stand abfragen, aufladen, verlängern. Der Stand zeigt auch, **wie viele Briefe pro Woche und in welcher Sprache** genutzt wurden (für das Auswertungsgespräch).
- ✅ **Kontingent leer oder abgelaufen:** kein Kaufangebot, stattdessen ein Hinweis mit deiner Telefonnummer.
### Handy ↔ PC (verschlüsselt per QR-Code)
- ✅ **Handy als Kamera für den PC:** Die Erklärung erscheint am PC auf **Deutsch** und am Handy in der **Muttersprache**.
- ✅ **Ergebnis aufs Handy mitnehmen** (QR-Code vom PC).
- ✅ **Guthaben/Zugang aufs Handy holen** (QR-Code vom PC).
- ✅ **App:** „Am Computer zeigen“ und „Vom Computer übernehmen“, im iPhone-Simulator getestet.
### Korrekturen aus der großen Prüfung (6 Blickwinkel)
- ✅ Die Ampel zeigt am 29.10. **Rot** statt Gelb (sie rechnet jetzt in Kalendertagen).
- ✅ Kein „Deutsch | Deutsch“ mehr, wenn die Übersetzung scheitert.
- ✅ Die Beraterin bekommt **immer** eine deutsche Fassung.
- ✅ Das Code-Feld bleibt sichtbar, auch nach den 2 Gratis-Analysen.
- ✅ Das Handy sagt nach dem ersten Brief klar, wie es weitergeht.
- ✅ Die Einwilligung gilt **pro Brief** und nennt Gesundheit, Herkunft und Aufenthalt.
- ✅ Arabisch und Farsi werden richtig herum angezeigt (rechts nach links).
- ✅ Der PC meldet Probleme („dauert ungewöhnlich lange“, „keine Verbindung“), statt endlos zu warten.
- ✅ Die Seite funktioniert auch in Browsern, die das Speichern verbieten.
- ✅ Größerer QR-Code und eine Sprachliste mit deutschem Zusatz („العربية – Arabisch“).
### Material
- ✅ [Handzettel](handzettel.pdf) (A4 zum Dalassen). Der **QR-Code zeigt noch nicht auf den MBE-Code**, siehe Di 27.10.
- ✅ [Musterbrief Ausländerbehörde](musterbrief-auslaenderbehoerde.pdf) (Termin 03.11., damit die Ampel rot ist)
- ✅ [Ablaufplan / Spickzettel](ablaufplan.pdf) (2 Seiten, nur für dich)
- ✅ Datenschutzerklärung ergänzt (Einrichtungs-Code, Übergabe, Nutzungszähler, „in der Regel 30 Tage“)
### Design
- ✅ Die App sieht jetzt aus wie die Webseite (Papier, Amtsblau, Textmarker, Georgia-Schrift).
---
## 2. Gegenprüfung erledigt
- ✅ **Zweite Prüfung der Korrekturen:** 17 Funde, alle behoben und lokal getestet:
- Bei der allerletzten Analyse einer Beratungsstelle erscheint kein Kaufangebot mehr, sondern dein Hinweis mit Telefonnummer.
- Nach dem Aufladen oder Verlängern reicht ein Neuladen der Seite.
- Das Handy erkennt seine eigene Sprache, wenn am PC „Deutsch“ steht, und speichert auf dem Handy der Person nichts dauerhaft.
- Handy ohne Zugang: klarer Hinweis auf beiden Geräten statt einer Sackgasse.
- Ein Neuladen am Handy beendet die Verbindung zum PC nicht mehr.
- Kommt das Ergebnis nicht beim PC an, sagt das Handy das ehrlich und bietet „Noch einmal an den Computer senden“.
- Der PC wartet geduldig, solange das Handy verbunden ist (z. B. wenn erst noch geredet wird).
- Die Ampel nimmt das nächste kommende Datum, nicht ein schon vergangenes.
- Der Worker antwortet bei jedem Fehler mit einer verständlichen Meldung, und die Protokolle enthalten keinen Briefinhalt.
- Datenschutzerklärung: Server-Protokolle (max. 7 Tage) ergänzt, Nutzungszähler max. 13 Monate.
- Startseite, **alle 27 Sprachseiten** und App: „in der Regel nach 30 Tagen gelöscht“ statt „spätestens“ (von Übersetzern umformuliert und gegengeprüft).
- ✅ Alle Prüfungen grün: Webseite, Worker, App, 43 Tests.
> ⚠️ **Hinweis für die Generalproben:** Beim Musterbrief zeigt die Ampel **am Sa 24.10. Gelb** („Noch 10 Tage“) und am Mo 26.10. ebenfalls Gelb. Das ist richtig so. **Ab Di 27.10. ist sie rot.** Bildschirmfotos für den Notfall deshalb erst am Mi 28.10. machen.
## 2b. Zweiter Deploy (Do 22.10., nachts): live ✅
- ✅ **Bedienung am Handy in 27 Sprachen.** Einwilligung, Knöpfe, Hinweise, Fehlermeldungen, Ergebnis-Überschriften, Ampel, Kalender und Anruf-Hilfe kommen in der Sprache der Person, Arabisch/Farsi/Urdu/Paschtu von rechts nach links. **Am PC bleibt alles deutsch.** Je Sprache übersetzt und von einem Lektor geprüft, maschinell auf Vollständigkeit geprüft (0 Fehler).
- ✅ Cloudflare-Kästchen passt auf schmale Handys. Oben steht „Für Sie kostenlos über …“ statt „2 Analysen gratis“.
- ✅ Knopf für die Beratung umbenannt: „🔑 Zugang dauerhaft auf ein Handy geben (nur für Mitarbeitende)“, damit man ihn nicht mit „Lieber mit dem Handy fotografieren“ verwechselt.
- ✅ **[Datenschutz-Blatt](datenschutz-blatt.pdf)** für die Datenschutzbeauftragte des Trägers (2 Seiten, gegen den Code geprüft).
- ✅ **[Kurzanleitung](kurzanleitung-beratung.pdf)** für Beraterinnen (1 Seite). Der Code wird am Di 27.10. eingetragen.
- ✅ **Webseite veröffentlicht**, Gesundheits-Check 12/12 grün.
- ℹ️ Der Live-Test im automatisierten Browser ging nicht, weil Cloudflare ferngesteuerte Browser blockt. Deshalb ist der echte Test die **Generalprobe morgen mit deinen Geräten**.
- ✅ **Sprach-Durchlauf (lokal, Handy-Größe 375 px):** alle 27 Sprachen nacheinander eingestellt. In jeder Sprache ist **kein deutscher Text mehr übrig**, **nichts ragt über den Rand**, die Schreibrichtung stimmt (Arabisch, Farsi, Urdu und Paschtu von rechts). Tigrinya und Hindi zusätzlich mit Bildschirmfoto angeschaut: Schrift sauber.
- Noch offen: die **echte KI-Erklärung** in den Sprachen (kommt erst mit der Generalprobe) und eine Prüfung durch Muttersprachler für Tigrinya, Somali, Paschtu und Kurdisch (nach dem Termin).
## 3. Was du tun musst
- ✅ ~~„deploy“ schreiben~~: erledigt, live seit Do 22.10. abends.
- 👤 **Fr 23.10.: Generalprobe 1** mit Laptop und Handy: `behoerdenklar.de/demo?code=VORF-2EDF`, Arabisch, Musterbrief (Ampel noch gelb = richtig).
- ✅ ~~Mo 26.10. MBE anrufen~~: Entscheidung vom 22.10.: Träger und AVV klärst du **erst, wenn die MBE Interesse hat**, also im Termin oder danach. Den Satz dafür findest du im Spickzettel.
- 👤 **Entscheiden: Testzeitraum.** „1 Monat“ (deine Wahl) oder ein festes Datum „bis 18.12.“ (mein Vorschlag, damit der Code nicht mitten in der Bestellung abläuft). Wichtig ist das erst beim Anlegen des Codes am Di 27.10.
- 👤 **Generalproben mitmachen** (mit deinem Handy und Laptop, siehe unten).
- 👤 🔴 **Alten Anthropic-Schlüssel tauschen**, der im Juli im Chat gepostet wurde (console.anthropic.com → API Keys).
---
## 4. Wochenplan
| Tag | Was | Wer |
|---|---|---|
| **Do 22.** | Korrekturen ✅, Gegenprüfung ✅, alles gesichert ✅ | Claude |
| **Do 22.** | **Deploy** ✅, Vorführ-Code `VORF-2EDF` ✅, `npm run pruefung` ✅ | Claude |
| **Fr 23. / Sa 24.** | **Generalprobe 1 mit echter KI:** Musterbrief, Arabisch **und** Farsi, mit Stoppuhr, Bildschirmfotos für den Notfall | du und Claude |
| **So 25.** | Fehler aus der Probe beheben. (Datenschutz-Blatt und Kurzanleitung sind schon fertig ✅) | Claude |
| **Mo 26.** | **Generalprobe 2.** Spätester Tag für Code-Änderungen am Worker. | du und Claude |
| **Di 27.** | **MBE-Code anlegen**, in den Handzettel eintragen, gedruckten QR mit dem Handy testen, alles drucken (Handzettel 5×, Musterbrief 2×, Kurzanleitung 5×, Datenschutz-Blatt 2×). Ab abends **nichts mehr am Code ändern.** | du und Claude |
| **Mi 28.** | **Generalprobe 3**, genau wie am Termin. Geräte laden, Checkliste vom Spickzettel. | du |
| **Do 29.** | **Termin.** Ziel: Code übergeben und Auswertungstermin am 26.11. ausmachen. | du |
---
## 5. Wenn noch Zeit ist (vor dem Termin)
- ✅ ~~Arabische Bedienung~~: erledigt, sogar in allen 27 Sprachen.
- ✅ ~~„Kostenlos über …“ im Seitenkopf~~: erledigt.
---
## 6. Nach dem Termin
- 💤 **AVV-Entwurf**, TOMs und Eintrag für das Verarbeitungsverzeichnis, bevor die MBE echte Briefe hochlädt.
- 💤 **Mehrere Fotos pro Brief** (mehrseitige Briefe).
- 💤 Knopf **„Drucken / als PDF“**.
- 💤 Knopf **„Fertig, nächste Person“** für den Beratungs-PC.
- 💤 Verbindung PC ↔ Handy übersteht ein Neuladen der Seite.
- 💤 Wöchentliche Stand-Mail an die MBE, Erinnerung 7 Tage vor Testende.
- 💤 Angebot und Rechnung (§ 19 UStG) bis zum 26.11.
- 💤 App: Haiku 5.5 statt 4.5 prüfen (laut Preisliste ca. 10× günstiger).
---
## 7. Befehle, falls du selbst etwas machen willst
```bash
# Stand eines Codes (inkl. Nutzung pro Woche und Sprache)
node tools/einrichtungs-code.js stand MBE-XXXX
# Code verlängern
node tools/einrichtungs-code.js verlaengern MBE-XXXX 2026-12-31
# Gesundheits-Check der Live-Seite (nach jedem Deploy)
npm run pruefung
# Live-Logs des Workers ansehen (wenn bei der Probe etwas hakt)
cd proxy && npx wrangler tail
```
+164
View File
@@ -0,0 +1,164 @@
<!DOCTYPE html>
<!--
Ablaufplan / Spickzettel für die Präsentation bei der MBE Kirchheimbolanden
(Do 29.10.2026). Nur für Justin, nicht zum Dalassen. 2 Seiten A4.
PDF: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new \
--no-pdf-header-footer --print-to-pdf=ablaufplan.pdf "file://$PWD/ablaufplan.html"
-->
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Ablaufplan Präsentation MBE</title>
<style>
@page { size: A4; margin: 0; }
:root { --papier: #faf7f0; --tinte: #21211d; --grau: #6b675e; --amtsblau: #1d4e89; --rot: #b3372b; --marker: #ffe66b; --gruen: #2e7d4f; }
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body { font-family: Georgia, 'Times New Roman', serif; color: var(--tinte); }
.seite { width: 210mm; height: 297mm; padding: 11mm 14mm; background: var(--papier); overflow: hidden; position: relative; page-break-after: always; font-size: 9.3pt; line-height: 1.34; }
.seite:last-child { page-break-after: auto; }
.mono { font-family: 'Courier New', monospace; }
.kopf { display: flex; justify-content: space-between; align-items: baseline; border-bottom: 0.6mm solid var(--tinte); padding-bottom: 2mm; }
.kopf b { font-size: 12.5pt; }
.kopf span { font-family: 'Courier New', monospace; font-size: 8pt; color: var(--grau); }
h1 { font-size: 18pt; line-height: 1.15; margin-top: 3mm; }
.markiert { background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%); padding: 0 1.5mm; }
.unter { color: var(--grau); margin-top: 1.5mm; font-size: 9.5pt; }
h2 { font-family: 'Courier New', monospace; font-weight: 400; font-size: 8pt; text-transform: uppercase; letter-spacing: .14em; color: var(--grau);
margin: 5mm 0 2.5mm; display: flex; gap: 3mm; align-items: center; }
h2::after { content: ''; flex: 1; border-top: 0.25mm solid rgba(33,33,29,.25); }
.schritt { display: grid; grid-template-columns: 17mm 1fr; gap: 3mm; padding: 1.6mm 0; border-bottom: 0.25mm dotted rgba(33,33,29,.35); }
.zeit { font-family: 'Courier New', monospace; font-weight: 700; color: var(--amtsblau); font-size: 9pt; }
.schritt h3 { font-size: 10.5pt; margin-bottom: .8mm; }
.sag { border-left: 0.8mm solid var(--amtsblau); background: #fff; padding: 1mm 2.5mm; margin: 0.8mm 0; font-style: italic; }
ol, ul { padding-left: 5mm; }
li { margin-bottom: .6mm; }
.ziel { margin-top: 3mm; border: 0.6mm solid var(--tinte); box-shadow: 1.5mm 1.8mm 0 rgba(33,33,29,.9); background: #fff; padding: 3mm 4mm; }
.ziel b { color: var(--rot); }
.zwei { display: grid; grid-template-columns: 1fr 1fr; gap: 5mm; }
.kasten { background: #fff; border: 0.3mm solid rgba(33,33,29,.3); padding: 2.5mm 3.5mm; }
.check li { list-style: none; position: relative; padding-left: 5mm; }
.check li::before { content: '☐'; position: absolute; left: 0; color: var(--amtsblau); }
.frage { margin-bottom: 2mm; }
.frage b { display: block; }
.nicht { border-left: 0.8mm solid var(--rot); background: #fdf1ef; padding: 1.5mm 2.5mm; }
.klein { font-size: 8.3pt; color: var(--grau); }
</style>
</head>
<body>
<div class="seite">
<div class="kopf"><b>📬 BehördenKlar · Ablaufplan</b><span>MBE Kirchheimbolanden · Do 29.10.2026 · nur für mich</span></div>
<h1>10–15 Minuten. <span class="markiert">Zeigen, zuhören, Testzugang dalassen.</span></h1>
<p class="unter">Die Webseite ist die Präsentation. Alles läuft im Browser, ohne App. Die App nur kurz erwähnen.</p>
<h2>Ablauf</h2>
<div class="schritt"><span class="zeit">0–1'</span><div>
<h3>Begrüßung und wer ich bin</h3>
<p class="sag">„Ich bin Justin Klein aus Winnweiler. Meine Mutter ist selbst nach Deutschland gekommen. Briefe vom Amt kenne ich von klein auf, deshalb habe ich BehördenKlar gebaut.“</p>
</div></div>
<div class="schritt"><span class="zeit">1–3'</span><div>
<h3>Das Problem aus ihrer Sicht: erst fragen, dann zuhören</h3>
<p class="sag">„Wie oft kommt jemand zu Ihnen, bei dem die Frist schon fast vorbei ist? Oder bei dem der Termin erst mal nur fürs Vorlesen draufgeht?“</p>
<p>Kurz zuhören und ihre Antwort aufgreifen. Das ist der Aufhänger für alles Weitere.</p>
</div></div>
<div class="schritt"><span class="zeit">3–8'</span><div>
<h3>Live-Vorführung mit dem Musterbrief</h3>
<ol>
<li>Laptop: <b>behoerdenklar.de/demo</b> ist schon offen, Sprache <b>Arabisch</b> gewählt, Vorführ-Code eingelöst.</li>
<li>„Lieber mit dem Handy fotografieren“ erst jetzt antippen (QR gilt 15 Min.), mit dem Handy scannen. Am Handy: Häkchen, „Brief auswählen“, Foto, „Foto verwenden“.</li>
<li>Ausgedruckten Musterbrief fotografieren. <i>„Das dauert ein bis zwei Minuten.“</i> In der Wartezeit den Datenschutz-Teil erzählen.</li>
<li>Laptop zeigt die Erklärung <b>auf Deutsch</b>, das Handy <b>auf Arabisch</b>. Kurz umschalten.</li>
<li>Zeigen: <b>rote Ampel</b> (Termin nächste Woche), <b>„In meinen Kalender“</b>, <b>Anruf-Hilfe</b> mit Aktenzeichen, Fachbegriff „Fiktionsbescheinigung“ bleibt deutsch.</li>
<li>„Ergebnis aufs Handy mitnehmen“: eine Beraterin mit <b>ihrem</b> Handy scannen lassen. Den Kalender-Eintrag am Handy zeigen, nicht am Laptop.</li>
</ol>
<p class="sag">„Alles, was Sie sehen, geht ohne App, im Browser, auf jedem Handy, ohne Konto. Eine App kommt später dazu, zum Beispiel für Erinnerungen.“</p>
</div></div>
<div class="schritt"><span class="zeit">8–9'</span><div>
<h3>Datenschutz ehrlich (bevor sie fragen)</h3>
<p class="sag">„Kein Konto, wir speichern keine Briefe. Zwischen PC und Handy ist alles Ende-zu-Ende verschlüsselt. Die Erklärung macht ein KI-Dienst in den USA, Anthropic. Der löscht in der Regel nach 30 Tagen und trainiert nicht damit. Das steht vor dem Hochladen auf der Seite, und die Person willigt für jeden Brief einzeln ein.“</p>
</div></div>
<div class="schritt"><span class="zeit">9–11'</span><div>
<h3>Das Angebot: Handzettel übergeben</h3>
<p class="sag">„Ich möchte Ihnen einen Testzugang dalassen: einen Monat, bis zu 100 Briefe, kostenlos. Ihre Ratsuchenden zahlen nichts. Der Code steht hier auf dem Zettel.“</p>
<p><b>Wenn Interesse da ist:</b> „Zu welchem Träger gehören Sie, und gibt es bei Ihnen eine Datenschutzbeauftragte? Dann schicke ich ihr unser Datenschutz-Blatt und auf Wunsch einen AVV.“</p>
<p><b>Gleich den Auswertungstermin ausmachen:</b> „Darf ich in vier Wochen wiederkommen, am <b>Do 26.11.</b>, und hören, was es gebracht hat?“</p>
</div></div>
<div class="schritt" style="border-bottom: none;"><span class="zeit">11–15'</span><div>
<h3>Zuhören (das ist der eigentliche Gewinn)</h3>
<ol>
<li>Welche Briefe bringen Ihre Ratsuchenden am häufigsten mit?</li>
<li>Was fehlt in dem, was ich gezeigt habe?</li>
<li>Würden Sie es empfehlen? Wenn nicht, warum nicht?</li>
<li>Darf ich Ihre Stelle auf meiner Seite nennen, und würden Sie auf BehördenKlar verlinken?</li>
</ol>
</div></div>
<div class="ziel">
<b>Ziel des Termins:</b> Code übergeben <i>und</i> Termin am 26.11. im Kalender. Ein ehrliches „das fehlt noch“ ist auch ein Erfolg. Mitschreiben!
</div>
</div>
<div class="seite">
<div class="kopf"><b>📬 BehördenKlar · Vorbereitung &amp; Fragen</b><span>Seite 2</span></div>
<div class="zwei" style="margin-top: 4mm;">
<div>
<h2 style="margin-top: 0;">Am Vorabend</h2>
<ul class="check kasten">
<li><b>Deploy erledigt</b>, <span class="mono">npm run pruefung</span> alles grün</li>
<li>MBE-Code angelegt, im Handzettel eingetragen</li>
<li>Handzettel <b>5×</b> gedruckt, Musterbrief <b>2×</b> gedruckt</li>
<li>Einmal komplett durchgespielt, mit Stoppuhr (Ampel erst ab Di 27.10. rot, vorher Gelb ist richtig)</li>
<li>Screenshots und kurzes Video als Notfall offline auf dem Laptop</li>
<li>Laptop und Handy laden, Ladekabel einpacken</li>
</ul>
<h2>Kurz vor dem Termin</h2>
<ul class="check kasten">
<li><b>Eigener Handy-Hotspot</b> an (nicht ihr WLAN)</li>
<li>Demo-Seite offen, <b>Vorführ-Code</b> eingelöst, Arabisch gewählt</li>
<li>Handy: Automatische Sperre „Nie“, Stromsparmodus aus, Ton aus</li>
<li>Mac wach halten: im Terminal <span class="mono">caffeinate -d</span></li>
<li>Oben steht „Kostenlos über MBE Donnersberg (Vorführung)“</li>
<li>Notizblock und Stift</li>
</ul>
<h2>Notfallplan</h2>
<div class="kasten">
<p><b>Kein Internet:</b> Hotspot. Wenn das auch nicht geht: Screenshots zeigen, <i>„Ich lasse Ihnen den Link da, probieren Sie es selbst.“</i></p>
<p style="margin-top: 1.5mm;"><b>Die KI braucht lange:</b> ruhig weitererzählen (Datenschutz-Teil vorziehen).</p>
<p style="margin-top: 1.5mm;"><b>Fehlermeldung:</b> nicht nervös werden, zweiten Musterbrief nehmen oder Screenshots.</p>
<p style="margin-top: 1.5mm;"><b>Laptop hängt, Handy hat das Ergebnis:</b> einfach das Handy zeigen, dort auf Deutsch umschalten.</p>
<p style="margin-top: 1.5mm;"><b>„Deutsch | Arabisch“ fehlt:</b> „Hier hat die Übersetzung gehakt“, Screenshots zeigen.</p>
</div>
</div>
<div>
<h2 style="margin-top: 0;">Typische Fragen</h2>
<div class="frage"><b>„Was passiert mit den Daten?“</b>Kein Konto. Wir speichern den Brief nicht. Den Brief muss die KI lesen (Anthropic, USA): in der Regel nach 30 Tagen gelöscht, kein Training. Die Übergabe PC ↔ Handy ist zusätzlich Ende-zu-Ende verschlüsselt. Einen AVV bekommt Ihr Datenschutzbeauftragter von mir.</div>
<div class="frage"><b>„Ist das nicht einfach ChatGPT?“</b>ChatGPT braucht ein Konto und eine gute Frage, und danach ist der Brief vergessen. Hier reicht ein Foto: Frist in den Kalender, Spickzettel fürs Telefon. Wir erfinden keine Nummern oder Daten.</div>
<div class="frage"><b>„Ersetzt das unsere Beratung?“</b>Nein. Jede Erklärung verweist bei wichtigen Entscheidungen auf eine Beratungsstelle. Die einfachen Fälle werden abgefangen, die schweren kommen vorbereitet.</div>
<div class="frage"><b>„Was kostet das nach dem Test?“</b>Ein festes Briefkontingent für die Einrichtung, Ratsuchende zahlen nichts. <span class="klein">Nur auf Nachfrage Zahlen: 100 Briefe 20 €, 500 Briefe 75 €. Untergrenze 10 ct pro Brief. Keine USt. (§ 19).</span></div>
<div class="frage"><b>„Was kostet Sie ein Brief?“</b>Ehrlich: ein paar Cent für die KI, dazu Server, Zahlungsgebühren und Arbeitszeit.</div>
<div class="frage"><b>„Gibt es eine App?“</b>Sie ist in Arbeit. Alles Gezeigte geht ohne App. <i>Keinen Termin versprechen.</i></div>
<div class="frage"><b>„Mehrseitige Briefe?“</b>Zurzeit ein Foto pro Erklärung: die Seite mit Betreff und Frist. Ein Scanner-PDF am PC geht mehrseitig.</div>
<div class="frage"><b>„Die Bedienung ist ja Deutsch?“</b>Die Erklärung kommt in der Muttersprache. Die Knöpfe übersetze ich als Nächstes, gern mit Ihrer Hilfe beim Gegenlesen.</div>
<div class="frage"><b>„Kann man das ausdrucken?“</b>Kommt als Nächstes. Bis dahin: Kalender-Eintrag und „Mitnehmen“ aufs Handy.</div>
<div class="frage"><b>„Wer steckt dahinter?“</b>Ich, mit der Webklar GbR in Winnweiler. Aus dem Donnersbergkreis.</div>
<h2>Nicht sagen</h2>
<div class="nicht">
<p>✗ „Alles bleibt auf dem Handy“: Das stimmt für die Webseite nicht.</p>
<p>✗ „Unbegrenzt“: immer eine feste Briefzahl.</p>
<p>✗ Ein Datum für die App oder neue Funktionen versprechen.</p>
</div>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.
@@ -0,0 +1,357 @@
<!DOCTYPE html>
<!--
Datenschutz-Blatt für die Datenschutzbeauftragte des Trägers (2 Seiten A4).
Beschreibt die WEBSEITE behoerdenklar.de/demo inkl. Einrichtungs-Code und
Übergabe PC <-> Handy. Ersetzt für die Webseite das alte Factsheet vom Juli
(rechtliches/datenschutz-factsheet.md beschreibt die App).
Quellen: webseite/datenschutz.html (Stand 22.10.2026), proxy/src/index.ts,
webseite/demo.html, webseite/uebergabe.js, proxy/wrangler.toml.
PDF: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new \
--disable-gpu --allow-file-access-from-files --no-pdf-header-footer \
--print-to-pdf=datenschutz-blatt.pdf "file://$PWD/datenschutz-blatt.html"
-->
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Datenschutz auf einen Blick</title>
<script src="../../webseite/qrcode.js"></script>
<style>
@page { size: A4; margin: 0; }
:root {
--papier: #faf7f0; --tinte: #21211d; --grau: #6b675e; --text: #3a3833;
--amtsblau: #1d4e89; --stempelrot: #b3372b; --marker: #ffe66b; --gruen: #2e7d4f;
}
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body { font-family: Georgia, 'Times New Roman', serif; color: var(--tinte); background: #fff; }
.seite {
width: 210mm; height: 297mm; overflow: hidden; position: relative;
padding: 11mm 14mm 0;
background: repeating-linear-gradient(0deg, transparent 0 6.5mm, rgba(33,33,29,0.03) 6.5mm 6.7mm), var(--papier);
font-size: 8.8pt; line-height: 1.36;
page-break-after: always; break-after: page;
}
.seite:last-child { page-break-after: auto; break-after: auto; }
.mono { font-family: 'Courier New', ui-monospace, monospace; }
.briefkopf { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 2mm; border-bottom: 0.6mm solid var(--tinte); }
.absender { font-weight: 700; font-size: 12.5pt; }
.absender small { font-weight: 400; font-size: 8pt; opacity: .6; letter-spacing: .04em; }
.az { color: var(--grau); font-size: 8pt; }
.betreff { margin-top: 4mm; font-size: 8pt; color: var(--grau); text-transform: uppercase; letter-spacing: .14em; }
h1 { font-size: 22pt; line-height: 1.14; margin-top: 1mm; letter-spacing: -0.01em; }
.markiert { background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%); padding: 0 1.5mm; }
.anrede { margin-top: 2mm; font-size: 9.6pt; line-height: 1.4; }
.anrede i { color: var(--grau); }
h2 { font-size: 8pt; text-transform: uppercase; letter-spacing: .14em; color: var(--grau); font-weight: 400;
margin: 3.8mm 0 2mm; display: flex; align-items: center; gap: 2.5mm; }
h2 .nr { font-family: 'Courier New', monospace; font-weight: 700; color: var(--amtsblau); letter-spacing: 0; font-size: 9pt; }
h2::after { content: ''; flex: 1; border-top: 0.25mm solid rgba(33,33,29,.25); }
.zwei { display: grid; grid-template-columns: 1fr 1fr; gap: 5mm; }
.eins-box { display: grid; grid-template-columns: 1fr 81mm; gap: 5mm; align-items: start; }
.kasten { background: #fff; border: 0.5mm solid var(--tinte); padding: 3.4mm 3.6mm 2.4mm; position: relative; }
.kasten .label { position: absolute; top: -2.6mm; left: 3.5mm; font-family: 'Courier New', monospace; font-size: 7pt;
text-transform: uppercase; letter-spacing: .1em; background: var(--tinte); color: var(--papier); padding: .4mm 2mm; }
ul.haken { list-style: none; font-size: 8.5pt; line-height: 1.32; }
ul.haken li { padding-left: 4mm; position: relative; margin-bottom: .9mm; }
ul.haken li::before { content: '✓'; position: absolute; left: 0; color: var(--gruen); font-weight: 700; }
ul.haken li.ehrlich::before { content: '!'; color: var(--stempelrot); left: .8mm; }
/* Datenfluss */
.schritte { display: grid; grid-template-columns: 1fr 1fr; gap: 2mm 5mm; }
.schritt { display: grid; grid-template-columns: 6mm 1fr; gap: 1.2mm; font-size: 8.5pt; line-height: 1.32; color: var(--text); }
.schritt .n { font-family: 'Courier New', monospace; font-weight: 700; font-size: 10.5pt; color: var(--amtsblau);
border: 0.35mm solid var(--amtsblau); background: #fff; width: 5.4mm; height: 5.4mm; display: flex; align-items: center; justify-content: center; }
.schritt b { color: var(--tinte); }
.wege { display: grid; grid-template-columns: 1fr 1fr; gap: 5mm; margin-top: 2.6mm; }
.weg { border: 0.3mm solid rgba(33,33,29,.35); background: #fff; padding: 1.5mm 2.4mm; font-size: 8pt; line-height: 1.3; color: var(--text); }
.weg b { color: var(--tinte); }
/* Tabellen */
table { width: 100%; border-collapse: collapse; font-size: 8pt; line-height: 1.24; }
th { font-family: 'Courier New', monospace; font-weight: 400; font-size: 7pt; text-transform: uppercase; letter-spacing: .1em;
color: var(--grau); text-align: left; padding: 0 2mm .8mm 0; border-bottom: 0.4mm solid var(--tinte); }
td { padding: .8mm 2mm .8mm 0; vertical-align: top; border-bottom: 0.25mm dotted rgba(33,33,29,.35); color: var(--text); }
td:first-child { color: var(--tinte); font-weight: 700; }
td.dauer { color: var(--tinte); }
td.dauer b { color: var(--amtsblau); }
tr.gruppe td { font-family: 'Courier New', monospace; font-weight: 400; font-size: 7pt; text-transform: uppercase; letter-spacing: .1em;
color: var(--papier); background: var(--tinte); padding: .4mm 2mm; border-bottom: none; }
tr.gruppe.hell td { background: rgba(33,33,29,.12); color: var(--grau); }
tr.nebensache td { color: var(--grau); }
tr.nebensache td:first-child { color: #4d4a43; }
.tabfuss { font-size: 7.8pt; color: var(--grau); margin-top: 1.4mm; line-height: 1.32; }
.tabfuss b { color: var(--tinte); }
/* Rollen */
.fall h3 { font-size: 9.8pt; line-height: 1.22; margin-bottom: .9mm; }
.fall p { font-size: 8.4pt; line-height: 1.32; color: var(--text); margin-bottom: 1mm; }
.fall p:last-child { margin-bottom: 0; }
.fall .folge { color: var(--tinte); border-left: 0.8mm solid var(--amtsblau); padding-left: 2.2mm; }
.fall .folge b { color: var(--amtsblau); }
.absprache { margin-top: 3mm; font-size: 8.4pt; line-height: 1.34; color: var(--text); }
.absprache .gross { font-weight: 700; font-size: 10.4pt; color: var(--tinte); display: inline-block; margin-bottom: .8mm; }
/* Einwilligung */
.zitat { background: #fff; border-left: 1mm solid var(--amtsblau); padding: 2mm 3mm; font-style: italic; font-size: 8.5pt; line-height: 1.36; }
.zitat .quelle { display: block; font-style: normal; font-family: 'Courier New', monospace; font-size: 6.8pt; color: var(--grau); margin-top: .8mm; letter-spacing: .04em; }
ul.punkte { list-style: none; font-size: 8.4pt; line-height: 1.32; color: var(--text); }
ul.punkte li { padding-left: 3.5mm; position: relative; margin-bottom: .9mm; }
ul.punkte li::before { content: '›'; position: absolute; left: 0; color: var(--amtsblau); font-weight: 700; }
ul.punkte b { color: var(--tinte); }
/* Grenzen */
.grenzen { display: grid; grid-template-columns: 1fr 1fr; gap: 1.3mm 5mm; list-style: none; font-size: 8.4pt; line-height: 1.32; color: var(--text); }
.grenzen li { padding-left: 4mm; position: relative; }
.grenzen li::before { content: '!'; position: absolute; left: .8mm; color: var(--stempelrot); font-weight: 700; }
.grenzen b { color: var(--tinte); }
/* Kontakt (Seite 2 unten, ersetzt dort die Fußzeile) */
.kontakt-unten { position: absolute; left: 14mm; right: 14mm; bottom: 9mm; }
.kontakt-unten h2 { margin-top: 0; }
.kontakt { display: grid; grid-template-columns: 1fr 1fr 19mm; gap: 5mm; align-items: center; border: 0.7mm solid var(--tinte);
box-shadow: 1.6mm 1.8mm 0 rgba(33,33,29,.9); background: #fff; padding: 2.8mm 4mm; }
.kontakt .name { font-size: 11pt; font-weight: 700; line-height: 1.2; }
.kontakt p { font-size: 8.4pt; line-height: 1.36; }
.kontakt .link { font-family: 'Courier New', monospace; color: var(--amtsblau); font-size: 9.5pt; font-weight: 700; }
.kontakt .klein { color: var(--grau); font-size: 7.8pt; }
#qr { width: 19mm; height: 19mm; }
#qr svg { width: 100%; height: 100%; display: block; }
.stempel { display: inline-block; padding: .8mm 2.2mm; border: 0.5mm solid var(--stempelrot); color: var(--stempelrot);
font-family: 'Courier New', monospace; font-weight: 700; font-size: 7pt; letter-spacing: .1em;
text-transform: uppercase; transform: rotate(-3deg); }
.fuss { position: absolute; left: 14mm; right: 14mm; bottom: 8mm; border-top: 0.4mm solid var(--tinte); padding-top: 2mm;
display: flex; justify-content: space-between; align-items: flex-end; font-size: 8pt; line-height: 1.4; }
.fuss .wer b { font-size: 9.5pt; }
.fuss .seitenzahl { font-family: 'Courier New', monospace; font-size: 7.5pt; color: var(--grau); letter-spacing: .06em; }
</style>
</head>
<body>
<!-- ============================ SEITE 1 ============================ -->
<div class="seite">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar <small>by Webklar</small></span>
<span class="az mono">Az: KLAR-DATENSCHUTZ/2026 · Stand: 22.10.2026</span>
</div>
<p class="betreff">Für die Datenschutzbeauftragte Ihres Trägers · Webseite behoerdenklar.de/demo</p>
<h1>Datenschutz <span class="markiert">auf einen Blick</span></h1>
<p class="anrede">
Was mit den Daten passiert, wenn Ihre Beratungsstelle BehördenKlar im Browser nutzt, geprüft am
Server-Code und an der Datenschutzerklärung. <i>Das Blatt vom Juli 2026 beschrieb die App, nicht die Webseite.</i>
</p>
<h2><span class="nr">1</span> Was BehördenKlar tut</h2>
<div class="eins-box">
<p>
BehördenKlar erklärt Behördenbriefe in einfacher Sprache, auf Wunsch in 27 Sprachen.
Ein Foto oder PDF genügt: Eine KI (Claude von Anthropic) schreibt auf, was das Amt will,
was zu tun ist und bis wann. Kein Nutzerkonto, keine Anmeldung. Mit einem
<b>Einrichtungs-Code</b> ist der Dienst für Ihre Ratsuchenden kostenlos. Über den Code erfährt
Ihre Stelle nichts über Briefe, nur Zahlen: übrige Analysen und Nutzung pro Woche und Sprache.
</p>
<div class="kasten">
<span class="label">Kurz gesagt</span>
<ul class="haken">
<li>Briefe speichern wir nicht, Erklärungen nur verschlüsselt zur Übergabe (max. 15&nbsp;Min.).</li>
<li>Mit Code: kein Konto, keine E-Mail-Adresse nötig.</li>
<li class="ehrlich">Den Brief liest die KI: Anthropic (USA), in der Regel 30&nbsp;Tage gespeichert, kein Training.</li>
</ul>
</div>
</div>
<h2><span class="nr">2</span> So läuft ein Brief durch das System · Knopf „Lieber mit dem Handy fotografieren“</h2>
<div class="schritte">
<div class="schritt"><span class="n">1</span><div>
<b>QR-Code am Beratungs-PC.</b> Der Browser des PCs erzeugt eine zufällige Kennung und einen
neuen AES-256-Schlüssel. Beides steht im QR-Code hinter dem
„#“ der Adresse. <b>Den Schlüssel bekommt unser Server nie.</b>
</div></div>
<div class="schritt"><span class="n">4</span><div>
<b>Erklärung.</b> Anthropic (Modell Claude) erklärt den Brief auf Deutsch. Für eine andere
Sprache übersetzt ein zweiter KI-Aufruf die Erklärung. Die Antwort geht über unseren Server
zurück ans Handy, ohne dort gespeichert zu werden.
</div></div>
<div class="schritt"><span class="n">2</span><div>
<b>Einwilligung am eigenen Handy.</b> Die ratsuchende Person scannt den Code, setzt das
Häkchen für diesen einen Brief (Abschnitt&nbsp;6) und fotografiert ihn. Kein Konto, keine App.
</div></div>
<div class="schritt"><span class="n">5</span><div>
<b>Übergabe, Ende-zu-Ende verschlüsselt.</b> Das Handy verschlüsselt die Erklärung mit dem
Schlüssel aus dem QR-Code (AES-256-GCM) und legt sie bei uns ab. Der PC holt sie und
entschlüsselt sie. <b>Unser Server kann sie nicht lesen.</b>
</div></div>
<div class="schritt"><span class="n">3</span><div>
<b>Weg zur KI.</b> Das Foto geht über eine verschlüsselte Verbindung (TLS) an unseren Server
(Cloudflare). Er prüft Bot-Schutz und Guthaben und reicht es an Anthropic weiter,
<b>ohne es zu speichern</b>.
</div></div>
<div class="schritt"><span class="n">6</span><div>
<b>Löschen.</b> Der Zwischenspeicher wird beim Abholen gelöscht, sonst <b>spätestens
15&nbsp;Minuten</b> nach dem letzten Päckchen. Am PC bleibt die Erklärung nur in der offenen
Seite, bis diese neu geladen wird.
</div></div>
</div>
<div class="wege">
<div class="weg"><b>Beraterin lädt am Dienst-PC hoch:</b> nur Schritt 3 und 4. Das Häkchen setzt
dann die Beraterin (zur Rolle siehe Abschnitt&nbsp;4).</div>
<div class="weg"><b>„Ergebnis aufs Handy mitnehmen“:</b> genauso verschlüsselt, beim Abholen
gelöscht, sonst nach spätestens 15&nbsp;Minuten.</div>
</div>
<h2><span class="nr">3</span> Was gespeichert wird und wie lange</h2>
<table>
<colgroup><col style="width: 37mm"><col><col style="width: 45mm"></colgroup>
<thead><tr><th>Was</th><th>Was genau</th><th>Wie lange</th></tr></thead>
<tbody>
<tr class="gruppe"><td colspan="3">Beim Zugang über einen Einrichtungs-Code</td></tr>
<tr><td>Brief und Erklärung</td><td>Laufen nur durch unseren Server. Ausnahme: Übergabe, nächste Zeile.</td><td class="dauer"><b>bei uns: gar nicht</b></td></tr>
<tr><td>Übergabe-Päckchen</td><td>Erklärung und Statusmeldungen, verschlüsselt, für uns nicht lesbar. Dazu eine Schreib-Bremse pro IP-Adresse (Zeitfenster 1 Minute).</td><td class="dauer">beim Abholen, <b>spätestens 15 Min.</b> nach dem letzten Päckchen</td></tr>
<tr><td>Bei Anthropic (KI)</td><td>Foto und Erklärung. Keine Nutzung zum Training der KI.</td><td class="dauer"><b>in der Regel 30 Tage</b>, länger nur bei Missbrauchsverdacht</td></tr>
<tr><td>Einrichtungs-Guthaben</td><td>Code, Kennung, Name der Stelle, Ablaufdatum, Restbriefe. Nichts über Ratsuchende.</td><td class="dauer">ohne automatische Frist</td></tr>
<tr><td>Nutzungszähler</td><td>Je Analyse nur Datum und Sprache. Kein Briefinhalt, kein Personenbezug.</td><td class="dauer"><b>max. 13 Monate</b></td></tr>
<tr><td>Code-Fehlversuche</td><td>Zähler pro IP-Adresse und Tag, nur bei falscher Eingabe.</td><td class="dauer"><b>48 Stunden</b></td></tr>
<tr><td>Server-Protokolle</td><td>Zeitpunkt, aufgerufene Adresse, Fehlerart, technische Metadaten. Kein Briefinhalt.</td><td class="dauer"><b>max. 7 Tage</b></td></tr>
<tr><td>Im Browser des Geräts</td><td>Zugangs-Nachweis, Code und Name der Stelle, Ablaufdatum, Zahl übriger Briefe, Sprache. Keine Briefe.</td><td class="dauer">bis zum Löschen im Browser</td></tr>
<tr class="gruppe hell"><td colspan="3">Nur ohne Einrichtungs-Code: 2 Gratis-Analysen für Privatpersonen</td></tr>
<tr class="nebensache"><td>Gratis-Zähler</td><td>Zähler pro IP-Adresse und Tag, dazu die Tagessumme aller Gratis-Analysen.</td><td class="dauer">48 Stunden (Datenschutz&shy;erklärung: „max. 30 Tage“)</td></tr>
<tr class="nebensache"><td>E-Mail-Prüfsumme</td><td>Nur für die 2. Gratis-Analyse: SHA-256-Prüfsumme, die Adresse selbst nicht.</td><td class="dauer">unbefristet</td></tr>
</tbody>
</table>
<p class="tabfuss">
<b>Rechtsgrundlagen laut Datenschutzerklärung:</b> Brief und Erklärung: Einwilligung (Art. 6 Abs. 1 lit. a,
Art. 9 Abs. 2 lit. a DSGVO). Zähler, Schutz vor Missbrauch, Protokolle: berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO).
</p>
<div class="fuss">
<div class="wer">
<b>Justin Klein</b> · Webklar GbR · Am Schwimmbad 10, 67722 Winnweiler<br>
Tel. 0170 4969375 · justin@webklar.com · <b>behoerdenklar.de/datenschutz</b>
</div>
<span class="seitenzahl">Seite 1 von 2 · bitte wenden</span>
</div>
</div>
<!-- ============================ SEITE 2 ============================ -->
<div class="seite">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar <small>by Webklar</small></span>
<span class="az mono">Datenschutz auf einen Blick · Seite 2 von 2</span>
</div>
<h2 style="margin-top: 5mm; margin-bottom: 3mm;"><span class="nr">4</span> Wer ist verantwortlich? Das hängt davon ab, wer hochlädt</h2>
<div class="zwei">
<div class="kasten fall">
<span class="label">Fall A · Die Person selbst</span>
<h3>Die ratsuchende Person nutzt den Dienst selbst</h3>
<p>Eigenes Handy, eigenes Häkchen für jeden Brief: zu Hause oder in der Beratung über
„Lieber mit dem Handy fotografieren“.</p>
<p class="folge"><b>Verantwortlich ist die Webklar GbR</b> (BehördenKlar). Grundlage ist die
Einwilligung der Person.</p>
<p>Bitte beachten: In der Beratung erscheint die Erklärung auch am Bildschirm Ihrer Stelle.
Gespeichert wird sie dort nicht.</p>
</div>
<div class="kasten fall">
<span class="label">Fall B · Ihre Stelle</span>
<h3>Ihre Beratungsstelle lädt den Brief selbst hoch</h3>
<p>Die Beraterin lädt den Brief einer Klientin am Dienst-PC hoch.</p>
<p class="folge">Dann arbeiten <b>wir als Auftragsverarbeiter</b> für Ihre Stelle (Art. 28 DSGVO),
mit Cloudflare und Anthropic als Unterauftragsverarbeitern. Einen <b>AVV mit
Verschwiegenheitsklausel</b> stellen wir auf Wunsch.</p>
<p>Das Häkchen am Dienst-PC setzt dann die Beraterin. Es ersetzt keine Einwilligung der Klientin.</p>
</div>
</div>
<p class="absprache">
<span class="gross"><span class="markiert">Vor dem ersten echten Brief am Dienst-PC sprechen wir das ab.</span></span><br>
Unser Vorschlag für den Test: Fall A. Haben Sie eine eigene AVV-Vorlage (z. B. nach DSG-EKD oder KDG),
schicken Sie sie uns gern.
</p>
<h2><span class="nr">5</span> Dienstleister</h2>
<table>
<colgroup><col style="width: 37mm"><col><col style="width: 30mm"></colgroup>
<thead><tr><th>Wer</th><th>Wofür</th><th>Beim Code-Zugang</th></tr></thead>
<tbody>
<tr><td>Cloudflare, Inc. (USA)</td><td>Unser Server (Cloudflare Workers), Zwischenspeicher und Zähler, Bot-Schutz (Turnstile), Hosting der Webseite, Besucherstatistik ohne Cookies</td><td class="dauer"><b>ja</b></td></tr>
<tr><td>Anthropic PBC (USA)</td><td>KI-Erklärung und Übersetzung (Modell Claude), keine Nutzung zum Training</td><td class="dauer"><b>ja</b></td></tr>
<tr class="nebensache"><td>Stripe und Brevo</td><td>Nur für Privatpersonen: Bezahlung beim Kauf von Guthaben (Stripe, Irland/USA), E-Mail mit dem Guthaben-Link und Warteliste (Brevo, Deutschland/Frankreich)</td><td class="dauer">nein, nicht relevant</td></tr>
</tbody>
</table>
<p class="tabfuss" style="color: var(--text); font-size: 8.2pt;">
<b>Übermittlung in die USA:</b> gestützt auf EU-Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO).
Sie sind Teil der Auftragsverarbeitungsverträge (DPA) von Cloudflare und Anthropic, die mit deren
Geschäftsbedingungen gelten. Die Fundstellen nennen wir Ihnen gern.
</p>
<h2><span class="nr">6</span> Einwilligung: für jeden Brief neu</h2>
<div class="zwei" style="grid-template-columns: 1.12fr 1fr;">
<p class="zitat">
„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.“
<span class="quelle">Text am Häkchen vor dem Hochladen, mit Link zur Datenschutzerklärung</span>
</p>
<ul class="punkte">
<li><b>Ohne Häkchen wird nichts hochgeladen.</b></li>
<li><b>Für genau einen Brief:</b> Nach jeder Erklärung ist das Häkchen wieder leer. Die nächste Person wird neu gefragt.</li>
<li><b>Grundlage:</b> Art. 6 Abs. 1 lit. a und Art. 9 Abs. 2 lit. a DSGVO.</li>
<li><b>Sprache:</b> Der Text steht derzeit nur auf Deutsch, Übersetzungen sind in Arbeit.
Bis dahin erklärt die Beraterin ihn bei Bedarf mündlich.</li>
</ul>
</div>
<h2><span class="nr">7</span> Ehrliche Grenzen</h2>
<ul class="grenzen">
<li><b>USA.</b> Anthropic und Cloudflare sitzen in den USA. Eine Beschränkung auf Rechenzentren
in der EU ist nicht eingestellt.</li>
<li><b>30 Tage bei Anthropic</b> in der Regel, bei Verdacht auf Missbrauch länger. Darauf haben
wir keinen Einfluss.</li>
<li><b>Ende-zu-Ende gilt nur für die Übergabe.</b> Der Brief selbst geht per TLS über unseren
Server zur KI.</li>
<li><b>QR-Code nur der richtigen Person zeigen.</b> Wer ihn abfotografiert, kann die Erklärung
in diesen 15 Minuten öffnen.</li>
<li><b>Gemeinsamer Dienst-PC.</b> Nach jedem Gespräch die Seite neu laden. Kalender-Einträge am
Handy der Person anlegen, am PC landet sonst eine Datei mit der Frist im Download-Ordner.</li>
<li><b>Keine Rechtsberatung.</b> BehördenKlar erklärt Briefe, ersetzt aber keine Beratung. Auch
eine KI kann sich irren.</li>
<li><b>Selbstauskunft, kein Zertifikat.</b> Eine anwaltliche Prüfung von Datenschutzerklärung und
Blatt steht noch aus.</li>
<li><b>Noch offen:</b> AVV-Entwurf, TOMs und ein Textbaustein fürs Verarbeitungsverzeichnis.
Sie kommen vor dem ersten echten Brief am Dienst-PC.</li>
</ul>
<div class="kontakt-unten">
<h2><span class="nr">8</span> Kontakt und vollständige Datenschutzerklärung</h2>
<div class="kontakt">
<div>
<p class="name">Justin Klein</p>
<p>Webklar GbR, vertreten durch<br>Justin Klein und Kenso Grimm<br>
Am Schwimmbad 10, 67722 Winnweiler<br>
Tel. 0170 4969375 · justin@webklar.com</p>
</div>
<div>
<p class="link">behoerdenklar.de/datenschutz</p>
<p class="klein">Für die Webseite besonders die Abschnitte 3a, 4, 5, 8, 9a, 9b und 10.
Rückfragen beantworten wir gern, auch im Gespräch mit Ihnen.</p>
<p style="margin-top: 1.4mm;"><span class="stempel">Selbstauskunft · Stand 22.10.2026</span></p>
</div>
<div id="qr"></div>
</div>
</div>
</div>
<script>
var qr = qrcode(0, 'M');
qr.addData('https://behoerdenklar.de/datenschutz');
qr.make();
document.getElementById('qr').innerHTML = qr.createSvgTag({ cellSize: 4, margin: 0, scalable: true });
</script>
</body>
</html>
Binary file not shown.
+203
View File
@@ -0,0 +1,203 @@
<!DOCTYPE html>
<!--
Handzettel für Beratungsstellen (DIN A4, zum Dalassen nach der Präsentation).
CODE unten eintragen, sobald der Einrichtungs-Code angelegt ist
(node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --monate 1),
dann neu als PDF ausgeben:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new \
--no-pdf-header-footer --print-to-pdf=handzettel.pdf "file://$PWD/handzettel.html"
-->
<html lang="de">
<head>
<meta charset="UTF-8">
<title>BehördenKlar für Beratungsstellen</title>
<script src="../../webseite/qrcode.js"></script>
<style>
@page { size: A4; margin: 0; }
:root {
--papier: #faf7f0; --tinte: #21211d; --grau: #6b675e;
--amtsblau: #1d4e89; --stempelrot: #b3372b; --marker: #ffe66b; --gruen: #2e7d4f;
}
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body { font-family: Georgia, 'Times New Roman', serif; color: var(--tinte); background: #fff; }
.seite {
width: 210mm; height: 297mm; overflow: hidden; position: relative;
padding: 11mm 14mm 0;
background: repeating-linear-gradient(0deg, transparent 0 6.5mm, rgba(33,33,29,0.03) 6.5mm 6.7mm), var(--papier);
font-size: 9.6pt; line-height: 1.42;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; }
.briefkopf { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 2mm; border-bottom: 0.6mm solid var(--tinte); }
.absender { font-weight: 700; font-size: 12.5pt; }
.absender small { font-weight: 400; font-size: 8pt; opacity: .6; letter-spacing: .04em; }
.az { color: var(--grau); font-size: 8pt; }
.betreff { margin-top: 5mm; font-size: 8pt; color: var(--grau); text-transform: uppercase; letter-spacing: .14em; }
h1 { font-size: 23pt; line-height: 1.14; margin-top: 1.5mm; letter-spacing: -0.01em; }
.markiert { background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%); padding: 0 1.5mm; }
.anrede { margin-top: 3mm; font-size: 10.5pt; max-width: 165mm; }
.schritte { display: flex; gap: 3mm; margin-top: 4.5mm; }
.schritt { flex: 1; border: 0.4mm solid var(--tinte); background: #fff; padding: 2.2mm 3mm; font-size: 9pt; line-height: 1.3; }
.schritt b { font-family: 'Courier New', monospace; color: var(--amtsblau); margin-right: 1mm; }
h2 { font-size: 8pt; text-transform: uppercase; letter-spacing: .14em; color: var(--grau); font-weight: 400;
margin: 5mm 0 2.5mm; display: flex; align-items: center; gap: 3mm; }
h2::after { content: ''; flex: 1; border-top: 0.25mm solid rgba(33,33,29,.25); }
.raster { display: grid; grid-template-columns: 1fr 1fr; gap: 2.4mm 5mm; }
.punkt { display: flex; gap: 2.5mm; align-items: flex-start; }
.punkt .ik { font-size: 12pt; line-height: 1.2; width: 6mm; flex-shrink: 0; text-align: center; }
.punkt h3 { font-size: 10pt; line-height: 1.25; }
.punkt p { color: #3a3833; font-size: 8.8pt; line-height: 1.36; }
.punkt.neu h3::after { content: 'NEU'; font-family: 'Courier New', monospace; font-size: 6.5pt; letter-spacing: .1em;
color: var(--stempelrot); border: 0.3mm solid var(--stempelrot); padding: 0 1mm; margin-left: 1.5mm; vertical-align: 1px; }
.unten { display: grid; grid-template-columns: 1fr 72mm; gap: 5mm; margin-top: 5mm; }
.kasten { background: #fff; border: 0.5mm solid var(--tinte); padding: 3.5mm 4mm; position: relative; }
.kasten .label { position: absolute; top: -2.6mm; left: 3.5mm; font-family: 'Courier New', monospace; font-size: 7pt;
text-transform: uppercase; letter-spacing: .1em; background: var(--tinte); color: var(--papier); padding: .4mm 2mm; }
.kasten ul { list-style: none; font-size: 8.7pt; line-height: 1.38; }
.kasten li { padding-left: 4mm; position: relative; margin-bottom: 1mm; }
.kasten li::before { content: '✓'; position: absolute; left: 0; color: var(--gruen); font-weight: 700; }
.kasten li.ehrlich::before { content: '!'; color: var(--stempelrot); left: .8mm; }
.angebot { border: 0.7mm solid var(--tinte); box-shadow: 1.6mm 1.8mm 0 rgba(33,33,29,.9); background: #fff; padding: 4mm; text-align: center; }
.angebot .kopf { font-family: 'Courier New', monospace; font-size: 7.5pt; text-transform: uppercase; letter-spacing: .1em; color: var(--grau); }
.angebot .gross { font-size: 13pt; font-weight: 700; line-height: 1.25; margin: 1.5mm 0; }
.angebot .gross .markiert { padding: 0 1mm; }
.angebot .klein { font-size: 8pt; color: var(--grau); line-height: 1.35; }
#qr { width: 30mm; height: 30mm; margin: 2mm auto 1mm; }
#qr svg { width: 100%; height: 100%; display: block; }
.code { font-family: 'Courier New', monospace; font-weight: 700; font-size: 13pt; letter-spacing: .08em; background: var(--marker); display: inline-block; padding: .3mm 2.5mm; }
.link { font-family: 'Courier New', monospace; font-size: 7.5pt; color: var(--amtsblau); word-break: break-all; margin-top: 1mm; }
.chatgpt { margin-top: 4mm; font-size: 8.7pt; line-height: 1.4; color: #3a3833; }
.chatgpt b { color: var(--tinte); }
.fuss { position: absolute; left: 14mm; right: 14mm; bottom: 8mm; border-top: 0.4mm solid var(--tinte); padding-top: 2mm;
display: flex; justify-content: space-between; align-items: flex-end; font-size: 8pt; line-height: 1.4; }
.fuss .wer b { font-size: 9.5pt; }
.stempel { display: inline-block; padding: 1mm 2.5mm; border: 0.5mm solid var(--stempelrot); color: var(--stempelrot);
font-family: 'Courier New', monospace; font-weight: 700; font-size: 7.5pt; letter-spacing: .1em;
text-transform: uppercase; transform: rotate(-4deg); }
</style>
</head>
<body>
<div class="seite">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar <small>by Webklar</small></span>
<span class="az mono">Az: KLAR-BERATUNG/2026 · Anlage: 1 Testzugang</span>
</div>
<p class="betreff">Für Beratungsstellen · Migrationsberatung · Soziale Beratung</p>
<h1>Ihre Ratsuchenden verstehen den Brief, <span class="markiert">bevor sie zu Ihnen kommen.</span></h1>
<p class="anrede">
BehördenKlar erklärt schwierige Post in einfacher Sprache, auf Wunsch in 27 Sprachen:
von Ausländerbehörde, Jobcenter, Finanzamt, Krankenkasse, Vermieter oder Inkasso.
Ein Foto genügt. Ihre Beratungszeit geht in die Lösung, nicht ins Vorlesen.
</p>
<div class="schritte">
<div class="schritt"><b>1</b> Brief mit dem Handy fotografieren, im Browser, ohne App und ohne Konto.</div>
<div class="schritt"><b>2</b> Nach etwa einer Minute: Was will das Amt, was ist zu tun, bis wann?</div>
<div class="schritt"><b>3</b> Frist in den Kalender, Spickzettel fürs Telefonat, mit den wichtigen Fragen zu Ihnen.</div>
</div>
<h2>Was BehördenKlar kann</h2>
<div class="raster">
<div class="punkt"><span class="ik">🌐</span><div>
<h3>27 Sprachen, Fachbegriffe bleiben deutsch</h3>
<p>Arabisch, Farsi, Paschtu, Kurdisch, Tigrinya, Türkisch, Ukrainisch u. v. m. Das deutsche Wort bleibt stehen, damit man es im Brief wiederfindet.</p>
</div></div>
<div class="punkt"><span class="ik">🚦</span><div>
<h3>Frist erkannt, Dringlichkeit sichtbar</h3>
<p>Ampel: Rot bei 7 Tagen oder weniger. Mit konkreter To-do-Liste: welche Unterlagen, wohin, bis wann.</p>
</div></div>
<div class="punkt"><span class="ik">📅</span><div>
<h3>Frist und Termin in den Handy-Kalender</h3>
<p>Ein Tipp genügt, das Handy erinnert 3 Tage und 1 Tag vorher. Keine Frist geht zwischen zwei Beratungsterminen unter.</p>
</div></div>
<div class="punkt"><span class="ik">📞</span><div>
<h3>Anruf-Hilfe fürs Amt</h3>
<p>Telefonnummer und Aktenzeichen aus dem Brief, dazu Sätze zum Sagen: auf Deutsch, darunter die Bedeutung in der eigenen Sprache.</p>
</div></div>
<div class="punkt neu"><span class="ik">💻</span><div>
<h3>Beratung am PC, Foto mit dem Handy</h3>
<p>QR-Code am Bildschirm scannen, Brief fotografieren: Sie lesen die Erklärung auf Deutsch, Ihr Gegenüber gleichzeitig in seiner Sprache.</p>
</div></div>
<div class="punkt neu"><span class="ik">📱</span><div>
<h3>Ergebnis zum Mitnehmen</h3>
<p>Per QR-Code wandert die Erklärung vom Beratungs-PC aufs Handy der Ratsuchenden, mit Kalender und Anruf-Hilfe.</p>
</div></div>
<div class="punkt"><span class="ik">🔎</span><div>
<h3>Erfindet nichts</h3>
<p>Steht keine Telefonnummer oder kein Datum im Brief, sagt BehördenKlar das, statt zu raten.</p>
</div></div>
<div class="punkt"><span class="ik">⚖️</span><div>
<h3>Ergänzung, kein Ersatz</h3>
<p>Jede Erklärung verweist bei wichtigen Entscheidungen ausdrücklich auf eine Beratungsstelle.</p>
</div></div>
</div>
<div class="unten">
<div>
<div class="kasten">
<span class="label">Datenschutz, ehrlich erklärt</span>
<ul>
<li>Kein Konto, keine Anmeldung, keine Installation nötig.</li>
<li>Briefe werden bei uns nicht dauerhaft gespeichert.</li>
<li>Übergabe zwischen PC und Handy zusätzlich Ende-zu-Ende verschlüsselt: Der Schlüssel steckt nur im QR-Code.</li>
<li>Keine Werbung, kein Verkauf von Daten.</li>
<li class="ehrlich">Den Brief selbst muss die KI lesen: Anthropic (USA). Gelöscht in der Regel nach 30 Tagen, kein Training. Das steht offen auf der Seite, und die Person willigt für jeden Brief einzeln ein.</li>
</ul>
</div>
<p class="chatgpt">
<b>„Ist das nicht einfach ChatGPT?“</b> Dort braucht man ein Konto und eine gut
formulierte Frage, und danach ist der Brief vergessen. Bei BehördenKlar macht man
ein Foto. Frist, Termin und Telefonnummer werden zu Kalender-Eintrag und
Anruf-Hilfe, und deutsche Fachbegriffe bleiben erkennbar.
</p>
<p class="chatgpt">
<b>Und nach dem Test?</b> Ihre Einrichtung bekommt ein festes Briefkontingent zu
einem Preis für Beratungsstellen. Die Ratsuchenden zahlen weiterhin nichts. Ohne
Abo-Falle: Nach vier Wochen schauen wir gemeinsam, was es gebracht hat.
</p>
</div>
<div class="angebot">
<p class="kopf">Ihr Testzugang</p>
<p class="gross">1 Monat · bis 100 Briefe<br><span class="markiert">kostenlos</span></p>
<p class="klein">Ratsuchende zahlen nichts. Kein Vertrag, endet von selbst, verlängerbar.</p>
<div id="qr"></div>
<p class="klein">Ihr Zugangscode für den Beratungs-PC:</p>
<p><span class="code" id="codeText"></span></p>
<p class="link" id="linkText"></p>
<p class="klein" style="margin-top: 1.5mm;">Einmal am Beratungs-PC öffnen, dann ist auch „Mit dem Handy fotografieren“ für Ihre Ratsuchenden kostenlos.</p>
</div>
</div>
<div class="fuss">
<div class="wer">
<b>Justin Klein</b> · Webklar GbR · Am Schwimmbad 10, 67722 Winnweiler<br>
Tel. 0170 4969375 · justin@webklar.com · <b>behoerdenklar.de</b>
</div>
<span class="stempel">Aus dem Donnersbergkreis</span>
</div>
</div>
<script>
// Einrichtungs-Code hier eintragen, sobald er angelegt ist (z. B. 'MBE-K7QX').
var CODE = '';
var url = CODE ? 'https://behoerdenklar.de/demo?code=' + CODE : 'https://behoerdenklar.de/demo';
var qr = qrcode(0, 'M');
qr.addData(url);
qr.make();
document.getElementById('qr').innerHTML = qr.createSvgTag({ cellSize: 4, margin: 0, scalable: true });
var codeEl = document.getElementById('codeText');
codeEl.textContent = CODE || 'wird beim Termin eingetragen';
if (!CODE) { codeEl.style.fontSize = '8.5pt'; codeEl.style.letterSpacing = '0'; }
document.getElementById('linkText').textContent = url.replace('https://', '');
</script>
</body>
</html>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

@@ -0,0 +1,203 @@
<!DOCTYPE html>
<!--
Kurzanleitung „So geht's am Beratungs-PC“ (DIN A4, 1 Seite, für Beraterinnen).
CODE (und ggf. STELLE) unten im Skript eintragen, sobald der Einrichtungs-Code
angelegt ist, dann neu als PDF ausgeben (im Ordner marketing/praesentation-mbe):
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --disable-gpu \
--allow-file-access-from-files --no-pdf-header-footer \
--print-to-pdf=kurzanleitung-beratung.pdf "file://$PWD/kurzanleitung-beratung.html"
Knopf- und Meldungstexte stammen wörtlich aus webseite/demo.html, „Der Zugang über …
ist abgelaufen“ aus proxy/src/index.ts (Stand 22.10.2026).
-->
<html lang="de">
<head>
<meta charset="UTF-8">
<title>So geht's am Beratungs-PC</title>
<script src="../../webseite/qrcode.js"></script>
<style>
@page { size: A4; margin: 0; }
:root {
--papier: #faf7f0; --tinte: #21211d; --grau: #6b675e;
--amtsblau: #1d4e89; --stempelrot: #b3372b; --marker: #ffe66b; --gruen: #2e7d4f;
}
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body { font-family: Georgia, 'Times New Roman', serif; color: var(--tinte); background: #fff; }
.seite {
width: 210mm; height: 297mm; overflow: hidden; position: relative;
padding: 10mm 14mm 0;
background: repeating-linear-gradient(0deg, transparent 0 6.5mm, rgba(33,33,29,0.03) 6.5mm 6.7mm), var(--papier);
font-size: 11pt; line-height: 1.36;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; }
.briefkopf { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 2mm; border-bottom: 0.6mm solid var(--tinte); }
.absender { font-weight: 700; font-size: 12.5pt; }
.absender small { font-weight: 400; font-size: 8pt; opacity: .6; letter-spacing: .04em; }
.az { color: var(--grau); font-size: 8pt; }
.betreff { margin-top: 3.5mm; font-size: 8pt; color: var(--grau); text-transform: uppercase; letter-spacing: .14em; }
h1 { font-size: 23pt; line-height: 1.12; margin-top: 1mm; letter-spacing: -0.01em; }
.markiert { background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%); padding: 0 1.5mm; }
.anrede { margin-top: 2mm; font-size: 11.5pt; line-height: 1.36; }
/* Knöpfe und Bildschirm-Texte so, wie man sie auf der Seite wiedererkennt */
.k { font-weight: 700; background: #fff; border: 0.35mm solid var(--tinte); border-radius: 1mm;
padding: 0 1.3mm; margin-right: .5mm; box-shadow: 0.5mm 0.5mm 0 var(--tinte);
-webkit-box-decoration-break: clone; box-decoration-break: clone; }
.m { background: rgba(29,78,137,.09); padding: 0 .8mm; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
.kasten { background: #fff; border: 0.5mm solid var(--tinte); padding: 3.6mm 4mm 3.2mm; position: relative; }
.kasten .label { position: absolute; top: -2.6mm; left: 3.5mm; font-family: 'Courier New', monospace; font-size: 7.5pt;
text-transform: uppercase; letter-spacing: .1em; background: var(--tinte); color: var(--papier); padding: .4mm 2mm; }
.einmalig { margin-top: 4.4mm; display: grid; grid-template-columns: 1fr 27mm; gap: 5mm; align-items: start; }
.einmalig p { font-size: 10.6pt; line-height: 1.34; }
.adresse { font-family: 'Courier New', monospace; font-weight: 700; font-size: 12.5pt; color: var(--amtsblau);
background: var(--marker); display: inline-block; padding: .4mm 2mm; margin: 1.2mm 0 1.6mm; word-break: break-all; }
.luecke { display: inline-block; width: 26mm; border-bottom: 0.35mm solid var(--amtsblau); height: 4.2mm; vertical-align: -0.6mm; }
.einmalig ul { list-style: none; font-size: 10pt; line-height: 1.32; }
.einmalig li { padding-left: 4mm; position: relative; margin-top: .9mm; }
.einmalig li::before { content: '›'; position: absolute; left: .5mm; color: var(--amtsblau); font-weight: 700; }
.qrteil { text-align: center; }
#qr { width: 25mm; height: 25mm; margin: 0 auto 1.2mm; }
#qr svg { width: 100%; height: 100%; display: block; }
.qrteil .klein { font-size: 7.8pt; color: var(--grau); line-height: 1.3; }
.code { font-family: 'Courier New', monospace; font-weight: 700; font-size: 11pt; letter-spacing: .08em; background: var(--marker); padding: 0 1.5mm; margin-right: 1mm; }
h2 { font-size: 8pt; text-transform: uppercase; letter-spacing: .14em; color: var(--grau); font-weight: 400;
margin: 3.6mm 0 1.4mm; display: flex; align-items: center; gap: 3mm; }
h2::after { content: ''; flex: 1; border-top: 0.25mm solid rgba(33,33,29,.25); order: 1; }
h2 .legende { order: 2; text-transform: none; letter-spacing: 0; font-size: 9.5pt; color: var(--grau); }
.schritte { list-style: none; }
.schritte li { display: grid; grid-template-columns: 9mm 1fr; gap: 1.5mm; padding: 0.8mm 0; border-bottom: 0.25mm dashed rgba(33,33,29,.22); }
.schritte li:last-child { border-bottom: none; }
.nr { font-family: 'Courier New', monospace; font-weight: 700; font-size: 17pt; line-height: 1; color: var(--amtsblau); padding-top: .4mm; }
.schritte b.t { font-size: 11.6pt; }
.schritte p { font-size: 11pt; line-height: 1.34; }
.schritte .zusatz { font-size: 9.6pt; color: #3a3833; margin-top: .4mm; }
.wichtig { margin-top: 3.8mm; border-color: var(--stempelrot); border-width: 0.6mm; }
.wichtig .label { background: var(--stempelrot); }
.wichtig > p { font-size: 10.4pt; line-height: 1.33; padding-left: 4.5mm; position: relative; margin-bottom: .8mm; }
.wichtig > p::before { content: '!'; position: absolute; left: .6mm; color: var(--stempelrot); font-weight: 700; }
.meldungen { width: 100%; border-collapse: collapse; margin-top: 1.4mm; font-size: 9.6pt; line-height: 1.28; }
.meldungen caption { text-align: left; font-family: 'Courier New', monospace; font-size: 7.5pt; text-transform: uppercase; letter-spacing: .1em; color: var(--grau); padding-bottom: .8mm; }
.meldungen td { padding: .4mm 0; vertical-align: top; border-top: 0.25mm solid rgba(33,33,29,.15); }
.meldungen td:first-child { width: 39%; padding-right: 3mm; }
.meldungen tr.leer td { background: rgba(255,230,107,.45); }
.meldungen tr.leer td:first-child { padding-left: 1mm; }
.fuss { position: absolute; left: 14mm; right: 14mm; bottom: 8mm; border-top: 0.4mm solid var(--tinte); padding-top: 2mm;
display: flex; justify-content: space-between; align-items: flex-end; font-size: 8pt; line-height: 1.4; }
.fuss .anruf { font-size: 12.5pt; font-weight: 700; line-height: 1.3; }
.fuss .anruf .nummer { font-family: 'Courier New', monospace; background: var(--marker); padding: 0 1.5mm; }
.stempel { display: inline-block; padding: 1mm 2.5mm; border: 0.5mm solid var(--stempelrot); color: var(--stempelrot);
font-family: 'Courier New', monospace; font-weight: 700; font-size: 7.5pt; letter-spacing: .1em;
text-transform: uppercase; transform: rotate(-4deg); margin-bottom: 1mm; }
</style>
</head>
<body>
<div class="seite">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar <small>by Webklar</small></span>
<span class="az mono">Az: KLAR-BERATUNG/2026 · Kurzanleitung Beratungs-PC</span>
</div>
<p class="betreff">Kurzanleitung · Für Beraterinnen und Berater</p>
<h1>So geht's am <span class="markiert">Beratungs-PC</span></h1>
<p class="anrede">
Die Person fotografiert ihren Brief mit dem eigenen Handy. Sie lesen am PC mit.
</p>
<div class="kasten einmalig">
<span class="label">Einmal einrichten: Lesezeichen</span>
<div>
<p>Diese Adresse am Beratungs-PC als Lesezeichen speichern, genau so, mit Code:</p>
<p><span class="adresse" id="adresseText"></span></p>
<ul>
<li><b>Lesezeichen von Hand anlegen</b> und die Adresse eintragen. Nicht nur den Stern ☆ klicken: Nach dem Öffnen ist der Code aus der Adresszeile verschwunden.</li>
<li>Ihr Code: <span class="code" id="codeText"></span> Nur für die Beratungsstelle: nicht an Ratsuchende weitergeben, Blatt nicht offen auslegen.</li>
</ul>
</div>
<div class="qrteil">
<div id="qr"></div>
<p class="klein">Gleiche Adresse als QR-Code</p>
</div>
</div>
<h2><span>Ablauf für jede Person</span><span class="legende"><span class="k">Knopf</span> = klicken oder tippen · <span class="m">„Text“</span> = steht so auf dem Bildschirm</span></h2>
<ol class="schritte">
<li><span class="nr">1</span><div>
<p><b class="t">Seite öffnen.</b> Lesezeichen anklicken. Oben steht <span class="m">„🤝 Kostenlos über <span id="stelleText"></span> …“</span></p>
<p class="zusatz">Fehlt das? Unten <span class="k">🤝 Sie haben einen Code von einer Beratungsstelle?</span> öffnen und den Code einlösen.</p>
</div></li>
<li><span class="nr">2</span><div>
<p><b class="t">Sprache wählen.</b> Bei <span class="m">„🌐 Erklärung in dieser Sprache:“</span> die Sprache der Person einstellen.
Oder <span class="m">„Deutsch“</span> lassen: Dann erkennt das Handy seine Sprache selbst.</p>
</div></li>
<li><span class="nr">3</span><div>
<p><b class="t">QR-Code zeigen.</b> <span class="k">📱 Lieber mit dem Handy fotografieren</span> klicken. Die Person scannt den QR-Code mit der Handy-Kamera.
Am PC steht dann <span class="m">„✅ Handy verbunden …“</span></p>
<p class="zusatz">Nicht <span class="k">🔑 Zugang dauerhaft auf ein Handy geben</span> klicken: Das gibt Ihren Zugang dauerhaft aufs Handy.</p>
</div></li>
<li><span class="nr">4</span><div>
<p><b class="t">Am Handy: Häkchen und Foto.</b> Das Häkchen setzt <b>die Person selbst</b>. Es ist ihre Einwilligung: Das Foto geht an BehördenKlar und an den KI-Dienst Anthropic (USA).
Dann tippt sie auf <span class="k">📷 Brief auswählen &amp; erklären lassen</span> und fotografiert den Brief.</p>
<p class="zusatz">Häkchen-Text unklar? Kurz erklären. Ein Foto pro Brief, bei mehreren Seiten die mit Betreff und Frist.</p>
</div></li>
<li><span class="nr">5</span><div>
<p><b class="t">1 bis 2 Minuten warten.</b> Dann steht die Erklärung am PC auf Deutsch und am Handy in der Sprache der Person.
Umschalten über dem Ergebnis, z. B. <span class="k">Deutsch</span> <span class="k">العربية</span></p>
</div></li>
<li><span class="nr">6</span><div>
<p><b class="t">Kalender am Handy.</b> Die Person tippt am Handy auf <span class="k">📅 In meinen Kalender eintragen</span>. Nicht am PC, sonst landet die Datei auf dem Dienst-PC.</p>
<p class="zusatz">Seite am Handy schon zu? Am PC <span class="k">📱 Ergebnis aufs Handy mitnehmen</span> klicken.</p>
</div></li>
<li><span class="nr">7</span><div>
<p><b class="t">Fertig: Seite neu laden (Taste F5).</b> Nächste Person: Sprache neu wählen.</p>
</div></li>
</ol>
<div class="kasten wichtig">
<span class="label">Wichtig</span>
<p><b>Beim Warten nicht neu laden.</b> Falls doch: Das Ergebnis steht am Handy, dort <span class="k">Deutsch</span> tippen.</p>
<p><b>Ein QR-Code gilt für einen Brief.</b> Nächster Brief: ab Schritt 3.</p>
<table class="meldungen">
<caption>Meldungen am PC und was sie bedeuten</caption>
<tr><td><span class="m">„⚠️ Am Handy hat es nicht geklappt: …“</span></td><td>Am Handy steht, warum. Dort neu fotografieren.</td></tr>
<tr><td><span class="m">„⚠️ Das dauert ungewöhnlich lange …“</span></td><td>Am Handy: <span class="k">🔁 Noch einmal an den Computer senden</span><br>Kein Knopf da? Am PC <span class="k">Abbrechen</span>, dann ab Schritt 3.</td></tr>
<tr><td><span class="m">„Der QR-Code ist abgelaufen …“</span></td><td>Schritt 3 wiederholen.</td></tr>
<tr><td><span class="m">„⚠️ Keine Verbindung zum Server …“</span></td><td>Internet am PC prüfen, sonst Ihre IT fragen.</td></tr>
<tr class="leer"><td><span class="m">„Die kostenlosen Analysen sind aufgebraucht oder abgelaufen …“</span><br><span class="m">„Der Zugang über … ist abgelaufen …“</span></td><td><b>Kontingent leer oder Test zu Ende: Justin anrufen, <span style="white-space: nowrap;">0170 4969375</span>.</b> Danach das Lesezeichen neu öffnen.</td></tr>
</table>
</div>
<div class="fuss">
<div class="wer">
<span class="anruf">Bei Fragen einfach anrufen: Justin Klein · <span class="nummer">0170 4969375</span></span><br>
Webklar GbR · Am Schwimmbad 10, 67722 Winnweiler · justin@webklar.com · <b>behoerdenklar.de</b>
</div>
<span class="stempel">Stand Oktober 2026</span>
</div>
</div>
<script>
// Einrichtungs-Code hier eintragen, sobald er angelegt ist (z. B. 'MBE-K7QX').
var CODE = '';
// Name der Einrichtung, so wie er beim Anlegen des Codes vergeben wurde (steht oben auf der Seite).
var STELLE = 'MBE Donnersberg';
var url = CODE ? 'https://behoerdenklar.de/demo?code=' + CODE : 'https://behoerdenklar.de/demo';
var qr = qrcode(0, 'M');
qr.addData(url);
qr.make();
document.getElementById('qr').innerHTML = qr.createSvgTag({ cellSize: 4, margin: 0, scalable: true });
var codeEl = document.getElementById('codeText');
codeEl.textContent = CODE || 'wird beim Termin eingetragen';
if (!CODE) { codeEl.style.fontSize = '9pt'; codeEl.style.letterSpacing = '0'; }
var adresseEl = document.getElementById('adresseText');
adresseEl.textContent = 'behoerdenklar.de/demo?code=' + CODE;
if (!CODE) { var l = document.createElement('span'); l.className = 'luecke'; adresseEl.appendChild(l); }
document.getElementById('stelleText').textContent = STELLE || 'Ihre Beratungsstelle';
</script>
</body>
</html>
@@ -0,0 +1,141 @@
<!DOCTYPE html>
<!--
Musterbrief für Vorführungen (Präsentation MBE Kirchheimbolanden, 29.10.2026).
Behörde, Person, Aktenzeichen und Telefonnummer sind frei erfunden.
Zum Ausdrucken: Chrome → Drucken → A4, Ränder "Keine". PDF/PNG daneben.
Daten bei Bedarf anpassen: Frist und Termin sollen bei der Vorführung
in 1–3 Wochen liegen, damit die Ampel Gelb/Rot zeigt.
-->
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Musterbrief Ausländerbehörde</title>
<style>
@page { size: A4; margin: 0; }
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { background: #fff; }
body {
font-family: Arial, Helvetica, sans-serif; color: #111;
font-size: 10pt; line-height: 1.32;
}
.seite { width: 210mm; height: 297mm; position: relative; padding: 0 20mm 0 25mm; overflow: hidden; }
.kopf { position: absolute; top: 12mm; left: 25mm; right: 20mm; display: flex; justify-content: space-between; align-items: flex-start; }
.wappen { display: flex; gap: 4mm; align-items: center; }
.wappen svg { width: 13mm; height: 15mm; }
.behoerde { font-size: 13pt; font-weight: bold; line-height: 1.2; }
.behoerde small { display: block; font-size: 10pt; font-weight: normal; }
.absenderzeile { position: absolute; top: 45mm; left: 25mm; font-size: 7pt; text-decoration: underline; }
.anschrift { position: absolute; top: 51mm; left: 25mm; width: 85mm; font-size: 10.5pt; line-height: 1.35; }
.info { position: absolute; top: 45mm; left: 125mm; width: 65mm; font-size: 8.5pt; line-height: 1.45; }
.info td { vertical-align: top; padding: 0 2mm 0.6mm 0; }
.info td:first-child { color: #444; white-space: nowrap; }
.text { position: absolute; top: 92mm; left: 25mm; right: 20mm; }
.betreff { font-weight: bold; margin-bottom: 4mm; }
p { margin-bottom: 2.4mm; text-align: justify; hyphens: auto; }
ol { margin: 0 0 3mm 6mm; }
ol li { margin-bottom: 0.8mm; padding-left: 1mm; }
.fett { font-weight: bold; }
.gruss { margin-top: 3mm; margin-bottom: 0; }
.unterschrift { font-family: 'Brush Script MT', 'Segoe Script', cursive; font-size: 16pt; color: #1b3a7a; margin: 0 0 0 2mm; line-height: 1.2; }
.fuss {
position: absolute; bottom: 9mm; left: 25mm; right: 20mm;
border-top: 0.3mm solid #999; padding-top: 2mm;
display: flex; justify-content: space-between; font-size: 7pt; color: #444; line-height: 1.35;
}
.muster { position: absolute; bottom: 3.5mm; left: 25mm; right: 20mm; font-size: 6.5pt; color: #999; text-align: center; }
.falz { position: absolute; left: 4mm; width: 4mm; border-top: 0.2mm solid #bbb; }
</style>
</head>
<body>
<div class="seite">
<span class="falz" style="top: 105mm;"></span>
<span class="falz" style="top: 148.5mm;"></span>
<span class="falz" style="top: 210mm;"></span>
<div class="kopf">
<div class="wappen">
<svg viewBox="0 0 26 30" aria-hidden="true">
<path d="M1 1h24v13c0 8-6 13-12 15C7 27 1 22 1 14z" fill="#f4f4f4" stroke="#333" stroke-width="1.2"/>
<path d="M1 10h24M13 1v28" stroke="#333" stroke-width="0.8"/>
<circle cx="7" cy="5.5" r="2" fill="#2c5aa0"/><circle cx="19" cy="18" r="2.5" fill="#b3372b"/>
</svg>
<div class="behoerde">Kreisverwaltung Musterhausen<small>Fachbereich 32 – Ausländerbehörde</small></div>
</div>
<div style="font-size: 8pt; text-align: right; color: #333;">Musterplatz 1<br>67999 Musterhausen</div>
</div>
<div class="absenderzeile">Kreisverwaltung Musterhausen · Musterplatz 1 · 67999 Musterhausen</div>
<div class="anschrift">
Herrn<br>
Ahmad Beispiel<br>
Lindenweg 5<br>
67999 Musterhausen
</div>
<table class="info">
<tr><td>Aktenzeichen</td><td>32.1-AE-2026-04711</td></tr>
<tr><td>Auskunft erteilt</td><td>Frau Muster</td></tr>
<tr><td>Zimmer</td><td>2.14</td></tr>
<tr><td>Telefon</td><td>069 90009-314</td></tr>
<tr><td>E-Mail</td><td>abh@musterhausen.example</td></tr>
<tr><td>Datum</td><td>20.10.2026</td></tr>
</table>
<div class="text">
<p class="betreff">Ihr Antrag auf Verlängerung der Aufenthaltserlaubnis vom 01.10.2026<br>
hier: Aufforderung zur Vorlage fehlender Unterlagen gemäß § 82 Abs. 1 AufenthG<br>
und Einladung zur persönlichen Vorsprache</p>
<p>Sehr geehrter Herr Beispiel,</p>
<p>in Ihrer o. g. aufenthaltsrechtlichen Angelegenheit nehmen wir Bezug auf Ihren Antrag auf
Verlängerung der Aufenthaltserlaubnis. Ihr derzeitiger Aufenthaltstitel ist bis zum
15.11.2026 befristet. Zur abschließenden Prüfung der Erteilungsvoraussetzungen gemäß
§§ 5, 8 AufenthG sind die nachfolgend aufgeführten Unterlagen bislang nicht bzw. nicht
vollständig vorgelegt worden:</p>
<ol>
<li>gültiger Nationalpass im Original bzw. Nachweise über Ihre Bemühungen zur Passbeschaffung,</li>
<li>aktuelle Meldebescheinigung (nicht älter als drei Monate),</li>
<li>Nachweise über die Sicherung des Lebensunterhalts (Arbeitsvertrag sowie Lohn- und
Gehaltsabrechnungen der letzten drei Monate bzw. aktueller Bewilligungsbescheid des Jobcenters),</li>
<li>Nachweis über einen ausreichenden Krankenversicherungsschutz,</li>
<li>ein aktuelles biometrisches Lichtbild.</li>
</ol>
<p>Gemäß § 82 Abs. 1 AufenthG obliegt es Ihnen, die für Ihr Anliegen erforderlichen Nachweise
unverzüglich beizubringen. Wir fordern Sie daher auf, die vorgenannten Unterlagen
<span class="fett">bis spätestens 12.11.2026</span> bei der Ausländerbehörde einzureichen.</p>
<p>Zur Aufnahme Ihrer biometrischen Merkmale (Fingerabdrücke, Unterschrift) werden Sie
gebeten, am <span class="fett">Dienstag, den 03.11.2026, um 10:30 Uhr</span> in Zimmer 2.14
persönlich vorzusprechen. Bitte bringen Sie dieses Schreiben sowie Ihren derzeitigen
Aufenthaltstitel mit. Sollten Sie den Termin nicht wahrnehmen können, ist dieser rechtzeitig
vorher telefonisch abzusagen.</p>
<p>Werden die angeforderten Unterlagen nicht fristgerecht vorgelegt, kann über Ihren Antrag
nach Aktenlage entschieden werden. Dies kann zur Ablehnung des Antrags führen.</p>
<p>Hinweis: Da Sie den Antrag vor Ablauf Ihres Aufenthaltstitels gestellt haben, gilt dieser
gemäß § 81 Abs. 4 AufenthG bis zur Entscheidung der Ausländerbehörde als fortbestehend
(Fiktionswirkung). Eine entsprechende Fiktionsbescheinigung wird Ihnen bei Ihrer Vorsprache
ausgehändigt.</p>
<p>Sprechzeiten: Mo, Di und Do 08:00–12:00 Uhr, Do zusätzlich 14:00–17:30 Uhr.
Vorsprachen nur nach vorheriger Terminvereinbarung.</p>
<p class="gruss">Mit freundlichen Grüßen<br>Im Auftrag</p>
<div class="unterschrift">Muster</div>
<p style="margin-top: 0;">Muster</p>
</div>
<div class="fuss">
<div>Kreisverwaltung Musterhausen<br>Musterplatz 1 · 67999 Musterhausen</div>
<div>Telefon 069 90009-0<br>www.musterhausen.example</div>
<div>Bankverbindung: Sparkasse Musterhausen<br>IBAN DE00 0000 0000 0000 0000 00</div>
</div>
<div class="muster">Beispielbrief für Vorführzwecke · Behörde, Person und alle Angaben sind frei erfunden</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

@@ -0,0 +1,350 @@
# BehördenKlar: Plan bis zum MBE-Termin am 29.10.2026
Alle 63 Funde aus den 6 Perspektiven hat ein Prüfer bestätigt, aber er hat viele davon heruntergestuft. Unten zählt die Schwere nach dieser Prüfung. Doppelte Funde sind zusammengefasst, die IDs stehen in Klammern.
---
## 1. Urteil
**Frage 1: Läuft die Vorführung flüssig genug?**
- **Für die Vorführung: Fast.** Der geplante Weg ist kurz. Am PC wählt man die Sprache und klickt einmal, am Handy sind es etwa 6 bis 7 Fingertipps. Der PC zeigt dabei live an, was am Handy passiert. Vier Dinge können den wichtigsten Moment aber kippen:
- Die zweisprachige Analyse (PC Deutsch, Handy Arabisch) ist **noch nie mit echter KI gelaufen und nicht deployed** (LI1, TE3).
- Scheitert die Übersetzung, stehen zwei Knöpfe „Deutsch | Deutsch“ da und die Person bekommt Deutsch statt Arabisch (BE2, TE2).
- Vor 12 Uhr zeigt die Ampel beim Musterbrief Gelb statt Rot (LI2).
- Ein zweiter Brief vom Handy kommt am PC nicht mehr an (TE4).
- **Für den Alltag im Testmonat: Noch nicht.** Es gibt mehrere stille Sackgassen:
- Steht am PC „Deutsch“, bekommt die Beraterin kein Deutsch (TE1).
- Wer schon 2 Gratis-Analysen gemacht hat, sieht das Code-Feld nicht mehr (AN1).
- Wird die Seite neu geladen, ist die Verbindung still weg (BE1, TE5).
- Fehler am Handy kommen am PC nicht an (RA8).
- Die meisten Punkte sind in Minuten bis wenigen Stunden behoben. Nach der MUSS-Liste lautet die Antwort für die Vorführung: **Ja**.
**Frage 2: Ist es bequem genug für die Beratungsstelle?**
- **Für einen kostenlosen Test: Fast.** Das reicht, wenn die MUSS-Liste erledigt ist und Justin die Grenzen offen sagt.
- **Was schon gut ist:**
- Wenige Klicks am PC.
- Die ratsuchende Person setzt das Häkchen selbst am eigenen Handy.
- Mit Code erscheint kein Kaufangebot.
- Fehlgeschlagene Analysen kosten nichts.
- Oben steht der eigene Name der Stelle („Kostenlos über MBE Donnersberg“).
- **Was für den Dauerbetrieb noch fehlt:**
- Nur ein Foto pro Brief, mehrseitige Briefe gehen am Handy nicht (BE4).
- Kein Ausdruck (BE8).
- Kein Knopf „Fertig – nächste Person“ (BE9, DA8).
- Die Briefzahl ist veraltet und das Ablaufdatum unsichtbar (BE3).
- Die Bedienung am Handy ist nur Deutsch (RA1).
- Es gibt keine Kurzanleitung für Kolleginnen (BE5).
- **Die größte Hürde ist aber nicht die Technik, sondern das Papier.** Ungeklärt ist, wer verantwortlich ist, wenn die Beraterin am Dienst-PC hochlädt, und ob dafür ein Vertrag zur Auftragsverarbeitung (AVV) nötig ist (DA1). Das Datenschutz-Blatt ist veraltet und ging im Juli wahrscheinlich schon an die MBE (AN6, DA6).
---
## 2. MUSS vor dem 29.10.
Zusammen etwa 12 bis 14 Stunden.
### Wochenplan
| Tag | Was |
|---|---|
| **Do 22. / Fr 23.** | Code-Paket M1 und Sätze in der Datenschutzerklärung (M5c), wenn Zeit ist auch schnelle SOLLTE-Punkte |
| **Sa 24.** | Deploy (erst Worker, dann Webseite), Vorführ-Code anlegen, Generalprobe 1 |
| **So 25.** | Fehler aus der Probe beheben, Datenschutz-Blatt und Kurzanleitung schreiben |
| **Mo 26.** | MBE anrufen (wer ist Träger?), Generalprobe 2. **Spätester Deploy-Termin.** |
| **Di 27.** | MBE-Code anlegen, Handzettel neu erzeugen, QR testen, alles drucken. Ab abends nichts mehr am Code ändern. |
| **Mi 28.** | Generalprobe 3 genau wie am Termin, Geräte vorbereiten, Checkliste |
### M1. Code-Korrekturen vor dem Deploy (zusammen ca. 3–4 Std.)
| # | Aufgabe | Funde | Aufwand |
|---|---|---|---|
| a | Die Ampel in **Kalendertagen** rechnen, so wie die App (`ampel.ts`). Zusätzlich den Termin im Musterbrief auf **Di 03.11.2026, 10:30** legen. Im Spickzettel „Termin in 7 Tagen“ durch „Termin nächste Woche“ ersetzen. Die Ampelzeile soll „Termin am …“ oder „Frist bis …“ sagen. Die Frist-Aufgabe nur anhängen, wenn wirklich die Frist das früheste Datum ist. | LI2, LI6 | 30–45 Min. |
| b | „Deutsch \| Deutsch“ verhindern: `analyse_de` nur anhängen, wenn `sprache ≠ 'de'`. Scheitert die Übersetzung, am Handy den Hinweis „Übersetzung hat nicht geklappt, hier auf Deutsch“ zeigen. `[observability] enabled = true` in `wrangler.toml` eintragen. | BE2, TE2, LI8 | 30–45 Min. |
| c | Die Beraterin bekommt immer Deutsch: Das Häkchen „auch Deutsch“ am PC immer zeigen, standardmäßig an. `d=1` setzen, egal welche Sprache am PC eingestellt ist. | TE1 | 15 Min. |
| d | Das Code-Feld (`details#codeTeil`) aus `#eingabe` herausnehmen. Danach mit `bk_demo_v2=2` testen. | AN1 | 15–30 Min. |
| e | Nach dem ersten Ergebnis den Handy-Modus beenden. Am Handy steht dann: „Für den nächsten Brief am Computer noch einmal auf ‚Lieber mit dem Handy fotografieren‘ tippen.“ | TE4, BE4 | 20 Min. |
| f | Einwilligung: Das Häkchen nach **jeder** Analyse zurücksetzen (`konsens.checked = false`). Im Einrichtungs-Modus einen eigenen Text zeigen: Er nennt Gesundheit, Herkunft und Aufenthalt sowie Anthropic (USA) und sagt „in der Regel nach 30 Tagen gelöscht, kein Training“. | DA2, LI11 | 45 Min. |
### M2. Deploy und Generalprobe
**Funde:** LI1 (Blocker), TE3, BE2, LI8, DA7
**Aufwand:** 3–4 Std. für die Proben, dazu Zeit für Korrekturen
- **Bis Sa 24.10., spätestens Mo 26.10.**
1. Zuerst den Worker deployen (`cd proxy && npx wrangler deploy`).
2. Danach die Webseite (Ordner `webseite/`).
3. Wenn die Webseite vor dem Worker live geht, laufen „Code einlösen“ und die Übergabe ins Leere.
- **2–3 komplette Durchläufe genau nach Ablaufplan:**
- gleicher Laptop, gleiches Handy, Hotspot, gedruckter Musterbrief
- Arabisch **und** Farsi
- einmal über mobile Daten
- mit Stoppuhr
- Bildschirmfotos für den Notfall machen
- **Dabei prüfen:**
- Falscher Code ergibt „Diesen Code kennen wir nicht“.
- Der Laptop zeigt „Handy verbunden“.
- Am Ende stehen zwei Knöpfe **„Deutsch | العربية“**.
- Die Ampel ist rot.
- Auf der Live-Datenschutzseite steht „9b.“.
- **Wenn „Deutsch | Deutsch“ erscheint:** sofort mit `npx wrangler tail` ins Log schauen.
- **Unsicher:** Ob die Rate-Limit-Einstellung `[[ratelimits]]` im kostenlosen Cloudflare-Tarif läuft, ist nicht geprüft. Scheitert der Deploy daran: den Block und `index.ts:832-836` vorerst entfernen (TE3).
### M3. Codes richtig anlegen
**Funde:** LI3, AN2, BE3, TE11
**Aufwand:** Minuten
```
node tools/einrichtungs-code.js neu "MBE Donnersberg (Vorführung)" VORF --briefe 30 --bis 2026-11-30
node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --bis 2026-12-18
node tools/einrichtungs-code.js stand <CODE>
```
- **Ein festes Datum nach dem 26.11.** lässt der MBE Zeit für die Bestellung. Sonst sperrt der Code mitten im Bestellvorgang und löscht den Zugang bei den Ratsuchenden.
- Handzettel und Spickzettel sagen dann „Test bis 18.12., Auswertung am 26.11.“ statt „1 Monat“.
- Nach dem Anlegen mit `stand` prüfen, dass kein „NaN“ und das richtige Datum erscheint.
- Den Vorführ-Code vor den Proben am Laptop einlösen.
### M4. Handzettel überarbeiten und neu erzeugen
**Funde:** AN9, LI5, BE6, DA4, AN2
**Aufwand:** ca. 1 Std.
- Den MBE-Code in `handzettel.html:190` eintragen. Der QR muss auf `/demo?code=MBE-…` zeigen.
- „Ihr Zugangscode für den Beratungs-PC“ statt „Code für Ihre Ratsuchenden“ schreiben.
- Dazu den Satz: „Einmal am Beratungs-PC öffnen, dann ist auch ‚Mit dem Handy fotografieren‘ kostenlos.“
- „gültig bis 18.12.2026“ eintragen.
- Datenschutz-Sätze korrigieren: „in der Regel nach 30 Tagen (länger nur bei Verdacht auf Missbrauch), kein Training“. Und: „Den Brief selbst muss die KI lesen. Ende-zu-Ende verschlüsselt ist zusätzlich die Übergabe zwischen PC und Handy.“
- **Den gedruckten QR mit dem Handy testen.** Erst wenn „Kostenlos über MBE Donnersberg“ erscheint, 5 Stück drucken. Die alte `handzettel.png/pdf` ohne Code nicht mehr verwenden.
### M5. Datenschutz-Unterlagen, nur das Nötigste
**Funde:** DA1, DA4, DA5, DA6, AN6
**Aufwand:** ca. 3 Std.
- **a) Mo 26.10. bei der MBE anrufen:** Wer ist der Träger (z. B. Diakonie oder Caritas)? Gibt es eine eigene AVV-Vorlage? (DA1)
- **b) Neues Datenschutz-Blatt für die Webseite schreiben (1–2 Seiten):**
- Datenfluss: Foto, dann Worker, dann Anthropic. Die QR-Übergabe ist verschlüsselt und nach 15 Minuten weg.
- Unterauftragsverarbeiter: Cloudflare und Anthropic.
- Rollenaussage: „Lädt die Stelle selbst hoch, arbeiten wir als Auftragsverarbeiter. Einen AVV mit Verschwiegenheitsklausel bekommen Sie auf Wunsch.“
- **Das alte Blatt vom Juli nicht mehr weitergeben.** Darin steht für die Webseite Falsches.
- **c) In der Datenschutzerklärung (geht mit dem Deploy raus):**
- Für Anthropic nur „EU-Standardvertragsklauseln“ schreiben, nicht das EU-US-Datenschutzabkommen (DPF), denn dafür gibt es keinen Beleg (DA5).
- Den 30-Tage-Satz wie in M4 anpassen (DA4).
### M6. Spickzettel (`ablaufplan.html`) nachschärfen
**Aufwand:** 30–45 Min.
- **Checkliste am Vorabend:**
- Vorführ-Code eingelöst, oben steht „Kostenlos über …“ (LI3).
- Gedruckten QR getestet (AN9).
- **Kurz vor dem Termin** (LI4):
- iPhone: Automatische Sperre auf „Nie“, Stromsparmodus aus.
- Mac: im Terminal `caffeinate -d`.
- **Bei der Vorführung:**
- QR erst live erzeugen. Steht „abgelaufen“ da, einfach neu klicken (LI10).
- Die Handy-Schritte üben: scrollen, Häkchen, Brief auswählen, Foto, verwenden (LI7).
- „Ein bis zwei Minuten“ sagen und den Datenschutz-Teil fest in die Wartezeit legen (LI8).
- Den Kalender-Knopf nur am Handy zeigen (LI9).
- Den Mitnehmen-QR von einer Beraterin mit ihrem eigenen Handy scannen lassen (LI10).
- Den Fachbegriff zeigen, der tatsächlich erscheint (LI10).
- Zeile 75 ehrlich machen: „Das Ergebnis bleibt, solange die Seite offen ist. Der Kalender-Eintrag bleibt immer.“ (RA6)
- **Angebotsteil:**
- Den Code gleich live am PC einer Beraterin einlösen und das Lesezeichen mit `?code=` setzen (LI5, BE3).
- Eine Beraterin macht einmal den Handy-Ablauf an **ihrem** Dienst-PC (BE10).
- „Auch auf dem Handy nutzen“ erst zeigen, wenn am Laptop der MBE-Code eingelöst ist. Sonst gibt der Laptop den Vorführ-Zugang weiter (AN8).
- **Ehrliche Antworten auf typische Fragen:**
- Mehrseitige Briefe: „Zurzeit ein Foto pro Erklärung, die Seite mit Betreff und Frist. Scanner-PDF am PC geht mehrseitig, dann aber nur in einer Sprache.“ (BE4)
- Bedienung auf Deutsch: „Die Erklärung kommt in der Muttersprache, die Bedienung übersetze ich als Nächstes.“ (RA1, LI7)
- AVV und Verantwortung: „Bekommt Ihr Datenschutzbeauftragter von mir.“ (DA1)
- Ausdrucken: „Kommt als Nächstes.“ (BE8)
- **Notfallplan:**
- Der Laptop hängt, das Handy zeigt das Ergebnis: einfach das Handy zeigen (LI4).
- „Deutsch | Deutsch“: „Hier hat die Übersetzung gehakt“ sagen und die Bildschirmfotos zeigen (LI8).
---
## 3. SOLLTE vor dem 29.10.
In dieser Reihenfolge, so weit die Zeit reicht. Zusammen ca. 7–9 Std.
| # | Aufgabe | Funde | Aufwand |
|---|---|---|---|
| S1 | **Fehler bei Arabisch (rechts nach links), sichtbar in der Vorführung:** Die Briefart-Plakette nicht in Courier setzen, sondern `font-family:inherit`, `letter-spacing:0`. Ampel- und Fristzeile mit KI-Text in `<bdi>` packen. Den Absatz bei den Fachbegriffen mit `dir=auto` versehen. `.todo::before` mit `margin-inline-end`. Danach am Vorführ-Handy ansehen. | RA2 | < 1 Std. |
| S2 | **Arabische Minimal-Bedienung:** Einwilligung, Startknopf, Verbunden-Banner, Wartetext und „Verbindung fehlgeschlagen“ auf Arabisch, dazu `dir=rtl`. Im Termin eine arabischsprachige Beraterin gegenlesen lassen. | RA1, DA3, BE7, LI7 | 2–3 Std. |
| S3 | **Handy-Seite aufräumen:** zum Banner scrollen (`scrollIntoView`). Im Einrichtungs-Modus die Gratis-Texte ersetzen. Im Kamera-Modus (a=k) Kaufbereich und Code-Feld immer ausblenden. Den Knopf nicht grau machen, sondern beim Tippen auf das fehlende Häkchen hinweisen. Sprachliste mit deutschem Zusatz („العربية – Arabisch“, „پښتو – Paschtu“). | RA7, BE11 | ca. 1 Std. |
| S4 | **Robustheit:** Warnung „Seite wirklich verlassen?“, solange der PC wartet. Wartezeit bei jeder Handy-Meldung verlängern. `meldeAnPc` auch im catch-Zweig aufrufen. `dateiFeld.value = ''` setzen. Nach 5 fehlgeschlagenen Abfragen oder mehr als 3 Minuten „analysiert“ einen Hinweis am PC zeigen. Im Worker den Anthropic-Aufruf in try/catch setzen. localStorage nur über einen abgesicherten Helfer ansprechen. | BE1, RA8, TE10, BE10, TE7 | 1–2 Std. |
| S5 | **Anruf-Sätze beim Mitnehmen übersetzen:** `mitBedeutung` aus der angezeigten Version nehmen und `l=` in den Mitnehmen-QR schreiben. | RA3, TE9 | 15 Min. |
| S6 | **Code-Eingabe tolerant machen:** alles außer A–Z und 0–9 entfernen, den Bindestrich vor den letzten 4 Zeichen wieder einsetzen, die Sperrgrenze auf 30 Fehlversuche anheben. | AN7, TE6, RA5 | 20 Min. |
| S7 | **Nutzung zählen ab Tag 1:** pro Analyse über einen Einrichtungs-Code ein eigener Eintrag mit Datum. Wird auch die Sprache gezählt, einen Satz in Abschnitt 9a der Datenschutzerklärung ergänzen. | TE8, AN4 | 30 Min. |
| S8 | **Kontingent leer oder abgelaufen:** Den Code nicht löschen, kein Kaufangebot zeigen, der PC hört auf zu warten und zeigt einen Kasten mit Justins Telefonnummer. Oben „gültig bis …“ anzeigen. | BE3, AN2 | 1–2 Std. |
| S9 | **Handy-Sprache übernehmen:** aus `navigator.languages` vorauswählen, aber nicht speichern. | RA4 | 15 Min. |
| S10 | **QR-Code am PC größer:** auf ca. 280–300 px (eine Zeile CSS). | BE12 | 1 Min. |
---
## 4. Zum Termin mitbringen
- **Geräte:** Laptop mit eingelöstem Vorführ-Code, eigenes Handy (Sperre aus, Akku voll), Hotspot, Ladegeräte.
- **Musterbrief** (Termin 03.11.) 2× gedruckt (LI2).
- **Handzettel mit MBE-Code**, 5× (M4).
- **Kurzanleitung „So geht's am Beratungs-PC“**, 5× (BE5): eine Seite mit 5 Schritten:
1. Lesezeichen genau auf `behoerdenklar.de/demo?code=MBE-…` setzen.
2. Sprache wählen.
3. „Lieber mit dem Handy fotografieren“ klicken.
4. Beim Warten nicht neu laden. Falls doch, steht das Ergebnis am Handy, dort auf Deutsch umschalten.
5. Nach jeder Person die Seite neu laden und die Sprache prüfen (BE9, DA8). Kalender-Einträge nur am Handy machen.
Dazu die Telefonnummer. Die Bildschirmfotos erst nach dem Deploy machen.
- **Neues Datenschutz-Blatt**, 2× „für Ihre Datenschutzbeauftragte“, mit Rollenaussage (M5, DA1, AN6). Dazu sagen: „Das Blatt aus dem Juli beschrieb die App.“
- *Optional:* eine einseitige Papier-Einwilligung zum Abheften für Klientinnen (DA2, DA3).
- **Bildschirmfotos der Generalprobe** als Notfall-Ersatz (LI1).
- **3 Fragen für die Auswertung** (genutzt? welche Sprache? Zeit gespart?) und einen festen Termin am 26.11. (AN4).
- **Nicht mitbringen:** das alte Factsheet/PDF vom Juli und den alten Handzettel ohne Code.
**Entscheidung vor dem Termin, wohin der Code geht (BE6, AN3):** Empfehlung für den Test: Der Code bleibt bei den Beraterinnen. Ratsuchende nutzen ihn nur in der Beratung über „Mit dem Handy fotografieren“. Dieser Weg gibt den Zugang nur für die Sitzung weiter. Einzelnen Personen kann die Beraterin den Zugang gezielt über „Auch auf dem Handy nutzen“ geben. Achtung: Dieser Zugang bleibt dauerhaft auf deren Handy. Einen Aushang mit Code erst nach der Auswertung machen, wenn die MBE das will.
---
## 5. NACH dem Termin
**Bevor der erste echte Brief am Dienst-PC hochgeladen wird:**
- AVV-Entwurf (nach dem Muster von GDD oder Bitkom, mit Verschwiegenheitsklausel), technische und organisatorische Maßnahmen (TOMs) in Stichpunkten, ein Absatz für das Verarbeitungsverzeichnis des Trägers (DA1, DA6).
- Brevo in den AVV-Nachweis aufnehmen (DA6).
- Die DPA-Verträge von Anthropic und Cloudflare als PDF ablegen und in beiden Konten die Organisation auf „Webklar GbR“ stellen (DA5).
**In Woche 1–2 des Tests:**
- Mehrere Fotos pro Brief, zählt als 1 Brief (BE4, ca. 1 Tag).
- Knopf „Drucken / als PDF speichern“ mit Druck-Ansicht und Teilen-Knopf (BE8, RA6).
- Knopf „Fertig – nächste Person“ (BE9, DA8).
- Verbindung nach Neuladen wiederherstellen (sessionStorage) für PC und Handy (BE1, TE5, RA9).
**Im Testmonat:**
- Die Bedienung in die Hauptsprachen übersetzen und von den Beraterinnen gegenlesen lassen (RA1, BE7, DA3).
- Aushang und Karten nur, wenn die MBE sie will (AN3).
**Laufender Betrieb:**
- Zweimal pro Woche `stand` prüfen und montags eine kurze Stand-Mail an die MBE schicken (BE3, AN4).
- Erinnerung 7 Tage vor Ablauf.
- Bei einer Zusage sofort `verlaengern`, erst danach die Rechnung schicken (AN2).
**Bis zum 26.11.:**
- Ein einseitiges Angebot und eine Rechnung (§ 19 UStG), am besten als Jahreskontingent.
- Die veraltete Zeile 334 in `outreach-september.md` korrigieren (AN5).
**Später:**
- Guthaben atomar in einem Durable Object führen (TE8).
- Das Token nicht mehr in den QR-Code legen (BE12).
- Eigene Adresse für den Worker, z. B. `api.behoerdenklar.de` (BE10).
- Tagesobergrenze pro Code (BE6).
- Tests für `/code` und `/uebergabe` in `npm run pruefung` (LI1, TE3).
- Eingabeprüfung im Werkzeug (TE11).
- Kleinigkeiten in der Datenschutzerklärung: 48 Stunden statt 30 Tage, E-Mail-Prüfsumme, Kontakt nicht über Gmail (DA9).
- Webseite und App mit derselben Ampel-Formel (LI2).
---
## 6. Größte Live-Risiken und Gegenmittel
| Risiko | Funde | Gegenmittel |
|---|---|---|
| Die zweisprachige Analyse läuft live zum ersten Mal und scheitert oder zeigt „Deutsch \| Deutsch“ | LI1, BE2, TE2 | Bis Sa/Mo deployen, 3 Generalproben, Korrektur M1b, Bildschirmfotos bereithalten, ruhig erklären |
| Die Analyse dauert deutlich länger als 1 Minute (nicht gemessen) | LI8 | Stoppuhr in der Probe, „ein bis zwei Minuten“ sagen, Datenschutz-Teil in die Wartezeit legen |
| Das Handy sperrt oder der Laptop schläft ein, das Ergebnis kommt nicht am Laptop an (**unsicher**, ob Safari dabei abbricht) | LI4 | Automatische Sperre „Nie“, `caffeinate -d`, im Notfall das Handy zeigen |
| Die Ampel zeigt Gelb statt Rot | LI2 | Formel reparieren und Termin im Musterbrief auf den 03.11. legen |
| Am Laptop ist kein Code eingelöst, das Handy landet in der Gratis-Demo oder ist gesperrt | LI3, LI5 | Vorführ-Code eingelöst, oben muss „Kostenlos über …“ stehen (Checkliste) |
| Eine Beraterin fotografiert spontan einen zweiten Brief oder Seite 2 | TE4, BE4 | Korrektur M1e und die vorbereitete ehrliche Antwort |
| „Versteht mein Klient, was er ankreuzt?“ oder „Brauchen wir einen AVV?“ | RA1, DA1–DA3 | S2 (Arabisch), Datenschutz-Blatt mit Rollenaussage, AVV zusagen |
| Das Netz der Einrichtung sperrt `workers.dev` (**unsicher**) | BE10 | Vorführen über den eigenen Hotspot, am Ende ein Test am Dienst-PC, bei Problemen die IT fragen |
| Der PC wird beim Warten neu geladen | BE1 | Warnung „Seite verlassen?“ (S4). Das Ergebnis steht trotzdem am Handy, dort auf Deutsch umschalten |
| Der QR-Code ist abgelaufen | LI10 | Erst live erzeugen, sonst neu klicken |
**Wichtigste Regel:** Was am Mi 28.10. nicht in der Generalprobe lief, kommt nicht mehr in den Code.
---
## Anhang: alle Funde im Detail
### Beraterin am PC
_Für die Vorführung am 29.10. ist der Ablauf kurz und überzeugend. Am PC wählt man die Sprache und klickt einmal, am Handy sind es etwa sechs Fingertipps. Danach steht die Erklärung am PC auf Deutsch und am Handy in der Muttersprache. Das gilt aber nur, wenn die zweisprachige Analyse live funktioniert, und die wurde noch nie getestet. Für den Alltag im Testmonat ist es aus Sicht einer Beraterin noch nicht bequem und robust genug. Lädt jemand die Seite neu oder schließt das Handy den Tab, geht still ein Brief verloren. Die Beraterin sieht weder den echten Reststand noch das Ablaufdatum. Es gibt keinen Ausdruck und keine mehrseitigen Briefe, die Handy-Seite ist nur auf Deutsch, und für Kolleginnen ohne Justin fehlt eine Anleitung. Die meisten Punkte lassen sich mit wenigen Stunden Arbeit beheben._
- **BE1 · Neu laden oder Handy-Tab weg: Ergebnis verloren, Brief trotzdem verbraucht** (mittel, stunden, vor Termin) — Am PC: Wer die Seite neu lädt (F5, aus Versehen, oder der Browser stellt sich nach einem Absturz wieder her), während das Handy fotografiert, verliert den Schlüssel der Übergabe. Das Handy schickt sein Ergebnis trotzdem ab, aber niemand holt es mehr ab. Es gibt keine Warnung 'Seite wirklich verlassen?'. Am Handy: Die Verbindung zum PC liegt nur im Arbeitsspeicher. Lädt der Handy-Browser die Seite neu, ist sie weg. Das kann passieren, wenn Android den Tab beim Öffnen der Kamera-App aus dem Speicher wirft; das ist ein Risiko und nicht getestet. Danach ist die Seite eine normale Gratis-Demo ohne Code und ohne Verbindung, eventuell mit E-Mail-Abfrage und Kaufangebot. Der PC zeigt dann 15 Minuten lang '✅ Handy verbunden'. Außerdem läuft die 15-Minuten-Uhr am PC ab Anzeige des QR-Codes und nicht ab der letzten Handy-Meldung: Fotografiert das Handy spät, gibt der PC auf, während das Handy noch analysiert. Jede Wiederholung kostet einen weiteren Brief aus den 100. *Beleg:* webseite/demo.html:1292 (pcWarten nur im Speicher), :806-809 (Handy legt Ergebnis ab, egal ob der PC noch wartet), :609 + :1507-1511 (Handy-Verbindung nur im Speicher, # wird sofort gelöscht), :1316 + :1337-1341 (Ablauf ab QR-Erzeugung), keine beforeunload-Warnung (grep: keine Treffer); proxy/src/index.ts:767/786 (Päckchen verfallen nach 15 Min.) *Vorschlag:* Vor dem Termin nur die billigen Teile (Minuten): 1) beforeunload-Warnung, solange pcWarten aktiv ist. 2) Bei jeder Status-Meldung vom Handy pcWarten.bis verlängern. 3) In die Kurzanleitung schreiben: 'Beim Warten nicht neu laden. Falls doch: Das Ergebnis steht am Handy, dort auf Deutsch umschalten.' sessionStorage-Wiederherstellung für PC und Handy erst nach dem Termin (Stunden).
- **BE2 · Der Kern der Vorführung (PC Deutsch, Handy Arabisch) ist ungetestet; bei einem Fehler zweimal 'Deutsch'** (hoch, stunden, vor Termin) — Genau der Moment, der die Beraterinnen überzeugen soll, wurde noch nie durchgespielt. Scheitert die Übersetzung mit Haiku, schickt der Worker zwei deutsche Fassungen. Dann zeigen PC und Handy zwei Knöpfe 'Deutsch | Deutsch', und die ratsuchende Person bekommt kommentarlos Deutsch statt Arabisch. Außerdem laufen Analyse (Sonnet) und Übersetzung (Haiku) nacheinander, die Seite verspricht aber 'bis zu einer Minute'. Wie lange es wirklich dauert, ist unbekannt. *Beleg:* proxy/src/index.ts:477-484 (Fallback: analyse und analyse_de beide deutsch, sprache 'de'); webseite/demo.html:857-861 (hängt analyse_de immer als zweite Version an, daher doppelter Knopf); index.ts:395 und :481 (nacheinander); demo.html:383 ('bis zu einer Minute'); laut Hintergrund lokal nicht getestet (kein KI-Schlüssel) *Vorschlag:* Wie vorgeschlagen. Ergänzung: Beim Probedurchlauf nach dem Deploy mindestens zwei Sprachen testen (Arabisch und Farsi) und das Handy einmal über mobile Daten verbinden. Bei 'Deutsch | Deutsch' sofort mit 'npx wrangler tail' ins Worker-Log schauen ('Übersetzung fehlgeschlagen'). Die Code-Korrektur (analyse_de nur bei sprache != 'de' anhängen, plus Hinweis am Handy) dauert Minuten und sollte vor dem Termin rein.
- **BE3 · Kontingent: Zahl veraltet, Ablaufdatum unsichtbar, leeres Kontingent fällt erst mitten in der Beratung auf** (mittel, stunden, vor Termin) — Als Beraterin sehe ich 'Kostenlos über MBE Donnersberg: 87 Analysen'. Diese Zahl stammt aus meinem eigenen Browser und wird nur durch meine eigenen Analysen kleiner. Nutzen Ratsuchende den Code zu Hause, merkt mein PC das nicht, die Zahl ist zu hoch. Ein Ablaufdatum sehe ich nirgends, obwohl der Server es mitschickt. Ist das Kontingent mitten in der Beratung leer, steht am Handy 'Bitte fragen Sie dort nach', obwohl die Person ja bei uns sitzt. Am PC steht '…aufgebraucht… Bitte dort noch einmal versuchen', und der PC wartet weiter, was sinnlos ist. Passiert es beim direkten Hochladen am PC, wird der Code aus meinem Browser gelöscht und das Kaufangebot für Privatleute erscheint. Nach dem Aufladen muss ich den Code neu eingeben. Eine Vorwarnung wie 'nur noch 5 Briefe' gibt es nicht, und Justin erfährt es auch nicht automatisch. Dazu kommt: Das Werkzeug setzt das Ablaufdatum auf 'heute + 1 Monat'. Wird der Code vor dem 26.10. angelegt, läuft er vor dem Auswertungstermin am 26.11. ab. *Beleg:* proxy/src/index.ts:742 (liefert 'ablauf') vs. webseite/demo.html:1257-1266 (ignoriert ihn); demo.html:596/645 (Zahl aus localStorage), :820 und :1368 (nur eigene Analysen aktualisieren); index.ts:446 (Server zieht für jedes Gerät ab); index.ts:329 + demo.html:766-768 + :1356 (widersprüchliche Meldungen); demo.html:783-790 (Code wird gelöscht) + :655 (Kauf-Block); tools/einrichtungs-code.js:46-50 + :81 (Ablauf = heute + 1 Monat) *Vorschlag:* In dieser Reihenfolge: 1) Code mit festem Datum anlegen: --bis 2026-11-30 (Sekunden). 2) Lesezeichen am Beratungs-PC genau auf behoerdenklar.de/demo?code=MBE-… setzen. Jedes Öffnen ruft dann /code auf (demo.html:1467) und holt den Stand frisch, das löst das 'veraltet' ohne Programmierung. 3) Bei kontingent_leer/abgelaufen den Code NICHT löschen, kein Kaufangebot zeigen, den PC das Warten beenden lassen und einen Kasten mit Justins Telefonnummer zeigen (Stunden). 4) Das Ablaufdatum im Banner anzeigen. 5) Zweimal pro Woche 'stand' prüfen.
- **BE4 · Mehrseitige Briefe gehen nicht, und Seite 2 landet nicht am PC** (hoch, tag, vor Termin) — Viele Bescheide (Jobcenter, Ausländerbehörde) haben mehrere Seiten. Am Handy kann man nur ein Foto pro Erklärung hochladen. Nach dem ersten Ergebnis hört der PC auf zu warten, das Handy bietet das Hochladen aber wieder an, und das Banner verspricht weiter 'erscheint hier und auf dem Bildschirm'. Ein Foto von Seite 2 erscheint also nur am Handy, kostet einen weiteren Brief und weiß nichts von Seite 1. Bei der Vorführung fällt das nicht auf, weil der Musterbrief einseitig ist. Die Frage kommt aber mit Sicherheit. *Beleg:* webseite/demo.html:331 (input ohne 'multiple'), :679 (nur files[0]), :1348-1352 (PC beendet das Warten nach dem ersten Ergebnis), :814 (Handy öffnet das Hochladen wieder), :1417 (Banner-Text bleibt); musterbrief-auslaenderbehoerde.html hat nur eine .seite *Vorschlag:* Bis 29.10.: Antwort in den Ablaufplan aufnehmen, aber ehrlich formuliert: 'Zurzeit ein Foto pro Erklärung: die Seite mit Betreff und Frist. Mehrseitig geht als Scanner-PDF direkt am PC, dann aber nur in EINER Sprache. Für die Beraterin Deutsch wählen.' Dazu den Handy-Banner nach dem ersten Ergebnis ändern (Minuten). Danach (etwa 1 Tag): 'Weitere Seite hinzufügen', bis zu 4 Fotos in einer KI-Anfrage, zählt als 1 Brief.
- **BE5 · Keine Kurzanleitung für den Beratungs-PC; der Code ist nach dem Termin schwer wiederzufinden** (mittel, stunden, vor Termin) — Der Handzettel wirbt, erklärt aber nicht, wie man es am PC benutzt. Der PC-Ablauf steht dort nur als ein Satz unter 'Was BehördenKlar kann'. Kolleginnen, die nicht beim Termin waren, wissen nicht, was sie klicken sollen. Das Code-Feld am PC steckt in einem zugeklappten Bereich ganz unten. Öffnet man den Link mit ?code=, verschwindet der Code sofort aus der Adresse. Ein Lesezeichen, das man danach anlegt, enthält ihn also nicht. Löscht der Dienst-Browser beim Schließen die Daten, ist der Code weg und muss gesucht werden. *Beleg:* marketing/praesentation-mbe/handzettel.html:93-176 (nur Werbung und Angebot), :125-128 (PC-Ablauf als Merkmal-Satz); webseite/demo.html:372-377 (Code-Feld in zugeklapptem details), :1467 (reinigeUrl entfernt ?code=) *Vorschlag:* Eine kurze Seite 'So geht's am Beratungs-PC' (5 Schritte, wie vorgeschlagen) ist gut und in Stunden machbar. Das Lesezeichen mit ?code= hat einen Zusatznutzen: Es holt bei jedem Öffnen den aktuellen Briefstand (siehe BE3). Bildschirmfotos erst nach dem Deploy machen, weil live noch der alte Stand läuft.
- **BE6 · Weitergabe des Codes an Ratsuchende ist ungeklärt und unbegrenzt** (mittel, stunden, vor Termin) — Auf dem Handzettel steht 'Code für Ihre Ratsuchenden' mit einem QR-Code zu demo?code=…. Diesen Link kann man weiterleiten, und jeder, der ihn öffnet, hat den Code dauerhaft gespeichert. Für Einrichtungs-Guthaben greift im Worker kein Tages- oder IP-Limit. Ein Link in einer WhatsApp-Gruppe kann also die 100 Briefe aufbrauchen, und die Beraterinnen merken es erst, wenn es leer ist (siehe BE3). Für Ratsuchende gibt es kein eigenes Material, weder Aushang noch Kärtchen, und nichts in ihrer Sprache. Der vorhandene Weg 'Zugang per QR vom PC aufs Handy' ist ein kleiner Link-Knopf mit der Käufer-Beschriftung 'Auch auf dem Handy nutzen'. *Beleg:* handzettel.html:173-175 + :191 (Code und Link für Ratsuchende); webseite/demo.html:1257-1259 + :1467 (Code wird dauerhaft gespeichert); proxy/src/index.ts:333-342 (Limits nur, wenn nicht bezahlt); demo.html:294 + :1392-1399 (a=g-Knopf) *Vorschlag:* Vor dem 29.10. entscheiden. Empfehlung für den Test: Der Code bleibt bei den Beraterinnen (Handzettel: 'Ihr Zugangscode für den Beratungs-PC' statt 'Code für Ihre Ratsuchenden'). Ratsuchende nutzen ihn nur während der Beratung über 'Lieber mit dem Handy fotografieren' (a=k, nur für die Sitzung). Den a=g-Knopf im Einrichtungs-Modus besser ausblenden oder deutlich als 'dauerhaft' kennzeichnen. Eine Tagesobergrenze im Worker ist eine sinnvolle Notbremse, aber nicht zwingend vor dem Termin, denn die 100 sind selbst schon die Obergrenze.
- **BE7 · Die Handy-Seite der Ratsuchenden ist komplett auf Deutsch, auch die Einwilligung** (mittel, tag) — Die gewählte Sprache ändert nur die KI-Erklärung. Was die ratsuchende Person am Handy bedienen muss, ist alles Deutsch: der Einwilligungstext mit dem Hinweis auf Anthropic (USA), der Start-Knopf, das Banner 'Mit dem Computer verbunden…', Kamera-Hinweise und Fehlermeldungen. In der Beratung muss ich die Einwilligung also erst übersetzen oder die Person setzt ein Häkchen, das sie nicht lesen kann. Menschlich und beim Datenschutz ist das wackelig. Übersetzte Texte gibt es für die Sprachseiten schon, aber nicht für die Demo. *Beleg:* webseite/demo.html:306-316 (Einwilligung), :332 (Knopf), :1417 (Banner), :728/:832 (Fehlermeldungen); :747 (Sprache geht nur an die KI); tools/sprachseiten/texte.js enthält z. B. kaufKonsens in allen Sprachen, aber keinen Upload-Konsens *Vorschlag:* Wenn das Handy per QR mit Sprache l≠de geöffnet wird, die etwa 8 Texte, die man bedienen muss, in der Zielsprache zeigen: Banner, Einwilligung, Start-Knopf, Kamera-Hinweis, Wartetext und 3 häufige Fehlermeldungen. Mit den häufigsten MBE-Sprachen anfangen (Arabisch, Farsi, Türkisch, Ukrainisch, Russisch, Kurdisch) und von Muttersprachlern gegenlesen lassen. Für den 29.10. reicht eine ehrliche Antwort: 'Die Bedienung am Handy wird als Nächstes übersetzt.'
- **BE8 · Kein Ausdruck, und am Handy ist das Ergebnis weg, sobald der Tab zu ist** (mittel, stunden, vor Termin) — Viele Ratsuchende wollen Papier, und für die Akte wäre ein Ausdruck praktisch. Es gibt aber weder einen Druck-Knopf noch Druck-Einstellungen. Mit Strg+P druckt man die ganze Seite mit Formular, Einwilligungstext und Fußzeile. Die Erklärungen der Fachbegriffe fehlen auf Papier, weil sie zugeklappt sind. Am Handy wird das Ergebnis nirgends gespeichert. Schließt die Person den Tab oder lädt neu, ist alles weg. Nur der Kalender-Eintrag bleibt. Der Handzettel verspricht aber 'Ergebnis zum Mitnehmen'. *Beleg:* webseite/demo.html: grep 'print' ohne Treffer; :905-917 (Fachbegriffe in zugeklappten details); :565-630 (localStorage speichert nur Sprache/Guthaben, kein Ergebnis); :1508-1511 (# wird gelöscht, Neuladen stellt nichts wieder her); handzettel.html:129-131 *Vorschlag:* Druck-Knopf mit @media print wie vorgeschlagen (Stunden). Er ist aber nicht zwingend vor dem Termin. Wenn die Zeit knapp ist, auf die Frage 'Kann man das ausdrucken?' ehrlich antworten und es danach bauen. Der Druck-Knopf am Handy ('Als PDF speichern') löst nebenbei das Aufbewahren.
- **BE9 · Nächste Person: Das Ergebnis der vorigen Person bleibt auf dem Bildschirm** (mittel, stunden) — Es gibt keinen Knopf 'Fertig'. Kommt die nächste Person an den Tisch, steht die Erklärung des vorigen Briefs weiter unten auf dem Bildschirm, mit Aktenzeichen und Telefonnummer, bis ein neues Ergebnis sie ersetzt. Auch die Sprache der vorigen Person bleibt eingestellt. Wer das übersieht, schickt der nächsten Person die falsche Sprache aufs Handy. *Beleg:* webseite/demo.html:879-927 (zeigeAnalyse ersetzt erst bei neuem Ergebnis, keine Zurücksetzen-Funktion); :1501 (Sprache wird dauerhaft gespeichert) *Vorschlag:* Wie vorgeschlagen. Bis der Knopf da ist, in die Kurzanleitung schreiben: 'Nach jeder Person die Seite neu laden und die Sprache prüfen.'
- **BE10 · Dienst-PC mit strengem Netz: Fehler bleiben stumm** (mittel, stunden, vor Termin) — Alle Anfragen gehen an behoerdenklar-proxy.behoerdenbrief.workers.dev, dazu an challenges.cloudflare.com. Ob der Web-Filter der Einrichtung workers.dev-Adressen durchlässt, ist unbekannt; manche Filter sperren diese Adressen. Scheitert das Abfragen am PC, wird der Fehler verschluckt. Die Beraterin sieht 15 Minuten '⏳ Warte auf das Handy …' und dann 'QR-Code ist abgelaufen', ohne Hinweis auf das Netzwerk. Außerdem greift die Seite an mehreren Stellen ohne Absicherung auf den Browser-Speicher zu. Sperrt die IT Website-Daten, bricht das Skript beim Start ab und kein Knopf funktioniert. Das ist eine Bedingung, die nicht getestet ist; die Sprach-Einstellung ist dagegen abgesichert. *Beleg:* webseite/demo.html:540-542 + webseite/uebergabe.js:15-16 (workers.dev); demo.html:534 (Turnstile); demo.html:1359-1361 (Abfragefehler werden still ignoriert), :618 (Handy-Meldungen an den PC ebenso); demo.html:594-597, :601-604, :629-630 (localStorage ohne try/catch) vs. :565-566 (mit try/catch) *Vorschlag:* Punkt 1 ist der wichtigste und kostet nur Minuten. Am Ende des Termins eine Beraterin bitten, den Link an IHREM Dienst-PC zu öffnen und einmal den Handy-Ablauf mitzumachen. Die Punkte 2 und 4 (Meldung nach mehreren Fehlversuchen, try/catch) dauern Minuten bis eine Stunde. Punkt 3 (eigene Adresse für den Worker) kommt später.
- **BE11 · Oberfläche im Einrichtungs-Modus: Gratis-Texte, falscher Hauptknopf, Sprachliste nur in fremder Schrift** (mittel, minuten, vor Termin) — Auch mit aktivem Code steht oben weiter '2 Analysen gratis', 'Antrag auf Klartext · 2 Gratis-Analysen' und 'Gratis-Demo'. Justin sagt aber '100 Briefe', das verwirrt. Der Knopf, den die Beraterin wirklich braucht ('Lieber mit dem Handy fotografieren'), ist der weiße Zweitknopf unter einem großen, ausgegrauten Hauptknopf. Der ist grau, weil das Häkchen nur für das direkte Hochladen gesetzt ist. Die Sprachliste zeigt nur die Eigennamen in Originalschrift und ist nicht alphabetisch sortiert. 'ትግርኛ' (Tigrinya) oder 'پښتو' (Paschtu) erkennt eine deutsche Beraterin nicht. *Beleg:* webseite/demo.html:268, :277, :284 (feste Gratis-Texte); :332-333 + :667 (Hauptknopf erst nach dem Häkchen aktiv, Handy-Knopf zweitrangig); :556-562 + :1494-1498 (nur Originalnamen) *Vorschlag:* Wie vorgeschlagen, aber die Reihenfolge nach Größe der Sprachgruppe beibehalten (nicht alphabetisch). Wichtiger als das Sortieren ist der deutsche Zusatz ('العربية – Arabisch', 'فارسی – Farsi/Dari', 'پښتو – Paschtu'). Aufwand: Minuten.
- **BE12 · QR-Code am Bildschirm wird mit Code doppelt so dicht** (niedrig, minuten, vor Termin) — Hat der PC einen Einrichtungs-Code, steckt das komplette Guthaben-Token im QR-Code. Der Code wird dadurch deutlich dichter: 61×61 statt 45×45 Punkte, bei fester Größe von 220 Pixeln nur 3,4 Pixel pro Punkt. Der weiße Rand ist nur 2 Punkte breit. Moderne Handys schaffen das. Ältere, günstige Handys von Ratsuchenden, ein spiegelnder Laptop-Bildschirm oder verkleinerte Browser-Ansicht machen das Scannen aber zäher. *Beleg:* Selbst nachgerechnet mit webseite/qrcode.js: a=k mit Code = 217 Zeichen, QR-Version 11 (61 Module), 3,38 px/Modul; ohne Code = 113 Zeichen, Version 7, 4,49 px/Modul. webseite/demo.html:218 (220 px), :1301 (Token im QR), webseite/uebergabe.js:113 (margin 2) *Vorschlag:* Nur .qr-box .qr auf etwa 280 bis 300 px vergrößern (eine Zeile CSS). Den margin zu ändern ist unnötig. Das Token aus dem QR herauszunehmen kann warten.
### Ratsuchende mit wenig Deutsch
_Wenn die Beraterin daneben sitzt (QR am PC scannen, a=k), funktioniert der Ablauf: Nach etwa 6 Fingertipps ist der Brief fotografiert, das Ergebnis steht auf Arabisch am Handy und auf Deutsch am PC. Allein zu Hause, mit dem Code vom Handzettel, bleibt ein Ratsuchender mit wenig Deutsch aber an mehreren Stellen hängen. Bis auf den Ergebnistext ist alles auf Deutsch: Einwilligung, Knöpfe, Hinweise, Fehler und Kartenüberschriften. Die Erklärungssprache steht auf Deutsch, obwohl das Handy auf Arabisch eingestellt ist, und nach dem Schließen der Seite ist das Ergebnis weg. Dazu kommen kleine Fehler bei Arabisch (rechts-nach-links) und eine fehlende Übersetzung in der Anruf-Hilfe. Beides sieht man genau in der geplanten Arabisch-Vorführung, und beides lässt sich in Minuten beheben._
- **RA1 · Handy-Seite ist bis auf das Ergebnis komplett Deutsch, auch die Pflicht-Einwilligung** (hoch, tag, vor Termin) — Nach dem QR-Scan sieht der Ratsuchende nur deutschen Text: den Kopf 'Probieren Sie es aus … 2 Analysen gratis', das Verbunden-Banner, die lange Einwilligung (Häkchen ist Pflicht, sonst bleibt der Knopf grau), den Knopf 'Brief auswählen & erklären lassen', 'Ihr Brief wird analysiert…', die Hinweise zu Kamera und Galerie und alle Kartenüberschriften im Ergebnis ('Einfach erklärt', 'Das müssen Sie tun', 'Anrufen', 'Danach werden Sie gefragt', 'Noch 7 Tage Zeit', 'In meinen Kalender eintragen', Rechtshinweis). Wer kaum Deutsch liest, hakt blind an oder bleibt stecken. Bei der Einwilligung nach Art. 9 DSGVO ist das außerdem heikel: Wer den Text nicht lesen kann, willigt nicht wirklich informiert ein. Das ist ein Hinweis, keine Rechtsauskunft. *Beleg:* demo.html:2 (lang="de", kein dir), 306-316 (Einwilligung), 332, 382-383, 446-473, 849, 1083-1084, 1122-1131, 1417. Gerendert bei 390 px Breite mit Handy-Sprache Arabisch: Alles außer den KI-Texten ist deutsch (Screenshots ak.png und am2.png im Scratchpad). Übersetzungen für die Demo-Seite gibt es nicht; tools/sprachseiten/texte.js deckt nur die Startseiten ab. *Vorschlag:* Vor dem 29.10. nur Arabisch, und nur das Nötigste: Einwilligung, Startknopf, Verbunden-Banner, Wartetext, 'Verbindung fehlgeschlagen'. Dazu dir='rtl' am Container, wenn die Sprache ar/fa/ur/ps ist. Den Rest wie die Sprachseiten nachziehen. Im Termin auf jeden Fall offen sagen: 'Die Bedienung ist noch deutsch, die Erklärung kommt in der Muttersprache.' Rechne damit, dass jemand nach der Einwilligung fragt: Die Beraterin kann sie im Moment vorlesen oder übersetzen.
- **RA2 · Arabisch (rechts-nach-links) an mehreren Stellen falsch dargestellt, sichtbar in der Vorführung** (mittel, stunden, vor Termin) — Vier Stellen sind betroffen. (1) Die Briefart-Plakette über der Kernaussage nutzt Courier mit letter-spacing. Dadurch werden arabische Buchstaben auseinandergerissen ('ر س ا ل ة' statt 'رسالة') und sind schwer lesbar. (2) In der Ampel-Zeile und der Frist-Zeile hängt der arabische Text hinter deutschem 'Noch 7 Tage Zeit · ' bzw. '⚠️ Frist: …'. Die Zeile läuft deshalb links-nach-rechts, und der Schlusspunkt sowie die Klammern landen auf der falschen Seite. (3) Die arabische Erklärung der Fachbegriffe ist linksbündig, der Punkt steht vorne. (4) Bei den Checkbox-Kästchen fehlt der Abstand zum Text. Eine arabischsprachige Beraterin sieht das sofort. *Beleg:* Headless-Chrome-Screenshot bei 390 px mit arabischem Beispielergebnis über eine echte lokale a=m-Übergabe (crop1.png, crop2.png). Code: demo.html:173-178 (.karten-label mit letter-spacing und Courier, 885 setzt dort brieftyp ein); 442 und 896 (ergAmpel ohne dir, computed direction 'ltr'); 1058 und 1083 (dir=auto, aber die Zeile beginnt mit 'Frist:', also ltr); 912-913 (p ohne dir, beobachtet: getAttribute('dir') = null); 186 (margin-right statt margin-inline-end). *Vorschlag:* #ergBrieftyp (oder .karten-label allgemein, sobald KI-Text darin steht): font-family:inherit oder eine Systemschrift, dazu letter-spacing:0 und text-transform:none. Die Schrift ist der eigentliche Fehler. Den Rest wie vorgeschlagen: Fachbegriff-<p> mit dir=auto, KI-Text in Ampel- und Fristzeile in ein eigenes <bdi>/<span dir=auto>, .todo::before mit margin-inline-end. Danach einmal auf dem eigenen Vorführ-Handy mit Arabisch ansehen. Aufwand eher unter einer Stunde.
- **RA3 · 'Ergebnis aufs Handy mitnehmen': Telefonsätze kommen ohne Übersetzung an** (mittel, minuten, vor Termin) — Ob unter dem deutschen Telefonsatz die Bedeutung in der Muttersprache steht, hängt an der gespeicherten Sprache des Handys, nicht an der Sprache des Ergebnisses. Der Mitnehmen-QR (a=m) überträgt keine Sprache. Auf einem fremden Handy, das noch nie auf der Seite war, steht die Sprache auf 'de'. Der Ratsuchende bekommt also ein arabisches Ergebnis, aber die Anruf-Hilfe zeigt nur deutsche Sätze, die er nicht versteht. Bei der Vorführung fällt das nicht auf: Justins Handy hat durch den a=k-Schritt vorher schon 'ar' gespeichert. *Beleg:* demo.html:1133 (mitBedeutung = zielsprache() !== 'de'); 1383 (Mitnehmen-QR nur mit a, s, k, ohne l). Beobachtet auf einem frischen Handy-Profil mit arabischem Ergebnis: Sprachauswahl 'de', Anzahl .anruf-bed = 0, nur '„Guten Tag, mein Aktenzeichen ist AB-123/26.“' ohne arabische Zeile. *Vorschlag:* zeigeAnruf die Sprache der gerade angezeigten Version mitgeben: mitBedeutung = versionen[aktiveVersion].code !== 'de'. Zusätzlich l= in den Mitnehmen-QR schreiben und am Handy übernehmen wie bei a=k (Zeilen 1413-1416). Danach den Ablauf einmal mit einem Inkognito-Fenster am Handy testen.
- **RA4 · Code vom Handzettel: Erklärung kommt auf Deutsch, obwohl das Handy auf Arabisch steht** (mittel, minuten, vor Termin) — Wer den Handzettel-QR (/demo?code=…) zu Hause scannt, löst den Code ein. Die Erklärungssprache bleibt aber auf 'Deutsch'. Die Seite fragt die Sprache des Handys nicht ab. Die Auswahl ist deutsch beschriftet ('Erklärung in dieser Sprache:') und wird leicht übersehen. Die erste Analyse aus dem Kontingent der MBE kommt dann auf Deutsch heraus, also verschwendet. *Beleg:* demo.html:563-568 (Rückfall immer 'de', navigator.language wird nirgends gelesen, per grep geprüft). Beobachtet mit Handy-Sprache Arabisch und ?code=TEST-AB23: navigator.language = 'ar', Sprachauswahl = 'de'. *Vorschlag:* Wie vorgeschlagen: In zielsprache() vor dem Rückfall navigator.languages durchgehen (Kürzel vor dem Bindestrich) und die erste Sprache nehmen, die es in SPRACH_NAMEN gibt. Die erkannte Sprache NICHT in localStorage speichern, nur verwenden. Dann gewinnt eine spätere Auswahl von Hand. Steht das Handy auf Deutsch, bleibt alles wie bisher.
- **RA5 · Code abtippen: Leerzeichen statt Bindestrich, fehlender Bindestrich oder arabische Ziffern scheitern und zählen als Fehlversuch** (niedrig, minuten, vor Termin) — Wer 'MBE K7QX' (mit Leerzeichen) oder 'MBEK7QX' tippt, bekommt 'Diesen Code kennen wir nicht', also eine deutsche Meldung. Arabische und persische Handy-Tastaturen tippen Ziffern oft als ٧ bzw. ۷. Diese fallen durch die Prüfung [A-Z0-9-]. Jeder Fehlversuch zählt zur Sperre von 10 pro IP und Tag. In der Beratungsstelle teilen sich alle dasselbe WLAN, also dieselbe IP. Nach ein paar Tippfehlern ist das Einlösen für den Rest des Tages für alle gesperrt, auch über den QR-Link. Dazu kommt: Das Code-Feld steckt in einem eingeklappten, deutsch beschrifteten Bereich ganz unten im Kasten. *Beleg:* proxy/src/index.ts:719 (entfernt Leerzeichen, setzt aber keinen Bindestrich ein), 729 (Prüfung /^[A-Z0-9-]{4,40}$/, dann exakter Abgleich), 723-733 (Fehlversuch-Zähler pro IP). Lokal getestet: 'TEST AB23' → code_unbekannt, ' test-ab23 ' → ok. Hinweis: Mein Test hat den lokalen Zähler für ::1 heute um 1 erhöht (vorher 5). demo.html:372-377 (details am Ende). *Vorschlag:* Im Worker (und gleich auf der Seite): alles außer A-Z0-9 entfernen und den Bindestrich vor den letzten 4 Zeichen wieder einsetzen. Das deckt Leerzeichen und den fehlenden Bindestrich ab. Die Umwandlung arabischer Ziffern ist optional. Nicht dringend vor dem Termin, auf dem Handzettel steht der QR.
- **RA6 · Ergebnis ist nach dem Schließen weg, es gibt keinen Weg zurück zur Erklärung** (mittel, stunden) — Das Ergebnis lebt nur in der offenen Seite. Es wird nirgends gespeichert und hat keinen Teilen- oder Speichern-Knopf. Nach a=k entfernt die Seite den QR-Teil aus der Adresse, nach a=m wird die Übergabe sofort gelöscht (einmalig, 15 Minuten). Wenn der Ratsuchende den Browser schließt, das Handy aufräumt oder die Seite neu lädt, sieht er wieder nur die deutsche Startansicht 'Probieren Sie es aus'. Die Erklärung, die To-do-Liste und die Telefonsätze sind weg. Ein Beraterinnen-Satz wie 'Schauen Sie es sich zu Hause noch mal an' lässt sich nicht einlösen. Nur den Kalender-Eintrag kann man behalten. *Beleg:* demo.html:856-863 (letzteDaten nur im Speicher), per grep: kein localStorage oder sessionStorage für Ergebnisse; 1507-1511 (Adresse wird bereinigt); 1440 (a=m löscht nach dem Abholen); 1437 ('gilt 15 Minuten und nur einmal'). *Vorschlag:* Vor dem Termin: Den Satz im Ablaufplan (Zeile 75) ehrlich machen, z. B. 'Das Ergebnis bleibt im Browser, solange die Seite offen ist. Wichtig ist der Kalender-Eintrag, der bleibt.' Nach dem Termin: zuerst einen Knopf 'Teilen/Speichern' mit navigator.share einbauen. Es wird nichts gespeichert, der Nutzer entscheidet selbst. Erst danach über localStorage nachdenken, dann mit Löschen-Knopf und einem Satz in der Datenschutzerklärung.
- **RA7 · Am Handy liegt der Foto-Knopf über einen Bildschirm tief, ist grau und erklärt nicht, warum** (mittel, minuten, vor Termin) — Nach dem QR-Scan zeigt der erste Bildschirm nur den deutschen Werbekopf 'Probieren Sie es aus … 2 Analysen gratis', obwohl die Einrichtung zahlt. Das Verbunden-Banner beginnt bei 588 px, das Häkchen bei 816 px (unterer Rand) und der Foto-Knopf bei 1060 px. Er ist ausgegraut, bis man die deutsche Einwilligung anhakt. Antippen tut nichts, es kommt keine Erklärung. Zählung bis zum Foto: Link öffnen, scrollen, Häkchen, Knopf, Auswahl Kamera/Galerie (Android, weil kein capture-Attribut gesetzt ist), Foto, 'OK'. Das sind etwa 6-7 Schritte. Mit Beraterin daneben ist das machbar, allein ist es schwer. Beim Handzettel-Link liegt der Knopf sogar bei 1182 px. *Beleg:* Gemessen in Handy-Emulation 390×844: bannerTop 588, konsensTop 816, knopfTop 1060, knopfDisabled true; mit ?code=: knopfTop 1182. Code: demo.html:271-279 (Kopf), 284 ('2 Gratis-Analysen'), 331 (input ohne capture), 332 (disabled), 667. *Vorschlag:* Im Handy-Modus (handyModus/leseModus) nach dem Verbinden verbundenBanner.scrollIntoView() aufrufen und den Kopf ausblenden. Den Knopf nicht deaktivieren: Beim Tippen ohne Häkchen das Häkchen markieren und hinscrollen. capture nur bei a=k und dann den Galerie-Hinweis für diesen Fall ausblenden. Zusätzlich im a=k-Modus kaufBlock und codeTeil immer ausblenden, auch ohne w.
- **RA8 · Fehlerfälle am Handy: deutsche Meldung, PC wartet bei Netzabbruch endlos, gleiches Foto nochmal auswählen tut nichts** (mittel, minuten, vor Termin) — (1) Alle Fehlermeldungen vom Server und von der Seite sind deutsch, z. B. 'Die Analyse ist fehlgeschlagen. Bitte versuchen Sie es mit einem neuen, gut beleuchteten Foto.' oder 'Verbindung fehlgeschlagen …'. (2) Reißt am Handy die Verbindung ab, landet der Fehler im catch-Zweig. Dieser meldet dem PC nichts. Der PC zeigt dann bis zu 15 Minuten '⏳ Das Handy hat fotografiert. Der Brief wird erklärt …', und die Beraterin merkt nicht, dass etwas schiefging. (3) Nach einem Fehler wie 'KI ausgelastet' wählt der Ratsuchende oft dasselbe Foto aus der Galerie. Das Datei-Feld wird nie zurückgesetzt, und Browser melden dieselbe Auswahl nicht erneut als Änderung. Es passiert dann nichts. *Beleg:* proxy/src/index.ts:284-474 (alle Meldungen deutsch). demo.html:828-833 (catch ohne meldeAnPc; im Gegensatz zu 764, wo der PC informiert wird), 751 (Status 'analyse' wird vorher gesendet), 1354-1357, 1337-1341 (Abbruch erst nach 15 Minuten). Per grep: kein dateiFeld.value = '' in der Datei, change-Listener in 678-680. Punkt (3) ist bekanntes Browser-Verhalten, auf einem echten Gerät nicht getestet. *Vorschlag:* Wie vorgeschlagen (meldeAnPc im catch, dateiFeld.value = '' vor dateiFeld.click()). Zusätzlich robuster am PC: Steht der Status länger als ca. 3 Minuten auf 'analyse', zeigt der PC 'Am Handy hat es wohl nicht geklappt, bitte dort neu versuchen'. Das hilft auch, wenn das Handy gar nichts mehr senden kann. Im Worker den Anthropic-fetch in try/catch setzen, damit immer JSON mit code 'ki' zurückkommt.
- **RA9 · Älteres Android: Wird der Tab beim Öffnen der Kamera neu geladen, ist die Verbindung zum PC weg (Risiko)** (niedrig, stunden) — Auf Handys mit wenig Arbeitsspeicher kann Android den Browser-Tab beenden, während die Kamera-App offen ist. Danach lädt der Tab neu. Die Seite hat die Übergabe-Daten aber schon beim Start aus der Adresse gelöscht und nirgends behalten. Nach dem Neuladen ist das Handy nicht mehr verbunden. Es zeigt die normale Gratis-Demo (mit Kaufbereich), das Guthaben vom PC fehlt, und der PC wartet weiter. Für den Ratsuchenden sieht das aus wie 'kaputt'. *Beleg:* demo.html:1507-1511 (Adresse wird beim Start bereinigt), 609 und 1412 (handyModus inkl. Guthaben w nur im Speicher), 652-655 (ohne Guthaben wird der Kaufbereich gezeigt). Das Neuladen selbst ist geräteabhängig und hier nicht auf echter Hardware beobachtet. *Vorschlag:* sessionStorage für a/s/k/l/d/w wie vorgeschlagen, mit Zeitstempel und Ablauf nach 15 Minuten. Nicht vor dem Termin nötig. Für den Termin reicht der Hinweis an die Beraterinnen: 'Wenn das Handy die Verbindung verliert, einfach am PC neu auf „Lieber mit dem Handy fotografieren“ tippen.'
### Live-Vorführung am 29.10.
_Der Ablaufplan passt im Großen zum Code: Die drei Knöpfe heißen genau so, und die Aufteilung (Laptop zeigt zuerst Deutsch, Handy zuerst Arabisch, dazu ein Umschalter) ist richtig gebaut. Drei Dinge können die Vorführung aber kippen. Erstens ist der Kern der Vorführung, die zweisprachige Analyse übers Handy, noch nie mit echter KI gelaufen und auch nicht deployed. Zweitens zeigt die Ampel für den Musterbrief vor 12 Uhr Gelb mit „Noch 8 Tage“ statt Rot. Drittens kommt das Ergebnis nur über das Handy auf den Laptop, das Handy darf in der Wartezeit also nicht in die Sperre gehen. In 10–15 Minuten ist das machbar, die 5 Minuten für die Live-Vorführung sind aber knapp. Für die Beraterinnen am PC ist es bequem. Für Ratsuchende am Handy ist es noch umständlich: deutsche Bedienung, eine deutsche Einwilligung zum Anhaken und mehrere Schritte._
- **LI1 · Kern der Vorführung nie mit echter KI getestet, nicht deployed, Prüfskript prüft das Neue nicht** (blocker, stunden, vor Termin) — Vorgeführt wird genau der Weg, der noch nie gelaufen ist: Laptop zeigt den QR-Code, das Handy fotografiert, dann zweisprachige Analyse (Sonnet auf Deutsch, danach Übersetzung mit Haiku). Lokal gibt es keinen KI-Schlüssel, und live läuft noch der alte Stand ohne /code und /uebergabe. Der Ablaufplan setzt „Deploy erledigt“ erst auf den Vorabend. Bleibt dann ein Fehler übrig, ist keine Zeit mehr zum Reparieren. Außerdem prüft „npm run pruefung alles grün“ die neuen Funktionen gar nicht. Wird nur die Webseite deployed und nicht der Worker, sieht man live den Handy-Knopf, aber die Übergabe geht nicht. *Beleg:* ablaufplan.html:113 (Deploy erst „Am Vorabend“). tools/pruefung.js: Kein Test ruft /code oder /uebergabe auf (grep findet nur hole(BASIS + '/demo') in Zeile 175). index.ts:479-484 (Übersetzungsweg, lokal ohne Schlüssel nicht ausführbar). demo.html:1326 und uebergabe.js:16 (Seite fragt live den Worker unter /uebergabe/ ab). *Vorschlag:* Wie vorgeschlagen: bis spätestens Mo 26.10. zuerst den Worker deployen, dann die Webseite, dann 2–3 komplette Durchläufe auf dem Live-System (gleiche Geräte, Hotspot, gedruckter Musterbrief, Arabisch, Zeit stoppen, Screenshots). Dazu von Hand prüfen: falscher Code → „Diesen Code kennen wir nicht“, Laptop zeigt nach dem Scan „Handy verbunden“, am Ende zwei Sprachknöpfe „Deutsch“ und „العربية“ (nicht zweimal „Deutsch“). Später einen kleinen Test für /code und /uebergabe in npm run pruefung ergänzen.
- **LI2 · Ampel zeigt am 29.10. vormittags Gelb mit „Noch 8 Tage“, nicht Rot** (hoch, minuten, vor Termin) — Die Web-Ampel rechnet vom jetzigen Zeitpunkt bis 12:00 Uhr am Zieltag und rundet auf. Für den Termin am 05.11.2026 kommt deshalb vor 12 Uhr „8 Tage“ heraus, also Gelb. Erst ab 12 Uhr sind es 7 Tage und damit Rot. Der Ablaufplan verspricht „rote Ampel (Termin in 7 Tagen)“. Findet die Präsentation vormittags statt, stimmt der wichtigste Moment nicht. Dazu kommt: Die App zählt anders, dort ist ein Abstand von genau 7 Tagen Gelb. Der Handzettel sagt dagegen „Rot bei 7 Tagen oder weniger“. *Beleg:* demo.html:843-846 (Math.ceil(...T12:00:00 - jetzt), rot bei ≤ 7). Mit derselben Formel nachgerechnet (TZ=Europe/Berlin): 29.10. 09:00 und 11:59 → 05.11. = 8 Tage, GELB. 29.10. 12:01 und 15:00 → 7 Tage, ROT. App: src/utils/ampel.ts:22-27 und 113 (Kalendertage, rot nur bei < 7). ablaufplan.html:74, handzettel.html:115, musterbrief-auslaenderbehoerde.html:112. *Vorschlag:* Besser die Formel reparieren als nur den Brief ändern, denn ein Deploy kommt sowieso. In demo.html ampel() wie tageBis() in ampel.ts in Kalendertagen rechnen (heute 00:00 bis Zieltag 00:00, Math.round). Dann gilt am 29.10. für den 05.11. zu jeder Uhrzeit „Noch 7 Tage“ und Rot, und am Fristtag erscheint „HEUTE“. Zusätzlich darf man den Termin im Musterbrief auf Dienstag, 03.11.2026, 10:30 legen, das ist dann doppelt sicher. Danach neu drucken und im Ablaufplan „Termin in 7 Tagen“ durch „Termin nächste Woche“ ersetzen.
- **LI3 · Den „Vorführ-Code“ gibt es nirgends, und der MBE-Code kann vor dem Auswertungstermin ablaufen** (mittel, minuten, vor Termin) — Der Ablaufplan setzt einen eingelösten Vorführ-Code voraus. In der Checkliste für den Vorabend steht aber nur der MBE-Code. Ohne Code am Laptop enthält der QR-Code kein Guthaben, und das Handy fällt in die Gratis-Demo. Nach zwei Übungsläufen mit demselben Handy blendet die Seite den Hochladen-Bereich aus und meldet „Ihre 2 Gratis-Analysen sind verbraucht“. Dann scheitert die Vorführung. Zweites Problem: „--monate 1“ rechnet ab dem Tag, an dem der Code angelegt wird. Legt Justin den MBE-Code heute an, endet er am 22.11., also vor dem Auswertungstermin am 26.11. Drittens ist der Handzettel noch ohne Code gedruckt: Dort steht „wird beim Termin eingetragen“, und der QR-Code führt nur auf /demo. *Beleg:* ablaufplan.html:70 und 123 („Vorführ-Code eingelöst“) gegen ablaufplan.html:114 (nur „MBE-Code angelegt“). demo.html:1301 (Guthaben kommt nur mit hatGuthaben() in den QR-Code). demo.html:657-661 (ab 2 Gratis-Analysen wird die Eingabe gesperrt). tools/einrichtungs-code.js plusMonate() rechnet ab heute. handzettel.html:190 (CODE = ''). handzettel.png zeigt „wird beim Termin eingetragen“. *Vorschlag:* Den Vorführ-Code ausdrücklich in die Checkliste „Am Vorabend“ schreiben und vor den Übungsläufen anlegen. Beide Codes mit festem Enddatum anlegen (Option --bis gibt es, einrichtungs-code.js:81), z. B. MBE mit --briefe 100 --bis 2026-11-30 und VORF mit --briefe 30 --bis 2026-11-30. Danach den MBE-Code in handzettel.html eintragen, das PDF neu erzeugen und prüfen, dass der QR-Code auf /demo?code=MBE-… zeigt.
- **LI4 · Das Ergebnis kommt nur über das Handy auf den Laptop, das Handy darf in der Wartezeit nicht sperren** (mittel, minuten, vor Termin) — Der Laptop bekommt das Ergebnis nicht vom Server, sondern nur, wenn das Handy es nach der Analyse weiterschickt. Das Handy muss also die ganze Wartezeit mit offener Seite im Vordergrund bleiben. Bei vielen iPhones geht der Bildschirm nach 30 Sekunden aus, im Stromsparmodus immer. Die Analyse dauert aber wahrscheinlich länger. Ob Safari die laufende Anfrage nach dem Sperren noch zu Ende bringt, ist nicht getestet. Wenn nicht: Das Handy zeigt „Verbindung fehlgeschlagen“, der Laptop bleibt bei „Der Brief wird erklärt …“ hängen, und die Analyse ist trotzdem schon abgebucht. Ähnlich beim Laptop: Geht er in den Ruhezustand, hört er auf nachzufragen. *Beleg:* demo.html:753-758 (Handy wartet auf /demo). demo.html:806-809 (erst danach meldeAnPc(..., 'ergebnis')). demo.html:1335-1358 (der PC fragt nur das Übergabe-Fach ab, alle 1,5 s). index.ts:444-446 (Guthaben wird auf dem Server abgezogen, egal ob das Handy die Antwort noch bekommt). ablaufplan.html:124 nennt nur „Handy entsperrt“, nicht die automatische Sperre. *Vorschlag:* In „Kurz vor dem Termin“ ergänzen: Am iPhone Einstellungen → Anzeige & Helligkeit → Automatische Sperre „Nie“ und den Stromsparmodus aus. Das Handy während der Analyse in der Hand behalten und nicht in eine andere App wechseln. Am Mac den Ruhezustand verhindern, z. B. im Terminal mit „caffeinate -d“. In den Notfallplan schreiben: „Laptop hängt, aber das Handy zeigt das Ergebnis → einfach das Handy zeigen.“
- **LI5 · Beratungs-PC ohne eingelösten Code: Ratsuchende landen in der Gratis-Demo mit Kaufangebot** (hoch, minuten, vor Termin) — Das „Foto mit dem Handy“ läuft nur dann über das Kontingent der Einrichtung, wenn der Code vorher am Beratungs-PC eingelöst wurde. Sonst steckt im QR-Code kein Guthaben. Das Handy der ratsuchenden Person nutzt dann ihre 2 Gratis-Analysen, fragt beim zweiten Mal nach der E-Mail und zeigt sofort den Abschnitt „Guthaben kaufen“ mit 2,99 €. Das widerspricht „Ratsuchende zahlen nichts“. Auf dem Handzettel steht nur „Code für Ihre Ratsuchenden“. Dass der Code auch am PC der Beraterin eingegeben werden muss, steht nirgends. Gerade die als NEU beworbene Funktion „Beratung am PC“ geht im Testmonat so leicht schief. *Beleg:* demo.html:1301 (w nur mit hatGuthaben()). demo.html:612-615 (Handy ohne Guthaben nutzt die Gratis-Stufe). demo.html:652-655 (kaufBlock wird in der Gratis-Stufe immer gezeigt). handzettel.html:125-128 und 173. *Vorschlag:* Im Termin, Abschnitt 9–11', den Code gleich live an einem PC der Beraterinnen einlösen („Ich richte es Ihnen sofort ein, eine Minute“). Das wirkt bequem und vermeidet den Fehler. Auf den Handzettel einen Satz setzen: „Einmal am Beratungs-PC unter ‚Sie haben einen Code von einer Beratungsstelle?‘ eingeben, dann ist auch ‚Mit dem Handy fotografieren‘ kostenlos.“ Danach den Handzettel neu erzeugen.
- **LI6 · Ampel-Zeile verbindet das Termin-Datum mit der Aufgabe aus der Frist** (mittel, minuten, vor Termin) — Die Ampel nimmt das früheste Datum, beim Musterbrief also den Termin am 05.11. Dahinter hängt sie aber immer die Aufgabe aus der Frist. Heraus kommt so etwas wie „Noch 7 Tage Zeit (05.11.2026) · Unterlagen einreichen“, obwohl die Unterlagen bis zum 12.11. reichen. Beraterinnen arbeiten jeden Tag mit Fristen und sehen das sofort. Das kostet Vertrauen, ausgerechnet beim Punkt „Frist erkannt“. *Beleg:* demo.html:839-842 (Frist- und Termin-Datum werden gemischt und sortiert, das erste gewinnt). demo.html:849 (Text mit daten[0]). demo.html:894-896 (hängt immer frist.aktion an). Musterbrief: Termin 05.11. (Zeile 112), Frist 12.11. (Zeile 109). *Vorschlag:* In ampel() mitschreiben, welches Datum gewonnen hat, und „Termin am 05.11.“ bzw. „Frist bis 12.11.“ anzeigen. frist.aktion nur anhängen, wenn das früheste Datum wirklich die Frist ist. Das ist eine kleine Änderung. Sonst im Vortrag selbst erklären: „Die Ampel richtet sich nach dem nächsten Datum, hier dem Termin.“
- **LI7 · Am Handy viele Schritte, Bedienung und Einwilligung nur auf Deutsch** (mittel, minuten, vor Termin) — Nach dem Scannen öffnet sich am Handy die ganze Demo-Seite. Oben steht „Probieren Sie es aus … 2 Analysen gratis“, der Hinweis „Mit dem Computer verbunden“ liegt weiter unten, und die Seite scrollt nicht von selbst dorthin. Danach muss man die Einwilligung anhaken (vorher bleibt der Knopf grau), auf „Brief auswählen“ tippen, „Foto aufnehmen“ wählen, fotografieren und „Foto verwenden“ antippen. Im Ablaufplan steht nur „Musterbrief fotografieren“. Für Ratsuchende sind die Einwilligung (Daten an Anthropic in den USA), alle Kartentitel, der Kalender-Knopf, die Ampel („Noch 7 Tage Zeit“) und der Hinweis „ersetzt keine Rechtsberatung“ auf Deutsch. Nur der Inhalt erscheint auf Arabisch. Beraterinnen der Migrationsberatung fragen sehr wahrscheinlich: „Versteht mein Klient, was er da ankreuzt?“ *Beleg:* demo.html:272-284 (Kopf und „2 Gratis-Analysen“, auch im Handy-Modus sichtbar). demo.html:306-316 (Einwilligung nur auf Deutsch). demo.html:667 (Knopf erst nach dem Häkchen aktiv). demo.html:1411-1422 (Handy-Modus ohne Scrollen zum Banner). demo.html:446-473 und 485-488 (deutsche Titel). demo.html:847-849 (Ampeltext fest auf Deutsch). *Vorschlag:* Vor dem Termin die genauen Handy-Schritte in den Ablaufplan schreiben und üben: scrollen → Häkchen → Brief auswählen → Foto aufnehmen → Foto verwenden. Klein und schnell zu bauen: im Handy-Modus mit verbundenBanner.scrollIntoView() zum Banner springen. Für die Frage eine ehrliche Antwort bereithalten: „Inhalt in der Muttersprache, die Bedienknöpfe und die Einwilligung übersetze ich als Nächstes.“ Danach den Text der Einwilligung und die Kartentitel in die 27 Sprachen übersetzen, z. B. über texte.js.
- **LI8 · Wartezeit nicht gemessen, „etwa eine Minute“ ist für zwei KI-Aufrufe nacheinander gewagt** (mittel, minuten, vor Termin) — Bei der zweisprachigen Analyse laufen zwei KI-Aufrufe nacheinander: erst Sonnet mit dem Bild, danach Haiku mit der ganzen Antwort. Der Server antwortet erst, wenn beide fertig sind. Dazu kommen das Hochladen über den Hotspot und bis zu 1,5 s, bis der Laptop das nächste Mal nachfragt. Ablaufplan, Handzettel und Seite versprechen „etwa“ bzw. „bis zu einer Minute“. Wie lange es wirklich dauert, hat niemand gemessen, denn lokal gibt es keinen Schlüssel. Außerdem: Scheitert nur die Übersetzung, zeigt das Handy Deutsch statt Arabisch, und im Umschalter steht zweimal „Deutsch“. Ausgerechnet die arabische Fassung würde dann in der Vorführung fehlen. *Beleg:* index.ts:395-419 (Sonnet) und danach index.ts:479-484 (Haiku, nacheinander). Bei Fehlschlag gilt ausgabeSprache = 'de', während analyse_de weiter mitgeschickt wird. demo.html:857-861 legt dann zwei Fassungen mit dem Code 'de' an. demo.html:383 („bis zu einer Minute“). ablaufplan.html:72. handzettel.html:103. demo.html:1326 (Abfrage alle 1,5 s). *Vorschlag:* Die Wartezeit in den Übungsläufen stoppen (siehe LI1). Im Vortrag lieber „ein bis zwei Minuten“ sagen. Den Datenschutz-Teil fest in die Wartezeit legen, nicht nur im Notfall: Dann ist die Wartezeit eingeplant und nicht peinlich. Erscheint zweimal „Deutsch“, ist die Übersetzung gescheitert. Dann ruhig sagen „Hier hat die Übersetzung gehakt“ und die Screenshots zeigen.
- **LI9 · Kalender-Knopf ist ein Datei-Download: „Ein Tipp genügt“ ist ungetestet, am Laptop landet der Eintrag im eigenen Kalender** (niedrig, minuten, vor Termin) — „In meinen Kalender eintragen“ erzeugt eine .ics-Datei und lädt sie herunter. Am Laptop landet die Datei im Download-Ordner. Beim Öffnen trägt der Mac den Termin in Justins eigenen Kalender ein, vor Publikum. Am Handy kommen wahrscheinlich je nach Browser Rückfragen wie Laden, Öffnen und Hinzufügen. Der Handzettel verspricht „Ein Tipp genügt, das Handy erinnert 3 Tage und 1 Tag vorher“. Wie viele Tipps es auf iPhone und Android wirklich sind, ist nicht dokumentiert. *Beleg:* demo.html:1088-1097 (Blob-Download mit link.download = 'BehoerdenKlar-Termin.ics'). handzettel.html:118-119. ablaufplan.html:74 (Vorführen von „In meinen Kalender“ ohne Gerät). *Vorschlag:* Den Kalender-Knopf nur am Handy vorführen, nicht am Laptop. Vorher auf dem eigenen iPhone und auf einem Android-Handy testen und die Zahl der Tipps zählen. Danach den Handzettel, falls nötig, auf „Mit einem Tipp in den Kalender, das Handy erinnert …“ oder „Mit wenigen Tipps …“ anpassen.
- **LI10 · Ablaufplan nachschärfen: Mitnehmen-Schritt doppelt, QR-Code läuft ab, Fachbegriff nicht sicher** (niedrig, minuten, vor Termin) — 1) Nach dem Handy-Foto hat das Handy das Ergebnis schon. Danach noch „Ergebnis aufs Handy mitnehmen“ mit demselben Handy zu zeigen, wirkt doppelt und kostet einen zweiten Scan. Die Seite liest den Teil hinter dem # nur beim Laden. Öffnet der Browser den neuen Link im schon offenen Tab nur als Sprung auf derselben Seite, passiert nichts. Das ist nicht getestet. 2) Der QR-Code für das Handy läuft 15 Minuten nach dem Klick ab. Wer ihn vor dem Gespräch schon „bereitlegt“, sieht mitten in der Vorführung „Der QR-Code ist abgelaufen“. 3) Ob „Fiktionsbescheinigung“ wirklich unter den Fachbegriffen auftaucht, entscheidet die KI. Fest versprechen lässt sich das nicht. *Beleg:* demo.html:1507-1516 (ausAdresse/pruefeUebergabe nur beim Start, kein hashchange-Handler). demo.html:1316 und 1337-1341 (15-Minuten-Grenze ab dem Klick). index.ts:140-147 (fachbegriffe frei von der KI gewählt). ablaufplan.html:74-75. *Vorschlag:* Den Mitnehmen-QR von einer Beraterin mit ihrem eigenen Handy scannen lassen. Das ist ein schöner Moment zum Mitmachen und umgeht das Tab-Problem. Mit dem Musterbrief ist das unbedenklich. In den Ablaufplan schreiben: „QR erst live erzeugen, bei ‚abgelaufen‘ einfach neu klicken.“ Beim Fachbegriff den zeigen, der tatsächlich erscheint.
- **LI11 · „Steht vor dem Hochladen auf der Seite“ stimmt nur zur Hälfte** (niedrig, minuten, vor Termin) — Ablaufplan und Handzettel sagen, die Seite nenne schon vor dem Hochladen „löscht nach spätestens 30 Tagen, kein Training“. In der Einwilligung auf der Demo-Seite steht aber nur „Es wird dort nicht dauerhaft gespeichert“. Die 30 Tage und das fehlende Training stehen nur in der Datenschutzerklärung. Eine aufmerksame Beraterin kann das während der Vorführung nachlesen. *Beleg:* demo.html:309-313 (Text der Einwilligung). ablaufplan.html:82 und 137. handzettel.html:152. datenschutz.html:64-65 und 115 (dort stehen die 30 Tage und das fehlende Training). *Vorschlag:* Entweder einen halben Satz in die Einwilligung einfügen („Anthropic löscht es nach spätestens 30 Tagen und nutzt es nicht zum Training.“). Oder im Vortrag sagen: „Das steht in der Datenschutzerklärung, die vor dem Hochladen verlinkt ist.“
### Datenschutz des Trägers
_Technisch ist BehördenKlar datensparsam gebaut, sparsamer als die meisten Werkzeuge, die Beratungsstellen sonst nutzen. Briefe werden nicht gespeichert, die Übergabe per QR ist wirklich Ende-zu-Ende verschlüsselt, und der Weg über den Einrichtungs-Code kommt ohne E-Mail und ohne IP-Zähler aus. Die Lücken liegen im Papier, nicht im Code. Erstens ist nicht geklärt, wer verantwortlich ist, wenn die Beraterin am Dienst-PC den Brief einer Klientin hochlädt. Zweitens passt die Einwilligung nicht zur Beratungssituation: Sie ist nur auf Deutsch, spricht von „mein Brief-Foto“ und wird nach dem ersten Brief nicht zurückgesetzt. Drittens fehlen die Unterlagen, die ein Träger-DSB vor einem Test sehen will: AVV-Entwurf, technische und organisatorische Maßnahmen (TOMs) und ein aktuelles Datenschutz-Blatt. Realistisch: Die Beraterinnen können am 29.10. Ja sagen wollen, müssen aber an ihren Träger bzw. Datenschutzbeauftragten verweisen. Bringt Justin eine kurze Datenschutz-Mappe mit und schlägt er als Standard vor, dass die Klientin mit ihrem eigenen Handy fotografiert und selbst einwilligt (a=k), steigen die Chancen auf einen schnellen Start deutlich._
- **DA1 · Ungeklärt: Wer ist verantwortlich, wenn die Beraterin am Dienst-PC hochlädt?** (hoch, tag, vor Termin) — Die Datenschutzerklärung kennt nur eine Rolle: Webklar ist verantwortlich, die Rechtsgrundlage ist die Einwilligung des Nutzers selbst. Lädt aber die Beraterin im Dienst den Brief einer Klientin hoch, nutzt der Träger das Werkzeug für seine Beratung. Ein Träger-DSB wird dann fast sicher sagen: Wir sind verantwortlich, Webklar arbeitet in unserem Auftrag, also brauchen wir einen Auftragsverarbeitungsvertrag (AVV) nach Art. 28 DSGVO. Den gibt es nicht. Der Handzettel sagt sogar „Kein Vertrag“. Dazu kommen typische Rückfragen: Beraterinnen mit staatlicher Anerkennung unterliegen der Schweigepflicht (§ 203 StGB), dann wird eine Verschwiegenheitsverpflichtung des Dienstleisters verlangt. Ist der Träger kirchlich (Diakonie: DSG-EKD, Caritas: KDG), gelten eigene Datenschutzgesetze mit eigenen Vorgaben für den AVV. Wer Träger der MBE Donnersberg ist (mbe@fid-donnersberg.de), ist in den Unterlagen nicht festgehalten. *Beleg:* webseite/datenschutz.html:35-42 und 68-75 (nur Webklar als Verantwortlicher, nur Einwilligung des Nutzers); marketing/praesentation-mbe/handzettel.html:171 („Kein Vertrag, endet von selbst“); rechtliches/ enthält keinen AVV-Entwurf für Einrichtungen; marketing/outreach-september.md:20 (Träger nicht genannt) *Vorschlag:* Vor dem Termin: (a) Anrufen und fragen, wer Träger der MBE ist und ob es eine eigene AVV-Vorlage gibt. (b) Eine kurze schriftliche Aussage zur Rolle mitbringen (eine halbe Seite): „Lädt die Beratungsstelle selbst hoch, arbeiten wir als Auftragsverarbeiter. Einen AVV mit Verschwiegenheitsklausel und Cloudflare/Anthropic als Unterauftragsverarbeitern stellen wir auf Wunsch bereit.“ (c) Für den Testmonat empfehlen, dass die Klientin selbst am eigenen Handy fotografiert und einwilligt (a=k). Dabei offen sagen, dass das Ergebnis trotzdem am PC der Stelle erscheint. Den vollständigen AVV-Entwurf (nach GDD- oder Bitkom-Muster) gleich nach dem Termin schicken, spätestens bevor echte Briefe am Dienst-PC hochgeladen werden. Ein fertiger AVV in einem Tag ist für einen Nicht-Juristen ehrgeizig. Wichtiger ist, dass er vor dem ersten echten Brief vorliegt.
- **DA2 · Häkchen-Text passt nicht zur Beratung und gilt nach dem ersten Brief weiter** (hoch, stunden, vor Termin) — Der Text lautet „Ich bin einverstanden, dass MEIN Brief-Foto …“. Am Dienst-PC setzt die Beraterin das Häkchen, ist aber nicht die betroffene Person und kann nicht für die Klientin einwilligen. Außerdem nennt der Text keine besonderen Daten (Gesundheit, Herkunft o. Ä.). Art. 9 DSGVO verlangt aber eine ausdrückliche Einwilligung, und gerade diese Daten stehen in Briefen von Ausländerbehörde, Krankenkasse oder Jobcenter. Dazu kommt: Das Häkchen wird nie zurückgesetzt. Mit Einrichtungs-Code öffnet sich nach jeder Analyse sofort wieder das Eingabefeld, das Häkchen ist noch gesetzt. Die zweite, dritte und vierte Klientin des Tages wird also gar nicht mehr gefragt. *Beleg:* webseite/demo.html:306-316 (Text „mein Brief-Foto“, keine besonderen Daten genannt); demo.html:667 (einzige Stelle mit konsens, nirgends wird es zurückgesetzt); demo.html:936-940 (bei Guthaben öffnet sich die Eingabe nach dem Ergebnis sofort wieder) *Vorschlag:* Im Einrichtungs-Modus einen eigenen Text zeigen, z. B.: „Die ratsuchende Person ist einverstanden, dass ihr Brief (der Angaben z. B. zu Gesundheit, Herkunft oder Aufenthalt enthalten kann) zur Erklärung an BehördenKlar und den KI-Dienst Anthropic (USA) geschickt wird.“ Nach jeder Analyse `konsens.checked = false` setzen und den Knopf sperren. Zusätzlich für die Beratungsstelle eine einseitige Papier-Einwilligung zum Unterschreiben anbieten, die sie mit ihrer üblichen Schweigepflichtentbindung abheften kann.
- **DA3 · Einwilligung nur auf Deutsch, auch für Klientinnen, die kein Deutsch lesen** (mittel, stunden, vor Termin) — Eine Einwilligung zählt nur, wenn die Person versteht, wozu sie Ja sagt (Art. 4 Nr. 11 und Art. 7 Abs. 2 DSGVO: klare, einfache Sprache). Die Zielgruppe der MBE liest oft wenig Deutsch. Trotzdem ist die Demo-Seite samt Häkchen-Text komplett deutsch, auch im a=k-Ablauf, in dem die Klientin am eigenen Handy zustimmt und die Zielsprache (z. B. Arabisch) schon bekannt ist. Die 27 Sprachseiten erwähnen weder Anthropic noch die USA und verlinken direkt auf die deutsche Demo. Ein DSB wird fragen: „Wie willigt eine Klientin ein, die nur Dari spricht?“ *Beleg:* webseite/demo.html:2 (lang=de) und 306-316 (fester deutscher Text, keine Übersetzungslogik im Skript); grep „Anthropic“ in ar.html, tr.html, uk.html, fa.html: jeweils 0 Treffer; ar.html:286 verlinkt auf /demo?sprache=ar; demo.html:1411-1416 (a=k setzt nur die Ergebnis-Sprache, nicht den Text der Seite) *Vorschlag:* Den 2-3 Sätze langen Häkchen-Text in der gewählten Zielsprache unter dem deutschen Text einblenden. Zuerst Arabisch, Farsi/Dari, Ukrainisch, Türkisch, Russisch, den Rest danach. Maschinelle Übersetzung möglichst von einer Muttersprachlerin gegenlesen lassen (z. B. beim Auswertungstermin die Beraterinnen fragen). Bis dahin beim Termin offen sagen, dass die Beraterin den Hinweis mündlich erklären muss. Die Papier-Einwilligung aus DA2 kann diese Lücke für den Testmonat überbrücken.
- **DA4 · Zu absolute Aussagen auf Handzettel und Spickzettel (30 Tage, „Server sieht nichts“)** (mittel, minuten, vor Termin) — (a) „löscht nach spätestens 30 Tagen“ stimmt so nicht. Laut Anthropics eigener Datenschutz-Hilfeseite ist 30 Tage der Normalfall, aber bei Verdacht auf Missbrauch (automatisch markiert) bleiben Ein- und Ausgaben bis zu 2 Jahre gespeichert, dazu gibt es gesetzliche Ausnahmen. (b) „unser Server sieht nichts“ gilt nur für die QR-Übergabe. Das Brief-Foto selbst läuft im Klartext (nur über TLS geschützt) durch den Worker zu Anthropic. Der Satz im Spickzettel „Zwischen PC und Handy ist alles Ende-zu-Ende verschlüsselt“ kann als „der Brief ist Ende-zu-Ende verschlüsselt“ verstanden werden. Ein DSB fragt dann: „Aber die KI liest den Brief doch?“ Das schadet der Glaubwürdigkeit, die der „ehrlich“-Kasten aufbauen soll. *Beleg:* marketing/praesentation-mbe/handzettel.html:150 und 152; ablaufplan.html:82 und 137; rechtliches/datenschutz-factsheet.md:30; webseite/datenschutz.html:64-65 und 115; Worker reicht das Bild im Klartext weiter: proxy/src/index.ts:390-419; Anthropic-Hilfeseite „How long do you store my organization's data“ (privacy.claude.com, Artikel 7996866): Ausnahme „inputs and outputs for up to 2 years“ bei markierten Verstößen *Vorschlag:* Umformulieren und Handzettel-PDF neu erzeugen: „Anthropic löscht Anfragen in der Regel nach 30 Tagen (länger nur bei Verdacht auf Missbrauch) und nutzt sie nicht zum Training.“ Und: „Den Brief selbst muss die KI lesen. Er geht verschlüsselt übertragen über unseren Server zu Anthropic und wird bei uns nicht gespeichert. Die Übergabe zwischen PC und Handy ist zusätzlich Ende-zu-Ende verschlüsselt.“ Dieselbe Formulierung in Datenschutzerklärung und Factsheet übernehmen.
- **DA5 · Drittland-Grundlage und Vertragspartner nicht belegt** (mittel, stunden, vor Termin) — Datenschutzerklärung und AVV-Nachweis stützen die Übermittlung an Anthropic auch auf das EU-U.S. Data Privacy Framework (DPF). Einen Beleg dafür gibt es in den Unterlagen nicht. Anthropics eigene Zertifizierungsseite nennt ISO 27001, ISO 42001 und SOC 2, aber kein DPF, und die Websuche brachte keinen DPF-Eintrag (unbestätigt, nicht widerlegt). Die DPA-PDFs sind nicht gesichert (offener Punkt im Nachweis). Außerdem steht im AVV-Nachweis als Verantwortlicher „Justin Klein“ mit einer privaten Gmail-Adresse als Kontoinhaber, in der Datenschutzerklärung dagegen die Webklar GbR. Ein DSB fragt: „Wer genau hat den Vertrag mit Anthropic geschlossen?“ *Beleg:* rechtliches/avv-nachweis.md:9-12 (Justin Klein, einkauf.justin@gmail.com), :20 (DPF ohne Quelle), :35-36 (DPA-PDFs noch nicht abgelegt); webseite/datenschutz.html:37-38 (Webklar GbR) und :107-110 („DPF bzw. SCC“) *Vorschlag:* In datenschutz.html:107-110, im Factsheet und im AVV-Nachweis für Anthropic nur „EU-Standardvertragsklauseln (Bestandteil des Anthropic-DPA)“ schreiben. Für Cloudflare darf das DPF stehen bleiben. Einen Eintrag nur dann ergänzen, wenn er auf dataprivacyframework.gov wirklich zu finden ist. Die DPA-PDFs ablegen und die Organisation in beiden Konten auf „Webklar GbR“ stellen. Das dauert eher Minuten als Stunden.
- **DA6 · Unterlagen für den Träger fehlen, das vorhandene Datenschutz-Blatt ist veraltet** (mittel, tag, vor Termin) — Vor einem Test mit echten Klientendaten verlangt ein Träger-DSB typischerweise: (1) eine Beschreibung der Datenflüsse, (2) die Liste der Unterauftragsverarbeiter, (3) die TOMs, (4) einen AVV oder eine klare Rollenaussage, (5) einen Textbaustein für sein Verarbeitungsverzeichnis, (6) Angaben für seine Datenschutz-Folgenabschätzung (KI, Art.-9-Daten, verletzliche Personen: eine solche Prüfung ist hier naheliegend). Davon gibt es nur das Factsheet, und das ist vom 15.07.: „9 weitere Sprachen“, die alte Adresse behoerdenklar.pages.dev, kein Wort zu Web-Demo, Einrichtungs-Code oder QR-Übergabe. Im Spickzettel fehlen die Fragen „Brauchen wir einen AVV?“ und „Wer ist verantwortlich?“. Brevo (Kauf-Mails, Warteliste) fehlt im AVV-Nachweis, obwohl es genutzt wird. *Beleg:* ls rechtliches/: nur avv-nachweis.md, datenschutz-factsheet.md, datenschutzerklaerung.md, docx; repoweite Suche nach TOM/Verarbeitungsverzeichnis/Folgenabschätzung/AVV-Vertrag: 0 Treffer; datenschutz-factsheet.md:4, 9 und 79 (Stand 15.07., 9 Sprachen, pages.dev); ablaufplan.html:137ff. (keine AVV-Frage); avv-nachweis.md:39-40 (Brevo als „später“), aber proxy/src/index.ts:562-578 und demo.html:509 nutzen Brevo *Vorschlag:* Vor dem Termin nur das Nötigste: ein aktualisiertes Factsheet auf 1-2 Seiten (Datenfluss Web-Demo, Code und QR-Übergabe, Unterauftragsverarbeiter Cloudflare und Anthropic, die korrigierten Sätze aus DA4/DA5, die Rollenaussage aus DA1). Das alte Factsheet bis dahin nicht weitergeben. Brevo in den AVV-Nachweis aufnehmen. TOMs in Stichpunkten, einen Absatz fürs Verarbeitungsverzeichnis und den AVV-Entwurf beim Termin anbieten („bekommt Ihr Datenschutzbeauftragter von mir“) und innerhalb weniger Tage nachliefern. In den Spickzettel die Fragen „Brauchen wir einen AVV?“ und „Wer ist verantwortlich?“ mit Antwort aufnehmen.
- **DA7 · Live-Datenschutzerklärung kennt Einrichtungs-Code und QR-Übergabe noch nicht** (niedrig, minuten, vor Termin) — Die neuen Abschnitte 9a (Einrichtungs-Code) und 9b (Übergabe Handy/PC) stehen nur in der lokalen Datei. Live ist die Fassung vom 18.08.2026. Wird nur der Worker deployt oder die Webseite vergessen, laufen die neuen Funktionen ohne passende Datenschutzerklärung. Klickt eine Beraterin bei der Vorführung auf „Datenschutzerklärung“, findet sie nichts zum QR-Code. *Beleg:* Beobachtet: curl https://behoerdenklar.de/datenschutz zeigt „Stand: 18.08.2026“ und die Überschriften 1–12 ohne 9a/9b; lokal webseite/datenschutz.html:32 (Stand 22.10.2026), :218-257 (9a, 9b) *Vorschlag:* Nach dem Deploy der Webseite kurz live prüfen, ob „9b. Übergabe zwischen Handy und Computer“ zu sehen ist. Optional einen Prüfpunkt in tools/pruefung.js aufnehmen (auf der Live-Datenschutzseite nach „9b.“ suchen).
- **DA8 · Gemeinsamer Dienst-PC: Das Ergebnis der vorigen Klientin bleibt auf dem Bildschirm** (mittel, stunden) — Nach einer Analyse bleibt die Erklärung (Name der Behörde, Aktenzeichen, Frist, Inhalt) sichtbar, bis der nächste Brief sie ersetzt oder die Seite neu geladen wird. Einen Knopf „Fertig, vom Bildschirm löschen“ gibt es nicht. Die nächste Person am Schreibtisch kann also das Ergebnis der vorigen sehen. Das gilt auch für Ergebnisse, die per a=k vom Handy kommen. Klickt die Beraterin am PC auf „In meinen Kalender eintragen“, landet die .ics-Datei mit der Frist der Klientin im Download-Ordner des Dienst-PCs. *Beleg:* demo.html:929-947 (zeigeErgebnis öffnet die Eingabe wieder, das Ergebnis bleibt stehen); keine Funktion zum Leeren oder Zurücksetzen (grep nach reload/zurücksetzen/leeren: nur Kartenreset beim nächsten Brief, demo.html:881); demo.html:1366-1373 (Ergebnis vom Handy wird ebenfalls nur angezeigt) *Vorschlag:* Unter dem Ergebnis einen Knopf „Fertig – vom Bildschirm löschen“ einbauen: Ergebnis ausblenden, letzteDaten leeren, Häkchen zurücksetzen (siehe DA2), nach oben scrollen. Bis dahin im Spickzettel und auf dem Handzettel den Tipp geben: „Nach jedem Gespräch die Seite neu laden. Am PC keinen Kalender-Eintrag, den macht die Klientin per ‚Ergebnis aufs Handy mitnehmen‘.“
- **DA9 · Kleine Unstimmigkeiten zwischen Datenschutzerklärung und Code** (niedrig, stunden) — (a) In der Erklärung steht, die IP-Zähler würden „max. 30 Tage“ gespeichert. Im Code laufen sie nach 48 Stunden ab. Die Erklärung ist damit vorsichtiger als nötig, aber ungenau, und der Code-Kommentar redet noch von „30 Tagen“. (b) Der E-Mail-Hash wird ohne Ablaufzeit gespeichert, also dauerhaft, fehlt aber in Abschnitt 5 „Speicherdauer“. (c) Widerruf der Einwilligung über „Alle Daten löschen in den Einstellungen“ gibt es nur in der App, nicht auf der Webseite. (d) Als Datenschutz-Kontakt steht eine Gmail-Adresse. Ein Träger-DSB stört sich daran, weil Anfragen zu Klientendaten dann bei Google landen. (e) Die IP-Adressen stehen im Klartext in KV-Schlüsseln, und KV wird weltweit verteilt. Im Einrichtungs-Weg betrifft das nur falsche Code-Eingaben. *Beleg:* datenschutz.html:168 vs. proxy/src/index.ts:443 und 449-453 (TTL 48 h) sowie :73-77 (veralteter Kommentar); index.ts:456-457 (mailKey ohne TTL) vs. datenschutz.html:113-118; datenschutz.html:74-75; datenschutz.html:41 und 133; index.ts:356-357 und 723 (IP in Schlüsseln) *Vorschlag:* Speicherfristen in der Erklärung an den Code angleichen (48 Stunden), den E-Mail-Hash mit „dauerhaft, nur als Prüfsumme“ in Abschnitt 5 aufnehmen. Für die Webseite schreiben: „Widerruf jederzeit per E-Mail, wirkt für künftige Analysen.“ Als Kontakt eine Adresse @webklar.com oder @behoerdenklar.de verwenden, wie auf dem Handzettel. Optional die IP vor dem Speichern hashen.
### Technik-Check des neuen Codes
_Technisch ist der neue Code sauber gebaut. Verschlüsselung, Durable Object, Limits und Fehlerpfade sind durchdacht, nichts stürzt ab, und der geplante Vorführ-Ablauf (Arabisch am Laptop vorgewählt, ein Brief, eigenes Handy) funktioniert, soweit man ihn lokal testen kann. Flüssig ist es aber nur auf diesem Vorführ-Pfad: Bleibt der PC auf Deutsch eingestellt, kommt am PC kein Deutsch an. Ein zweiter Brief erscheint nicht am PC, und wenn das Handy neu lädt, ist die Verbindung still weg. Das größte Risiko für den 29.10.: Die zweisprachige Analyse und der ganze Handy-Ablauf sind noch nie echt gelaufen. Bis spätestens zum Wochenende muss deployt und mit einem echten Handy geprobt werden. (Pfade unten relativ zu /Users/homefolder/Behörden_App/beh-rde-brief)_
- **TE1 · PC zeigt nur dann Deutsch, wenn am PC vorher eine Fremdsprache gewählt war** (hoch, minuten, vor Termin) — Im Beratungsalltag bleibt am PC meist 'Deutsch' eingestellt. Dann steht im QR-Code kein 'auch Deutsch' (d=1), und das Häkchen dafür ist sogar ausgeblendet. Wählt die ratsuchende Person am Handy ihre Sprache (z. B. Arabisch), wird nur auf Arabisch analysiert, und die Beraterin bekommt am PC Arabisch zu sehen. Damit klappt genau das nicht, was der Handzettel verspricht ('Sie lesen die Erklärung auf Deutsch, Ihr Gegenüber gleichzeitig in seiner Sprache'). In der Vorführung fällt es nicht auf, weil laut Ablaufplan am Laptop schon Arabisch gewählt ist. Im Testmonat trifft es aber die Beraterinnen. *Beleg:* webseite/demo.html:1297 (Häkchen bei 'de' versteckt), :1300 (d nur, wenn PC-Sprache nicht 'de'), :747 (Sprache kommt aus der Auswahl am Handy), :750 (auchDeutsch nur aus d); proxy/src/index.ts:281. Lokal beobachtet: PC auf Deutsch, in der Handy-Simulation auf Arabisch umgestellt, gesendet wurde {zielsprache:'ar'} OHNE auchDeutsch. Versprechen: marketing/praesentation-mbe/handzettel.html:127. *Vorschlag:* Die Richtung stimmt, nur 'immer true' würde ein bewusst abgewähltes Häkchen übergehen. Besser so: Das Häkchen am PC immer zeigen (Standard: an), in :1300 d='1' setzen, sobald es angehakt ist, egal welche Sprache am PC gewählt ist. Am Handy bleibt :750 dann unverändert. Der Worker ignoriert auchDeutsch bei 'de' sowieso (index.ts:281).
- **TE2 · Zweisprachige Analyse ist nie gelaufen, und wenn die Übersetzung scheitert, merkt es niemand** (hoch, stunden, vor Termin) — Das Herzstück der Vorführung (PC Deutsch, Handy Arabisch, umschalten) wurde nie mit echtem KI-Schlüssel ausprobiert. Wenn die Haiku-Übersetzung scheitert, schickt der Worker still Deutsch zurück. Dann (a) zeigt das Handy ohne jeden Hinweis Deutsch, (b) zeigt der Umschalter zwei Knöpfe 'Deutsch | Deutsch', und (c) landet der Grund nur in console.log. Ohne [observability] in wrangler.toml kann man das später nicht nachlesen, nur live mit 'wrangler tail'. Außerdem laufen jetzt zwei KI-Aufrufe nacheinander (Sonnet, dann Haiku), die Wartezeit wird also länger. Die Seite verspricht 'bis zu einer Minute', bei Schriften wie Tigrinya kann die Übersetzung spürbar länger dauern (nicht gemessen). *Beleg:* proxy/src/index.ts:222-251 (jeder Fehler ergibt null), :479-484 (Rückfall: analyse_de = analyse, sprache 'de'); webseite/demo.html:857-861 (analyse_de wird immer als zweite Version angehängt). Lokal mit genau dieser Rückfall-Antwort nachgestellt: Der Umschalter zeigte ['Deutsch','Deutsch']. proxy/wrangler.toml hat keinen [observability]-Block. *Vorschlag:* 1) Gleich nach dem Deploy mit dem Musterbrief echt testen: Arabisch, Türkisch, Tigrinya, Zeit stoppen. 2) In bereiteVersionen die deutsche Version nur anhängen, wenn d.sprache !== 'de'. 3) Bei einem Rückfall im Worker z. B. 'uebersetzung_fehlgeschlagen: true' mitschicken und am Handy einen kurzen Satz zeigen ('Übersetzung hat nicht geklappt, hier auf Deutsch'). 4) In wrangler.toml '[observability] enabled = true' eintragen, dann sind Fehler im Cloudflare-Dashboard nachlesbar.
- **TE3 · Den Handy-PC-Ablauf kann man lokal nicht echt testen: früh deployen und mit echtem Handy proben** (hoch, stunden, vor Termin) — Mit einem echten Handy lässt sich der neue Ablauf lokal nicht testen. Der QR-Code enthält 'http://localhost:4173/…', und das öffnet am Handy nichts. Öffnet man die Seite übers WLAN (http://192.168.…), gilt sie nicht als lokal: Sie spricht den LIVE-Worker an (der /uebergabe noch nicht kennt), nimmt den echten Turnstile-Schlüssel (an die Domain gebunden), und ohne HTTPS fehlt crypto.subtle. Der erste echte Test passiert also erst nach dem Deploy. Beim ersten Deploy kommen neu dazu: die Durable-Object-Migration und das [[ratelimits]]-Binding. SQLite-Durable-Objects laufen laut Cloudflare-Doku im kostenlosen Tarif. Ob das Rate-Limit-Binding im kostenlosen Tarif geht, sagen die Doku-Seiten nicht eindeutig. Die Reihenfolge ist wichtig: Geht die Webseite vor dem Worker live, laufen 'Code einlösen' und die QR-Übergabe ins Leere. npm run pruefung prüft die neuen Routen nicht. *Beleg:* webseite/uebergabe.js:15-16 (Live-Worker, außer bei localhost), :93 (QR-Adresse = aktuelle Seitenadresse); webseite/demo.html:354 (Test-Sitekey nur bei localhost), :540-542; proxy/wrangler.toml:24-38; tools/pruefung.js enthält keinen Test für /code oder /uebergabe. wrangler 4.110 ist installiert (für [[ratelimits]] ist mindestens 4.36 nötig). *Vorschlag:* Spätestens am Wochenende: erst den Worker deployen (cd proxy && npx wrangler deploy), dann die Webseite. Danach eine Generalprobe genau wie im Ablaufplan: Laptop und eigenes Handy über den Hotspot, einen TEST-Code mit dem Werkzeug anlegen (nicht den MBE-Code), Arabisch, ein Brief, 'Mitnehmen', 'Guthaben aufs Handy'. Scheitert der Deploy am Rate-Limit-Binding: den [[ratelimits]]-Block und index.ts:832-836 vorerst entfernen, die Grenze von 50 Schreibvorgängen pro Übergabe schützt weiter. In pruefung.js zwei Checks ergänzen: GET /uebergabe/<22 Zeichen> mit Origin ergibt 200 {faecher:{}}, OPTIONS /code ergibt 204.
- **TE4 · Ein zweiter Brief vom Handy kommt am PC nicht mehr an** (hoch, minuten, vor Termin) — Nach dem ersten Ergebnis hört der PC auf zu warten und löscht die Übergabe. Das Handy bleibt aber im Modus 'verbunden', zeigt wieder den Hochlade-Knopf und weiter den Satz 'Die Erklärung erscheint hier und auf dem Bildschirm'. Fotografiert die Person einen zweiten Brief, sieht ihn nur das Handy, am PC passiert nichts. In der Beratung bringen Menschen oft mehrere Briefe mit. Gut möglich, dass eine Beraterin das schon bei der Vorführung ausprobieren will. *Beleg:* webseite/demo.html:1348-1352 (stoppeWarten + loeschen nach dem ersten Ergebnis), :812-816 (Handy öffnet die Eingabe wieder), :1417 (Banner-Text). Lokal nachgestellt: zwei verschlüsselte Ergebnisse nacheinander abgelegt, der PC zeigte weiter 'Kernaussage Brief 1'. *Vorschlag:* Schnell (Minuten): Nach dem ersten Ergebnis am Handy den Handy-Modus beenden und anzeigen: 'Für den nächsten Brief am Computer noch einmal auf „Lieber mit dem Handy fotografieren“ tippen.' Besser (später, Stunden): Der PC wartet weiter bis Ablauf oder Abbrechen, das Handy schickt eine laufende Nummer mit (ergebnis.nr), der PC zeigt jedes neue Ergebnis und löscht die Übergabe erst beim Abbrechen.
- **TE5 · Lädt das Handy neu, ist die Verbindung zum PC weg, und die Seite springt still in den Gratis-/Kauf-Modus** (mittel, stunden, vor Termin) — Die Schlüssel stehen nur hinter dem # und werden direkt nach dem Öffnen aus der Adresse gelöscht (gewollt, wegen Datenschutz). Lädt die Seite am Handy neu, ist der Handy-Modus weg. Das passiert durch Herunterwischen, oder wenn ältere Android-Handys den Browser im Hintergrund beenden, während die Kamera-App offen ist. Dann fehlt das Banner, stattdessen erscheinen 'Guthaben kaufen' und der Code-Bereich. Analysiert die Person trotzdem, verbraucht sie ihre eigenen Gratis-Analysen (oder wird nach ihrer E-Mail gefragt), und der PC wartet bis zu 15 Minuten vergeblich. *Beleg:* webseite/demo.html:1507-1511 (Hash wird entfernt), :1412 (handyModus liegt nur im Arbeitsspeicher). Lokal beobachtet: Handy-Simulation neu geladen, danach Banner 'none', kaufBlock 'block', codeTeil 'block'. *Vorschlag:* Die Übergabe-Daten (s, k, w, d, l und die Startzeit) zusätzlich in sessionStorage merken (gilt nur für diesen Tab, wird beim Schließen gelöscht, mit try/catch absichern) und beim Laden ohne # wiederherstellen, solange die 15 Minuten nicht um sind.
- **TE6 · Ein paar falsch getippte Codes sperren die ganze Beratungsstelle für den Tag** (mittel, minuten, vor Termin) — Pro IP-Adresse sind 10 falsche Codes am Tag erlaubt, danach wird JEDER Code abgelehnt, auch der richtige, denn die Sperre wird vor der Prüfung angewendet. In einer Beratungsstelle teilen sich alle PCs und meist auch das Gäste-WLAN eine öffentliche IP. Dazu kommt: 'MBEK7QX' ohne Bindestrich zählt als falsch, weil nur Leerzeichen entfernt werden. Umgekehrt schützt die Sperre schwächer gegen Raten, als der Kommentar behauptet: 4 Zufallszeichen aus 31 ergeben rund 920.000 Möglichkeiten, und mit IPv6 kann man die Adresse fast beliebig wechseln. Schlimmster Schaden: Das Test-Kontingent ist weg. *Beleg:* proxy/src/index.ts:719 (nur Leerzeichen raus, Großbuchstaben), :724-727 (Sperre vor der Prüfung), :732 (Fehlversuch zählen); tools/einrichtungs-code.js:82 (Code = PRÄFIX-4 Zeichen, mit Bindestrich). *Vorschlag:* Vergleichen ohne Bindestriche: im Worker code.replace(/[^A-Z0-9]/g, '') und den Bindestrich nach dem Präfix wieder einsetzen. Alternativ die Codes ohne Bindestrich speichern und beim Einlösen alles außer Buchstaben und Ziffern entfernen. Die Grenze auf etwa 30 anheben. Die restlichen Punkte passen so.
- **TE7 · Verbietet der Browser das Speichern, reagiert die Demo-Seite gar nicht mehr** (niedrig, minuten, vor Termin) — Mehrere Funktionen greifen ohne try/catch auf localStorage zu. In Browsern, die Website-Daten blockieren (Safari mit 'Alle Cookies blockieren', Firefox mit gesperrten Cookies, manche App-internen Browser), wirft schon der Zugriff einen Fehler. Folge: Das Startskript bricht ab (Handy-Knopf erscheint nicht, ein gescannter QR wird nicht ausgewertet), und 'Brief auswählen' öffnet keine Dateiauswahl. Die Person sieht einfach nichts passieren. Die Sprachwahl ist schon abgesichert, der Rest nicht. Der normale private Modus im aktuellen Safari ist nicht betroffen. *Beleg:* webseite/demo.html:594-605 und :629-630 ohne try/catch (zum Vergleich :565-566 mit try/catch). Abbruch beim Start in :1513 (zeigeStand), deshalb laufen :1514-1516 nicht mehr. Lokal nachgestellt: localStorage zum Werfen gebracht, Häkchen gesetzt, 'Brief auswählen' geklickt, Ergebnis 'Uncaught SecurityError', Dateiauswahl nicht geöffnet. *Vorschlag:* Eine kleine Hilfsfunktion 'speicher' mit get/set/remove in try/catch, die bei Fehler ein Objekt im Arbeitsspeicher nimmt. Alle localStorage-Aufrufe darüber laufen lassen. Außerdem sollte zielsprache() den aktuellen Wert der Auswahlliste nehmen, nicht nur localStorage.
- **TE8 · Der Guthaben-Zähler kann sich verzählen, und für die Auswertung gibt es keine anderen Zahlen** (mittel, minuten, vor Termin) — Das Abziehen läuft so: lesen, KI fragen (30-90 s), dann Wert minus 1 schreiben. Laufen zwei Analysen über denselben Einrichtungs-Code gleichzeitig (mehrere Beraterinnen, oder Ratsuchende zuhause), lesen beide z. B. 57 und schreiben beide 56, eine Analyse wird also nicht gezählt. KV ist außerdem nicht sofort überall aktuell. Auch das Aufladen mit dem Werkzeug kann eine gerade laufende Analyse überschreiben. Bei gekauftem Guthaben ist das eine Lücke: viele gleichzeitige Anfragen ergeben viele Analysen für 1 Credit. Für den Auswertungstermin in 4 Wochen ist der Restzähler die einzige Zahl. Es gibt kein Protokoll, wie viele Briefe an welchen Tagen in welchen Sprachen erklärt wurden. *Beleg:* proxy/src/index.ts:318-322 (lesen), :395-419 (KI-Aufruf dazwischen), :446 (walletRest - 1 schreiben); tools/einrichtungs-code.js:93-96 (aufladen = lesen + schreiben). Cloudflare-KV-Doku: Änderungen erst 'bis zu 60 Sekunden oder mehr' später an anderen Standorten sichtbar, keine atomaren Operationen. *Vorschlag:* Vor dem 29.10. (Minuten): pro Analyse über einen Einrichtungs-Code einen eigenen KV-Schlüssel schreiben, z. B. 'nutzung:<walletId>:<datum>:<zufall>' mit der Sprache als Wert. Jeder Schlüssel ist eindeutig, also kein Verzählen. Zählen mit 'wrangler kv key list --prefix nutzung:<walletId>'. Workers-Logs eignen sich dafür nicht, sie werden nur wenige Tage aufbewahrt. Später (Stunden): Guthaben pro Wallet in einem Durable Object führen (atomar, wie bei der Übergabe), vor dem KI-Aufruf reservieren und bei Fehler zurückbuchen.
- **TE9 · 'Ergebnis aufs Handy mitnehmen': Auf einem fremden Handy fehlt die Übersetzung der Anruf-Sätze** (mittel, minuten, vor Termin) — Ob unter jedem deutschen Telefon-Satz die Übersetzung steht, hängt an der Sprache, die im Browser des Handys gespeichert ist, nicht an der Sprache des Ergebnisses. Der Mitnehmen-QR enthält keine Sprache. Auf dem Handy einer ratsuchenden Person, die die Seite noch nie offen hatte, ist 'de' gespeichert, also werden z. B. die arabischen Bedeutungen der Anruf-Sätze ausgeblendet. In der Vorführung fällt das nicht auf, weil Justins Handy vorher per QR auf Arabisch gestellt wurde. *Beleg:* webseite/demo.html:1133 (mitBedeutung = zielsprache() !== 'de'), :1383 (Mitnehmen-QR nur mit a, s, k), :563-568 (zielsprache aus URL oder localStorage). *Vorschlag:* zeigeAnruf die Sprache der gerade angezeigten Version mitgeben (versionen[aktiveVersion].code) und mitBedeutung = code !== 'de' setzen. Alternativ l=<sprache> in den Mitnehmen-QR schreiben.
- **TE10 · Nach einem Fehler dieselbe Datei nochmal wählen: Es passiert nichts** (niedrig, minuten, vor Termin) — Das Datei-Feld wird nie geleert, und Browser melden eine Dateiauswahl nur, wenn sie sich ändert. Wer nach 'KI ausgelastet, bitte erneut versuchen' denselben Scan oder dasselbe PDF wieder auswählt (typisch am Beratungs-PC), sieht keine Reaktion. Mit der Handy-Kamera passiert das nicht, weil jedes Foto eine neue Datei ist. *Beleg:* webseite/demo.html:678-680 (nur das change-Ereignis startet die Analyse). Die Datei enthält nirgends 'dateiFeld.value = ""'. Fehlerpfade danach: :760-799 und :828-833. *Vorschlag:* In starteAnalyse direkt am Anfang (nachdem die Datei übergeben wurde) dateiFeld.value = '' setzen.
- **TE11 · Werkzeug einrichtungs-code.js: Ein Tippfehler kann unbegrenztes Guthaben erzeugen** (niedrig, minuten, vor Termin) — '--briefe' ohne gültige Zahl ergibt NaN. Der Worker prüft nur 'walletRest <= 0', und 'NaN <= 0' ist falsch, also hätte der Code unbegrenzt viele Analysen. '--bis' wird nicht geprüft: '31.12.2026' wird als Text mit '2026-10-29' verglichen und läuft praktisch nie ab. Ist der wrangler-Login abgelaufen, meldet 'stand' fälschlich 'Code gibt es nicht', weil jeder Lesefehler als 'nicht vorhanden' gilt. Relevant, weil Justin den MBE-Code vor dem Termin mit diesem Werkzeug anlegt. *Beleg:* tools/einrichtungs-code.js:80-81 (keine Prüfung von --briefe/--bis), :29-31 (jeder Fehler wird zu null); proxy/src/index.ts:322 + :327 (NaN kommt durch), :324 (Datum als Text verglichen). *Vorschlag:* Im Werkzeug abbrechen, wenn briefe keine Zahl größer 0 ist oder bis nicht im Format JJJJ-MM-TT vorliegt. Im Worker 'if (!(walletRest > 0))' statt 'if (walletRest <= 0)'. Beim Lesen nur bei 'nicht gefunden' null zurückgeben, sonst den Fehler anzeigen. Nach dem Anlegen einmal 'stand' ausführen und die Werte prüfen.
### Angebot und Betrieb nach dem Termin
_Das Angebot ist für eine MBE einladend und ohne Risiko: kostenlos, kein Vertrag, Ratsuchende zahlen nichts, ein Code für alle, und der Datenschutz ist ehrlich erklärt. Der Alltag nach dem Termin ist aber noch nicht bequem genug. Es gibt nichts, was den Code zu den Ratsuchenden bringt (kein Aushang, keine Karte, nichts in ihren Sprachen). Das Testende ist so eingestellt, dass der Zugang ungefähr am Auswertungstag ausläuft, und niemand bekommt vorher eine Erinnerung. Die Beratungsstelle sieht ihren Verbrauch kaum. Für die Zeit nach dem Test fehlen ein schriftliches Angebot und eine Rechnung, wie Träger sie brauchen. Dazu kommt ein echter Fehler: Wer die 2 Gratis-Analysen schon verbraucht hat, sieht das Code-Feld gar nicht mehr. Fast alles davon ist mit wenigen Stunden Arbeit bis zum 29.10. lösbar._
- **AN1 · Wer die 2 Gratis-Analysen schon verbraucht hat, sieht das Code-Feld nicht mehr** (hoch, minuten, vor Termin) — Das Feld 'Sie haben einen Code von einer Beratungsstelle?' liegt innerhalb des Eingabebereichs. Wenn in einem Browser schon 2 Gratis-Analysen gemacht wurden, blendet die Seite diesen ganzen Bereich aus. Dann bleiben nur noch die Kauf-Knöpfe (2,99 €/6,99 €) und die Warteliste. Das Code-Feld ist nicht mehr zu finden. Das trifft genau die Richtigen: Beraterinnen, die den Link aus Justins Mails (outreach-september.md:50) am Büro-PC schon ausprobiert haben, und Ratsuchende, die BehördenKlar schon kennen. Sie können den Code vom Zettel nicht eintippen und landen beim Bezahlen. Nur der QR-Code oder der vollständige Link funktioniert dann noch. *Beleg:* demo.html:287 (div#eingabe) umschließt demo.html:372-377 (details#codeTeil). demo.html:658-661 setzt bei benutzt()>=2 eingabe.style.display='none'. Lokal im Browser geprüft (bk_demo_v2=2): #eingabe display:none, Code-Feld unsichtbar. Sichtbar sind nur noch '5 Analysen 2,99 €', '15 Analysen 6,99 €', Kauf-Häkchen und Warteliste. *Vorschlag:* Der Vorschlag passt (details#codeTeil hinter #eingabe setzen; zeigeStand steuert es schon selbst). Danach zwei Fälle testen: localStorage bk_demo_v2=2 und eine Antwort 'aufgebraucht' vom Worker. Darauf achten, dass das Feld nicht beim Laden der Analyse und nicht im Mitnehmen-Modus (a=m) am Handy unter dem Ergebnis auftaucht. Gegebenenfalls in starteAnalyse und pruefeUebergabe auch codeTeil ausblenden.
- **AN2 · Der Test endet ungefähr am Auswertungstag, und niemand bekommt eine Erinnerung** (mittel, minuten, vor Termin) — '--monate 1' rechnet einen Monat ab dem Tag, an dem der Code angelegt wird. Wird er wie geplant am Vorabend (28.10.) angelegt, gilt er bis 28.11., also nur 2 Tage nach der Auswertung am 26.11. Wird er schon jetzt für den Druck angelegt (22.10.), endet er am 22.11., also VOR der Auswertung. Sagt die MBE am 26.11. 'Ja', laufen Angebot, Bestellung und Rechnung beim Träger länger als 2 Tage. In dieser Lücke bekommen Ratsuchende 'abgelaufen. Bitte fragen Sie dort nach.' Danach wird ihr Zugang gelöscht und die Seite bietet ihnen den Kauf für 2,99 € an. Das widerspricht dem Versprechen 'Ratsuchende zahlen nichts'. Außerdem ist 1 Monat für eine kleine MBE knapp: Die erste Woche geht für Team-Info und Aushang drauf, und viele Ratsuchende kommen erst Wochen später wieder. Bis zur Auswertung gibt es also wenig Erfahrung mit der Nutzung zu Hause. Weder die MBE noch Justin wird an das Testende erinnert, und auf dem Handzettel steht kein Datum. *Beleg:* einrichtungs-code.js:46-50 und :81 (Ablauf = heute + 1 Monat). proxy/src/index.ts:324-326 und :735-736 (nach dem Ablaufdatum gesperrt). demo.html:783-789 (Zugang wird gelöscht, zeigeStand zeigt den Kaufbereich, demo.html:655). ablaufplan.html:88 (Auswertung 26.11.) und :114 (Code am Vorabend). handzettel.html:170 ('1 Monat', kein Datum). *Vorschlag:* Code mit festem Datum anlegen, das deutlich nach der Auswertung liegt, z. B. --bis 2026-12-18 oder 2026-12-31. Dann aber auch Handzettel (handzettel.html:170 '1 Monat') und den Satz im Ablaufplan (Zeile 87 'einen Monat') anpassen, z. B. 'Test bis 18.12., nach 4 Wochen schauen wir gemeinsam'. Sonst widersprechen sich Zettel und Code. Kalender-Erinnerung 7 Tage vor Ablauf. Bei Zusage sofort 'verlaengern', also bevor der Zugang abläuft und die Seiten der Ratsuchenden den Zugang löschen.
- **AN3 · Nichts bringt den Code zu den Ratsuchenden (kein Aushang, keine Karte, nichts in ihren Sprachen)** (mittel, stunden, vor Termin) — Der Handzettel ist für Beraterinnen gemacht: deutsch, dichter Text in 9 pt, A4, 5 Stück. Der Code steht nur in einem kleinen Kasten 'Code für Ihre Ratsuchenden'. Ratsuchende bekommen also nur etwas, wenn die Beraterin jedem einzeln das A4-Blatt hinhält oder den Code diktiert. Dann bleibt das Versprechen in der Überschrift ('verstehen den Brief, bevor sie zu Ihnen kommen') leer, denn zu Hause hat niemand den Code. Für Beraterinnen ist das zusätzliche Arbeit in jedem Termin, und das ist erfahrungsgemäß der häufigste Grund, warum ein Test einschläft. Die Seite hinter dem QR-Code ist außerdem ganz deutsch ('Gratis-Demo', '2 Gratis-Analysen', Einverständnis-Häkchen). Wer kein Deutsch liest, weiß nicht, dass er zuerst das Häkchen setzen muss. *Beleg:* handzettel.html:93-99 (richtet sich an die Beratungsstelle), :172-175 (der einzige QR mit Code). demo.html:2 lang=de, :284 'Antrag auf Klartext · 2 Gratis-Analysen' (bleibt auch mit Code so), :305-315 Einverständnis auf Deutsch. Lokal geprüft: /demo.html?code=…&sprache=ar stellt die Sprache auf Arabisch, die Bedienung bleibt deutsch. *Vorschlag:* Vor dem 29.10. reicht eine einfache Version: ein A5-Aushang und A7-Karten mit großem QR (Code schon im Link), 'kostenlos' und 3 Symbol-Schritte (Häkchen, Foto, Sprache). Text auf Deutsch, höchstens mit 1-2 Sätzen in den Hauptsprachen der MBE. Mehrsprachige Texte und eigene QRs pro Sprache besser NACH dem Termin gemeinsam mit den Beraterinnen machen: Sie wissen, welche Sprachen gebraucht werden, und können gegenlesen. Ungeprüfte KI-Übersetzungen (z. B. Tigrinya, Paschtu, siehe FEATURES.md) nicht ungeprüft drucken.
- **AN4 · Die Beratungsstelle sieht kaum, wie viel verbraucht ist, und für die Auswertung gibt es nur eine einzige Zahl** (niedrig, stunden) — Die Datenschutzerklärung verspricht, dass die Beratungsstelle sieht, 'wie viele Analysen noch übrig sind'. Das geht aber nur über die Zahl im Banner im eigenen Browser. Diese Zahl ist im Browser gespeichert und wird nur aktualisiert, wenn dieser Browser selbst etwas tut. Briefe, die Ratsuchende zu Hause machen, sieht die Beraterin nicht. Ein Ablaufdatum wird nirgends angezeigt, obwohl der Worker es beim Einlösen mitschickt. Für die MBE heißt das: Sie muss Justin anrufen. Justin selbst bekommt mit 'stand' nur 'Übrig: X Briefe'. Bei der Auswertung am 26.11. kann er also nur sagen: '23 Briefe verbraucht'. Er weiß nicht, wann (abends oder in der Beratung), in welchen Sprachen und ob sie von MBE-Ratsuchenden kamen. Dabei sind genau diese Zahlen das stärkste Argument für das Bezahlen. *Beleg:* datenschutz.html:224-225 (Versprechen). demo.html:620-623 und :643-645 (Banner liest den gespeicherten Wert). proxy/src/index.ts:742 schickt 'ablauf' mit, in demo.html kommt 'ablauf' kein einziges Mal vor (grep). einrichtungs-code.js:61-69 (zeigt nur Übrig und Gültig bis). index.ts:446 (es wird nur heruntergezählt). *Vorschlag:* Nur die kostenlosen Teile übernehmen: wöchentliche Stand-Mail aus 'stand' und den Beraterinnen sagen 'Link vom Zettel neu öffnen, dann stimmt die Zahl'. Für die Auswertung reicht eine kurze Strichliste oder 3 Fragen. 'gültig bis' im Banner anzeigen ist ein kleiner Gewinn für später. Zusätzliche Zählung nach Sprache und Zeit erst dann, wenn die Datenschutzerklärung angepasst ist.
- **AN5 · Kein Angebot und keine Rechnungsvorlage für die Zeit nach dem Test, und die Pakete sind für Träger zu kleinteilig** (mittel, stunden) — Die Preise gibt es nur intern, 'nur im Gespräch' (100 Briefe 20 €, 500 Briefe 75 €). Auf dem Handzettel steht nur 'festes Briefkontingent zu einem Preis für Beratungsstellen'. Ein Träger kann ohne schriftliches Angebot nichts bestellen und ohne ordentliche Rechnung nichts per Überweisung bezahlen. Im Repo gibt es weder Angebot noch Rechnung, und die AGB gelten nur für Verbraucher, die mit Stripe kaufen. Kleine 20-€-Pakete bedeuten außerdem: Jedes Mal, wenn sie leer sind, braucht es einen Anruf, eine neue Rechnung und eine neue Zahlungsanweisung. Der Verwaltungsaufwand ist dann größer als der Betrag. Die interne Notiz 'Den Einrichtungs-Code gibt es technisch noch nicht' ist außerdem veraltet. *Beleg:* outreach-september.md:314-335 (Preisblatt, Zeile 334 veraltet). handzettel.html:161-164 (keine Zahl). grep nach Rechnung/Angebot/IBAN in marketing/ und rechtliches/: nur Treffer im Musterbrief. agb.html:44-45 (Verbraucher). FEATURES.md: Kleinunternehmer § 19. *Vorschlag:* Bis zum 26.11. ein einseitiges Angebot vorbereiten (Leistung, Briefzahl, Laufzeit, Endpreis mit Hinweis auf § 19 UStG, Zahlung auf Rechnung). Für die Rechnung die vorhandene Webklar-Vorlage nehmen, falls es eine gibt. Ein Jahreskontingent mit fester Briefzahl und einer Rechnung pro Jahr ist sinnvoll ('Jahreslizenz mit X Briefen' steht schon in outreach-september.md). Nachladen aber nur gegen neue Rechnung im Voraus, damit die eigene Regel 'erst bezahlt, dann genutzt' gilt. Die veraltete Zeile 334 korrigieren.
- **AN6 · Das Datenschutz-Blatt für Beratungsstellen ist veraltet und sagt etwas Falsches über die Webseite** (mittel, stunden, vor Termin) — Bevor ein Träger einen Test erlaubt, fragt erfahrungsgemäß die Leitung oder die Datenschutzbeauftragte nach einer Unterlage. Die einzige Unterlage dafür ist vom 15.07. Darin stehen '9 weitere Sprachen', die alte Domain behoerdenklar.pages.dev und eine Gmail-Adresse. Vor allem steht dort: 'die Briefe ... liegen verschlüsselt auf deren eigenem Handy, nicht bei uns'. Das stimmt nur für die App. Im eigenen Ablaufplan steht genau das unter 'Nicht sagen'. Einrichtungs-Code und PC-Handy-Übergabe kommen im Blatt gar nicht vor. Schickt Justin dieses Blatt nach dem Termin, widerspricht es dem Handzettel und kann den Teststart verzögern. *Beleg:* rechtliches/datenschutz-factsheet.md Zeile 4 (Stand 15.07., pages.dev), Zeile 9 ('9 weiteren Sprachen'), Zeilen 17-19 ('liegen verschlüsselt auf deren eigenem Handy'). ablaufplan.html:147 ('Alles bleibt auf dem Handy: Das stimmt für die Webseite nicht'). Aktuelle Fassung in datenschutz.html:218-228 (9a/9b). *Vorschlag:* Der Vorschlag passt. Ergänzung: Im Termin kurz sagen 'Das Datenschutz-Blatt aus dem Juli beschrieb die App. Für die Webseite gilt dieses neue Blatt.' Wenn die Zeit vor dem 29.10. knapp ist, reicht auch ein Ausdruck der aktuellen datenschutz.html (Abschnitte zur Demo, 9a und 9b) als Übergangslösung. Das alte factsheet/PDF als veraltet markieren, damit es nicht noch einmal verschickt wird.
- **AN7 · Ein Leerzeichen statt Bindestrich zählt als falscher Code, und 10 Fehler sperren das ganze Büro für den Tag** (mittel, minuten, vor Termin) — Der Worker entfernt nur Leerzeichen. Wer 'MBE K7QX' oder 'MBEK7QX' tippt, bekommt 'Diesen Code kennen wir nicht', und das zählt als Fehlversuch. Nach 10 Fehlversuchen pro IP-Adresse und Tag ist Schluss. Die Sperre wird geprüft, BEVOR der Code geprüft wird, also wird dann auch der richtige Code abgelehnt ('Bitte versuchen Sie es morgen wieder'). In einer Beratungsstelle teilen sich alle Büro-PCs und oft auch das WLAN für Gäste eine IP-Adresse. Ein paar Tippfehler von Ratsuchenden reichen, und an diesem Tag kann im ganzen Haus niemand mehr einen Code einlösen. *Beleg:* proxy/src/index.ts:719 (.replace(/\s+/g,'') entfernt nur Leerzeichen, fügt keinen Bindestrich ein), :688 (CODE_FEHLVERSUCHE_PRO_TAG = 10), :723-727 (die Sperre kommt vor der Code-Prüfung), :731-733 (jeder unbekannte Code wird gezählt). demo.html:1235 (die Seite macht nur Großbuchstaben). Lokal per curl getestet: unbekannter Code ergibt code_unbekannt. *Vorschlag:* Im Worker alles außer A-Z und 0-9 entfernen und den Bindestrich vor den letzten 4 Zeichen selbst einsetzen. Dann gehen 'mbe k7qx', 'MBEK7QX' und 'MBE-K7QX'. Die Grenze auf etwa 30 pro Tag anheben: Bei 31^4 möglichen Codes pro Präfix bleibt Raten trotzdem aussichtslos. Das geht in den Deploy, der vor dem 29.10. sowieso ansteht.
- **AN8 · Den Test im Termin starten, nicht danach, und den Weg 'Zugang aufs Handy' zeigen** (mittel, minuten, vor Termin) — Im Ablaufplan endet das Angebot damit, dass der Handzettel übergeben wird ('Der Code steht hier auf dem Zettel'). Danach muss die MBE selbst anfangen: Code einlösen, Kolleginnen informieren, Ratsuchenden erklären. Erfahrungsgemäß bleibt das im Alltag liegen. Der bequemste Weg im Alltag kommt weder in der Vorführung noch auf dem Handzettel vor: Am Beratungs-PC auf '📱 Auch auf dem Handy nutzen' klicken. Die Ratsuchende scannt den QR-Code und kann danach zu Hause selbst Briefe erklären lassen, ohne Zettel und ohne Tippen. Gezeigt wird nur 'Ergebnis mitnehmen'. *Beleg:* ablaufplan.html:85-88 (Übergabe nur auf Papier), :70-76 (Vorführung ohne a=g). handzettel.html:125-132 (nur 'Beratung am PC' und 'Ergebnis zum Mitnehmen'). demo.html:294 und :1392-1397 (Knopf 'Auch auf dem Handy nutzen' gibt Zugang und Name der Stelle weiter, Art a=g). demo.html:1457 ('Übernommen. BehördenKlar ist für Sie kostenlos, über …'). *Vorschlag:* Der Vorschlag passt, mit einer Änderung: Für den Schritt 'Auch auf dem Handy nutzen' vorher den MBE-Code am Laptop einlösen (Handzettel-QR oder ?code=), sonst geht der Vorführ-Zugang weiter. Alternativ den Knopf nur zeigen und erklären, aber nicht scannen lassen. Die Zeile auf dem Handzettel ergänzen.
- **AN9 · Ohne eingetragenen Code führt der QR auf dem Handzettel zur Kaufseite** (niedrig, minuten, vor Termin) — Das vorhandene handzettel.pdf/png wurde ohne Code erzeugt: Dort steht 'wird beim Termin eingetragen', und der QR zeigt nur auf behoerdenklar.de/demo. Druckt Justin diese Fassung oder trägt den Code nur von Hand ein, landen alle, die den QR scannen (also der bequemste Weg), in der normalen Gratis-Demo. Nach 2 Briefen kommt dort der Kauf für 2,99 €. Nur wer den Code von Hand eintippt, nutzt den Zugang der MBE. Das ist das Gegenteil von 'Ratsuchende zahlen nichts'. *Beleg:* handzettel.html:190-191 (CODE='' ergibt QR auf https://behoerdenklar.de/demo), :197 (Platzhaltertext). handzettel.png angesehen: Platzhalter 'wird beim Termin eingetragen', Link 'behoerdenklar.de/demo'. Der Haken 'MBE-Code angelegt, im Handzettel eingetragen' steht in ablaufplan.html:114. *Vorschlag:* Die Checkliste in ablaufplan.html:113-115 um 2 Punkte ergänzen: 'gedruckten QR mit dem Handy getestet, es erscheint Kostenlos über MBE Donnersberg' und 'alte handzettel.pdf/png ohne Code nicht mehr verwenden'. Den Rest gibt es schon.
+313 -13
View File
@@ -1,3 +1,5 @@
import { DurableObject } from 'cloudflare:workers';
/**
* BehördenKlar Backend-Proxy — Cloudflare Worker
*
@@ -36,6 +38,12 @@ interface Env {
TOKEN_SIGNING_SECRET: string;
/** Bezahlung: `wrangler secret put BREVO_API_KEY` (für die Guthaben-Link-Mail) */
BREVO_API_KEY: string;
/** Nur lokal (.dev.vars): "1" erlaubt Aufrufe von http://localhost */
LOKAL_TEST?: string;
/** Handy ↔ PC: verschlüsselte Übergabe (Durable Object, siehe unten) */
UEBERGABE: DurableObjectNamespace<Uebergabe>;
/** Handy ↔ PC: Schreib-Bremse pro IP (Rate-Limit-Binding in wrangler.toml) */
UEBERGABE_LIMIT: RateLimit;
}
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
@@ -71,6 +79,12 @@ function fehler(status: number, typ: string, meldung: string): Response {
/** Nur die eigene Webseite darf den Demo-Endpunkt aufrufen (CORS). */
const DEMO_ORIGIN_MUSTER = /^https:\/\/([a-z0-9-]+\.)?behoerdenklar\.(de|pages\.dev)$/;
/** Nur für `wrangler dev` mit LOKAL_TEST=1 in .dev.vars — live nie gesetzt. */
const LOKAL_MUSTER = /^http:\/\/localhost:\d+$/;
let lokalTest = false;
function originOk(origin: string | null): origin is string {
return !!origin && (DEMO_ORIGIN_MUSTER.test(origin) || (lokalTest && LOKAL_MUSTER.test(origin)));
}
/** ~5 MB Datei entsprechen ~6,7 Mio. Base64-Zeichen. */
const DEMO_MAX_BASE64 = 7_000_000;
@@ -82,6 +96,10 @@ const WEGWERF_DOMAINS = new Set([
'getnada.com', 'dispostable.com', 'maildrop.cc', 'throwawaymail.com',
]);
/** Was in jeder Sprache deutsch bzw. unverändert bleiben muss. */
const SPRACH_AUSNAHMEN = (sprachName: string) =>
`Ausnahme: das Feld "begriff" bei fachbegriffe bleibt der deutsche Originalbegriff aus dem Brief (der Nutzer muss ihn im Brief wiederfinden) — nur dessen "erklaerung" wird auf ${sprachName} geschrieben. Datumsangaben bleiben im ISO-Format. Ebenfalls NICHT übersetzen: das Feld "deutsch" bei anruf (das wird am Telefon auf Deutsch gesagt) und alle kontakt-Felder (sie stehen so im Brief).`;
/** Identisch zur App (src/services/analyse.ts) — gleiche Qualität in der Demo. */
/** Zielsprachen der Web-Demo — identisch zur Sprachauswahl der Webseite.
* Der Wert wird NUR als Name in den Prompt gesetzt, nie als freier Text,
@@ -178,7 +196,7 @@ const DEMO_SCHEMA = {
};
function demoCorsHeaders(origin: string | null): Record<string, string> {
const erlaubt = origin && DEMO_ORIGIN_MUSTER.test(origin) ? origin : 'https://behoerdenklar.de';
const erlaubt = originOk(origin) ? origin : 'https://behoerdenklar.de';
return {
'access-control-allow-origin': erlaubt,
'access-control-allow-methods': 'POST, OPTIONS',
@@ -199,6 +217,40 @@ async function emailHash(email: string, pepper: string): Promise<string> {
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
}
/** Fertige deutsche Analyse in eine andere Sprache übersetzen (Haiku, gleiches Schema).
* Gibt null zurück, wenn etwas schiefgeht — der Aufrufer zeigt dann Deutsch. */
async function uebersetzeAnalyse(env: Env, analyse: unknown, sprachName: string): Promise<unknown | null> {
try {
const r = 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-haiku-4-5',
max_tokens: MAX_TOKENS_OBERGRENZE,
system: `Du übersetzt die Erklärung eines deutschen Behördenbriefs für Menschen mit wenig Deutschkenntnissen. Du bekommst ein JSON-Objekt. Gib dasselbe Objekt mit derselben Struktur zurück, aber schreibe alle Textfelder auf ${sprachName}, in einfacher, klarer Sprache. Erfinde nichts dazu und lass nichts weg; null bleibt null. ${SPRACH_AUSNAHMEN(sprachName)}`,
output_config: { format: { type: 'json_schema', schema: DEMO_SCHEMA } },
messages: [{ role: 'user', content: JSON.stringify(analyse) }],
}),
});
if (!r.ok) {
console.log('Übersetzung fehlgeschlagen', r.status, (await r.text()).slice(0, 300));
return null;
}
const d = (await r.json()) as { stop_reason?: string; content?: { type: string; text?: string }[] };
const text = [...(d.content ?? [])].reverse().find((b) => b.type === 'text')?.text;
if (d.stop_reason !== 'end_turn' || !text) return null;
return JSON.parse(text);
} catch (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;
}
}
async function demoHandler(request: Request, env: Env): Promise<Response> {
const origin = request.headers.get('origin');
if (request.method === 'OPTIONS') {
@@ -207,11 +259,11 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return demoFehler(405, 'methode', 'Nur POST erlaubt.', origin);
}
if (!origin || !DEMO_ORIGIN_MUSTER.test(origin)) {
if (!originOk(origin)) {
return demoFehler(403, 'origin', 'Aufruf nur von der BehördenKlar-Webseite erlaubt.', origin);
}
let body: { bild?: string; mimeType?: string; turnstileToken?: string; email?: string; walletToken?: string; zielsprache?: string };
let body: { bild?: string; mimeType?: string; turnstileToken?: string; email?: string; walletToken?: string; zielsprache?: string; auchDeutsch?: boolean };
try {
body = await request.json();
} catch {
@@ -224,6 +276,10 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
const sprachCode = typeof body.zielsprache === 'string' && DEMO_SPRACHEN[body.zielsprache]
? body.zielsprache : 'de';
const sprachName = DEMO_SPRACHEN[sprachCode];
// Zweisprachig (Beratung am PC, Ratsuchende am Handy): erst auf Deutsch
// analysieren, dann mit Haiku übersetzen — wie in der App. Kostet den
// Nutzer weiterhin nur EINE Analyse.
const zweisprachig = body.auchDeutsch === true && sprachCode !== 'de';
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);
@@ -254,14 +310,25 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
// (die sind nur ein Kosten-Deckel für die kostenlose Stufe).
let walletId: string | null = null;
let walletRest = 0;
let einrichtungsName: string | null = null;
if (walletToken) {
walletId = await verifyWallet(walletToken, env.TOKEN_SIGNING_SECRET);
if (!walletId) {
return demoFehler(403, 'wallet_ungueltig', 'Ihr Zugangs-Token ist ungültig. Bitte laden Sie die Seite neu.', origin);
}
walletRest = parseInt((await env.RATE_LIMIT.get(`wallet:${walletId}`)) ?? '0', 10);
const [restWert, einrichtung] = await Promise.all([
env.RATE_LIMIT.get(`wallet:${walletId}`),
ladeEinrichtung(env, walletId),
]);
walletRest = parseInt(restWert ?? '0', 10);
einrichtungsName = einrichtung?.name ?? null;
if (einrichtung && einrichtung.ablauf < heute) {
return demoFehler(402, 'kontingent_abgelaufen', `Der Zugang über ${einrichtung.name} ist abgelaufen. Bitte fragen Sie dort nach.`, origin);
}
if (walletRest <= 0) {
return demoFehler(402, 'kein_guthaben', 'Ihr Guthaben ist aufgebraucht. Bitte kaufen Sie neues Guthaben.', origin);
return einrichtung
? demoFehler(402, 'kontingent_leer', `Die Analysen von ${einrichtung.name} sind aufgebraucht. Bitte fragen Sie dort nach.`, origin)
: demoFehler(402, 'kein_guthaben', 'Ihr Guthaben ist aufgebraucht. Bitte kaufen Sie neues Guthaben.', origin);
}
}
const bezahlt = walletId !== null;
@@ -326,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',
@@ -336,9 +405,9 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
body: JSON.stringify({
model: 'claude-sonnet-5',
max_tokens: MAX_TOKENS_OBERGRENZE,
system: sprachCode === 'de'
system: sprachCode === 'de' || zweisprachig
? DEMO_SYSTEM_PROMPT
: `${DEMO_SYSTEM_PROMPT}\n\nWICHTIG — ZIELSPRACHE: Schreibe ALLE Textfelder deiner Antwort auf ${sprachName}, nicht auf Deutsch. Ausnahme: das Feld "begriff" bei fachbegriffe bleibt der deutsche Originalbegriff aus dem Brief (der Nutzer muss ihn im Brief wiederfinden) — nur dessen "erklaerung" wird auf ${sprachName} geschrieben. Datumsangaben bleiben im ISO-Format. Ebenfalls NICHT übersetzen: das Feld "deutsch" bei anruf (das wird am Telefon auf Deutsch gesagt) und alle kontakt-Felder (sie stehen so im Brief).`,
: `${DEMO_SYSTEM_PROMPT}\n\nWICHTIG — ZIELSPRACHE: Schreibe ALLE Textfelder deiner Antwort auf ${sprachName}, nicht auf Deutsch. ${SPRACH_AUSNAHMEN(sprachName)}`,
output_config: { format: { type: 'json_schema', schema: DEMO_SCHEMA } },
messages: [
{
@@ -351,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;
@@ -367,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 }[];
};
@@ -378,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 }),
@@ -408,12 +498,31 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
return demoFehler(502, 'ki', 'Die Antwort war unlesbar. Bitte erneut versuchen.', origin);
}
let analyseDe: unknown = undefined;
let ausgabeSprache = sprachCode;
let uebersetzungFehlgeschlagen = false;
if (zweisprachig) {
const uebersetzt = await uebersetzeAnalyse(env, analyse, sprachName);
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(
{
analyse,
analyse_de: analyseDe,
versuch: bezahlt ? 'bezahlt' : email ? 2 : 1,
guthaben: bezahlt ? walletRest - 1 : undefined,
sprache: sprachCode,
einrichtung: einrichtungsName ?? undefined,
sprache: ausgabeSprache,
uebersetzung_fehlgeschlagen: uebersetzungFehlgeschlagen || undefined,
},
{ headers: demoCorsHeaders(origin) }
);
@@ -518,7 +627,7 @@ async function kaufenHandler(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);
if (!originOk(origin)) return demoFehler(403, 'origin', 'Aufruf nur von der BehördenKlar-Webseite erlaubt.', origin);
let body: { priceId?: string; sprache?: string };
try {
@@ -571,7 +680,7 @@ async function kaufenHandler(request: Request, env: Env): Promise<Response> {
async function kaufStatusHandler(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 (!origin || !DEMO_ORIGIN_MUSTER.test(origin)) return demoFehler(403, 'origin', 'Nicht erlaubt.', origin);
if (!originOk(origin)) return demoFehler(403, 'origin', 'Nicht erlaubt.', origin);
const sessionId = new URL(request.url).searchParams.get('session_id') ?? '';
if (!/^cs_[A-Za-z0-9_]+$/.test(sessionId)) return demoFehler(400, 'session', 'Ungültige Sitzung.', origin);
@@ -593,6 +702,189 @@ async function kaufStatusHandler(request: Request, env: Env): Promise<Response>
return Response.json({ token, guthaben: rest }, { headers: demoCorsHeaders(origin) });
}
// ============================================================
// Einrichtungs-Codes — Beratungsstellen zahlen, Ratsuchende nicht
// ============================================================
//
// Ein Code (z. B. `MBE-K7QX`) zeigt auf ein Guthaben wie beim Kauf, nur dass
// es allen gehört, die den Code kennen. Angelegt wird er von Hand mit
// tools/einrichtungs-code.js (kein Secret nötig, nur wrangler-Login).
//
// KV: code:<CODE> -> walletId
// wallet:<walletId> -> Restguthaben (wie beim Kauf)
// einrichtung:<walletId> -> {"name":"MBE Donnersberg","ablauf":"2027-01-31"}
//
// Der zufällige Teil im Code macht Raten aussichtslos; zusätzlich sind
// Fehlversuche pro IP und Tag begrenzt.
// 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;
/** letzter gültiger Tag, ISO-Datum */
ablauf: string;
}
async function ladeEinrichtung(env: Env, walletId: string): Promise<Einrichtung | null> {
const roh = await env.RATE_LIMIT.get(`einrichtung:${walletId}`);
if (!roh) return null;
try {
return JSON.parse(roh) as Einrichtung;
} catch {
return null;
}
}
/** POST /code — Einrichtungs-Code einlösen, signiertes Wallet-Token liefern. */
async function codeHandler(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 (!originOk(origin)) return demoFehler(403, 'origin', 'Nicht erlaubt.', origin);
let body: { code?: string };
try {
body = await request.json();
} catch {
return demoFehler(400, 'json', 'Ungültige Anfrage.', origin);
}
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);
const failKey = `codefail:${ip}:${heute}`;
const fehlversuche = parseInt((await env.RATE_LIMIT.get(failKey)) ?? '0', 10);
if (fehlversuche >= CODE_FEHLVERSUCHE_PRO_TAG) {
return demoFehler(429, 'code_gesperrt', 'Zu viele falsche Codes. Bitte versuchen Sie es morgen wieder.', origin);
}
const walletId = /^[A-Z0-9-]{4,40}$/.test(code) ? await env.RATE_LIMIT.get(`code:${code}`) : null;
const einrichtung = walletId ? await ladeEinrichtung(env, walletId) : null;
if (!walletId || !einrichtung) {
await env.RATE_LIMIT.put(failKey, String(fehlversuche + 1), { expirationTtl: 60 * 60 * 48 });
return demoFehler(404, 'code_unbekannt', 'Diesen Code kennen wir nicht. Bitte prüfen Sie die Schreibweise.', origin);
}
if (einrichtung.ablauf < heute) {
return demoFehler(410, 'kontingent_abgelaufen', `Der Zugang über ${einrichtung.name} ist abgelaufen. Bitte fragen Sie dort nach.`, origin);
}
const rest = parseInt((await env.RATE_LIMIT.get(`wallet:${walletId}`)) ?? '0', 10);
const token = await signWallet(walletId, env.TOKEN_SIGNING_SECRET);
return Response.json(
{ token, guthaben: rest, einrichtung: einrichtung.name, ablauf: einrichtung.ablauf },
{ headers: demoCorsHeaders(origin) }
);
}
// ============================================================
// Übergabe Handy ↔ PC — Ende-zu-Ende verschlüsselt
// ============================================================
//
// Ein Gerät zeigt einen QR-Code, das andere scannt ihn. Im QR-Code stehen
// eine zufällige Übergabe-ID und ein AES-Schlüssel — der Schlüssel steht
// hinter dem `#` der Adresse und wird deshalb NIE an einen Server geschickt.
// Hier liegen nur verschlüsselte Päckchen, die wir selbst nicht lesen
// können. Jede Übergabe löscht sich nach 15 Minuten von selbst.
//
// PUT /uebergabe/<id>/<fach> -> Päckchen ablegen (Text, verschlüsselt)
// GET /uebergabe/<id> -> alle Päckchen dieser Übergabe
// DELETE /uebergabe/<id> -> sofort löschen
//
// Durable Object statt KV, weil KV Abfragen auf noch nicht vorhandene
// Schlüssel zwischenspeichert — das Warten am PC würde bis zu einer Minute
// hängen. Ein Durable Object ist sofort konsistent.
//
// Ein Fach (z. B. „status", „ergebnis") wird überschrieben, nicht angehängt.
const UEBERGABE_LEBENSDAUER_MS = 15 * 60 * 1000;
const UEBERGABE_MAX_ZEICHEN = 400_000; // Analyse-JSON in zwei Sprachen, verschlüsselt
const UEBERGABE_MAX_SCHREIBVORGAENGE = 50;
const UEBERGABE_ID_MUSTER = /^[A-Za-z0-9_-]{22}$/; // 128 Bit, base64url
const UEBERGABE_FACH_MUSTER = /^[a-z]{2,16}$/;
export class Uebergabe extends DurableObject<Env> {
async lesen(): Promise<Record<string, string>> {
const eintraege = await this.ctx.storage.list<string>({ prefix: 'fach:' });
const faecher: Record<string, string> = {};
for (const [k, v] of eintraege) faecher[k.slice(5)] = v;
return faecher;
}
async schreiben(fach: string, wert: string): Promise<boolean> {
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 });
// 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;
}
async loeschen(): Promise<void> {
await this.ctx.storage.deleteAlarm();
await this.ctx.storage.deleteAll();
}
async alarm(): Promise<void> {
await this.ctx.storage.deleteAll();
}
}
function uebergabeCors(origin: string | null): Record<string, string> {
return {
...demoCorsHeaders(origin),
'access-control-allow-methods': 'GET, PUT, DELETE, OPTIONS',
'cache-control': 'no-store',
};
}
async function uebergabeHandler(request: Request, env: Env, pfad: string): Promise<Response> {
const origin = request.headers.get('origin');
const cors = uebergabeCors(origin);
if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: cors });
// Webseite: nur unsere eigene. Die App schickt keinen Origin-Header.
if (origin && !originOk(origin)) {
return Response.json({ code: 'origin', meldung: 'Nicht erlaubt.' }, { status: 403, headers: cors });
}
const [, , id, fach] = pfad.split('/'); // "", "uebergabe", id, fach?
if (!id || !UEBERGABE_ID_MUSTER.test(id)) {
return Response.json({ code: 'id', meldung: 'Ungültige Übergabe.' }, { status: 400, headers: cors });
}
const stub = env.UEBERGABE.get(env.UEBERGABE.idFromName(id));
if (request.method === 'GET' && !fach) {
return Response.json({ faecher: await stub.lesen() }, { headers: cors });
}
if (request.method === 'DELETE' && !fach) {
await stub.loeschen();
return new Response(null, { status: 204, headers: cors });
}
if (request.method === 'PUT' && fach && UEBERGABE_FACH_MUSTER.test(fach)) {
const ip = request.headers.get('cf-connecting-ip') ?? 'unbekannt';
const { success } = await env.UEBERGABE_LIMIT.limit({ key: ip });
if (!success) {
return Response.json({ code: 'zu_viele', meldung: 'Zu viele Anfragen. Bitte kurz warten.' }, { status: 429, headers: cors });
}
const wert = await request.text();
if (!wert || wert.length > UEBERGABE_MAX_ZEICHEN || !/^[A-Za-z0-9_-]+$/.test(wert)) {
return Response.json({ code: 'inhalt', meldung: 'Ungültiger Inhalt.' }, { status: 400, headers: cors });
}
if (!(await stub.schreiben(fach, wert))) {
return Response.json({ code: 'voll', meldung: 'Diese Übergabe ist abgeschlossen.' }, { status: 409, headers: cors });
}
return new Response(null, { status: 204, headers: cors });
}
return Response.json({ code: 'methode', meldung: 'Nicht erlaubt.' }, { status: 405, headers: cors });
}
/** POST /stripe-webhook — Sicherungsnetz: Signatur prüfen, Guthaben aktivieren, Link mailen. */
async function webhookHandler(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') return new Response('nur POST', { status: 405 });
@@ -641,10 +933,18 @@ async function webhookHandler(request: Request, env: Env): Promise<Response> {
export default {
async fetch(request: Request, env: Env): Promise<Response> {
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);
if (pfad.startsWith('/uebergabe/')) return uebergabeHandler(request, env, pfad);
if (pfad === '/stripe-webhook') return webhookHandler(request, env);
if (request.method !== 'POST') {
+23
View File
@@ -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)
@@ -18,3 +23,21 @@ DEMO_LIMIT_IP = "8"
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "7577adef8ceb4ed6bfaeef81a1d496af"
# Handy <-> PC: verschluesselte Uebergabe per QR-Code (siehe src/index.ts).
# SQLite-Durable-Objects laufen auch im kostenlosen Workers-Tarif.
[[durable_objects.bindings]]
name = "UEBERGABE"
class_name = "Uebergabe"
[[migrations]]
tag = "v1-uebergabe"
new_sqlite_classes = ["Uebergabe"]
# Schreib-Bremse fuer die Uebergabe: 60 Paeckchen pro Minute und IP
[[ratelimits]]
name = "UEBERGABE_LIMIT"
namespace_id = "1001"
[ratelimits.simple]
limit = 60
period = 60
+3 -3
View File
@@ -5,7 +5,7 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { AmpelStatus } from '../utils/ampel';
import { abstand, schrift } from '../theme';
import { abstand, schrift, schriftart, ecke } from '../theme';
import { Ikone, IkonenName } from './Ikone';
const IKONE: Record<string, IkonenName> = { rot: 'warnung', gelb: 'wecker', gruen: 'info' };
@@ -28,9 +28,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
borderLeftWidth: 6,
borderRadius: 12,
borderRadius: ecke,
padding: abstand.m,
gap: abstand.s,
},
text: { flex: 1, fontSize: schrift.gross, fontWeight: '700' },
text: { fontFamily: schriftart.serif, flex: 1, fontSize: schrift.gross, fontWeight: '700' },
});
+4 -4
View File
@@ -14,7 +14,7 @@ import { AppState, AppStateStatus, StyleSheet, Text, View } from 'react-native';
import * as LocalAuthentication from 'expo-local-authentication';
import { holeAppSperre } from '../services/storage';
import { GrossButton } from './GrossButton';
import { farben, schrift, abstand } from '../theme';
import { farben, schrift, abstand, schriftart } from '../theme';
export function AppSchutz({ children }: { children: React.ReactNode }) {
const [entsperrt, setEntsperrt] = useState(false);
@@ -97,14 +97,14 @@ const styles = StyleSheet.create({
gap: abstand.m,
},
sichtschutz: { justifyContent: 'center', alignItems: 'center' },
symbol: { fontSize: 56, textAlign: 'center' },
titel: {
symbol: { fontFamily: schriftart.serif, fontSize: 56, textAlign: 'center' },
titel: { fontFamily: schriftart.serif,
fontSize: schrift.titel,
fontWeight: '700',
color: farben.text,
textAlign: 'center',
},
text: {
text: { fontFamily: schriftart.serif,
fontSize: schrift.basis,
color: farben.textSekundaer,
textAlign: 'center',
+13 -11
View File
@@ -8,7 +8,7 @@
*/
import React from 'react';
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
import { farben, schrift, abstand, TOUCH_TARGET, schriftart, ecke } from '../theme';
import { Ikone, IkonenName } from './Ikone';
interface Props {
@@ -30,8 +30,8 @@ export function GrossButton({
}: Props) {
const istPrimaer = variante === 'primaer';
const istGefahr = variante === 'gefahr';
// Sekundär: dunkles Bronze statt hellem Gold — auf grauer Fläche gut lesbar
const inhaltsFarbe = istPrimaer || istGefahr ? farben.primaerText : farben.primaerFuellung;
// Wie `.cta` der Webseite: Amtsblau gefüllt bzw. weißes Blatt mit blauer Schrift
const inhaltsFarbe = istPrimaer ? farben.primaerText : istGefahr ? farben.fehler : farben.primaer;
return (
<Pressable
onPress={onPress}
@@ -44,7 +44,7 @@ export function GrossButton({
variante === 'sekundaer' && styles.sekundaer,
istGefahr && styles.gefahr,
(deaktiviert || laedt) && styles.deaktiviert,
pressed && styles.gedrueckt,
pressed && !(deaktiviert || laedt) && styles.gedrueckt,
]}
>
{laedt ? (
@@ -62,17 +62,19 @@ export function GrossButton({
const styles = StyleSheet.create({
button: {
minHeight: TOUCH_TARGET,
borderRadius: 14,
borderRadius: ecke,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: abstand.l,
paddingVertical: abstand.s,
},
zeile: { flexDirection: 'row', alignItems: 'center', gap: abstand.xs + 2 },
primaer: { backgroundColor: farben.primaerFuellung },
sekundaer: { backgroundColor: farben.flaecheSekundaer },
gefahr: { backgroundColor: farben.fehler },
deaktiviert: { opacity: 0.4 },
gedrueckt: { opacity: 0.65 },
text: { fontSize: schrift.gross, fontWeight: '600', textAlign: 'center' },
// Tinten-Rahmen + harter Versatz-Schatten wie `.cta` auf der Webseite;
// beim Drücken rutscht der Knopf in den Schatten (wie dort beim Hover)
primaer: { backgroundColor: farben.primaerFuellung, borderWidth: 2, borderColor: farben.tinte, boxShadow: '4px 4px 0px #21211D' },
sekundaer: { backgroundColor: farben.flaeche, borderWidth: 2, borderColor: farben.tinte, boxShadow: '4px 4px 0px #21211D' },
gefahr: { backgroundColor: farben.flaeche, borderWidth: 2, borderColor: farben.fehler },
deaktiviert: { opacity: 0.4, boxShadow: 'none' },
gedrueckt: { transform: [{ translateX: 2 }, { translateY: 2 }], boxShadow: '2px 2px 0px #21211D' },
text: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', textAlign: 'center' },
});
+5 -1
View File
@@ -32,7 +32,9 @@ export type IkonenName =
| 'archiv'
| 'schloss'
| 'lupe'
| 'pfeilLinks';
| 'pfeilLinks'
| 'computer'
| 'qrScannen';
// `as const` hält die Namen als Literal-Typen — so prüft TypeScript gegen
// die offizielle SF-Symbol-/Material-Namensliste von expo-symbols
@@ -62,6 +64,8 @@ const NAMEN = {
schloss: { ios: 'lock.fill', android: 'lock' },
lupe: { ios: 'magnifyingglass', android: 'search' },
pfeilLinks: { ios: 'chevron.left', android: 'chevron_left' },
computer: { ios: 'desktopcomputer', android: 'computer' },
qrScannen: { ios: 'qrcode.viewfinder', android: 'qr_code_scanner' },
} as const;
interface Props {
+39 -29
View File
@@ -24,7 +24,7 @@ import { terminZumKalender } from '../services/kalender';
import { berechneAmpel, naechsteFrist, formatiereLangDatum } from '../utils/ampel';
import { GrossButton } from '../components/GrossButton';
import { Ikone } from '../components/Ikone';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
import { farben, schrift, abstand, TOUCH_TARGET, schriftart, ecke, kartenSchatten } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Analyse'>;
@@ -287,6 +287,16 @@ export function AnalyseScreen({ navigation, route }: Props) {
</View>
)}
{/* ── In der Beratung: Brief auf dem Bildschirm zeigen ── */}
<View style={{ marginTop: abstand.l }}>
<GrossButton
titel="Am Computer zeigen"
ikone="computer"
variante="sekundaer"
onPress={() => navigation.navigate('Verbinden', { modus: 'senden', briefId: brief.id })}
/>
</View>
{/* ── Datenschutz-Fuß + Löschen ── */}
<View style={styles.schutzZeile}>
<Ikone name="schloss" groesse={13} farbe={farben.textTertiaer} />
@@ -333,66 +343,66 @@ const styles = StyleSheet.create({
zentriert: { flex: 1, justifyContent: 'center', alignItems: 'center' },
// Frist-Karte
fristKarte: { borderRadius: 16, borderWidth: 1, padding: abstand.l },
fristKarte: { borderRadius: ecke, borderWidth: 1, padding: abstand.l },
fristKopf: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 10 },
fristKicker: { fontSize: 12, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: '700' },
grosseZahl: { fontSize: 64, lineHeight: 64, fontWeight: '800', fontVariant: ['tabular-nums'] },
zahlEinheit: { fontSize: 22, fontWeight: '700', lineHeight: 24 },
zahlUnter: { fontSize: 13, color: farben.textSekundaer },
fristKicker: { fontFamily: schriftart.mono, fontSize: 12, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: '700' },
grosseZahl: { fontFamily: schriftart.serif, fontSize: 64, lineHeight: 64, fontWeight: '800', fontVariant: ['tabular-nums'] },
zahlEinheit: { fontFamily: schriftart.serif, fontSize: 22, fontWeight: '700', lineHeight: 24 },
zahlUnter: { fontFamily: schriftart.serif, fontSize: 13, color: farben.textSekundaer },
trenner: { height: 1, backgroundColor: farben.rand, marginVertical: 14 },
datenZeile: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 5 },
datenLabel: { fontSize: schrift.klein, color: farben.textSekundaer },
datenWert: { fontSize: schrift.klein, fontWeight: '700', color: farben.text },
datenLabel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer },
datenWert: { fontFamily: schriftart.serif, fontSize: schrift.klein, fontWeight: '700', color: farben.text },
keineFrist: {
flexDirection: 'row', gap: abstand.s, alignItems: 'center',
backgroundColor: farben.ampelGruenHintergrund, borderRadius: 12,
backgroundColor: farben.ampelGruenHintergrund, borderRadius: ecke,
borderLeftWidth: 4, borderLeftColor: farben.ampelGruen, padding: abstand.m,
},
keineFristTitel: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
keineFristText: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
keineFristTitel: { fontFamily: schriftart.serif, fontSize: schrift.basis, fontWeight: '700', color: farben.text },
keineFristText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
// Werkzeuge
werkzeugZeile: { flexDirection: 'row', gap: abstand.s, marginTop: abstand.m },
werkzeug: {
werkzeug: { ...kartenSchatten,
flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 7,
backgroundColor: farben.flaeche, borderRadius: 12, minHeight: 48, paddingHorizontal: abstand.s,
backgroundColor: farben.flaeche, borderRadius: ecke, minHeight: 48, paddingHorizontal: abstand.s,
},
werkzeugText: { fontSize: schrift.klein, fontWeight: '600', color: farben.primaerFuellung },
werkzeugText: { fontFamily: schriftart.serif, fontSize: schrift.klein, fontWeight: '600', color: farben.primaerFuellung },
kicker: {
kicker: { fontFamily: schriftart.mono,
fontSize: 12, letterSpacing: 1.1, textTransform: 'uppercase',
color: farben.primaer, fontWeight: '700', marginTop: abstand.l, marginBottom: abstand.xs,
color: farben.textSekundaer, fontWeight: '700', marginTop: abstand.l, marginBottom: abstand.xs,
},
kernaussage: { fontSize: schrift.gross, color: farben.text, lineHeight: 29, fontWeight: '600' },
erklaerung: { fontSize: schrift.basis, color: farben.textSekundaer, lineHeight: 27, marginTop: abstand.s },
kernaussage: { fontFamily: schriftart.serif, fontSize: schrift.gross, color: farben.text, lineHeight: 29, fontWeight: '600' },
erklaerung: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.textSekundaer, lineHeight: 27, marginTop: abstand.s },
dünnerTrenner: { height: 1, backgroundColor: farben.rand, marginTop: abstand.l },
// To-do
todoZeile: { flexDirection: 'row', gap: abstand.s, alignItems: 'flex-start', minHeight: 40 },
todoKreis: {
width: 28, height: 28, borderRadius: 14, borderWidth: 1.5, borderColor: farben.primaer,
width: 28, height: 28, borderRadius: ecke, borderWidth: 1.5, borderColor: farben.primaer,
alignItems: 'center', justifyContent: 'center', marginTop: 1,
},
todoKreisFertig: { backgroundColor: farben.ampelGruen, borderColor: farben.ampelGruen },
todoNummer: { fontSize: schrift.klein, fontWeight: '700', color: farben.primaerFuellung, fontVariant: ['tabular-nums'] },
todoText: { flex: 1, fontSize: schrift.basis, color: farben.text, lineHeight: 25, paddingTop: 3 },
todoNummer: { fontFamily: schriftart.serif, fontSize: schrift.klein, fontWeight: '700', color: farben.primaerFuellung, fontVariant: ['tabular-nums'] },
todoText: { fontFamily: schriftart.serif, flex: 1, fontSize: schrift.basis, color: farben.text, lineHeight: 25, paddingTop: 3 },
durchgestrichen: { textDecorationLine: 'line-through', color: farben.textSekundaer },
// Fachbegriffe
begriffKarte: { backgroundColor: farben.flaeche, borderRadius: 12, padding: abstand.m },
begriff: { fontSize: schrift.basis, fontWeight: '700', color: farben.text, marginBottom: 3 },
begriffText: { fontSize: schrift.klein, color: farben.textSekundaer, lineHeight: 23 },
begriffKarte: { ...kartenSchatten, backgroundColor: farben.flaeche, borderRadius: ecke, padding: abstand.m },
begriff: { fontFamily: schriftart.serif, fontSize: schrift.basis, fontWeight: '700', color: farben.text, marginBottom: 3 },
begriffText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, lineHeight: 23 },
schutzZeile: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: abstand.l },
schutzText: { fontSize: 11.5, color: farben.textTertiaer },
schutzText: { fontFamily: schriftart.serif, fontSize: 11.5, color: farben.textTertiaer },
text: { fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
text: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
// Modal
modalHintergrund: { flex: 1, backgroundColor: 'rgba(0,0,0,0.45)', justifyContent: 'center', padding: abstand.l },
modalKarte: { backgroundColor: farben.flaeche, borderRadius: 14, padding: abstand.m, maxHeight: '75%' },
modalTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text, marginBottom: abstand.xs },
modalKarte: { ...kartenSchatten, backgroundColor: farben.flaeche, borderRadius: ecke, padding: abstand.m, maxHeight: '75%' },
modalTitel: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text, marginBottom: abstand.xs },
sprachZeile: { minHeight: TOUCH_TARGET, justifyContent: 'center' },
sprachZeileText: { fontSize: schrift.gross, color: farben.text },
sprachZeileText: { fontFamily: schriftart.serif, fontSize: schrift.gross, color: farben.text },
});
+18 -18
View File
@@ -26,7 +26,7 @@ import { generiereAntwort } from '../services/antwort';
import { ClaudeFehler } from '../services/claudeClient';
import { GrossButton } from '../components/GrossButton';
import { Ikone, IkonenName } from '../components/Ikone';
import { farben, schrift, abstand } from '../theme';
import { farben, schrift, abstand, schriftart, ecke, kartenSchatten } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Antwort'>;
@@ -258,28 +258,28 @@ function escapeHtml(s: string): string {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
zentriert: { flex: 1, justifyContent: 'center', alignItems: 'center' },
text: { fontSize: schrift.basis, color: farben.text },
text: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.text },
schrittLabel: { fontSize: schrift.klein, color: farben.textSekundaer, fontWeight: '600', marginBottom: abstand.xs },
schrittLabel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, fontWeight: '600', marginBottom: abstand.xs },
fortschritt: { flexDirection: 'row', gap: 6, marginBottom: abstand.l },
balken: { flex: 1, height: 5, borderRadius: 3 },
titel: { fontSize: schrift.titel, fontWeight: '800', color: farben.text, marginBottom: 4 },
unterTitel: { fontSize: schrift.klein, color: farben.textSekundaer, lineHeight: 22, marginBottom: abstand.s },
titel: { fontFamily: schriftart.serif, fontSize: schrift.titel, fontWeight: '800', color: farben.text, marginBottom: 4 },
unterTitel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, lineHeight: 22, marginBottom: abstand.s },
// Schritt 1 — Karten
typKarte: {
typKarte: { ...kartenSchatten,
flexDirection: 'row', alignItems: 'center', gap: abstand.s,
backgroundColor: farben.flaeche, borderRadius: 14, padding: abstand.m,
backgroundColor: farben.flaeche, borderRadius: ecke, padding: abstand.m,
},
typIkone: {
width: 44, height: 44, borderRadius: 12, borderWidth: 1, borderColor: farben.primaer,
width: 44, height: 44, borderRadius: ecke, borderWidth: 1, borderColor: farben.primaer,
alignItems: 'center', justifyContent: 'center', flexShrink: 0,
},
typTitel: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
typBeschreibung: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2, lineHeight: 20 },
typTitel: { fontFamily: schriftart.serif, fontSize: schrift.basis, fontWeight: '700', color: farben.text },
typBeschreibung: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2, lineHeight: 20 },
empfohlenTag: { backgroundColor: farben.hervorhebung, borderRadius: 6, paddingHorizontal: 8, paddingVertical: 2 },
empfohlenText: { fontSize: 11, fontWeight: '700', color: farben.primaerFuellung },
empfohlenText: { fontFamily: schriftart.serif, fontSize: 11, fontWeight: '700', color: farben.primaerFuellung },
// Schritt 2/3
wahlPill: {
@@ -287,19 +287,19 @@ const styles = StyleSheet.create({
borderWidth: 1, borderColor: farben.primaer, borderRadius: 9999,
paddingHorizontal: 12, paddingVertical: 6, marginBottom: abstand.m,
},
wahlPillText: { fontSize: schrift.klein, color: farben.primaerFuellung, fontWeight: '600' },
feldLabel: { fontSize: schrift.klein, color: farben.textSekundaer, fontWeight: '600', marginTop: abstand.s, marginBottom: 6 },
eingabe: {
borderWidth: 1, borderColor: farben.rand, borderRadius: 12, padding: abstand.s,
wahlPillText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.primaerFuellung, fontWeight: '600' },
feldLabel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, fontWeight: '600', marginTop: abstand.s, marginBottom: 6 },
eingabe: { fontFamily: schriftart.serif,
borderWidth: 1, borderColor: farben.rand, borderRadius: ecke, padding: abstand.s,
fontSize: schrift.basis, color: farben.text, backgroundColor: farben.flaeche, minHeight: 52,
},
mehrzeilig: { minHeight: 110 },
briefText: { minHeight: 300, lineHeight: 26 },
infoBox: {
flexDirection: 'row', gap: abstand.s, alignItems: 'flex-start',
backgroundColor: farben.hervorhebung, borderRadius: 12, padding: abstand.m, marginTop: abstand.m,
backgroundColor: farben.hervorhebung, borderRadius: ecke, padding: abstand.m, marginTop: abstand.m,
},
infoBoxText: { flex: 1, fontSize: schrift.klein, color: farben.text, lineHeight: 21 },
infoBoxText: { fontFamily: schriftart.serif, flex: 1, fontSize: schrift.klein, color: farben.text, lineHeight: 21 },
zurueck: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: abstand.m, paddingVertical: abstand.s },
zurueckText: { fontSize: schrift.klein, color: farben.primaerFuellung, fontWeight: '600' },
zurueckText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.primaerFuellung, fontWeight: '600' },
});
+28 -17
View File
@@ -12,7 +12,8 @@ import { useAppStore } from '../store/useAppStore';
import { berechneAmpel } from '../utils/ampel';
import { formatiereDatum } from '../services/erinnerungen';
import { Ikone } from '../components/Ikone';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand, TOUCH_TARGET, schriftart, ecke, kartenSchatten } from '../theme';
type Nav = NativeStackNavigationProp<RootStackParamList>;
@@ -144,45 +145,55 @@ export function ArchivScreen() {
})}
</View>
)}
{/* Ergebnis vom Beratungs-PC per QR-Code übernehmen */}
<View style={{ marginTop: abstand.l }}>
<GrossButton
titel="Vom Computer übernehmen"
ikone="qrScannen"
variante="sekundaer"
onPress={() => navigation.navigate('Verbinden', { modus: 'empfangen' })}
/>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
kicker: {
kicker: { fontFamily: schriftart.mono,
fontSize: 12, letterSpacing: 1.3, textTransform: 'uppercase',
color: farben.primaer, fontWeight: '600', marginBottom: 4,
color: farben.textSekundaer, fontWeight: '600', marginBottom: 4,
},
titel: { fontSize: schrift.riesig, fontWeight: '800', color: farben.text, marginBottom: abstand.m },
sucheBox: {
titel: { fontFamily: schriftart.serif, fontSize: schrift.riesig, fontWeight: '800', color: farben.text, marginBottom: abstand.m },
sucheBox: { ...kartenSchatten,
flexDirection: 'row', alignItems: 'center', gap: 8,
backgroundColor: farben.flaeche, borderRadius: 12,
backgroundColor: farben.flaeche, borderRadius: ecke,
paddingHorizontal: abstand.s, marginBottom: abstand.s, minHeight: 48,
},
sucheFeld: { flex: 1, fontSize: schrift.basis, color: farben.text, paddingVertical: abstand.s },
sucheFeld: { fontFamily: schriftart.serif, flex: 1, fontSize: schrift.basis, color: farben.text, paddingVertical: abstand.s },
chip: {
borderRadius: 9999, borderWidth: 1, borderColor: farben.rand,
backgroundColor: farben.flaeche, paddingHorizontal: 14, paddingVertical: 8,
},
chipAktiv: { borderColor: farben.primaer, backgroundColor: farben.hervorhebung },
chipText: { fontSize: schrift.klein, color: farben.textSekundaer },
chipText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer },
chipTextAktiv: { color: farben.primaer, fontWeight: '600' },
zahlZeile: {
zahlZeile: { fontFamily: schriftart.mono,
fontSize: 12, letterSpacing: 1, textTransform: 'uppercase',
color: farben.textTertiaer, marginBottom: abstand.s,
},
zeile: {
zeile: { ...kartenSchatten,
flexDirection: 'row', alignItems: 'center', gap: abstand.s,
backgroundColor: farben.flaeche, borderRadius: 12,
backgroundColor: farben.flaeche, borderRadius: ecke,
padding: abstand.m, marginBottom: abstand.s, minHeight: TOUCH_TARGET,
},
punkt: { width: 10, height: 10, borderRadius: 5 },
zeileTyp: { flex: 1, fontSize: schrift.basis, fontWeight: '700', color: farben.text },
datum: { fontSize: schrift.klein, color: farben.textTertiaer, fontVariant: ['tabular-nums'] },
zeileMeta: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
ampelText: { fontSize: schrift.klein, fontWeight: '600', marginTop: 2 },
zeileTyp: { fontFamily: schriftart.serif, flex: 1, fontSize: schrift.basis, fontWeight: '700', color: farben.text },
datum: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textTertiaer, fontVariant: ['tabular-nums'] },
zeileMeta: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
ampelText: { fontFamily: schriftart.serif, fontSize: schrift.klein, fontWeight: '600', marginTop: 2 },
leerBox: { alignItems: 'center', gap: abstand.s, marginTop: abstand.xl },
leerTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
leerText: { fontSize: schrift.basis, color: farben.textSekundaer, textAlign: 'center', lineHeight: 26 },
leerTitel: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text },
leerText: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.textSekundaer, textAlign: 'center', lineHeight: 26 },
});
+5 -5
View File
@@ -8,7 +8,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../types';
import { speichereConsent } from '../services/storage';
import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand } from '../theme';
import { farben, schrift, abstand, schriftart, ecke, kartenSchatten } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Consent'>;
@@ -60,18 +60,18 @@ export function ConsentScreen({ navigation }: Props) {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
inhalt: { padding: abstand.l },
titel: {
titel: { fontFamily: schriftart.serif,
fontSize: schrift.titel,
fontWeight: '700',
color: farben.text,
marginBottom: abstand.l,
},
karte: {
karte: { ...kartenSchatten,
backgroundColor: farben.flaeche,
borderRadius: 14,
borderRadius: ecke,
padding: abstand.m,
marginBottom: abstand.xl,
gap: abstand.m,
},
punkt: { fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
punkt: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
});
+9 -9
View File
@@ -33,7 +33,7 @@ import {
import { NUTZT_PROXY } from '../services/claudeClient';
import { useAppStore } from '../store/useAppStore';
import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand } from '../theme';
import { farben, schrift, abstand, schriftart, ecke, kartenSchatten } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Einstellungen'>;
@@ -249,7 +249,7 @@ export function EinstellungenScreen({ navigation }: Props) {
<View style={styles.karte}>
<Text style={styles.text}>
• Brieffotos werden nur zur Analyse an den KI-Dienst Anthropic (USA)
gesendet und dort nach spätestens 30 Tagen gelöscht. Sie werden nicht
gesendet und dort in der Regel nach 30 Tagen gelöscht. Sie werden nicht
zum Training der KI verwendet.{'\n\n'}
• Alle Ergebnisse liegen ausschließlich auf Ihrem Gerät — verschlüsselt
gespeichert (AES-256).{'\n\n'}
@@ -268,30 +268,30 @@ export function EinstellungenScreen({ navigation }: Props) {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
abschnittTitel: {
abschnittTitel: { fontFamily: schriftart.serif,
fontSize: schrift.gross,
fontWeight: '700',
color: farben.primaer,
marginBottom: abstand.s,
},
text: { fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
hinweis: {
text: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
hinweis: { fontFamily: schriftart.serif,
fontSize: schrift.klein,
color: farben.textSekundaer,
lineHeight: 22,
marginVertical: abstand.s,
},
eingabe: {
eingabe: { fontFamily: schriftart.serif,
borderWidth: 1,
borderColor: farben.rand,
borderRadius: 12,
borderRadius: ecke,
padding: abstand.s,
fontSize: schrift.basis,
color: farben.text,
minHeight: 56,
marginBottom: abstand.s,
},
karte: { backgroundColor: farben.flaeche, borderRadius: 14, padding: abstand.m },
karte: { ...kartenSchatten, backgroundColor: farben.flaeche, borderRadius: ecke, padding: abstand.m },
trenner: { height: 1, backgroundColor: farben.rand, marginVertical: abstand.l },
schalterZeile: {
flexDirection: 'row',
@@ -301,7 +301,7 @@ const styles = StyleSheet.create({
option: {
borderWidth: 1,
borderColor: farben.rand,
borderRadius: 12,
borderRadius: ecke,
padding: abstand.s,
minHeight: 56,
justifyContent: 'center',
+50 -35
View File
@@ -21,7 +21,7 @@ import {
NaechsteFrist,
} from '../utils/ampel';
import { Ikone } from '../components/Ikone';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
import { farben, schrift, abstand, TOUCH_TARGET, schriftart, ecke, kartenSchatten, formularRahmen } from '../theme';
type Nav = NativeStackNavigationProp<RootStackParamList>;
@@ -93,8 +93,10 @@ export function FristenScreen() {
{/* Kopf */}
<View style={styles.kopf}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Ikone name="uhr" groesse={22} farbe={farben.primaer} />
<Text style={styles.marke}>BehördenKlar</Text>
<Ikone name="brief" groesse={22} farbe={farben.primaer} />
<Text style={styles.marke}>
BehördenKlar <Text style={styles.markeZusatz}>by Webklar</Text>
</Text>
</View>
<View style={{ flexDirection: 'row', gap: 8 }}>
<KopfKnopf ikone="buch" label="Glossar" onPress={() => navigation.navigate('Glossar')} />
@@ -104,7 +106,9 @@ export function FristenScreen() {
<View style={{ paddingHorizontal: abstand.m }}>
<Text style={styles.kicker}>Fristen-Übersicht</Text>
<Text style={styles.titel}>Ihre Fristen</Text>
<Text style={styles.titel}>
Ihre <Text style={styles.markiert}> Fristen </Text>
</Text>
<Text style={styles.unterTitel}>
Nach Dringlichkeit geordnet. Kümmern Sie sich zuerst um das Oberste.
</Text>
@@ -238,44 +242,55 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: abstand.m,
marginBottom: abstand.m,
marginHorizontal: abstand.m,
paddingBottom: abstand.s,
marginBottom: abstand.l,
// Briefkopf wie auf der Webseite: Tinten-Linie unter Absender
borderBottomWidth: 2,
borderBottomColor: farben.tinte,
},
marke: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
markeZusatz: { fontFamily: schriftart.serif, fontSize: 12, fontWeight: '400', color: farben.textSekundaer },
// gelber Textmarker — das Erkennungszeichen der Webseite
markiert: { backgroundColor: farben.marker },
marke: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text },
kopfKnopf: {
width: 40,
height: 40,
borderRadius: 20,
borderRadius: ecke,
backgroundColor: farben.flaeche,
borderWidth: 1.5,
borderColor: farben.tinte,
alignItems: 'center',
justifyContent: 'center',
},
kicker: {
kicker: { fontFamily: schriftart.mono,
fontSize: 12,
letterSpacing: 1.3,
textTransform: 'uppercase',
color: farben.primaer,
color: farben.textSekundaer,
fontWeight: '600',
marginBottom: 4,
},
titel: { fontSize: schrift.riesig, fontWeight: '800', color: farben.text, marginBottom: 4 },
unterTitel: { fontSize: schrift.klein, color: farben.textSekundaer, marginBottom: abstand.l },
titel: { fontFamily: schriftart.serif, fontSize: schrift.riesig, fontWeight: '800', color: farben.text, marginBottom: 4 },
unterTitel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginBottom: abstand.l },
// wichtigste Frist wie ein Formular der Webseite: Tinten-Rahmen, harter Schatten
heldKarte: {
borderRadius: 16,
borderLeftWidth: 5,
...formularRahmen,
borderRadius: ecke,
borderLeftWidth: 7,
padding: abstand.l,
marginBottom: abstand.l,
},
heldKopf: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 12 },
punkt: { width: 9, height: 9, borderRadius: 5 },
heldKicker: { fontSize: 12, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: '700' },
grosseZahl: { fontSize: 68, lineHeight: 68, fontWeight: '800', fontVariant: ['tabular-nums'] },
zahlEinheit: { fontSize: 22, fontWeight: '700', lineHeight: 24 },
zahlDatum: { fontSize: 13, color: farben.textSekundaer, marginTop: 2 },
heldKicker: { fontFamily: schriftart.mono, fontSize: 12, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: '700' },
grosseZahl: { fontFamily: schriftart.serif, fontSize: 68, lineHeight: 68, fontWeight: '800', fontVariant: ['tabular-nums'] },
zahlEinheit: { fontFamily: schriftart.serif, fontSize: 22, fontWeight: '700', lineHeight: 24 },
zahlDatum: { fontFamily: schriftart.serif, fontSize: 13, color: farben.textSekundaer, marginTop: 2 },
trenner: { height: 1, backgroundColor: farben.rand, marginVertical: 15 },
briefTyp: { fontSize: schrift.gross, fontWeight: '700', color: farben.text, marginBottom: 3 },
briefMeta: { fontSize: schrift.klein, color: farben.textSekundaer },
abschnitt: {
briefTyp: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text, marginBottom: 3 },
briefMeta: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer },
abschnitt: { fontFamily: schriftart.mono,
fontSize: 12,
letterSpacing: 1.1,
textTransform: 'uppercase',
@@ -284,42 +299,42 @@ const styles = StyleSheet.create({
marginTop: abstand.l,
marginBottom: abstand.s,
},
zeile: {
zeile: { ...kartenSchatten,
flexDirection: 'row',
alignItems: 'center',
gap: abstand.m,
backgroundColor: farben.flaeche,
borderRadius: 12,
borderRadius: ecke,
borderLeftWidth: 4,
padding: abstand.m,
},
zahlBox: { width: 54, alignItems: 'center' },
zahlKlein: { fontSize: 30, fontWeight: '800', fontVariant: ['tabular-nums'], lineHeight: 32 },
zahlBoxLabel: { fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase', color: farben.textTertiaer },
zeileTyp: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
zeileMeta: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
zahlKlein: { fontFamily: schriftart.serif, fontSize: 30, fontWeight: '800', fontVariant: ['tabular-nums'], lineHeight: 32 },
zahlBoxLabel: { fontFamily: schriftart.mono, fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase', color: farben.textTertiaer },
zeileTyp: { fontFamily: schriftart.serif, fontSize: schrift.basis, fontWeight: '700', color: farben.text },
zeileMeta: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
tag: { alignSelf: 'flex-start', borderRadius: 5, paddingHorizontal: 9, paddingVertical: 3, marginTop: 7 },
tagText: { fontSize: 11, fontWeight: '600' },
infoZeile: {
tagText: { fontFamily: schriftart.serif, fontSize: 11, fontWeight: '600' },
infoZeile: { ...kartenSchatten,
flexDirection: 'row',
alignItems: 'center',
gap: abstand.s,
backgroundColor: farben.flaeche,
borderRadius: 12,
borderRadius: ecke,
padding: abstand.m,
minHeight: TOUCH_TARGET,
},
schutz: {
schutz: { ...kartenSchatten,
flexDirection: 'row',
gap: abstand.s,
alignItems: 'flex-start',
backgroundColor: farben.flaeche,
borderRadius: 12,
borderRadius: ecke,
padding: abstand.m,
marginTop: abstand.l,
},
schutzText: { flex: 1, fontSize: 12.5, lineHeight: 19, color: farben.textSekundaer },
schutzText: { fontFamily: schriftart.serif, flex: 1, fontSize: 12.5, lineHeight: 19, color: farben.textSekundaer },
leerBox: { alignItems: 'center', gap: abstand.s, marginTop: abstand.xl, marginBottom: abstand.l },
leerTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
leerText: { fontSize: schrift.basis, color: farben.textSekundaer, textAlign: 'center', lineHeight: 26 },
leerTitel: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text },
leerText: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.textSekundaer, textAlign: 'center', lineHeight: 26 },
});
+8 -8
View File
@@ -4,7 +4,7 @@
import React, { useState } from 'react';
import { FlatList, StyleSheet, Text, TextInput, View } from 'react-native';
import { GLOSSAR } from '../data/glossar';
import { farben, schrift, abstand } from '../theme';
import { farben, schrift, abstand, schriftart, ecke, kartenSchatten } from '../theme';
export function GlossarScreen() {
const [suche, setSuche] = useState('');
@@ -44,30 +44,30 @@ export function GlossarScreen() {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
suche: {
suche: { fontFamily: schriftart.serif,
margin: abstand.m,
borderWidth: 1,
borderColor: farben.rand,
borderRadius: 12,
borderRadius: ecke,
padding: abstand.s,
fontSize: schrift.basis,
color: farben.text,
minHeight: 48,
},
karte: {
karte: { ...kartenSchatten,
backgroundColor: farben.flaeche,
borderRadius: 14,
borderRadius: ecke,
padding: abstand.m,
marginBottom: abstand.s,
},
begriff: { fontSize: schrift.gross, fontWeight: '700', color: farben.primaer },
erklaerung: {
begriff: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.primaer },
erklaerung: { fontFamily: schriftart.serif,
fontSize: schrift.basis,
color: farben.text,
lineHeight: 27,
marginTop: abstand.xs,
},
leer: {
leer: { fontFamily: schriftart.serif,
textAlign: 'center',
fontSize: schrift.basis,
color: farben.textSekundaer,
+5 -5
View File
@@ -22,7 +22,7 @@ import {
import { planeErinnerungen } from '../services/erinnerungen';
import { useAppStore, neueId } from '../store/useAppStore';
import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand } from '../theme';
import { farben, schrift, abstand, schriftart } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Scan'>;
@@ -204,7 +204,7 @@ const styles = StyleSheet.create({
gap: abstand.m,
justifyContent: 'center',
},
anleitung: {
anleitung: { fontFamily: schriftart.serif,
fontSize: schrift.basis,
color: farben.textSekundaer,
textAlign: 'center',
@@ -219,14 +219,14 @@ const styles = StyleSheet.create({
padding: abstand.xl,
gap: abstand.m,
},
kontingent: {
kontingent: { fontFamily: schriftart.serif,
fontSize: schrift.klein,
color: farben.textSekundaer,
textAlign: 'center',
marginTop: abstand.m,
},
ladeText: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
ladeHinweis: {
ladeText: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text },
ladeHinweis: { fontFamily: schriftart.serif,
fontSize: schrift.basis,
color: farben.textSekundaer,
textAlign: 'center',
+191
View File
@@ -0,0 +1,191 @@
/**
* QR-Code vom Beratungs-PC scannen (Übergabe Handy ↔ PC).
*
* Zwei Wege, beide Ende-zu-Ende verschlüsselt:
* - „senden": ein Brief aus dem Archiv erscheint am PC (der PC zeigt dafür
* auf behoerdenklar.de/demo „Lieber mit dem Handy fotografieren")
* - „empfangen": das Ergebnis vom PC landet im Archiv, mit Erinnerungen
* (der PC zeigt dafür „Ergebnis aufs Handy mitnehmen")
*
* Ohne Kamera (Simulator, Erlaubnis verweigert) kann man den Link einfügen.
*/
import React, { useRef, useState } from 'react';
import { ActivityIndicator, StyleSheet, Text, TextInput, View, ScrollView } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../types';
import { useAppStore, neueId } from '../store/useAppStore';
import { planeErinnerungen } from '../services/erinnerungen';
import { holeAusUebergabe, loescheUebergabe, oeffneUebergabe, sendeAnUebergabe } from '../services/uebergabe';
import { briefZuWebPaket, leseUebergabeLink, webPaketZuBrief, WebPaket } from '../utils/uebergabeFormat';
import { GrossButton } from '../components/GrossButton';
import { Ikone } from '../components/Ikone';
import { farben, schrift, abstand, schriftart, ecke, kartenSchatten } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Verbinden'>;
type Zustand =
| { art: 'scannen' }
| { art: 'arbeitet'; text: string }
| { art: 'fertig'; text: string }
| { art: 'fehler'; text: string };
export function VerbindenScreen({ navigation, route }: Props) {
const modus = route.params.modus;
const briefId = route.params.modus === 'senden' ? route.params.briefId : null;
const brief = useAppStore((s) => s.briefe.find((b) => b.id === briefId));
const addBrief = useAppStore((s) => s.addBrief);
const [erlaubnis, frageErlaubnis] = useCameraPermissions();
const [zustand, setZustand] = useState<Zustand>({ art: 'scannen' });
const [eingefuegt, setEingefuegt] = useState('');
const beschaeftigt = useRef(false); // Kamera meldet denselben Code mehrfach
async function verarbeite(text: string) {
if (beschaeftigt.current) return;
beschaeftigt.current = true;
try {
const link = leseUebergabeLink(text);
if (!link) {
setZustand({ art: 'fehler', text: 'Das ist kein QR-Code von BehördenKlar. Bitte den Code auf dem Bildschirm der Beratung scannen.' });
return;
}
if (link.art === 'g') {
setZustand({ art: 'fehler', text: 'Dieser Code ist für die Webseite gedacht. Öffnen Sie ihn mit der normalen Handy-Kamera.' });
return;
}
if (modus === 'senden') {
if (link.art !== 'k') {
setZustand({ art: 'fehler', text: 'Dieser Code ist zum Mitnehmen. Gehen Sie dafür ins Archiv auf „Vom Computer übernehmen".' });
return;
}
if (!brief) {
setZustand({ art: 'fehler', text: 'Der Brief wurde nicht gefunden.' });
return;
}
setZustand({ art: 'arbeitet', text: 'Der Brief wird verschlüsselt geschickt …' });
const u = await oeffneUebergabe(link);
await sendeAnUebergabe(u, 'status', { schritt: 'verbunden' });
await sendeAnUebergabe(u, 'ergebnis', briefZuWebPaket(brief));
setZustand({ art: 'fertig', text: 'Fertig. Der Brief ist jetzt auf dem Bildschirm der Beratung zu sehen.' });
return;
}
// empfangen
if (link.art !== 'm') {
setZustand({ art: 'fehler', text: 'Dieser Code wartet auf einen Brief. Öffnen Sie dafür den Brief und tippen Sie auf „Am Computer zeigen".' });
return;
}
setZustand({ art: 'arbeitet', text: 'Das Ergebnis wird geholt …' });
const u = await oeffneUebergabe(link);
const faecher = await holeAusUebergabe(u);
const paket = faecher.ergebnis as WebPaket | undefined;
if (!paket?.analyse) {
setZustand({ art: 'fehler', text: 'Dieser Code ist abgelaufen oder wurde schon benutzt (er gilt 15 Minuten und nur einmal). Bitte am Computer einen neuen erzeugen.' });
return;
}
loescheUebergabe(u);
const neu = webPaketZuBrief(paket, neueId(), new Date());
await addBrief(neu);
planeErinnerungen(neu).catch(() => {});
navigation.replace('Analyse', { briefId: neu.id });
} catch {
setZustand({ art: 'fehler', text: 'Keine Verbindung. Bitte prüfen Sie das Internet und versuchen Sie es noch einmal.' });
} finally {
beschaeftigt.current = false;
}
}
const titel = modus === 'senden' ? 'Am Computer zeigen' : 'Vom Computer übernehmen';
const anleitung =
modus === 'senden'
? 'Die Beratung öffnet behoerdenklar.de/demo und tippt auf „Lieber mit dem Handy fotografieren". Scannen Sie den QR-Code, dann erscheint Ihr Brief auf dem Bildschirm.'
: 'Die Beratung tippt am Computer auf „Ergebnis aufs Handy mitnehmen". Scannen Sie den QR-Code, dann landet der Brief verschlüsselt in Ihrem Archiv.';
return (
<ScrollView style={styles.container} contentContainerStyle={{ padding: abstand.m, paddingBottom: abstand.xl }} keyboardShouldPersistTaps="handled">
<Text style={styles.titel}>{titel}</Text>
<Text style={styles.text}>{anleitung}</Text>
{zustand.art === 'scannen' && (
<View style={styles.kameraRahmen}>
{erlaubnis?.granted ? (
<CameraView
style={StyleSheet.absoluteFill}
facing="back"
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={({ data }) => verarbeite(data)}
/>
) : (
<View style={styles.kameraLeer}>
<Text style={styles.text}>Für den QR-Code braucht die App kurz die Kamera.</Text>
<GrossButton titel="Kamera erlauben" ikone="kamera" onPress={frageErlaubnis} />
</View>
)}
</View>
)}
{zustand.art === 'arbeitet' && (
<View style={styles.meldung}>
<ActivityIndicator color={farben.primaer} />
<Text style={styles.text}>{zustand.text}</Text>
</View>
)}
{zustand.art === 'fertig' && (
<View style={styles.meldung}>
<Ikone name="checkAn" groesse={28} farbe={farben.ampelGruen} />
<Text style={styles.text}>{zustand.text}</Text>
<GrossButton titel="Zurück zum Brief" onPress={() => navigation.goBack()} />
</View>
)}
{zustand.art === 'fehler' && (
<View style={styles.meldung}>
<Ikone name="warnung" groesse={26} farbe={farben.fehler} />
<Text style={styles.text}>{zustand.text}</Text>
<GrossButton titel="Noch einmal scannen" variante="sekundaer" onPress={() => setZustand({ art: 'scannen' })} />
</View>
)}
<View style={styles.schutzZeile}>
<Ikone name="schloss" groesse={13} farbe={farben.textTertiaer} />
<Text style={styles.schutzText}>Ende-zu-Ende verschlüsselt · nach 15 Minuten gelöscht</Text>
</View>
{zustand.art === 'scannen' && (
<View style={{ marginTop: abstand.l }}>
<Text style={styles.klein}>Kein Scannen möglich? Link hier einfügen:</Text>
<TextInput
style={styles.feld}
value={eingefuegt}
onChangeText={setEingefuegt}
placeholder="https://behoerdenklar.de/demo#…"
placeholderTextColor={farben.textTertiaer}
autoCapitalize="none"
autoCorrect={false}
accessibilityLabel="Link vom Computer einfügen"
/>
<GrossButton titel="Link verwenden" variante="sekundaer" deaktiviert={!eingefuegt.trim()} onPress={() => verarbeite(eingefuegt)} />
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
titel: { fontFamily: schriftart.serif, fontSize: schrift.titel, fontWeight: '700', color: farben.text, marginBottom: abstand.s },
text: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.text, lineHeight: 26 },
klein: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginBottom: abstand.s },
kameraRahmen: {
marginTop: abstand.m, height: 300, borderRadius: ecke, overflow: 'hidden',
backgroundColor: farben.flaecheSekundaer,
},
kameraLeer: { flex: 1, justifyContent: 'center', padding: abstand.m, gap: abstand.m },
meldung: { ...kartenSchatten, marginTop: abstand.m, gap: abstand.m, padding: abstand.m, borderRadius: ecke, backgroundColor: farben.flaeche },
schutzZeile: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: abstand.m },
schutzText: { fontFamily: schriftart.serif, fontSize: 13, color: farben.textTertiaer },
feld: { fontFamily: schriftart.serif,
backgroundColor: farben.flaeche, borderRadius: ecke, padding: 14, fontSize: schrift.klein,
color: farben.text, borderWidth: 1, borderColor: farben.rand, marginBottom: abstand.s,
},
});
+59
View File
@@ -0,0 +1,59 @@
/**
* Übergabe Handy ↔ PC — Ende-zu-Ende verschlüsselt (AES-256-GCM).
*
* Päckchen-Format wie auf der Webseite: base64url(IV[12] || Chiffrat || Tag[16]).
* expo-crypto liefert mit `combined()` genau diese Reihenfolge, deshalb können
* App und Webseite die Päckchen gegenseitig lesen. Der Worker sieht nur
* Chiffrat und löscht alles nach 15 Minuten.
*/
import { AESEncryptionKey, AESSealedData, aesDecryptAsync, aesEncryptAsync } from 'expo-crypto';
import { utf8Dekodieren, utf8Kodieren } from '../utils/utf8';
import { UebergabeLink, base64UrlZuBase64, base64ZuBase64Url } from '../utils/uebergabeFormat';
export interface OffeneUebergabe {
link: UebergabeLink;
schluessel: AESEncryptionKey;
}
export async function oeffneUebergabe(link: UebergabeLink): Promise<OffeneUebergabe> {
const schluessel = await AESEncryptionKey.import(base64UrlZuBase64(link.schluessel), 'base64');
return { link, schluessel };
}
async function verschluesseln(u: OffeneUebergabe, daten: unknown): Promise<string> {
const versiegelt = await aesEncryptAsync(utf8Kodieren(JSON.stringify(daten)), u.schluessel);
return base64ZuBase64Url((await versiegelt.combined('base64')) as string);
}
async function entschluesseln(u: OffeneUebergabe, text: string): Promise<unknown | null> {
try {
const versiegelt = AESSealedData.fromCombined(base64UrlZuBase64(text));
const bytes = (await aesDecryptAsync(versiegelt, u.schluessel)) as Uint8Array;
return JSON.parse(utf8Dekodieren(bytes));
} catch {
return null; // nicht von dieser Übergabe oder beschädigt
}
}
const adresse = (u: OffeneUebergabe) => `${u.link.basis}/uebergabe/${u.link.id}`;
export async function sendeAnUebergabe(u: OffeneUebergabe, fach: string, daten: unknown): Promise<void> {
const r = await fetch(`${adresse(u)}/${fach}`, { method: 'PUT', body: await verschluesseln(u, daten) });
if (!r.ok) throw new Error(`Übergabe fehlgeschlagen (${r.status})`);
}
export async function holeAusUebergabe(u: OffeneUebergabe): Promise<Record<string, unknown>> {
const r = await fetch(adresse(u), { headers: { 'cache-control': 'no-store' } });
if (!r.ok) throw new Error(`Übergabe nicht erreichbar (${r.status})`);
const faecher = ((await r.json()) as { faecher?: Record<string, string> }).faecher ?? {};
const aus: Record<string, unknown> = {};
for (const [fach, wert] of Object.entries(faecher)) {
const d = await entschluesseln(u, wert);
if (d) aus[fach] = d;
}
return aus;
}
export function loescheUebergabe(u: OffeneUebergabe): void {
fetch(adresse(u), { method: 'DELETE' }).catch(() => {});
}
+65 -34
View File
@@ -1,38 +1,51 @@
/**
* Design-System: ruhig, vertrauenswürdig, barrierefrei.
* Design-System — wie die Webseite behoerdenklar.de: ein Amtsbrief, der
* endlich verständlich ist. Papier, Tinte, Amtsblau, gelber Textmarker,
* Georgia-Serifenschrift, Etiketten in Schreibmaschinenschrift.
*
* - Große Schrift (Basis 18) für die Zielgruppe
* - Hoher Kontrast (dunkles Blau auf Weiß, WCAG AA)
* - Hoher Kontrast (Tinte auf Papier, WCAG AA)
* - Große Touch-Targets (min. 56px Höhe)
*
* Die Werte entsprechen den CSS-Variablen der Webseite (--papier, --tinte,
* --amtsblau, --marker, --stempelrot …). Ändert sich dort etwas, hier mitziehen.
*/
import { Platform, TextStyle, ViewStyle } from 'react-native';
export const farben = {
// Warme, ruhige Palette nach dem Claude-Design-Entwurf ("Postklar/Classical")
// und passend zur Website: Papier-Beige, Gold-Akzent, dunkle Tinte.
// Gut lesbar (hoher Kontrast bei Fließtext), nur die Schrift bleibt
// die klare System-Schrift statt der dünnen Serifenschrift.
hintergrund: '#F1EFE9', // warmes Off-White (Grund)
flaeche: '#FBFAF6', // warme, fast weiße Karten
flaecheSekundaer: '#EAE7E0', // gedämpfte Fläche (graue Taste)
primaer: '#B68235', // Gold-Akzent (Icons, aktive Tabs, Rahmen, Punkte)
primaerFuellung: '#7D5411', // dunkles Bronze — Hintergrund gefüllter Buttons (weiße Schrift lesbar)
primaerText: '#FBF7EF', // warmes Weiß (Text auf gefüllten Buttons)
text: '#201F1D', // dunkle Tinte
textSekundaer: '#6B675E', // warmes Grau (wie --grau der Website)
textTertiaer: '#A8A29A', // helles warmes Grau (Chevrons)
rand: '#E2DED6', // warme Trennlinie
// Dezente Gold-Hervorhebung (z. B. aktive Filter-Chips)
hervorhebung: '#F7EFE0',
hervorhebungRand: '#E9D8BC',
hintergrund: '#FAF7F0', // Papier (--papier)
flaeche: '#FFFFFF', // weißes Formularblatt (Karten, Felder)
flaecheSekundaer: '#F1ECDF', // dunkleres Papier (--papier-dunkel)
primaer: '#1D4E89', // Amtsblau (Icons, aktive Tabs, Links)
primaerFuellung: '#1D4E89', // Amtsblau — gefüllte Knöpfe
primaerText: '#FFFFFF', // Text auf gefüllten Knöpfen
text: '#21211D', // Tinte
textSekundaer: '#6B675E', // Grau (--grau)
textTertiaer: '#9A958A', // helles Grau (Platzhalter, Chevrons)
rand: 'rgba(33,33,29,0.22)', // dünne Linie wie auf der Webseite
tinte: '#21211D', // harte Rahmen und Schatten
marker: '#FFE66B', // gelber Textmarker (Signatur der Marke)
stempelrot: '#B3372B',
// Hervorhebung = Textmarker (z. B. aktive Filter)
hervorhebung: '#FFE66B',
hervorhebungRand: '#21211D',
// Dringlichkeits-Ampel in den warmen Design-Tönen — als Text auf hellem
// Tint gut lesbar (WCAG): Backstein-Rot, Bronze-Gold, Salbei-Grün.
ampelRot: '#A23B2B',
ampelGelb: '#8A5E14',
ampelGruen: '#3F5A4C',
ampelRotHintergrund: '#F5E9E5',
ampelGelbHintergrund: '#F5EEDF',
ampelGruenHintergrund: '#E8EFEA',
// Dringlichkeits-Ampel wie auf der Webseite (--rot, --gelb, --gruen),
// Gelb etwas dunkler, damit es als Text auf hellem Grund WCAG AA erreicht.
ampelRot: '#B3372B',
ampelGelb: '#8A5A00',
ampelGruen: '#2E7D4F',
ampelRotHintergrund: '#FDF1EF',
ampelGelbHintergrund: '#FFF6D6',
ampelGruenHintergrund: '#E7F4EA',
fehler: '#A23B2B',
fehler: '#B3372B',
};
/** Schriften der Webseite: Georgia (Text) und Courier (Etiketten). */
export const schriftart = {
serif: Platform.select({ ios: 'Georgia', android: 'serif', default: 'Georgia' }) as string,
mono: Platform.select({ ios: 'Courier New', android: 'monospace', default: 'Courier New' }) as string,
};
export const schrift = {
@@ -54,9 +67,27 @@ export const abstand = {
/** Mindesthöhe für alle interaktiven Elemente (Barrierefreiheit). */
export const TOUCH_TARGET = 56;
/**
* Bewusst leer: Nach Apple-Vorbild heben sich weiße Karten allein durch
* den grauen System-Hintergrund ab — ohne Schatten und Rahmen.
* (Als No-Op behalten, damit bestehende Spreads harmlos bleiben.)
*/
export const kartenSchatten = {} as const;
/** Papier hat (fast) keine runden Ecken. */
export const ecke = 2;
/** Karte wie `.brief-karte` der Webseite: weißes Blatt, feine Linie, flacher Schatten. */
export const kartenSchatten: ViewStyle = {
borderWidth: 1,
borderColor: 'rgba(33,33,29,0.28)',
boxShadow: '4px 5px 0px rgba(33,33,29,0.08)',
};
/** Formular wie `.antrag`: Tinten-Rahmen mit hartem Versatz-Schatten. */
export const formularRahmen: ViewStyle = {
borderWidth: 2,
borderColor: farben.tinte,
boxShadow: '6px 7px 0px rgba(33,33,29,0.9)',
};
/** Etikett wie `h2`/`.karten-label` der Webseite: Schreibmaschine, Versalien. */
export const etikett: TextStyle = {
fontFamily: schriftart.mono,
fontSize: 13,
letterSpacing: 1.4,
textTransform: 'uppercase',
};
+2
View File
@@ -145,4 +145,6 @@ export type RootStackParamList = {
Antwort: { briefId: string };
Glossar: undefined;
Einstellungen: undefined;
/** QR-Code vom Beratungs-PC: Brief hinschicken oder Ergebnis übernehmen. */
Verbinden: { modus: 'senden'; briefId: string } | { modus: 'empfangen' };
};
+112
View File
@@ -0,0 +1,112 @@
/// <reference types="node" />
/**
* Übergabe Handy ↔ PC: App und Webseite müssen die Päckchen gegenseitig lesen.
*
* Die Web-Seite wird hier mit WebCrypto nachgebaut — exakt wie in
* webseite/uebergabe.js. Für die App läuft die echte Web-Implementierung von
* expo-crypto (gleiche Kombi-Reihenfolge IV‖Chiffrat‖Tag wie nativ).
*/
jest.mock('expo-crypto/build/aes/ExpoCryptoAES', () => jest.requireActual('expo-crypto/build/aes/ExpoCryptoAES.web'));
import { webcrypto } from 'crypto';
import { holeAusUebergabe, oeffneUebergabe, sendeAnUebergabe } from '../../services/uebergabe';
import { briefZuWebPaket, leseUebergabeLink, webPaketZuBrief } from '../uebergabeFormat';
import { BriefEintrag } from '../../types';
const subtle = webcrypto.subtle;
const b64u = (b: Uint8Array) => Buffer.from(b).toString('base64url');
// --- Web-Seite (wie webseite/uebergabe.js) ---
async function webSchluessel(k: string) {
return subtle.importKey('raw', Buffer.from(k, 'base64url'), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
}
async function webVerschluesseln(k: string, daten: unknown) {
const iv = webcrypto.getRandomValues(new Uint8Array(12));
const ct = new Uint8Array(await subtle.encrypt({ name: 'AES-GCM', iv }, await webSchluessel(k), new TextEncoder().encode(JSON.stringify(daten))));
return b64u(Buffer.concat([iv, ct]));
}
async function webEntschluesseln(k: string, text: string) {
const b = Buffer.from(text, 'base64url');
const klar = await subtle.decrypt({ name: 'AES-GCM', iv: b.subarray(0, 12) }, await webSchluessel(k), b.subarray(12));
return JSON.parse(new TextDecoder().decode(klar));
}
const ID = 'v1wRNjDlmRZeGSS_YRiRVA';
const KEY = b64u(webcrypto.getRandomValues(new Uint8Array(32)));
const link = (a: string) => `https://behoerdenklar.de/demo#a=${a}&s=${ID}&k=${KEY}&l=ar&d=1`;
const ANALYSE_DE = {
brieftyp: 'Aufforderung', absender: 'Ausländerbehörde', kernaussage: 'Unterlagen fehlen.',
erklaerung_einfach: 'Bitte bis 12.11. einreichen.', fachbegriffe: [{ begriff: 'Fiktionsbescheinigung', erklaerung: 'Papier' }],
frist: { datum: '2026-11-12', aktion: 'Unterlagen einreichen' }, termin: null, checkliste: ['Pass'],
kontakt: { telefon: '069 90009-314' }, anruf: [],
};
const ANALYSE_AR = { ...ANALYSE_DE, kernaussage: 'المستندات ناقصة.', checkliste: ['جواز السفر'] };
describe('leseUebergabeLink', () => {
it('liest Kamera- und Mitnehmen-Codes', () => {
expect(leseUebergabeLink(link('k'))).toMatchObject({ art: 'k', id: ID, schluessel: KEY, basis: 'https://behoerdenklar-proxy.behoerdenbrief.workers.dev' });
expect(leseUebergabeLink(link('m'))?.art).toBe('m');
});
it('nimmt beim lokalen Test den lokalen Worker', () => {
expect(leseUebergabeLink(`http://localhost:4173/demo.html#a=m&s=${ID}&k=${KEY}`)?.basis).toBe('http://localhost:8799');
});
it('lehnt fremde Seiten und kaputte Codes ab', () => {
expect(leseUebergabeLink(`https://boese.example/demo#a=m&s=${ID}&k=${KEY}`)).toBeNull();
expect(leseUebergabeLink('https://behoerdenklar.de/demo#a=m&s=kurz&k=kurz')).toBeNull();
expect(leseUebergabeLink('Hallo')).toBeNull();
});
});
describe('Verschlüsselung App ↔ Webseite', () => {
const fetchOriginal = global.fetch;
afterEach(() => { global.fetch = fetchOriginal; });
it('App liest, was die Webseite verschlüsselt hat', async () => {
const chiffrat = await webVerschluesseln(KEY, { analyse: ANALYSE_AR, analyse_de: ANALYSE_DE, sprache: 'ar' });
global.fetch = jest.fn(async () => new Response(JSON.stringify({ faecher: { ergebnis: chiffrat, fremd: 'QUJD' } }))) as typeof fetch;
const u = await oeffneUebergabe(leseUebergabeLink(link('m'))!);
const faecher = await holeAusUebergabe(u);
expect(faecher.ergebnis).toEqual({ analyse: ANALYSE_AR, analyse_de: ANALYSE_DE, sprache: 'ar' });
expect(faecher.fremd).toBeUndefined(); // nicht entschlüsselbar → ignoriert
});
it('Webseite liest, was die App verschlüsselt hat', async () => {
let gesendet = '';
let adresse = '';
global.fetch = jest.fn(async (url: string, opt: RequestInit) => {
adresse = url;
gesendet = String(opt.body);
return new Response(null, { status: 204 });
}) as unknown as typeof fetch;
const u = await oeffneUebergabe(leseUebergabeLink(link('k'))!);
await sendeAnUebergabe(u, 'ergebnis', { analyse: ANALYSE_DE, sprache: 'de' });
expect(adresse).toBe(`https://behoerdenklar-proxy.behoerdenbrief.workers.dev/uebergabe/${ID}/ergebnis`);
expect(gesendet).toMatch(/^[A-Za-z0-9_-]+$/); // base64url, wie der Worker verlangt
expect(await webEntschluesseln(KEY, gesendet)).toEqual({ analyse: ANALYSE_DE, sprache: 'de' });
});
});
describe('Umwandeln Web ↔ App', () => {
it('Ergebnis vom PC wird ein Brief: Deutsch als Grundlage, Arabisch als Übersetzung', () => {
const b = webPaketZuBrief({ analyse: ANALYSE_AR, analyse_de: ANALYSE_DE, sprache: 'ar' }, 'id1', new Date('2026-10-29T10:00:00Z'));
expect(b.analyse.kernaussage).toBe('Unterlagen fehlen.');
expect(b.analyse.frist).toEqual({ datum: '2026-11-12', aktion: 'Unterlagen einreichen' });
expect(b.analyse.antwort_noetig).toBe(false);
expect(b.uebersetzungen.ar.kernaussage).toBe('المستندات ناقصة.');
expect(b.erstelltAm).toBe('2026-10-29T10:00:00.000Z');
});
it('rein deutsches Ergebnis hat keine Übersetzung', () => {
const b = webPaketZuBrief({ analyse: ANALYSE_DE, sprache: 'de' }, 'id2', new Date());
expect(b.analyse.kernaussage).toBe('Unterlagen fehlen.');
expect(b.uebersetzungen).toEqual({});
});
it('Brief aus dem Archiv geht zweisprachig an den PC', () => {
const brief = webPaketZuBrief({ analyse: ANALYSE_AR, analyse_de: ANALYSE_DE, sprache: 'ar' }, 'id3', new Date());
const p = briefZuWebPaket(brief as BriefEintrag);
expect(p.sprache).toBe('ar');
expect(p.analyse.kernaussage).toBe('المستندات ناقصة.');
expect(p.analyse.frist).toEqual(ANALYSE_DE.frist);
expect(p.analyse_de?.kernaussage).toBe('Unterlagen fehlen.');
});
});
+117
View File
@@ -0,0 +1,117 @@
/**
* Übergabe Handy ↔ PC — reine Hilfsfunktionen ohne native Module (testbar).
*
* Gleiches Protokoll wie die Webseite (webseite/uebergabe.js): Der QR-Code
* enthält hinter dem `#` eine Übergabe-ID (s), einen AES-256-Schlüssel (k)
* und die Art der Übergabe (a):
* a=k „Kamera": der PC wartet auf einen Brief → App schickt ihn hin
* a=m „Mitnehmen": der PC hat ein Ergebnis bereitgelegt → App holt es
* a=g Guthaben der Webseite (gilt nur im Browser, nicht in der App)
*/
import { BriefAnalyse, BriefEintrag, Fachbegriff, Uebersetzung } from '../types';
export type UebergabeArt = 'k' | 'm' | 'g';
export interface UebergabeLink {
art: UebergabeArt;
id: string;
schluessel: string; // base64url, 32 Byte
/** Worker, der die Päckchen hält (lokal beim Testen, sonst live). */
basis: string;
}
const LIVE_BASIS = 'https://behoerdenklar-proxy.behoerdenbrief.workers.dev';
const LOKAL_BASIS = 'http://localhost:8799';
const ERLAUBTE_SEITEN = /^https:\/\/([a-z0-9-]+\.)?behoerdenklar\.(de|pages\.dev)\//;
const LOKALE_SEITE = /^http:\/\/(localhost|127\.0\.0\.1):\d+\//;
/** Liest einen gescannten QR-Code. null = kein BehördenKlar-Code. */
export function leseUebergabeLink(text: string): UebergabeLink | null {
const t = (text || '').trim();
const lokal = LOKALE_SEITE.test(t);
if (!ERLAUBTE_SEITEN.test(t) && !lokal) return null;
const raute = t.indexOf('#');
if (raute < 0) return null;
const werte: Record<string, string> = {};
for (const teil of t.slice(raute + 1).split('&')) {
const i = teil.indexOf('=');
if (i > 0) werte[teil.slice(0, i)] = decodeURIComponent(teil.slice(i + 1));
}
const art = werte.a as UebergabeArt;
if (art === 'g') return { art, id: '', schluessel: '', basis: '' };
if ((art !== 'k' && art !== 'm') || !/^[A-Za-z0-9_-]{22}$/.test(werte.s ?? '') || !/^[A-Za-z0-9_-]{43}$/.test(werte.k ?? '')) {
return null;
}
return { art, id: werte.s, schluessel: werte.k, basis: lokal ? LOKAL_BASIS : LIVE_BASIS };
}
export function base64UrlZuBase64(s: string): string {
const b = s.replace(/-/g, '+').replace(/_/g, '/');
return b + '='.repeat((4 - (b.length % 4)) % 4);
}
export function base64ZuBase64Url(s: string): string {
return s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
/** Was die Webseite als Ergebnis schickt bzw. erwartet. */
export interface WebPaket {
analyse: WebAnalyse;
analyse_de?: WebAnalyse;
sprache?: string;
}
/** Die Analyse der Webseite: wie BriefAnalyse, dafür mit Kontakt + Anruf-Hilfe. */
type WebAnalyse = Partial<BriefAnalyse> & {
kontakt?: unknown;
anruf?: unknown;
};
function textOder(s: unknown, ersatz = ''): string {
return typeof s === 'string' ? s : ersatz;
}
function alsBriefAnalyse(w: WebAnalyse): BriefAnalyse {
return {
brieftyp: textOder(w.brieftyp, 'Brief'),
absender: textOder(w.absender),
kernaussage: textOder(w.kernaussage),
erklaerung_einfach: textOder(w.erklaerung_einfach),
fachbegriffe: Array.isArray(w.fachbegriffe) ? (w.fachbegriffe as Fachbegriff[]) : [],
frist: w.frist ?? null,
termin: w.termin ?? null,
checkliste: Array.isArray(w.checkliste) ? w.checkliste.filter((x) => typeof x === 'string') : [],
// Die Web-Analyse kennt keine Antwort-Vorschläge — die App kann sie nicht erfinden
antwort_noetig: false,
antwort_optionen: [],
};
}
/** Ergebnis vom PC → Brief fürs Archiv. Deutsch ist die Grundlage, die
* andere Sprache landet im Übersetzungs-Cache (wie bei App-Übersetzungen). */
export function webPaketZuBrief(p: WebPaket, id: string, jetzt: Date): BriefEintrag {
const sprache = p.sprache && p.sprache !== 'de' ? p.sprache : null;
const deutsch = p.analyse_de ?? (sprache ? null : p.analyse);
const basis = alsBriefAnalyse(deutsch ?? p.analyse);
const uebersetzungen: Record<string, Uebersetzung> = {};
if (sprache) {
const f = alsBriefAnalyse(p.analyse);
uebersetzungen[sprache] = {
kernaussage: f.kernaussage,
erklaerung_einfach: f.erklaerung_einfach,
checkliste: f.checkliste,
fachbegriffe: f.fachbegriffe,
};
}
return { id, erstelltAm: jetzt.toISOString(), analyse: basis, uebersetzungen };
}
/** Brief aus dem Archiv → Ergebnis für den PC. Gibt es eine Übersetzung,
* bekommt der PC beide Sprachen (Deutsch zuerst für die Beratung). */
export function briefZuWebPaket(b: BriefEintrag): WebPaket {
const deutsch: WebAnalyse = { ...b.analyse };
const [sprache] = Object.keys(b.uebersetzungen ?? {});
if (!sprache) return { analyse: deutsch, sprache: 'de' };
return {
analyse: { ...b.analyse, ...b.uebersetzungen[sprache] },
analyse_de: deutsch,
sprache,
};
}
+147
View File
@@ -0,0 +1,147 @@
/**
* 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 (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
*
* Ein Code ist ein Guthaben wie beim Kauf, das allen gehört, die den Code
* kennen. Die Ratsuchenden zahlen nichts, die Einrichtung zahlt (oder testet
* gratis). Gilt bis zum Ablaufdatum oder bis die Briefe aufgebraucht sind.
*
* Braucht nur den wrangler-Login (npx wrangler login), kein Secret.
* Link für Handzettel/QR-Code: https://behoerdenklar.de/demo?code=<CODE>
*/
const { execFileSync } = require('child_process');
const crypto = require('crypto');
const path = require('path');
const PROXY = path.join(__dirname, '..', 'proxy');
const KV_ID = '7577adef8ceb4ed6bfaeef81a1d496af'; // RATE_LIMIT aus proxy/wrangler.toml
// ohne 0/O, 1/I/L: am Telefon und auf Papier nicht zu verwechseln
const ZEICHEN = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
function kv(...args) {
return execFileSync('npx', ['wrangler', 'kv', 'key', ...args, '--namespace-id', KV_ID, '--remote'],
{ cwd: PROXY, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
}
function lies(schluessel) {
try { return kv('get', schluessel, '--text'); } catch { return null; }
}
function schreib(schluessel, wert) { kv('put', schluessel, wert); }
function option(name, standard) {
const i = process.argv.indexOf('--' + name);
return i > 0 ? process.argv[i + 1] : standard;
}
function abbruch(text) { console.error('\x1b[31m' + text + '\x1b[0m'); process.exit(1); }
function zufall(laenge) {
let s = '';
for (const b of crypto.randomBytes(laenge)) s += ZEICHEN[b % ZEICHEN.length];
return s;
}
function plusMonate(monate) {
const d = new Date();
d.setMonth(d.getMonth() + 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 = normCode(code);
const walletId = lies(`code:${code}`);
if (!walletId) abbruch(`Code ${code} gibt es nicht.`);
const einrichtung = JSON.parse(lies(`einrichtung:${walletId}`) || '{}');
const rest = parseInt(lies(`wallet:${walletId}`) || '0', 10);
return { code, walletId, 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}${auswertung}
`);
}
const [befehl, a, b] = process.argv.slice(2);
if (befehl === 'neu') {
const name = a;
const praefix = String(b || '').toUpperCase();
if (!name || !/^[A-Z]{2,12}$/.test(praefix)) {
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 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();
schreib(`wallet:${walletId}`, String(briefe));
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, walletId });
} else if (befehl === 'stand') {
zeige(ladeCode(a));
} else if (befehl === 'aufladen') {
const c = ladeCode(a);
const dazu = parseInt(b, 10);
if (!(dazu > 0)) abbruch('So geht es: node tools/einrichtungs-code.js aufladen MBE-K7QX 100');
schreib(`wallet:${c.walletId}`, String(c.rest + dazu));
console.log(`\n \x1b[32m✓ ${dazu} Briefe dazu\x1b[0m`);
zeige({ ...c, rest: c.rest + dazu });
} else if (befehl === 'verlaengern') {
const c = ladeCode(a);
if (!/^\d{4}-\d{2}-\d{2}$/.test(b || '')) abbruch('So geht es: node tools/einrichtungs-code.js verlaengern MBE-K7QX 2027-06-30');
const einrichtung = { ...c.einrichtung, ablauf: b };
schreib(`einrichtung:${c.walletId}`, JSON.stringify(einrichtung));
console.log('\n \x1b[32m✓ verlängert\x1b[0m');
zeige({ ...c, einrichtung });
} else {
console.log(require('fs').readFileSync(__filename, 'utf8').split('*/')[0].replace(/^\/\*\*?|^ \* ?/gm, ''));
}
+27 -27
View File
@@ -60,7 +60,7 @@ TEXTE.tr = {
faq: [
{ f: "Ücreti ne kadar?", a: "İlk 3 mektup ücretsiz (bekleme listesi koduyla 5). Sonrasında kontör satın alırsınız: 5 analiz 2,99 €, 15 analiz 6,99 €. Abonelik yok, hesap açmak yok." },
{ f: "Hangi mektupları açıklayabilir?", a: "İş ve İşçi Bulma Kurumu (Jobcenter), Yabancılar Dairesi, emeklilik kurumu, hastalık sigortası, aile kasası (Kindergeld), vergi dairesi veya ceza dairesinden gelen mektuplar — kısacası hemen hemen her resmî mektup." },
{ f: "Verilerime ne oluyor?", a: "Mektuplarınız şifreli olarak kendi cihazınızda kalır. Analiz için fotoğraf bir kez güvenli şekilde yapay zekâya gönderilir ve orada en geç 30 gün sonra silinir. Hesap açmanız gerekmez." },
{ f: "Verilerime ne oluyor?", a: "Mektuplarınız şifreli olarak kendi cihazınızda kalır. Analiz için fotoğraf bir kez güvenli şekilde yapay zekâya gönderilir ve orada genellikle 30 gün sonra silinir. Hesap açmanız gerekmez." },
],
locale: 'tr_TR', navLabel: 'Dil seçin',
titel: 'BehördenKlar – Alman resmî mektuplarını anlayın',
@@ -104,7 +104,7 @@ TEXTE.ar = {
faq: [
{ f: "كم التكلفة؟", a: "أول 3 رسائل مجاناً (5 مع رمز قائمة الانتظار). بعد ذلك تشتري رصيداً: 5 تحليلات بـ 2,99 يورو أو 15 بـ 6,99 يورو. بدون اشتراك وبدون حساب." },
{ f: "أي الرسائل يمكن شرحها؟", a: "رسائل من مركز العمل (Jobcenter)، دائرة الأجانب، التأمين التقاعدي، التأمين الصحي، صندوق الأسرة (Kindergeld)، مكتب الضرائب أو دائرة المخالفات — باختصار: تقريباً كل رسالة رسمية." },
{ f: "ماذا يحدث لبياناتي؟", a: "تبقى رسائلك مشفّرة على جهازك. للتحليل تُرسل الصورة مرة واحدة بشكل آمن إلى الذكاء الاصطناعي وتُحذف هناك خلال 30 يوماً كحد أقصى. لا تحتاج إلى حساب." },
{ f: "ماذا يحدث لبياناتي؟", a: "تبقى رسائلك مشفّرة على جهازك. للتحليل تُرسل الصورة مرة واحدة بشكل آمن إلى الذكاء الاصطناعي وتُحذف هناك عادةً بعد 30 يوماً. لا تحتاج إلى حساب." },
],
locale: 'ar_AR', navLabel: 'اختر اللغة',
titel: 'BehördenKlar – افهم الرسائل الرسمية الألمانية',
@@ -148,7 +148,7 @@ TEXTE.ru = {
faq: [
{ f: "Сколько это стоит?", a: "Первые 3 письма бесплатно (5 с кодом из списка ожидания). Дальше вы покупаете баланс: 5 разборов за 2,99 €, 15 за 6,99 €. Без подписки и без учётной записи." },
{ f: "Какие письма приложение объясняет?", a: "Письма из джобцентра, ведомства по делам иностранцев, пенсионного страхования, больничной кассы, семейной кассы (Kindergeld), налоговой или органа по штрафам — словом, почти любое официальное письмо." },
{ f: "Что происходит с моими данными?", a: "Ваши письма остаются зашифрованными на вашем устройстве. Для разбора фото один раз безопасно отправляется ИИ и удаляется там не позднее чем через 30 дней. Учётная запись не нужна." },
{ f: "Что происходит с моими данными?", a: "Ваши письма остаются зашифрованными на вашем устройстве. Для разбора фото один раз безопасно отправляется ИИ и удаляется там обычно через 30 дней. Учётная запись не нужна." },
],
locale: 'ru_RU', navLabel: 'Выберите язык',
titel: 'BehördenKlar – понять письмо из немецкого ведомства',
@@ -192,7 +192,7 @@ TEXTE.uk = {
faq: [
{ f: "Скільки це коштує?", a: "Перші 3 листи безкоштовно (5 з кодом зі списку очікування). Далі ви купуєте баланс: 5 аналізів за 2,99 €, 15 за 6,99 €. Без підписки й без облікового запису." },
{ f: "Які листи застосунок пояснює?", a: "Листи з джобцентру, відомства у справах іноземців, пенсійного страхування, лікарняної каси, сімейної каси (Kindergeld), податкової або органу зі штрафів — словом, майже будь-який офіційний лист." },
{ f: "Що відбувається з моїми даними?", a: "Ваші листи залишаються зашифрованими на вашому пристрої. Для аналізу фото один раз безпечно надсилається до ШІ і видаляється там щонайпізніше через 30 днів. Обліковий запис не потрібен." },
{ f: "Що відбувається з моїми даними?", a: "Ваші листи залишаються зашифрованими на вашому пристрої. Для аналізу фото один раз безпечно надсилається до ШІ і видаляється там зазвичай через 30 днів. Обліковий запис не потрібен." },
],
locale: 'uk_UA', navLabel: 'Оберіть мову',
titel: 'BehördenKlar – зрозуміти лист із німецької установи',
@@ -236,7 +236,7 @@ TEXTE.pl = {
faq: [
{ f: "Ile to kosztuje?", a: "Pierwsze 3 pisma za darmo (5 z kodem z listy oczekujących). Potem kupujesz doładowanie: 5 analiz za 2,99 €, 15 za 6,99 €. Bez abonamentu i bez konta." },
{ f: "Jakie pisma aplikacja wyjaśnia?", a: "Pisma z Jobcenter, urzędu do spraw cudzoziemców, ubezpieczenia emerytalnego, kasy chorych, kasy rodzinnej (Kindergeld), urzędu skarbowego lub organu mandatowego — krótko mówiąc prawie każde oficjalne pismo." },
{ f: "Co dzieje się z moimi danymi?", a: "Twoje pisma pozostają zaszyfrowane na Twoim urządzeniu. Do analizy zdjęcie jest raz bezpiecznie wysyłane do SI i usuwane tam najpóźniej po 30 dniach. Konto nie jest potrzebne." },
{ f: "Co dzieje się z moimi danymi?", a: "Twoje pisma pozostają zaszyfrowane na Twoim urządzeniu. Do analizy zdjęcie jest raz bezpiecznie wysyłane do SI i usuwane tam zwykle po 30 dniach. Konto nie jest potrzebne." },
],
locale: 'pl_PL', navLabel: 'Wybierz język',
titel: 'BehördenKlar – zrozum pismo z niemieckiego urzędu',
@@ -280,7 +280,7 @@ TEXTE.ro = {
faq: [
{ f: "Cât costă?", a: "Primele 3 scrisori sunt gratuite (5 cu codul din lista de așteptare). Apoi cumperi credit: 5 analize la 2,99 €, 15 la 6,99 €. Fără abonament și fără cont." },
{ f: "Ce scrisori poate explica aplicația?", a: "Scrisori de la Jobcenter, Oficiul pentru Străini, asigurarea de pensie, casa de sănătate, casa familială (Kindergeld), administrația fiscală sau oficiul de amenzi — pe scurt, aproape orice scrisoare oficială." },
{ f: "Ce se întâmplă cu datele mele?", a: "Scrisorile tale rămân criptate pe dispozitivul tău. Pentru analiză, fotografia este trimisă o singură dată în siguranță către IA și este ștearsă acolo în cel mult 30 de zile. Nu ai nevoie de cont." },
{ f: "Ce se întâmplă cu datele mele?", a: "Scrisorile tale rămân criptate pe dispozitivul tău. Pentru analiză, fotografia este trimisă o singură dată în siguranță către IA și este ștearsă acolo de regulă după 30 de zile. Nu ai nevoie de cont." },
],
locale: 'ro_RO', navLabel: 'Alege limba',
titel: 'BehördenKlar – înțelege scrisoarea de la autoritățile germane',
@@ -324,7 +324,7 @@ TEXTE.en = {
faq: [
{ f: "What does it cost?", a: "Your first 3 letters are free (5 with the waiting-list code). After that you buy credit: 5 analyses for €2.99 or 15 for €6.99. No subscription, no account." },
{ f: "Which letters can the app explain?", a: "Letters from the Jobcenter, the immigration office, pension insurance, health insurance, the family benefits office (Kindergeld), the tax office or the fines authority — in short, almost any official letter." },
{ f: "What happens to my data?", a: "Your letters stay encrypted on your own device. For the analysis the photo is sent once, securely, to the AI and is deleted there after 30 days at the latest. You do not need an account." },
{ f: "What happens to my data?", a: "Your letters stay encrypted on your own device. For the analysis the photo is sent once, securely, to the AI and is usually deleted there after 30 days. You do not need an account." },
],
locale: 'en_US', navLabel: 'Choose language',
titel: 'BehördenKlar – understand letters from German authorities',
@@ -368,7 +368,7 @@ TEXTE.fa = {
faq: [
{ f: "هزینه آن چقدر است؟", a: "سه نامه اول رایگان است (با کد فهرست انتظار ۵ نامه). پس از آن اعتبار می‌خرید: ۵ تحلیل با ۲٫۹۹ یورو یا ۱۵ تحلیل با ۶٫۹۹ یورو. بدون اشتراک و بدون حساب کاربری." },
{ f: "چه نامه‌هایی را می‌تواند توضیح دهد؟", a: "نامه‌های مرکز کاریابی (Jobcenter)، اداره اتباع خارجی، بیمه بازنشستگی، بیمه درمانی، صندوق خانواده (Kindergeld)، اداره مالیات یا اداره جرایم — به‌طور خلاصه تقریباً هر نامه رسمی." },
{ f: "با اطلاعات من چه می‌شود؟", a: "نامه‌های شما رمزگذاری‌شده روی دستگاه خودتان می‌مانند. برای تحلیل، عکس یک بار به‌صورت امن به هوش مصنوعی فرستاده می‌شود و حداکثر پس از ۳۰ روز آنجا پاک می‌شود. به حساب کاربری نیاز ندارید." },
{ f: "با اطلاعات من چه می‌شود؟", a: "نامه‌های شما رمزگذاری‌شده روی دستگاه خودتان می‌مانند. برای تحلیل، عکس یک بار به‌صورت امن به هوش مصنوعی فرستاده می‌شود و معمولاً پس از ۳۰ روز آنجا پاک می‌شود. به حساب کاربری نیاز ندارید." },
],
locale: 'fa_IR', navLabel: 'زبان را انتخاب کنید',
titel: 'BehördenKlar – نامه‌های اداری آلمان را بفهمید',
@@ -412,7 +412,7 @@ TEXTE.ku = {
faq: [
{ f: "Ew çiqas dike?", a: "Sê nameyên pêşîn belaş in (bi koda lîsteya bendewariyê 5). Piştre tu krediyê dikirî: 5 analîz bi 2,99 €, 15 bi 6,99 €. Bê abone, bê hesab." },
{ f: "Kîjan name dikare rave bike?", a: "Nameyên ji Jobcenter, Daîreya Biyaniyan, sîgorteya teqawidiyê, sîgorteya tenduristiyê, sindoqa malbatê (Kindergeld), daîreya bacê an daîreya cezayan — bi kurtî hema hema her nameya fermî." },
{ f: "Bi daneyên min çi tê kirin?", a: "Nameyên te bi şîfre li ser amûra te dimînin. Ji bo analîzê wêne carekê bi ewlehî ji AI re tê şandin û li wir herî dereng piştî 30 rojan tê jêbirin. Tu hewceyî hesabekî nînî." },
{ f: "Bi daneyên min çi tê kirin?", a: "Nameyên te bi şîfre li ser amûra te dimînin. Ji bo analîzê wêne carekê bi ewlehî ji AI re tê şandin û li wir bi gelemperî piştî 30 rojan tê jêbirin. Tu hewceyî hesabekî nînî." },
],
locale: 'ku_TR', navLabel: 'Zimên hilbijêre',
titel: 'BehördenKlar – nameyên fermî yên Almanyayê fêm bike',
@@ -456,7 +456,7 @@ TEXTE.bg = {
faq: [
{ f: "Колко струва?", a: "Първите 3 писма са безплатни (5 с кода от списъка на чакащите). След това купувате кредит: 5 анализа за 2,99 €, 15 за 6,99 €. Без абонамент и без регистрация." },
{ f: "Кои писма може да обясни приложението?", a: "Писма от Jobcenter, службата за чужденци, пенсионното осигуряване, здравната каса, семейната каса (Kindergeld), данъчната служба или службата за глоби — накратко почти всяко официално писмо." },
{ f: "Какво става с данните ми?", a: "Писмата ви остават криптирани на вашето устройство. За анализа снимката се изпраща еднократно и защитено към ИИ и се изтрива там най-късно след 30 дни. Не ви трябва регистрация." },
{ f: "Какво става с данните ми?", a: "Писмата ви остават криптирани на вашето устройство. За анализа снимката се изпраща еднократно и защитено към ИИ и се изтрива там обикновено след 30 дни. Не ви трябва регистрация." },
],
locale: 'bg_BG', navLabel: 'Изберете език',
titel: 'BehördenKlar – разберете писмото от немска институция',
@@ -500,7 +500,7 @@ TEXTE.hr = {
faq: [
{ f: "Koliko košta?", a: "Prva 3 pisma su besplatna (5 s kodom s liste čekanja). Zatim kupujete bodove: 5 analiza za 2,99 €, 15 za 6,99 €. Bez pretplate i bez računa." },
{ f: "Koja pisma aplikacija objašnjava?", a: "Pisma Jobcentra, Ureda za strance, mirovinskog osiguranja, zdravstvenog osiguranja, obiteljske blagajne (Kindergeld), porezne uprave ili ureda za kazne — ukratko, gotovo svako službeno pismo." },
{ f: "Što se događa s mojim podacima?", a: "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje umjetnoj inteligenciji i ondje se briše najkasnije nakon 30 dana. Račun nije potreban." },
{ f: "Što se događa s mojim podacima?", a: "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje umjetnoj inteligenciji i ondje se u pravilu briše nakon 30 dana. Račun nije potreban." },
],
locale: 'hr_HR', navLabel: 'Odaberite jezik',
titel: 'BehördenKlar – razumite pismo njemačke ustanove',
@@ -544,7 +544,7 @@ TEXTE.sr = {
faq: [
{ f: "Колико кошта?", a: "Прва 3 писма су бесплатна (5 са кодом са листе чекања). Затим купујете бодове: 5 анализа за 2,99 €, 15 за 6,99 €. Без претплате и без налога." },
{ f: "Која писма апликација објашњава?", a: "Писма Јобцентра, Управе за странце, пензијског осигурања, здравственог осигурања, породичне благајне (Kindergeld), пореске управе или службе за казне — укратко, готово свако службено писмо." },
{ f: "Шта се дешава са мојим подацима?", a: "Ваша писма остају шифрована на вашем уређају. За анализу се фотографија једном безбедно шаље вештачкој интелигенцији и тамо се брише најкасније после 30 дана. Налог није потребан." },
{ f: "Шта се дешава са мојим подацима?", a: "Ваша писма остају шифрована на вашем уређају. За анализу се фотографија једном безбедно шаље вештачкој интелигенцији и тамо се по правилу брише после 30 дана. Налог није потребан." },
],
locale: 'sr_RS', navLabel: 'Изаберите језик',
titel: 'BehördenKlar – разумејте писмо немачке установе',
@@ -588,7 +588,7 @@ TEXTE.bs = {
faq: [
{ f: "Koliko košta?", a: "Prva 3 pisma su besplatna (5 s kodom s liste čekanja). Zatim kupujete bodove: 5 analiza za 2,99 €, 15 za 6,99 €. Bez pretplate i bez računa." },
{ f: "Koja pisma aplikacija objašnjava?", a: "Pisma Jobcentra, Ureda za strance, penzionog osiguranja, zdravstvenog osiguranja, porodične blagajne (Kindergeld), poreske uprave ili ureda za kazne — ukratko, gotovo svako službeno pismo." },
{ f: "Šta se dešava s mojim podacima?", a: "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje vještačkoj inteligenciji i tamo se briše najkasnije nakon 30 dana. Račun nije potreban." },
{ f: "Šta se dešava s mojim podacima?", a: "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje vještačkoj inteligenciji i tamo se u pravilu briše nakon 30 dana. Račun nije potreban." },
],
locale: 'bs_BA', navLabel: 'Odaberite jezik',
titel: 'BehördenKlar – razumijte pismo njemačke ustanove',
@@ -632,7 +632,7 @@ TEXTE.sq = {
faq: [
{ f: "Sa kushton?", a: "3 letrat e para janë falas (5 me kodin e listës së pritjes). Më pas blini kredi: 5 analiza për 2,99 €, 15 për 6,99 €. Pa abonim dhe pa llogari." },
{ f: "Cilat letra mund të shpjegojë?", a: "Letra nga Jobcenter, Zyra e të Huajve, sigurimi i pensionit, sigurimi shëndetësor, arka familjare (Kindergeld), zyra e tatimeve ose zyra e gjobave — shkurt, pothuajse çdo letër zyrtare." },
{ f: "Çfarë ndodh me të dhënat e mia?", a: "Letrat tuaja mbeten të koduara në pajisjen tuaj. Për analizën, fotoja dërgohet një herë në mënyrë të sigurt te inteligjenca artificiale dhe fshihet atje më së voni pas 30 ditësh. Nuk ju duhet llogari." },
{ f: "Çfarë ndodh me të dhënat e mia?", a: "Letrat tuaja mbeten të koduara në pajisjen tuaj. Për analizën, fotoja dërgohet një herë në mënyrë të sigurt te inteligjenca artificiale dhe fshihet atje zakonisht pas 30 ditësh. Nuk ju duhet llogari." },
],
locale: 'sq_AL', navLabel: 'Zgjidhni gjuhën',
titel: 'BehördenKlar – kuptoni letrën e një institucioni gjerman',
@@ -676,7 +676,7 @@ TEXTE.it = {
faq: [
{ f: "Quanto costa?", a: "Le prime 3 lettere sono gratis (5 con il codice della lista d’attesa). Poi acquisti credito: 5 analisi a 2,99 € o 15 a 6,99 €. Nessun abbonamento, nessun account." },
{ f: "Quali lettere può spiegare?", a: "Lettere del Jobcenter, dell’Ufficio stranieri, dell’assicurazione pensionistica, della cassa malattia, della cassa familiare (Kindergeld), dell’ufficio delle imposte o dell’ufficio sanzioni — in breve, quasi ogni lettera ufficiale." },
{ f: "Che fine fanno i miei dati?", a: "Le tue lettere restano cifrate sul tuo dispositivo. Per l’analisi la foto viene inviata una sola volta in modo sicuro all’IA e viene cancellata lì al più tardi dopo 30 giorni. Non serve un account." },
{ f: "Che fine fanno i miei dati?", a: "Le tue lettere restano cifrate sul tuo dispositivo. Per l’analisi la foto viene inviata una sola volta in modo sicuro all’IA e viene cancellata lì di norma dopo 30 giorni. Non serve un account." },
],
locale: 'it_IT', navLabel: 'Scegli la lingua',
titel: 'BehördenKlar – capire le lettere degli uffici tedeschi',
@@ -720,7 +720,7 @@ TEXTE.el = {
faq: [
{ f: "Πόσο κοστίζει;", a: "Τα πρώτα 3 γράμματα είναι δωρεάν (5 με τον κωδικό της λίστας αναμονής). Έπειτα αγοράζετε μονάδες: 5 αναλύσεις 2,99 € ή 15 για 6,99 €. Χωρίς συνδρομή, χωρίς λογαριασμό." },
{ f: "Ποια γράμματα μπορεί να εξηγήσει;", a: "Γράμματα από το Jobcenter, την Υπηρεσία Αλλοδαπών, την ασφάλιση σύνταξης, το ταμείο υγείας, το οικογενειακό ταμείο (Kindergeld), την εφορία ή την υπηρεσία προστίμων — με λίγα λόγια, σχεδόν κάθε επίσημο γράμμα." },
{ f: "Τι γίνεται με τα δεδομένα μου;", a: "Τα γράμματά σας παραμένουν κρυπτογραφημένα στη συσκευή σας. Για την ανάλυση η φωτογραφία στέλνεται μία φορά με ασφάλεια στην ΤΝ και διαγράφεται εκεί το αργότερο μετά από 30 ημέρες. Δεν χρειάζεστε λογαριασμό." },
{ f: "Τι γίνεται με τα δεδομένα μου;", a: "Τα γράμματά σας παραμένουν κρυπτογραφημένα στη συσκευή σας. Για την ανάλυση η φωτογραφία στέλνεται μία φορά με ασφάλεια στην ΤΝ και διαγράφεται εκεί συνήθως μετά από 30 ημέρες. Δεν χρειάζεστε λογαριασμό." },
],
locale: 'el_GR', navLabel: 'Επιλέξτε γλώσσα',
titel: 'BehördenKlar – κατανοήστε το γράμμα γερμανικής υπηρεσίας',
@@ -764,7 +764,7 @@ TEXTE.es = {
faq: [
{ f: "¿Cuánto cuesta?", a: "Las primeras 3 cartas son gratis (5 con el código de la lista de espera). Después compras saldo: 5 análisis por 2,99 € o 15 por 6,99 €. Sin suscripción y sin cuenta." },
{ f: "¿Qué cartas puede explicar?", a: "Cartas del Jobcenter, de la Oficina de Extranjería, del seguro de pensiones, de la mutua de salud, de la caja familiar (Kindergeld), de Hacienda o de la oficina de multas — en resumen, casi cualquier carta oficial." },
{ f: "¿Qué pasa con mis datos?", a: "Tus cartas permanecen cifradas en tu propio dispositivo. Para el análisis, la foto se envía una sola vez de forma segura a la IA y allí se borra como máximo a los 30 días. No necesitas cuenta." },
{ f: "¿Qué pasa con mis datos?", a: "Tus cartas permanecen cifradas en tu propio dispositivo. Para el análisis, la foto se envía una sola vez de forma segura a la IA y allí se borra normalmente a los 30 días. No necesitas cuenta." },
],
locale: 'es_ES', navLabel: 'Elige idioma',
titel: 'BehördenKlar – entiende las cartas de las oficinas alemanas',
@@ -808,7 +808,7 @@ TEXTE.pt = {
faq: [
{ f: "Quanto custa?", a: "As primeiras 3 cartas são grátis (5 com o código da lista de espera). Depois compra saldo: 5 análises por 2,99 € ou 15 por 6,99 €. Sem assinatura e sem conta." },
{ f: "Que cartas consegue explicar?", a: "Cartas do Jobcenter, do Serviço de Estrangeiros, do seguro de pensões, da caixa de saúde, da caixa familiar (Kindergeld), das finanças ou do serviço de multas — em resumo, quase qualquer carta oficial." },
{ f: "O que acontece aos meus dados?", a: "As suas cartas ficam cifradas no seu próprio dispositivo. Para a análise, a foto é enviada uma única vez de forma segura para a IA e é apagada lá no máximo ao fim de 30 dias. Não precisa de conta." },
{ f: "O que acontece aos meus dados?", a: "As suas cartas ficam cifradas no seu próprio dispositivo. Para a análise, a foto é enviada uma única vez de forma segura para a IA e é apagada lá normalmente ao fim de 30 dias. Não precisa de conta." },
],
locale: 'pt_PT', navLabel: 'Escolha o idioma',
titel: 'BehördenKlar – perceber as cartas das repartições alemãs',
@@ -852,7 +852,7 @@ TEXTE.fr = {
faq: [
{ f: "Combien ça coûte ?", a: "Les 3 premiers courriers sont gratuits (5 avec le code de la liste d’attente). Ensuite vous achetez du crédit : 5 analyses pour 2,99 € ou 15 pour 6,99 €. Sans abonnement et sans compte." },
{ f: "Quels courriers peut-elle expliquer ?", a: "Les courriers du Jobcenter, du service des étrangers, de l’assurance retraite, de la caisse maladie, de la caisse familiale (Kindergeld), du fisc ou du service des amendes — bref, presque tout courrier officiel." },
{ f: "Que deviennent mes données ?", a: "Vos courriers restent chiffrés sur votre appareil. Pour l’analyse, la photo est envoyée une seule fois de façon sécurisée à l’IA et y est supprimée au plus tard au bout de 30 jours. Aucun compte nécessaire." },
{ f: "Que deviennent mes données ?", a: "Vos courriers restent chiffrés sur votre appareil. Pour l’analyse, la photo est envoyée une seule fois de façon sécurisée à l’IA et y est supprimée en général au bout de 30 jours. Aucun compte nécessaire." },
],
locale: 'fr_FR', navLabel: 'Choisir la langue',
titel: 'BehördenKlar – comprendre les courriers administratifs allemands',
@@ -896,7 +896,7 @@ TEXTE.hu = {
faq: [
{ f: "Mennyibe kerül?", a: "Az első 3 levél ingyenes (a várólista kódjával 5). Utána egyenleget vásárol: 5 elemzés 2,99 €, 15 elemzés 6,99 €. Előfizetés és fiók nélkül." },
{ f: "Milyen leveleket tud elmagyarázni?", a: "A Jobcenter, az idegenrendészet, a nyugdíjbiztosító, a betegpénztár, a családi pénztár (Kindergeld), az adóhivatal vagy a bírságoló hatóság leveleit — röviden szinte minden hivatalos levelet." },
{ f: "Mi történik az adataimmal?", a: "A levelei titkosítva a saját készülékén maradnak. Az elemzéshez a fénykép egyszer, biztonságosan kerül el az MI-hez, és ott legkésőbb 30 nap múlva törlődik. Nincs szükség fiókra." },
{ f: "Mi történik az adataimmal?", a: "A levelei titkosítva a saját készülékén maradnak. Az elemzéshez a fénykép egyszer, biztonságosan kerül el az MI-hez, és ott általában 30 nap múlva törlődik. Nincs szükség fiókra." },
],
locale: 'hu_HU', navLabel: 'Válasszon nyelvet',
titel: 'BehördenKlar – értse meg a német hivatali levelet',
@@ -940,7 +940,7 @@ TEXTE.vi = {
faq: [
{ f: "Giá bao nhiêu?", a: "3 lá thư đầu tiên miễn phí (5 nếu có mã từ danh sách chờ). Sau đó bạn mua lượt: 5 lần phân tích 2,99 €, 15 lần 6,99 €. Không thuê bao, không cần tài khoản." },
{ f: "Ứng dụng giải thích được những thư nào?", a: "Thư từ Jobcenter, Sở Ngoại kiều, bảo hiểm hưu trí, bảo hiểm y tế, quỹ gia đình (Kindergeld), sở thuế hoặc cơ quan xử phạt — nói ngắn gọn là hầu hết mọi thư chính thức." },
{ f: "Dữ liệu của tôi thì sao?", a: "Thư của bạn được mã hóa và nằm trên thiết bị của bạn. Để phân tích, ảnh được gửi một lần duy nhất và an toàn tới AI, rồi bị xóa ở đó chậm nhất sau 30 ngày. Bạn không cần tài khoản." },
{ f: "Dữ liệu của tôi thì sao?", a: "Thư của bạn được mã hóa và nằm trên thiết bị của bạn. Để phân tích, ảnh được gửi một lần duy nhất và an toàn tới AI, rồi thường bị xóa ở đó sau 30 ngày. Bạn không cần tài khoản." },
],
locale: 'vi_VN', navLabel: 'Chọn ngôn ngữ',
titel: 'BehördenKlar – hiểu thư từ cơ quan công quyền Đức',
@@ -984,7 +984,7 @@ TEXTE.hi = {
faq: [
{ f: "इसकी कीमत क्या है?", a: "पहली 3 चिट्ठियाँ मुफ़्त हैं (प्रतीक्षा सूची के कोड से 5)। उसके बाद आप क्रेडिट खरीदते हैं: 5 विश्लेषण 2,99 € में या 15 विश्लेषण 6,99 € में। कोई सदस्यता नहीं, कोई खाता नहीं।" },
{ f: "यह कौन-सी चिट्ठियाँ समझा सकता है?", a: "जॉबसेंटर, विदेशी विभाग, पेंशन बीमा, स्वास्थ्य बीमा, परिवार कोष (Kindergeld), कर विभाग या जुर्माना विभाग की चिट्ठियाँ — संक्षेप में, लगभग हर सरकारी चिट्ठी।" },
{ f: "मेरे डेटा का क्या होता है?", a: "आपकी चिट्ठियाँ आपके अपने उपकरण पर एन्क्रिप्टेड रहती हैं। विश्लेषण के लिए फ़ोटो एक बार सुरक्षित रूप से एआई को भेजी जाती है और वहाँ अधिकतम 30 दिनों में मिटा दी जाती है। आपको खाते की ज़रूरत नहीं।" },
{ f: "मेरे डेटा का क्या होता है?", a: "आपकी चिट्ठियाँ आपके अपने उपकरण पर एन्क्रिप्टेड रहती हैं। विश्लेषण के लिए फ़ोटो एक बार सुरक्षित रूप से एआई को भेजी जाती है और वहाँ आमतौर पर 30 दिन बाद मिटा दी जाती है। आपको खाते की ज़रूरत नहीं।" },
],
locale: 'hi_IN', navLabel: 'भाषा चुनें',
titel: 'BehördenKlar – जर्मन सरकारी चिट्ठी समझें',
@@ -1028,7 +1028,7 @@ TEXTE.ur = {
faq: [
{ f: "اس کی قیمت کیا ہے؟", a: "پہلے 3 خط مفت ہیں (انتظار کی فہرست کے کوڈ کے ساتھ 5)۔ اس کے بعد آپ کریڈٹ خریدتے ہیں: 5 تجزیے 2,99 € میں یا 15 تجزیے 6,99 € میں۔ کوئی سبسکرپشن نہیں، کوئی اکاؤنٹ نہیں۔" },
{ f: "یہ کون سے خط سمجھا سکتا ہے؟", a: "جاب سینٹر، غیر ملکی دفتر، پنشن انشورنس، ہیلتھ انشورنس، فیملی فنڈ (Kindergeld)، ٹیکس دفتر یا جرمانہ دفتر کے خطوط — مختصراً تقریباً ہر سرکاری خط۔" },
{ f: "میرے ڈیٹا کا کیا ہوتا ہے؟", a: "آپ کے خطوط آپ کے اپنے آلے پر خفیہ شکل میں رہتے ہیں۔ تجزیے کے لیے تصویر ایک بار محفوظ طریقے سے اے آئی کو بھیجی جاتی ہے اور وہاں زیادہ سے زیادہ 30 دن میں مٹا دی جاتی ہے۔ آپ کو اکاؤنٹ کی ضرورت نہیں۔" },
{ f: "میرے ڈیٹا کا کیا ہوتا ہے؟", a: "آپ کے خطوط آپ کے اپنے آلے پر خفیہ شکل میں رہتے ہیں۔ تجزیے کے لیے تصویر ایک بار محفوظ طریقے سے اے آئی کو بھیجی جاتی ہے اور وہاں عام طور پر 30 دن بعد مٹا دی جاتی ہے۔ آپ کو اکاؤنٹ کی ضرورت نہیں۔" },
],
locale: 'ur_PK', navLabel: 'زبان منتخب کریں',
titel: 'BehördenKlar – جرمن سرکاری خط سمجھیں',
@@ -1072,7 +1072,7 @@ TEXTE.ps = {
faq: [
{ f: "بیه یې څومره ده؟", a: "لومړي ۳ لیکونه وړیا دي (د انتظار لړلیک کوډ سره ۵). وروسته تاسو کریډیټ اخلئ: ۵ شننې په ۲٫۹۹ یورو یا ۱۵ په ۶٫۹۹ یورو. نه ګډون، نه حساب." },
{ f: "کوم لیکونه رابرسېره کولی شي؟", a: "د جاب سنټر، د بهرنیانو ادارې، د تقاعد بیمې، روغتیايي بیمې، کورنۍ صندوق (Kindergeld)، مالیې ادارې یا د جریمو ادارې لیکونه — په لنډه توګه نږدې هر رسمي لیک." },
{ f: "زما معلوماتو ته څه کیږي؟", a: "ستاسو لیکونه پټ شوي ستاسو په خپله وسیله پاتې کیږي. د شننې لپاره عکس یو ځل په خوندي ډول مصنوعي ځیرکتیا ته لیږل کیږي او هلته تر ۳۰ ورځو وروسته ړنګېږي. حساب ته اړتیا نشته." },
{ f: "زما معلوماتو ته څه کیږي؟", a: "ستاسو لیکونه پټ شوي ستاسو په خپله وسیله پاتې کیږي. د شننې لپاره عکس یو ځل په خوندي ډول مصنوعي ځیرکتیا ته لیږل کیږي او هلته معمولاً تر ۳۰ ورځو وروسته ړنګېږي. حساب ته اړتیا نشته." },
],
locale: 'ps_AF', navLabel: 'ژبه وټاکئ',
titel: 'BehördenKlar – د آلمان رسمي لیکونه پوه شئ',
@@ -1116,7 +1116,7 @@ TEXTE.ti = {
faq: [
{ f: "ክንደይ ይኸፍል?", a: "እቶም ቀዳሞት 3 ደብዳበታት ብነጻ እዮም (ብናይ ምጽባይ ዝርዝር ኮድ 5)። ድሕሪኡ ክሬዲት ትገዝእ፦ 5 መጽናዕቲ ብ2,99 € ወይ 15 ብ6,99 €። ዝኾነ ምዝገባ ወይ ኣካውንት ኣየድልን።" },
{ f: "ኣየኖት ደብዳበታት ከብርህ ይኽእል?", a: "ካብ Jobcenter፣ ቤት ጽሕፈት ወጻእተኛታት፣ ውሕስነት ጡረታ፣ ውሕስነት ጥዕና፣ ስድራቤታዊ ካሳ (Kindergeld)፣ ቤት ጽሕፈት ግብሪ ወይ ቤት ጽሕፈት መቕጻዕቲ ዝመጹ ደብዳበታት — ብሓጺሩ ኩሉ ወግዓዊ ደብዳበ ማለት እዩ።" },
{ f: "ብዛዕባ ዳታይ እንታይ ይኸውን?", a: "ደብዳበታትካ ኣብ ናትካ መሳርሒ ተመስጢሮም ይጸንሑ። ንመጽናዕቲ እቲ ስእሊ ሓንሳብ ብውሑስ መንገዲ ናብ AI ይለኣኽ፣ ኣብኡ ድማ ዝበዝሐ ድሕሪ 30 መዓልቲ ይድምሰስ። ኣካውንት ኣየድልየካን።" },
{ f: "ብዛዕባ ዳታይ እንታይ ይኸውን?", a: "ደብዳበታትካ ኣብ ናትካ መሳርሒ ተመስጢሮም ይጸንሑ። ንመጽናዕቲ እቲ ስእሊ ሓንሳብ ብውሑስ መንገዲ ናብ AI ይለኣኽ፣ ኣብኡ ድማ ብልሙድ ድሕሪ 30 መዓልቲ ይድምሰስ። ኣካውንት ኣየድልየካን።" },
],
locale: 'ti_ER', navLabel: 'ቋንቋ ምረጽ',
titel: 'BehördenKlar – ናይ ጀርመን ወግዓዊ ደብዳበ ተረዳእ',
@@ -1160,7 +1160,7 @@ TEXTE.so = {
faq: [
{ f: "Intee ayey ku kacaysaa?", a: "3-da warqadood ee ugu horreeya waa bilaash (5 haddii aad haysato koodhka liiska sugitaanka). Kadib waxaad iibsanaysaa rasiid: 5 falanqayn 2,99 € ama 15 falanqayn 6,99 €. Rukun ma jiro, koonto looma baahna." },
{ f: "Warqadaha noocee ah ayuu sharxi karaa?", a: "Warqadaha ka yimid Jobcenter, Xafiiska Ajaanibta, caymiska hawlgabka, caymiska caafimaadka, sanduuqa qoyska (Kindergeld), xafiiska canshuuraha ama xafiiska ganaaxyada — kooban: ku dhawaad warqad kasta oo rasmi ah." },
{ f: "Maxaa ku dhacaya xogtayda?", a: "Warqadahaagu waxay ku hadhayaan qalabkaaga iyagoo sir ah. Falanqaynta awgeed sawirka hal mar ayaa si ammaan ah loogu diraa AI-ga, halkaasna waxaa laga tirtiraa ugu badnaan 30 maalmood gudahood. Koonto uma baahnid." },
{ f: "Maxaa ku dhacaya xogtayda?", a: "Warqadahaagu waxay ku hadhayaan qalabkaaga iyagoo sir ah. Falanqaynta awgeed sawirka hal mar ayaa si ammaan ah loogu diraa AI-ga, halkaasna waxaa laga tirtiraa inta badan 30 maalmood kadib. Koonto uma baahnid." },
],
locale: 'so_SO', navLabel: 'Dooro luqadda',
titel: 'BehördenKlar – faham warqadda hay’adaha Jarmalka',
@@ -1204,7 +1204,7 @@ TEXTE.zh = {
faq: [
{ f: "收费多少?", a: "前 3 封信免费(用等候名单的兑换码则是 5 封)。之后购买额度:5 次分析 2,99 €,15 次 6,99 €。无订阅,无需注册账号。" },
{ f: "能解释哪些信件?", a: "来自劳动介绍所(Jobcenter)、外管局、养老保险、医疗保险、家庭金库(Kindergeld)、税务局或罚款机关的信件——简而言之,几乎任何官方信件。" },
{ f: "我的数据会怎样?", a: "您的信件以加密方式保存在您自己的设备上。分析时照片会安全地发送给人工智能一次,并最迟在 30 天后在那里删除。您不需要账号。" },
{ f: "我的数据会怎样?", a: "您的信件以加密方式保存在您自己的设备上。分析时照片会安全地发送给人工智能一次,并通常在 30 天后在那里删除。您不需要账号。" },
],
locale: 'zh_CN', navLabel: '选择语言',
titel: 'BehördenKlar – 看懂德国政府部门的信件',
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "ماذا يحدث لبياناتي؟",
"acceptedAnswer": {
"@type": "Answer",
"text": "تبقى رسائلك مشفّرة على جهازك. للتحليل تُرسل الصورة مرة واحدة بشكل آمن إلى الذكاء الاصطناعي وتُحذف هناك خلال 30 يوماً كحد أقصى. لا تحتاج إلى حساب."
"text": "تبقى رسائلك مشفّرة على جهازك. للتحليل تُرسل الصورة مرة واحدة بشكل آمن إلى الذكاء الاصطناعي وتُحذف هناك عادةً بعد 30 يوماً. لا تحتاج إلى حساب."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>ماذا يحدث لبياناتي؟</summary>
<p>تبقى رسائلك مشفّرة على جهازك. للتحليل تُرسل الصورة مرة واحدة بشكل آمن إلى الذكاء الاصطناعي وتُحذف هناك خلال 30 يوماً كحد أقصى. لا تحتاج إلى حساب.</p>
<p>تبقى رسائلك مشفّرة على جهازك. للتحليل تُرسل الصورة مرة واحدة بشكل آمن إلى الذكاء الاصطناعي وتُحذف هناك عادةً بعد 30 يوماً. لا تحتاج إلى حساب.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Какво става с данните ми?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Писмата ви остават криптирани на вашето устройство. За анализа снимката се изпраща еднократно и защитено към ИИ и се изтрива там най-късно след 30 дни. Не ви трябва регистрация."
"text": "Писмата ви остават криптирани на вашето устройство. За анализа снимката се изпраща еднократно и защитено към ИИ и се изтрива там обикновено след 30 дни. Не ви трябва регистрация."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Какво става с данните ми?</summary>
<p>Писмата ви остават криптирани на вашето устройство. За анализа снимката се изпраща еднократно и защитено към ИИ и се изтрива там най-късно след 30 дни. Не ви трябва регистрация.</p>
<p>Писмата ви остават криптирани на вашето устройство. За анализа снимката се изпраща еднократно и защитено към ИИ и се изтрива там обикновено след 30 дни. Не ви трябва регистрация.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Šta se dešava s mojim podacima?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje vještačkoj inteligenciji i tamo se briše najkasnije nakon 30 dana. Račun nije potreban."
"text": "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje vještačkoj inteligenciji i tamo se u pravilu briše nakon 30 dana. Račun nije potreban."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Šta se dešava s mojim podacima?</summary>
<p>Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje vještačkoj inteligenciji i tamo se briše najkasnije nakon 30 dana. Račun nije potreban.</p>
<p>Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje vještačkoj inteligenciji i tamo se u pravilu briše nakon 30 dana. Račun nije potreban.</p>
</details>
</section>
+55 -8
View File
@@ -29,7 +29,7 @@
<body>
<div class="wrap">
<h1>Datenschutzerklärung</h1>
<p class="stand">für die App und Webseite „BehördenKlar" — Stand: 18.08.2026</p>
<p class="stand">für die App und Webseite „BehördenKlar" — Stand: 22.10.2026</p>
<div class="karte">
<h2>1. Verantwortlicher</h2>
@@ -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>
@@ -215,6 +217,51 @@
für die gesetzlichen Fristen auf (Art. 6 Abs. 1 lit. c DSGVO).
</p>
<h2>9a. Zugang über eine Beratungsstelle (Einrichtungs-Code)</h2>
<p>
Beratungsstellen können ihren Ratsuchenden einen Code geben, mit dem
BehördenKlar für sie kostenlos ist. Der Code verweist auf ein
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 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>
<p>
Sie können ein Ergebnis per QR-Code von einem Gerät auf ein anderes
bringen, zum Beispiel vom Computer der Beratungsstelle auf Ihr Handy,
oder mit dem Handy fotografieren und das Ergebnis am Computer anzeigen.
Dabei gilt:
</p>
<ul>
<li>
<strong>Ende-zu-Ende-Verschlüsselung.</strong> Das Ergebnis wird auf
dem einen Gerät mit AES-256 verschlüsselt und erst auf dem anderen
Gerät wieder entschlüsselt. Den Schlüssel enthält nur der QR-Code
(im Teil der Adresse nach dem „#", den Browser nicht an Server
senden). <strong>Wir können den Inhalt nicht lesen.</strong>
</li>
<li>
<strong>Kurze Zwischenspeicherung.</strong> Das verschlüsselte
Ergebnis liegt bis zur Abholung auf unserem Server (Cloudflare),
höchstens 15 Minuten, und wird beim Abholen bzw. danach automatisch
gelöscht. Zum Schutz vor Missbrauch begrenzen wir die Zahl der
Übertragungen pro IP-Adresse (Art. 6 Abs. 1 lit. f DSGVO).
</li>
<li>
Wer den QR-Code fotografiert oder abfotografiert, kann das Ergebnis
innerhalb dieser 15 Minuten öffnen. Zeigen Sie den Code deshalb nur
der Person, für die er bestimmt ist.
</li>
</ul>
<h2>10. Reichweitenmessung (Webseite)</h2>
<p>
Auf dieser Webseite nutzen wir Cloudflare Web Analytics (Cloudflare,
+821 -91
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Τι γίνεται με τα δεδομένα μου;",
"acceptedAnswer": {
"@type": "Answer",
"text": "Τα γράμματά σας παραμένουν κρυπτογραφημένα στη συσκευή σας. Για την ανάλυση η φωτογραφία στέλνεται μία φορά με ασφάλεια στην ΤΝ και διαγράφεται εκεί το αργότερο μετά από 30 ημέρες. Δεν χρειάζεστε λογαριασμό."
"text": "Τα γράμματά σας παραμένουν κρυπτογραφημένα στη συσκευή σας. Για την ανάλυση η φωτογραφία στέλνεται μία φορά με ασφάλεια στην ΤΝ και διαγράφεται εκεί συνήθως μετά από 30 ημέρες. Δεν χρειάζεστε λογαριασμό."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Τι γίνεται με τα δεδομένα μου;</summary>
<p>Τα γράμματά σας παραμένουν κρυπτογραφημένα στη συσκευή σας. Για την ανάλυση η φωτογραφία στέλνεται μία φορά με ασφάλεια στην ΤΝ και διαγράφεται εκεί το αργότερο μετά από 30 ημέρες. Δεν χρειάζεστε λογαριασμό.</p>
<p>Τα γράμματά σας παραμένουν κρυπτογραφημένα στη συσκευή σας. Για την ανάλυση η φωτογραφία στέλνεται μία φορά με ασφάλεια στην ΤΝ και διαγράφεται εκεί συνήθως μετά από 30 ημέρες. Δεν χρειάζεστε λογαριασμό.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "What happens to my data?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Your letters stay encrypted on your own device. For the analysis the photo is sent once, securely, to the AI and is deleted there after 30 days at the latest. You do not need an account."
"text": "Your letters stay encrypted on your own device. For the analysis the photo is sent once, securely, to the AI and is usually deleted there after 30 days. You do not need an account."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>What happens to my data?</summary>
<p>Your letters stay encrypted on your own device. For the analysis the photo is sent once, securely, to the AI and is deleted there after 30 days at the latest. You do not need an account.</p>
<p>Your letters stay encrypted on your own device. For the analysis the photo is sent once, securely, to the AI and is usually deleted there after 30 days. You do not need an account.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "¿Qué pasa con mis datos?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Tus cartas permanecen cifradas en tu propio dispositivo. Para el análisis, la foto se envía una sola vez de forma segura a la IA y allí se borra como máximo a los 30 días. No necesitas cuenta."
"text": "Tus cartas permanecen cifradas en tu propio dispositivo. Para el análisis, la foto se envía una sola vez de forma segura a la IA y allí se borra normalmente a los 30 días. No necesitas cuenta."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>¿Qué pasa con mis datos?</summary>
<p>Tus cartas permanecen cifradas en tu propio dispositivo. Para el análisis, la foto se envía una sola vez de forma segura a la IA y allí se borra como máximo a los 30 días. No necesitas cuenta.</p>
<p>Tus cartas permanecen cifradas en tu propio dispositivo. Para el análisis, la foto se envía una sola vez de forma segura a la IA y allí se borra normalmente a los 30 días. No necesitas cuenta.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "با اطلاعات من چه می‌شود؟",
"acceptedAnswer": {
"@type": "Answer",
"text": "نامه‌های شما رمزگذاری‌شده روی دستگاه خودتان می‌مانند. برای تحلیل، عکس یک بار به‌صورت امن به هوش مصنوعی فرستاده می‌شود و حداکثر پس از ۳۰ روز آنجا پاک می‌شود. به حساب کاربری نیاز ندارید."
"text": "نامه‌های شما رمزگذاری‌شده روی دستگاه خودتان می‌مانند. برای تحلیل، عکس یک بار به‌صورت امن به هوش مصنوعی فرستاده می‌شود و معمولاً پس از ۳۰ روز آنجا پاک می‌شود. به حساب کاربری نیاز ندارید."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>با اطلاعات من چه می‌شود؟</summary>
<p>نامه‌های شما رمزگذاری‌شده روی دستگاه خودتان می‌مانند. برای تحلیل، عکس یک بار به‌صورت امن به هوش مصنوعی فرستاده می‌شود و حداکثر پس از ۳۰ روز آنجا پاک می‌شود. به حساب کاربری نیاز ندارید.</p>
<p>نامه‌های شما رمزگذاری‌شده روی دستگاه خودتان می‌مانند. برای تحلیل، عکس یک بار به‌صورت امن به هوش مصنوعی فرستاده می‌شود و معمولاً پس از ۳۰ روز آنجا پاک می‌شود. به حساب کاربری نیاز ندارید.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Que deviennent mes données ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Vos courriers restent chiffrés sur votre appareil. Pour l’analyse, la photo est envoyée une seule fois de façon sécurisée à l’IA et y est supprimée au plus tard au bout de 30 jours. Aucun compte nécessaire."
"text": "Vos courriers restent chiffrés sur votre appareil. Pour l’analyse, la photo est envoyée une seule fois de façon sécurisée à l’IA et y est supprimée en général au bout de 30 jours. Aucun compte nécessaire."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Que deviennent mes données ?</summary>
<p>Vos courriers restent chiffrés sur votre appareil. Pour l’analyse, la photo est envoyée une seule fois de façon sécurisée à l’IA et y est supprimée au plus tard au bout de 30 jours. Aucun compte nécessaire.</p>
<p>Vos courriers restent chiffrés sur votre appareil. Pour l’analyse, la photo est envoyée une seule fois de façon sécurisée à l’IA et y est supprimée en général au bout de 30 jours. Aucun compte nécessaire.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "मेरे डेटा का क्या होता है?",
"acceptedAnswer": {
"@type": "Answer",
"text": "आपकी चिट्ठियाँ आपके अपने उपकरण पर एन्क्रिप्टेड रहती हैं। विश्लेषण के लिए फ़ोटो एक बार सुरक्षित रूप से एआई को भेजी जाती है और वहाँ अधिकतम 30 दिनों में मिटा दी जाती है। आपको खाते की ज़रूरत नहीं।"
"text": "आपकी चिट्ठियाँ आपके अपने उपकरण पर एन्क्रिप्टेड रहती हैं। विश्लेषण के लिए फ़ोटो एक बार सुरक्षित रूप से एआई को भेजी जाती है और वहाँ आमतौर पर 30 दिन बाद मिटा दी जाती है। आपको खाते की ज़रूरत नहीं।"
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>मेरे डेटा का क्या होता है?</summary>
<p>आपकी चिट्ठियाँ आपके अपने उपकरण पर एन्क्रिप्टेड रहती हैं। विश्लेषण के लिए फ़ोटो एक बार सुरक्षित रूप से एआई को भेजी जाती है और वहाँ अधिकतम 30 दिनों में मिटा दी जाती है। आपको खाते की ज़रूरत नहीं।</p>
<p>आपकी चिट्ठियाँ आपके अपने उपकरण पर एन्क्रिप्टेड रहती हैं। विश्लेषण के लिए फ़ोटो एक बार सुरक्षित रूप से एआई को भेजी जाती है और वहाँ आमतौर पर 30 दिन बाद मिटा दी जाती है। आपको खाते की ज़रूरत नहीं।</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Što se događa s mojim podacima?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje umjetnoj inteligenciji i ondje se briše najkasnije nakon 30 dana. Račun nije potreban."
"text": "Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje umjetnoj inteligenciji i ondje se u pravilu briše nakon 30 dana. Račun nije potreban."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Što se događa s mojim podacima?</summary>
<p>Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje umjetnoj inteligenciji i ondje se briše najkasnije nakon 30 dana. Račun nije potreban.</p>
<p>Vaša pisma ostaju šifrirana na vašem uređaju. Za analizu se fotografija jednom sigurno šalje umjetnoj inteligenciji i ondje se u pravilu briše nakon 30 dana. Račun nije potreban.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Mi történik az adataimmal?",
"acceptedAnswer": {
"@type": "Answer",
"text": "A levelei titkosítva a saját készülékén maradnak. Az elemzéshez a fénykép egyszer, biztonságosan kerül el az MI-hez, és ott legkésőbb 30 nap múlva törlődik. Nincs szükség fiókra."
"text": "A levelei titkosítva a saját készülékén maradnak. Az elemzéshez a fénykép egyszer, biztonságosan kerül el az MI-hez, és ott általában 30 nap múlva törlődik. Nincs szükség fiókra."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Mi történik az adataimmal?</summary>
<p>A levelei titkosítva a saját készülékén maradnak. Az elemzéshez a fénykép egyszer, biztonságosan kerül el az MI-hez, és ott legkésőbb 30 nap múlva törlődik. Nincs szükség fiókra.</p>
<p>A levelei titkosítva a saját készülékén maradnak. Az elemzéshez a fénykép egyszer, biztonságosan kerül el az MI-hez, és ott általában 30 nap múlva törlődik. Nincs szükség fiókra.</p>
</details>
</section>
+2 -2
View File
@@ -336,7 +336,7 @@
{
"@type": "Question",
"name": "Was passiert mit meinen Briefen?",
"acceptedAnswer": { "@type": "Answer", "text": "Ihre Briefe bleiben auf Ihrem Handy — sicher verschlüsselt. Für die Analyse wird das Foto einmal sicher an die KI geschickt. Nach spätestens 30 Tagen wird es dort gelöscht. Sie brauchen kein Konto." }
"acceptedAnswer": { "@type": "Answer", "text": "Ihre Briefe bleiben auf Ihrem Handy — sicher verschlüsselt. Für die Analyse wird das Foto einmal sicher an die KI geschickt. In der Regel wird es dort nach 30 Tagen gelöscht. Sie brauchen kein Konto." }
},
{
"@type": "Question",
@@ -607,7 +607,7 @@
</details>
<details>
<summary>Was passiert mit meinen Briefen?</summary>
<p>Ihre Briefe bleiben auf Ihrem Handy — sicher verschlüsselt. Für die Analyse wird das Foto einmal sicher an die KI geschickt. Nach spätestens 30 Tagen wird es dort gelöscht. Sie brauchen kein Konto.</p>
<p>Ihre Briefe bleiben auf Ihrem Handy — sicher verschlüsselt. Für die Analyse wird das Foto einmal sicher an die KI geschickt. In der Regel wird es dort nach 30 Tagen gelöscht. Sie brauchen kein Konto.</p>
</details>
<details>
<summary>Ersetzt die App einen Anwalt?</summary>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Che fine fanno i miei dati?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Le tue lettere restano cifrate sul tuo dispositivo. Per l’analisi la foto viene inviata una sola volta in modo sicuro all’IA e viene cancellata lì al più tardi dopo 30 giorni. Non serve un account."
"text": "Le tue lettere restano cifrate sul tuo dispositivo. Per l’analisi la foto viene inviata una sola volta in modo sicuro all’IA e viene cancellata lì di norma dopo 30 giorni. Non serve un account."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Che fine fanno i miei dati?</summary>
<p>Le tue lettere restano cifrate sul tuo dispositivo. Per l’analisi la foto viene inviata una sola volta in modo sicuro all’IA e viene cancellata lì al più tardi dopo 30 giorni. Non serve un account.</p>
<p>Le tue lettere restano cifrate sul tuo dispositivo. Per l’analisi la foto viene inviata una sola volta in modo sicuro all’IA e viene cancellata lì di norma dopo 30 giorni. Non serve un account.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Bi daneyên min çi tê kirin?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Nameyên te bi şîfre li ser amûra te dimînin. Ji bo analîzê wêne carekê bi ewlehî ji AI re tê şandin û li wir herî dereng piştî 30 rojan tê jêbirin. Tu hewceyî hesabekî nînî."
"text": "Nameyên te bi şîfre li ser amûra te dimînin. Ji bo analîzê wêne carekê bi ewlehî ji AI re tê şandin û li wir bi gelemperî piştî 30 rojan tê jêbirin. Tu hewceyî hesabekî nînî."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Bi daneyên min çi tê kirin?</summary>
<p>Nameyên te bi şîfre li ser amûra te dimînin. Ji bo analîzê wêne carekê bi ewlehî ji AI re tê şandin û li wir herî dereng piştî 30 rojan tê jêbirin. Tu hewceyî hesabekî nînî.</p>
<p>Nameyên te bi şîfre li ser amûra te dimînin. Ji bo analîzê wêne carekê bi ewlehî ji AI re tê şandin û li wir bi gelemperî piştî 30 rojan tê jêbirin. Tu hewceyî hesabekî nînî.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Co dzieje się z moimi danymi?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Twoje pisma pozostają zaszyfrowane na Twoim urządzeniu. Do analizy zdjęcie jest raz bezpiecznie wysyłane do SI i usuwane tam najpóźniej po 30 dniach. Konto nie jest potrzebne."
"text": "Twoje pisma pozostają zaszyfrowane na Twoim urządzeniu. Do analizy zdjęcie jest raz bezpiecznie wysyłane do SI i usuwane tam zwykle po 30 dniach. Konto nie jest potrzebne."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Co dzieje się z moimi danymi?</summary>
<p>Twoje pisma pozostają zaszyfrowane na Twoim urządzeniu. Do analizy zdjęcie jest raz bezpiecznie wysyłane do SI i usuwane tam najpóźniej po 30 dniach. Konto nie jest potrzebne.</p>
<p>Twoje pisma pozostają zaszyfrowane na Twoim urządzeniu. Do analizy zdjęcie jest raz bezpiecznie wysyłane do SI i usuwane tam zwykle po 30 dniach. Konto nie jest potrzebne.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "زما معلوماتو ته څه کیږي؟",
"acceptedAnswer": {
"@type": "Answer",
"text": "ستاسو لیکونه پټ شوي ستاسو په خپله وسیله پاتې کیږي. د شننې لپاره عکس یو ځل په خوندي ډول مصنوعي ځیرکتیا ته لیږل کیږي او هلته تر ۳۰ ورځو وروسته ړنګېږي. حساب ته اړتیا نشته."
"text": "ستاسو لیکونه پټ شوي ستاسو په خپله وسیله پاتې کیږي. د شننې لپاره عکس یو ځل په خوندي ډول مصنوعي ځیرکتیا ته لیږل کیږي او هلته معمولاً تر ۳۰ ورځو وروسته ړنګېږي. حساب ته اړتیا نشته."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>زما معلوماتو ته څه کیږي؟</summary>
<p>ستاسو لیکونه پټ شوي ستاسو په خپله وسیله پاتې کیږي. د شننې لپاره عکس یو ځل په خوندي ډول مصنوعي ځیرکتیا ته لیږل کیږي او هلته تر ۳۰ ورځو وروسته ړنګېږي. حساب ته اړتیا نشته.</p>
<p>ستاسو لیکونه پټ شوي ستاسو په خپله وسیله پاتې کیږي. د شننې لپاره عکس یو ځل په خوندي ډول مصنوعي ځیرکتیا ته لیږل کیږي او هلته معمولاً تر ۳۰ ورځو وروسته ړنګېږي. حساب ته اړتیا نشته.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "O que acontece aos meus dados?",
"acceptedAnswer": {
"@type": "Answer",
"text": "As suas cartas ficam cifradas no seu próprio dispositivo. Para a análise, a foto é enviada uma única vez de forma segura para a IA e é apagada lá no máximo ao fim de 30 dias. Não precisa de conta."
"text": "As suas cartas ficam cifradas no seu próprio dispositivo. Para a análise, a foto é enviada uma única vez de forma segura para a IA e é apagada lá normalmente ao fim de 30 dias. Não precisa de conta."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>O que acontece aos meus dados?</summary>
<p>As suas cartas ficam cifradas no seu próprio dispositivo. Para a análise, a foto é enviada uma única vez de forma segura para a IA e é apagada lá no máximo ao fim de 30 dias. Não precisa de conta.</p>
<p>As suas cartas ficam cifradas no seu próprio dispositivo. Para a análise, a foto é enviada uma única vez de forma segura para a IA e é apagada lá normalmente ao fim de 30 dias. Não precisa de conta.</p>
</details>
</section>
+2297
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Ce se întâmplă cu datele mele?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Scrisorile tale rămân criptate pe dispozitivul tău. Pentru analiză, fotografia este trimisă o singură dată în siguranță către IA și este ștearsă acolo în cel mult 30 de zile. Nu ai nevoie de cont."
"text": "Scrisorile tale rămân criptate pe dispozitivul tău. Pentru analiză, fotografia este trimisă o singură dată în siguranță către IA și este ștearsă acolo de regulă după 30 de zile. Nu ai nevoie de cont."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Ce se întâmplă cu datele mele?</summary>
<p>Scrisorile tale rămân criptate pe dispozitivul tău. Pentru analiză, fotografia este trimisă o singură dată în siguranță către IA și este ștearsă acolo în cel mult 30 de zile. Nu ai nevoie de cont.</p>
<p>Scrisorile tale rămân criptate pe dispozitivul tău. Pentru analiză, fotografia este trimisă o singură dată în siguranță către IA și este ștearsă acolo de regulă după 30 de zile. Nu ai nevoie de cont.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Что происходит с моими данными?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ваши письма остаются зашифрованными на вашем устройстве. Для разбора фото один раз безопасно отправляется ИИ и удаляется там не позднее чем через 30 дней. Учётная запись не нужна."
"text": "Ваши письма остаются зашифрованными на вашем устройстве. Для разбора фото один раз безопасно отправляется ИИ и удаляется там обычно через 30 дней. Учётная запись не нужна."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Что происходит с моими данными?</summary>
<p>Ваши письма остаются зашифрованными на вашем устройстве. Для разбора фото один раз безопасно отправляется ИИ и удаляется там не позднее чем через 30 дней. Учётная запись не нужна.</p>
<p>Ваши письма остаются зашифрованными на вашем устройстве. Для разбора фото один раз безопасно отправляется ИИ и удаляется там обычно через 30 дней. Учётная запись не нужна.</p>
</details>
</section>
+28 -28
View File
@@ -3,7 +3,7 @@
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://behoerdenklar.de/</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -36,7 +36,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/tr</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -69,7 +69,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/ar</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -102,7 +102,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/ru</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -135,7 +135,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/uk</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -168,7 +168,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/pl</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -201,7 +201,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/ro</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -234,7 +234,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/en</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -267,7 +267,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/fa</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -300,7 +300,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/ku</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -333,7 +333,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/bg</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -366,7 +366,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/hr</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -399,7 +399,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/sr</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -432,7 +432,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/bs</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -465,7 +465,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/sq</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -498,7 +498,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/it</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -531,7 +531,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/el</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -564,7 +564,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/es</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -597,7 +597,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/pt</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -630,7 +630,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/fr</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -663,7 +663,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/hu</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -696,7 +696,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/vi</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -729,7 +729,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/hi</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -762,7 +762,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/ur</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -795,7 +795,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/ps</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -828,7 +828,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/ti</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -861,7 +861,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/so</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
@@ -894,7 +894,7 @@
</url>
<url>
<loc>https://behoerdenklar.de/zh</loc>
<lastmod>2026-09-23</lastmod>
<lastmod>2026-10-22</lastmod>
<xhtml:link rel="alternate" hreflang="de" href="https://behoerdenklar.de/"/>
<xhtml:link rel="alternate" hreflang="tr" href="https://behoerdenklar.de/tr"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://behoerdenklar.de/ar"/>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Maxaa ku dhacaya xogtayda?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Warqadahaagu waxay ku hadhayaan qalabkaaga iyagoo sir ah. Falanqaynta awgeed sawirka hal mar ayaa si ammaan ah loogu diraa AI-ga, halkaasna waxaa laga tirtiraa ugu badnaan 30 maalmood gudahood. Koonto uma baahnid."
"text": "Warqadahaagu waxay ku hadhayaan qalabkaaga iyagoo sir ah. Falanqaynta awgeed sawirka hal mar ayaa si ammaan ah loogu diraa AI-ga, halkaasna waxaa laga tirtiraa inta badan 30 maalmood kadib. Koonto uma baahnid."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Maxaa ku dhacaya xogtayda?</summary>
<p>Warqadahaagu waxay ku hadhayaan qalabkaaga iyagoo sir ah. Falanqaynta awgeed sawirka hal mar ayaa si ammaan ah loogu diraa AI-ga, halkaasna waxaa laga tirtiraa ugu badnaan 30 maalmood gudahood. Koonto uma baahnid.</p>
<p>Warqadahaagu waxay ku hadhayaan qalabkaaga iyagoo sir ah. Falanqaynta awgeed sawirka hal mar ayaa si ammaan ah loogu diraa AI-ga, halkaasna waxaa laga tirtiraa inta badan 30 maalmood kadib. Koonto uma baahnid.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Çfarë ndodh me të dhënat e mia?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Letrat tuaja mbeten të koduara në pajisjen tuaj. Për analizën, fotoja dërgohet një herë në mënyrë të sigurt te inteligjenca artificiale dhe fshihet atje më së voni pas 30 ditësh. Nuk ju duhet llogari."
"text": "Letrat tuaja mbeten të koduara në pajisjen tuaj. Për analizën, fotoja dërgohet një herë në mënyrë të sigurt te inteligjenca artificiale dhe fshihet atje zakonisht pas 30 ditësh. Nuk ju duhet llogari."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Çfarë ndodh me të dhënat e mia?</summary>
<p>Letrat tuaja mbeten të koduara në pajisjen tuaj. Për analizën, fotoja dërgohet një herë në mënyrë të sigurt te inteligjenca artificiale dhe fshihet atje më së voni pas 30 ditësh. Nuk ju duhet llogari.</p>
<p>Letrat tuaja mbeten të koduara në pajisjen tuaj. Për analizën, fotoja dërgohet një herë në mënyrë të sigurt te inteligjenca artificiale dhe fshihet atje zakonisht pas 30 ditësh. Nuk ju duhet llogari.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Шта се дешава са мојим подацима?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ваша писма остају шифрована на вашем уређају. За анализу се фотографија једном безбедно шаље вештачкој интелигенцији и тамо се брише најкасније после 30 дана. Налог није потребан."
"text": "Ваша писма остају шифрована на вашем уређају. За анализу се фотографија једном безбедно шаље вештачкој интелигенцији и тамо се по правилу брише после 30 дана. Налог није потребан."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Шта се дешава са мојим подацима?</summary>
<p>Ваша писма остају шифрована на вашем уређају. За анализу се фотографија једном безбедно шаље вештачкој интелигенцији и тамо се брише најкасније после 30 дана. Налог није потребан.</p>
<p>Ваша писма остају шифрована на вашем уређају. За анализу се фотографија једном безбедно шаље вештачкој интелигенцији и тамо се по правилу брише после 30 дана. Налог није потребан.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "ብዛዕባ ዳታይ እንታይ ይኸውን?",
"acceptedAnswer": {
"@type": "Answer",
"text": "ደብዳበታትካ ኣብ ናትካ መሳርሒ ተመስጢሮም ይጸንሑ። ንመጽናዕቲ እቲ ስእሊ ሓንሳብ ብውሑስ መንገዲ ናብ AI ይለኣኽ፣ ኣብኡ ድማ ዝበዝሐ ድሕሪ 30 መዓልቲ ይድምሰስ። ኣካውንት ኣየድልየካን።"
"text": "ደብዳበታትካ ኣብ ናትካ መሳርሒ ተመስጢሮም ይጸንሑ። ንመጽናዕቲ እቲ ስእሊ ሓንሳብ ብውሑስ መንገዲ ናብ AI ይለኣኽ፣ ኣብኡ ድማ ብልሙድ ድሕሪ 30 መዓልቲ ይድምሰስ። ኣካውንት ኣየድልየካን።"
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>ብዛዕባ ዳታይ እንታይ ይኸውን?</summary>
<p>ደብዳበታትካ ኣብ ናትካ መሳርሒ ተመስጢሮም ይጸንሑ። ንመጽናዕቲ እቲ ስእሊ ሓንሳብ ብውሑስ መንገዲ ናብ AI ይለኣኽ፣ ኣብኡ ድማ ዝበዝሐ ድሕሪ 30 መዓልቲ ይድምሰስ። ኣካውንት ኣየድልየካን።</p>
<p>ደብዳበታትካ ኣብ ናትካ መሳርሒ ተመስጢሮም ይጸንሑ። ንመጽናዕቲ እቲ ስእሊ ሓንሳብ ብውሑስ መንገዲ ናብ AI ይለኣኽ፣ ኣብኡ ድማ ብልሙድ ድሕሪ 30 መዓልቲ ይድምሰስ። ኣካውንት ኣየድልየካን።</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Verilerime ne oluyor?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Mektuplarınız şifreli olarak kendi cihazınızda kalır. Analiz için fotoğraf bir kez güvenli şekilde yapay zekâya gönderilir ve orada en geç 30 gün sonra silinir. Hesap açmanız gerekmez."
"text": "Mektuplarınız şifreli olarak kendi cihazınızda kalır. Analiz için fotoğraf bir kez güvenli şekilde yapay zekâya gönderilir ve orada genellikle 30 gün sonra silinir. Hesap açmanız gerekmez."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Verilerime ne oluyor?</summary>
<p>Mektuplarınız şifreli olarak kendi cihazınızda kalır. Analiz için fotoğraf bir kez güvenli şekilde yapay zekâya gönderilir ve orada en geç 30 gün sonra silinir. Hesap açmanız gerekmez.</p>
<p>Mektuplarınız şifreli olarak kendi cihazınızda kalır. Analiz için fotoğraf bir kez güvenli şekilde yapay zekâya gönderilir ve orada genellikle 30 gün sonra silinir. Hesap açmanız gerekmez.</p>
</details>
</section>
+123
View File
@@ -0,0 +1,123 @@
/**
* Übergabe Handy ↔ PC — Ende-zu-Ende verschlüsselt.
*
* Ein Gerät erzeugt eine Übergabe (zufällige ID + AES-256-Schlüssel) und
* zeigt sie als QR-Code. Beides steht hinter dem `#` der Adresse; dieser
* Teil wird von Browsern NIE an einen Server geschickt. Der Worker sieht nur
* verschlüsselte Päckchen und löscht sie nach 15 Minuten.
*
* Päckchen-Format: base64url( IV[12 Byte] || AES-GCM-Chiffrat ) eines JSON.
* Die App (src/utils/uebergabe.ts) benutzt exakt dasselbe Format.
*/
(function (w) {
'use strict';
var LOKAL = /^(localhost|127\.0\.0\.1)$/.test(w.location.hostname);
var BASIS = (LOKAL ? 'http://localhost:8799' : 'https://behoerdenklar-proxy.behoerdenbrief.workers.dev') + '/uebergabe/';
function b64u(bytes) {
var s = '';
for (var i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function ausB64u(text) {
var s = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
var b = new Uint8Array(s.length);
for (var i = 0; i < s.length; i++) b[i] = s.charCodeAt(i);
return b;
}
function zufall(n) { return w.crypto.getRandomValues(new Uint8Array(n)); }
function schluessel(roh) {
return w.crypto.subtle.importKey('raw', roh, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
}
/** Neue Übergabe: { id, k, key } — k ist der Schlüssel als Text für den QR-Code. */
async function neu() {
var roh = zufall(32);
return { id: b64u(zufall(16)), k: b64u(roh), key: await schluessel(roh) };
}
/** Übergabe aus ID + Schlüsseltext (aus dem QR-Code) öffnen. */
async function oeffne(id, k) {
if (!/^[A-Za-z0-9_-]{22}$/.test(id || '') || !/^[A-Za-z0-9_-]{43}$/.test(k || '')) return null;
return { id: id, k: k, key: await schluessel(ausB64u(k)) };
}
async function verschluesseln(ue, daten) {
var iv = zufall(12);
var klar = new TextEncoder().encode(JSON.stringify(daten));
var ct = new Uint8Array(await w.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, ue.key, klar));
var alles = new Uint8Array(iv.length + ct.length);
alles.set(iv, 0); alles.set(ct, iv.length);
return b64u(alles);
}
/** Entschlüsseln; null, wenn das Päckchen nicht von dieser Übergabe stammt. */
async function entschluesseln(ue, text) {
try {
var b = ausB64u(text);
var klar = await w.crypto.subtle.decrypt({ name: 'AES-GCM', iv: b.slice(0, 12) }, ue.key, b.slice(12));
return JSON.parse(new TextDecoder().decode(klar));
} catch (e) { return null; }
}
/** Päckchen in ein Fach legen (z. B. „status", „ergebnis"). */
async function senden(ue, fach, daten) {
var r = await fetch(BASIS + ue.id + '/' + fach, { method: 'PUT', body: await verschluesseln(ue, daten) });
if (!r.ok) throw new Error('Übergabe fehlgeschlagen (' + r.status + ')');
}
/** Alle Fächer holen und entschlüsseln: { status: {...}, ergebnis: {...} }. */
async function holen(ue) {
// 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 = {};
for (var f in faecher) {
var d = await entschluesseln(ue, faecher[f]);
if (d) aus[f] = d;
}
return aus;
}
function loeschen(ue) {
return fetch(BASIS + ue.id, { method: 'DELETE' }).catch(function () {});
}
/** Adresse für den QR-Code. `werte` landet komplett hinter dem `#`. */
function adresse(werte) {
var teile = [];
for (var k in werte) if (werte[k] !== undefined && werte[k] !== null && werte[k] !== '') {
teile.push(k + '=' + encodeURIComponent(werte[k]));
}
return w.location.origin + w.location.pathname + '#' + teile.join('&');
}
/** Werte hinter dem `#` lesen (oder null, wenn keine da sind). */
function ausAdresse() {
var h = w.location.hash.replace(/^#/, '');
if (!h) return null;
var werte = {};
h.split('&').forEach(function (t) {
var i = t.indexOf('=');
if (i > 0) werte[t.slice(0, i)] = decodeURIComponent(t.slice(i + 1));
});
return werte;
}
/** QR-Code als SVG-Text (braucht qrcode.js). */
function qrSvg(text) {
var qr = w.qrcode(0, 'M');
qr.addData(text);
qr.make();
return qr.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
}
w.BKUebergabe = {
neu: neu, oeffne: oeffne, senden: senden, holen: holen, loeschen: loeschen,
adresse: adresse, ausAdresse: ausAdresse, qrSvg: qrSvg,
LEBENSDAUER_MIN: 15,
};
})(window);
+2138
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Що відбувається з моїми даними?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ваші листи залишаються зашифрованими на вашому пристрої. Для аналізу фото один раз безпечно надсилається до ШІ і видаляється там щонайпізніше через 30 днів. Обліковий запис не потрібен."
"text": "Ваші листи залишаються зашифрованими на вашому пристрої. Для аналізу фото один раз безпечно надсилається до ШІ і видаляється там зазвичай через 30 днів. Обліковий запис не потрібен."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Що відбувається з моїми даними?</summary>
<p>Ваші листи залишаються зашифрованими на вашому пристрої. Для аналізу фото один раз безпечно надсилається до ШІ і видаляється там щонайпізніше через 30 днів. Обліковий запис не потрібен.</p>
<p>Ваші листи залишаються зашифрованими на вашому пристрої. Для аналізу фото один раз безпечно надсилається до ШІ і видаляється там зазвичай через 30 днів. Обліковий запис не потрібен.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "میرے ڈیٹا کا کیا ہوتا ہے؟",
"acceptedAnswer": {
"@type": "Answer",
"text": "آپ کے خطوط آپ کے اپنے آلے پر خفیہ شکل میں رہتے ہیں۔ تجزیے کے لیے تصویر ایک بار محفوظ طریقے سے اے آئی کو بھیجی جاتی ہے اور وہاں زیادہ سے زیادہ 30 دن میں مٹا دی جاتی ہے۔ آپ کو اکاؤنٹ کی ضرورت نہیں۔"
"text": "آپ کے خطوط آپ کے اپنے آلے پر خفیہ شکل میں رہتے ہیں۔ تجزیے کے لیے تصویر ایک بار محفوظ طریقے سے اے آئی کو بھیجی جاتی ہے اور وہاں عام طور پر 30 دن بعد مٹا دی جاتی ہے۔ آپ کو اکاؤنٹ کی ضرورت نہیں۔"
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>میرے ڈیٹا کا کیا ہوتا ہے؟</summary>
<p>آپ کے خطوط آپ کے اپنے آلے پر خفیہ شکل میں رہتے ہیں۔ تجزیے کے لیے تصویر ایک بار محفوظ طریقے سے اے آئی کو بھیجی جاتی ہے اور وہاں زیادہ سے زیادہ 30 دن میں مٹا دی جاتی ہے۔ آپ کو اکاؤنٹ کی ضرورت نہیں۔</p>
<p>آپ کے خطوط آپ کے اپنے آلے پر خفیہ شکل میں رہتے ہیں۔ تجزیے کے لیے تصویر ایک بار محفوظ طریقے سے اے آئی کو بھیجی جاتی ہے اور وہاں عام طور پر 30 دن بعد مٹا دی جاتی ہے۔ آپ کو اکاؤنٹ کی ضرورت نہیں۔</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "Dữ liệu của tôi thì sao?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Thư của bạn được mã hóa và nằm trên thiết bị của bạn. Để phân tích, ảnh được gửi một lần duy nhất và an toàn tới AI, rồi bị xóa ở đó chậm nhất sau 30 ngày. Bạn không cần tài khoản."
"text": "Thư của bạn được mã hóa và nằm trên thiết bị của bạn. Để phân tích, ảnh được gửi một lần duy nhất và an toàn tới AI, rồi thường bị xóa ở đó sau 30 ngày. Bạn không cần tài khoản."
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>Dữ liệu của tôi thì sao?</summary>
<p>Thư của bạn được mã hóa và nằm trên thiết bị của bạn. Để phân tích, ảnh được gửi một lần duy nhất và an toàn tới AI, rồi bị xóa ở đó chậm nhất sau 30 ngày. Bạn không cần tài khoản.</p>
<p>Thư của bạn được mã hóa và nằm trên thiết bị của bạn. Để phân tích, ảnh được gửi một lần duy nhất và an toàn tới AI, rồi thường bị xóa ở đó sau 30 ngày. Bạn không cần tài khoản.</p>
</details>
</section>
+2 -2
View File
@@ -231,7 +231,7 @@
"name": "我的数据会怎样?",
"acceptedAnswer": {
"@type": "Answer",
"text": "您的信件以加密方式保存在您自己的设备上。分析时照片会安全地发送给人工智能一次,并最迟在 30 天后在那里删除。您不需要账号。"
"text": "您的信件以加密方式保存在您自己的设备上。分析时照片会安全地发送给人工智能一次,并通常在 30 天后在那里删除。您不需要账号。"
}
}
]
@@ -368,7 +368,7 @@
</details>
<details>
<summary>我的数据会怎样?</summary>
<p>您的信件以加密方式保存在您自己的设备上。分析时照片会安全地发送给人工智能一次,并最迟在 30 天后在那里删除。您不需要账号。</p>
<p>您的信件以加密方式保存在您自己的设备上。分析时照片会安全地发送给人工智能一次,并通常在 30 天后在那里删除。您不需要账号。</p>
</details>
</section>