feat: make DDNS domain_name the effective domain across all services
Unit Tests / test (push) Successful in 11m35s
Unit Tests / test (push) Successful in 11m35s
- ConfigManager.get_effective_domain(): returns domain_name when DDNS
active (pic_ngo/cloudflare/duckdns), domain otherwise. Used by all
public-facing services so they use the real registered FQDN.
- ConfigManager.get_internal_domain(): always returns _identity.domain
(CoreDNS zone name, dnsmasq, cell-link invites — stays internal).
- Silent migration: if domain_mode != lan and domain is generic "cell",
auto-set to {cell_name}.local for unique CoreDNS zone naming.
- caddy_manager: fix custom_domain bug — cloudflare/http01 modes were
reading identity.get('custom_domain') which never exists; now reads
domain_name correctly.
- routes/config, app: expose effective_domain in GET /api/config and
/api/status responses.
- email_manager, routes/email: use get_effective_domain() for
OVERRIDE_HOSTNAME, POSTMASTER_ADDRESS, and new-user email defaults.
- ServiceBus.IDENTITY_CHANGED event: emitted from PUT /api/config and
POST /api/ddns/register after identity writes; caddy_manager and
email_manager subscribe to regenerate config automatically.
- Settings.jsx: hide Local Domain input in non-LAN modes; show
read-only effective_domain with "managed by DDNS" badge and an
Advanced toggle for the internal CoreDNS zone name.
- 11 new test classes covering all new helpers, event subscriptions,
caddy/email handlers, and the custom_domain fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -433,6 +433,8 @@ function Settings() {
|
||||
const [identity, setIdentity] = useState({ cell_name: '', domain: '', ip_range: '' });
|
||||
const [identityDirty, setIdentityDirty] = useState(false);
|
||||
const [loadedCellName, setLoadedCellName] = useState('');
|
||||
const [effectiveDomain, setEffectiveDomain] = useState('');
|
||||
const [showAdvancedZone, setShowAdvancedZone] = useState(false);
|
||||
|
||||
// DDNS
|
||||
const [domainMode, setDomainMode] = useState('lan');
|
||||
@@ -477,6 +479,7 @@ function Settings() {
|
||||
ip_range: cfg.ip_range || '',
|
||||
});
|
||||
setLoadedCellName(cfg.cell_name || '');
|
||||
setEffectiveDomain(cfg.effective_domain || cfg.domain_name || cfg.domain || '');
|
||||
setIdentityDirty(false);
|
||||
setDomainMode(cfg.domain_mode || 'lan');
|
||||
setDomainName(cfg.domain_name || '');
|
||||
@@ -514,9 +517,11 @@ function Settings() {
|
||||
? 'Cell name must be 255 characters or fewer'
|
||||
: (!identity.cell_name ? 'Cell name is required' : null);
|
||||
|
||||
const domainError = identity.domain && identity.domain.length > 255
|
||||
? 'Domain must be 255 characters or fewer'
|
||||
: (!identity.domain ? 'Domain is required' : null);
|
||||
const domainError = domainMode !== 'lan'
|
||||
? null
|
||||
: (identity.domain && identity.domain.length > 255
|
||||
? 'Domain must be 255 characters or fewer'
|
||||
: (!identity.domain ? 'Domain is required' : null));
|
||||
|
||||
// pic_ngo availability check — fires 900ms after cell_name changes
|
||||
const picAvailTimerRef = useRef(null);
|
||||
@@ -553,6 +558,7 @@ function Settings() {
|
||||
// Refresh to get updated domain_name after DDNS registration
|
||||
const cfgRes = await cellAPI.getConfig();
|
||||
setDomainName(cfgRes.data.domain_name || '');
|
||||
setEffectiveDomain(cfgRes.data.effective_domain || cfgRes.data.domain_name || cfgRes.data.domain || '');
|
||||
setDdnsHasToken(cfgRes.data.ddns?.has_token || false);
|
||||
refreshConfig();
|
||||
} catch (err) {
|
||||
@@ -867,14 +873,48 @@ function Settings() {
|
||||
<p className="mt-1 text-xs text-gray-400">External: <span className="font-mono">{identity.cell_name || '…'}.pic.ngo</span></p>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="Local Domain" error={domainError}>
|
||||
<TextInput
|
||||
value={identity.domain}
|
||||
onChange={(v) => { setIdentity((i) => ({ ...i, domain: v })); setIdentityDirty(true); draftConfig?.setDirty('identity', true); }}
|
||||
placeholder="cell"
|
||||
maxLength={255}
|
||||
/>
|
||||
</Field>
|
||||
{domainMode === 'lan' ? (
|
||||
<Field label="Local Domain" error={domainError}>
|
||||
<TextInput
|
||||
value={identity.domain}
|
||||
onChange={(v) => { setIdentity((i) => ({ ...i, domain: v })); setIdentityDirty(true); draftConfig?.setDirty('identity', true); }}
|
||||
placeholder="cell"
|
||||
maxLength={255}
|
||||
/>
|
||||
</Field>
|
||||
) : (
|
||||
<Field label="Cell Domain">
|
||||
<div className="rounded-lg bg-gray-50 border border-gray-200 px-3 py-2.5 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-500 mb-0.5">Cell Domain</p>
|
||||
<p className="text-sm font-mono text-gray-800">{effectiveDomain || `${identity.cell_name}.pic.ngo`}</p>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">managed by DDNS</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvancedZone((v) => !v)}
|
||||
className="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1"
|
||||
>
|
||||
{showAdvancedZone ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||
Advanced
|
||||
</button>
|
||||
{showAdvancedZone && (
|
||||
<div className="mt-2 pl-1 border-l-2 border-gray-100">
|
||||
<Field label="Internal zone name (advanced)" error={identity.domain && identity.domain.length > 255 ? 'Domain must be 255 characters or fewer' : null} hint="Used for LAN DNS — most users should not change this">
|
||||
<TextInput
|
||||
value={identity.domain}
|
||||
onChange={(v) => { setIdentity((i) => ({ ...i, domain: v })); setIdentityDirty(true); draftConfig?.setDirty('identity', true); }}
|
||||
placeholder="cell"
|
||||
maxLength={255}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
<Field label="IP Range" hint="Docker bridge subnet" error={ipRangeError}>
|
||||
<TextInput
|
||||
value={identity.ip_range}
|
||||
|
||||
Reference in New Issue
Block a user