Compare commits

...

17 Commits

Author SHA1 Message Date
2c48a729aa Landingpage: zweite Vereinfachungs-Runde (A2) — Amtsdeutsch-Zitat bleibt
- 'Früh-Zugang beantragen' -> 'Jetzt anmelden', 'Die Verwandlung' ->
  'Vorher und Nachher', 'analysieren' -> 'erklären lassen'
- Insider-Witz 'i. A. der Verständlichkeit' entfernt, Fußzeile vereinfacht

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:39:14 +02:00
f21e0b6001 Besuchs-Checkliste: Frage nach Entscheidern beim Träger ergänzt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:04:57 +02:00
0931986396 Marketing: E-Mail-Vorlage für Beratungsstellen + App-Screenshots
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:57:58 +02:00
72809d3868 Backlog: Berater-Modus mit Fall-Ordnern konkretisiert (Idee Justin)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:54:48 +02:00
f2f7059fbf Cloudflare Web Analytics: cookieloser Besucher-Zähler auf allen 9 Seiten
- Beacon-Script vor </body> auf jeder Seite (DE, 6 Sprachen, Impressum,
  Datenschutz)
- Datenschutzerklärung (Web + Entwurf): neuer Abschnitt Reichweitenmessung
  (Art. 6 Abs. 1 lit. f DSGVO, ohne Cookies, keine Personen-Profile)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:26:20 +02:00
2021390d98 Feature-Backlog für die Zeit nach dem Launch
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:01:00 +02:00
6ed940a77d Frische Farbwelt: Apple-System-Blau + klare Ampel-Signalfarben
- Primärfarbe: Apple-System-Blau (#007AFF) statt dunklem Marineblau —
  Buttons, Icons und Links wirken wie in Apples eigenen Apps
- Header nach Apple-Konvention: Titel in Textfarbe, nur Interaktives im Tint
- Ampel: frische Signalfarben in zugänglichen Varianten (klares Rot,
  Orange statt Braun, sattes Grün) — als Text auf Weiß weiter WCAG-lesbar
- Großtitel entfernt (renderte auf iOS 26 nicht zuverlässig) — Standard-
  Header ist stabil auf allen Screens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:39:51 +02:00
4ce397026c Apple-natives Design: SF Symbols statt Emojis, iOS-System-Optik
- Neue Ikone-Komponente (expo-symbols): echte SF Symbols auf iOS,
  Material Symbols auf Android — alle Emojis in Buttons/Kacheln/
  Checkboxen/Ampel/Chevrons ersetzt
- Theme: iOS-Systemgrau (#F2F2F7) statt Papier-Ton, weiße Karten ohne
  Rahmen und Schatten (wie Einstellungen/Health), "graue Taste" für
  Sekundär-Buttons statt Umrandung
- Kernaussage-Karte: dezenter Marken-Tint statt Textmarker-Gelb
- Frist/Termin-Zeilen ohne Emoji-Präfixe
- Layout unverändert (Kacheln, Ampel-Streifen, großer Titel)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:32:18 +02:00
a670cc1758 Word-Sperrdateien ignorieren
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:14:27 +02:00
fdfd2d392a App-Design-Politur: Papier-Look, Kacheln, Ampel-Streifen, großer Titel
- Theme: warmer Papier-Hintergrund (#FAF7F0) wie die Webseite, Inhalte
  als weiße Karten mit dezentem Schatten + warmem Rand; Marker-Gelb
  als Akzentfarbe für die Kernaussage
- Home: iOS-Großtitel (kollabiert beim Scrollen), Hero-Button mit
  Schatten, Kachel-Navigation statt halbbreiter Text-Buttons (behebt
  Umbruch "Einstellunge/n"), Abschnitts-Label, Brief-Karten mit
  Ampel-Farbstreifen links, freundlicher Leerzustand
- Analyse: Kernaussage als Textmarker-Karte (Markenzeichen), Frist/
  Termin-Karte in Ampel-Tönung mit Farbstreifen, einheitliche Karten
- Ampel-Banner: Streifen-Look statt Vollrahmen
- Layout-Fix: Home als FlatList-Wurzel mit automatischem Inset (vorher
  verdeckte der große Titel die Kopf-Buttons)

Barrierefreiheit unverändert: Schriftgrößen, Touch-Targets, Kontraste.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:14:11 +02:00
7cfe830e9f Landingpage: einfacheres Deutsch (A2/B1) + 6 Sprachversionen
- Deutsche Seite sprachlich vereinfacht (kurze Sätze, keine schweren
  Wörter — passend zum Produktversprechen); Sprachleiste ergänzt
- Neue Seiten: tr, ar (RTL), en, uk, ru, pl — kompakt, einfache Sprache,
  gleiche Papier-Optik, Vorher/Nachher-Demo (deutsches Amtszitat +
  Erklärung in der Zielsprache), Warteliste-CTA je Sprache
- hreflang-Verweise auf allen Seiten; arabische Seite mit dir=rtl,
  deutsches Zitat dort explizit ltr
- Live geprüft: alle 6 Seiten erreichbar, RTL-Layout korrekt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 09:00:08 +02:00
3dac4cfe14 AVV/DPA-Nachweis dokumentiert (Anthropic + Cloudflare, beide automatisch via AGB)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 08:44:14 +02:00
a7838e4db6 Pitch-Dokument (Word): Datenschutz & Sicherheit für Kooperationspartner
- rechtliches/BehoerdenKlar-Datenschutz.docx — pitchfertiges Word-Dokument
  im Marken-Look (Kernaussage-Box, Datenfluss-Schritte, Maßnahmen-Tabelle,
  transparente Punkte, Kontakt-Box)
- rechtliches/datenschutz-docx-generator.js — docx-js-Skript zum Neu-Erzeugen
  (node datenschutz-docx-generator.js <ausgabe.docx>, braucht `npm i docx`)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:32:10 +02:00
ad1c2bf76c Datenschutz-Ausbau: Archiv-Verschlüsselung, App-Sperre, Auto-Löschen
- Brief-Archiv jetzt AES-256-GCM-verschlüsselt (expo-crypto, Schlüssel im
  SecureStore); Migration alter Klartext-Bestände beim ersten Laden;
  im Simulator auf der Platte verifiziert (nur Chiffrat, kein Klartext)
- AppSchutz-Komponente: optionale App-Sperre (Face ID / Geräte-Code,
  expo-local-authentication) + Sichtschutz im App-Umschalter
- Auto-Löschen alter Briefe (30/90 Tage, Einstellungen), pure Filter-
  Funktion mit Tests
- UTF-8-Helfer (Umlaute/Arabisch/Emoji-sicher) mit Tests; gesamt 30 Tests
- Einstellungen: neuer Abschnitt "Sicherheit"
- Datenschutzerklärung (Web + Entwurf) um die neuen Maßnahmen ergänzt
- Neu: rechtliches/datenschutz-factsheet.md für Kooperations-Pitches
- EU-Verarbeitung geprüft: inference_geo bietet aktuell nur us/global,
  keine EU-Option (Haiku 4.5 gar nicht) — als Ausbaupfad dokumentiert

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 15:30:19 +02:00
12ebf514fb Landingpage-Redesign: Behördenbrief-Ästhetik mit Gedächtnis-Ankern
Konzept: Die Seite sieht selbst aus wie ein Behördenbrief, der endlich
verständlich ist (Briefkopf mit Aktenzeichen, Paragraphen-Gliederung,
Stempel, Textmarker, Antragsformular-Parodie, Grußformel).

Eingesetzte Prinzipien: Von-Restorff (unverwechselbare Gestaltung),
Vorher/Nachher-Konkretbeispiel statt Abstraktion, Verlustaversion/
Exklusivität beim Wartelisten-Bonus, Social Proof über LEO-Studie,
Reim-Slogan "Brief rein. Klartext raus.", humorvoller Schluss
(Peak-End-Regel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 08:53:58 +02:00
8f9f988d57 Landingpage mit Warteliste + Aktions-Code (5 statt 3 Gratis-Analysen)
- Webseite: neue Landingpage (Hero, Features, Warteliste-Anmeldung per
  Mail-Interim, FAQ); Datenschutz um Warteliste-Abschnitt ergänzt
- App: Aktions-Code-Einlösung in den Einstellungen (FRUEHSTART -> 5
  Gratis-Analysen), ScanScreen nutzt dynamisches Kontingent
- Newsletter-Dienst (Brevo/MailerLite) folgt: Formular-Embed ersetzt
  dann den Mail-Button, Datenschutz-Platzhalter ausfuellen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 08:40:15 +02:00
9dc1d8d87b Release-Vorbereitung: Proxy, Kostenoptimierung, Freemium, Store-Assets
- Backend-Proxy (Cloudflare Worker in proxy/): API-Key serverseitig,
  Modell-Whitelist, max_tokens-Deckel, Tageslimits pro Gerät und IP
- Modell-Split: Analyse auf Sonnet 5, Übersetzung/Antwort auf Haiku 4.5
- Bild-Downscaling auf 2000px vor dem Upload (ScanScreen)
- Expo SDK 56 -> 57 Upgrade
- Freemium: 3 Gratis-Analysen mit lokalem Zähler und Paywall-Hinweis
- Einstellungen: API-Key-Feld nur noch im Dev-Modus (NUTZT_PROXY)
- Build-Setup: Bundle-IDs, eas.json, Splash-Screen-Plugin
- Tests: jest-expo + 17 Tests für die Ampel-/Fristlogik
- Rechtliches: Datenschutzerklärung (Entwurf), Webseite (webseite/,
  live auf behoerdenklar.pages.dev), Store-Texte (store/)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:14:00 +02:00
55 changed files with 9835 additions and 627 deletions

10
.gitignore vendored
View File

@@ -33,9 +33,19 @@ yarn-error.*
# local env files
.env*.local
# Cloudflare Worker (Proxy)
.wrangler/
.dev.vars
# typescript
*.tsbuildinfo
# generated native folders
/ios
/android
# generierte Vorschau-Bilder
*.docx.png
# Word-Sperrdateien
~$*

15
App.tsx
View File

@@ -9,6 +9,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { RootStackParamList } from './src/types';
import { useAppStore } from './src/store/useAppStore';
import { initialisiereBenachrichtigungen } from './src/services/erinnerungen';
import { AppSchutz } from './src/components/AppSchutz';
import { farben, schrift } from './src/theme';
import { HomeScreen } from './src/screens/HomeScreen';
import { ConsentScreen } from './src/screens/ConsentScreen';
@@ -29,13 +30,16 @@ export default function App() {
}, [initialisiere]);
return (
<NavigationContainer>
<StatusBar style="dark" />
<AppSchutz>
<NavigationContainer>
<StatusBar style="dark" />
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: farben.hintergrund },
headerShadowVisible: false,
// Apple-Konvention: Titel in Textfarbe, nur Interaktives im Blau-Tint
headerTintColor: farben.primaer,
headerTitleStyle: { fontSize: schrift.gross, fontWeight: '700' },
headerTitleStyle: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
headerBackTitle: 'Zurück',
contentStyle: { backgroundColor: farben.hintergrund },
}}
@@ -47,7 +51,8 @@ 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.Navigator>
</NavigationContainer>
</Stack.Navigator>
</NavigationContainer>
</AppSchutz>
);
}

43
BACKLOG.md Normal file
View File

@@ -0,0 +1,43 @@
# Feature-Backlog (nach dem Launch)
Grundsatz: Der Kern-Flow (Scannen → Verstehen → Reagieren) bleibt 3 Tipps
kurz. Neues kommt hinter Kacheln/Tabs, nie in den Hauptweg. Erst bauen,
wenn echtes Nutzer-Feedback den Bedarf zeigt.
## Naheliegend (hoher Nutzen, kleiner Aufwand — Kandidaten für v1.1)
- [ ] **Fristen-Übersicht**: alle Fristen/Termine aller Briefe in einer
sortierten Liste (Daten sind schon da — nur ein neuer Screen).
Passt perfekt zum Kern-Job "nichts verpassen".
- [ ] **App-Icon** im Marken-Stil der Webseite (Briefumschlag/Stempel-Motiv)
- [ ] **Home-Widget** (iOS): nächste Frist auf dem Homescreen
## Mittelfristig (validieren, dann bauen)
- [ ] Ordner/Kategorien im Archiv (erst relevant, wenn Nutzer >10 Briefe haben)
- [ ] Briefe nach Behörde/Aktenzeichen gruppieren
- [ ] Musterbrief-Bibliothek offline (häufige Antworten ohne KI-Kosten)
- [ ] On-Device-Texterkennung (Apple Vision Framework): Foto verlässt das
Handy nie — primär Datenschutz-Feature; braucht Dev-Build + Qualitätstest
der Fristen-Erkennung gegen Claude Vision (siehe Gespräch 16.07.)
## Groß (eigene Projekte, nicht nebenbei)
- [ ] Formular-Ausfüllhilfe (Anträge) — rechtlich heikler, eigener Freigabe-Check
- [ ] Familien-/Betreuer-Modus (Briefe für Angehörige verwalten)
- [ ] EU-Datenverarbeitung via AWS Bedrock Frankfurt (Proxy-Umbau)
- [ ] **Berater-Modus für Beratungsstellen** (B2B, Lizenz-Modell ~50100 €/Monat
pro Stelle): eigene Oberfläche mit Fall-Ordnern ("Nummer pro Klient" —
Idee von Justin, 18.07.) auf dem Berater-Gerät; Beraterin scannt Briefe
im Termin und legt sie pro Fall ab, kein Gratis-Limit. WICHTIG: kein
Fernzugriff aufs Klienten-Handy (zerstört das Datenschutz-Versprechen) —
stattdessen Fall-Ordner lokal beim Berater + optional aktives Teilen
einzelner Analysen durch den Klienten. Vor dem Bauen in den
Beratungsstellen-Besuchen validieren (Interview-Fragen: Nutzen?
Anforderungen? Zahlungsbereitschaft des Trägers?)
## UI-Wachstumspfad (damit es nie überladen wirkt)
1. Jetzt: 1 Hero-Button + 2 Kacheln
2. Bis 4 Funktionen: Kachel-Reihe erweitern (2x2)
3. Darüber: Tab-Leiste (Home / Fristen / Mehr) — Standard-iOS-Muster

View File

@@ -36,19 +36,19 @@ App in Expo Go (oder Development Build) öffnen, dann:
## Produktion: Backend-Proxy statt API-Key in der App
Für Endkunden darf kein API-Key in der App stecken. Vorbereitet in
`src/services/claudeClient.ts`:
Für Endkunden darf kein API-Key in der App stecken. Der fertige Proxy
(Cloudflare Worker) liegt in [`proxy/`](proxy/README.md) — er hält den Key
serverseitig, erlaubt nur die App-Modelle, deckelt `max_tokens` und setzt ein
Tageslimit pro Gerät durch.
Nach dem Deployment (Anleitung: `proxy/README.md`) in
`src/services/claudeClient.ts` die Worker-URL eintragen:
```ts
const PROXY_URL: string | null = null; // -> eigene Server-URL eintragen
const PROXY_URL: string | null = 'https://behoerdenklar-proxy.<account>.workers.dev';
```
Der Proxy (z. B. Cloudflare Worker / Vercel Function) muss nur:
1. den Request-Body unverändert an `https://api.anthropic.com/v1/messages` weiterleiten,
2. dabei serverseitig `x-api-key` + `anthropic-version: 2023-06-01` setzen,
3. die Antwort unverändert zurückgeben.
Abrechnung der Endkunden dann per Abo/In-App-Kauf.
Abrechnung der Endkunden dann per Abo/In-App-Kauf (Abo-Prüfung im Proxy ergänzen).
## Architektur

View File

@@ -7,9 +7,11 @@
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"ios": {
"supportsTablet": true
"supportsTablet": true,
"bundleIdentifier": "de.behoerdenklar.app"
},
"android": {
"package": "de.behoerdenklar.app",
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/android-icon-foreground.png",
@@ -38,7 +40,21 @@
}
],
"expo-notifications",
"expo-status-bar"
"expo-status-bar",
[
"expo-local-authentication",
{
"faceIDPermission": "BehördenKlar nutzt Face ID, um Ihre gescannten Briefe vor fremdem Zugriff zu schützen."
}
],
[
"expo-splash-screen",
{
"image": "./assets/splash-icon.png",
"backgroundColor": "#E6F4FE",
"imageWidth": 200
}
]
]
}
}

21
eas.json Normal file
View File

@@ -0,0 +1,21 @@
{
"cli": {
"version": ">= 16.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

View File

@@ -0,0 +1,69 @@
# E-Mail-Vorlage: Beratungsstellen
Absender: behoerdenbriefhelfer@gmail.com
Anhänge: BehoerdenKlar-Datenschutz als **PDF** (docx in Word öffnen →
Datei → Exportieren → PDF) + die 2 Screenshots aus marketing/screenshots/
Vor dem Senden anpassen: [ANREDE] — wenn auf der Webseite ein Name der
Beraterin/des Beraters steht, unbedingt persönlich anschreiben.
---
**An:** mbe@fid-donnersberg.de
**Betreff:** App aus dem Donnersbergkreis erklärt Behördenbriefe in einfacher Sprache — 10 Minuten Feedback aus Ihrer Praxis?
Guten Tag [ANREDE / sehr geehrtes Team der Migrationsberatung],
mein Name ist Justin Klein, ich komme aus Winnweiler und habe eine
iPhone-App entwickelt, die Behördenbriefe verständlich macht:
Brief fotografieren — die App erklärt in einfacher Sprache (A2), was das
Amt will, zeigt Fristen und Termine mit Erinnerung und erstellt auf Wunsch
einen Antwort-Entwurf. Die Erklärung lässt sich in 9 Sprachen übersetzen,
unter anderem Arabisch, Türkisch, Ukrainisch und Farsi — die deutschen
Fachbegriffe bleiben dabei sichtbar, damit Ihre Klientinnen und Klienten
sie im Brief wiederfinden.
Ich glaube, das könnte die Menschen entlasten, die zu Ihnen kommen — und
vielleicht auch Ihre Beratungszeit: weniger Vorlesen, mehr Lösen.
Bevor die App im App Store erscheint, möchte ich sie Menschen aus der
Praxis zeigen und ehrliches Feedback einholen: Würde das Ihren Klientinnen
und Klienten wirklich helfen? Was fehlt? **Dürfte ich dafür einmal für
1015 Minuten bei Ihnen in Kirchheimbolanden vorbeikommen?** Ich richte
mich ganz nach Ihrer Zeit.
Zum Datenschutz, weil das bei Behördenpost besonders zählt: Alle Briefe
bleiben verschlüsselt auf dem Handy der Nutzer, es gibt kein Konto, kein
Tracking und keine Werbung — eine Übersicht liegt als PDF bei. Die ersten
Analysen sind kostenlos, Vielnutzer zahlen später ein kleines Abo
(2,99 €/Monat).
Webseite (auch auf Arabisch, Türkisch u. a.): https://behoerdenklar.pages.dev
Mit freundlichen Grüßen
Justin Klein
Winnweiler · behoerdenbriefhelfer@gmail.com
---
## Tipps zum Versand
1. **Anhänge klein halten:** das PDF + maximal 2 Screenshots — keine
riesigen Dateien.
2. **Nach 57 Tagen ohne Antwort: einmal freundlich anrufen** (06352
7897955) und auf die Mail verweisen. Das ist normal und nicht aufdringlich.
3. Dieselbe Vorlage passt (mit angepasster Anrede/Ort) auch für:
- Haus der Diakonie Donnersbergkreis — slb.kibo@diakonie-pfalz.de
- Caritas-Zentrum Kaiserslautern (Migrationsfachdienst)
4. Beim Besuch mitnehmen: Handy/Simulator mit der App, ausgedrucktes
Factsheet, Zettel mit Webseiten-Link. Und die drei Fragen stellen:
- „Was würde Ihre Klienten davon abhalten?"
- „Würden Sie einen Berater-Modus nutzen (Briefe im Termin scannen,
pro Fall ablegen)?"
- Bei Begeisterung: „Wer entscheidet bei Ihrem Träger über solche
Anschaffungen — und würden Sie mich dort vorstellen?"
(Merke: Die Beraterin EMPFIEHLT die App ihren Klienten — dafür
braucht sie niemandes Erlaubnis. Die Lizenz kauft später die
Leitung — aber nur auf Empfehlung ihrer eigenen Berater.)

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

4287
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,34 +6,46 @@
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.3.8",
"@react-navigation/native-stack": "^7.17.10",
"expo": "^56.0.15",
"expo-calendar": "~56.0.9",
"expo-camera": "~56.0.8",
"expo-document-picker": "~56.0.4",
"expo-file-system": "~56.0.8",
"expo-image-picker": "~56.0.20",
"expo-network": "~56.0.5",
"expo-notifications": "~56.0.20",
"expo-print": "~56.0.4",
"expo-secure-store": "~56.0.4",
"expo-sharing": "~56.0.21",
"expo-speech": "~56.0.3",
"expo-status-bar": "~56.0.4",
"expo": "^57.0.0",
"expo-calendar": "~57.0.0",
"expo-camera": "~57.0.1",
"expo-crypto": "~57.0.1",
"expo-document-picker": "~57.0.0",
"expo-file-system": "~57.0.0",
"expo-image-manipulator": "~57.0.2",
"expo-image-picker": "~57.0.2",
"expo-local-authentication": "~57.0.1",
"expo-network": "~57.0.0",
"expo-notifications": "~57.0.3",
"expo-print": "~57.0.0",
"expo-secure-store": "~57.0.0",
"expo-sharing": "~57.0.3",
"expo-speech": "~57.0.0",
"expo-splash-screen": "~57.0.2",
"expo-status-bar": "~57.0.0",
"expo-symbols": "~57.0.1",
"react": "19.2.3",
"react-native": "0.85.3",
"react-native": "0.86.0",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2",
"zustand": "^5.0.14"
},
"devDependencies": {
"@types/jest": "~29.5.14",
"@types/react": "~19.2.2",
"jest": "~29.7.0",
"jest-expo": "~57.0.1",
"typescript": "~6.0.3"
},
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
"web": "expo start --web",
"test": "jest"
},
"jest": {
"preset": "jest-expo"
},
"private": true
}

61
proxy/README.md Normal file
View File

@@ -0,0 +1,61 @@
# BehördenKlar Backend-Proxy
Cloudflare Worker, der den Anthropic-API-Key serverseitig hält. Die App schickt
ihre Claude-Anfragen an diesen Worker statt direkt an Anthropic — kein API-Key
in der App, Missbrauchsschutz inklusive.
## Was der Worker macht
1. Prüft die anonyme Geräte-ID (`x-geraete-id`-Header) und setzt ein
**Tageslimit pro Gerät** durch (Standard: 20 Anfragen, siehe `wrangler.toml`)
2. Erlaubt nur die **Modelle, die die App nutzt**, und deckelt `max_tokens`
3. Leitet den Request-Body unverändert an `api.anthropic.com/v1/messages`
weiter und setzt dabei serverseitig `x-api-key` + `anthropic-version`
4. Gibt die Anthropic-Antwort unverändert zurück
## Deployment (einmalig)
```bash
cd proxy
npm install
# Bei Cloudflare anmelden (kostenloser Account reicht für den Start)
npx wrangler login
# KV-Namespace für das Rate-Limit anlegen …
npx wrangler kv namespace create RATE_LIMIT
# … und die ausgegebene ID in wrangler.toml bei [[kv_namespaces]] eintragen
# Anthropic-API-Key als Secret hinterlegen (von console.anthropic.com)
npx wrangler secret put ANTHROPIC_API_KEY
# Deployen
npm run deploy
```
Der Deploy gibt eine URL aus, z. B. `https://behoerdenklar-proxy.<account>.workers.dev`.
## App umstellen
In [`src/services/claudeClient.ts`](../src/services/claudeClient.ts) die URL eintragen:
```ts
const PROXY_URL: string | null = 'https://behoerdenklar-proxy.<account>.workers.dev';
```
Fertig — die App braucht dann keinen API-Key mehr in den Einstellungen.
## Lokal testen
```bash
npm run dev # startet den Worker auf http://localhost:8787
```
Dann in `claudeClient.ts` vorübergehend `PROXY_URL = 'http://localhost:8787'`
setzen (im Simulator; auf echtem Gerät die LAN-IP des Rechners verwenden).
## Später ergänzen (wenn Abo/IAP kommt)
- Abo-Prüfung: RevenueCat-Webhook oder Receipt-Validierung vor dem Weiterleiten
- Tageslimit je nach Abo-Stufe (Free: 3, Abo: 20+)
- Die Geräte-ID durch eine echte Nutzer-ID ersetzen

1576
proxy/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

15
proxy/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "behoerdenklar-proxy",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/workers-types": "^5.20260708.1",
"typescript": "~5.9.0",
"wrangler": "^4.0.0"
}
}

128
proxy/src/index.ts Normal file
View File

@@ -0,0 +1,128 @@
/**
* BehördenKlar Backend-Proxy — Cloudflare Worker
*
* Hält den Anthropic-API-Key serverseitig, damit kein Key in der App steckt.
* Die App schickt denselben Request-Body wie an die Anthropic Messages API;
* der Worker prüft, limitiert und leitet weiter — die Antwort geht
* unverändert zurück (die App parst sie wie eine direkte Anthropic-Antwort).
*
* Schutzmaßnahmen gegen Missbrauch des Endpunkts:
* - Nur POST, nur erlaubte Modelle, max_tokens gedeckelt
* - Tageslimit pro Geräte-ID (KV-basiert, weiches Limit)
*
* Deployment: siehe proxy/README.md
*/
interface Env {
/** Geheimnis: `wrangler secret put ANTHROPIC_API_KEY` */
ANTHROPIC_API_KEY: string;
/** KV-Namespace für das Rate-Limit (Binding in wrangler.toml) */
RATE_LIMIT: KVNamespace;
/** Erlaubte Anfragen pro Gerät und Tag (in wrangler.toml unter [vars]) */
TAGES_LIMIT: string;
/** Erlaubte Anfragen pro IP-Adresse und Tag (in wrangler.toml unter [vars]) */
TAGES_LIMIT_IP: string;
}
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
const ANTHROPIC_VERSION = '2023-06-01';
/** Nur Modelle, die die App tatsächlich nutzt — verhindert, dass ein
* extrahierter Endpunkt als Gratis-Zugang für teure Modelle dient. */
const ERLAUBTE_MODELLE = new Set([
'claude-sonnet-5', // Brief-Analyse (Vision)
'claude-haiku-4-5', // Übersetzung & Antwort-Entwürfe
]);
const MAX_TOKENS_OBERGRENZE = 16000;
/** Fehlerantwort im Anthropic-Format, damit die App sie normal verarbeitet. */
function fehler(status: number, typ: string, meldung: string): Response {
return Response.json(
{ type: 'error', error: { type: typ, message: meldung } },
{ status }
);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return fehler(405, 'invalid_request_error', 'Nur POST erlaubt.');
}
// Geräte-ID der App (anonym, dient nur dem Tageslimit)
const geraeteId = request.headers.get('x-geraete-id');
if (!geraeteId || !/^g_[a-z0-9]{24}$/.test(geraeteId)) {
return fehler(400, 'invalid_request_error', 'Fehlende oder ungültige Geräte-ID.');
}
let body: Record<string, unknown>;
try {
body = await request.json();
} catch {
return fehler(400, 'invalid_request_error', 'Request-Body ist kein gültiges JSON.');
}
if (typeof body.model !== 'string' || !ERLAUBTE_MODELLE.has(body.model)) {
return fehler(400, 'invalid_request_error', 'Dieses Modell ist nicht erlaubt.');
}
// max_tokens deckeln statt ablehnen — schützt vor Kosten-Missbrauch
if (typeof body.max_tokens !== 'number' || body.max_tokens > MAX_TOKENS_OBERGRENZE) {
body.max_tokens = MAX_TOKENS_OBERGRENZE;
}
// Die App streamt nicht; Streaming würde das Durchreichen verkomplizieren
if (body.stream) {
return fehler(400, 'invalid_request_error', 'Streaming wird nicht unterstützt.');
}
// Tageslimits prüfen (weiche Limits: KV ist eventually consistent,
// parallele Anfragen können das Limit minimal überschreiten — okay).
// Zwei Ebenen: pro Gerät (normale Nutzung) und pro IP-Adresse (dämmt
// Angreifer ein, die sich beliebig neue Geräte-IDs ausdenken). Das
// IP-Limit ist bewusst höher, weil sich viele Nutzer eine IP teilen
// können (Familien-WLAN, Mobilfunk/CGNAT).
const heute = new Date().toISOString().slice(0, 10);
const ip = request.headers.get('cf-connecting-ip') ?? 'unbekannt';
const kvKey = `rl:${geraeteId}:${heute}`;
const kvKeyIp = `rlip:${ip}:${heute}`;
const limit = parseInt(env.TAGES_LIMIT || '20', 10);
const limitIp = parseInt(env.TAGES_LIMIT_IP || '100', 10);
const [bisher, bisherIp] = (
await Promise.all([env.RATE_LIMIT.get(kvKey), env.RATE_LIMIT.get(kvKeyIp)])
).map((wert) => parseInt(wert ?? '0', 10));
if (bisher >= limit || bisherIp >= limitIp) {
return fehler(
429,
'rate_limit_error',
'Tageslimit erreicht. Bitte versuchen Sie es morgen erneut.'
);
}
// An Anthropic weiterleiten — Header werden frisch gebaut, nichts vom
// Client wird durchgereicht (außer dem geprüften Body)
const antwort = await fetch(ANTHROPIC_URL, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': env.ANTHROPIC_API_KEY,
'anthropic-version': ANTHROPIC_VERSION,
},
body: JSON.stringify(body),
});
// Nur zählen, wenn die Anfrage Anthropic erreicht hat (5xx kostet kein Kontingent)
if (antwort.status < 500) {
await Promise.all([
env.RATE_LIMIT.put(kvKey, String(bisher + 1), { expirationTtl: 60 * 60 * 48 }),
env.RATE_LIMIT.put(kvKeyIp, String(bisherIp + 1), { expirationTtl: 60 * 60 * 48 }),
]);
}
return new Response(antwort.body, {
status: antwort.status,
headers: {
'content-type': antwort.headers.get('content-type') ?? 'application/json',
},
});
},
} satisfies ExportedHandler<Env>;

