# Layer-2 Country-Pivot **Status:** Plan, awaiting implementation **Decided:** 2026-05-25 (during MDM-Sandwich-Test Restore-wait) **Owner:** Backend + iOS coordinated rollout --- ## Was wir ändern Layer 1 und Layer 2 werden **komplett entkoppelt**. Custom-Domains fließen nur noch in Layer 1, Layer 2 wird Pure-Country-Curated. ### Vorher (aktuell) ``` User Custom Domain → Layer 1 (VPN blocklist.bin) + Layer 2 (webcontent-domains, 30-Cap mit Swap) ↑ Verwirrend für gestresste User ``` ### Nachher (Ziel) ``` User Custom Domain → Layer 1 only (VPN blocklist.bin) └ Pro: 10 Slots / Legend: 20 Slots (rückfüllbar nach Admin-Decision) Country-Curated Liste → Layer 2 only (webcontent-domains, 50-Cap pro Land, hard read-only für User) └ Travel-Detection: OS-Region (Origin) + Cellular-MCC (Travel) └ Merge wenn Travel ≠ Origin und Travel-Country-Liste existiert User-Suggestion → Admin-Inbox (24h-SLA wie Legend-Support) └ Quelle: BlockerPage-Button + Lyra-Reply-Chip └ Admin entscheidet add to country_blocklists[country] ``` --- ## Konkrete Code-Änderungen ### A) Backend #### A1. Schema-Migration: drop VIP-Swap-Fields **File:** `backend/prisma/migrations/2026XXXXXX_drop_vip_swap_fields/migration.sql` ```sql -- Drop the VIP-Swap-Cooldown fields — Layer 2 ist nicht mehr User-Custom-gespeist ALTER TABLE rebreak.user_custom_domains DROP COLUMN IF EXISTS vip_defer_until; ALTER TABLE rebreak.user_custom_domains DROP COLUMN IF EXISTS vip_evict_at; ``` Plus Schema-Update: - `backend/prisma/schema.prisma` — `UserCustomDomain.vipDeferUntil` und `vipEvictAt` entfernen + Kommentar-Block "VIP-Slot-Replace..." #### A2. webcontent-domains.get.ts — komplett vereinfachen **File:** `backend/server/api/protection/webcontent-domains.get.ts` **Aktuelle Logik (raus):** - Lädt User-Custom-Web-Domains - Kapt auf 30 - Merged Custom + Curated pro Land **Neue Logik:** - KEIN User-Lookup mehr für Custom-Domains - Nur Country-Curated: - Statische `gambling-domains.json` - Plus DB-approved `CuratedDomain` pro Country - Optional: Query-Param `?origin=DE&travel=FR` für Multi-Country-Merge - Hard-Cap 50 pro Country (Apple-Limit) - Response-Shape unverändert: `{_meta, DE: [], GB: [], FR: [], TN: []}` — iOS parst weiter ```typescript // Skelett (Draft): export default defineEventHandler(async (event) => { const user = await requireUser(event); // Auth bleibt, aber kein User-Custom-Lookup mehr const db = usePrisma(); const approvedCurated = await db.curatedDomain.findMany({ where: { status: "approved" }, select: { domain: true, country: true }, }); const curatedByCountry: Record = {}; for (const c of approvedCurated) { (curatedByCountry[c.country] ??= []).push(c.domain); } const composed: Record = {} as Record; for (const country of COUNTRY_KEYS) { const merged = [ ...new Set([ ...(GLOBAL_LISTS[country] ?? []), ...(curatedByCountry[country] ?? []), ]), ]; composed[country] = merged.slice(0, MAX_PER_COUNTRY); } return { _meta: gamblingDomains._meta, ...composed }; }); ``` #### A3. custom-domains/index.post.ts — VIP-Logic raus **File:** `backend/server/api/custom-domains/index.post.ts` Raus: - `MAX_VIP_CUSTOM` constant - `vipDeferUntil` setzen bei vollem VIP-Cap - `vipDomains.length > MAX_VIP_CUSTOM` check - `vipFull: true` response field Bleibt: - Slot-Check via `getPlanLimits(user.plan).customDomains` (Pro=10, Legend=20) - `countActiveCustomDomains(userId)` check - Domain-Validation + DB-Insert in `user_custom_domains` - Refill-Logic über `domainRefill`-Flag (existing) #### A4. vip-swap.post.ts — DELETE **File:** `backend/server/api/custom-domains/vip-swap.post.ts` Komplett löschen. ⚠️ iOS-Coordination: nach Delete müssen alle Client-Calls weg sein (sonst 404). #### A5. Suggestion-Endpoint: BleibtDomainSubmission? **Question:** Aktuell hat `DomainSubmission` ein `customDomainId @unique` field — ein Submission ist an eine existing Custom-Domain gekoppelt. User-Wunsch: User kann Domain vorschlagen **ohne** sie selbst custom zu haben. → Schema-Change nötig: - Option A: `customDomainId` nullable machen → "freier Vorschlag" möglich - Option B: Neuer Table `domain_suggestions` (cleaner, decoupled von User-Custom-Pool) - Option C: `CuratedDomain.status="suggested"` reaktivieren (Tabelle existiert schon, hat `suggestedByUserId`!) **Empfehlung: Option C** — `CuratedDomain` mit `status="suggested"` ist bereits da. Workflow: - User klickt "Domain vorschlagen" mit Country + Domain - POST creates `CuratedDomain` row mit `status="suggested"`, `suggestedByUserId=user.id` - Admin sieht in Inbox alle `status="suggested"` Einträge - Admin entscheidet: `status="approved"` (in Liste) oder `status="rejected"` - Approved-Liste wird automatisch von webcontent-domains.get.ts gepullt (siehe A2 — `findMany({where:{status:"approved"}})`) → Nur 1 neuer Endpoint nötig: - `POST /api/custom-domains/suggest` (User-Submit) - `GET /api/admin/curated-domains?status=suggested` (Admin-Inbox — falls noch nicht da) - `PATCH /api/admin/curated-domains/[id]` (Admin Approve/Reject — falls noch nicht da) #### A6. DomainVote-Tabelle: Decision User hat gesagt: "Voting später, jetzt nur Admin-Decision". → `DomainVote` Tabelle behalten (nichts droppen), aber Voting-API erstmal deaktivieren. Phase 2 später. --- ### B) iOS (apps/rebreak-native) #### B1. Travel-Detection einbauen **New file:** `apps/rebreak-native/lib/countryDetection.ts` ```typescript import { getLocales } from "expo-localization"; // + NativeModule für CTTelephonyNetworkInfo (eigenes Native-Modul oder community package) export function getOriginCountry(): string { return getLocales()[0]?.regionCode ?? "DE"; // Fallback } export async function getTravelCountry(): Promise { // CTTelephonyNetworkInfo.serviceSubscriberCellularProviders → MCC → CountryCode // null wenn WiFi-only oder keine SIM } export async function getActiveCountries(): Promise<{ origin: string; travel: string | null }> { const [origin, travel] = await Promise.all([ getOriginCountry(), getTravelCountry(), ]); return { origin, travel: travel === origin ? null : travel }; } ``` **Native-Modul nötig:** CTTelephony ist iOS-native, kein Expo-default. Entweder: - expo-cellular nutzen (bietet `getCellularGenerationAsync()` aber nicht MCC direkt — prüfen) - Eigenes Expo-Modul schreiben (in `apps/rebreak-native/modules/rebreak-protection/`) - Community-Package wie `react-native-carrier-info` #### B2. useWebContentDomains.ts — Country-Param **File:** `apps/rebreak-native/hooks/useWebContentDomains.ts` - Aktuell: GET `/api/protection/webcontent-domains` ohne Param - Neu: optional `?origin=DE&travel=FR` für Server-side Multi-Country-Merge - Sync-Trigger: bei App-Foreground + Network-Change-Event (Cellular-MCC könnte sich geändert haben) #### B3. BlockerPage UI-Refactor **Files vermutlich:** `apps/rebreak-native/app/(tabs)/protection/` o.ä. Raus: - VIP-Swap-Dialog - VIP-Cap-Indicator ("30 von 30 belegt") - "Domain swap"-Action Rein: - Klare Sektion "Meine zusätzlichen Domains" (= Layer 1 Custom) - Slot-Indicator "7 von 10" (Pro) oder "12 von 20" (Legend) - Klare Sektion "Deutschland-Schutzliste" (= Layer 2 Country) - Read-only Liste - "Domain vorschlagen"-Button → Sheet/Modal - Travel-Notice (wenn Travel ≠ Origin): - "Du bist in Frankreich — Französische Schutzliste zusätzlich aktiv" #### B4. Lyra-Reply-Chip - Chip-Text: "Domain vorschlagen" - On-Tap: opens same Submit-Sheet #### B5. useCustomDomains hook — VIP-Swap raus - Drop VIP-related state - Add Suggest-Mutation --- ### C) Admin-UI (apps/rebreak) **Vermutet:** Existing admin-Pfad in Nuxt-App `apps/rebreak/`. Wo genau noch zu prüfen. - Inbox-View für `CuratedDomain` mit `status="suggested"` - Approve / Reject Buttons mit Reason-Input - (Optional) Filter nach Country - (Optional) Migration-Tooling: Existing-User-Custom-Domains als "Migration-Backlog" für Team-Review --- ### D) Country-Listen Curation Initial-Recherche durch Rebreak-Team: - **DE Top-25-50**: GambleAware-ähnliche Quellen + Google-Suche + Memory-Liste aus aktuellem `gambling-domains.json` - **FR Top-25-50**: ähnlich - **GB Top-25-50**: ähnlich (UK ist regulierter — Liste eventuell kürzer aber präziser) - **TN Top-X**: aktuell sehr kurz, weiter mit Recherche Wie eingespielt: - Manuell via Admin-UI (siehe C) ODER - Seed-Script `backend/scripts/seed-country-blocklists.ts` --- ## Migration Plan für Existing-User Aktuell haben User Custom-Domains die SOWOHL in Layer 1 als auch (über Hybrid-Composition) in Layer 2 landen. Nach Pivot: - Layer 1: User-Custom-Domains bleiben unverändert (Pro=10/Legend=20 Slots, refillable) - Layer 2: User-Custom-Domains werden NICHT mehr gepullt — nur Country-Curated - Wenn ein User eine Custom-Domain hatte die "good idea for Country-List" ist → Admin-Migration-Backlog (manual review) **User-Communication:** In-App-Notification "Wir haben unseren Schutz vereinfacht — deine eigenen Domains bleiben, Layer 2 zeigt jetzt die Schutzliste für dein Land." --- ## Bug: 5-10min Sync-Lag — Hypothese & Test-Plan User-Beobachtung: Custom-Domain-Add ist 5-10min delayed (sollte sofort sein). **Memory** sagt: Server-side instant, Client-Lag <60s. **Mögliche Lag-Quellen:** 1. `blocklist.bin` Rebuild-Frequency (cron-based?) — `backend/server/plugins/blocklist-cron.ts` 2. iOS-NEFilter Content-Reload-Trigger 3. Server-side Cache vor Endpoint 4. iOS DNS-Cache der OS (Apple-side, kaum kontrollierbar) **Hypothese:** Cron-Build von `blocklist.bin` läuft alle N Minuten. Bei N=10 wäre das genau das beobachtete Verhalten. **Test:** - Check `backend/server/plugins/blocklist-cron.ts` für Interval - Add Domain → log timestamp - Tail Server-Log: wann wird blocklist.bin rebuild? - Diff = lag-source **Fix (wenn Cron-Lag):** - Cron-Frequency erhöhen (z.B. 1min) - ODER Event-Driven Rebuild bei Custom-Add (POST trigger) --- ## Aufwand-Schätzung pro Block | Block | Effort | Risk | |---|---|---| | A1 Schema-Migration | 1h | low (reviewable) | | A2 webcontent-domains refactor | 2h | medium (response shape change → iOS coordination) | | A3 custom-domains/index.post simplify | 2h | medium (regression-risk auf existing slots-logic) | | A4 vip-swap.post.ts delete | 5min | high (iOS-coordinated) | | A5 Suggest-Endpoint | 4h | low (greenfield) | | A6 DomainVote behalten | 0h | low | | B1 Travel-Detection iOS | 8h | high (native module work) | | B2 useWebContentDomains | 2h | low | | B3 BlockerPage UI-Refactor | 8h | high (UX-heavy) | | B4 Lyra-Reply-Chip | 2h | low | | B5 useCustomDomains | 3h | medium | | C Admin-UI | 6h | low | | D Country-Listen Curation | 6h | low (mostly research) | | Bug: 5-10min-Lag-Root-Cause | 4h | low (Recherche + Fix) | | Migration + Comms | 4h | low | | **Total** | **~50h = ~6-8 Arbeitstage** | | --- ## Vorgeschlagene Rollout-Reihenfolge (sicherste) 1. **Phase 0** — Schema-Migration (A1) — non-breaking 2. **Phase 1** — Suggest-Endpoint (A5) + Admin-UI (C) — additive, kein Breakage 3. **Phase 2** — Country-Listen initial befüllen (D) 4. **Phase 3** — iOS Travel-Detection + UI (B1-B5) — koordiniert mit Backend 5. **Phase 4** — Backend Refactor (A2 + A3 + A4) — gleichzeitig mit iOS-Release 6. **Phase 5** — Migration-Comms an existing User 7. **Phase 6** — Bug 5-10min Lag analysieren + fixen --- ## Offene Fragen für User-Klärung - **Cellular-MCC NativeModule**: expo-cellular reicht oder eigenes Modul? (= recherche-Aufgabe) - **Admin-Team**: Wer hat Zugriff auf Admin-Inbox? Nur du, oder auch Olfa/Rayén? - **Travel-List Edge-Case**: was wenn User in DE mit Roaming auf US-Provider (z.B. AT&T-SIM in DE) — Cellular-MCC sagt US, OS-Region sagt DE. Was tun? - **Notification bei Admin-Decision**: Push + In-App-Toast, oder nur In-App? - **Existing-User-Custom-Migrations-Inbox**: alle Domains durchschauen oder nur Top-N pro Country (z.B. die häufigsten 50)?