14 Commits

Author SHA1 Message Date
chahinebrini
d31e45e2a8 feat(streak): protection-coverage metric (DiGA core) replacing broken streak
The old streak was non-functional: streaks.current_days was always 0 (never
computed/incremented), and the profile page read me.streak (0) + account
created_at as the "since" date — showing "0 days protected since <signup>"
for everyone. This is the DiGA key metric, so it had to be rebuilt.

New model: optimistic protection-coverage based on actual VPN/MDM protection
state, never resets to 0.
- backend: append-only protection_state_log + migration; POST /api/protection/event
  (ingestion, deduped) + GET /api/protection/coverage (read-time compute, no cron);
  server-side cooldown_disable event on cooldown resolve. Generous >6h-off/day rule.
- frontend: report protection on/off transitions (initial + flips, deduped) from
  useProtectionState; rewrote profile StreakSection → half-donut (protected vs
  unprotected) + progress bar (current streak → personal record) + empty state.
- coverage starts fresh from deploy (no historical backfill — clean data for DiGA).
- spec: docs/specs/protection-coverage-streak.md (shared contract).
- old streaks/streak_events/profiles.streak left intact (coach/scores consumers).

Also adds go-to-market one-pagers under docs/marketing/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 10:54:55 +02:00
chahinebrini
ab4b9c48e5 feat(ios): Screen Time Passcode als Layer 3 (setup flow)
User generiert 4-stelligen Code in der App, setzt ihn manuell als
Screen Time Passcode → ReBreak speichert ihn auf dem Backend.
Damit kann niemand Screen Time deaktivieren → deny-removal bleibt
aktiv → App nicht deinstallierbar ohne den Passcode.

Backend:
- Profile.screentimePasscode Feld (Migration add_screentime_passcode)
- POST /api/protection/screentime-passcode — Code speichern
- GET /api/protection/screentime-passcode — Code abrufen (nach Cooldown)

iOS UI (blocker.tsx):
- ScreentimePasscodeCard erscheint wenn Layer 1 + 2 aktiv (iOS only)
- Code-Generierung → Einmal-Anzeige → Deep-Link zu Settings → Screen Time
- Bestätigung speichert Code auf Backend, Card zeigt Confirmed-State

Locales: DE/EN/FR/AR screentime_* Keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:19:43 +02:00
chahinebrini
8e562c982d feat(backend): MDM-Managed Flag — migration + endpoint + guards
- Prisma migration: users.mdm_managed (Boolean DEFAULT false) + users.mdm_detected_at (DateTime?)
- setMdmManaged() helper in server/db/profile.ts
- POST /api/users/me/mdm-status — App reports MDM status to backend
- cooldown/status + cooldown/request — early-return 400 when mdm_managed
- protection/state — response extended with mdmManaged: boolean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:46:44 +02:00
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
chahinebrini
265859467a feat(vip): Curated-Domain-Vorschläge — Suggest-Backend
User können länderspezifische Glücksspielseiten für die kuratierte
VIP-Layer-2-Liste vorschlagen — wichtig für Länder mit kurzer
Starter-Liste (z.B. TN).

- Schema: CuratedDomain (domain, country, status, suggestedByUserId);
  Migration 20260522_curated_domains
- webcontent-domains.get.ts komponiert jetzt JSON-Basis + DB-approved
  Curated-Domains pro Land
- POST /api/curated-domains/suggest legt einen suggested-Eintrag an

Admin-Approve (Endpoint + Admin-App-View) folgt als nächster Block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:09:00 +02:00
chahinebrini
f555c5e4d8 feat(vip): Tunesien (TN) als VIP-Land + kuratierte Starter-Liste
TN-User fielen bisher mangels TN-Liste auf die DE-Liste zurück. Jetzt
eigene (kurze) TN-Starter-Liste: mbet216.com, 2xbet365.com, cesar365.com,
icombet.com, unibet365.net (von einem TN-Test-User gemeldet).

TN in COUNTRY_KEYS (webcontent-Endpoint) + VIP_COUNTRIES (Geräte-Region-
Auflösung + Add-Check). Native Region-Logik ist generisch (Locale.region
→ JSON-Key) — kein Native-Code nötig. gambling-domains.json _meta v3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:52:20 +02:00
chahinebrini
fe156a5f58 feat(blocker/vip): Freigabe-Button, landabhängige VIP-Liste, Hybrid-Komposition + Add-Check
Blocker-UI:
- FilterTile: Trash-Button → status-aware Freigabe-Button (Freigeben/Erneut/
  in-Prüfung); RemoveDomainSheet entfernt — kein Domain-Entfernen mehr in der UI