12
proxy/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"types": ["@cloudflare/workers-types"]
},
"include": ["src"]
}

14
proxy/wrangler.toml Normal file
View File

@@ -0,0 +1,14 @@
name = "behoerdenklar-proxy"
main = "src/index.ts"
compatibility_date = "2026-07-01"
# 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)
[vars]
TAGES_LIMIT = "20"
TAGES_LIMIT_IP = "100"
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "7577adef8ceb4ed6bfaeef81a1d496af"

Binary file not shown.

View File

@@ -0,0 +1,40 @@
# Nachweis: Auftragsverarbeitungsverträge (AVV / DPA)
Dokumentation für die Rechenschaftspflicht (Art. 5 Abs. 2, Art. 28 DSGVO) —
z. B. bei Rückfragen von Kooperationspartnern, Aufsichtsbehörden oder bei der
anwaltlichen Prüfung vorlegen.
Geprüft am: **16.07.2026** (bei Gelegenheit jährlich neu prüfen)
## Verantwortlicher
Justin Klein, Am Schwimmbad 10, 67722 Winnweiler
Konto-E-Mail bei beiden Anbietern: einkauf.justin@gmail.com
## Auftragsverarbeiter 1: Anthropic PBC (KI-Analyse)
| Punkt | Detail |
|---|---|
| Zweck | KI-Analyse der Brieffotos (Claude API) |
| Vertragsgrundlage | Commercial Terms of Service — das **Data Processing Addendum (DPA) ist automatisch Bestandteil**; mit Annahme der Commercial Terms (Konto-Erstellung auf console.anthropic.com) gilt der AVV. Keine separate Unterschrift erforderlich. |
| Drittlandtransfer | EU-Standardvertragsklauseln (SCCs) im DPA enthalten; zusätzlich EU-U.S. Data Privacy Framework |
| Speicherdauer | API-Eingaben max. 30 Tage (Missbrauchserkennung), kein Training mit API-Daten |
| Quellen | https://privacy.claude.com/en/articles/7996862 · https://www.anthropic.com/legal/commercial-terms · https://www.anthropic.com/legal/data-processing-addendum |
## Auftragsverarbeiter 2: Cloudflare, Inc. (Proxy-Server + Webseite)
| Punkt | Detail |
|---|---|
| Zweck | Weiterleitung der Analyse-Anfragen (Worker), Rate-Limit-Zähler (KV), Hosting der Webseite (Pages) |
| Vertragsgrundlage | Self-Serve Subscription Agreement — das **Customer DPA ist per Verweis automatisch Bestandteil**, auch für kostenlose Konten. Keine separate Unterschrift erforderlich. |
| Drittlandtransfer | EU-SCCs im DPA enthalten; Cloudflare ist zudem unter dem EU-U.S. Data Privacy Framework zertifiziert |
| Quellen | https://www.cloudflare.com/cloudflare-customer-dpa/ · https://www.cloudflare.com/trust-hub/gdpr/ |
## Noch zu tun
- [ ] Aktuelle DPA-PDFs beider Anbieter herunterladen und in diesem Ordner
ablegen (Versionsstand einfrieren — Webseiten ändern sich)
- [ ] Bei der anwaltlichen Prüfung der Datenschutzerklärung diesen Nachweis
mit vorlegen
- [ ] Wenn später ein Newsletter-Dienst (z. B. Brevo) dazukommt: dessen AVV
hier ergänzen

View File

@@ -0,0 +1,280 @@
const {
Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType,
Table, TableRow, TableCell, WidthType, BorderStyle, ShadingType,
Numbering, LevelFormat, PageBreak,
} = require('docx');
const fs = require('fs');
// --- Markenfarben ---
const BLAU = '1A365D'; // primär (dunkles Blau)
const BLAU_HELL = 'E8EEF5';
const GRUEN = '276749';
const GELB = 'B7791F';
const GRAU = '5A6472';
const GRAU_HELL = 'F4F6F8';
const TINTE = '1A202C';
const WEISS = 'FFFFFF';
const FONT = 'Calibri';
// ---------- Hilfsfunktionen ----------
function h1(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_1,
spacing: { before: 320, after: 140 },
children: [new TextRun({ text, bold: true, size: 30, color: BLAU, font: FONT })],
});
}
function absatz(runs, opts = {}) {
return new Paragraph({
spacing: { after: opts.after ?? 140, line: 276 },
alignment: opts.align,
children: Array.isArray(runs) ? runs : [new TextRun({ text: runs, size: 21, color: TINTE, font: FONT })],
});
}
function bullet(text, opts = {}) {
return new Paragraph({
numbering: { reference: 'punkte', level: 0 },
spacing: { after: 90, line: 264 },
children: [
opts.fett
? new TextRun({ text: opts.fett + ' ', bold: true, size: 21, color: TINTE, font: FONT })
: null,
new TextRun({ text, size: 21, color: TINTE, font: FONT }),
].filter(Boolean),
});
}
// ---------- Tabelle: Schutzmaßnahmen ----------
function zelle(text, { fett, breite, farbe, textColor, size } = {}) {
return new TableCell({
width: { size: breite, type: WidthType.DXA },
margins: { top: 90, bottom: 90, left: 130, right: 130 },
shading: farbe ? { type: ShadingType.CLEAR, fill: farbe, color: 'auto' } : undefined,
children: [new Paragraph({
spacing: { after: 0, line: 252 },
children: [new TextRun({ text, bold: fett, size: size ?? 20, color: textColor ?? TINTE, font: FONT })],
})],
});
}
function massnahmeTabelle() {
const B1 = 3100, B2 = 6260; // Summe = 9360 (Textbreite bei 2,5cm Rändern)
const kopf = new TableRow({
tableHeader: true,
children: [
zelle('Maßnahme', { fett: true, breite: B1, farbe: BLAU, textColor: WEISS }),
zelle('Was das konkret bedeutet', { fett: true, breite: B2, farbe: BLAU, textColor: WEISS }),
],
});
const daten = [
['Lokale Verschlüsselung', 'Das gesamte Brief-Archiv ist mit AES-256-GCM verschlüsselt. Der Schlüssel liegt im Sicherheitsspeicher des Geräts (iOS Keychain / Android Keystore) und verlässt es nie.'],
['App-Sperre (optional)', 'Face ID / Fingerabdruck / Geräte-Code beim Öffnen der App.'],
['Sichtschutz', 'Im App-Umschalter wird der Inhalt verdeckt — keine Brief-Vorschau.'],
['Auto-Löschen (optional)', 'Briefe werden nach 30 oder 90 Tagen automatisch entfernt.'],
['„Alles löschen"', 'Ein Tipp entfernt sämtliche Daten inklusive Verschlüsselungs-Schlüssel.'],
['Kein Konto', 'Keine Registrierung, keine E-Mail, kein Passwort nötig.'],
['Kein Tracking', 'Keine Analyse- oder Werbe-Bausteine, keine Werbung, keine Datenweitergabe an Dritte.'],
['Einwilligung zuerst', 'Vor dem ersten Scan wird verständlich erklärt und aktiv zugestimmt (Art. 6 & 9 DSGVO).'],
['Server-Härtung', 'KI-Zugang nur serverseitig, Anfrage-Limits pro Gerät und pro IP-Adresse.'],
];
const zeilen = daten.map(([a, b], i) => new TableRow({
children: [
zelle(a, { fett: true, breite: B1, farbe: i % 2 ? GRAU_HELL : WEISS }),
zelle(b, { breite: B2, farbe: i % 2 ? GRAU_HELL : WEISS }),
],
}));
return new Table({
columnWidths: [B1, B2],
width: { size: B1 + B2, type: WidthType.DXA },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: 'D5DBE2' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: 'D5DBE2' },
left: { style: BorderStyle.SINGLE, size: 2, color: 'D5DBE2' },
right: { style: BorderStyle.SINGLE, size: 2, color: 'D5DBE2' },
insideHorizontal: { style: BorderStyle.SINGLE, size: 2, color: 'D5DBE2' },
insideVertical: { style: BorderStyle.SINGLE, size: 2, color: 'D5DBE2' },
},
rows: [kopf, ...zeilen],
});
}
// ---------- Datenfluss-Schritt (nummerierte Box) ----------
function flussSchritt(nr, titel, text) {
return new Table({
columnWidths: [720, 8640],
width: { size: 9360, type: WidthType.DXA },
borders: {
top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.NONE },
left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE },
insideHorizontal: { style: BorderStyle.NONE }, insideVertical: { style: BorderStyle.NONE },
},
rows: [new TableRow({ children: [
new TableCell({
width: { size: 720, type: WidthType.DXA },
verticalAlign: 'center',
children: [new Paragraph({
alignment: AlignmentType.CENTER, spacing: { after: 0 },
children: [new TextRun({ text: String(nr), bold: true, size: 34, color: BLAU, font: FONT })],
})],
}),
new TableCell({
width: { size: 8640, type: WidthType.DXA },
margins: { top: 40, bottom: 40, left: 120 },
children: [new Paragraph({
spacing: { after: 40, line: 264 },
children: [
new TextRun({ text: titel + ' ', bold: true, size: 21, color: TINTE, font: FONT }),
new TextRun({ text, size: 21, color: GRAU, font: FONT }),
],
})],
}),
] })],
});
}
function trennlinie() {
return new Paragraph({
spacing: { before: 60, after: 200 },
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: BLAU } },
children: [new TextRun({ text: '', size: 2 })],
});
}
// ---------- Dokument ----------
const doc = new Document({
creator: 'BehördenKlar',
title: 'BehördenKlar — Datenschutz & Sicherheit',
numbering: {
config: [{
reference: 'punkte',
levels: [{
level: 0, format: LevelFormat.BULLET, text: '•',
alignment: AlignmentType.LEFT,
style: { run: { color: BLAU }, paragraph: { indent: { left: 360, hanging: 220 } } },
}],
}],
},
styles: { default: { document: { run: { font: FONT, size: 21, color: TINTE } } } },
sections: [{
properties: {
page: {
size: { width: 11906, height: 16838 }, // A4
margin: { top: 1250, bottom: 1250, left: 1418, right: 1418 }, // ~2,5 cm
},
},
children: [
// ===== Kopf =====
new Paragraph({
spacing: { after: 40 },
children: [new TextRun({ text: '📬 BehördenKlar', bold: true, size: 40, color: BLAU, font: FONT })],
}),
new Paragraph({
spacing: { after: 60 },
children: [new TextRun({ text: 'Datenschutz & Sicherheit auf einen Blick', size: 26, color: TINTE, font: FONT })],
}),
new Paragraph({
spacing: { after: 40 },
children: [new TextRun({ text: 'Für Beratungsstellen, Sozialverbände und Kooperationspartner', italics: true, size: 21, color: GRAU, font: FONT })],
}),
new Paragraph({
spacing: { after: 120 },
children: [new TextRun({ text: 'Stand: 15.07.2026 · behoerdenbriefhelfer@gmail.com · behoerdenklar.pages.dev', size: 18, color: GRAU, font: FONT })],
}),
trennlinie(),
// ===== Was ist die App =====
absatz([
new TextRun({ text: 'BehördenKlar hilft Menschen, Behördenbriefe zu verstehen: ', size: 21, color: TINTE, font: FONT }),
new TextRun({ text: 'Brief fotografieren, Erklärung in einfacher Sprache erhalten — mit Fristen, Checkliste und Antwort-Hilfe, auf Wunsch in 9 weiteren Sprachen. Weil Behördenbriefe hochsensible Daten enthalten, wurde die App von Grund auf datensparsam gebaut. Dieses Dokument erklärt transparent, was mit den Daten passiert — auch die Punkte, die noch offen sind.', size: 21, color: TINTE, font: FONT }),
], { after: 160 }),
// ===== Kernaussage-Box =====
new Table({
columnWidths: [9360],
width: { size: 9360, type: WidthType.DXA },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: BLAU_HELL },
bottom: { style: BorderStyle.SINGLE, size: 2, color: BLAU_HELL },
left: { style: BorderStyle.SINGLE, size: 18, color: BLAU },
right: { style: BorderStyle.SINGLE, size: 2, color: BLAU_HELL },
},
rows: [new TableRow({ children: [new TableCell({
width: { size: 9360, type: WidthType.DXA },
shading: { type: ShadingType.CLEAR, fill: BLAU_HELL, color: 'auto' },
margins: { top: 130, bottom: 130, left: 170, right: 170 },
children: [new Paragraph({
spacing: { after: 0, line: 276 },
children: [
new TextRun({ text: 'Das Wichtigste in einem Satz: ', bold: true, size: 21, color: BLAU, font: FONT }),
new TextRun({ text: 'Es gibt kein Nutzerkonto und keinen zentralen Datenspeicher — die Briefe Ihrer Klientinnen und Klienten liegen verschlüsselt auf deren eigenem Handy, nicht bei uns.', size: 21, color: TINTE, font: FONT }),
],
})],
})] })],
}),
// ===== Datenfluss =====
h1('Der Datenfluss beim Scannen'),
flussSchritt(1, 'Foto entsteht auf dem Gerät', 'und wird vor dem Versand verkleinert.'),
flussSchritt(2, 'Übertragung TLS-verschlüsselt', 'an unseren Vermittlungsserver (Cloudflare). Dieser leitet nur weiter und speichert keine Briefinhalte — nur einen anonymen Tageszähler pro Gerät für 48 Stunden.'),
flussSchritt(3, 'KI-Analyse bei Anthropic (USA)', 'Verarbeitung nur zur Analyse, Löschung nach spätestens 30 Tagen, keine Nutzung zum KI-Training.'),
flussSchritt(4, 'Ergebnis zurück aufs Gerät', 'dort endet der Weg. Keine Kopie bei uns.'),
// ===== Schutzmaßnahmen =====
h1('Schutzmaßnahmen in der App'),
absatz([new TextRun({ text: 'Alle folgenden Maßnahmen sind umgesetzt und technisch überprüfbar:', size: 21, color: GRAU, font: FONT, italics: true })], { after: 120 }),
massnahmeTabelle(),
// ===== Ehrliche Punkte (auf neuer Seite) =====
new Paragraph({
heading: HeadingLevel.HEADING_1,
pageBreakBefore: true,
spacing: { before: 320, after: 140 },
children: [new TextRun({ text: 'Woran wir transparent erinnern', bold: true, size: 30, color: BLAU, font: FONT })],
}),
absatz([new TextRun({ text: 'Vertrauen entsteht durch Ehrlichkeit — deshalb benennen wir auch das, was nicht perfekt ist:', size: 21, color: GRAU, font: FONT, italics: true })], { after: 120 }),
bullet('Die KI-Analyse läuft bei Anthropic (USA), auf Grundlage des EU-U.S. Data Privacy Framework bzw. EU-Standardvertragsklauseln. Eine Verarbeitung in der EU ist beim Anbieter derzeit nicht verfügbar; als Ausbaupfad prüfen wir den Betrieb über EU-Rechenzentren (AWS Frankfurt).', { fett: 'Verarbeitung in den USA:' }),
bullet('Anthropic hält Anfragen bis zu 30 Tage zur Missbrauchserkennung vor — danach automatische Löschung. Wir sagen das offen, statt „Ihre Daten verlassen nie das Handy" zu behaupten.', { fett: '30-Tage-Fenster:' }),
bullet('Die App erklärt Briefe, ersetzt aber keine Beratung — sie ist als Ergänzung Ihrer Arbeit gedacht, nicht als Ersatz.', { fett: 'Keine Rechtsberatung:' }),
bullet('Dieses Dokument ist eine Selbstauskunft, kein Zertifikat. Die vollständige Datenschutzerklärung ist öffentlich unter behoerdenklar.pages.dev/datenschutz.', { fett: 'Transparenz:' }),
// ===== Nutzen für die Beratung =====
h1('Warum das für Ihre Beratungsarbeit interessant ist'),
bullet('Klientinnen und Klienten verstehen Briefe bereits vor dem Termin — Ihre Beratungszeit fließt in die Lösung, nicht ins Vorlesen.'),
bullet('9 Übersetzungssprachen (u. a. Türkisch, Arabisch, Ukrainisch, Farsi) — die deutschen Fachbegriffe bleiben sichtbar und wiedererkennbar.'),
bullet('Große Schrift, große Tasten, einfache Sprache (A2/B1), Vorlese-Funktion — gebaut für Menschen, für die normale Apps zu kompliziert sind.'),
// ===== Abschluss-Box =====
new Paragraph({ spacing: { before: 200 } }),
new Table({
columnWidths: [9360],
width: { size: 9360, type: WidthType.DXA },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: GRUEN },
bottom: { style: BorderStyle.SINGLE, size: 2, color: GRUEN },
left: { style: BorderStyle.SINGLE, size: 18, color: GRUEN },
right: { style: BorderStyle.SINGLE, size: 2, color: GRUEN },
},
rows: [new TableRow({ children: [new TableCell({
width: { size: 9360, type: WidthType.DXA },
shading: { type: ShadingType.CLEAR, fill: 'EAF6EE', color: 'auto' },
margins: { top: 150, bottom: 150, left: 170, right: 170 },
children: [
new Paragraph({ spacing: { after: 60, line: 276 }, children: [
new TextRun({ text: 'Sprechen Sie uns an. ', bold: true, size: 22, color: GRUEN, font: FONT }),
new TextRun({ text: 'Wir zeigen die App gern persönlich und beantworten jede Datenschutz-Rückfrage.', size: 21, color: TINTE, font: FONT }),
] }),
new Paragraph({ spacing: { after: 0 }, children: [
new TextRun({ text: '📧 behoerdenbriefhelfer@gmail.com 🌐 behoerdenklar.pages.dev', size: 21, color: TINTE, font: FONT, bold: true }),
] }),
],
})] })],
}),
],
}],
});
Packer.toBuffer(doc).then((buf) => {
const out = process.argv[2] || 'BehoerdenKlar-Datenschutz.docx';
fs.writeFileSync(out, buf);
console.log('geschrieben:', out);
});

View File

@@ -0,0 +1,74 @@
# BehördenKlar — Datenschutz auf einen Blick
**Für Beratungsstellen, Sozialverbände und Kooperationspartner**
Stand: 15.07.2026 · Kontakt: behoerdenbriefhelfer@gmail.com · behoerdenklar.pages.dev
> BehördenKlar hilft Menschen, Behördenbriefe zu verstehen: Brief fotografieren,
> Erklärung in einfacher Sprache erhalten — mit Fristen, Checkliste und
> Antwort-Hilfe, auf Wunsch in 9 weiteren Sprachen.
>
> Weil Behördenbriefe hochsensible Daten enthalten, wurde die App von Grund
> auf datensparsam gebaut. Dieses Dokument erklärt transparent, was mit den
> Daten passiert — einschließlich der Punkte, die noch offen sind.
---
## Das Wichtigste in einem Satz
**Es gibt kein Nutzerkonto und keinen zentralen Datenspeicher — die Briefe
Ihrer Klientinnen und Klienten liegen verschlüsselt auf deren eigenem Handy,
nicht bei uns.**
## Der Datenfluss beim Scannen (vollständig)
1. **Foto entsteht auf dem Gerät** und wird vor dem Versand verkleinert.
2. **Übertragung TLS-verschlüsselt** an unseren Vermittlungsserver
(Cloudflare Workers). Dieser leitet nur weiter — **er speichert keine
Briefinhalte** (gespeichert wird ausschließlich ein anonymer Tageszähler
pro Gerät für 48 h, als Missbrauchsschutz).
3. **KI-Analyse bei Anthropic PBC (USA)**, Betreiber des Modells „Claude":
Verarbeitung nur zur Analyse, **Löschung nach spätestens 30 Tagen, keine
Nutzung zum KI-Training.**
4. **Ergebnis zurück aufs Gerät** — dort endet der Weg. Keine Kopie bei uns.
## Schutzmaßnahmen in der App (implementiert und überprüfbar)
| Maßnahme | Detail |
|---|---|
| **Lokale Verschlüsselung** | Das gesamte Brief-Archiv ist mit AES-256-GCM verschlüsselt; der Schlüssel liegt im Sicherheitsspeicher des Geräts (iOS Keychain / Android Keystore) und verlässt es nie |
| **App-Sperre** (optional) | Face ID / Fingerabdruck / Geräte-Code beim Öffnen der App |
| **Sichtschutz** | Im App-Umschalter wird der Inhalt verdeckt — keine Brief-Vorschau |
| **Auto-Löschen** (optional) | Briefe werden nach 30 oder 90 Tagen automatisch entfernt |
| **„Alles löschen"** | Ein Tipp entfernt sämtliche Daten inklusive Verschlüsselungs-Schlüssel |
| **Kein Konto** | Keine Registrierung, keine E-Mail, kein Passwort nötig |
| **Kein Tracking** | Keine Analyse-/Werbe-SDKs, keine Werbung, keine Datenweitergabe an Dritte |
| **Einwilligung zuerst** | Vor dem ersten Scan wird verständlich erklärt und aktiv zugestimmt (Art. 6 Abs. 1 lit. a, Art. 9 Abs. 2 lit. a DSGVO) |
| **Server-Härtung** | API-Zugang nur serverseitig, Anfrage-Limits pro Gerät und IP-Adresse |
## Woran wir transparent erinnern (die ehrlichen Punkte)
- **Verarbeitung in den USA:** Die KI-Analyse läuft bei Anthropic (USA), auf
Grundlage des EU-U.S. Data Privacy Framework bzw. EU-Standardvertragsklauseln.
Eine Verarbeitung in der EU ist beim Anbieter derzeit nicht verfügbar; wir
beobachten das und prüfen als Ausbaupfad den Betrieb über EU-Rechenzentren
(AWS Bedrock, Region Frankfurt).
- **30-Tage-Fenster:** Anthropic hält Anfragen bis zu 30 Tage zur
Missbrauchserkennung vor — danach automatische Löschung. Wir sagen das offen,
statt „Ihre Daten verlassen nie das Handy" zu behaupten.
- **Keine Rechtsberatung:** Die App erklärt Briefe, ersetzt aber keine
Beratung — sie ist als *Ergänzung* Ihrer Arbeit gedacht, nicht als Ersatz.
- **Dieses Dokument ist eine Selbstauskunft**, kein Zertifikat. Die
vollständige Datenschutzerklärung ist öffentlich:
https://behoerdenklar.pages.dev/datenschutz
## Warum das für Ihre Beratungsarbeit interessant ist
- Klientinnen und Klienten verstehen Briefe **vor** dem Termin — Ihre
Beratungszeit geht in die Lösung, nicht ins Vorlesen.
- 9 Übersetzungssprachen (u. a. Türkisch, Arabisch, Ukrainisch, Farsi) —
die deutschen Fachbegriffe bleiben sichtbar und wiedererkennbar.
- Große Schrift, große Tasten, einfache Sprache (A2/B1), Vorlese-Funktion —
gebaut für Menschen, für die normale Apps zu kompliziert sind.
**Sprechen Sie uns an — wir zeigen die App gern persönlich und beantworten
jede Datenschutz-Rückfrage:** behoerdenbriefhelfer@gmail.com

View File

