## 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>
60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
import { requireUser } from "../../utils/auth";
|
|
import { getActiveCooldown, resolveCooldown } from "../../db/cooldown";
|
|
import { getProfile } from "../../db/profile";
|
|
|
|
/**
|
|
* GET /api/protection/state
|
|
* Combined protection + cooldown state polled every 30 s by the app.
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
|
|
const [cooldown, profile] = await Promise.all([
|
|
getActiveCooldown(user.id),
|
|
getProfile(user.id),
|
|
]);
|
|
|
|
const now = new Date();
|
|
let active = false;
|
|
let remainingSeconds = 0;
|
|
let cooldownEndsAt: string | null = null;
|
|
|
|
if (cooldown) {
|
|
const expired = now >= cooldown.cooldownEndsAt;
|
|
if (expired) {
|
|
await resolveCooldown(cooldown.id);
|
|
// After resolve: no active cooldown
|
|
} else {
|
|
active = true;
|
|
remainingSeconds = Math.max(
|
|
0,
|
|
Math.floor((cooldown.cooldownEndsAt.getTime() - now.getTime()) / 1000),
|
|
);
|
|
cooldownEndsAt = cooldown.cooldownEndsAt.toISOString();
|
|
}
|
|
}
|
|
|
|
const plan = (profile?.plan ?? "free") as "free" | "pro" | "legend";
|
|
|
|
// protectionShouldBeActive = "der Schutz sollte gerade auf dem Device laufen"
|
|
// - false wenn Cooldown aktiv ist (User darf grad nicht zurück-reaktivieren)
|
|
// - false wenn User per Cooldown-Resolve explizit abgeschaltet hat
|
|
// (protectionDisabledAt gesetzt) → Frontend macht KEINE Auto-Reactivation,
|
|
// User muss explizit re-aktivieren via /api/protection/mark-active.
|
|
// - true sonst (Normal-Zustand: Schutz sollte laufen)
|
|
const protectionShouldBeActive = !active && profile?.protectionDisabledAt === null;
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
protectionShouldBeActive,
|
|
cooldown: {
|
|
active,
|
|
remainingSeconds,
|
|
cooldownEndsAt,
|
|
},
|
|
plan,
|
|
},
|
|
};
|
|
});
|