diff --git a/apps/rebreak-native/app/debug.tsx b/apps/rebreak-native/app/debug.tsx
index fc9a9d1..7e1e930 100644
--- a/apps/rebreak-native/app/debug.tsx
+++ b/apps/rebreak-native/app/debug.tsx
@@ -7,6 +7,7 @@ import {
TouchableOpacity,
Alert,
Clipboard,
+ Platform,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
@@ -15,7 +16,7 @@ import { useColors } from '../lib/theme';
import { useMe, invalidateMe, type Plan } from '../hooks/useMe';
import { apiFetch } from '../lib/api';
import { PlanChangeSheet } from '../components/plan/PlanChangeSheet';
-import { getCooldownTestMode, setCooldownTestMode } from '../lib/protection';
+import { getCooldownTestMode, setCooldownTestMode, protection } from '../lib/protection';
import { useRealtimeDebugStore, type LogEntry } from '../stores/realtimeDebug';
import { supabase } from '../lib/supabase';
@@ -115,6 +116,8 @@ export default function DebugScreen() {
+ {Platform.OS === 'android' ? : null}
+
@@ -690,9 +693,13 @@ function OnboardingResetToggle({
});
invalidateMe();
if (step === 'welcome') {
- router.replace('/onboarding/welcome');
+ // /onboarding/welcome existiert nicht mehr (Duo-Rewrite → /onboarding/index.tsx).
+ // Korrekter Pfad ist /onboarding (Expo Router löst index.tsx auto auf).
+ router.replace('/onboarding');
} else if (step === 'nickname') {
- router.replace('/profile/edit');
+ // Legacy-Stage — Duo-Flow navigiert intern; /onboarding triggert via
+ // slideFromStep den Resume zur Nickname-Slide.
+ router.replace('/onboarding');
} else if (step === 'block') {
router.replace('/(app)/blocker');
} else if (step === 'done') {
@@ -783,6 +790,117 @@ function OnboardingResetToggle({
);
}
+// ─── Android: A11y-Settings Quick-Open ──────────────────────────────────────
+
+/**
+ * Android-only Dev-Helper: öffnet Settings → Bedienungshilfen damit User den
+ * ReBreak-Service manuell off-toggeln kann. Wird gebraucht für Screenshot-
+ * Capture vom a11y-Settings-Page in 4 Sprachen — nach Cooldown disarmt
+ * forceDisable() den tamper-lock, aber das System-Switch in den Einstellungen
+ * bleibt programmatisch un-modifizierbar (Android-OS-Restriction).
+ */
+function AndroidA11yResetToggle({
+ colors,
+}: {
+ colors: import('../lib/theme').ColorScheme;
+}) {
+ const [busy, setBusy] = useState(false);
+
+ async function forceResetAndOpen() {
+ if (busy) return;
+ setBusy(true);
+ try {
+ // Step 1: native forceDisable — stoppt VPN, disarmt tamper-lock, setzt
+ // filter_enabled=false → a11y-service wird passiv → blockt nichts mehr.
+ await protection.forceDisable();
+
+ // Step 2: backend protection-state auf "explizit disabled" markieren →
+ // enforceProtection-Loop feuert KEINE Auto-Reactivation in den nächsten
+ // Pollings. Sonst wäre der a11y-Service-Off-Toggle zwecklos weil die
+ // App den Filter sofort wieder hochfährt.
+ await apiFetch('/api/protection/dev-force-disabled', { method: 'POST' });
+ invalidateMe();
+
+ // Step 3: Android-Settings → Bedienungshilfen öffnen — User toggelt
+ // ReBreak-Service manuell off (Android-OS-Restriction: programmatisch
+ // nicht setzbar). Danach Screenshot-Capture frischer Trigger möglich.
+ await protection.openSystemSettings('accessibility');
+ } catch (e: unknown) {
+ Alert.alert('Fehler', e instanceof Error ? e.message : String(e));
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ Force Reset (Android)
+
+
+ VPN stoppen + tamper-lock disarmen + Backend als disabled markieren
+ + Settings öffnen — Anti-Auto-Reactivation für Screenshot-Capture.
+
+
+
+
+
+
+ {busy ? 'Reset läuft...' : 'Force Reset + Settings öffnen'}
+
+
+
+ );
+}
+
// ─── Cooldown Test Mode ────────────────────────────────────────────────────
function CooldownTestModeToggle() {
diff --git a/backend/server/api/protection/dev-force-disabled.post.ts b/backend/server/api/protection/dev-force-disabled.post.ts
new file mode 100644
index 0000000..ba26815
--- /dev/null
+++ b/backend/server/api/protection/dev-force-disabled.post.ts
@@ -0,0 +1,37 @@
+import { requireUser } from "../../utils/auth";
+import { usePrisma } from "../../utils/prisma";
+
+/**
+ * POST /api/protection/dev-force-disabled
+ *
+ * DEV/STAGING-ONLY: Setzt protectionDisabledAt = NOW() ohne Cooldown-Vorlauf.
+ * Frontend-Debug-Button für Screenshot-Capture (Android-a11y-reset-flow).
+ *
+ * Production-Guard: appUrl enthält "rebreak.org" aber NICHT "staging" → 403.
+ *
+ * Sobald gesetzt:
+ * - /api/protection/state gibt protectionShouldBeActive=false zurück
+ * - Frontend's enforceProtection-Loop feuert KEINE Auto-Reactivation mehr
+ * - User kann a11y-Settings öffnen und manuell den ReBreak-Service off-toggeln
+ *
+ * Zum Wiedereinschalten: POST /api/protection/mark-active (clear flag).
+ */
+export default defineEventHandler(async (event) => {
+ const user = await requireUser(event);
+
+ const config = useRuntimeConfig(event);
+ const appUrl = (config.public?.appUrl as string) ?? "";
+ const isProductionUrl =
+ appUrl.includes("rebreak.org") && !appUrl.includes("staging");
+ if (isProductionUrl) {
+ throw createError({ statusCode: 403, message: "dev-only" });
+ }
+
+ const db = usePrisma();
+ await db.profile.update({
+ where: { id: user.id },
+ data: { protectionDisabledAt: new Date() },
+ });
+
+ return { success: true };
+});