import { useState, useEffect } from 'react'; import { Skeleton } from '@/components/ui/skeleton'; import { RefreshCw } from 'lucide-react'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; import { useNodes } from '@/context/NodeContext'; import { useAuth } from '@/context/AuthContext'; import { canManageNode } from '@/lib/canManageNode'; 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'; import { TogglePill } from '@/components/ui/toggle-pill'; import { NumberChip } from './SystemControls'; interface DockerStorageSectionProps { onDirtyChange?: (dirty: boolean) => void; } function SectionSkeleton() { return (
); } type DockerStorageFields = Pick; const DEFAULT_DOCKER_STORAGE: DockerStorageFields = { docker_janitor_gb: DEFAULT_SETTINGS.docker_janitor_gb, prune_on_update: DEFAULT_SETTINGS.prune_on_update, reclaim_hero: DEFAULT_SETTINGS.reclaim_hero, }; export function DockerStorageSection({ onDirtyChange }: DockerStorageSectionProps) { const { activeNode } = useNodes(); const { can } = useAuth(); const readOnly = !canManageNode(can, activeNode?.id); const { settings, setSettings, dirtyCount, hasChanges, reset, markSaved } = useSettingsDirty({ ...DEFAULT_DOCKER_STORAGE }); 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: 'EDITED', value: hasChanges ? `${dirtyCount} pending` : 'saved', tone: hasChanges ? 'warn' : 'value', }, ], ); useEffect(() => { let cancelled = false; setIsSaving(false); void (async () => { const nodeData = await load(); if (cancelled || !nodeData) return; const safe: DockerStorageFields = { docker_janitor_gb: nodeData.docker_janitor_gb ?? DEFAULT_SETTINGS.docker_janitor_gb, prune_on_update: (nodeData.prune_on_update as '0' | '1') ?? DEFAULT_SETTINGS.prune_on_update, reclaim_hero: (nodeData.reclaim_hero as '0' | '1') ?? DEFAULT_SETTINGS.reclaim_hero, }; reset(safe); })(); return () => { cancelled = true; }; }, [activeNode?.id, load, reset]); const onSettingChange = (key: K, value: DockerStorageFields[K]) => { setSettings(prev => ({ ...prev, [key]: value })); }; const saveSettings = async () => { const saveGuard = captureSaveGuard(); const submitted = { ...settings }; setIsSaving(true); try { const res = await apiFetch('/settings', { method: 'PATCH', nodeId: saveGuard.nodeId, body: JSON.stringify(submitted), }); 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('Docker & storage settings saved.'); } catch (e: unknown) { if (!isSaveOwner(saveGuard)) return; toast.error((e as Error)?.message || 'Something went wrong.'); } finally { if (isSaveOwner(saveGuard)) setIsSaving(false); } }; return ( }>
onSettingChange('docker_janitor_gb', v)} suffix="GiB" min={0} step={0.5} warnOver={10} /> onSettingChange('reclaim_hero', next ? '1' : '0')} /> onSettingChange('prune_on_update', next ? '1' : '0')} /> {!readOnly && ( {isSaving ? ( <> Saving ) : ( 'Save settings' )} )}
); }