import { useState, useEffect } from 'react'; import { Checkbox } from '@/components/ui/checkbox'; import { SegmentedControl } from '@/components/ui/segmented-control'; 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 { useDeployFeedbackEnabled } from '@/hooks/use-deploy-feedback-enabled'; import { useDeployFeedbackStyle, type DeployFeedbackStyle } from '@/hooks/use-deploy-feedback-style'; import { useComposeDiffPreviewEnabled } from '@/hooks/use-compose-diff-preview-enabled'; 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 { TogglePill } from '@/components/ui/toggle-pill'; import { NumberChip } from './SystemControls'; const DEPLOY_STYLE_OPTIONS: { value: DeployFeedbackStyle; label: string }[] = [ { value: 'modal', label: 'Modal' }, { value: 'inline', label: 'Inline' }, ]; interface StacksSectionProps { onDirtyChange?: (dirty: boolean) => void; } type GuardrailFields = Pick; const DEFAULT_GUARDRAILS: GuardrailFields = { health_gate_enabled: DEFAULT_SETTINGS.health_gate_enabled, health_gate_window_seconds: DEFAULT_SETTINGS.health_gate_window_seconds, env_block_deploy_on_missing_required: DEFAULT_SETTINGS.env_block_deploy_on_missing_required, }; function GuardrailSkeleton() { return (
); } export function StacksSection({ onDirtyChange }: StacksSectionProps) { // Browser-local workflow controls (unchanged, no backend fetch) const [isEnabled, setEnabled] = useDeployFeedbackEnabled(); const [feedbackStyle, setFeedbackStyle] = useDeployFeedbackStyle(); const [diffPreviewEnabled, setDiffPreviewEnabled] = useComposeDiffPreviewEnabled(); // Node-scoped deploy guardrails const { activeNode } = useNodes(); const { isAdmin } = useAuth(); const readOnly = !isAdmin; const { settings, setSettings, dirtyCount, hasChanges, reset, markSaved } = useSettingsDirty({ ...DEFAULT_GUARDRAILS }); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); useEffect(() => { onDirtyChange?.(hasChanges); }, [hasChanges, onDirtyChange]); useMastheadStats( isLoading ? null : [ { label: 'EDITED', value: hasChanges ? `${dirtyCount} pending` : 'saved', tone: hasChanges ? 'warn' : 'value', }, ], ); useEffect(() => { const fetchSettings = async () => { setIsLoading(true); try { const nodeRes = await apiFetch('/settings'); const nodeData: Record = nodeRes.ok ? await nodeRes.json() : {}; const safe: GuardrailFields = { health_gate_enabled: (nodeData.health_gate_enabled as '0' | '1') ?? DEFAULT_SETTINGS.health_gate_enabled, health_gate_window_seconds: nodeData.health_gate_window_seconds ?? DEFAULT_SETTINGS.health_gate_window_seconds, env_block_deploy_on_missing_required: (nodeData.env_block_deploy_on_missing_required as '0' | '1') ?? DEFAULT_SETTINGS.env_block_deploy_on_missing_required, }; reset(safe); } catch (e) { console.error('Failed to fetch deploy guardrail settings', e); } finally { setIsLoading(false); } }; fetchSettings(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeNode?.id]); const onGuardrailChange = (key: K, value: GuardrailFields[K]) => { setSettings(prev => ({ ...prev, [key]: value })); }; const saveGuardrails = async () => { const submitted = { ...settings }; setIsSaving(true); try { const res = await apiFetch('/settings', { method: 'PATCH', body: JSON.stringify(submitted), }); if (!res.ok) { const err = await res.json().catch(() => ({})); toast.error(err?.error || err?.message || 'Failed to save settings.'); return; } markSaved(submitted); toast.success('Deploy guardrail settings saved.'); } catch (e: unknown) { toast.error((e as Error)?.message || 'Something went wrong.'); } finally { setIsSaving(false); } }; return (
setEnabled(v === true)} />
{isEnabled && ( )}
setDiffPreviewEnabled(v === true)} />

ⓘ saved to this browser only · every device remembers its own choice

{isLoading ? ( ) : (

Node-level safety checks and post-deploy observation used during stack deploys and updates.

onGuardrailChange('health_gate_enabled', next ? '1' : '0')} /> onGuardrailChange('health_gate_window_seconds', v)} suffix="s" min={15} max={600} /> onGuardrailChange('env_block_deploy_on_missing_required', next ? '1' : '0')} />
{!readOnly && ( {isSaving ? ( <> Saving ) : ( 'Save settings' )} )}
)}
); }