rebreak-monorepo/docs/concepts/layer2-country-pivot.md
chahinebrini 8f2ef2cc98 feat(mdm,vip): MDM-VPN-Pivot + Layer-2-Country-Curated + Custom-Domain-Refactor
MDM-VPN-Pivot (Phase F.2 done):
- ops/mdm/profiles/rebreak-iphone-protection.mobileconfig auf v5 mit
  com.apple.vpn.managed Payload + OnDemandUserOverrideDisabled. iPhone-User
  kann ReBreak-VPN-Profile nicht entfernen und "Bedarf verbinden"-Toggle
  ist disabled. allowEnablingRestrictions empirisch widerlegt für FC-Toggle-
  Lock — out.
- DEV-removable Variante als Test-Profile dazu.
- Bootstrap-Tool (rebreak-supervise.sh) + Supervision-Identity-Setup-Doc.
- PHASES.md updated mit empirischen Befunden.

App-side MDM-Detect (Pfad-a Banner-Logic):
- modules/rebreak-protection: getDeviceState() returnt mdmManaged via
  Heuristik NETunnelProviderManager.count > 1 (App selbst kann nur einen
  eigenen erstellen, MDM-Push fügt einen zweiten hinzu).
- DeviceLayers.mdmManaged?: boolean Type.
- blocker.tsx: lockedIn-Bedingung erweitert um mdmManaged. Bei MDM-managed
  iPhones wird der App-Lock-Card (FC-Authorization-Toggle UI) ausgeblendet
  weil der per-App FC-Toggle nicht lockbar ist und durch den MDM-VPN-Layer
  redundant.

Layer-2-Country-Curated-Pivot:
- backend: vip-swap.post.ts raus, suggest.post.ts rein. Curated-domains
  durch admin (separate Tabelle/Pfad), getrennt von User-Custom-Domains.
- Admin-APIs für curated-domain Pflege (index.get + [id].patch).
- seed-country-blocklists Script für initiale Curated-Domain-Liste.
- protection/webcontent-domains.get refactored für Country-Curated-Pfad.
- Migration drop_vip_swap_fields.sql + schema.prisma adjusted.
- docs/concepts/layer2-country-pivot.md mit Architektur + Decision-Trail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 07:11:47 +02:00

12 KiB

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

-- 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.prismaUserCustomDomain.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
// 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<string, string[]> = {};
  for (const c of approvedCurated) {
    (curatedByCountry[c.country] ??= []).push(c.domain);
  }

  const composed: Record<CountryKey, string[]> = {} as Record<CountryKey, string[]>;
  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 CCuratedDomain 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

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<string | null> {
  // 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)?