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 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:56:52 -04:00
parent ad2eaca273
commit 742e4209ee
+10 -2
View File
@@ -432,6 +432,7 @@ function Settings() {
// identity // identity
const [identity, setIdentity] = useState({ cell_name: '', domain: '', ip_range: '' }); const [identity, setIdentity] = useState({ cell_name: '', domain: '', ip_range: '' });
const [identityDirty, setIdentityDirty] = useState(false); const [identityDirty, setIdentityDirty] = useState(false);
const [loadedCellName, setLoadedCellName] = useState('');
// DDNS // DDNS
const [domainMode, setDomainMode] = useState('lan'); const [domainMode, setDomainMode] = useState('lan');
@@ -475,6 +476,7 @@ function Settings() {
domain: cfg.domain || '', domain: cfg.domain || '',
ip_range: cfg.ip_range || '', ip_range: cfg.ip_range || '',
}); });
setLoadedCellName(cfg.cell_name || '');
setIdentityDirty(false); setIdentityDirty(false);
setDomainMode(cfg.domain_mode || 'lan'); setDomainMode(cfg.domain_mode || 'lan');
setDomainName(cfg.domain_name || ''); setDomainName(cfg.domain_name || '');
@@ -545,6 +547,7 @@ function Settings() {
try { try {
const res = await cellAPI.updateConfig(identity); const res = await cellAPI.updateConfig(identity);
setIdentityDirty(false); setIdentityDirty(false);
setLoadedCellName(identity.cell_name);
draftConfig?.setDirty('identity', false); draftConfig?.setDirty('identity', false);
if (res.data.warnings?.length) res.data.warnings.forEach((w) => toast(w, 'warning')); if (res.data.warnings?.length) res.data.warnings.forEach((w) => toast(w, 'warning'));
// Refresh to get updated domain_name after DDNS registration // Refresh to get updated domain_name after DDNS registration
@@ -687,10 +690,15 @@ function Settings() {
useEffect(() => { useEffect(() => {
if (!identityDirty) return; if (!identityDirty) return;
if (ipRangeError || cellNameError || domainError) 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); const timer = setTimeout(() => saveIdentityRef.current(), 800);
return () => clearTimeout(timer); 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(() => { useEffect(() => {
const timers = SERVICE_DEFS const timers = SERVICE_DEFS