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>
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.prisma—UserCustomDomain.vipDeferUntilundvipEvictAtentfernen + 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
CuratedDomainpro Country
- Statische
- Optional: Query-Param
?origin=DE&travel=FRfü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_CUSTOMconstantvipDeferUntilsetzen bei vollem VIP-CapvipDomains.length > MAX_VIP_CUSTOMcheckvipFull: trueresponse 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:
customDomainIdnullable 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, hatsuggestedByUserId!)
Empfehlung: Option C — CuratedDomain mit status="suggested" ist bereits da. Workflow:
- User klickt "Domain vorschlagen" mit Country + Domain
- POST creates
CuratedDomainrow mitstatus="suggested",suggestedByUserId=user.id - Admin sieht in Inbox alle
status="suggested"Einträge - Admin entscheidet:
status="approved"(in Liste) oderstatus="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-domainsohne Param - Neu: optional
?origin=DE&travel=FRfü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
CuratedDomainmitstatus="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:
blocklist.binRebuild-Frequency (cron-based?) —backend/server/plugins/blocklist-cron.ts- iOS-NEFilter Content-Reload-Trigger
- Server-side Cache vor Endpoint
- 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.tsfü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)
- Phase 0 — Schema-Migration (A1) — non-breaking
- Phase 1 — Suggest-Endpoint (A5) + Admin-UI (C) — additive, kein Breakage
- Phase 2 — Country-Listen initial befüllen (D)
- Phase 3 — iOS Travel-Detection + UI (B1-B5) — koordiniert mit Backend
- Phase 4 — Backend Refactor (A2 + A3 + A4) — gleichzeitig mit iOS-Release
- Phase 5 — Migration-Comms an existing User
- 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)?