From 742e4209ee0ed584cdc30d5a390d9fa1aad3b7d0 Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Wed, 27 May 2026 13:56:52 -0400 Subject: [PATCH] fix: don't register pic.ngo subdomain until availability check completes Auto-save was firing with picAvail === null (the moment the user typed a new cell name, before the 900ms availability debounce even started), which caused the backend to immediately register the subdomain on DDNS. Track the last saved/loaded cell name in loadedCellName. When domainMode is pic_ngo and the typed name differs from the loaded name, block auto-save until picAvail reaches a terminal state (available or unreachable). Also update loadedCellName on successful save so subsequent edits to the same name are not blocked. Co-Authored-By: Claude Sonnet 4.6 --- webui/src/pages/Settings.jsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/webui/src/pages/Settings.jsx b/webui/src/pages/Settings.jsx index 115e62f..654cc97 100644 --- a/webui/src/pages/Settings.jsx +++ b/webui/src/pages/Settings.jsx @@ -432,6 +432,7 @@ function Settings() { // identity const [identity, setIdentity] = useState({ cell_name: '', domain: '', ip_range: '' }); const [identityDirty, setIdentityDirty] = useState(false); + const [loadedCellName, setLoadedCellName] = useState(''); // DDNS const [domainMode, setDomainMode] = useState('lan'); @@ -475,6 +476,7 @@ function Settings() { domain: cfg.domain || '', ip_range: cfg.ip_range || '', }); + setLoadedCellName(cfg.cell_name || ''); setIdentityDirty(false); setDomainMode(cfg.domain_mode || 'lan'); setDomainName(cfg.domain_name || ''); @@ -545,6 +547,7 @@ function Settings() { try { const res = await cellAPI.updateConfig(identity); setIdentityDirty(false); + setLoadedCellName(identity.cell_name); draftConfig?.setDirty('identity', false); if (res.data.warnings?.length) res.data.warnings.forEach((w) => toast(w, 'warning')); // Refresh to get updated domain_name after DDNS registration @@ -687,10 +690,15 @@ function Settings() { useEffect(() => { if (!identityDirty) return; if (ipRangeError || cellNameError || domainError) return; - if (domainMode === 'pic_ngo' && (picAvail === 'taken' || picAvail === 'checking')) return; + // In pic_ngo mode, if the cell name differs from what was last saved/loaded, + // wait for the availability check to reach a terminal state before saving. + // 'available' and 'unreachable' are terminal; null/'checking'/'taken' are not. + if (domainMode === 'pic_ngo' && identity.cell_name !== loadedCellName) { + if (picAvail !== 'available' && picAvail !== 'unreachable') return; + } const timer = setTimeout(() => saveIdentityRef.current(), 800); return () => clearTimeout(timer); - }, [identity, identityDirty, ipRangeError, cellNameError, domainError, domainMode, picAvail]); // eslint-disable-line react-hooks/exhaustive-deps + }, [identity, identityDirty, ipRangeError, cellNameError, domainError, domainMode, picAvail, loadedCellName]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { const timers = SERVICE_DEFS