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
+20
View File
@@ -37,6 +37,10 @@ class DDNSError(Exception):
"""Raised when a DDNS provider returns an error response."""
class DDNSTokenExpired(DDNSError):
"""Raised when the DDNS service rejects the token (401) — usually after a DB reset."""
# ---------------------------------------------------------------------------
# Provider base class
# ---------------------------------------------------------------------------
@@ -96,6 +100,10 @@ class PicNgoDDNS(DDNSProvider):
def _raise_for_status(self, response: requests.Response, action: str):
if not response.ok:
if response.status_code == 401:
raise DDNSTokenExpired(
f"PicNgoDDNS {action} rejected token: HTTP 401 — {response.text}"
)
raise DDNSError(
f"PicNgoDDNS {action} failed: HTTP {response.status_code}{response.text}"
)
@@ -432,6 +440,18 @@ class DDNSManager(BaseServiceManager):
self._last_ip = current_ip
else:
logger.warning("DDNS update_ip: provider.update() returned False")
except DDNSTokenExpired:
logger.warning("DDNS update_ip: token rejected (401) — attempting re-registration")
try:
cell_name = self._identity().get('cell_name', '')
if cell_name:
self.register(cell_name, current_ip)
logger.info("DDNS re-registered after token expiry: cell_name=%r", cell_name)
self._last_ip = current_ip
else:
logger.error("DDNS update_ip: cannot re-register — cell_name not in identity")
except Exception as exc2:
logger.error("DDNS update_ip: re-registration failed: %s", exc2)
except DDNSError as exc:
logger.error("DDNS update_ip: provider error: %s", exc)