diff --git a/api/routes/config.py b/api/routes/config.py index 87dbae5..c74ea3a 100644 --- a/api/routes/config.py +++ b/api/routes/config.py @@ -586,6 +586,42 @@ def update_ddns_config(): return jsonify({'error': str(e)}), 500 +_ddns_public_ip_cache: dict = {'ip': None, 'at': 0} + +@bp.route('/api/ddns/status', methods=['GET']) +def ddns_status(): + import time as _time + from app import config_manager + ddns_cfg = config_manager.configs.get('ddns', {}) + identity = config_manager.configs.get('_identity', {}) + + now = _time.time() + if now - _ddns_public_ip_cache['at'] > 30 or not _ddns_public_ip_cache['ip']: + try: + import requests as _req + resp = _req.get('https://api.ipify.org', timeout=5) + if resp.ok: + _ddns_public_ip_cache['ip'] = resp.text.strip() + _ddns_public_ip_cache['at'] = now + except Exception: + pass + + last_ip = None + try: + from app import ddns_manager as _ddns_mgr_singleton + last_ip = _ddns_mgr_singleton._last_ip + except Exception: + pass + + registered = bool(ddns_cfg.get('token')) + return jsonify({ + 'registered': registered, + 'domain_name': identity.get('domain_name', ''), + 'public_ip': _ddns_public_ip_cache['ip'], + 'last_ip': last_ip, + }) + + @bp.route('/api/ddns/register', methods=['POST']) def ddns_register(): """Trigger (re-)registration with the configured DDNS provider.""" diff --git a/api/routes/setup.py b/api/routes/setup.py index da8722a..fa2afb8 100644 --- a/api/routes/setup.py +++ b/api/routes/setup.py @@ -88,6 +88,19 @@ def complete_setup(): payload = request.get_json(silent=True) or {} result = sm.complete_setup(payload) + if result.get('success'): + try: + from app import config_manager, service_bus, EventType + identity = config_manager.configs.get('_identity', {}) + service_bus.publish_event(EventType.IDENTITY_CHANGED, 'setup', { + 'cell_name': identity.get('cell_name'), + 'domain': identity.get('domain'), + 'domain_name': identity.get('domain_name'), + 'domain_mode': identity.get('domain_mode'), + 'effective_domain': config_manager.get_effective_domain(), + }) + except Exception as exc: + logger.warning(f'Failed to publish IDENTITY_CHANGED after setup: {exc}') status_code = 200 if result.get('success') else 400 return jsonify(result), status_code diff --git a/webui/src/pages/Settings.jsx b/webui/src/pages/Settings.jsx index 92f27d5..811764b 100644 --- a/webui/src/pages/Settings.jsx +++ b/webui/src/pages/Settings.jsx @@ -354,6 +354,8 @@ function Settings() { const [ddnsDirty, setDdnsDirty] = useState(false); const [ddnsSaving, setDdnsSaving] = useState(false); const [ddnsRegistering, setDdnsRegistering] = useState(false); + const [ddnsStatus, setDdnsStatus] = useState(null); + const [ddnsStatusLoading, setDdnsStatusLoading] = useState(false); const [certStatus, setCertStatus] = useState(null); // {status, expiry, days_remaining} // service configs @@ -411,6 +413,10 @@ function Settings() { useEffect(() => { loadAll(); }, [loadAll]); + useEffect(() => { + if (domainMode === 'pic_ngo') checkDdnsStatus(); + }, [domainMode, checkDdnsStatus]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { const handler = () => loadAll(); window.addEventListener('pic-config-discarded', handler); @@ -514,6 +520,18 @@ function Settings() { } }, [ddnsCfToken, domainName]); + const checkDdnsStatus = useCallback(async () => { + setDdnsStatusLoading(true); + try { + const res = await ddnsAPI.getStatus(); + setDdnsStatus(res.data); + } catch { + setDdnsStatus(null); + } finally { + setDdnsStatusLoading(false); + } + }, []); + const reRegister = useCallback(async () => { setDdnsRegistering(true); try { @@ -522,12 +540,13 @@ function Settings() { setDdnsHasToken(true); setPicAvail(null); toast(`Registered as ${res.data.subdomain}`); + checkDdnsStatus(); } catch (err) { toast(err.response?.data?.error || 'Registration failed', 'error'); } finally { setDdnsRegistering(false); } - }, []); + }, [checkDdnsStatus]); const verifyDuck = useCallback(async () => { if (!ddnsDuckToken.trim()) return; @@ -843,6 +862,35 @@ function Settings() { Your cell is registered as {domainName || `${identity.cell_name}.pic.ngo`} on pic.ngo. Change the Cell Name above to update this subdomain. + {(() => { + const ipsMatch = ddnsStatus?.registered && ddnsStatus?.public_ip && ddnsStatus?.last_ip && ddnsStatus.public_ip === ddnsStatus.last_ip; + const dotColor = !ddnsStatus || !ddnsStatus.registered + ? 'bg-gray-400' + : ipsMatch + ? 'bg-green-500' + : 'bg-yellow-400'; + const label = !ddnsStatus || !ddnsStatus.registered + ? 'Not registered' + : ipsMatch + ? 'Registered — IP current' + : 'Registered — IP stale'; + return ( +