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
+17 -7
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useConfig } from '../contexts/ConfigContext';
import {
Settings as SettingsIcon, Server, Shield, Network, Mail, Calendar,
HardDrive, GitBranch, Archive, Upload, Download, Trash2, RotateCcw,
@@ -194,12 +195,16 @@ function EmailForm({ data, onChange }) {
<Field label="Mail Domain">
<TextInput value={data.domain} onChange={(v) => onChange({ ...data, domain: v })} placeholder="mail.example.com" />
</Field>
<Field label="SMTP Port">
<NumberInput value={data.smtp_port} onChange={(v) => onChange({ ...data, smtp_port: v })} min={1} max={65535} />
<Field label="SMTP Port" hint="Fixed by docker-compose.yml">
<TextInput value={data.smtp_port ?? 587} readOnly />
</Field>
<Field label="IMAP Port">
<NumberInput value={data.imap_port} onChange={(v) => onChange({ ...data, imap_port: v })} min={1} max={65535} />
<Field label="IMAP Port" hint="Fixed by docker-compose.yml">
<TextInput value={data.imap_port ?? 993} readOnly />
</Field>
<p className="text-xs text-gray-400">
Ports 587 (SMTP) and 993 (IMAP) are set by docker-compose port bindings and cannot be changed at runtime.
Only <strong>domain</strong> is applied on Save.
</p>
</div>
);
}
@@ -207,7 +212,7 @@ function EmailForm({ data, onChange }) {
function CalendarForm({ data, onChange }) {
return (
<div className="space-y-3">
<Field label="CalDAV Port">
<Field label="Radicale Port" hint="Internal port; clients use port 80 via Caddy">
<NumberInput value={data.port} onChange={(v) => onChange({ ...data, port: v })} min={1} max={65535} />
</Field>
<Field label="Data Directory">
@@ -220,8 +225,8 @@ function CalendarForm({ data, onChange }) {
function FilesForm({ data, onChange }) {
return (
<div className="space-y-3">
<Field label="WebDAV Port">
<NumberInput value={data.port} onChange={(v) => onChange({ ...data, port: v })} min={1} max={65535} />
<Field label="Internal Port" hint="Fixed by docker-compose.yml">
<TextInput value={data.port ?? 80} readOnly />
</Field>
<Field label="Data Directory">
<TextInput value={data.data_dir} onChange={(v) => onChange({ ...data, data_dir: v })} placeholder="/app/data/webdav" />
@@ -229,6 +234,9 @@ function FilesForm({ data, onChange }) {
<Field label="Default Quota (MB)">
<NumberInput value={data.quota} onChange={(v) => onChange({ ...data, quota: v })} min={0} />
</Field>
<p className="text-xs text-gray-400">
Clients always connect on port 80 via Caddy reverse proxy, regardless of internal port.
</p>
</div>
);
}
@@ -274,6 +282,7 @@ const SERVICE_DEFS = [
function Settings() {
const toasts = useToasts();
const { refresh: refreshConfig } = useConfig();
// identity
const [identity, setIdentity] = useState({ cell_name: '', domain: '', ip_range: '', wireguard_port: 51820 });
@@ -334,6 +343,7 @@ function Settings() {
const res = await cellAPI.updateConfig(identity);
setIdentityDirty(false);
_applyResult(res, 'Cell identity');
refreshConfig();
} catch {
toast('Failed to save identity', 'error');
} finally {