import { useState, useEffect } from 'react'; import { TogglePill } from '@/components/ui/toggle-pill'; import { Skeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/context/AuthContext'; import { RefreshCw } from 'lucide-react'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; import { useNodes } from '@/context/NodeContext'; import { SENCHO_SETTINGS_CHANGED } from '@/lib/events'; import type { SenchoSettingsChangedDetail } from '@/lib/events'; import { DEFAULT_SETTINGS } from './types'; import type { PatchableSettings } from './types'; import { SettingsSection } from './SettingsSection'; import { SettingsField } from './SettingsField'; import { SettingsActions, SettingsPrimaryButton } from './SettingsActions'; import { useMastheadStats } from './MastheadStatsContext'; import { useSettingsDirty } from './useSettingsDirty'; import { useNodeSettingsLoad } from './useNodeSettingsLoad'; import { SettingsLoadGate } from './SettingsLoadError'; interface DeveloperSectionProps { onDirtyChange?: (dirty: boolean) => void; } function SectionSkeleton() { return (
); } type DeveloperFields = Pick; const DEFAULT_DEVELOPER: DeveloperFields = { developer_mode: DEFAULT_SETTINGS.developer_mode, }; export function DeveloperSection({ onDirtyChange }: DeveloperSectionProps) { const { can, permissionsReady } = useAuth(); const { activeNode } = useNodes(); const readOnly = !permissionsReady || !can('system:settings'); const { settings, setSettings, hasChanges, reset, markSaved } = useSettingsDirty({ ...DEFAULT_DEVELOPER }); const { phase, isCurrentNodeLoaded, load, isSaveOwner, captureSaveGuard } = useNodeSettingsLoad(activeNode?.id); const [isSaving, setIsSaving] = useState(false); const reportDirty = isCurrentNodeLoaded && hasChanges; useEffect(() => { onDirtyChange?.(reportDirty); }, [reportDirty, onDirtyChange]); useMastheadStats( !isCurrentNodeLoaded ? null : [ { label: 'DEV MODE', value: settings.developer_mode === '1' ? 'on' : 'off', tone: settings.developer_mode === '1' ? 'warn' : 'subtitle', }, ], ); useEffect(() => { let cancelled = false; setIsSaving(false); void (async () => { const nodeData = await load(); if (cancelled || !nodeData) return; const safe: DeveloperFields = { developer_mode: (nodeData.developer_mode as '0' | '1') ?? DEFAULT_SETTINGS.developer_mode, }; reset(safe); })(); return () => { cancelled = true; }; }, [activeNode?.id, load, reset]); const onSettingChange = (key: K, value: DeveloperFields[K]) => { setSettings(prev => ({ ...prev, [key]: value })); }; const saveSettings = async () => { const saveGuard = captureSaveGuard(); const submitted = { ...settings }; const payload = { developer_mode: submitted.developer_mode, }; setIsSaving(true); try { const res = await apiFetch('/settings', { method: 'PATCH', nodeId: saveGuard.nodeId, body: JSON.stringify(payload), }); if (!isSaveOwner(saveGuard)) return; if (!res.ok) { const err = await res.json().catch(() => ({})); toast.error(err?.error || err?.message || 'Failed to save settings.'); return; } markSaved(submitted); toast.success('Developer settings saved.'); window.dispatchEvent(new CustomEvent(SENCHO_SETTINGS_CHANGED, { detail: { changedKeys: Object.keys(payload) }, })); } catch (e: unknown) { if (!isSaveOwner(saveGuard)) return; toast.error((e as Error)?.message || 'Something went wrong.'); } finally { if (isSaveOwner(saveGuard)) setIsSaving(false); } }; return ( }>
onSettingChange('developer_mode', c ? '1' : '0')} /> {!readOnly && ( {isSaving ? ( <> Saving ) : ( 'Save settings' )} )}
); }