@@ -0,0 +1,127 @@
# Datenschutzerklärung — BehördenKlar
> **⚠️ ENTWURF — vor Veröffentlichung prüfen (lassen).**
> Dieser Text ist eine sorgfältige Vorlage, aber keine Rechtsberatung.
> Vor dem App-Store-Release von einer Anwältin/einem Anwalt für Datenschutzrecht
> oder einem Dienst wie eRecht24/activeMind prüfen lassen. Alle Stellen in
> [ECKIGEN KLAMMERN] müssen ausgefüllt werden.
**Stand: 14.07.2026**
## 1. Verantwortlicher
Justin Klein
Am Schwimmbad 10
67722 Winnweiler
E-Mail: behoerdenbriefhelfer@gmail.com
## 2. Was macht die App?
BehördenKlar hilft Ihnen, Behördenbriefe zu verstehen: Sie fotografieren einen
Brief, und eine Künstliche Intelligenz (KI) erklärt den Inhalt in einfacher
Sprache, findet Fristen und Termine und hilft beim Antworten.
## 3. Welche Daten werden verarbeitet?
### a) Brieffotos und PDF-Dateien (nur mit Ihrer Einwilligung)
Wenn Sie einen Brief scannen, wird das Foto bzw. die PDF-Datei zur Analyse
übertragen an:
1. **Unseren Server** (Cloudflare Workers, Anbieter: Cloudflare, Inc., USA) —
er leitet die Anfrage nur weiter und **speichert den Briefinhalt nicht**.
2. **Anthropic PBC (USA)** — 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.
**Wichtig:** Behördenbriefe können sehr persönliche und besondere Kategorien
von Daten enthalten (z. B. Angaben zu Sozialleistungen, Gesundheit oder
finanzieller Situation). Die Analyse erfolgt deshalb nur nach Ihrer
**ausdrücklichen Einwilligung**, die Sie vor dem ersten Scan erteilen
(Art. 6 Abs. 1 lit. a, Art. 9 Abs. 2 lit. a DSGVO). Sie können die
Einwilligung jederzeit widerrufen, indem Sie in den Einstellungen
„Alle Daten löschen" wählen.
### b) Analyse-Ergebnisse, Übersetzungen, Antwort-Entwürfe
Alle Ergebnisse werden **ausschließlich lokal auf Ihrem Gerät** gespeichert —
zusätzlich verschlüsselt (AES-256-GCM; Schlüssel im Sicherheitsspeicher des
Geräts). Wir betreiben keine Nutzerkonten und keine Cloud-Speicherung Ihrer
Briefe. Optional: App-Sperre (Face ID / Geräte-Code) und automatisches
Löschen nach 30/90 Tagen in den Einstellungen.
### c) Anonyme Geräte-Kennung
Die App erzeugt eine zufällige, anonyme Kennung (z. B. „g_a1b2…"). Sie enthält
keine Angaben über Sie oder Ihr Gerät und dient ausschließlich dazu, ein
tägliches Nutzungslimit pro Installation durchzusetzen (Schutz vor Missbrauch,
Art. 6 Abs. 1 lit. f DSGVO). Sie wird zusammen mit einem Tageszähler für
maximal 48 Stunden auf unserem Server gespeichert.
### d) Kalender, Erinnerungen, Vorlesen
Kalendereinträge und Frist-Erinnerungen werden nur auf Ihrem Gerät angelegt
(mit Ihrer Erlaubnis über die iOS-/Android-Berechtigungen). Die
Vorlese-Funktion nutzt die Sprachausgabe Ihres Geräts. Dabei werden keine
Daten an uns übertragen.
## 4. Übermittlung in die USA
Anthropic PBC und Cloudflare, Inc. haben ihren Sitz in den USA. Die
Übermittlung stützt sich auf [ZU PRÜFEN: EU-U.S. Data Privacy Framework —
Zertifizierung von Anthropic und Cloudflare verifizieren — und/oder
EU-Standardvertragsklauseln].
## 5. Speicherdauer
- Brieffotos bei Anthropic: maximal 30 Tage, danach automatische Löschung
- Anonyme Geräte-Kennung + Tageszähler auf unserem Server: maximal 48 Stunden
- Daten auf Ihrem Gerät: bis Sie sie löschen („Alle Daten löschen" in den
Einstellungen oder Deinstallation der App)
## 6. Ihre Rechte
Sie haben nach der DSGVO das Recht auf Auskunft (Art. 15), Berichtigung
(Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18),
Datenübertragbarkeit (Art. 20) und Widerspruch (Art. 21). Außerdem können Sie
sich bei einer Datenschutz-Aufsichtsbehörde beschweren (Art. 77 DSGVO),
z. B. beim Landesbeauftragten für den Datenschutz und die Informationsfreiheit
Rheinland-Pfalz.
Da wir keine Nutzerkonten führen, liegen die meisten Daten nur auf Ihrem
Gerät — dort können Sie sie selbst einsehen und löschen. Für Anfragen zu den
serverseitig verarbeiteten Daten wenden Sie sich an:
behoerdenbriefhelfer@gmail.com
## 7. Warteliste / Launch-Benachrichtigung (Webseite)
Auf unserer Webseite können Sie sich für die Warteliste anmelden. Dabei
verarbeiten wir Ihre E-Mail-Adresse auf Grundlage Ihrer Einwilligung
(Art. 6 Abs. 1 lit. a DSGVO), ausschließlich für die Launch-Benachrichtigung
und Informationen zur App. Abmeldung jederzeit möglich; die Adresse wird dann
gelöscht.
> [NACH BREVO/MAILERLITE-EINRICHTUNG ERGÄNZEN: Anbieter des
> Newsletter-Dienstes als Auftragsverarbeiter benennen, z. B. „Brevo GmbH,
> Köpenicker Str. 126, 10179 Berlin" + Double-Opt-in-Verfahren erwähnen.]
## 8. Reichweitenmessung (Webseite)
Auf der Webseite nutzen wir Cloudflare Web Analytics (Cloudflare, Inc.,
USA) — Besucherstatistik **ohne Cookies** und ohne Verfolgung einzelner
Personen; erfasst werden nur zusammengefasste Werte (Seitenaufrufe,
Herkunftsland, Browser). Rechtsgrundlage: berechtigtes Interesse
(Art. 6 Abs. 1 lit. f DSGVO).
## 9. Keine Rechtsberatung
BehördenKlar erklärt Briefe verständlich, ersetzt aber keine Rechtsberatung.
Bei rechtlich wichtigen Entscheidungen (z. B. Widerspruch) wenden Sie sich an
eine Beratungsstelle oder eine Anwältin/einen Anwalt.
## 10. Änderungen
Wir passen diese Datenschutzerklärung an, wenn sich die App oder die
Rechtslage ändert. Die aktuelle Fassung finden Sie immer in der App unter
Einstellungen → Datenschutz.

View File

@@ -1,22 +1,23 @@
/**
* Dringlichkeits-Ampel als gut sichtbares Banner.
* Farbe + Symbol + Text — nicht nur Farbe (Barrierefreiheit: Farbenblindheit).
* Farbe + Icon + Text — nicht nur Farbe (Barrierefreiheit: Farbenblindheit).
*/
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { AmpelStatus } from '../utils/ampel';
import { abstand, schrift } from '../theme';
import { Ikone, IkonenName } from './Ikone';
const SYMBOL: Record<string, string> = { rot: '⚠️', gelb: '', gruen: '' };
const IKONE: Record<string, IkonenName> = { rot: 'warnung', gelb: 'wecker', gruen: 'info' };
export function Ampel({ status }: { status: AmpelStatus }) {
return (
<View
style={[styles.banner, { backgroundColor: status.hintergrund, borderColor: status.farbe }]}
style={[styles.banner, { backgroundColor: status.hintergrund, borderLeftColor: status.farbe }]}
accessibilityRole="alert"
accessibilityLabel={`Dringlichkeit: ${status.text}`}
>
<Text style={styles.symbol}>{SYMBOL[status.stufe]}</Text>
<Ikone name={IKONE[status.stufe]} groesse={26} farbe={status.farbe} />
<Text style={[styles.text, { color: status.farbe }]}>{status.text}</Text>
</View>
);
@@ -26,11 +27,10 @@ const styles = StyleSheet.create({
banner: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 2,
borderLeftWidth: 6,
borderRadius: 12,
padding: abstand.m,
gap: abstand.s,
},
symbol: { fontSize: 28 },
text: { flex: 1, fontSize: schrift.gross, fontWeight: '700' },
});

View File

@@ -0,0 +1,113 @@
/**
* Schützt den App-Inhalt auf zwei Ebenen:
* 1. App-Sperre (optional, Einstellungen): Beim Öffnen und nach jedem
* Wechsel in den Hintergrund muss das Gerät entsperrt werden
* (Face ID / Touch ID / Geräte-Code).
* 2. Sichtschutz (immer): Im App-Umschalter wird der Inhalt verdeckt,
* damit Briefe dort nicht als Vorschau lesbar sind.
*
* Der Inhalt bleibt gemountet (Navigation geht nicht verloren) — die
* Schutzschichten legen sich als Overlay darüber.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
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';
export function AppSchutz({ children }: { children: React.ReactNode }) {
const [entsperrt, setEntsperrt] = useState(false);
const [bereit, setBereit] = useState(false);
const [verdeckt, setVerdeckt] = useState(false);
const authLaeuft = useRef(false);
const entsperren = useCallback(async () => {
if (authLaeuft.current) return;
authLaeuft.current = true;
try {
const ergebnis = await LocalAuthentication.authenticateAsync({
promptMessage: 'BehördenKlar entsperren',
cancelLabel: 'Abbrechen',
});
if (ergebnis.success) setEntsperrt(true);
} finally {
authLaeuft.current = false;
}
}, []);
// Beim Start: Sperre-Einstellung prüfen und ggf. direkt entsperren lassen
useEffect(() => {
holeAppSperre().then((aktiv) => {
if (!aktiv) {
setEntsperrt(true);
} else {
entsperren();
}
setBereit(true);
});
}, [entsperren]);
// Hintergrund-Wechsel: Sichtschutz an/aus, Sperre ggf. neu scharf stellen.
// Die Einstellung wird hier frisch gelesen, damit ein Umschalten in den
// Einstellungen sofort wirkt.
useEffect(() => {
const abo = AppState.addEventListener('change', (status: AppStateStatus) => {
if (status === 'active') {
setVerdeckt(false);
} else {
setVerdeckt(true);
if (status === 'background') {
holeAppSperre().then((aktiv) => {
if (aktiv) setEntsperrt(false);
});
}
}
});
return () => abo.remove();
}, []);
if (!bereit) return <View style={styles.deckschicht} />;
return (
<View style={{ flex: 1 }}>
{children}
{!entsperrt && (
<View style={[StyleSheet.absoluteFill, styles.deckschicht, styles.sperrschirm]}>
<Text style={styles.symbol}>🔒</Text>
<Text style={styles.titel}>BehördenKlar ist gesperrt</Text>
<Text style={styles.text}>Ihre Briefe sind geschützt.</Text>
<GrossButton titel="Entsperren" onPress={entsperren} />
</View>
)}
{entsperrt && verdeckt && (
<View style={[StyleSheet.absoluteFill, styles.deckschicht, styles.sichtschutz]}>
<Text style={styles.symbol}>📬</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
deckschicht: { flex: 1, backgroundColor: farben.hintergrund },
sperrschirm: {
justifyContent: 'center',
padding: abstand.xl,
gap: abstand.m,
},
sichtschutz: { justifyContent: 'center', alignItems: 'center' },
symbol: { fontSize: 56, textAlign: 'center' },
titel: {
fontSize: schrift.titel,
fontWeight: '700',
color: farben.text,
textAlign: 'center',
},
text: {
fontSize: schrift.basis,
color: farben.textSekundaer,
textAlign: 'center',
marginBottom: abstand.m,
},
});

View File

@@ -1,10 +1,15 @@
/**
* Großer, barrierefreier Button: min. 56px hoch, große Schrift,
* hoher Kontrast. Varianten: primär (gefüllt) und sekundär (Umriss).
* Großer, barrierefreier Button nach iOS-Vorbild:
* - primär: gefüllt mit der Marken-Tintfarbe (wie "prominente" iOS-Buttons)
* - sekundär: hellgraue Fläche mit Tint-Text (wie Apples "graue Taste" —
* bewusst keine Umrandung, das wirkt nativer)
* - gefahr: rot gefüllt
* Min. 56px hoch, große Schrift, System-Icons statt Emojis.
*/
import React from 'react';
import { ActivityIndicator, Pressable, StyleSheet, Text } from 'react-native';
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
import { Ikone, IkonenName } from './Ikone';
interface Props {
titel: string;
@@ -12,7 +17,7 @@ interface Props {
variante?: 'primaer' | 'sekundaer' | 'gefahr';
deaktiviert?: boolean;
laedt?: boolean;
symbol?: string;
ikone?: IkonenName;
}
export function GrossButton({
@@ -21,10 +26,11 @@ export function GrossButton({
variante = 'primaer',
deaktiviert = false,
laedt = false,
symbol,
ikone,
}: Props) {
const istPrimaer = variante === 'primaer';
const istGefahr = variante === 'gefahr';
const inhaltsFarbe = istPrimaer || istGefahr ? farben.primaerText : farben.primaer;
return (
<Pressable
onPress={onPress}
@@ -41,18 +47,12 @@ export function GrossButton({
]}
>
{laedt ? (
<ActivityIndicator color={istPrimaer || istGefahr ? farben.primaerText : farben.primaer} />
<ActivityIndicator color={inhaltsFarbe} />
) : (
<Text
style={[
styles.text,
(istPrimaer || istGefahr) && { color: farben.primaerText },
variante === 'sekundaer' && { color: farben.primaer },
]}
>
{symbol ? `${symbol} ` : ''}
{titel}
</Text>
<View style={styles.zeile}>
{ikone ? <Ikone name={ikone} farbe={inhaltsFarbe} /> : null}
<Text style={[styles.text, { color: inhaltsFarbe }]}>{titel}</Text>
</View>
)}
</Pressable>
);
@@ -67,10 +67,11 @@ const styles = StyleSheet.create({
paddingHorizontal: abstand.l,
paddingVertical: abstand.s,
},
zeile: { flexDirection: 'row', alignItems: 'center', gap: abstand.xs + 2 },
primaer: { backgroundColor: farben.primaer },
sekundaer: { backgroundColor: farben.hintergrund, borderWidth: 2, borderColor: farben.primaer },
sekundaer: { backgroundColor: farben.flaecheSekundaer },
gefahr: { backgroundColor: farben.fehler },
deaktiviert: { opacity: 0.5 },
gedrueckt: { opacity: 0.8 },
text: { fontSize: schrift.gross, fontWeight: '700', textAlign: 'center' },
deaktiviert: { opacity: 0.4 },
gedrueckt: { opacity: 0.65 },
text: { fontSize: schrift.gross, fontWeight: '600', textAlign: 'center' },
});

72
src/components/Ikone.tsx Normal file
View File

@@ -0,0 +1,72 @@
/**
* Zentrale Icon-Komponente — echte System-Icons statt Emojis:
* SF Symbols auf iOS (Apple-Look), Material Symbols auf Android.
* Alle in der App verwendeten Icons sind hier benannt, damit die
* Plattform-Zuordnung an einer Stelle liegt.
*/
import React from 'react';
import { SymbolView } from 'expo-symbols';
export type IkonenName =
| 'kamera'
| 'galerie'
| 'dokument'
| 'buch'
| 'zahnrad'
| 'brief'
| 'pfeilRechts'
| 'vorlesen'
| 'stopp'
| 'kalender'
| 'stift'
| 'teilen'
| 'muell'
| 'globus'
| 'checkAn'
| 'checkAus'
| 'warnung'
| 'wecker'
| 'info'
| 'funken';
// `as const` hält die Namen als Literal-Typen — so prüft TypeScript gegen
// die offizielle SF-Symbol-/Material-Namensliste von expo-symbols
const NAMEN = {
kamera: { ios: 'camera.fill', android: 'photo_camera' },
galerie: { ios: 'photo.on.rectangle', android: 'image' },
dokument: { ios: 'doc.fill', android: 'description' },
buch: { ios: 'book.fill', android: 'menu_book' },
zahnrad: { ios: 'gearshape.fill', android: 'settings' },
brief: { ios: 'envelope.open.fill', android: 'mail' },
pfeilRechts: { ios: 'chevron.right', android: 'chevron_right' },
vorlesen: { ios: 'speaker.wave.2.fill', android: 'volume_up' },
stopp: { ios: 'stop.fill', android: 'stop' },
kalender: { ios: 'calendar.badge.plus', android: 'event' },
stift: { ios: 'square.and.pencil', android: 'edit' },
teilen: { ios: 'square.and.arrow.up', android: 'share' },
muell: { ios: 'trash.fill', android: 'delete' },
globus: { ios: 'globe', android: 'language' },
checkAn: { ios: 'checkmark.square.fill', android: 'check_box' },
checkAus: { ios: 'square', android: 'check_box_outline_blank' },
warnung: { ios: 'exclamationmark.triangle.fill', android: 'warning' },
wecker: { ios: 'alarm.fill', android: 'alarm' },
info: { ios: 'info.circle.fill', android: 'info' },
funken: { ios: 'wand.and.stars', android: 'auto_awesome' },
} as const;
interface Props {
name: IkonenName;
groesse?: number;
farbe: string;
}
export function Ikone({ name, groesse = 22, farbe }: Props) {
const eintrag = NAMEN[name];
return (
<SymbolView
name={{ ios: eintrag.ios, android: eintrag.android }}
size={groesse}
tintColor={farbe}
/>
);
}

View File

@@ -23,6 +23,7 @@ import { formatiereDatum } from '../services/erinnerungen';
import { berechneAmpel } from '../utils/ampel';
import { Ampel } from '../components/Ampel';
import { GrossButton } from '../components/GrossButton';
import { Ikone } from '../components/Ikone';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Analyse'>;
@@ -133,9 +134,13 @@ export function AnalyseScreen({ navigation, route }: Props) {
accessibilityRole="button"
accessibilityLabel={`Sprache ändern. Aktuell: ${sprache.name}`}
>
<Text style={styles.sprachButtonText}>
🌐 {sprache.eigenname} {uebersetzt ? ' (übersetzt…)' : ''}
</Text>
<View style={styles.sprachZeileInhalt}>
<Ikone name="globus" groesse={20} farbe={farben.primaer} />
<Text style={styles.sprachButtonText}>
{sprache.eigenname} {uebersetzt ? ' (übersetzt…)' : ''}
</Text>
<Ikone name="pfeilRechts" groesse={16} farbe={farben.textTertiaer} />
</View>
</Pressable>
{/* Kernaussage: Was will das Amt von mir? */}
@@ -145,7 +150,7 @@ export function AnalyseScreen({ navigation, route }: Props) {
<View style={{ marginTop: abstand.s }}>
<GrossButton
titel={liest ? 'Vorlesen stoppen' : 'Vorlesen'}
symbol={liest ? '⏹️' : '🔊'}
ikone={liest ? 'stopp' : 'vorlesen'}
variante="sekundaer"
onPress={vorlesen}
/>
@@ -154,10 +159,15 @@ export function AnalyseScreen({ navigation, route }: Props) {
{/* Frist / Termin */}
{(analyse.frist || analyse.termin) && (
<View style={[styles.karte, { borderColor: ampel.farbe, borderWidth: 2 }]}>
<View
style={[
styles.karte,
{ backgroundColor: ampel.hintergrund, borderLeftWidth: 6, borderLeftColor: ampel.farbe },
]}
>
{analyse.frist && (
<Text style={styles.fristText}>
📅 Frist: <Text style={styles.fett}>{formatiereDatum(analyse.frist.datum)}</Text>
Frist: <Text style={styles.fett}>{formatiereDatum(analyse.frist.datum)}</Text>
{'\n'}
{analyse.frist.aktion}
</Text>
@@ -165,13 +175,13 @@ export function AnalyseScreen({ navigation, route }: Props) {
{analyse.termin && (
<>
<Text style={styles.fristText}>
🕐 Termin: <Text style={styles.fett}>{formatiereDatum(analyse.termin.datum)}</Text>
Termin: <Text style={styles.fett}>{formatiereDatum(analyse.termin.datum)}</Text>
{analyse.termin.uhrzeit ? ` um ${analyse.termin.uhrzeit} Uhr` : ''}
{analyse.termin.ort ? `\n📍 ${analyse.termin.ort}` : ''}
{analyse.termin.ort ? `\nOrt: ${analyse.termin.ort}` : ''}
</Text>
<GrossButton
titel="Termin zum Kalender hinzufügen"
symbol="🗓️"
ikone="kalender"
variante="sekundaer"
onPress={kalenderExport}
/>
@@ -198,7 +208,11 @@ export function AnalyseScreen({ navigation, route }: Props) {
accessibilityRole="checkbox"
accessibilityState={{ checked: !!erledigt[i] }}
>
<Text style={styles.checkBox}>{erledigt[i] ? '☑️' : '⬜'}</Text>
<Ikone
name={erledigt[i] ? 'checkAn' : 'checkAus'}
groesse={26}
farbe={erledigt[i] ? farben.ampelGruen : farben.textTertiaer}
/>
<Text style={[styles.text, { flex: 1 }, erledigt[i] && styles.durchgestrichen]}>
{punkt}
</Text>
@@ -224,13 +238,13 @@ export function AnalyseScreen({ navigation, route }: Props) {
{analyse.antwort_noetig && (
<GrossButton
titel="Antwort erstellen"
symbol="✍️"
ikone="stift"
onPress={() => navigation.navigate('Antwort', { briefId: brief.id })}
/>
)}
<View style={{ height: abstand.m }} />
<GrossButton titel="Brief löschen" symbol="🗑️" variante="gefahr" onPress={loeschen} />
<GrossButton titel="Brief löschen" ikone="muell" variante="gefahr" onPress={loeschen} />
<View style={{ height: abstand.xl }} />
{/* Sprach-Auswahl-Dialog */}
@@ -270,16 +284,17 @@ const styles = StyleSheet.create({
zentriert: { flex: 1, justifyContent: 'center', alignItems: 'center' },
karte: {
backgroundColor: farben.flaeche,
borderRadius: 14,
borderRadius: 12,
padding: abstand.m,
marginTop: abstand.m,
gap: abstand.s,
},
// DIE Antwort der App — dezent im Marken-Tint hervorgehoben
karteWichtig: {
backgroundColor: farben.flaeche,
borderRadius: 14,
borderWidth: 2,
borderColor: farben.primaer,
backgroundColor: farben.hervorhebung,
borderRadius: 12,
borderWidth: 1,
borderColor: farben.hervorhebungRand,
padding: abstand.m,
marginTop: abstand.m,
},
@@ -289,7 +304,7 @@ const styles = StyleSheet.create({
color: farben.primaer,
marginBottom: abstand.xs,
},
kernaussage: { fontSize: schrift.gross, color: farben.text, lineHeight: 30 },
kernaussage: { fontSize: schrift.gross, color: farben.text, lineHeight: 30, fontWeight: '500' },
text: { fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
fett: { fontWeight: '700' },
fristText: { fontSize: schrift.basis, color: farben.text, lineHeight: 28 },
@@ -300,20 +315,19 @@ const styles = StyleSheet.create({
minHeight: 44,
paddingVertical: 4,
},
checkBox: { fontSize: 24 },
durchgestrichen: { textDecorationLine: 'line-through', color: farben.textSekundaer },
begriffBlock: { marginBottom: abstand.s },
begriff: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
sprachButton: {
marginTop: abstand.m,
minHeight: TOUCH_TARGET,
borderRadius: 14,
borderWidth: 2,
borderColor: farben.rand,
borderRadius: 12,
backgroundColor: farben.flaeche,
justifyContent: 'center',
paddingHorizontal: abstand.m,
},
sprachButtonText: { fontSize: schrift.basis, color: farben.text, fontWeight: '600' },
sprachZeileInhalt: { flexDirection: 'row', alignItems: 'center', gap: abstand.s },
sprachButtonText: { flex: 1, fontSize: schrift.basis, color: farben.text, fontWeight: '600' },
modalHintergrund: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
@@ -321,7 +335,7 @@ const styles = StyleSheet.create({
padding: abstand.l,
},
modalKarte: {
backgroundColor: farben.hintergrund,
backgroundColor: farben.flaeche,
borderRadius: 14,
padding: abstand.m,
maxHeight: '75%',

View File

@@ -146,7 +146,7 @@ export function AntwortScreen({ route }: Props) {
<GrossButton
titel={text ? 'Neuen Entwurf erstellen' : 'Entwurf erstellen'}
symbol="✨"
ikone="funken"
onPress={erzeugen}
deaktiviert={!typ}
laedt={laedt}
@@ -173,8 +173,8 @@ export function AntwortScreen({ route }: Props) {
accessibilityLabel="Brieftext"
/>
<View style={{ gap: abstand.s }}>
<GrossButton titel="Als PDF exportieren" symbol="📄" onPress={alsPdf} />
<GrossButton titel="Als Text teilen / kopieren" symbol="📤" variante="sekundaer" onPress={alsText} />
<GrossButton titel="Als PDF exportieren" ikone="dokument" onPress={alsPdf} />
<GrossButton titel="Als Text teilen / kopieren" ikone="teilen" variante="sekundaer" onPress={alsText} />
</View>
</>
)}

View File

@@ -1,13 +1,36 @@
/**
* Einstellungen: API-Schlüssel (Dev-Modus), Datenschutz-Info,
* "Alle Daten löschen".
* Einstellungen: Datenschutz-Info und "Alle Daten löschen".
* Das API-Schlüssel-Feld erscheint nur im Entwickler-Modus (ohne Proxy) —
* im Produktionsmodus läuft der KI-Zugang über den Backend-Proxy.
*/
import React, { useEffect, useState } from 'react';
import { Alert, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import {
Alert,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import * as Notifications from 'expo-notifications';
import * as LocalAuthentication from 'expo-local-authentication';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../types';
import { holeApiKey, speichereApiKey, loescheAlleDaten } from '../services/storage';
import {
BONUS_ANALYSEN,
holeApiKey,
holeAppSperre,
holeAutoLoeschTage,
holeBonusAktiv,
loeseBonusCodeEin,
loescheAlleDaten,
setzeAppSperre,
setzeAutoLoeschTage,
speichereApiKey,
} from '../services/storage';
import { NUTZT_PROXY } from '../services/claudeClient';
import { useAppStore } from '../store/useAppStore';
import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand } from '../theme';
@@ -19,11 +42,64 @@ export function EinstellungenScreen({ navigation }: Props) {
const [apiKey, setApiKey] = useState('');
const [keyVorhanden, setKeyVorhanden] = useState(false);
const [gespeichert, setGespeichert] = useState(false);
const [bonusCode, setBonusCode] = useState('');
const [bonusAktiv, setBonusAktiv] = useState(false);
const [sperreAktiv, setSperreAktiv] = useState(false);
const [autoTage, setAutoTage] = useState(0);
useEffect(() => {
holeApiKey().then((k) => setKeyVorhanden(!!k));
holeBonusAktiv().then(setBonusAktiv);
holeAppSperre().then(setSperreAktiv);
holeAutoLoeschTage().then(setAutoTage);
}, []);
/** App-Sperre umschalten — beide Richtungen erfordern eine Authentifizierung. */
const sperreUmschalten = async (an: boolean) => {
if (an) {
const stufe = await LocalAuthentication.getEnrolledLevelAsync();
if (stufe === LocalAuthentication.SecurityLevel.NONE) {
Alert.alert(
'Keine Geräte-Sperre eingerichtet',
'Bitte richten Sie zuerst in den Geräte-Einstellungen einen Code, Face ID oder Fingerabdruck ein.'
);
return;
}
}
const ergebnis = await LocalAuthentication.authenticateAsync({
promptMessage: an ? 'App-Sperre aktivieren' : 'App-Sperre deaktivieren',
cancelLabel: 'Abbrechen',
});
if (!ergebnis.success) return;
await setzeAppSperre(an);
setSperreAktiv(an);
};
/** Aufbewahrungsdauer setzen und sofort anwenden. */
const autoTageSetzen = async (tage: number) => {
await setzeAutoLoeschTage(tage);
setAutoTage(tage);
// Sofort anwenden statt erst beim nächsten App-Start
await useAppStore.getState().initialisiere();
};
const codeEinloesen = async () => {
const erfolg = await loeseBonusCodeEin(bonusCode);
if (erfolg) {
setBonusAktiv(true);
setBonusCode('');
Alert.alert(
'Code eingelöst! 🎉',
`Sie haben jetzt ${BONUS_ANALYSEN} kostenlose Brief-Analysen.`
);
} else {
Alert.alert(
'Code ungültig',
'Bitte prüfen Sie die Schreibweise. Den Code finden Sie in Ihrer Anmelde-E-Mail.'
);
}
};
const speichern = async () => {
await speichereApiKey(apiKey);
setKeyVorhanden(!!apiKey.trim());
@@ -35,7 +111,7 @@ export function EinstellungenScreen({ navigation }: Props) {
const allesLoeschen = () => {
Alert.alert(
'Wirklich ALLE Daten löschen?',
'Alle gescannten Briefe, Übersetzungen, Entwürfe, der API-Schlüssel und die Einwilligung werden dauerhaft gelöscht. Das kann nicht rückgängig gemacht werden.',
'Alle gescannten Briefe, Übersetzungen, Entwürfe und die Einwilligung werden dauerhaft gelöscht. Das kann nicht rückgängig gemacht werden.',
[
{ text: 'Abbrechen', style: 'cancel' },
{
@@ -57,43 +133,126 @@ export function EinstellungenScreen({ navigation }: Props) {
return (
<ScrollView style={styles.container} contentContainerStyle={{ padding: abstand.m }}>
<Text style={styles.abschnittTitel}>KI-Zugang (API-Schlüssel)</Text>
<Text style={styles.text}>
Status:{' '}
<Text style={{ fontWeight: '700', color: keyVorhanden ? farben.ampelGruen : farben.fehler }}>
{keyVorhanden ? 'Schlüssel hinterlegt ✓' : 'Kein Schlüssel hinterlegt'}
{!NUTZT_PROXY && (
<>
<Text style={styles.abschnittTitel}>KI-Zugang (API-Schlüssel)</Text>
<Text style={styles.text}>
Status:{' '}
<Text style={{ fontWeight: '700', color: keyVorhanden ? farben.ampelGruen : farben.fehler }}>
{keyVorhanden ? 'Schlüssel hinterlegt ✓' : 'Kein Schlüssel hinterlegt'}
</Text>
</Text>
<Text style={styles.hinweis}>
Der Schlüssel wird verschlüsselt auf dem Gerät gespeichert (Secure Store)
und nur für die Brief-Analyse verwendet. Einen Schlüssel erhalten Sie
unter console.anthropic.com.
</Text>
<TextInput
style={styles.eingabe}
placeholder="sk-ant-…"
placeholderTextColor={farben.textSekundaer}
value={apiKey}
onChangeText={setApiKey}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
accessibilityLabel="API-Schlüssel eingeben"
/>
<GrossButton
titel={gespeichert ? 'Gespeichert ✓' : 'Schlüssel speichern'}
onPress={speichern}
deaktiviert={!apiKey.trim()}
/>
<View style={styles.trenner} />
</>
)}
<Text style={styles.abschnittTitel}>Aktions-Code</Text>
{bonusAktiv ? (
<Text style={styles.text}>
<Text style={{ fontWeight: '700', color: farben.ampelGruen }}>
Code eingelöst
</Text>{' '}
Sie haben {BONUS_ANALYSEN} kostenlose Analysen.
</Text>
) : (
<>
<Text style={styles.hinweis}>
Sie haben sich auf behoerdenklar.pages.dev angemeldet? Lösen Sie
hier den Code aus Ihrer E-Mail ein.
</Text>
<TextInput
style={styles.eingabe}
placeholder="Code eingeben"
placeholderTextColor={farben.textSekundaer}
value={bonusCode}
onChangeText={setBonusCode}
autoCapitalize="characters"
autoCorrect={false}
accessibilityLabel="Aktions-Code eingeben"
/>
<GrossButton
titel="Code einlösen"
onPress={codeEinloesen}
deaktiviert={!bonusCode.trim()}
/>
</>
)}
<View style={styles.trenner} />
<Text style={styles.abschnittTitel}>Sicherheit</Text>
<View style={styles.schalterZeile}>
<View style={{ flex: 1 }}>
<Text style={styles.text}>App-Sperre</Text>
<Text style={styles.hinweis}>
Face ID / Geräte-Code beim Öffnen der App schützt Ihre Briefe,
falls jemand Ihr Handy in die Hand bekommt.
</Text>
</View>
<Switch
value={sperreAktiv}
onValueChange={sperreUmschalten}
accessibilityLabel="App-Sperre umschalten"
/>
</View>
<Text style={[styles.text, { marginTop: abstand.m, fontWeight: '700' }]}>
Briefe automatisch löschen
</Text>
<Text style={styles.hinweis}>
Der Schlüssel wird verschlüsselt auf dem Gerät gespeichert (Secure Store)
und nur für die Brief-Analyse verwendet. Einen Schlüssel erhalten Sie
unter console.anthropic.com.
Alte Briefe werden dann von selbst vom Gerät entfernt.
</Text>
<TextInput
style={styles.eingabe}
placeholder="sk-ant-…"
placeholderTextColor={farben.textSekundaer}
value={apiKey}
onChangeText={setApiKey}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
accessibilityLabel="API-Schlüssel eingeben"
/>
<GrossButton
titel={gespeichert ? 'Gespeichert ✓' : 'Schlüssel speichern'}
onPress={speichern}
deaktiviert={!apiKey.trim()}
/>
{[
{ tage: 0, label: 'Nie (Standard)' },
{ tage: 30, label: 'Nach 30 Tagen' },
{ tage: 90, label: 'Nach 90 Tagen' },
].map(({ tage, label }) => (
<TouchableOpacity
key={tage}
style={[styles.option, autoTage === tage && styles.optionAktiv]}
onPress={() => autoTageSetzen(tage)}
accessibilityRole="radio"
accessibilityState={{ selected: autoTage === tage }}
>
<Text style={[styles.text, autoTage === tage && styles.optionAktivText]}>
{autoTage === tage ? '● ' : '○ '}
{label}
</Text>
</TouchableOpacity>
))}
<View style={styles.trenner} />
<Text style={styles.abschnittTitel}>Datenschutz</Text>
<View style={styles.karte}>
<Text style={styles.text}>
Brieffotos werden nur zur Analyse an Anthropic gesendet und dort
nicht dauerhaft gespeichert.{'\n\n'}
Alle Ergebnisse liegen ausschließlich auf Ihrem Gerät.{'\n\n'}
Brieffotos werden nur zur Analyse an den KI-Dienst Anthropic (USA)
gesendet und dort nach spätestens 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'}
Diese App ersetzt keine Rechtsberatung.
</Text>
</View>
@@ -101,7 +260,7 @@ export function EinstellungenScreen({ navigation }: Props) {
<View style={styles.trenner} />
<Text style={styles.abschnittTitel}>Daten löschen</Text>
<GrossButton titel="Alle Daten löschen" symbol="🗑️" variante="gefahr" onPress={allesLoeschen} />
<GrossButton titel="Alle Daten löschen" ikone="muell" variante="gefahr" onPress={allesLoeschen} />
<View style={{ height: abstand.xl }} />
</ScrollView>
);
@@ -134,4 +293,20 @@ const styles = StyleSheet.create({
},
karte: { backgroundColor: farben.flaeche, borderRadius: 14, padding: abstand.m },
trenner: { height: 1, backgroundColor: farben.rand, marginVertical: abstand.l },
schalterZeile: {
flexDirection: 'row',
alignItems: 'center',
gap: abstand.m,
},
option: {
borderWidth: 1,
borderColor: farben.rand,
borderRadius: 12,
padding: abstand.s,
minHeight: 56,
justifyContent: 'center',
marginTop: abstand.s,
},
optionAktiv: { borderColor: farben.primaer, backgroundColor: farben.flaeche },
optionAktivText: { fontWeight: '700', color: farben.primaer },
});

View File

@@ -18,10 +18,37 @@ import { holeConsent } from '../services/storage';
import { berechneAmpel } from '../utils/ampel';
import { formatiereDatum } from '../services/erinnerungen';
import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand } from '../theme';
import { Ikone, IkonenName } from '../components/Ikone';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
/** Quadratische Navigations-Kachel: Icon oben, Beschriftung darunter.
* (Löst das Platzproblem nebeneinanderstehender Text-Buttons.) */
function Kachel({
ikone,
titel,
onPress,
}: {
ikone: IkonenName;
titel: string;
onPress: () => void;
}) {
return (
<Pressable
style={({ pressed }) => [styles.kachel, pressed && { opacity: 0.65 }]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={titel}
>
<Ikone name={ikone} groesse={26} farbe={farben.primaer} />
<Text style={styles.kachelTitel} numberOfLines={1}>
{titel}
</Text>
</Pressable>
);
}
export function HomeScreen({ navigation }: Props) {
const briefe = useAppStore((s) => s.briefe);
const removeBrief = useAppStore((s) => s.removeBrief);
@@ -58,13 +85,16 @@ export function HomeScreen({ navigation }: Props) {
const ampel = berechneAmpel(item.analyse);
return (
<Pressable
style={({ pressed }) => [styles.karte, pressed && { opacity: 0.7 }]}
style={({ pressed }) => [
styles.karte,
{ borderLeftColor: ampel.farbe },
pressed && { opacity: 0.7 },
]}
onPress={() => navigation.navigate('Analyse', { briefId: item.id })}
onLongPress={() => loeschenBestaetigen(item)}
accessibilityRole="button"
accessibilityLabel={`Brief: ${item.analyse.brieftyp}. ${ampel.text}. Lange drücken zum Löschen.`}
>
<View style={[styles.ampelPunkt, { backgroundColor: ampel.farbe }]} />
<View style={{ flex: 1 }}>
<Text style={styles.kartenTitel} numberOfLines={1}>
{item.analyse.brieftyp}
@@ -76,73 +106,91 @@ export function HomeScreen({ navigation }: Props) {
{ampel.text}
</Text>
</View>
<Text style={styles.pfeil}></Text>
<Ikone name="pfeilRechts" groesse={18} farbe={farben.textTertiaer} />
</Pressable>
);
};
return (
<View style={styles.container}>
<View style={styles.kopf}>
<GrossButton titel="Brief scannen" symbol="📷" onPress={scanStarten} />
<View style={styles.reihe}>
<View style={{ flex: 1 }}>
<GrossButton
titel="Glossar"
variante="sekundaer"
symbol="📖"
onPress={() => navigation.navigate('Glossar')}
/>
</View>
<View style={{ width: abstand.s }} />
<View style={{ flex: 1 }}>
<GrossButton
<FlatList
style={styles.container}
// iOS: sorgt beim großen Titel für korrekte Abstände + Kollabieren beim Scrollen
contentInsetAdjustmentBehavior="automatic"
keyboardShouldPersistTaps="handled"
data={gefiltert}
keyExtractor={(b) => b.id}
renderItem={renderBrief}
contentContainerStyle={styles.inhalt}
ListHeaderComponent={
<View style={styles.kopf}>
<GrossButton titel="Brief scannen" ikone="kamera" onPress={scanStarten} />
<View style={styles.reihe}>
<Kachel ikone="buch" titel="Glossar" onPress={() => navigation.navigate('Glossar')} />
<Kachel
ikone="zahnrad"
titel="Einstellungen"
variante="sekundaer"
symbol="⚙️"
onPress={() => navigation.navigate('Einstellungen')}
/>
</View>
{briefe.length > 0 && (
<>
<Text style={styles.abschnittsLabel}>Ihre Briefe</Text>
<TextInput
style={styles.suche}
placeholder="Im Archiv suchen…"
placeholderTextColor={farben.textSekundaer}
value={suche}
onChangeText={setSuche}
accessibilityLabel="Archiv durchsuchen"
/>
</>
)}
</View>
</View>
{briefe.length > 0 && (
<TextInput
style={styles.suche}
placeholder="Im Archiv suchen…"
placeholderTextColor={farben.textSekundaer}
value={suche}
onChangeText={setSuche}
accessibilityLabel="Archiv durchsuchen"
/>
)}
<FlatList
data={gefiltert}
keyExtractor={(b) => b.id}
renderItem={renderBrief}
contentContainerStyle={{ padding: abstand.m, paddingTop: 0 }}
ListEmptyComponent={
<Text style={styles.leer}>
{briefe.length === 0
? 'Noch keine Briefe.\nScannen Sie Ihren ersten Behördenbrief!'
: 'Keine Treffer.'}
</Text>
}
/>
</View>
}
ListEmptyComponent={
briefe.length === 0 ? (
<View style={styles.leerBox}>
<Ikone name="brief" groesse={48} farbe={farben.textTertiaer} />
<Text style={styles.leerTitel}>Noch keine Briefe</Text>
<Text style={styles.leer}>
Scannen Sie Ihren ersten Behördenbrief {'\n'}in einer Minute
wissen Sie, was er bedeutet.
</Text>
</View>
) : (
<Text style={styles.leer}>Keine Treffer.</Text>
)
}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
kopf: { padding: abstand.m, gap: abstand.s },
reihe: { flexDirection: 'row' },
inhalt: { paddingHorizontal: abstand.m, paddingBottom: abstand.l },
kopf: { paddingTop: abstand.s, paddingBottom: abstand.s, gap: abstand.s },
reihe: { flexDirection: 'row', gap: abstand.s },
kachel: {
flex: 1,
minHeight: TOUCH_TARGET + 22,
backgroundColor: farben.flaeche,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: abstand.s,
gap: 6,
},
kachelTitel: { fontSize: schrift.klein + 1, fontWeight: '600', color: farben.primaer },
abschnittsLabel: {
marginTop: abstand.m,
fontSize: schrift.klein - 1,
fontWeight: '600',
letterSpacing: 1.2,
textTransform: 'uppercase',
color: farben.textSekundaer,
},
suche: {
marginHorizontal: abstand.m,
marginBottom: abstand.s,
borderWidth: 1,
borderColor: farben.rand,
backgroundColor: farben.flaeche,
borderRadius: 12,
padding: abstand.s,
fontSize: schrift.basis,
@@ -153,21 +201,21 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: farben.flaeche,
borderRadius: 14,
borderRadius: 12,
borderLeftWidth: 6,
padding: abstand.m,
marginBottom: abstand.s,
gap: abstand.s,
},
ampelPunkt: { width: 16, height: 16, borderRadius: 8 },
kartenTitel: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
kartenUntertitel: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
kartenAmpelText: { fontSize: schrift.klein, fontWeight: '600', marginTop: 2 },
pfeil: { fontSize: 32, color: farben.textSekundaer },
leerBox: { alignItems: 'center', marginTop: abstand.xl, gap: abstand.s },
leerTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
leer: {
textAlign: 'center',
fontSize: schrift.basis,
color: farben.textSekundaer,
marginTop: abstand.xl,
lineHeight: 28,
},
});

View File

@@ -2,15 +2,23 @@
* Brief erfassen: Foto aufnehmen, aus Galerie wählen oder PDF hochladen.
* Danach läuft die KI-Analyse; bei Erfolg geht es direkt zum Ergebnis.
*/
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { ActivityIndicator, Alert, StyleSheet, Text, View } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import * as ImagePicker from 'expo-image-picker';
import * as DocumentPicker from 'expo-document-picker';
import { ImageManipulator, SaveFormat } from 'expo-image-manipulator';
import { File } from 'expo-file-system';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList, BriefEintrag } from '../types';
import { analysiereBrief } from '../services/analyse';
import { ClaudeFehler } from '../services/claudeClient';
import { ClaudeFehler, NUTZT_PROXY } from '../services/claudeClient';
import {
GRATIS_ANALYSEN,
holeAnzahlAnalysen,
holeGratisKontingent,
zaehleAnalyse,
} from '../services/storage';
import { planeErinnerungen } from '../services/erinnerungen';
import { useAppStore, neueId } from '../store/useAppStore';
import { GrossButton } from '../components/GrossButton';
@@ -18,15 +26,72 @@ import { farben, schrift, abstand } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Scan'>;
/**
* Maximale Kantenlänge fürs KI-Bild. Ein A4-Brief bleibt bei 2000px gut
* lesbar (~170 dpi); moderne Handyfotos (4000px+) würden unverkleinert
* bis zu 3x mehr Bild-Token kosten.
*/
const MAX_BILD_KANTE = 2000;
/**
* Verkleinert ein Foto auf max. MAX_BILD_KANTE (lange Kante, Seitenverhältnis
* bleibt erhalten), komprimiert als JPEG und liefert base64 für die Analyse.
*/
async function bildFuerAnalyse(asset: ImagePicker.ImagePickerAsset): Promise<string> {
const kontext = ImageManipulator.manipulate(asset.uri);
if (Math.max(asset.width, asset.height) > MAX_BILD_KANTE) {
kontext.resize(
asset.width >= asset.height
? { width: MAX_BILD_KANTE }
: { height: MAX_BILD_KANTE }
);
}
const bild = await kontext.renderAsync();
const ergebnis = await bild.saveAsync({
format: SaveFormat.JPEG,
compress: 0.6,
base64: true,
});
return ergebnis.base64!;
}
export function ScanScreen({ navigation }: Props) {
const addBrief = useAppStore((s) => s.addBrief);
const [laedt, setLaedt] = useState(false);
const [verbraucht, setVerbraucht] = useState(0);
const [kontingent, setKontingent] = useState(GRATIS_ANALYSEN);
// Zähler + Kontingent bei jedem Screen-Besuch frisch laden
// (Kontingent kann sich ändern, wenn in den Einstellungen ein
// Aktions-Code eingelöst wurde)
useFocusEffect(
useCallback(() => {
holeAnzahlAnalysen().then(setVerbraucht);
holeGratisKontingent().then(setKontingent);
}, [])
);
// Freemium gilt nur im Produktionsmodus (Proxy). Im Dev-Modus mit
// eigenem API-Key zahlt der Entwickler selbst — kein Limit nötig.
const uebrig = kontingent - verbraucht;
const kontingentLeer = NUTZT_PROXY && uebrig <= 0;
/** Prüft das Gratis-Kontingent; false = Analyse nicht starten. */
const pruefeKontingent = (): boolean => {
if (!kontingentLeer) return true;
Alert.alert(
'Gratis-Analysen aufgebraucht',
`Sie haben Ihre ${kontingent} kostenlosen Brief-Analysen genutzt. Bald können Sie hier ein Abo abschließen, um weiter Briefe zu analysieren.`
);
return false;
};
/** Gemeinsamer Abschluss: analysieren, speichern, Erinnerungen, weiter. */
const verarbeite = async (base64: string, mimeType: string) => {
setLaedt(true);
try {
const analyse = await analysiereBrief(base64, mimeType);
await zaehleAnalyse();
const brief: BriefEintrag = {
id: neueId(),
erstelltAm: new Date().toISOString(),
@@ -49,6 +114,7 @@ export function ScanScreen({ navigation }: Props) {
};
const fotoAufnehmen = async () => {
if (!pruefeKontingent()) return;
const { granted } = await ImagePicker.requestCameraPermissionsAsync();
if (!granted) {
Alert.alert('Kamera-Zugriff nötig', 'Bitte erlauben Sie den Kamera-Zugriff in den Geräte-Einstellungen.');
@@ -56,16 +122,17 @@ export function ScanScreen({ navigation }: Props) {
}
const ergebnis = await ImagePicker.launchCameraAsync({
mediaTypes: 'images',
quality: 0.6, // Kompromiss: lesbar für die KI, aber kleine Datenmenge
base64: true,
// Volle Qualität vom Picker — verkleinert & komprimiert wird einmal
// zentral in bildFuerAnalyse (doppelte JPEG-Kompression vermeiden)
quality: 1,
});
if (!ergebnis.canceled && ergebnis.assets[0]?.base64) {
// base64 der ImagePicker-Assets ist laut Doku immer JPEG
await verarbeite(ergebnis.assets[0].base64, 'image/jpeg');
if (!ergebnis.canceled && ergebnis.assets[0]) {
await verarbeite(await bildFuerAnalyse(ergebnis.assets[0]), 'image/jpeg');
}
};
const ausGalerie = async () => {
if (!pruefeKontingent()) return;
const { granted } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!granted) {
Alert.alert('Foto-Zugriff nötig', 'Bitte erlauben Sie den Zugriff auf Ihre Fotos in den Geräte-Einstellungen.');
@@ -73,15 +140,15 @@ export function ScanScreen({ navigation }: Props) {
}
const ergebnis = await ImagePicker.launchImageLibraryAsync({
mediaTypes: 'images',
quality: 0.6,
base64: true,
quality: 1,
});
if (!ergebnis.canceled && ergebnis.assets[0]?.base64) {
await verarbeite(ergebnis.assets[0].base64, 'image/jpeg');
if (!ergebnis.canceled && ergebnis.assets[0]) {
await verarbeite(await bildFuerAnalyse(ergebnis.assets[0]), 'image/jpeg');
}
};
const pdfWaehlen = async () => {
if (!pruefeKontingent()) return;
const ergebnis = await DocumentPicker.getDocumentAsync({
type: 'application/pdf',
copyToCacheDirectory: true,
@@ -115,9 +182,16 @@ export function ScanScreen({ navigation }: Props) {
Fotografieren Sie den Brief gut beleuchtet und von oben. Der ganze Text
muss auf dem Foto sein.
</Text>
<GrossButton titel="Foto aufnehmen" symbol="📷" onPress={fotoAufnehmen} />
<GrossButton titel="Aus Galerie wählen" symbol="🖼️" variante="sekundaer" onPress={ausGalerie} />
<GrossButton titel="PDF hochladen" symbol="📄" variante="sekundaer" onPress={pdfWaehlen} />
<GrossButton titel="Foto aufnehmen" ikone="kamera" onPress={fotoAufnehmen} />
<GrossButton titel="Aus Galerie wählen" ikone="galerie" variante="sekundaer" onPress={ausGalerie} />
<GrossButton titel="PDF hochladen" ikone="dokument" variante="sekundaer" onPress={pdfWaehlen} />
{NUTZT_PROXY && (
<Text style={styles.kontingent}>
{kontingentLeer
? 'Gratis-Analysen aufgebraucht — Abo folgt in Kürze'
: `Noch ${uebrig} von ${kontingent} kostenlosen Analysen`}
</Text>
)}
</View>
);
}
@@ -145,6 +219,12 @@ const styles = StyleSheet.create({
padding: abstand.xl,
gap: abstand.m,
},
kontingent: {
fontSize: schrift.klein,
color: farben.textSekundaer,
textAlign: 'center',
marginTop: abstand.m,
},
ladeText: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
ladeHinweis: {
fontSize: schrift.basis,

View File

@@ -4,7 +4,7 @@
* Sprache, Fachbegriffe, Fristen, Termine, Checkliste und Antwort-Optionen.
*/
import { BriefAnalyse } from '../types';
import { claudeJsonAufruf, ContentBlock } from './claudeClient';
import { claudeJsonAufruf, ContentBlock, MODELL_ANALYSE } from './claudeClient';
/**
* JSON-Schema für die Analyse. Die Claude API erzwingt dieses Schema
@@ -159,6 +159,7 @@ export async function analysiereBrief(
};
return claudeJsonAufruf<BriefAnalyse>({
modell: MODELL_ANALYSE,
system: SYSTEM_PROMPT,
schema: ANALYSE_SCHEMA,
messages: [

View File

@@ -4,7 +4,7 @@
* Entwurf danach frei bearbeiten und als PDF exportieren.
*/
import { AntwortTyp, ANTWORT_TYP_LABEL, BriefAnalyse } from '../types';
import { claudeJsonAufruf } from './claudeClient';
import { claudeJsonAufruf, MODELL_EINFACH } from './claudeClient';
const ANTWORT_SCHEMA = {
type: 'object',
@@ -36,6 +36,8 @@ export async function generiereAntwort(
hinweise: string
): Promise<AntwortEntwurf> {
return claudeJsonAufruf<AntwortEntwurf>({
modell: MODELL_EINFACH,
maxTokens: 4000,
system: `Du schreibst formelle, höfliche Antwortbriefe an deutsche Behörden im Namen von Privatpersonen. Regeln:
- Korrektes, formelles Deutsch (der Empfänger ist eine Behörde — hier KEINE einfache Sprache).
- Kurz und präzise, keine Floskeln über das Nötige hinaus.

View File

@@ -13,16 +13,32 @@
* deshalb hier bewusst rohes fetch gegen die Messages API.
*/
import * as Network from 'expo-network';
import { holeApiKey } from './storage';
import { holeApiKey, holeGeraeteId } from './storage';
/** Für Produktion hier die eigene Server-URL eintragen, z. B. 'https://api.meine-app.de/claude'. */
const PROXY_URL: string | null = null;
/**
* Für Produktion hier die URL des deployten Workers eintragen,
* z. B. 'https://behoerdenklar-proxy.<account>.workers.dev'.
* Setup-Anleitung: proxy/README.md
*/
const PROXY_URL: string | null =
'https://behoerdenklar-proxy.behoerdenbrief.workers.dev';
/** true = Produktionsmodus über den Backend-Proxy (kein API-Key in der App nötig). */
export const NUTZT_PROXY = PROXY_URL !== null;
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
const ANTHROPIC_VERSION = '2023-06-01';
/** Standard-Modell für alle Aufrufe. */
export const MODELL = 'claude-opus-4-8';
/**
* Modell-Split zur Kostenoptimierung:
* - Brief-Analyse (Vision, Genauigkeit wichtig): Sonnet 5 (~2,5 Cent/Brief)
* - Übersetzung & Antwort-Entwürfe (einfache Textaufgaben): Haiku 4.5 (~halber Preis)
*
* Hinweis: Der Backend-Proxy erlaubt nur diese Modelle (Whitelist in
* proxy/src/index.ts) — bei Änderungen dort mitziehen.
*/
export const MODELL_ANALYSE = 'claude-sonnet-5';
export const MODELL_EINFACH = 'claude-haiku-4-5';
// ---- Typen für den Request-Body (Teilmenge der Messages API) ----
@@ -44,6 +60,8 @@ export interface DocumentBlock {
export type ContentBlock = TextBlock | ImageBlock | DocumentBlock;
export interface ClaudeRequest {
/** Welches Modell: MODELL_ANALYSE (Vision) oder MODELL_EINFACH (Text). */
modell: string;
system?: string;
messages: { role: 'user' | 'assistant'; content: string | ContentBlock[] }[];
/** JSON-Schema für Structured Output — garantiert valides JSON in der Antwort. */
@@ -75,10 +93,11 @@ export async function claudeJsonAufruf<T>(anfrage: ClaudeRequest): Promise<T> {
// 2. Request-Body bauen
const body: Record<string, unknown> = {
model: MODELL,
model: anfrage.modell,
max_tokens: anfrage.maxTokens ?? 16000,
// Adaptives Denken verbessert die Extraktions-Genauigkeit deutlich
thinking: { type: 'adaptive' },
// Kein explizites thinking-Feld: Sonnet 5 denkt adaptiv von selbst
// (gut für die Extraktions-Genauigkeit), Haiku 4.5 unterstützt den
// adaptiven Modus nicht und würde die Anfrage ablehnen.
messages: anfrage.messages,
};
if (anfrage.system) body.system = anfrage.system;
@@ -96,6 +115,8 @@ export async function claudeJsonAufruf<T>(anfrage: ClaudeRequest): Promise<T> {
};
if (PROXY_URL) {
url = PROXY_URL; // Der Proxy hält den API-Key serverseitig
// Anonyme ID, damit der Proxy das Tageslimit pro Gerät durchsetzen kann
headers['x-geraete-id'] = await holeGeraeteId();
} else {
const apiKey = await holeApiKey();
if (!apiKey) {
@@ -137,8 +158,11 @@ export async function claudeJsonAufruf<T>(anfrage: ClaudeRequest): Promise<T> {
technisch
);
case 429:
// Direkt-Modus: API-Rate-Limit. Proxy-Modus: Tageslimit pro Gerät.
throw new ClaudeFehler(
'Zu viele Anfragen. Bitte warten Sie einen Moment und versuchen Sie es erneut.',
PROXY_URL
? 'Das tägliche Kontingent ist aufgebraucht. Bitte versuchen Sie es morgen erneut.'
: 'Zu viele Anfragen. Bitte warten Sie einen Moment und versuchen Sie es erneut.',
technisch
);
case 413:

61
src/services/krypto.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* Verschlüsselung des Brief-Archivs (AES-256-GCM, nativ via expo-crypto).
*
* Warum: Die analysierten Briefe enthalten hochsensible Daten (Sozialleistungen,
* Gesundheit, Finanzen). AsyncStorage ist unverschlüsselter Klartext auf der
* Platte — deshalb wird das Archiv vor dem Speichern verschlüsselt.
*
* Schlüsselverwaltung: Ein 256-Bit-Schlüssel wird beim ersten Bedarf erzeugt
* und im Secure Store des Geräts abgelegt (iOS Keychain / Android Keystore).
* Er verlässt das Gerät nie. GCM liefert zusätzlich Integritätsschutz —
* manipulierte Daten fallen beim Entschlüsseln auf.
*/
import {
AESEncryptionKey,
AESSealedData,
aesDecryptAsync,
aesEncryptAsync,
} from 'expo-crypto';
import * as SecureStore from 'expo-secure-store';
import { utf8Dekodieren, utf8Kodieren } from '../utils/utf8';
const KEY_ARCHIV_SCHLUESSEL = 'behoerdenklar_archiv_schluessel';
/** Im Speicher gehaltener Schlüssel — spart SecureStore-Zugriffe pro Aufruf. */
let schluesselCache: AESEncryptionKey | null = null;
async function holeSchluessel(): Promise<AESEncryptionKey> {
if (schluesselCache) return schluesselCache;
const hex = await SecureStore.getItemAsync(KEY_ARCHIV_SCHLUESSEL);
if (hex) {
schluesselCache = await AESEncryptionKey.import(hex, 'hex');
} else {
schluesselCache = await AESEncryptionKey.generate(); // 256 Bit (Default)
await SecureStore.setItemAsync(
KEY_ARCHIV_SCHLUESSEL,
await schluesselCache.encoded('hex')
);
}
return schluesselCache;
}
/** Verschlüsselt Text -> base64-String (IV + Ciphertext + GCM-Tag kombiniert). */
export async function verschluesseleText(klartext: string): Promise<string> {
const schluessel = await holeSchluessel();
const versiegelt = await aesEncryptAsync(utf8Kodieren(klartext), schluessel);
return (await versiegelt.combined('base64')) as string;
}
/** Entschlüsselt einen base64-String zurück zu Text. Wirft bei Manipulation. */
export async function entschluesseleText(chiffrat: string): Promise<string> {
const schluessel = await holeSchluessel();
const versiegelt = AESSealedData.fromCombined(chiffrat);
const bytes = (await aesDecryptAsync(versiegelt, schluessel)) as Uint8Array;
return utf8Dekodieren(bytes);
}
/** Für "Alle Daten löschen": Schlüssel unwiderruflich entfernen. */
export async function loescheArchivSchluessel(): Promise<void> {
schluesselCache = null;
await SecureStore.deleteItemAsync(KEY_ARCHIV_SCHLUESSEL);
}

View File

@@ -1,19 +1,62 @@
/**
* Lokale Speicherung.
* - API-Key: expo-secure-store (verschlüsselt, nie im Klartext auf der Platte)
* - Brief-Archiv + Einwilligung: AsyncStorage (bleibt auf dem Gerät)
* - Brief-Archiv: AsyncStorage, aber AES-256-GCM-verschlüsselt (siehe krypto.ts)
* - Einwilligung & Einstellungen: AsyncStorage (unkritische Flags)
*
* Datenschutz: Alle Daten bleiben lokal. Nichts wird an Server gesendet,
* außer dem Briefinhalt zur KI-Analyse (nach expliziter Einwilligung).
* loescheAlleDaten() entfernt restlos alles.
* loescheAlleDaten() entfernt restlos alles — inklusive Archiv-Schlüssel.
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import { BriefEintrag } from '../types';
import {
entschluesseleText,
loescheArchivSchluessel,
verschluesseleText,
} from './krypto';
const KEY_API = 'anthropic_api_key';
const KEY_BRIEFE = 'behoerdenklar_briefe';
const KEY_CONSENT = 'behoerdenklar_consent';
const KEY_GERAETE_ID = 'behoerdenklar_geraete_id';
const KEY_SCAN_ANZAHL = 'behoerdenklar_scan_anzahl';
/**
* Freemium: so viele Brief-Analysen sind kostenlos. Danach braucht es das
* Abo (kommt mit dem IAP-Einbau). Der Zähler liegt lokal — das ist bewusst
* "weich" (Neuinstallation setzt ihn zurück); die harte Grenze ist das
* Tageslimit des Backend-Proxys. Echte Abo-Prüfung folgt serverseitig.
*/
export const GRATIS_ANALYSEN = 3;
/** Erhöhtes Kontingent für Warteliste-Anmelder (Code aus der Willkommens-Mail). */
export const BONUS_ANALYSEN = 5;
const KEY_BONUS = 'behoerdenklar_bonus';
/**
* Aktions-Codes der Landingpage-Warteliste. Bewusst ein geteilter Code
* (kein Einmal-Code): Der "Schaden" bei Weitergabe sind 2 Extra-Analysen
* (~5 Cent) — dafür brauchen wir keinerlei Server-Logik.
*/
const GUELTIGE_CODES = ['FRUEHSTART'];
export async function holeBonusAktiv(): Promise<boolean> {
return (await AsyncStorage.getItem(KEY_BONUS)) === 'ja';
}
/** true = Code war gültig und wurde eingelöst. */
export async function loeseBonusCodeEin(code: string): Promise<boolean> {
if (!GUELTIGE_CODES.includes(code.trim().toUpperCase())) return false;
await AsyncStorage.setItem(KEY_BONUS, 'ja');
return true;
}
/** Aktuelles Gratis-Kontingent dieses Geräts (3, mit Aktions-Code 5). */
export async function holeGratisKontingent(): Promise<number> {
return (await holeBonusAktiv()) ? BONUS_ANALYSEN : GRATIS_ANALYSEN;
}
// ---- API-Key (sensibel -> SecureStore) ----
@@ -29,6 +72,42 @@ export async function speichereApiKey(key: string): Promise<void> {
}
}
// ---- Geräte-ID (anonym, nur fürs Tageslimit des Backend-Proxys) ----
/**
* Liefert eine zufällige, anonyme Geräte-ID (wird beim ersten Aufruf erzeugt).
* Sie enthält keine Gerätedaten und dient dem Proxy ausschließlich dazu,
* das Anfrage-Tageslimit pro Installation durchzusetzen.
*/
export async function holeGeraeteId(): Promise<string> {
let id = await AsyncStorage.getItem(KEY_GERAETE_ID);
if (!id) {
// Math.random reicht hier: die ID ist kein Geheimnis, nur ein Zähl-Schlüssel
id =
'g_' +
Array.from({ length: 24 }, () =>
Math.floor(Math.random() * 36).toString(36)
).join('');
await AsyncStorage.setItem(KEY_GERAETE_ID, id);
}
return id;
}
// ---- Freemium-Zähler ----
/** Wie viele Brief-Analysen wurden auf diesem Gerät schon durchgeführt? */
export async function holeAnzahlAnalysen(): Promise<number> {
const roh = await AsyncStorage.getItem(KEY_SCAN_ANZAHL);
const anzahl = roh ? parseInt(roh, 10) : 0;
return Number.isFinite(anzahl) ? anzahl : 0;
}
/** Nach jeder erfolgreichen Analyse aufrufen. */
export async function zaehleAnalyse(): Promise<void> {
const bisher = await holeAnzahlAnalysen();
await AsyncStorage.setItem(KEY_SCAN_ANZAHL, String(bisher + 1));
}
// ---- Einwilligung (Datenschutz-Consent) ----
export async function holeConsent(): Promise<boolean> {
@@ -39,26 +118,83 @@ export async function speichereConsent(): Promise<void> {
await AsyncStorage.setItem(KEY_CONSENT, 'ja');
}
// ---- Brief-Archiv ----
// ---- Brief-Archiv (verschlüsselt) ----
export async function ladeBriefe(): Promise<BriefEintrag[]> {
const roh = await AsyncStorage.getItem(KEY_BRIEFE);
if (!roh) return [];
try {
return JSON.parse(roh) as BriefEintrag[];
// Migration: Bestände aus Versionen vor der Verschlüsselung liegen als
// Klartext-JSON-Array vor ('['), base64-Chiffrate nie. Einmalig
// verschlüsselt neu schreiben.
if (roh.startsWith('[')) {
const briefe = JSON.parse(roh) as BriefEintrag[];
await speichereBriefe(briefe);
return briefe;
}
return JSON.parse(await entschluesseleText(roh)) as BriefEintrag[];
} catch {
// Korrupte Daten nicht crashen lassen — leeres Archiv liefern
// Korrupte/unlesbare Daten nicht crashen lassen — leeres Archiv liefern
return [];
}
}
export async function speichereBriefe(briefe: BriefEintrag[]): Promise<void> {
await AsyncStorage.setItem(KEY_BRIEFE, JSON.stringify(briefe));
await AsyncStorage.setItem(
KEY_BRIEFE,
await verschluesseleText(JSON.stringify(briefe))
);
}
// ---- Sicherheits-Einstellungen ----
const KEY_APP_SPERRE = 'behoerdenklar_app_sperre';
const KEY_AUTO_LOESCHEN = 'behoerdenklar_auto_loeschen_tage';
/** Ist die App-Sperre (Face ID / Geräte-Code beim Öffnen) aktiviert? */
export async function holeAppSperre(): Promise<boolean> {
return (await AsyncStorage.getItem(KEY_APP_SPERRE)) === 'ja';
}
export async function setzeAppSperre(aktiv: boolean): Promise<void> {
if (aktiv) {
await AsyncStorage.setItem(KEY_APP_SPERRE, 'ja');
} else {
await AsyncStorage.removeItem(KEY_APP_SPERRE);
}
}
/** Nach wie vielen Tagen Briefe automatisch gelöscht werden (0 = nie). */
export async function holeAutoLoeschTage(): Promise<number> {
const roh = await AsyncStorage.getItem(KEY_AUTO_LOESCHEN);
const tage = roh ? parseInt(roh, 10) : 0;
return Number.isFinite(tage) && tage > 0 ? tage : 0;
}
export async function setzeAutoLoeschTage(tage: number): Promise<void> {
if (tage > 0) {
await AsyncStorage.setItem(KEY_AUTO_LOESCHEN, String(tage));
} else {
await AsyncStorage.removeItem(KEY_AUTO_LOESCHEN);
}
}
// ---- Alles löschen (Datenschutz-Anforderung) ----
export async function loescheAlleDaten(): Promise<void> {
await AsyncStorage.multiRemove([KEY_BRIEFE, KEY_CONSENT]);
// "Alle Daten löschen" heißt wirklich alle — auch Zähler und Geräte-ID.
// Dass damit das Gratis-Kontingent zurücksetzbar ist, nehmen wir in Kauf
// (Neuinstallation könnte das ohnehin); der Proxy limitiert weiterhin.
await AsyncStorage.multiRemove([
KEY_BRIEFE,
KEY_CONSENT,
KEY_GERAETE_ID,
KEY_SCAN_ANZAHL,
KEY_BONUS,
KEY_APP_SPERRE,
KEY_AUTO_LOESCHEN,
]);
await SecureStore.deleteItemAsync(KEY_API);
// Archiv-Schlüssel zuletzt: danach wären Restdaten ohnehin unlesbar
await loescheArchivSchluessel();
}

View File

@@ -4,7 +4,7 @@
* Archiv gecacht (siehe store), damit kein Aufruf doppelt läuft.
*/
import { BriefAnalyse, Sprache, Uebersetzung } from '../types';
import { claudeJsonAufruf } from './claudeClient';
import { claudeJsonAufruf, MODELL_EINFACH } from './claudeClient';
const UEBERSETZUNG_SCHEMA = {
type: 'object',
@@ -36,6 +36,8 @@ export async function uebersetzeAnalyse(
sprache: Sprache
): Promise<Uebersetzung> {
return claudeJsonAufruf<Uebersetzung>({
modell: MODELL_EINFACH,
maxTokens: 6000,
system: `Du übersetzt Erklärungen deutscher Behördenbriefe in andere Sprachen. Übersetze in einfache, klare Sprache (Niveau A2/B1 der Zielsprache). Deutsche Fachbegriffe (Feld "begriff") NICHT übersetzen — sie stehen so im Original-Brief und der Nutzer muss sie wiedererkennen. Nur die Erklärungen übersetzen.`,
schema: UEBERSETZUNG_SCHEMA,
messages: [

View File

@@ -4,7 +4,8 @@
*/
import { create } from 'zustand';
import { BriefEintrag, Uebersetzung, AntwortTyp } from '../types';
import { ladeBriefe, speichereBriefe } from '../services/storage';
import { holeAutoLoeschTage, ladeBriefe, speichereBriefe } from '../services/storage';
import { filtereAlteBriefe } from '../utils/aufbewahrung';
interface AppState {
briefe: BriefEintrag[];
@@ -28,7 +29,14 @@ export const useAppStore = create<AppState>((set, get) => ({
geladen: false,
initialisiere: async () => {
const briefe = await ladeBriefe();
let briefe = await ladeBriefe();
// Auto-Löschen (Datensparsamkeit): abgelaufene Briefe beim Start entfernen
const aufbewahrungTage = await holeAutoLoeschTage();
const gefiltert = filtereAlteBriefe(briefe, aufbewahrungTage);
if (gefiltert.length !== briefe.length) {
briefe = gefiltert;
await speichereBriefe(briefe);
}
set({ briefe, geladen: true });
},

View File

@@ -5,24 +5,31 @@
* - Große Touch-Targets (min. 56px Höhe)
*/
export const farben = {
// Grundfarben
hintergrund: '#FFFFFF',
flaeche: '#F2F5F9',
primaer: '#1A365D', // dunkles, ruhiges Blau
// Grundfarben — natives iOS-Vorbild: gruppierter System-Hintergrund,
// Inhalte als weiße Karten ohne Rahmen und Schatten (wie Einstellungen/Health)
hintergrund: '#F2F2F7',
flaeche: '#FFFFFF',
flaecheSekundaer: '#E9E9EB', // "graue Taste" nach Apple-Art (statt Umrandung)
primaer: '#007AFF', // Apple-System-Blau (Tint für Buttons, Icons, Links)
primaerText: '#FFFFFF',
text: '#1A202C',
textSekundaer: '#4A5568',
rand: '#CBD5E0',
textTertiaer: '#AEAEB2', // z. B. Chevrons (iOS systemGray2)
rand: '#E2E2E7',
// Dezente Hervorhebung für DIE Kernaussage (leichter Blau-Tint)
hervorhebung: '#EAF3FF',
hervorhebungRand: '#D4E6FB',
// Dringlichkeits-Ampel (kräftig, gut unterscheidbar)
ampelRot: '#C0392B',
ampelGelb: '#B7791F',
ampelGruen: '#276749',
ampelRotHintergrund: '#FDECEA',
ampelGelbHintergrund: '#FDF6E3',
ampelGruenHintergrund: '#EAF6EE',
// Dringlichkeits-Ampel: frische Signalfarben in Apples "zugänglichen"
// Varianten — kräftig, aber als Text auf Weiß noch gut lesbar (WCAG)
ampelRot: '#D70015',
ampelGelb: '#C25400',
ampelGruen: '#1E7E34',
ampelRotHintergrund: '#FFEAE8',
ampelGelbHintergrund: '#FFF1E0',
ampelGruenHintergrund: '#E7F6EB',
fehler: '#C0392B',
fehler: '#D70015',
};
export const schrift = {
@@ -43,3 +50,10 @@ 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;

View File

@@ -0,0 +1,161 @@
/**
* Tests für die Dringlichkeits-Ampel — die kritischste Logik der App:
* Eine falsche Ampel könnte dazu führen, dass ein Nutzer eine Behörden-Frist
* verpasst. Deshalb hier alle Grenzfälle deterministisch abgesichert.
*/
import { berechneAmpel, tageBis } from '../ampel';
import { BriefAnalyse } from '../../types';
/** Fixes "Heute" für alle Tests: 15.07.2026 (Mitte des Monats, kein Randfall). */
const HEUTE = new Date('2026-07-15T10:30:00');
/** Minimal gültige Analyse; frist/termin pro Test überschreiben. */
function analyse(teile: Partial<BriefAnalyse>): BriefAnalyse {
return {
brieftyp: 'Test',
absender: 'Testamt',
kernaussage: '',
erklaerung_einfach: '',
fachbegriffe: [],
frist: null,
termin: null,
checkliste: [],
antwort_noetig: false,
antwort_optionen: [],
...teile,
};
}
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(HEUTE);
});
afterAll(() => {
jest.useRealTimers();
});
describe('tageBis', () => {
it('liefert 0 für heute', () => {
expect(tageBis('2026-07-15')).toBe(0);
});
it('liefert positive Tage für die Zukunft', () => {
expect(tageBis('2026-07-16')).toBe(1);
expect(tageBis('2026-07-22')).toBe(7);
});
it('liefert negative Tage für die Vergangenheit', () => {
expect(tageBis('2026-07-14')).toBe(-1);
expect(tageBis('2026-07-01')).toBe(-14);
});
it('zählt ganze Tage unabhängig von der Uhrzeit "jetzt"', () => {
// 10:30 Uhr darf keine halben Tage erzeugen
expect(tageBis('2026-07-18')).toBe(3);
});
it('funktioniert über Monatsgrenzen hinweg', () => {
expect(tageBis('2026-08-01')).toBe(17);
});
});
describe('berechneAmpel — Stufen', () => {
it('grün ohne Frist und Termin', () => {
const status = berechneAmpel(analyse({}));
expect(status.stufe).toBe('gruen');
expect(status.text).toContain('Keine Frist');
});
it('rot bei abgelaufener Frist (inkl. Hinweis)', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-07-10', aktion: 'Unterlagen einreichen' } })
);
expect(status.stufe).toBe('rot');
expect(status.text).toContain('abgelaufen');
});
it('rot bei Frist heute', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-07-15', aktion: 'Antwort' } })
);
expect(status.stufe).toBe('rot');
expect(status.text).toContain('HEUTE');
});
it('rot bei Frist in 6 Tagen (< 7)', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-07-21', aktion: 'Antwort' } })
);
expect(status.stufe).toBe('rot');
});
it('gelb bei Frist in genau 7 Tagen (Grenzwert)', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-07-22', aktion: 'Antwort' } })
);
expect(status.stufe).toBe('gelb');
});
it('gelb bei Frist in genau 21 Tagen (Grenzwert)', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-08-05', aktion: 'Antwort' } })
);
expect(status.stufe).toBe('gelb');
});
it('grün bei Frist in 22 Tagen (> 21)', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-08-06', aktion: 'Antwort' } })
);
expect(status.stufe).toBe('gruen');
});
});
describe('berechneAmpel — Frist und Termin kombiniert', () => {
it('das dringendste Datum bestimmt die Ampel (Termin näher als Frist)', () => {
const status = berechneAmpel(
analyse({
frist: { datum: '2026-08-20', aktion: 'Unterlagen' }, // 36 Tage → grün
termin: { datum: '2026-07-18', uhrzeit: '09:30', ort: 'Raum 1' }, // 3 Tage → rot
})
);
expect(status.stufe).toBe('rot');
expect(status.text).toContain('Termin');
});
it('das dringendste Datum bestimmt die Ampel (Frist näher als Termin)', () => {
const status = berechneAmpel(
analyse({
frist: { datum: '2026-07-25', aktion: 'Antwort' }, // 10 Tage → gelb
termin: { datum: '2026-09-01', uhrzeit: null, ort: null }, // weit weg
})
);
expect(status.stufe).toBe('gelb');
expect(status.text).toContain('Frist');
});
it('nur Termin ohne Frist funktioniert', () => {
const status = berechneAmpel(
analyse({ termin: { datum: '2026-07-16', uhrzeit: null, ort: null } })
);
expect(status.stufe).toBe('rot');
expect(status.text).toContain('Termin');
});
});
describe('berechneAmpel — Textdetails', () => {
it('Einzahl bei 1 Tag ("in 1 Tag", nicht "Tagen")', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-07-16', aktion: 'Antwort' } })
);
expect(status.text).toContain('in 1 Tag ');
});
it('Einzahl bei 1 Tag abgelaufen', () => {
const status = berechneAmpel(
analyse({ frist: { datum: '2026-07-14', aktion: 'Antwort' } })
);
expect(status.text).toContain('vor 1 Tag)');
});
});

View File

@@ -0,0 +1,61 @@
/**
* Tests fürs Auto-Löschen — löschen ist unumkehrbar, deshalb müssen die
* Grenzfälle sitzen (nichts zu früh löschen, "nie" respektieren).
*/
import { filtereAlteBriefe } from '../aufbewahrung';
import { BriefEintrag } from '../../types';
const JETZT = new Date('2026-07-15T12:00:00Z').getTime();
const TAG = 86400000;
function brief(id: string, alterInTagen: number): BriefEintrag {
return {
id,
erstelltAm: new Date(JETZT - alterInTagen * TAG).toISOString(),
analyse: {
brieftyp: 'Test',
absender: 'Testamt',
kernaussage: '',
erklaerung_einfach: '',
fachbegriffe: [],
frist: null,
termin: null,
checkliste: [],
antwort_noetig: false,
antwort_optionen: [],
},
uebersetzungen: {},
};
}
describe('filtereAlteBriefe', () => {
it('0 Tage = nie löschen, alles bleibt', () => {
const briefe = [brief('a', 1), brief('b', 500)];
expect(filtereAlteBriefe(briefe, 0, JETZT)).toHaveLength(2);
});
it('löscht Briefe, die älter als die Frist sind', () => {
const briefe = [brief('neu', 5), brief('alt', 40)];
const ergebnis = filtereAlteBriefe(briefe, 30, JETZT);
expect(ergebnis.map((b) => b.id)).toEqual(['neu']);
});
it('behält Briefe exakt am Grenzwert (30 Tage alt bei 30-Tage-Frist)', () => {
const briefe = [brief('grenze', 30)];
expect(filtereAlteBriefe(briefe, 30, JETZT)).toHaveLength(1);
});
it('löscht bei 90-Tage-Frist erst nach 90 Tagen', () => {
const briefe = [brief('a', 89), brief('b', 91)];
expect(filtereAlteBriefe(briefe, 90, JETZT).map((b) => b.id)).toEqual(['a']);
});
it('behält Briefe mit kaputtem Datum (lieber zu viel als zu wenig)', () => {
const kaputt = { ...brief('kaputt', 0), erstelltAm: 'kein-datum' };
expect(filtereAlteBriefe([kaputt], 30, JETZT)).toHaveLength(1);
});
it('leeres Archiv bleibt leer', () => {
expect(filtereAlteBriefe([], 30, JETZT)).toEqual([]);
});
});

View File

@@ -0,0 +1,42 @@
/**
* Tests für die UTF-8-Helfer — kritisch für die Archiv-Verschlüsselung:
* Ein Kodierungsfehler würde gespeicherte Briefe unlesbar machen.
*/
import { utf8Kodieren, utf8Dekodieren } from '../utf8';
describe('utf8Kodieren / utf8Dekodieren', () => {
const rundreise = (text: string) => utf8Dekodieren(utf8Kodieren(text));
it('einfaches ASCII', () => {
expect(rundreise('Hallo Welt 123')).toBe('Hallo Welt 123');
});
it('deutsche Umlaute und ß', () => {
expect(rundreise('Bescheid über Bürgergeld gemäß § 32 — Änderung')).toBe(
'Bescheid über Bürgergeld gemäß § 32 — Änderung'
);
});
it('arabisch, ukrainisch, türkisch (Übersetzungssprachen)', () => {
const text = 'العربية · Українська · Türkçe İĞÜŞÖÇ';
expect(rundreise(text)).toBe(text);
});
it('Emoji (Surrogate-Paare)', () => {
expect(rundreise('Ampel: 🔴🟡🟢 📬')).toBe('Ampel: 🔴🟡🟢 📬');
});
it('JSON-Sonderzeichen', () => {
const json = JSON.stringify({ a: 'Zeile\nUmbruch "Anführung" \\ Slash' });
expect(rundreise(json)).toBe(json);
});
it('leerer String', () => {
expect(rundreise('')).toBe('');
});
it('kodiert Umlaute als Mehrbyte-Sequenzen (echtes UTF-8)', () => {
// "ü" ist in UTF-8 die Bytefolge 0xC3 0xBC
expect(Array.from(utf8Kodieren('ü'))).toEqual([0xc3, 0xbc]);
});
});

27
src/utils/aufbewahrung.ts Normal file
View File

@@ -0,0 +1,27 @@
/**
* Auto-Löschen alter Briefe (Datensparsamkeit, DSGVO-Grundsatz).
* Reine Funktion — die Anwendung passiert beim App-Start im Store.
*/
import { BriefEintrag } from '../types';
/**
* Filtert Briefe heraus, die älter als die Aufbewahrungsdauer sind.
*
* @param briefe - das aktuelle Archiv
* @param aufbewahrungTage - 0 = nie löschen (alles behalten)
* @param jetztMs - aktueller Zeitpunkt (injizierbar für Tests)
*/
export function filtereAlteBriefe(
briefe: BriefEintrag[],
aufbewahrungTage: number,
jetztMs: number = Date.now()
): BriefEintrag[] {
if (aufbewahrungTage <= 0) return briefe;
const grenzeMs = jetztMs - aufbewahrungTage * 86400000;
return briefe.filter((brief) => {
const erstellt = new Date(brief.erstelltAm).getTime();
// Unlesbare Datumswerte lieber behalten als versehentlich löschen
if (!Number.isFinite(erstellt)) return true;
return erstellt >= grenzeMs;
});
}

33
src/utils/utf8.ts Normal file
View File

@@ -0,0 +1,33 @@
/**
* UTF-8-Kodierung ohne Plattform-Abhängigkeiten.
*
* Warum selbst gebaut: expo-crypto akzeptiert Klartext nur als Uint8Array
* (oder base64-String), und TextEncoder/TextDecoder sind in Hermes nicht
* zuverlässig über alle Versionen verfügbar. Der encodeURIComponent-Trick
* ist korrekt für alle Unicode-Zeichen (Umlaute, Arabisch, Emoji —
* inklusive Surrogate-Paaren) und in Jest testbar.
*/
/** JavaScript-String -> UTF-8-Bytes. */
export function utf8Kodieren(text: string): Uint8Array {
const prozentKodiert = encodeURIComponent(text);
const bytes: number[] = [];
for (let i = 0; i < prozentKodiert.length; i++) {
if (prozentKodiert[i] === '%') {
bytes.push(parseInt(prozentKodiert.slice(i + 1, i + 3), 16));
i += 2;
} else {
bytes.push(prozentKodiert.charCodeAt(i));
}
}
return new Uint8Array(bytes);
}
/** UTF-8-Bytes -> JavaScript-String. Wirft bei ungültigem UTF-8 (URIError). */
export function utf8Dekodieren(bytes: Uint8Array): string {
let prozentKodiert = '';
for (const byte of bytes) {
prozentKodiert += '%' + byte.toString(16).padStart(2, '0');
}
return decodeURIComponent(prozentKodiert);
}

122
store/app-store-texte.md Normal file
View File

@@ -0,0 +1,122 @@
# App-Store-Texte — BehördenKlar (iOS)
Vorbereitet zum Einfügen in App Store Connect, sobald das Apple-Developer-Konto
da ist. Zeichenlimits sind von Apple vorgegeben und hier eingehalten.
---
## App-Name (max. 30 Zeichen)
```
BehördenKlar Brief-Hilfe
```
*(26 Zeichen ✓ — Alternative: „BehördenKlar" pur; der Untertitel trägt dann den Slogan)*
## Untertitel (max. 30 Zeichen)
```
Amtsdeutsch einfach erklärt
```
## Kategorie
- Primär: **Dienstprogramme** (Utilities)
- Sekundär: **Produktivität**
## Keywords (max. 100 Zeichen, kommagetrennt, ohne Leerzeichen)
```
behörde,amt,brief,bescheid,jobcenter,widerspruch,frist,amtsdeutsch,einfache sprache,übersetzen
```
## Werbetext / Promo-Text (max. 170 Zeichen, jederzeit änderbar)
```
Brief vom Amt? Einfach fotografieren BehördenKlar erklärt ihn in einfacher Sprache, findet Fristen und hilft beim Antworten. In 10 Sprachen.
```
## Beschreibung (max. 4000 Zeichen)
```
Ein Brief vom Amt und nichts verstanden? Damit sind Sie nicht allein. BehördenKlar übersetzt Amtsdeutsch in einfache, klare Sprache.
SO FUNKTIONIERT ES
Fotografieren Sie den Brief. Nach weniger als einer Minute wissen Sie:
• Was will das Amt von mir? in 2-3 einfachen Sätzen
• Was muss ich jetzt tun? als klare Checkliste
• Bis wann? alle Fristen und Termine auf einen Blick
DIE AMPEL ZEIGT, WIE DRINGEND ES IST
🔴 Rot: Frist in weniger als 7 Tagen jetzt handeln
🟡 Gelb: bald erledigen
🟢 Grün: keine Frist, nur zur Information
VERSTEHEN IN IHRER SPRACHE
Alle Erklärungen lassen sich in 9 weitere Sprachen übersetzen: Türkisch, Arabisch, Englisch, Russisch, Ukrainisch, Französisch, Farsi, Rumänisch und Polnisch. Die deutschen Fachbegriffe bleiben stehen, damit Sie sie im Brief wiederfinden.
ANTWORTEN LEICHT GEMACHT
BehördenKlar erstellt Ihnen einen fertigen Antwort-Entwurf zum Beispiel um einen Termin zu bestätigen oder zu verschieben, Unterlagen nachzureichen oder Widerspruch einzulegen. Sie können den Text frei bearbeiten und als PDF teilen oder drucken.
NICHTS MEHR VERPASSEN
• Termine mit einem Tipp in Ihren Kalender übernehmen
• Automatische Erinnerungen vor jeder Frist
• Alle gescannten Briefe übersichtlich im Archiv
IHRE DATEN GEHÖREN IHNEN
• Alle Ergebnisse werden nur auf Ihrem Gerät gespeichert
• Kein Konto, keine Registrierung nötig
• Mit einem Tipp alles löschen
AUSSERDEM
• Behörden-Glossar: die wichtigsten Amtsbegriffe einfach erklärt auch offline
• Vorlese-Funktion für alle Erklärungen
• Große Schrift und große Tasten einfach zu bedienen
3 Brief-Analysen sind kostenlos. Danach analysieren Sie mit dem Abo unbegrenzt weiter.
Wichtig: BehördenKlar erklärt Briefe verständlich, ersetzt aber keine Rechtsberatung. Bei rechtlich wichtigen Entscheidungen wenden Sie sich bitte an eine Beratungsstelle oder eine Anwältin/einen Anwalt.
```
## Screenshot-Plan (6,7"-iPhone Pflicht, je 1290×2796 px)
1. **Hero:** Scan-Screen mit Slogan „Brief fotografieren sofort verstehen"
2. **Analyse:** Ergebnis mit Kernaussage + roter Ampel „Frist in 5 Tagen"
3. **Checkliste:** „Das müssen Sie tun" mit Häkchen-Liste
4. **Übersetzung:** dieselbe Analyse auf Türkisch oder Arabisch
5. **Antwort:** fertiger Antwort-Entwurf mit „Als PDF teilen"-Button
6. **Archiv:** Briefliste mit Ampeln
*Tipp: Screenshots im Simulator (iPhone 17 Pro Max) mit dem Testbrief erstellen —
Cmd+S speichert einen Screenshot in der passenden Auflösung.*
## Altersfreigabe
4+ (keine bedenklichen Inhalte)
## App-Review-Hinweise (Feld „Notes" bei der Einreichung)
```
Die App analysiert Fotos von deutschen Behördenbriefen mit einem KI-Dienst
(Anthropic Claude) über unseren eigenen Backend-Server. Zum Testen:
Ein Beispiel-Behördenbrief zum Abfotografieren liegt unter [URL ZU TESTBRIEF
EINFÜGEN] bereit, oder nutzen Sie einen beliebigen deutschen Behördenbrief.
Vor dem ersten Scan erscheint eine Datenschutz-Einwilligung. 3 Analysen sind
ohne Kauf möglich.
```
## URLs (live, für die Formularfelder in App Store Connect)
- **Support-URL:** https://behoerdenklar.pages.dev
- **Datenschutz-URL:** https://behoerdenklar.pages.dev/datenschutz
- Kontakt-E-Mail: behoerdenbriefhelfer@gmail.com
- Quelldateien: `webseite/` — neu deployen mit
`npx wrangler pages deploy ../webseite --project-name behoerdenklar` (aus proxy/)
## Noch offen (vor Einreichung)
- [ ] „App-Datenschutz"-Fragebogen in App Store Connect ausfüllen
(Datenerfassung: Fotos → werden verarbeitet, nicht mit Identität verknüpft,
kein Tracking)
- [ ] Screenshots erstellen (siehe Plan oben)
```

View File

@@ -1,6 +1,8 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
"strict": true,
"types": ["jest"]
},
"exclude": ["proxy", "node_modules"]
}

231
webseite/ar.html Normal file
View File

@@ -0,0 +1,231 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BehördenKlar افهم الرسائل الرسمية الألمانية</title>
<meta name="description" content="BehördenKlar يشرح الرسائل الرسمية الألمانية بكلمات بسيطة — بالعربية. صوّر الرسالة، افهمها، وردّ بشكل صحيح. قريباً على iPhone.">
<link rel="alternate" hreflang="de" href="https://behoerdenklar.pages.dev/">
<link rel="alternate" hreflang="tr" href="https://behoerdenklar.pages.dev/tr">
<link rel="alternate" hreflang="ar" href="https://behoerdenklar.pages.dev/ar">
<link rel="alternate" hreflang="en" href="https://behoerdenklar.pages.dev/en">
<link rel="alternate" hreflang="uk" href="https://behoerdenklar.pages.dev/uk">
<link rel="alternate" hreflang="ru" href="https://behoerdenklar.pages.dev/ru">
<link rel="alternate" hreflang="pl" href="https://behoerdenklar.pages.dev/pl">
<style>
:root {
--papier: #faf7f0; --papier-dunkel: #f1ecdf; --tinte: #21211d;
--grau: #6b675e; --amtsblau: #1d4e89; --stempelrot: #b3372b;
--marker: #ffe66b; --gruen: #2e7d4f; color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33,33,29,0.025) 34px 35px),
var(--papier);
color: var(--tinte); line-height: 1.7; font-size: 17px;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte); font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
.sprachen {
padding: 12px 0 0; font-size: 15px; color: var(--grau);
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
}
.sprachen a { color: var(--amtsblau); text-decoration: none; border-bottom: 1px dotted var(--amtsblau); }
.sprachen .aktiv { font-weight: 700; color: var(--tinte); border-bottom: none; }
header { padding: 44px 0 8px; }
h1 { font-size: clamp(32px, 6.5vw, 48px); line-height: 1.14; font-weight: 700; }
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.intro { margin-top: 24px; font-size: 19px; max-width: 580px; }
.hero-fuss { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 28px; }
.cta {
display: inline-block; padding: 17px 30px; background: var(--amtsblau);
color: #fff; text-decoration: none; font-size: 18px; font-weight: 700;
border: 2px solid var(--tinte); box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.cta:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.stempel {
display: inline-block; padding: 7px 14px; border: 2.5px solid var(--stempelrot);
color: var(--stempelrot); font-family: 'Courier New', ui-monospace, monospace;
font-weight: 700; font-size: 14px; letter-spacing: 0.08em;
text-transform: uppercase; transform: rotate(-4deg); opacity: 0.9;
}
section { margin-top: 60px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
.verwandlung { display: grid; gap: 14px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33,33,29,0.28);
padding: 22px 24px; position: relative;
box-shadow: 4px 5px 0 rgba(33,33,29,0.08);
}
.brief-karte.amtsdeutsch { transform: rotate(-0.7deg); background: var(--papier-dunkel); }
.brief-karte.klartext { transform: rotate(0.5deg); }
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--tinte); color: var(--papier); padding: 3px 10px;
}
[dir="rtl"] .karten-label { left: auto; right: 16px; }
.brief-karte.klartext .karten-label { background: var(--gruen); }
.amtsdeutsch p {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 14.5px; color: #45423b; line-height: 1.75;
}
.klartext p { font-size: 18px; }
.pfeil { text-align: center; font-size: 15px; color: var(--grau); font-family: 'Courier New', ui-monospace, monospace; }
.hinweis { margin-top: 10px; font-size: 14.5px; color: var(--grau); }
.schritte { counter-reset: schritt; }
.schritt {
counter-increment: schritt; display: flex; gap: 20px; padding: 20px 4px;
border-bottom: 1px solid rgba(33,33,29,0.15); align-items: baseline;
}
.schritt:last-child { border-bottom: none; }
.schritt::before {
content: counter(schritt, decimal-leading-zero);
font-family: 'Courier New', ui-monospace, monospace;
font-size: 30px; font-weight: 700; color: var(--amtsblau);
flex-shrink: 0; width: 52px;
}
.schritt strong { font-size: 19px; display: block; margin-bottom: 2px; }
.schritt span { color: var(--grau); font-size: 16px; }
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33,33,29,0.9);
padding: 30px 28px; position: relative; margin-top: 8px;
}
.antrag .formular-kopf {
font-family: 'Courier New', ui-monospace, monospace; font-size: 13px;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--grau); margin-bottom: 6px;
}
.antrag h3 { font-size: 26px; line-height: 1.25; }
.antrag p { margin-top: 12px; max-width: 540px; }
.bonus {
margin: 18px 0; padding: 14px 16px; font-size: 17px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
}
.antrag .klein { font-size: 13.5px; color: var(--grau); margin-top: 14px; }
.antrag .klein a { color: var(--grau); }
a { color: var(--amtsblau); }
footer {
margin-top: 56px; padding-top: 14px; border-top: 2px solid var(--tinte);
font-size: 14.5px; color: var(--grau);
}
</style>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar</span>
<span class="az mono">Az: KLAR-01/2026</span>
</div>
<nav class="sprachen" aria-label="اختر اللغة">
🌐
<a href="index.html" lang="de">Deutsch</a>
<a href="tr.html" lang="tr">Türkçe</a>
<span class="aktiv">العربية</span>
<a href="en.html" lang="en">English</a>
<a href="uk.html" lang="uk">Українська</a>
<a href="ru.html" lang="ru">Русский</a>
<a href="pl.html" lang="pl">Polski</a>
</nav>
<header>
<h1>رسالة من دائرة ألمانية؟<br><span class="markiert">الآن فهمها سهل.</span></h1>
<p class="intro">
صوّر الرسالة بهاتفك. يشرح لك BehördenKlar الرسالة بألمانية بسيطة —
<strong>وبالعربية أيضاً</strong>. مع كل المواعيد وقائمة بما يجب أن
تفعله.
</p>
<div class="hero-fuss">
<a class="cta" href="#anmelden">سجّل في قائمة الانتظار&nbsp;</a>
<span class="stempel">قريباً على iPhone</span>
</div>
</header>
<section>
<h2>§ 1 &nbsp;التحويل</h2>
<div class="verwandlung">
<div class="brief-karte amtsdeutsch">
<span class="karten-label">قبل · الرسالة الأصلية (بالألمانية)</span>
<p dir="ltr" lang="de">
„Gemäß § 32 SGB II wird bei Nichterscheinen ohne wichtigen Grund
eine Minderung des maßgebenden Regelbedarfs in Höhe von 10 vom
Hundert festgestellt."
</p>
</div>
<p class="pfeil">— 📷 بعد صورة واحدة —</p>
<div class="brief-karte klartext">
<span class="karten-label">بعد · BehördenKlar بالعربية</span>
<p>
إذا لم تذهب إلى الموعد بدون سبب مهم، ستحصل على نقود أقل بنسبة
١٠٪ لمدة شهر واحد. اذهب إلى الموعد — أو ألغِه في الوقت المناسب.
</p>
</div>
</div>
<p class="hinweis">
يشرح التطبيق الرسائل بألمانية بسيطة ويترجمها إلى: العربية، التركية،
الإنجليزية، الروسية، الأوكرانية، الفرنسية، الفارسية، الرومانية
والبولندية.
</p>
</section>
<section>
<h2>§ 2 &nbsp;كيف يعمل</h2>
<div class="schritte">
<div class="schritt"><div><strong>صوّر الرسالة</strong> <span>أو ارفع ملف PDF.</span></div></div>
<div class="schritt"><div><strong>افهمها</strong> <span>كلمات بسيطة، وبلغتك. إشارة ضوئية تبيّن لك: هل الأمر مستعجل؟</span></div></div>
<div class="schritt"><div><strong>ردّ بشكل صحيح</strong> <span>قائمة مهام، تذكير بالموعد النهائي، وردّ جاهز.</span></div></div>
</div>
</section>
<section id="anmelden">
<h2>§ 3 &nbsp;الوصول المبكر</h2>
<div class="antrag">
<p class="formular-kopf">نموذج F-01 · قائمة الانتظار</p>
<h3>كن من الأوائل</h3>
<p>
التطبيق قادم قريباً لهواتف iPhone. سجّل الآن — نرسل لك رسالة
<strong>واحدة</strong> عندما يصبح التطبيق جاهزاً. بدون دعاية.
</p>
<p class="bonus">
🎁 <strong>هديتك:</strong> حلّل <strong>٥ رسائل مجاناً</strong>
بدلاً من ٣. تحصل على الرمز عبر البريد الإلكتروني.
</p>
<a class="cta" href="mailto:behoerdenbriefhelfer@gmail.com?subject=Anmeldung%20Warteliste%20(Arabisch)&body=Hallo%2C%20bitte%20informiert%20mich%2C%20wenn%20Beh%C3%B6rdenKlar%20verf%C3%BCgbar%20ist.%20%2F%20Please%20notify%20me%20when%20Beh%C3%B6rdenKlar%20is%20available.">
📧 سجّل عبر البريد الإلكتروني
</a>
<p class="klein">
نحفظ عنوان بريدك الإلكتروني فقط لهذه الرسالة الواحدة. يمكنك إلغاء
التسجيل في أي وقت. <a href="datenschutz.html">سياسة الخصوصية (بالألمانية)</a>.
</p>
</div>
</section>
<footer>
أسئلة؟ 📧 <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
&nbsp;·&nbsp; <a href="impressum.html">Impressum (بالألمانية)</a>
&nbsp;·&nbsp; <a href="datenschutz.html">سياسة الخصوصية (بالألمانية)</a>
</footer>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

180
webseite/datenschutz.html Normal file
View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutzerklärung BehördenKlar</title>
<meta name="robots" content="noindex">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #1a1a2e; background: #f4f8fb; line-height: 1.7;
}
.wrap { max-width: 640px; margin: 0 auto; padding: 32px 20px 64px; }
h1 { font-size: 28px; color: #144d8c; margin: 24px 0 4px; }
.stand { color: #4a5568; margin-bottom: 16px; }
h2 { font-size: 20px; color: #144d8c; margin: 24px 0 8px; }
h3 { font-size: 17px; color: #1a1a2e; margin: 16px 0 6px; }
p, li { margin-bottom: 8px; }
ul { padding-left: 24px; }
.karte {
background: #fff; border-radius: 16px; padding: 24px;
box-shadow: 0 2px 8px rgba(20, 77, 140, 0.08);
}
a { color: #1a6bc4; }
.zurueck { display: inline-block; margin-top: 24px; }
</style>
</head>
<body>
<div class="wrap">
<h1>Datenschutzerklärung</h1>
<p class="stand">für die App „BehördenKlar" — Stand: 14.07.2026</p>
<div class="karte">
<h2>1. Verantwortlicher</h2>
<p>
Justin Klein<br>
Am Schwimmbad 10<br>
67722 Winnweiler<br>
E-Mail: <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
</p>
<h2>2. Was macht die App?</h2>
<p>
BehördenKlar hilft Ihnen, Behördenbriefe zu verstehen: Sie fotografieren
einen Brief, und eine Künstliche Intelligenz (KI) erklärt den Inhalt in
einfacher Sprache, findet Fristen und Termine und hilft beim Antworten.
</p>
<h2>3. Welche Daten werden verarbeitet?</h2>
<h3>a) Brieffotos und PDF-Dateien (nur mit Ihrer Einwilligung)</h3>
<p>Wenn Sie einen Brief scannen, wird das Foto bzw. die PDF-Datei zur Analyse übertragen an:</p>
<ul>
<li>
<strong>Unseren Server</strong> (Cloudflare Workers, Anbieter:
Cloudflare, Inc., USA) — er leitet die Anfrage nur weiter und
speichert den Briefinhalt nicht.
</li>
<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.
</li>
</ul>
<p>
<strong>Wichtig:</strong> Behördenbriefe können sehr persönliche und
besondere Kategorien von Daten enthalten (z. B. Angaben zu
Sozialleistungen, Gesundheit oder finanzieller Situation). Die Analyse
erfolgt deshalb nur nach Ihrer ausdrücklichen Einwilligung, die Sie vor
dem ersten Scan erteilen (Art. 6 Abs. 1 lit. a, Art. 9 Abs. 2 lit. a
DSGVO). Sie können die Einwilligung jederzeit widerrufen, indem Sie in
den Einstellungen „Alle Daten löschen" wählen.
</p>
<h3>b) Analyse-Ergebnisse, Übersetzungen, Antwort-Entwürfe</h3>
<p>
Alle Ergebnisse werden ausschließlich lokal auf Ihrem Gerät gespeichert —
zusätzlich verschlüsselt (AES-256-GCM; der Schlüssel liegt im
Sicherheitsspeicher Ihres Geräts). Wir betreiben keine Nutzerkonten und
keine Cloud-Speicherung Ihrer Briefe. Optional können Sie in den
Einstellungen eine App-Sperre (Face ID / Geräte-Code) aktivieren und
Briefe nach 30 oder 90 Tagen automatisch löschen lassen.
</p>
<h3>c) Anonyme Geräte-Kennung</h3>
<p>
Die App erzeugt eine zufällige, anonyme Kennung. Sie enthält keine
Angaben über Sie oder Ihr Gerät und dient ausschließlich dazu, ein
tägliches Nutzungslimit pro Installation durchzusetzen (Schutz vor
Missbrauch, Art. 6 Abs. 1 lit. f DSGVO). Sie wird zusammen mit einem
Tageszähler für maximal 48 Stunden auf unserem Server gespeichert.
</p>
<h3>d) Kalender, Erinnerungen, Vorlesen</h3>
<p>
Kalendereinträge und Frist-Erinnerungen werden nur auf Ihrem Gerät
angelegt (mit Ihrer Erlaubnis über die iOS-/Android-Berechtigungen).
Die Vorlese-Funktion nutzt die Sprachausgabe Ihres Geräts. Dabei werden
keine Daten an uns übertragen.
</p>
<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).
</p>
<h2>5. Speicherdauer</h2>
<ul>
<li>Brieffotos bei Anthropic: maximal 30 Tage, danach automatische Löschung</li>
<li>Anonyme Geräte-Kennung + Tageszähler auf unserem Server: maximal 48 Stunden</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>
<h2>6. Ihre Rechte</h2>
<p>
Sie haben nach der DSGVO das Recht auf Auskunft (Art. 15), Berichtigung
(Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18),
Datenübertragbarkeit (Art. 20) und Widerspruch (Art. 21). Außerdem
können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren
(Art. 77 DSGVO), z. B. beim Landesbeauftragten für den Datenschutz und
die Informationsfreiheit Rheinland-Pfalz.
</p>
<p>
Da wir keine Nutzerkonten führen, liegen die meisten Daten nur auf
Ihrem Gerät — dort können Sie sie selbst einsehen und löschen. Für
Anfragen zu den serverseitig verarbeiteten Daten wenden Sie sich an:
<a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
</p>
<h2>7. Warteliste / Launch-Benachrichtigung (Webseite)</h2>
<p>
Auf unserer Webseite können Sie sich für die Warteliste anmelden, um
benachrichtigt zu werden, wenn die App verfügbar ist. Dabei
verarbeiten wir Ihre E-Mail-Adresse auf Grundlage Ihrer Einwilligung
(Art. 6 Abs. 1 lit. a DSGVO). Wir verwenden die Adresse ausschließlich
für die Launch-Benachrichtigung und Informationen zur App — keine
Werbung Dritter, keine Weitergabe. Sie können sich jederzeit abmelden
(Link in jeder E-Mail oder formlos an
<a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>);
Ihre Adresse wird dann gelöscht.
</p>
<h2>8. Reichweitenmessung (Webseite)</h2>
<p>
Auf dieser Webseite nutzen wir Cloudflare Web Analytics (Cloudflare,
Inc., USA) — eine datenschutzfreundliche Besucherstatistik
<strong>ohne Cookies</strong> und ohne Verfolgung einzelner Personen.
Erfasst werden nur zusammengefasste Werte (z. B. Seitenaufrufe,
Herkunftsland, verwendeter Browser), aus denen sich keine Person
identifizieren lässt. Rechtsgrundlage ist unser berechtigtes Interesse
an einer sicheren und nachgefragten Webseite (Art. 6 Abs. 1 lit. f
DSGVO).
</p>
<h2>9. Keine Rechtsberatung</h2>
<p>
BehördenKlar erklärt Briefe verständlich, ersetzt aber keine
Rechtsberatung. Bei rechtlich wichtigen Entscheidungen wenden Sie sich
an eine Beratungsstelle oder eine Anwältin/einen Anwalt.
</p>
<h2>10. Änderungen</h2>
<p>
Wir passen diese Datenschutzerklärung an, wenn sich die App oder die
Rechtslage ändert. Die aktuelle Fassung finden Sie immer auf dieser
Seite.
</p>
</div>
<a class="zurueck" href="index.html">← Zurück zur Startseite</a>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

231
webseite/en.html Normal file
View File

@@ -0,0 +1,231 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BehördenKlar Understand German official letters</title>
<meta name="description" content="BehördenKlar explains German official letters in simple words — in English. Take a photo, understand the letter, respond the right way. Coming soon for iPhone.">
<link rel="alternate" hreflang="de" href="https://behoerdenklar.pages.dev/">
<link rel="alternate" hreflang="tr" href="https://behoerdenklar.pages.dev/tr">
<link rel="alternate" hreflang="ar" href="https://behoerdenklar.pages.dev/ar">
<link rel="alternate" hreflang="en" href="https://behoerdenklar.pages.dev/en">
<link rel="alternate" hreflang="uk" href="https://behoerdenklar.pages.dev/uk">
<link rel="alternate" hreflang="ru" href="https://behoerdenklar.pages.dev/ru">
<link rel="alternate" hreflang="pl" href="https://behoerdenklar.pages.dev/pl">
<style>
:root {
--papier: #faf7f0; --papier-dunkel: #f1ecdf; --tinte: #21211d;
--grau: #6b675e; --amtsblau: #1d4e89; --stempelrot: #b3372b;
--marker: #ffe66b; --gruen: #2e7d4f; color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33,33,29,0.025) 34px 35px),
var(--papier);
color: var(--tinte); line-height: 1.7; font-size: 17px;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte); font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
.sprachen {
padding: 12px 0 0; font-size: 15px; color: var(--grau);
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
}
.sprachen a { color: var(--amtsblau); text-decoration: none; border-bottom: 1px dotted var(--amtsblau); }
.sprachen .aktiv { font-weight: 700; color: var(--tinte); border-bottom: none; }
header { padding: 44px 0 8px; }
h1 { font-size: clamp(32px, 6.5vw, 48px); line-height: 1.14; font-weight: 700; }
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.intro { margin-top: 24px; font-size: 19px; max-width: 580px; }
.hero-fuss { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 28px; }
.cta {
display: inline-block; padding: 17px 30px; background: var(--amtsblau);
color: #fff; text-decoration: none; font-size: 18px; font-weight: 700;
border: 2px solid var(--tinte); box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.cta:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.stempel {
display: inline-block; padding: 7px 14px; border: 2.5px solid var(--stempelrot);
color: var(--stempelrot); font-family: 'Courier New', ui-monospace, monospace;
font-weight: 700; font-size: 14px; letter-spacing: 0.08em;
text-transform: uppercase; transform: rotate(-4deg); opacity: 0.9;
}
section { margin-top: 60px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
.verwandlung { display: grid; gap: 14px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33,33,29,0.28);
padding: 22px 24px; position: relative;
box-shadow: 4px 5px 0 rgba(33,33,29,0.08);
}
.brief-karte.amtsdeutsch { transform: rotate(-0.7deg); background: var(--papier-dunkel); }
.brief-karte.klartext { transform: rotate(0.5deg); }
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--tinte); color: var(--papier); padding: 3px 10px;
}
[dir="rtl"] .karten-label { left: auto; right: 16px; }
.brief-karte.klartext .karten-label { background: var(--gruen); }
.amtsdeutsch p {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 14.5px; color: #45423b; line-height: 1.75;
}
.klartext p { font-size: 18px; }
.pfeil { text-align: center; font-size: 15px; color: var(--grau); font-family: 'Courier New', ui-monospace, monospace; }
.hinweis { margin-top: 10px; font-size: 14.5px; color: var(--grau); }
.schritte { counter-reset: schritt; }
.schritt {
counter-increment: schritt; display: flex; gap: 20px; padding: 20px 4px;
border-bottom: 1px solid rgba(33,33,29,0.15); align-items: baseline;
}
.schritt:last-child { border-bottom: none; }
.schritt::before {
content: counter(schritt, decimal-leading-zero);
font-family: 'Courier New', ui-monospace, monospace;
font-size: 30px; font-weight: 700; color: var(--amtsblau);
flex-shrink: 0; width: 52px;
}
.schritt strong { font-size: 19px; display: block; margin-bottom: 2px; }
.schritt span { color: var(--grau); font-size: 16px; }
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33,33,29,0.9);
padding: 30px 28px; position: relative; margin-top: 8px;
}
.antrag .formular-kopf {
font-family: 'Courier New', ui-monospace, monospace; font-size: 13px;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--grau); margin-bottom: 6px;
}
.antrag h3 { font-size: 26px; line-height: 1.25; }
.antrag p { margin-top: 12px; max-width: 540px; }
.bonus {
margin: 18px 0; padding: 14px 16px; font-size: 17px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
}
.antrag .klein { font-size: 13.5px; color: var(--grau); margin-top: 14px; }
.antrag .klein a { color: var(--grau); }
a { color: var(--amtsblau); }
footer {
margin-top: 56px; padding-top: 14px; border-top: 2px solid var(--tinte);
font-size: 14.5px; color: var(--grau);
}
</style>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar</span>
<span class="az mono">Az: KLAR-01/2026</span>
</div>
<nav class="sprachen" aria-label="Choose language">
🌐
<a href="index.html" lang="de">Deutsch</a>
<a href="tr.html" lang="tr">Türkçe</a>
<a href="ar.html" lang="ar">العربية</a>
<span class="aktiv">English</span>
<a href="uk.html" lang="uk">Українська</a>
<a href="ru.html" lang="ru">Русский</a>
<a href="pl.html" lang="pl">Polski</a>
</nav>
<header>
<h1>A letter from a German office?<br><span class="markiert">Now easy to understand.</span></h1>
<p class="intro">
Take a photo of the letter. BehördenKlar explains it in simple German —
<strong>and in English</strong>. With all deadlines and a list of what
to do.
</p>
<div class="hero-fuss">
<a class="cta" href="#anmelden">Join the waiting list&nbsp;</a>
<span class="stempel">Coming soon for iPhone</span>
</div>
</header>
<section>
<h2>§ 1 &nbsp;The transformation</h2>
<div class="verwandlung">
<div class="brief-karte amtsdeutsch">
<span class="karten-label">Before · Original letter (German)</span>
<p>
„Gemäß § 32 SGB II wird bei Nichterscheinen ohne wichtigen Grund
eine Minderung des maßgebenden Regelbedarfs in Höhe von 10 vom
Hundert festgestellt."
</p>
</div>
<p class="pfeil">— 📷 one photo later —</p>
<div class="brief-karte klartext">
<span class="karten-label">After · BehördenKlar in English</span>
<p>
If you do not go to the appointment without a good reason, you will
get 10% less money for one month. Go to the appointment — or cancel
it in time.
</p>
</div>
</div>
<p class="hinweis">
The app explains letters in simple German and translates into English,
Turkish, Arabic, Russian, Ukrainian, French, Farsi, Romanian and Polish.
</p>
</section>
<section>
<h2>§ 2 &nbsp;How it works</h2>
<div class="schritte">
<div class="schritt"><div><strong>Take a photo</strong> <span>of the letter — or upload a PDF.</span></div></div>
<div class="schritt"><div><strong>Understand it</strong> <span>Simple words, in your language. A traffic light shows how urgent it is.</span></div></div>
<div class="schritt"><div><strong>Respond the right way</strong> <span>To-do list, deadline reminder, and a ready-made reply.</span></div></div>
</div>
</section>
<section id="anmelden">
<h2>§ 3 &nbsp;Early access</h2>
<div class="antrag">
<p class="formular-kopf">Form F-01 · Waiting list</p>
<h3>Be one of the first</h3>
<p>
The app is coming soon for iPhone. Sign up now — we will send you
<strong>one</strong> email when it is ready. No spam.
</p>
<p class="bonus">
🎁 <strong>Your gift:</strong> analyse <strong>5 letters for free</strong>
instead of 3. You get the code by email.
</p>
<a class="cta" href="mailto:behoerdenbriefhelfer@gmail.com?subject=Anmeldung%20Warteliste%20(English)&body=Hallo%2C%20bitte%20informiert%20mich%2C%20wenn%20Beh%C3%B6rdenKlar%20verf%C3%BCgbar%20ist.%20%2F%20Please%20notify%20me%20when%20Beh%C3%B6rdenKlar%20is%20available.">
📧 Sign up by email
</a>
<p class="klein">
We only store your email address for this one message. You can
unsubscribe at any time. <a href="datenschutz.html">Privacy policy (in German)</a>.
</p>
</div>
</section>
<footer>
Questions? 📧 <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
&nbsp;·&nbsp; <a href="impressum.html">Impressum (German)</a>
&nbsp;·&nbsp; <a href="datenschutz.html">Privacy policy (German)</a>
</footer>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

48
webseite/impressum.html Normal file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum BehördenKlar</title>
<meta name="robots" content="noindex">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #1a1a2e; background: #f4f8fb; line-height: 1.7;
}
.wrap { max-width: 640px; margin: 0 auto; padding: 32px 20px 64px; }
h1 { font-size: 28px; color: #144d8c; margin: 24px 0 16px; }
.karte {
background: #fff; border-radius: 16px; padding: 24px;
box-shadow: 0 2px 8px rgba(20, 77, 140, 0.08);
}
a { color: #1a6bc4; }
.zurueck { display: inline-block; margin-top: 24px; }
</style>
</head>
<body>
<div class="wrap">
<h1>Impressum</h1>
<div class="karte">
<p><strong>Angaben gemäß § 5 DDG:</strong></p>
<p style="margin-top: 12px;">
Justin Klein<br>
Am Schwimmbad 10<br>
67722 Winnweiler<br>
Deutschland
</p>
<p style="margin-top: 12px;">
E-Mail: <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
</p>
<p style="margin-top: 16px;">
<strong>Verantwortlich für den Inhalt:</strong><br>
Justin Klein (Anschrift wie oben)
</p>
</div>
<a class="zurueck" href="index.html">← Zurück zur Startseite</a>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

403
webseite/index.html Normal file
View File

@@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BehördenKlar Brief rein. Klartext raus.</title>
<meta name="description" content="BehördenKlar übersetzt Behördenbriefe in einfache Sprache. Foto machen, verstehen, richtig reagieren. Bald für iPhone Warteliste sichert 5 Gratis-Analysen.">
<link rel="alternate" hreflang="de" href="https://behoerdenklar.pages.dev/">
<link rel="alternate" hreflang="tr" href="https://behoerdenklar.pages.dev/tr">
<link rel="alternate" hreflang="ar" href="https://behoerdenklar.pages.dev/ar">
<link rel="alternate" hreflang="en" href="https://behoerdenklar.pages.dev/en">
<link rel="alternate" hreflang="uk" href="https://behoerdenklar.pages.dev/uk">
<link rel="alternate" hreflang="ru" href="https://behoerdenklar.pages.dev/ru">
<link rel="alternate" hreflang="pl" href="https://behoerdenklar.pages.dev/pl">
<style>
:root {
--papier: #faf7f0;
--papier-dunkel: #f1ecdf;
--tinte: #21211d;
--grau: #6b675e;
--amtsblau: #1d4e89;
--stempelrot: #b3372b;
--marker: #ffe66b;
--gruen: #2e7d4f;
color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33, 33, 29, 0.025) 34px 35px),
var(--papier);
color: var(--tinte);
line-height: 1.7;
font-size: 17px;
}
.mono {
font-family: 'Courier New', ui-monospace, monospace;
letter-spacing: 0.02em;
}
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
/* ---------- Briefkopf ---------- */
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte);
font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
/* Sprachleiste */
.sprachen {
padding: 12px 0 0; font-size: 15px; color: var(--grau);
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
}
.sprachen a { color: var(--amtsblau); text-decoration: none; border-bottom: 1px dotted var(--amtsblau); }
.sprachen .aktiv { font-weight: 700; color: var(--tinte); border-bottom: none; }
/* ---------- Hero ---------- */
header { padding: 44px 0 8px; }
.betreff-label {
font-size: 13px; color: var(--grau); text-transform: uppercase;
letter-spacing: 0.14em; margin-bottom: 10px;
}
h1 {
font-size: clamp(34px, 7vw, 52px);
line-height: 1.12; font-weight: 700; letter-spacing: -0.01em;
}
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.anrede { margin-top: 26px; font-size: 19px; max-width: 560px; }
.anrede strong { font-weight: 700; }
.hero-fuss { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 30px; }
.cta {
display: inline-block; padding: 17px 30px;
background: var(--amtsblau); color: #fff; text-decoration: none;
font-family: inherit; font-size: 18px; font-weight: 700;
border: 2px solid var(--tinte);
box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.cta:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.stempel {
display: inline-block; padding: 7px 14px;
border: 2.5px solid var(--stempelrot); color: var(--stempelrot);
font-family: 'Courier New', ui-monospace, monospace; font-weight: 700;
font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase;
transform: rotate(-4deg); opacity: 0.9;
mask-image: radial-gradient(circle at 30% 60%, black 92%, transparent 99%);
}
/* ---------- Abschnitte ---------- */
section { margin-top: 64px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
h2 .nr { font-family: 'Courier New', ui-monospace, monospace; }
/* ---------- Verwandlung (Kernstück) ---------- */
.verwandlung { display: grid; gap: 14px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33, 33, 29, 0.28);
padding: 22px 24px; position: relative;
box-shadow: 4px 5px 0 rgba(33, 33, 29, 0.08);
}
.brief-karte.amtsdeutsch {
transform: rotate(-0.7deg);
background: var(--papier-dunkel);
}
.brief-karte.klartext { transform: rotate(0.5deg); }
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--tinte); color: var(--papier); padding: 3px 10px;
}
.brief-karte.klartext .karten-label { background: var(--gruen); }
.amtsdeutsch p {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 14.5px; color: #45423b; line-height: 1.75;
}
.klartext p { font-size: 18px; }
.klartext .ampelzeile {
margin-top: 14px; padding-top: 12px;
border-top: 1px dashed rgba(33, 33, 29, 0.3);
font-size: 16px; font-weight: 700;
}
.pfeil {
text-align: center; font-size: 15px; color: var(--grau);
font-family: 'Courier New', ui-monospace, monospace;
}
.quelle-hinweis { margin-top: 10px; font-size: 13.5px; color: var(--grau); }
/* ---------- Zahlen ---------- */
.zahlen { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 560px) { .zahlen { grid-template-columns: 1fr; } }
.zahl-karte {
border: 1px dashed rgba(33, 33, 29, 0.4); padding: 20px 22px;
background: rgba(255, 255, 255, 0.55);
}
.zahl-karte .zahl { font-size: 38px; font-weight: 700; line-height: 1.1; color: var(--amtsblau); }
.zahl-karte .erkl { font-size: 15px; color: var(--grau); margin-top: 6px; }
.zahl-karte .quelle { font-size: 12px; color: var(--grau); margin-top: 8px; font-family: 'Courier New', ui-monospace, monospace; }
/* ---------- Schritte ---------- */
.schritte { counter-reset: schritt; display: grid; gap: 0; }
.schritt {
counter-increment: schritt;
display: flex; gap: 20px; padding: 20px 4px;
border-bottom: 1px solid rgba(33, 33, 29, 0.15);
align-items: baseline;
}
.schritt:last-child { border-bottom: none; }
.schritt::before {
content: counter(schritt, decimal-leading-zero);
font-family: 'Courier New', ui-monospace, monospace;
font-size: 30px; font-weight: 700; color: var(--amtsblau);
flex-shrink: 0; width: 52px;
}
.schritt strong { font-size: 19px; display: block; margin-bottom: 2px; }
.schritt span { color: var(--grau); font-size: 16px; }
/* ---------- Können ---------- */
.koennen { display: grid; grid-template-columns: 1fr 1fr; gap: 0 32px; }
@media (max-width: 560px) { .koennen { grid-template-columns: 1fr; } }
.kann {
padding: 13px 0; border-bottom: 1px dotted rgba(33, 33, 29, 0.3);
font-size: 16.5px;
}
.kann::before { content: '☑'; color: var(--gruen); font-size: 18px; margin-right: 10px; }
/* ---------- Antrag / Warteliste ---------- */
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33, 33, 29, 0.9);
padding: 30px 28px; position: relative; margin-top: 8px;
}
.antrag .formular-kopf {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 13px; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--grau); margin-bottom: 6px;
}
.antrag h3 { font-size: 27px; line-height: 1.25; }
.antrag .genehmigt {
position: absolute; top: -16px; right: 14px;
transform: rotate(6deg);
}
.antrag p { margin-top: 12px; max-width: 520px; }
.bonus-zeile {
margin: 18px 0; padding: 14px 16px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
font-size: 17px;
}
.antrag .klein { font-size: 13.5px; color: var(--grau); margin-top: 14px; }
.antrag .klein a { color: var(--grau); }
/* ---------- FAQ ---------- */
details {
border-bottom: 1px solid rgba(33, 33, 29, 0.2);
padding: 14px 4px;
}
summary {
font-weight: 700; cursor: pointer; font-size: 17.5px;
list-style: none; display: flex; justify-content: space-between; gap: 12px;
}
summary::-webkit-details-marker { display: none; }
summary::after { content: '+'; font-family: 'Courier New', monospace; font-size: 22px; color: var(--amtsblau); }
details[open] summary::after { content: ''; }
details p { margin-top: 10px; color: var(--grau); font-size: 16px; max-width: 580px; }
details a { color: var(--amtsblau); }
/* ---------- Fuß ---------- */
.grussformel { margin-top: 64px; font-size: 18px; }
.grussformel .unterschrift {
font-size: 30px; margin: 10px 0 2px;
font-style: italic;
}
.anlage {
margin-top: 26px; padding-top: 14px; border-top: 2px solid var(--tinte);
font-family: 'Courier New', ui-monospace, monospace; font-size: 13.5px; color: var(--grau);
}
footer { margin-top: 20px; font-size: 14.5px; color: var(--grau); }
footer a { color: var(--grau); }
</style>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar</span>
<span class="az mono">Az: KLAR-01/2026 &nbsp;·&nbsp; Anlage: 1 App</span>
</div>
<nav class="sprachen" aria-label="Sprache wählen">
🌐
<span class="aktiv">Deutsch</span>
<a href="tr.html" lang="tr">Türkçe</a>
<a href="ar.html" lang="ar">العربية</a>
<a href="en.html" lang="en">English</a>
<a href="uk.html" lang="uk">Українська</a>
<a href="ru.html" lang="ru">Русский</a>
<a href="pl.html" lang="pl">Polski</a>
</nav>
<header>
<p class="betreff-label">Betreff</p>
<h1>Brief vom Amt.<br><span class="markiert">Endlich verstanden.</span></h1>
<p class="anrede">
Sehr geehrte Damen und Herren, <strong>ein Brief vom Amt muss nicht
schwer sein.</strong> Machen Sie ein Foto. BehördenKlar erklärt den
Brief in einfachen Worten. <em>Brief rein. Klartext raus.</em>
</p>
<div class="hero-fuss">
<a class="cta" href="#anmelden">Jetzt anmelden&nbsp;</a>
<span class="stempel">Bald im App&nbsp;Store</span>
</div>
</header>
<section>
<h2><span class="nr">§ 1</span> Vorher und Nachher</h2>
<div class="verwandlung">
<div class="brief-karte amtsdeutsch">
<span class="karten-label">Vorher · Original vom Amt</span>
<p>
„Gemäß § 32 SGB II wird bei Nichterscheinen ohne wichtigen Grund
eine Minderung des maßgebenden Regelbedarfs in Höhe von 10 vom
Hundert für die Dauer eines Monats festgestellt."
</p>
</div>
<p class="pfeil">— 📷 ein Foto später —</p>
<div class="brief-karte klartext">
<span class="karten-label">Nachher · BehördenKlar</span>
<p>
Wenn Sie ohne guten Grund nicht zum Termin kommen, bekommen Sie
einen Monat lang 10&nbsp;% weniger Geld. Gehen Sie hin — oder sagen
Sie rechtzeitig ab.
</p>
<p class="ampelzeile">🔴 Termin in 5 Tagen &nbsp;·&nbsp; ⏰ Erinnerung ist gestellt</p>
</div>
</div>
<p class="quelle-hinweis">
Dazu bekommen Sie: eine Liste, was Sie tun müssen — und wenn Sie
wollen, eine fertige Antwort.
</p>
</section>
<section>
<h2><span class="nr">§ 2</span> Sie sind nicht allein</h2>
<div class="zahlen">
<div class="zahl-karte">
<div class="zahl">6,2 Mio.</div>
<div class="erkl">Erwachsene in Deutschland haben Probleme mit schweren Texten. Behördenbriefe sind für sie besonders schwer.</div>
<div class="quelle">Quelle: LEO-Studie 2018, Uni Hamburg</div>
</div>
<div class="zahl-karte">
<div class="zahl">~ 60 Sek.</div>
<div class="erkl">braucht BehördenKlar, um einen Brief zu erklären — in Deutsch oder 9 weiteren Sprachen.</div>
<div class="quelle">Türkçe · العربية · English · Українська …</div>
</div>
</div>
</section>
<section>
<h2><span class="nr">§ 3</span> So funktioniert es</h2>
<div class="schritte">
<div class="schritt">
<div><strong>Brief fotografieren</strong> <span>Mit dem Handy, auch als PDF. Gutes Licht reicht.</span></div>
</div>
<div class="schritt">
<div><strong>Klartext lesen</strong> <span>„Was will das Amt von mir?" — kurz, einfach, ohne schwere Wörter. Die Ampel zeigt: Wie viel Zeit habe ich noch?</span></div>
</div>
<div class="schritt">
<div><strong>Richtig reagieren</strong> <span>Liste abhaken, Erinnerung stellen, fertige Antwort anpassen und abschicken.</span></div>
</div>
</div>
</section>
<section>
<h2><span class="nr">§ 4</span> Was die App kann</h2>
<div class="koennen">
<div class="kann">Ampel: Wie dringend? 🔴🟡🟢</div>
<div class="kann">Übersetzung in 9 Sprachen</div>
<div class="kann">Fertige Antworten</div>
<div class="kann">Erinnerung vor jeder Frist</div>
<div class="kann">Termine in den Kalender</div>
<div class="kann">Amts-Wörterbuch — auch ohne Internet</div>
<div class="kann">Die App kann vorlesen</div>
<div class="kann">Kein Konto, keine Anmeldung</div>
</div>
</section>
<section id="anmelden">
<h2><span class="nr">§ 5</span> Warteliste</h2>
<div class="antrag">
<span class="stempel genehmigt">Vorab genehmigt ✓</span>
<p class="formular-kopf">Formular F-01 · Warteliste</p>
<h3>Seien Sie früh dabei</h3>
<p>
Tragen Sie sich jetzt ein. Wir schicken Ihnen <strong>eine</strong>
E-Mail, wenn die App fertig ist. Keine Werbung.
</p>
<p class="bonus-zeile">
🎁 <strong>Ihr Dankeschön:</strong> Sie können <strong>5 Briefe
gratis</strong> erklären lassen — statt 3. Den Code bekommen Sie
per E-Mail.
</p>
<a class="cta" href="mailto:behoerdenbriefhelfer@gmail.com?subject=Anmeldung%20Warteliste&body=Hallo%2C%20bitte%20informiert%20mich%2C%20wenn%20Beh%C3%B6rdenKlar%20verf%C3%BCgbar%20ist.">
📧 Per E-Mail eintragen
</a>
<p class="klein">
Wir speichern Ihre E-Mail-Adresse nur für diese eine Nachricht.
Sie können sich jederzeit abmelden — <a href="datenschutz.html">Datenschutzerklärung</a>.
</p>
</div>
</section>
<section>
<h2><span class="nr">§ 6</span> Häufige Fragen</h2>
<details>
<summary>Was kostet die App?</summary>
<p>Die App ist gratis. Die ersten 3 Briefe sind auch gratis (mit Code: 5). Danach gibt es ein günstiges Abo. Sie testen zuerst — dann entscheiden Sie.</p>
</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. Mehr dazu in der <a href="datenschutz.html">Datenschutzerklärung</a>.</p>
</details>
<details>
<summary>Ersetzt die App einen Anwalt?</summary>
<p>Nein. Die App erklärt Briefe — sie ist keine Rechtsberatung. Bei wichtigen Entscheidungen (z.&nbsp;B. Widerspruch) hilft eine Beratungsstelle oder ein Anwalt.</p>
</details>
<details>
<summary>Kommt die App auch für Android?</summary>
<p>Ja, Android ist geplant. Das iPhone kommt zuerst. Auf der Warteliste erfahren Sie es als Erste.</p>
</details>
</section>
<div class="grussformel">
<p>Mit freundlichen und verständlichen Grüßen</p>
<p class="unterschrift">Ihr BehördenKlar</p>
<div class="anlage">
Anlagen: <a href="impressum.html" style="color: inherit;">Impressum</a> ·
<a href="datenschutz.html" style="color: inherit;">Datenschutzerklärung</a> ·
Kontakt: <a href="mailto:behoerdenbriefhelfer@gmail.com" style="color: inherit;">behoerdenbriefhelfer@gmail.com</a>
</div>
<footer>
Keine Sorge: Diese Seite ist kein echter Amtsbrief. Hier gibt es
keine Frist und kein Kleingedrucktes — nur eine App, die hilft.
</footer>
</div>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

233
webseite/pl.html Normal file
View File

@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BehördenKlar Zrozum niemieckie pisma urzędowe</title>
<meta name="description" content="BehördenKlar wyjaśnia niemieckie pisma urzędowe prostymi słowami — po polsku. Zrób zdjęcie, zrozum list, zareaguj właściwie. Wkrótce na iPhone'a.">
<link rel="alternate" hreflang="de" href="https://behoerdenklar.pages.dev/">
<link rel="alternate" hreflang="tr" href="https://behoerdenklar.pages.dev/tr">
<link rel="alternate" hreflang="ar" href="https://behoerdenklar.pages.dev/ar">
<link rel="alternate" hreflang="en" href="https://behoerdenklar.pages.dev/en">
<link rel="alternate" hreflang="uk" href="https://behoerdenklar.pages.dev/uk">
<link rel="alternate" hreflang="ru" href="https://behoerdenklar.pages.dev/ru">
<link rel="alternate" hreflang="pl" href="https://behoerdenklar.pages.dev/pl">
<style>
:root {
--papier: #faf7f0; --papier-dunkel: #f1ecdf; --tinte: #21211d;
--grau: #6b675e; --amtsblau: #1d4e89; --stempelrot: #b3372b;
--marker: #ffe66b; --gruen: #2e7d4f; color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33,33,29,0.025) 34px 35px),
var(--papier);
color: var(--tinte); line-height: 1.7; font-size: 17px;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte); font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
.sprachen {
padding: 12px 0 0; font-size: 15px; color: var(--grau);
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
}
.sprachen a { color: var(--amtsblau); text-decoration: none; border-bottom: 1px dotted var(--amtsblau); }
.sprachen .aktiv { font-weight: 700; color: var(--tinte); border-bottom: none; }
header { padding: 44px 0 8px; }
h1 { font-size: clamp(32px, 6.5vw, 48px); line-height: 1.14; font-weight: 700; }
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.intro { margin-top: 24px; font-size: 19px; max-width: 580px; }
.hero-fuss { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 28px; }
.cta {
display: inline-block; padding: 17px 30px; background: var(--amtsblau);
color: #fff; text-decoration: none; font-size: 18px; font-weight: 700;
border: 2px solid var(--tinte); box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.cta:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.stempel {
display: inline-block; padding: 7px 14px; border: 2.5px solid var(--stempelrot);
color: var(--stempelrot); font-family: 'Courier New', ui-monospace, monospace;
font-weight: 700; font-size: 14px; letter-spacing: 0.08em;
text-transform: uppercase; transform: rotate(-4deg); opacity: 0.9;
}
section { margin-top: 60px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
.verwandlung { display: grid; gap: 14px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33,33,29,0.28);
padding: 22px 24px; position: relative;
box-shadow: 4px 5px 0 rgba(33,33,29,0.08);
}
.brief-karte.amtsdeutsch { transform: rotate(-0.7deg); background: var(--papier-dunkel); }
.brief-karte.klartext { transform: rotate(0.5deg); }
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--tinte); color: var(--papier); padding: 3px 10px;
}
[dir="rtl"] .karten-label { left: auto; right: 16px; }
.brief-karte.klartext .karten-label { background: var(--gruen); }
.amtsdeutsch p {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 14.5px; color: #45423b; line-height: 1.75;
}
.klartext p { font-size: 18px; }
.pfeil { text-align: center; font-size: 15px; color: var(--grau); font-family: 'Courier New', ui-monospace, monospace; }
.hinweis { margin-top: 10px; font-size: 14.5px; color: var(--grau); }
.schritte { counter-reset: schritt; }
.schritt {
counter-increment: schritt; display: flex; gap: 20px; padding: 20px 4px;
border-bottom: 1px solid rgba(33,33,29,0.15); align-items: baseline;
}
.schritt:last-child { border-bottom: none; }
.schritt::before {
content: counter(schritt, decimal-leading-zero);
font-family: 'Courier New', ui-monospace, monospace;
font-size: 30px; font-weight: 700; color: var(--amtsblau);
flex-shrink: 0; width: 52px;
}
.schritt strong { font-size: 19px; display: block; margin-bottom: 2px; }
.schritt span { color: var(--grau); font-size: 16px; }
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33,33,29,0.9);
padding: 30px 28px; position: relative; margin-top: 8px;
}
.antrag .formular-kopf {
font-family: 'Courier New', ui-monospace, monospace; font-size: 13px;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--grau); margin-bottom: 6px;
}
.antrag h3 { font-size: 26px; line-height: 1.25; }
.antrag p { margin-top: 12px; max-width: 540px; }
.bonus {
margin: 18px 0; padding: 14px 16px; font-size: 17px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
}
.antrag .klein { font-size: 13.5px; color: var(--grau); margin-top: 14px; }
.antrag .klein a { color: var(--grau); }
a { color: var(--amtsblau); }
footer {
margin-top: 56px; padding-top: 14px; border-top: 2px solid var(--tinte);
font-size: 14.5px; color: var(--grau);
}
</style>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar</span>
<span class="az mono">Az: KLAR-01/2026</span>
</div>
<nav class="sprachen" aria-label="Wybierz język">
🌐
<a href="index.html" lang="de">Deutsch</a>
<a href="tr.html" lang="tr">Türkçe</a>
<a href="ar.html" lang="ar">العربية</a>
<a href="en.html" lang="en">English</a>
<a href="uk.html" lang="uk">Українська</a>
<a href="ru.html" lang="ru">Русский</a>
<span class="aktiv">Polski</span>
</nav>
<header>
<h1>List z niemieckiego urzędu?<br><span class="markiert">Teraz łatwo go zrozumieć.</span></h1>
<p class="intro">
Zrób zdjęcie listu. BehördenKlar wyjaśni go prostym niemieckim —
<strong>i po polsku</strong>. Ze wszystkimi terminami i listą, co
trzeba zrobić.
</p>
<div class="hero-fuss">
<a class="cta" href="#anmelden">Zapisz się na listę oczekujących&nbsp;</a>
<span class="stempel">Wkrótce na iPhone'a</span>
</div>
</header>
<section>
<h2>§ 1 &nbsp;Przemiana</h2>
<div class="verwandlung">
<div class="brief-karte amtsdeutsch">
<span class="karten-label">Przed · Oryginalny list (po niemiecku)</span>
<p>
„Gemäß § 32 SGB II wird bei Nichterscheinen ohne wichtigen Grund
eine Minderung des maßgebenden Regelbedarfs in Höhe von 10 vom
Hundert festgestellt."
</p>
</div>
<p class="pfeil">— 📷 jedno zdjęcie później —</p>
<div class="brief-karte klartext">
<span class="karten-label">Po · BehördenKlar po polsku</span>
<p>
Jeśli nie przyjdziesz na spotkanie bez ważnego powodu, przez
miesiąc będziesz dostawać o 10% mniej pieniędzy. Idź na spotkanie
— albo odwołaj je na czas.
</p>
</div>
</div>
<p class="hinweis">
Aplikacja wyjaśnia listy prostym niemieckim i tłumaczy na polski,
turecki, arabski, angielski, rosyjski, ukraiński, francuski, farsi
i rumuński.
</p>
</section>
<section>
<h2>§ 2 &nbsp;Jak to działa</h2>
<div class="schritte">
<div class="schritt"><div><strong>Zrób zdjęcie listu</strong> <span>albo wgraj PDF.</span></div></div>
<div class="schritt"><div><strong>Zrozum go</strong> <span>Proste słowa, w Twoim języku. Sygnalizacja pokazuje, jak pilna jest sprawa.</span></div></div>
<div class="schritt"><div><strong>Zareaguj właściwie</strong> <span>Lista zadań, przypomnienie o terminie i gotowa odpowiedź.</span></div></div>
</div>
</section>
<section id="anmelden">
<h2>§ 3 &nbsp;Wcześniejszy dostęp</h2>
<div class="antrag">
<p class="formular-kopf">Formularz F-01 · Lista oczekujących</p>
<h3>Bądź wśród pierwszych</h3>
<p>
Aplikacja wkrótce pojawi się na iPhone'a. Zapisz się teraz —
wyślemy Ci <strong>jedną</strong> wiadomość, gdy wszystko będzie
gotowe. Bez reklam.
</p>
<p class="bonus">
🎁 <strong>Twój prezent:</strong> analizuj <strong>5 listów za
darmo</strong> zamiast 3. Kod dostaniesz e-mailem.
</p>
<a class="cta" href="mailto:behoerdenbriefhelfer@gmail.com?subject=Anmeldung%20Warteliste%20(Polnisch)&body=Hallo%2C%20bitte%20informiert%20mich%2C%20wenn%20Beh%C3%B6rdenKlar%20verf%C3%BCgbar%20ist.%20%2F%20Please%20notify%20me%20when%20Beh%C3%B6rdenKlar%20is%20available.">
📧 Zapisz się e-mailem
</a>
<p class="klein">
Zapisujemy Twój adres tylko dla tej jednej wiadomości. Możesz
wypisać się w każdej chwili. <a href="datenschutz.html">Polityka prywatności (po niemiecku)</a>.
</p>
</div>
</section>
<footer>
Pytania? 📧 <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
&nbsp;·&nbsp; <a href="impressum.html">Impressum (po niemiecku)</a>
&nbsp;·&nbsp; <a href="datenschutz.html">Polityka prywatności (po niemiecku)</a>
</footer>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

232
webseite/ru.html Normal file
View File

@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BehördenKlar Поймите немецкие официальные письма</title>
<meta name="description" content="BehördenKlar объясняет немецкие официальные письма простыми словами — по-русски. Сфотографируйте письмо, поймите его, правильно отреагируйте. Скоро для iPhone.">
<link rel="alternate" hreflang="de" href="https://behoerdenklar.pages.dev/">
<link rel="alternate" hreflang="tr" href="https://behoerdenklar.pages.dev/tr">
<link rel="alternate" hreflang="ar" href="https://behoerdenklar.pages.dev/ar">
<link rel="alternate" hreflang="en" href="https://behoerdenklar.pages.dev/en">
<link rel="alternate" hreflang="uk" href="https://behoerdenklar.pages.dev/uk">
<link rel="alternate" hreflang="ru" href="https://behoerdenklar.pages.dev/ru">
<link rel="alternate" hreflang="pl" href="https://behoerdenklar.pages.dev/pl">
<style>
:root {
--papier: #faf7f0; --papier-dunkel: #f1ecdf; --tinte: #21211d;
--grau: #6b675e; --amtsblau: #1d4e89; --stempelrot: #b3372b;
--marker: #ffe66b; --gruen: #2e7d4f; color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33,33,29,0.025) 34px 35px),
var(--papier);
color: var(--tinte); line-height: 1.7; font-size: 17px;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte); font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
.sprachen {
padding: 12px 0 0; font-size: 15px; color: var(--grau);
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
}
.sprachen a { color: var(--amtsblau); text-decoration: none; border-bottom: 1px dotted var(--amtsblau); }
.sprachen .aktiv { font-weight: 700; color: var(--tinte); border-bottom: none; }
header { padding: 44px 0 8px; }
h1 { font-size: clamp(32px, 6.5vw, 48px); line-height: 1.14; font-weight: 700; }
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.intro { margin-top: 24px; font-size: 19px; max-width: 580px; }
.hero-fuss { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 28px; }
.cta {
display: inline-block; padding: 17px 30px; background: var(--amtsblau);
color: #fff; text-decoration: none; font-size: 18px; font-weight: 700;
border: 2px solid var(--tinte); box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.cta:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.stempel {
display: inline-block; padding: 7px 14px; border: 2.5px solid var(--stempelrot);
color: var(--stempelrot); font-family: 'Courier New', ui-monospace, monospace;
font-weight: 700; font-size: 14px; letter-spacing: 0.08em;
text-transform: uppercase; transform: rotate(-4deg); opacity: 0.9;
}
section { margin-top: 60px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
.verwandlung { display: grid; gap: 14px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33,33,29,0.28);
padding: 22px 24px; position: relative;
box-shadow: 4px 5px 0 rgba(33,33,29,0.08);
}
.brief-karte.amtsdeutsch { transform: rotate(-0.7deg); background: var(--papier-dunkel); }
.brief-karte.klartext { transform: rotate(0.5deg); }
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--tinte); color: var(--papier); padding: 3px 10px;
}
[dir="rtl"] .karten-label { left: auto; right: 16px; }
.brief-karte.klartext .karten-label { background: var(--gruen); }
.amtsdeutsch p {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 14.5px; color: #45423b; line-height: 1.75;
}
.klartext p { font-size: 18px; }
.pfeil { text-align: center; font-size: 15px; color: var(--grau); font-family: 'Courier New', ui-monospace, monospace; }
.hinweis { margin-top: 10px; font-size: 14.5px; color: var(--grau); }
.schritte { counter-reset: schritt; }
.schritt {
counter-increment: schritt; display: flex; gap: 20px; padding: 20px 4px;
border-bottom: 1px solid rgba(33,33,29,0.15); align-items: baseline;
}
.schritt:last-child { border-bottom: none; }
.schritt::before {
content: counter(schritt, decimal-leading-zero);
font-family: 'Courier New', ui-monospace, monospace;
font-size: 30px; font-weight: 700; color: var(--amtsblau);
flex-shrink: 0; width: 52px;
}
.schritt strong { font-size: 19px; display: block; margin-bottom: 2px; }
.schritt span { color: var(--grau); font-size: 16px; }
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33,33,29,0.9);
padding: 30px 28px; position: relative; margin-top: 8px;
}
.antrag .formular-kopf {
font-family: 'Courier New', ui-monospace, monospace; font-size: 13px;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--grau); margin-bottom: 6px;
}
.antrag h3 { font-size: 26px; line-height: 1.25; }
.antrag p { margin-top: 12px; max-width: 540px; }
.bonus {
margin: 18px 0; padding: 14px 16px; font-size: 17px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
}
.antrag .klein { font-size: 13.5px; color: var(--grau); margin-top: 14px; }
.antrag .klein a { color: var(--grau); }
a { color: var(--amtsblau); }
footer {
margin-top: 56px; padding-top: 14px; border-top: 2px solid var(--tinte);
font-size: 14.5px; color: var(--grau);
}
</style>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar</span>
<span class="az mono">Az: KLAR-01/2026</span>
</div>
<nav class="sprachen" aria-label="Выберите язык">
🌐
<a href="index.html" lang="de">Deutsch</a>
<a href="tr.html" lang="tr">Türkçe</a>
<a href="ar.html" lang="ar">العربية</a>
<a href="en.html" lang="en">English</a>
<a href="uk.html" lang="uk">Українська</a>
<span class="aktiv">Русский</span>
<a href="pl.html" lang="pl">Polski</a>
</nav>
<header>
<h1>Письмо из немецкого ведомства?<br><span class="markiert">Теперь его легко понять.</span></h1>
<p class="intro">
Сфотографируйте письмо. BehördenKlar объяснит его простым немецким —
<strong>и по-русски</strong>. Со всеми сроками и списком, что делать.
</p>
<div class="hero-fuss">
<a class="cta" href="#anmelden">Записаться в список ожидания&nbsp;</a>
<span class="stempel">Скоро для iPhone</span>
</div>
</header>
<section>
<h2>§ 1 &nbsp;Превращение</h2>
<div class="verwandlung">
<div class="brief-karte amtsdeutsch">
<span class="karten-label">До · Оригинал письма (на немецком)</span>
<p>
„Gemäß § 32 SGB II wird bei Nichterscheinen ohne wichtigen Grund
eine Minderung des maßgebenden Regelbedarfs in Höhe von 10 vom
Hundert festgestellt."
</p>
</div>
<p class="pfeil">— 📷 одно фото — и готово —</p>
<div class="brief-karte klartext">
<span class="karten-label">После · BehördenKlar по-русски</span>
<p>
Если вы не придёте на приём без уважительной причины, вы будете
получать на 10% меньше денег в течение одного месяца. Придите на
приём — или вовремя отмените его.
</p>
</div>
</div>
<p class="hinweis">
Приложение объясняет письма простым немецким языком и переводит на
русский, турецкий, арабский, английский, украинский, французский,
фарси, румынский и польский.
</p>
</section>
<section>
<h2>§ 2 &nbsp;Как это работает</h2>
<div class="schritte">
<div class="schritt"><div><strong>Сфотографируйте письмо</strong> <span>или загрузите PDF.</span></div></div>
<div class="schritt"><div><strong>Поймите его</strong> <span>Простые слова, на вашем языке. Светофор показывает, насколько это срочно.</span></div></div>
<div class="schritt"><div><strong>Правильно отреагируйте</strong> <span>Список дел, напоминание о сроке и готовый ответ.</span></div></div>
</div>
</section>
<section id="anmelden">
<h2>§ 3 &nbsp;Ранний доступ</h2>
<div class="antrag">
<p class="formular-kopf">Формуляр F-01 · Список ожидания</p>
<h3>Будьте в числе первых</h3>
<p>
Приложение скоро выйдет для iPhone. Запишитесь сейчас — мы пришлём
вам <strong>одно</strong> письмо, когда всё будет готово. Без
рекламы.
</p>
<p class="bonus">
🎁 <strong>Ваш подарок:</strong> анализируйте <strong>5 писем
бесплатно</strong> вместо 3. Код придёт по электронной почте.
</p>
<a class="cta" href="mailto:behoerdenbriefhelfer@gmail.com?subject=Anmeldung%20Warteliste%20(Russisch)&body=Hallo%2C%20bitte%20informiert%20mich%2C%20wenn%20Beh%C3%B6rdenKlar%20verf%C3%BCgbar%20ist.%20%2F%20Please%20notify%20me%20when%20Beh%C3%B6rdenKlar%20is%20available.">
📧 Записаться по электронной почте
</a>
<p class="klein">
Мы храним ваш адрес только для этого одного сообщения. Вы можете
отписаться в любой момент. <a href="datenschutz.html">Политика конфиденциальности (на немецком)</a>.
</p>
</div>
</section>
<footer>
Вопросы? 📧 <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
&nbsp;·&nbsp; <a href="impressum.html">Impressum (на немецком)</a>
&nbsp;·&nbsp; <a href="datenschutz.html">Политика конфиденциальности (на немецком)</a>
</footer>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

232
webseite/tr.html Normal file
View File

@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BehördenKlar Alman resmî mektuplarını anlayın</title>
<meta name="description" content="BehördenKlar Alman resmî mektuplarını basit kelimelerle — Türkçe — açıklar. Fotoğraf çekin, mektubu anlayın, doğru cevap verin. Yakında iPhone için.">
<link rel="alternate" hreflang="de" href="https://behoerdenklar.pages.dev/">
<link rel="alternate" hreflang="tr" href="https://behoerdenklar.pages.dev/tr">
<link rel="alternate" hreflang="ar" href="https://behoerdenklar.pages.dev/ar">
<link rel="alternate" hreflang="en" href="https://behoerdenklar.pages.dev/en">
<link rel="alternate" hreflang="uk" href="https://behoerdenklar.pages.dev/uk">
<link rel="alternate" hreflang="ru" href="https://behoerdenklar.pages.dev/ru">
<link rel="alternate" hreflang="pl" href="https://behoerdenklar.pages.dev/pl">
<style>
:root {
--papier: #faf7f0; --papier-dunkel: #f1ecdf; --tinte: #21211d;
--grau: #6b675e; --amtsblau: #1d4e89; --stempelrot: #b3372b;
--marker: #ffe66b; --gruen: #2e7d4f; color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33,33,29,0.025) 34px 35px),
var(--papier);
color: var(--tinte); line-height: 1.7; font-size: 17px;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte); font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
.sprachen {
padding: 12px 0 0; font-size: 15px; color: var(--grau);
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
}
.sprachen a { color: var(--amtsblau); text-decoration: none; border-bottom: 1px dotted var(--amtsblau); }
.sprachen .aktiv { font-weight: 700; color: var(--tinte); border-bottom: none; }
header { padding: 44px 0 8px; }
h1 { font-size: clamp(32px, 6.5vw, 48px); line-height: 1.14; font-weight: 700; }
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.intro { margin-top: 24px; font-size: 19px; max-width: 580px; }
.hero-fuss { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 28px; }
.cta {
display: inline-block; padding: 17px 30px; background: var(--amtsblau);
color: #fff; text-decoration: none; font-size: 18px; font-weight: 700;
border: 2px solid var(--tinte); box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.cta:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.stempel {
display: inline-block; padding: 7px 14px; border: 2.5px solid var(--stempelrot);
color: var(--stempelrot); font-family: 'Courier New', ui-monospace, monospace;
font-weight: 700; font-size: 14px; letter-spacing: 0.08em;
text-transform: uppercase; transform: rotate(-4deg); opacity: 0.9;
}
section { margin-top: 60px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
.verwandlung { display: grid; gap: 14px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33,33,29,0.28);
padding: 22px 24px; position: relative;
box-shadow: 4px 5px 0 rgba(33,33,29,0.08);
}
.brief-karte.amtsdeutsch { transform: rotate(-0.7deg); background: var(--papier-dunkel); }
.brief-karte.klartext { transform: rotate(0.5deg); }
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--tinte); color: var(--papier); padding: 3px 10px;
}
[dir="rtl"] .karten-label { left: auto; right: 16px; }
.brief-karte.klartext .karten-label { background: var(--gruen); }
.amtsdeutsch p {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 14.5px; color: #45423b; line-height: 1.75;
}
.klartext p { font-size: 18px; }
.pfeil { text-align: center; font-size: 15px; color: var(--grau); font-family: 'Courier New', ui-monospace, monospace; }
.hinweis { margin-top: 10px; font-size: 14.5px; color: var(--grau); }
.schritte { counter-reset: schritt; }
.schritt {
counter-increment: schritt; display: flex; gap: 20px; padding: 20px 4px;
border-bottom: 1px solid rgba(33,33,29,0.15); align-items: baseline;
}
.schritt:last-child { border-bottom: none; }
.schritt::before {
content: counter(schritt, decimal-leading-zero);
font-family: 'Courier New', ui-monospace, monospace;
font-size: 30px; font-weight: 700; color: var(--amtsblau);
flex-shrink: 0; width: 52px;
}
.schritt strong { font-size: 19px; display: block; margin-bottom: 2px; }
.schritt span { color: var(--grau); font-size: 16px; }
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33,33,29,0.9);
padding: 30px 28px; position: relative; margin-top: 8px;
}
.antrag .formular-kopf {
font-family: 'Courier New', ui-monospace, monospace; font-size: 13px;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--grau); margin-bottom: 6px;
}
.antrag h3 { font-size: 26px; line-height: 1.25; }
.antrag p { margin-top: 12px; max-width: 540px; }
.bonus {
margin: 18px 0; padding: 14px 16px; font-size: 17px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
}
.antrag .klein { font-size: 13.5px; color: var(--grau); margin-top: 14px; }
.antrag .klein a { color: var(--grau); }
a { color: var(--amtsblau); }
footer {
margin-top: 56px; padding-top: 14px; border-top: 2px solid var(--tinte);
font-size: 14.5px; color: var(--grau);
}
</style>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar</span>
<span class="az mono">Az: KLAR-01/2026</span>
</div>
<nav class="sprachen" aria-label="Dil seçin">
🌐
<a href="index.html" lang="de">Deutsch</a>
<span class="aktiv">Türkçe</span>
<a href="ar.html" lang="ar">العربية</a>
<a href="en.html" lang="en">English</a>
<a href="uk.html" lang="uk">Українська</a>
<a href="ru.html" lang="ru">Русский</a>
<a href="pl.html" lang="pl">Polski</a>
</nav>
<header>
<h1>Alman dairesinden mektup mu geldi?<br><span class="markiert">Artık anlamak kolay.</span></h1>
<p class="intro">
Mektubun fotoğrafını çekin. BehördenKlar mektubu basit Almanca ile —
<strong>ve Türkçe olarak</strong> — açıklar. Bütün süreler ve
yapılacaklar listesi ile birlikte.
</p>
<div class="hero-fuss">
<a class="cta" href="#anmelden">Bekleme listesine katıl&nbsp;</a>
<span class="stempel">Yakında iPhone'da</span>
</div>
</header>
<section>
<h2>§ 1 &nbsp;Dönüşüm</h2>
<div class="verwandlung">
<div class="brief-karte amtsdeutsch">
<span class="karten-label">Önce · Orijinal mektup (Almanca)</span>
<p>
„Gemäß § 32 SGB II wird bei Nichterscheinen ohne wichtigen Grund
eine Minderung des maßgebenden Regelbedarfs in Höhe von 10 vom
Hundert festgestellt."
</p>
</div>
<p class="pfeil">— 📷 bir fotoğraf sonra —</p>
<div class="brief-karte klartext">
<span class="karten-label">Sonra · BehördenKlar Türkçe</span>
<p>
Geçerli bir neden olmadan randevuya gitmezseniz, bir ay boyunca
%10 daha az para alırsınız. Randevuya gidin — veya zamanında
iptal edin.
</p>
</div>
</div>
<p class="hinweis">
Uygulama mektupları basit Almanca ile açıklar ve şu dillere çevirir:
Türkçe, Arapça, İngilizce, Rusça, Ukraynaca, Fransızca, Farsça,
Romence ve Lehçe.
</p>
</section>
<section>
<h2>§ 2 &nbsp;Nasıl çalışır</h2>
<div class="schritte">
<div class="schritt"><div><strong>Fotoğraf çekin</strong> <span>mektubun fotoğrafını çekin — veya PDF yükleyin.</span></div></div>
<div class="schritt"><div><strong>Anlayın</strong> <span>Basit kelimeler, kendi dilinizde. Bir lamba işin ne kadar acil olduğunu gösterir.</span></div></div>
<div class="schritt"><div><strong>Doğru cevap verin</strong> <span>Yapılacaklar listesi, süre hatırlatması ve hazır bir cevap.</span></div></div>
</div>
</section>
<section id="anmelden">
<h2>§ 3 &nbsp;Erken erişim</h2>
<div class="antrag">
<p class="formular-kopf">Form F-01 · Bekleme listesi</p>
<h3>İlk kullananlardan olun</h3>
<p>
Uygulama yakında iPhone için geliyor. Şimdi kaydolun — uygulama
hazır olunca size <strong>bir</strong> e-posta göndeririz. Reklam yok.
</p>
<p class="bonus">
🎁 <strong>Hediyeniz:</strong> 3 yerine <strong>5 mektubu ücretsiz</strong>
analiz edin. Kodu e-posta ile alırsınız.
</p>
<a class="cta" href="mailto:behoerdenbriefhelfer@gmail.com?subject=Anmeldung%20Warteliste%20(Tuerkisch)&body=Hallo%2C%20bitte%20informiert%20mich%2C%20wenn%20Beh%C3%B6rdenKlar%20verf%C3%BCgbar%20ist.%20%2F%20Please%20notify%20me%20when%20Beh%C3%B6rdenKlar%20is%20available.">
📧 E-posta ile kaydol
</a>
<p class="klein">
E-posta adresinizi sadece bu bir mesaj için saklarız. İstediğiniz
zaman listeden çıkabilirsiniz. <a href="datenschutz.html">Gizlilik politikası (Almanca)</a>.
</p>
</div>
</section>
<footer>
Sorunuz mu var? 📧 <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
&nbsp;·&nbsp; <a href="impressum.html">Impressum (Almanca)</a>
&nbsp;·&nbsp; <a href="datenschutz.html">Gizlilik politikası (Almanca)</a>
</footer>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>

233
webseite/uk.html Normal file
View File

@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BehördenKlar Зрозумійте німецькі офіційні листи</title>
<meta name="description" content="BehördenKlar пояснює німецькі офіційні листи простими словами — українською. Сфотографуйте лист, зрозумійте його, правильно відреагуйте. Скоро для iPhone.">
<link rel="alternate" hreflang="de" href="https://behoerdenklar.pages.dev/">
<link rel="alternate" hreflang="tr" href="https://behoerdenklar.pages.dev/tr">
<link rel="alternate" hreflang="ar" href="https://behoerdenklar.pages.dev/ar">
<link rel="alternate" hreflang="en" href="https://behoerdenklar.pages.dev/en">
<link rel="alternate" hreflang="uk" href="https://behoerdenklar.pages.dev/uk">
<link rel="alternate" hreflang="ru" href="https://behoerdenklar.pages.dev/ru">
<link rel="alternate" hreflang="pl" href="https://behoerdenklar.pages.dev/pl">
<style>
:root {
--papier: #faf7f0; --papier-dunkel: #f1ecdf; --tinte: #21211d;
--grau: #6b675e; --amtsblau: #1d4e89; --stempelrot: #b3372b;
--marker: #ffe66b; --gruen: #2e7d4f; color-scheme: light;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: Georgia, 'Times New Roman', serif;
background:
repeating-linear-gradient(0deg, transparent 0 34px, rgba(33,33,29,0.025) 34px 35px),
var(--papier);
color: var(--tinte); line-height: 1.7; font-size: 17px;
}
.mono { font-family: 'Courier New', ui-monospace, monospace; letter-spacing: 0.02em; }
.wrap { max-width: 720px; margin: 0 auto; padding: 0 20px 72px; }
.briefkopf {
display: flex; justify-content: space-between; align-items: baseline;
padding: 20px 0 12px; border-bottom: 2px solid var(--tinte); font-size: 14px;
}
.briefkopf .absender { font-weight: 700; font-size: 16px; }
.briefkopf .az { color: var(--grau); font-size: 13px; }
.sprachen {
padding: 12px 0 0; font-size: 15px; color: var(--grau);
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
}
.sprachen a { color: var(--amtsblau); text-decoration: none; border-bottom: 1px dotted var(--amtsblau); }
.sprachen .aktiv { font-weight: 700; color: var(--tinte); border-bottom: none; }
header { padding: 44px 0 8px; }
h1 { font-size: clamp(32px, 6.5vw, 48px); line-height: 1.14; font-weight: 700; }
.markiert {
background: linear-gradient(104deg, transparent 2%, var(--marker) 4%, var(--marker) 96%, transparent 98%);
padding: 0 6px; box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.intro { margin-top: 24px; font-size: 19px; max-width: 580px; }
.hero-fuss { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 28px; }
.cta {
display: inline-block; padding: 17px 30px; background: var(--amtsblau);
color: #fff; text-decoration: none; font-size: 18px; font-weight: 700;
border: 2px solid var(--tinte); box-shadow: 5px 5px 0 var(--tinte);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.cta:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--tinte); }
.stempel {
display: inline-block; padding: 7px 14px; border: 2.5px solid var(--stempelrot);
color: var(--stempelrot); font-family: 'Courier New', ui-monospace, monospace;
font-weight: 700; font-size: 14px; letter-spacing: 0.08em;
text-transform: uppercase; transform: rotate(-4deg); opacity: 0.9;
}
section { margin-top: 60px; }
h2 {
font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--grau); font-weight: 400; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
h2::after { content: ''; flex: 1; border-top: 1px solid rgba(33,33,29,0.25); }
.verwandlung { display: grid; gap: 14px; }
.brief-karte {
background: #fff; border: 1px solid rgba(33,33,29,0.28);
padding: 22px 24px; position: relative;
box-shadow: 4px 5px 0 rgba(33,33,29,0.08);
}
.brief-karte.amtsdeutsch { transform: rotate(-0.7deg); background: var(--papier-dunkel); }
.brief-karte.klartext { transform: rotate(0.5deg); }
.karten-label {
position: absolute; top: -12px; left: 16px;
font-family: 'Courier New', ui-monospace, monospace; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.1em;
background: var(--tinte); color: var(--papier); padding: 3px 10px;
}
[dir="rtl"] .karten-label { left: auto; right: 16px; }
.brief-karte.klartext .karten-label { background: var(--gruen); }
.amtsdeutsch p {
font-family: 'Courier New', ui-monospace, monospace;
font-size: 14.5px; color: #45423b; line-height: 1.75;
}
.klartext p { font-size: 18px; }
.pfeil { text-align: center; font-size: 15px; color: var(--grau); font-family: 'Courier New', ui-monospace, monospace; }
.hinweis { margin-top: 10px; font-size: 14.5px; color: var(--grau); }
.schritte { counter-reset: schritt; }
.schritt {
counter-increment: schritt; display: flex; gap: 20px; padding: 20px 4px;
border-bottom: 1px solid rgba(33,33,29,0.15); align-items: baseline;
}
.schritt:last-child { border-bottom: none; }
.schritt::before {
content: counter(schritt, decimal-leading-zero);
font-family: 'Courier New', ui-monospace, monospace;
font-size: 30px; font-weight: 700; color: var(--amtsblau);
flex-shrink: 0; width: 52px;
}
.schritt strong { font-size: 19px; display: block; margin-bottom: 2px; }
.schritt span { color: var(--grau); font-size: 16px; }
.antrag {
background: #fff; border: 2px solid var(--tinte);
box-shadow: 7px 8px 0 rgba(33,33,29,0.9);
padding: 30px 28px; position: relative; margin-top: 8px;
}
.antrag .formular-kopf {
font-family: 'Courier New', ui-monospace, monospace; font-size: 13px;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--grau); margin-bottom: 6px;
}
.antrag h3 { font-size: 26px; line-height: 1.25; }
.antrag p { margin-top: 12px; max-width: 540px; }
.bonus {
margin: 18px 0; padding: 14px 16px; font-size: 17px;
background: linear-gradient(104deg, transparent 1%, var(--marker) 3%, var(--marker) 97%, transparent 99%);
}
.antrag .klein { font-size: 13.5px; color: var(--grau); margin-top: 14px; }
.antrag .klein a { color: var(--grau); }
a { color: var(--amtsblau); }
footer {
margin-top: 56px; padding-top: 14px; border-top: 2px solid var(--tinte);
font-size: 14.5px; color: var(--grau);
}
</style>
</head>
<body>
<div class="wrap">
<div class="briefkopf">
<span class="absender">📬 BehördenKlar</span>
<span class="az mono">Az: KLAR-01/2026</span>
</div>
<nav class="sprachen" aria-label="Оберіть мову">
🌐
<a href="index.html" lang="de">Deutsch</a>
<a href="tr.html" lang="tr">Türkçe</a>
<a href="ar.html" lang="ar">العربية</a>
<a href="en.html" lang="en">English</a>
<span class="aktiv">Українська</span>
<a href="ru.html" lang="ru">Русский</a>
<a href="pl.html" lang="pl">Polski</a>
</nav>
<header>
<h1>Лист від німецької установи?<br><span class="markiert">Тепер його легко зрозуміти.</span></h1>
<p class="intro">
Сфотографуйте лист. BehördenKlar пояснить його простою німецькою —
<strong>і українською</strong>. З усіма термінами та списком, що
робити.
</p>
<div class="hero-fuss">
<a class="cta" href="#anmelden">Записатися в список очікування&nbsp;</a>
<span class="stempel">Скоро для iPhone</span>
</div>
</header>
<section>
<h2>§ 1 &nbsp;Перетворення</h2>
<div class="verwandlung">
<div class="brief-karte amtsdeutsch">
<span class="karten-label">До · Оригінальний лист (німецькою)</span>
<p>
„Gemäß § 32 SGB II wird bei Nichterscheinen ohne wichtigen Grund
eine Minderung des maßgebenden Regelbedarfs in Höhe von 10 vom
Hundert festgestellt."
</p>
</div>
<p class="pfeil">— 📷 одне фото — і готово —</p>
<div class="brief-karte klartext">
<span class="karten-label">Після · BehördenKlar українською</span>
<p>
Якщо ви не прийдете на зустріч без поважної причини, ви будете
отримувати на 10% менше грошей протягом одного місяця. Прийдіть
на зустріч — або вчасно скасуйте її.
</p>
</div>
</div>
<p class="hinweis">
Застосунок пояснює листи простою німецькою та перекладає українською,
турецькою, арабською, англійською, російською, французькою, фарсі,
румунською і польською.
</p>
</section>
<section>
<h2>§ 2 &nbsp;Як це працює</h2>
<div class="schritte">
<div class="schritt"><div><strong>Сфотографуйте лист</strong> <span>або завантажте PDF.</span></div></div>
<div class="schritt"><div><strong>Зрозумійте його</strong> <span>Прості слова, вашою мовою. Світлофор показує, наскільки це терміново.</span></div></div>
<div class="schritt"><div><strong>Правильно відреагуйте</strong> <span>Список справ, нагадування про термін і готова відповідь.</span></div></div>
</div>
</section>
<section id="anmelden">
<h2>§ 3 &nbsp;Ранній доступ</h2>
<div class="antrag">
<p class="formular-kopf">Формуляр F-01 · Список очікування</p>
<h3>Будьте серед перших</h3>
<p>
Застосунок скоро з'явиться для iPhone. Запишіться зараз — ми
надішлемо вам <strong>один</strong> лист, коли все буде готово.
Без реклами.
</p>
<p class="bonus">
🎁 <strong>Ваш подарунок:</strong> аналізуйте <strong>5 листів
безкоштовно</strong> замість 3. Код отримаєте електронною поштою.
</p>
<a class="cta" href="mailto:behoerdenbriefhelfer@gmail.com?subject=Anmeldung%20Warteliste%20(Ukrainisch)&body=Hallo%2C%20bitte%20informiert%20mich%2C%20wenn%20Beh%C3%B6rdenKlar%20verf%C3%BCgbar%20ist.%20%2F%20Please%20notify%20me%20when%20Beh%C3%B6rdenKlar%20is%20available.">
📧 Записатися електронною поштою
</a>
<p class="klein">
Ми зберігаємо вашу адресу лише для цього одного повідомлення. Ви
можете відписатися будь-коли. <a href="datenschutz.html">Політика конфіденційності (німецькою)</a>.
</p>
</div>
</section>
<footer>
Питання? 📧 <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
&nbsp;·&nbsp; <a href="impressum.html">Impressum (німецькою)</a>
&nbsp;·&nbsp; <a href="datenschutz.html">Політика конфіденційності (німецькою)</a>
</footer>
</div>
<!-- Cloudflare Web Analytics: cookielose Reichweitenmessung, keine Nutzer-Profile -->
<script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "218273478b5845c6bc821588d7d823db"}'></script>
</body>
</html>