• Provides debug context to its children, managing debug state and persistence.

    Parameters

    • children: Readonly<{ children: ReactNode }>

      The React children to be wrapped by the provider.

    Returns Element

    The debug context provider with value for consumers.

    export function DebugProvider({
    children,
    }: Readonly<{ children: React.ReactNode }>) {
    const [isDebugEnabled, setIsDebugEnabled] = useState(false);
    const [featureToggles, setFeatureToggles] = useState<DebugFeatureToggles>(
    DEFAULT_FEATURE_TOGGLES,
    );
    const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
    const [stateSourceSnapshots, setStateSourceSnapshots] = useState<
    StateInspectorSourceSnapshot[]
    >([]);
    const stateSourcesRef = useRef(
    new Map<string, StateInspectorSourceInternal>(),
    );

    const storageDebuggerEnabled = featureToggles.storageDebugger;
    const logViewerEnabled = featureToggles.logViewer;
    const stateInspectorEnabled = featureToggles.stateInspector;

    useEffect(() => {
    // Only install the console interceptor when BOTH debug mode AND the log viewer feature are enabled.
    if (!(isDebugEnabled && logViewerEnabled)) return;

    const detachConsole = installConsoleInterceptor();
    return () => {
    detachConsole?.();
    };
    }, [isDebugEnabled, logViewerEnabled]);

    useEffect(() => {
    if (!isDebugEnabled && !logViewerEnabled) {
    return;
    }

    setLogEntries(logCollector.getEntries());
    const unsubscribe = logCollector.subscribe((entries) => {
    setLogEntries(entries);
    });

    return () => {
    unsubscribe();
    };
    }, [isDebugEnabled, logViewerEnabled]);

    // Load debug state from localStorage on initialization
    useEffect(() => {
    try {
    const savedDebugState = localStorage.getItem(DEBUG_STORAGE_KEY);
    if (savedDebugState !== null) {
    setIsDebugEnabled(JSON.parse(savedDebugState));
    }
    } catch (error) {
    console.error("Failed to load debug state from localStorage:", error);
    }
    }, []);

    // Load feature toggles on initialization
    useEffect(() => {
    try {
    const storedToggles = localStorage.getItem(DEBUG_FEATURE_TOGGLES_KEY);
    if (storedToggles) {
    const parsed = JSON.parse(
    storedToggles,
    ) as Partial<DebugFeatureToggles>;
    setFeatureToggles((prev) => ({
    ...prev,
    ...parsed,
    }));
    }
    } catch (error) {
    console.error(
    "Failed to load debug feature toggles from localStorage:",
    error,
    );
    }
    }, []);

    // Save debug state to localStorage whenever it changes
    const setDebugEnabled = useCallback((enabled: boolean) => {
    setIsDebugEnabled(enabled);
    try {
    localStorage.setItem(DEBUG_STORAGE_KEY, JSON.stringify(enabled));
    } catch (error) {
    console.error("Failed to save debug state to localStorage:", error);
    }
    }, []);

    const toggleDebug = useCallback(() => {
    setDebugEnabled(!isDebugEnabled);
    }, [isDebugEnabled, setDebugEnabled]);

    const persistFeatureToggles = useCallback(
    (updater: (prev: DebugFeatureToggles) => DebugFeatureToggles) => {
    setFeatureToggles((prev) => {
    const next = updater(prev);
    try {
    localStorage.setItem(DEBUG_FEATURE_TOGGLES_KEY, JSON.stringify(next));
    } catch (error) {
    console.error(
    "Failed to save debug feature toggles to localStorage:",
    error,
    );
    }
    return next;
    });
    },
    [],
    );

    const setStorageDebuggerEnabled = useCallback(
    (enabled: boolean) => {
    persistFeatureToggles((prev) => ({
    ...prev,
    storageDebugger: enabled,
    }));
    },
    [persistFeatureToggles],
    );

    const toggleStorageDebugger = useCallback(() => {
    persistFeatureToggles((prev) => ({
    ...prev,
    storageDebugger: !prev.storageDebugger,
    }));
    }, [persistFeatureToggles]);

    const setLogViewerEnabled = useCallback(
    (enabled: boolean) => {
    persistFeatureToggles((prev) => ({
    ...prev,
    logViewer: enabled,
    }));
    },
    [persistFeatureToggles],
    );

    const toggleLogViewer = useCallback(() => {
    persistFeatureToggles((prev) => ({
    ...prev,
    logViewer: !prev.logViewer,
    }));
    }, [persistFeatureToggles]);

    const setStateInspectorEnabled = useCallback(
    (enabled: boolean) => {
    persistFeatureToggles((prev) => ({
    ...prev,
    stateInspector: enabled,
    }));
    },
    [persistFeatureToggles],
    );

    const toggleStateInspector = useCallback(() => {
    persistFeatureToggles((prev) => ({
    ...prev,
    stateInspector: !prev.stateInspector,
    }));
    }, [persistFeatureToggles]);

    const registerStateInspector = useCallback(
    <T,>(config: StateInspectorRegistration<T>): StateInspectorHandle<T> => {
    const serialize = config.serialize
    ? (value: unknown) => config.serialize!(value as T) as unknown
    : (value: unknown) => value;
    const deserialize = config.deserialize
    ? (value: unknown) => config.deserialize!(value) as unknown
    : (value: unknown) => value;
    const getSnapshot = () => config.getSnapshot() as unknown;
    const setSnapshot = config.setSnapshot
    ? (value: unknown) => {
    (config.setSnapshot as (next: T) => void)(value as T);
    }
    : undefined;

    const initialRaw = getSnapshot();
    const initialDisplay = serialize(initialRaw);
    const internal: StateInspectorSourceInternal = {
    id: config.id,
    label: config.label,
    description: config.description,
    group: config.group ?? "General",
    getSnapshot,
    setSnapshot,
    serialize,
    deserialize,
    latestRawValue: initialRaw,
    latestDisplayValue: initialDisplay,
    lastUpdated: Date.now(),
    };

    const nextSources = new Map(stateSourcesRef.current);
    nextSources.set(config.id, internal);
    stateSourcesRef.current = nextSources;
    setStateSourceSnapshots(toStateInspectorSnapshots(nextSources));

    return {
    publish: (value: T) => {
    const current = stateSourcesRef.current.get(config.id);
    if (!current) return;
    const next = {
    ...current,
    latestRawValue: value,
    latestDisplayValue: current.serialize(value as unknown),
    lastUpdated: Date.now(),
    } satisfies StateInspectorSourceInternal;
    const map = new Map(stateSourcesRef.current);
    map.set(config.id, next);
    stateSourcesRef.current = map;
    setStateSourceSnapshots(toStateInspectorSnapshots(map));
    },
    unregister: () => {
    const current = new Map(stateSourcesRef.current);
    current.delete(config.id);
    stateSourcesRef.current = current;
    setStateSourceSnapshots(toStateInspectorSnapshots(current));
    },
    };
    },
    [],
    );

    const refreshStateInspectorSource = useCallback((id: string) => {
    const source = stateSourcesRef.current.get(id);
    if (!source) return;
    try {
    const snapshot = source.getSnapshot();
    const map = new Map(stateSourcesRef.current);
    map.set(id, {
    ...source,
    latestRawValue: snapshot,
    latestDisplayValue: source.serialize(snapshot),
    lastUpdated: Date.now(),
    });
    stateSourcesRef.current = map;
    setStateSourceSnapshots(toStateInspectorSnapshots(map));
    } catch (error) {
    console.error("Failed to refresh state inspector source", {
    id,
    error,
    });
    }
    }, []);

    const applyStateInspectorUpdate = useCallback(
    (id: string, value: unknown) => {
    const source = stateSourcesRef.current.get(id);
    if (!source) {
    throw new Error(`Unknown state inspector source: ${id}`);
    }

    if (!source.setSnapshot) {
    throw new Error(`State inspector source '${id}' is read-only`);
    }

    const nextValue = source.deserialize(value);

    try {
    source.setSnapshot(nextValue);
    const refreshed = source.getSnapshot();
    const map = new Map(stateSourcesRef.current);
    map.set(id, {
    ...source,
    latestRawValue: refreshed,
    latestDisplayValue: source.serialize(refreshed),
    lastUpdated: Date.now(),
    });
    stateSourcesRef.current = map;
    setStateSourceSnapshots(toStateInspectorSnapshots(map));
    } catch (error) {
    console.error("Failed to apply state inspector update", {
    id,
    error,
    });
    throw error;
    }
    },
    [],
    );

    useEffect(() => {
    const settingsId = "settings-state";
    try {
    const handle = registerStateInspector<SettingsDebugSnapshot>({
    id: settingsId,
    label: "Application Settings",
    description:
    "Persisted sync and matching configuration stored in local preferences.",
    group: "Settings",
    getSnapshot: () => ({
    syncConfig: getSyncConfig(),
    matchConfig: getMatchConfig(),
    }),
    setSnapshot: (snapshot) => {
    if (snapshot.syncConfig) {
    saveSyncConfig(snapshot.syncConfig);
    }
    if (snapshot.matchConfig) {
    saveMatchConfig(snapshot.matchConfig);
    }
    },
    serialize: (snapshot) => ({
    syncConfig: snapshot.syncConfig,
    matchConfig: snapshot.matchConfig,
    }),
    deserialize: (value) => {
    const candidate = value as Partial<SettingsDebugSnapshot> | null;
    return {
    syncConfig: candidate?.syncConfig ?? getSyncConfig(),
    matchConfig: candidate?.matchConfig ?? getMatchConfig(),
    } satisfies SettingsDebugSnapshot;
    },
    });

    return () => {
    handle.unregister();
    };
    } catch (error) {
    console.error("Failed to register settings state inspector", error);
    return undefined;
    }
    }, [registerStateInspector]);

    const clearLogs = useCallback(() => {
    logCollector.clear();
    }, []);

    const exportLogs = useCallback(() => {
    const entries = logCollector.getEntries();
    if (!entries.length) {
    console.warn("No debug logs available to export");
    return;
    }

    try {
    const payload = {
    exportedAt: new Date().toISOString(),
    userAgent:
    typeof navigator === "undefined" ? undefined : navigator.userAgent,
    totalEntries: entries.length,
    maxEntries: MAX_LOG_ENTRIES,
    logs: serialiseLogEntries(entries),
    };

    const json = JSON.stringify(payload, null, 2);
    const blob = new Blob([json], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    const timestamp = new Date()
    .toISOString()
    .replaceAll(":", "-")
    .replaceAll(".", "-");
    link.href = url;
    link.download = `kenmei-debug-logs-${timestamp}.json`;
    document.body.appendChild(link);
    link.click();
    link.remove();
    URL.revokeObjectURL(url);
    } catch (error) {
    console.error("Failed to export debug logs:", error);
    }
    }, []);

    const value = React.useMemo<DebugContextType>(
    () => ({
    isDebugEnabled,
    toggleDebug,
    setDebugEnabled,
    storageDebuggerEnabled,
    setStorageDebuggerEnabled,
    toggleStorageDebugger,
    logViewerEnabled,
    setLogViewerEnabled,
    toggleLogViewer,
    stateInspectorEnabled,
    setStateInspectorEnabled,
    toggleStateInspector,
    stateInspectorSources: stateSourceSnapshots,
    registerStateInspector,
    applyStateInspectorUpdate,
    refreshStateInspectorSource,
    logEntries,
    clearLogs,
    exportLogs,
    maxLogEntries: MAX_LOG_ENTRIES,
    }),
    [
    isDebugEnabled,
    toggleDebug,
    setDebugEnabled,
    storageDebuggerEnabled,
    setStorageDebuggerEnabled,
    toggleStorageDebugger,
    logViewerEnabled,
    setLogViewerEnabled,
    toggleLogViewer,
    stateInspectorEnabled,
    setStateInspectorEnabled,
    toggleStateInspector,
    stateSourceSnapshots,
    registerStateInspector,
    applyStateInspectorUpdate,
    refreshStateInspectorSource,
    logEntries,
    clearLogs,
    exportLogs,
    ],
    );

    return (
    <DebugContext.Provider value={value}>{children}</DebugContext.Provider>
    );
    }