From 99dcb1332a16508afa6965068d0a1726078910e3 Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Tue, 26 May 2026 07:56:59 -0400 Subject: [PATCH] wizard: check pic.ngo availability on Next, not just on blur The availability check was only triggered onBlur, so clicking Next without blurring the field skipped the DDNS request entirely. Now handleNext awaits the check and blocks with an error if the name is taken. Unknown/unreachable DDNS is treated as available to avoid blocking the wizard. Co-Authored-By: Claude Sonnet 4.6 --- webui/src/pages/Setup.jsx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/webui/src/pages/Setup.jsx b/webui/src/pages/Setup.jsx index 1ec3cb1..fd21c42 100644 --- a/webui/src/pages/Setup.jsx +++ b/webui/src/pages/Setup.jsx @@ -281,14 +281,20 @@ function Step2Domain({ const [picAvail, setPicAvail] = useState(null); // null|checking|available|taken|unknown const [cfStatus, setCfStatus] = useState(null); const [dnsStatus, setDnsStatus] = useState(null); + const [nextLoading, setNextLoading] = useState(false); const checkPicAvail = async name => { - if (!CELL_NAME_RE.test(name)) return; + if (!CELL_NAME_RE.test(name)) return null; setPicAvail('checking'); try { const res = await setupAPI.validate('pic_ngo_available', { cell_name: name }); - setPicAvail(res.data?.available ? 'available' : 'taken'); - } catch { setPicAvail('unknown'); } + const result = res.data?.available ? 'available' : 'taken'; + setPicAvail(result); + return result; + } catch { + setPicAvail('unknown'); + return 'unknown'; + } }; const verifyCf = async () => { @@ -307,11 +313,19 @@ function Step2Domain({ } catch { setDnsStatus('invalid'); } }; - const handleNext = () => { + const handleNext = async () => { const e = {}; if (domainType === 'pic_ngo') { - if (!picName) e.name = 'Subdomain is required.'; - else if (!CELL_NAME_RE.test(picName)) e.name = 'Lowercase letters, digits, hyphens. 2–31 chars, must start with a letter.'; + if (!picName) { + e.name = 'Subdomain is required.'; + } else if (!CELL_NAME_RE.test(picName)) { + e.name = 'Lowercase letters, digits, hyphens. 2–31 chars, must start with a letter.'; + } else if (picAvail !== 'available') { + setNextLoading(true); + const result = await checkPicAvail(picName); + setNextLoading(false); + if (result === 'taken') e.name = 'This subdomain is already taken. Choose another.'; + } } else if (domainType === 'cloudflare') { if (!customDomain || !DOMAIN_RE.test(customDomain)) e.domain = 'Enter a valid domain (e.g. home.example.com).'; if (!cloudflareToken.trim()) e.token = 'Cloudflare API token is required.'; @@ -441,7 +455,7 @@ function Step2Domain({ )} - + ); }