- VIP-Liste landabhängig: zeigt die komponierte Endpoint-Liste statt nur
  eigener Customs; Land über Geräte-Region (expo-localization)
- VIP-Realtime: refetch bei Domain-Add/Approve/Reject, pulsierender Ring
  für neue/active/submitted Chips

VIP-Komposition (webcontent-domains):
- Hybrid: Customs auf 30 gekappt, 20 Slots fest für die kuratierte Top-Liste
  reserviert — Customs können die Top-Gambling-Domains nicht verdrängen

Add-Check (custom-domains POST), für web reaktiviert — 3 Fälle gegen
Layer 1 (global) + Layer 2 (kuratierte VIP):
- weder global noch kuratiert → normaler active-Eintrag
- global + kuratiert → alreadyProtected, kein Slot
- global, nicht kuratiert → inGlobalNotVip; per addToVip als status=approved
  speicherbar (kein Slot, nur VIP-Liste)

DE-Gambling-Liste 30→36, nach Relevanz sortiert (erste 20 = reservierte Plätze)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:27:10 +02:00
chahinebrini
bc65c7172c fix(protection): webcontent-domains — fehlenden getWebCustomDomains-Import
Der Endpoint nutzte getWebCustomDomains ohne Import → ReferenceError → HTTP 500.
server/db/ ist nicht auto-importiert (nur server/utils/), daher expliziter
Import wie in allen anderen 15+ db/domains-Konsumenten.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:53:49 +02:00
chahinebrini
9dfccfcbaa fix(protection): webcontent-domains-Endpoint konsolidiert — VIP-Komposition
Ersetzt die statische v1 des Endpoints durch die per-User-VIP-Komposition:
eigene Web-Custom-Domains zuerst + globale Auffuellung → dedup → Cap 50.
v1-Reste entfernt (backend/data/, serverAssets-Eintrag) — eine Datenquelle
(backend/server/data/gambling-domains.json, direkter JSON-Import).
pnpm build:backend gruen verifiziert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:21:22 +02:00
chahinebrini
86ed603a45 feat(protection): GET /api/protection/webcontent-domains — runtime-updatebare Layer-2-Domain-Liste
Neuer Nitro-Endpoint serviert die kuratierte Gambling-Domain-Liste pro Land
(DE/GB/FR) aus backend/data/gambling-domains.json. Auth wie alle
/api/protection/*-Routen (requireUser). 50-Domain-Cap pro Land serverseitig
erzwungen. Liste pflegen = JSON editieren + _meta.version hochzaehlen + deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:21:54 +02:00
chahinebrini
34005803da fix(protection): cooldown-elapse must set protectionDisabledAt in state endpoint
Bug: nach Cooldown-Ablauf reaktivierte sich der Schutz automatisch.

Root-Cause: Race zwischen zwei Endpoints die abgelaufene Cooldowns auflösen.
/api/cooldown/status setzt korrekt profile.protectionDisabledAt. /api/
protection/state — alle 5s während Cooldown gepollt — machte beim Ablauf
NUR resolveCooldown(), ohne protectionDisabledAt. state.get gewann das Race
fast immer → protectionDisabledAt blieb null → protectionShouldBeActive=true
→ Frontend-Bypass-Detection reaktivierte den Schutz.

Fix: state.get.ts setzt im expired-Branch ebenfalls protectionDisabledAt +
cooldownJustResolved-Flag.
2026-05-20 04:17:24 +02:00
chahinebrini
56bb59915d feat(debug,protection): Force Reset for Android screenshot-capture
Bug-context: user reports nach Cooldown-Disable auf v0.2.1 Android-Build
reactiviert sich Schutz auto → a11y-Settings bleibt blockiert → keine Screenshots
möglich. v0.3.0 hat den Backend-protectionDisabledAt-Guard der das verhindert,
aber Test-Devices brauchen ein direktes Reset-Tool für Multi-Locale-Screenshots.

Backend:
- POST /api/protection/dev-force-disabled — sets protectionDisabledAt=NOW()
  ohne Cooldown-Vorlauf. Production-Guard (rebreak.org-non-staging → 403).

Frontend:
- /debug Android-Section refactored: "Force Reset + Settings öffnen" Button
- Bundle aus 3 Steps:
  1. native forceDisable (VPN stop + tamper disarm + filter_enabled=false)
  2. backend dev-force-disabled (Anti-Auto-Reactivation-Mark)
  3. Settings → Bedienungshilfen öffnen
- Danach: User toggled ReBreak-Service in Android-Settings manuell off
  → frischer a11y-deep-link-Trigger für nächste Screenshot-Iteration

Also: fix /onboarding/welcome → /onboarding (Duo-Rewrite hat den alten Pfad
gelöscht). Route 404 auf Android sichtbar wenn User in debug-toggle 'welcome'
oder 'nickname' tappt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:33:40 +02:00
chahinebrini
1596a4ea7a feat(protection,onboarding): anti-auto-reactivation + protection pre-explainer + custom sheets
## Backend: Anti-Auto-Reactivation nach Cooldown

Bug: nach Cooldown-Ablauf wurde der URL-Filter automatisch wieder
reaktiviert (enforceProtection-Loop fängt 'recoveringFromBypass'-Phase ab).
Damit war der Cooldown-Schritt entwertet — User konnte nicht wirklich
abschalten, weil die App den Schutz sofort wieder hochfuhr.

Fix: Profile.protectionDisabledAt (DateTime nullable). Wird in
/api/cooldown/status auf cooldown-auto-resolve gesetzt. /api/protection/state
gibt dann protectionShouldBeActive=false zurück → Frontend macht KEINE
Auto-Reactivation. User muss explizit re-aktivieren (CTA in der App).

- Migration 20260517_protection_disabled_at
- Schema: Profile.protectionDisabledAt
- /api/cooldown/status: setzt das Feld auf expired+resolve
- /api/protection/state: includes profile.protectionDisabledAt in shouldBeActive-Berechnung
- /api/protection/mark-active (POST, NEU): clears das Feld, vom Frontend
  auto-aufgerufen nach erfolgreichem activateUrlFilter

Bypass-Recovery durch externe iOS-Settings-Disable (nicht cooldown-bezogen)
funktioniert weiter — protectionDisabledAt ist dann null, alte Logik greift.

## Frontend: ProtectionOffSheet (Custom-Sheet statt Alert.alert)

Bisheriges native Alert mit OK+Reactivate-Buttons hat keine visuelle
Hierarchy (iOS macht beide gleich). Ersetzt mit FormSheet:
 - Großer blauer Primary "Schutz wieder einschalten"
 - Ghost-Link "Später"
 - Swipe-down / Backdrop-Tap zum Schließen

## Frontend: ProtectionSlide mit Pre-Explainer (Screenshot + Pulse-Marker)

User-Request: vor dem iOS-Permission-Dialog ein Erklärungs-Screen zeigen
damit der User weiß wo er tappen muss (Apple's "Don't Allow" ist groß+
blau = Trap, "Allow" ist der unscheinbare Button unten).

- components/onboarding/ScreenshotPointer.tsx — Reanimated pulsing red
  ring, positionierbar via {xPercent, yPercent}
- lib/onboardingAssets.ts — locale-aware require()-Map für Screenshot-
  Assets mit de-Fallback
- assets/onboarding/de/ — 4 iOS-Screenshots vom User (url_filter +
  screen_time permission dialogs + 2 confirm screens)
- ProtectionSlide refactored: internal phase state preexplain_url →
  preexplain_lock → done. Jede Phase zeigt Screenshot + Pulse-Marker auf
  korrekten Button + Lyra-Bubble + activate-CTA.

## Locale-Keys

- onboarding.lyra.protection_url.body, onboarding.lyra.protection_lock.body
- onboarding.protection.url_title, .lock_title, .tap_marker_hint
- onboarding.protection.applock_failed_*, applock_skip
- blocker.protection_off_later, reactivate_btn (refined)

## Bugfix: de.json JSON-syntax

Smart-quote-typo: schließendes "" nach „Erlauben" und „Fortfahren" war
ein plain ASCII " (U+0022) statt U+201D, was den JSON-String früh
terminiert hat. Metro+Hermes warfen "unrecognized Unicode —".
Fix: escapte \" verwendet — JSON-safe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:05:37 +02:00
RaynisDev
b58588cf3c initial commit: rebreak-monorepo (RN app + standalone Nitro backend) 2026-05-06 07:13:43 +02:00