fix: all service pages use live domain; cell_name/domain propagate to DNS; /api/status reads stored identity

Changes:
- ConfigContext.jsx: React context that loads /api/config once; exposes domain,
  cell_name, refresh() — wraps entire app in App.jsx
- Email/Calendar/Files pages: replace hardcoded 'mail.cell', 'calendar.cell',
  'files.cell', 'webdav.cell' with domain from ConfigContext; hostname updates
  immediately after Settings save (refreshConfig() called on save)
- /api/status: cell_name and domain now read from stored _identity in config_manager,
  not hardcoded 'personal-internet-cell' / 'cell.local'
- network_manager.apply_cell_name(old, new): updates hostname A-record in primary
  zone file and reloads CoreDNS; called from PUT /api/config when cell_name changes
- Old identity captured before save so apply_cell_name gets the correct old value
- Settings EmailForm: smtp/imap ports are read-only with note (docker-compose.yml level)
- Settings FilesForm: port is read-only with note (Caddy proxies on 80 externally)
- Settings CalendarForm: port labeled "Internal port; clients use 80 via Caddy"

Tests added:
- test_apply_cell_name_renames_host_record
- test_apply_cell_name_noop_when_same

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 05:05:51 -04:00
parent ac9b26303f
commit 1f3386d43b
9 changed files with 157 additions and 29 deletions
+14 -2
View File
@@ -356,9 +356,10 @@ def get_cell_status():
current_time = time.time()
uptime_seconds = int(current_time - API_START_TIME)
identity = config_manager.configs.get('_identity', {})
return jsonify({
"cell_name": "personal-internet-cell",
"domain": "cell.local",
"cell_name": identity.get('cell_name', os.environ.get('CELL_NAME', 'mycell')),
"domain": identity.get('domain', os.environ.get('CELL_DOMAIN', 'cell')),
"uptime": uptime_seconds,
"peers_count": len(peers),
"services": services_status,
@@ -397,6 +398,8 @@ def update_config():
# Handle identity fields (cell_name, domain, ip_range, wireguard_port)
identity_keys = {'cell_name', 'domain', 'ip_range', 'wireguard_port'}
identity_updates = {k: v for k, v in data.items() if k in identity_keys}
# Capture old identity BEFORE saving, for apply_cell_name comparison
old_identity = dict(config_manager.configs.get('_identity', {}))
if identity_updates:
stored = config_manager.configs.get('_identity', {})
stored.update(identity_updates)
@@ -439,6 +442,15 @@ def update_config():
all_restarted.extend(net_result.get('restarted', []))
all_warnings.extend(net_result.get('warnings', []))
# Apply cell name change to DNS hostname record
if identity_updates.get('cell_name'):
old_name = old_identity.get('cell_name', os.environ.get('CELL_NAME', 'mycell'))
new_name = identity_updates['cell_name']
if old_name != new_name:
cn_result = network_manager.apply_cell_name(old_name, new_name)
all_restarted.extend(cn_result.get('restarted', []))
all_warnings.extend(cn_result.get('warnings', []))
logger.info(f"Updated config, restarted: {all_restarted}")
return jsonify({
"message": "Configuration updated and applied",
+30
View File
@@ -422,6 +422,36 @@ class NetworkManager(BaseServiceManager):
warnings.append(f"CoreDNS reload failed: {e}")
return {'restarted': restarted, 'warnings': warnings}
def apply_cell_name(self, old_name: str, new_name: str) -> Dict[str, Any]:
"""Update the cell hostname record in the primary DNS zone file."""
restarted = []
warnings = []
if not old_name or not new_name or old_name == new_name:
return {'restarted': restarted, 'warnings': warnings}
try:
dns_data = os.path.join(self.data_dir, 'dns')
if os.path.isdir(dns_data):
for fname in os.listdir(dns_data):
if fname.endswith('.zone') and 'local' not in fname:
zone_file = os.path.join(dns_data, fname)
with open(zone_file) as f:
content = f.read()
# Replace hostname record: old_name IN A ...
import re
content = re.sub(
rf'^{re.escape(old_name)}(\s+IN\s+A\s+)',
f'{new_name}\\1',
content, flags=re.MULTILINE
)
with open(zone_file, 'w') as f:
f.write(content)
break
self._reload_dns_service()
restarted.append('cell-dns (reloaded)')
except Exception as e:
warnings.append(f"cell_name DNS update failed: {e}")
return {'restarted': restarted, 'warnings': warnings}
def test_dns_resolution(self, domain: str) -> Dict:
"""Test DNS resolution for a domain using Python socket."""