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 { useExperimental } from '@/hooks/useExperimental';
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';
interface FleetMeshSectionProps {
onDirtyChange?: (dirty: boolean) => void;
}
function SectionSkeleton() {
return (
);
}
type FleetMeshFields = Pick;
type SnapshotOnlyFields = Pick;
const DEFAULT_FLEET_MESH: FleetMeshFields = {
mesh_auto_recreate: DEFAULT_SETTINGS.mesh_auto_recreate,
snapshot_documentation: DEFAULT_SETTINGS.snapshot_documentation,
};
export function FleetMeshSection({ onDirtyChange }: FleetMeshSectionProps) {
const { activeNode } = useNodes();
const { isAdmin } = useAuth();
const { experimental, experimentalReady } = useExperimental();
const showMesh = experimentalReady && experimental;
const readOnly = !isAdmin;
const { settings, setSettings, dirtyCount, hasChanges, reset, markSaved } = useSettingsDirty({ ...DEFAULT_FLEET_MESH });
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: FleetMeshFields = {
mesh_auto_recreate: (nodeData.mesh_auto_recreate as '0' | '1') ?? DEFAULT_SETTINGS.mesh_auto_recreate,
snapshot_documentation: (nodeData.snapshot_documentation as '0' | '1') ?? DEFAULT_SETTINGS.snapshot_documentation,
};
reset(safe);
})();
return () => {
cancelled = true;
};
}, [activeNode?.id, load, reset]);
const onSettingChange = (key: K, value: FleetMeshFields[K]) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
const saveSettings = async () => {
// When Mesh discovery is off, never write mesh_auto_recreate: a failed
// settings read would otherwise push the default and overwrite a real
// Mesh config the operator cannot see.
const saveGuard = captureSaveGuard();
const submitted: FleetMeshFields | SnapshotOnlyFields = showMesh
? { ...settings }
: { snapshot_documentation: settings.snapshot_documentation };
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;
}
if (showMesh) {
markSaved(submitted as FleetMeshFields);
} else {
markSaved({
...settings,
snapshot_documentation: (submitted as SnapshotOnlyFields).snapshot_documentation,
});
}
toast.success('Fleet 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 (
}>
);
}