feat: DDNS self-healing heartbeat + manual re-register endpoint
Unit Tests / test (push) Successful in 15m26s

- DDNSTokenExpired exception triggers auto re-register in update_ip()
  so cells recover silently after a DDNS DB reset
- POST /api/ddns/register lets the user force re-registration from Settings
- Re-register button in Settings → External Domain & DDNS (pic_ngo only)
- 3 new tests covering register endpoint: wrong provider, missing name, success

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 15:05:27 -04:00
parent cde177966d
commit 0b31d02f10
5 changed files with 106 additions and 3 deletions
+28 -3
View File
@@ -444,6 +444,7 @@ function Settings() {
const [ddnsDuckStatus, setDdnsDuckStatus] = useState(null);
const [ddnsDirty, setDdnsDirty] = useState(false);
const [ddnsSaving, setDdnsSaving] = useState(false);
const [ddnsRegistering, setDdnsRegistering] = useState(false);
// service configs
const [serviceConfigs, setServiceConfigs] = useState({});
@@ -595,6 +596,21 @@ function Settings() {
}
}, [ddnsCfToken, domainName]);
const reRegister = useCallback(async () => {
setDdnsRegistering(true);
try {
const res = await ddnsAPI.register();
setDomainName(res.data.subdomain || '');
setDdnsHasToken(true);
setPicAvail(null);
toast(`Registered as ${res.data.subdomain}`);
} catch (err) {
toast(err.response?.data?.error || 'Registration failed', 'error');
} finally {
setDdnsRegistering(false);
}
}, []);
const verifyDuck = useCallback(async () => {
if (!ddnsDuckToken.trim()) return;
setDdnsDuckStatus('checking');
@@ -865,9 +881,18 @@ function Settings() {
<Section icon={Globe} title="External Domain & DDNS" collapsible defaultOpen>
<div className="space-y-3">
{domainMode === 'pic_ngo' && (
<div className="rounded-lg bg-blue-50 border border-blue-100 px-4 py-3 text-sm text-blue-700">
Your cell is registered as <span className="font-mono font-semibold">{domainName || `${identity.cell_name}.pic.ngo`}</span> on pic.ngo.
Change the Cell Name above to update this subdomain.
<div className="space-y-2">
<div className="rounded-lg bg-blue-50 border border-blue-100 px-4 py-3 text-sm text-blue-700">
Your cell is registered as <span className="font-mono font-semibold">{domainName || `${identity.cell_name}.pic.ngo`}</span> on pic.ngo.
Change the Cell Name above to update this subdomain.
</div>
<button
onClick={reRegister}
disabled={ddnsRegistering}
className="px-3 py-1.5 text-xs font-medium rounded border border-blue-300 text-blue-700 hover:bg-blue-50 disabled:opacity-50"
>
{ddnsRegistering ? 'Registering…' : 'Re-register with pic.ngo'}
</button>
</div>
)}
{domainMode === 'cloudflare' && (
+1
View File
@@ -325,6 +325,7 @@ export const logsAPI = {
export const ddnsAPI = {
checkName: (name) => api.get(`/api/ddns/check/${name}`),
updateConfig: (data) => api.put('/api/ddns', data),
register: () => api.post('/api/ddns/register'),
};
// Setup Wizard API