import { useEffect, useState } from 'react'; import { View, Text, ScrollView, Switch, TouchableOpacity, Alert, Clipboard, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; 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 { useRealtimeDebugStore, type LogEntry } from '../stores/realtimeDebug'; import { supabase } from '../lib/supabase'; export default function DebugScreen() { const router = useRouter(); const colors = useColors(); const { me } = useMe(); useEffect(() => { if (!__DEV__) { router.replace('/'); } }, [router]); if (!__DEV__) { return ; } return ( router.back()} hitSlop={8} activeOpacity={0.6} style={{ width: 40, height: 40, alignItems: 'center', justifyContent: 'center' }} > Debug Dev only Diese Page ist nur in __DEV__ verfügbar. Production-Builds redirecten auf /. {me ? ( ) : null} ); } // ─── Realtime Status Card ────────────────────────────────────────────────── function RealtimeStatusCard() { const colors = useColors(); const connectionState = useRealtimeDebugStore((s) => s.connectionState); const reconnectCount = useRealtimeDebugStore((s) => s.reconnectCount); const lastEventAt = useRealtimeDebugStore((s) => s.lastEventAt); const tokenExpiresAt = useRealtimeDebugStore((s) => s.tokenExpiresAt); const refreshSnapshot = useRealtimeDebugStore((s) => s.refreshSnapshot); const [tick, setTick] = useState(0); const [channels, setChannels] = useState<{ topic: string; state: string }[]>([]); // Re-render once per second to keep relative timestamps live useEffect(() => { const t = setInterval(() => setTick((n) => n + 1), 1000); return () => clearInterval(t); }, []); // Refresh snapshot + channel list on each tick useEffect(() => { refreshSnapshot(); try { const rt = supabase.realtime as any; const raw: { topic?: string; state?: string }[] = rt.channels ?? []; setChannels( raw.map((ch) => ({ topic: ch.topic ?? '?', state: ch.state ?? '?', })), ); } catch { setChannels([]); } }, [tick, refreshSnapshot]); const stateColor = stateAccent(connectionState, colors); const tokenSecsLeft = tokenExpiresAt != null ? Math.round(tokenExpiresAt - Date.now() / 1000) : null; const lastEventLabel = lastEventAt ? relativeSeconds(Date.now() - lastEventAt) : 'never'; return ( Realtime Status refreshSnapshot()} hitSlop={8} activeOpacity={0.6} > {channels.length > 0 ? ( Active channels ({channels.length}) {channels.map((ch) => ( {ch.topic} {ch.state} ))} ) : ( Keine aktiven Channels )} ); } function StatusRow({ label, value, valueColor, colors, }: { label: string; value: string; valueColor?: string; colors: import('../lib/theme').ColorScheme; }) { return ( {label} {value} ); } // ─── Realtime Log Card ───────────────────────────────────────────────────── function RealtimeLogCard() { const colors = useColors(); const log = useRealtimeDebugStore((s) => s.log); const clearLog = useRealtimeDebugStore((s) => s.clearLog); function copyLog() { const text = log .map((e) => `[${new Date(e.ts).toISOString()}] ${e.text}`) .join('\n'); Clipboard.setString(text); Alert.alert('Kopiert', `${log.length} Log-Einträge in Zwischenablage.`); } return ( Realtime Log {log.length === 0 ? ( Noch keine Events — warte auf Subscription-Aktivität. ) : ( {log.map((entry) => ( ))} )} max 100 Einträge, neueste oben ); } function LogLine({ entry, colors, }: { entry: LogEntry; colors: import('../lib/theme').ColorScheme; }) { const time = new Date(entry.ts); const hms = `${pad(time.getHours())}:${pad(time.getMinutes())}:${pad(time.getSeconds())}`; const isError = entry.text.includes('error') || entry.text.includes('close') || entry.text.includes('CLOSED'); const isOpen = entry.text.includes('open') || entry.text.includes('joined'); return ( {hms} {entry.text} ); } // ─── Helpers ─────────────────────────────────────────────────────────────── function pad(n: number) { return String(n).padStart(2, '0'); } function relativeSeconds(diffMs: number): string { const s = Math.round(diffMs / 1000); if (s < 60) return `vor ${s}s`; const m = Math.floor(s / 60); if (m < 60) return `vor ${m}m`; return `vor ${Math.floor(m / 60)}h`; } function stateAccent(state: string, colors: import('../lib/theme').ColorScheme): string { const upper = state.toUpperCase(); if (upper === 'OPEN' || upper === 'JOINED' || upper === 'SUBSCRIBED') return colors.success; if (upper === 'CLOSED' || upper === 'CHANNEL_ERROR' || upper === 'TIMED_OUT') return colors.error; if (upper === 'CONNECTING' || upper === 'JOINING') return colors.warning; return colors.textMuted; } // ─── Plan Override ───────────────────────────────────────────────────────── const PLANS: Plan[] = ['free', 'pro', 'legend']; const PLAN_COLOR: Record = { free: '#737373', pro: '#007AFF', legend: '#f59e0b', }; function PlanOverrideToggle({ colors, userId, currentPlan, }: { colors: import('../lib/theme').ColorScheme; userId: string; currentPlan: Plan; }) { const [loading, setLoading] = useState(false); const [sheetTarget, setSheetTarget] = useState(null); async function applyPlanSwitch(plan: Plan) { setLoading(true); try { await apiFetch('/api/dev/set-plan', { method: 'POST', body: { plan }, }); invalidateMe(); } catch (e: unknown) { Alert.alert('Fehler', e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } } function switchPlan(plan: Plan) { if (plan === currentPlan) return; setSheetTarget(plan); } return ( Plan-Override (DEV) POST /api/dev/set-plan — nur staging {PLANS.map((plan) => { const isActive = plan === currentPlan; const accent = PLAN_COLOR[plan]; return ( switchPlan(plan)} disabled={loading || isActive} activeOpacity={0.7} style={{ flex: 1, paddingVertical: 10, borderRadius: 10, alignItems: 'center', backgroundColor: isActive ? accent : colors.surfaceElevated, opacity: loading ? 0.5 : 1, }} > {plan} ); })} {sheetTarget ? ( { const t = sheetTarget; setSheetTarget(null); applyPlanSwitch(t); }} onClose={() => setSheetTarget(null)} /> ) : null} ); } // ─── Cooldown Test Mode ──────────────────────────────────────────────────── function CooldownTestModeToggle() { const colors = useColors(); const [enabled, setEnabled] = useState(false); useEffect(() => { getCooldownTestMode().then(setEnabled); }, []); async function toggle(value: boolean) { setEnabled(value); await setCooldownTestMode(value); } return ( Test-Cooldown (40 Sek) Nur auf staging — der nächste "Cooldown starten" nutzt dann 40 Sekunden statt 24h. ); } // ─── Debug Stub ──────────────────────────────────────────────────────────── function DebugStub({ title, subtitle, icon, }: { title: string; subtitle: string; icon: React.ComponentProps['name']; }) { const colors = useColors(); return ( {title} {subtitle} ); }