generated from knso/webklar-preview-template
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>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -33,6 +33,10 @@ yarn-error.*
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# Cloudflare Worker (Proxy)
|
||||
.wrangler/
|
||||
.dev.vars
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -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
|
||||
|
||||
|
||||
14
app.json
14
app.json
@@ -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,15 @@
|
||||
}
|
||||
],
|
||||
"expo-notifications",
|
||||
"expo-status-bar"
|
||||
"expo-status-bar",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./assets/splash-icon.png",
|
||||
"backgroundColor": "#E6F4FE",
|
||||
"imageWidth": 200
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
21
eas.json
Normal file
21
eas.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
4225
package-lock.json
generated
4225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -6,34 +6,43 @@
|
||||
"@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-document-picker": "~57.0.0",
|
||||
"expo-file-system": "~57.0.0",
|
||||
"expo-image-manipulator": "~57.0.2",
|
||||
"expo-image-picker": "~57.0.2",
|
||||
"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",
|
||||
"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
61
proxy/README.md
Normal 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
1576
proxy/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
proxy/package.json
Normal file
15
proxy/package.json
Normal 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
128
proxy/src/index.ts
Normal 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
12
proxy/tsconfig.json
Normal 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
14
proxy/wrangler.toml
Normal 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"
|
||||
104
rechtliches/datenschutzerklaerung.md
Normal file
104
rechtliches/datenschutzerklaerung.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 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.
|
||||
Wir betreiben keine Nutzerkonten und keine Cloud-Speicherung Ihrer Briefe.
|
||||
|
||||
### 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. 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.
|
||||
|
||||
## 8. Ä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.
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 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';
|
||||
@@ -8,6 +9,7 @@ import * as Notifications from 'expo-notifications';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../types';
|
||||
import { holeApiKey, speichereApiKey, loescheAlleDaten } 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';
|
||||
@@ -35,7 +37,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,6 +59,8 @@ export function EinstellungenScreen({ navigation }: Props) {
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={{ padding: abstand.m }}>
|
||||
{!NUTZT_PROXY && (
|
||||
<>
|
||||
<Text style={styles.abschnittTitel}>KI-Zugang (API-Schlüssel)</Text>
|
||||
<Text style={styles.text}>
|
||||
Status:{' '}
|
||||
@@ -87,12 +91,15 @@ export function EinstellungenScreen({ navigation }: Props) {
|
||||
/>
|
||||
|
||||
<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'}
|
||||
• 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.{'\n\n'}
|
||||
• Diese App ersetzt keine Rechtsberatung.
|
||||
</Text>
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
* 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, zaehleAnalyse } from '../services/storage';
|
||||
import { planeErinnerungen } from '../services/erinnerungen';
|
||||
import { useAppStore, neueId } from '../store/useAppStore';
|
||||
import { GrossButton } from '../components/GrossButton';
|
||||
@@ -18,15 +21,68 @@ 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);
|
||||
|
||||
// Zähler bei jedem Screen-Besuch frisch laden
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
holeAnzahlAnalysen().then(setVerbraucht);
|
||||
}, [])
|
||||
);
|
||||
|
||||
// Freemium gilt nur im Produktionsmodus (Proxy). Im Dev-Modus mit
|
||||
// eigenem API-Key zahlt der Entwickler selbst — kein Limit nötig.
|
||||
const uebrig = GRATIS_ANALYSEN - 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 ${GRATIS_ANALYSEN} 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 +105,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 +113,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 +131,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,
|
||||
@@ -118,6 +176,13 @@ export function ScanScreen({ navigation }: Props) {
|
||||
<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} />
|
||||
{NUTZT_PROXY && (
|
||||
<Text style={styles.kontingent}>
|
||||
{kontingentLeer
|
||||
? 'Gratis-Analysen aufgebraucht — Abo folgt in Kürze'
|
||||
: `Noch ${uebrig} von ${GRATIS_ANALYSEN} kostenlosen Analysen`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -145,6 +210,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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -14,6 +14,16 @@ import { BriefEintrag } from '../types';
|
||||
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;
|
||||
|
||||
// ---- API-Key (sensibel -> SecureStore) ----
|
||||
|
||||
@@ -29,6 +39,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> {
|
||||
@@ -59,6 +105,14 @@ export async function speichereBriefe(briefe: BriefEintrag[]): Promise<void> {
|
||||
// ---- 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,
|
||||
]);
|
||||
await SecureStore.deleteItemAsync(KEY_API);
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
161
src/utils/__tests__/ampel.test.ts
Normal file
161
src/utils/__tests__/ampel.test.ts
Normal 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)');
|
||||
});
|
||||
});
|
||||
122
store/app-store-texte.md
Normal file
122
store/app-store-texte.md
Normal 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)
|
||||
```
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
"strict": true,
|
||||
"types": ["jest"]
|
||||
},
|
||||
"exclude": ["proxy", "node_modules"]
|
||||
}
|
||||
|
||||
149
webseite/datenschutz.html
Normal file
149
webseite/datenschutz.html
Normal file
@@ -0,0 +1,149 @@
|
||||
<!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.
|
||||
Wir betreiben keine Nutzerkonten und keine Cloud-Speicherung Ihrer Briefe.
|
||||
</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. 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>8. Ä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>
|
||||
</body>
|
||||
</html>
|
||||
46
webseite/impressum.html
Normal file
46
webseite/impressum.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!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>
|
||||
</body>
|
||||
</html>
|
||||
75
webseite/index.html
Normal file
75
webseite/index.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BehördenKlar – Behördenbriefe einfach verstehen</title>
|
||||
<meta name="description" content="BehördenKlar erklärt Behördenbriefe in einfacher Sprache. Support und Kontakt.">
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { 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; }
|
||||
header { text-align: center; padding: 48px 0 32px; }
|
||||
.logo { font-size: 56px; }
|
||||
h1 { font-size: 32px; margin-top: 12px; color: #144d8c; }
|
||||
.slogan { font-size: 18px; color: #4a5568; margin-top: 8px; }
|
||||
.karte {
|
||||
background: #fff; border-radius: 16px; padding: 24px;
|
||||
margin-top: 24px; box-shadow: 0 2px 8px rgba(20, 77, 140, 0.08);
|
||||
}
|
||||
h2 { font-size: 20px; color: #144d8c; margin-bottom: 12px; }
|
||||
a { color: #1a6bc4; }
|
||||
.kontakt { font-size: 18px; font-weight: 600; }
|
||||
footer { text-align: center; margin-top: 40px; font-size: 15px; color: #4a5568; }
|
||||
footer a { margin: 0 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="logo">📬</div>
|
||||
<h1>BehördenKlar</h1>
|
||||
<p class="slogan">Behördenbriefe fotografieren – und einfach verstehen.</p>
|
||||
</header>
|
||||
|
||||
<div class="karte">
|
||||
<h2>Was ist BehördenKlar?</h2>
|
||||
<p>
|
||||
BehördenKlar ist eine App für iPhone: Sie fotografieren einen Brief vom
|
||||
Amt, und die App erklärt ihn in einfacher Sprache — mit allen Fristen,
|
||||
Terminen und einer Checkliste, was zu tun ist. Auf Wunsch übersetzt in
|
||||
9 weitere Sprachen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="karte">
|
||||
<h2>Support & Kontakt</h2>
|
||||
<p>
|
||||
Sie haben eine Frage, ein Problem mit der App oder einen Verbesserungsvorschlag?
|
||||
Schreiben Sie uns — wir antworten so schnell wie möglich:
|
||||
</p>
|
||||
<p class="kontakt" style="margin-top: 12px;">
|
||||
📧 <a href="mailto:behoerdenbriefhelfer@gmail.com">behoerdenbriefhelfer@gmail.com</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="karte">
|
||||
<h2>Wichtiger Hinweis</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="impressum.html">Impressum</a> ·
|
||||
<a href="datenschutz.html">Datenschutzerklärung</a>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user