feat: improve setup wizard and DDNS UX
Unit Tests / test (push) Successful in 7m29s

Setup wizard (Issue 1 — UI):
- pic.ngo subdomain input now uses the same split-field style as DuckDNS:
  input + static '.pic.ngo' suffix in a flex row, availability status below

Setup wizard (Issue 2 — Caddy not regenerating after completion):
- complete_setup route now fires IDENTITY_CHANGED after a successful wizard
  submission so CaddyManager regenerates the Caddyfile immediately; users
  no longer need to press 'Renew Certificate' to start ACME

Settings — DDNS status (Issue 2 — domain status missing):
- New GET /api/ddns/status endpoint: returns registered flag, domain_name,
  public_ip (ipify with 30s cache), last_ip from heartbeat
- Settings DDNS section for pic_ngo now shows a live status row with
  color-coded dot (green=registered+current, yellow=registered+stale,
  gray=not registered), current public IP, and a Check button
- Status auto-refreshes on mount and after each successful re-registration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 00:36:47 -04:00
parent fb0326dae7
commit 40f9d90fad
5 changed files with 114 additions and 14 deletions
+36
View File
@@ -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."""