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:
@@ -16,6 +16,7 @@ import {
|
||||
Settings as SettingsIcon
|
||||
} from 'lucide-react';
|
||||
import { healthAPI } from './services/api';
|
||||
import { ConfigProvider } from './contexts/ConfigContext';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Peers from './pages/Peers';
|
||||
@@ -81,6 +82,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ConfigProvider>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Sidebar navigation={navigation} isOnline={isOnline} />
|
||||
|
||||
@@ -126,6 +128,7 @@ function App() {
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { cellAPI } from '../services/api';
|
||||
|
||||
const ConfigContext = createContext({ domain: 'cell', cell_name: 'mycell' });
|
||||
|
||||
export function ConfigProvider({ children }) {
|
||||
const [config, setConfig] = useState({ domain: 'cell', cell_name: 'mycell' });
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
cellAPI.getConfig().then(r => setConfig(r.data)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ ...config, refresh }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useConfig = () => useContext(ConfigContext);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Calendar as CalendarIcon, Users, Wifi, Copy, CheckCheck } from 'lucide-react';
|
||||
import { calendarAPI } from '../services/api';
|
||||
import { useConfig } from '../contexts/ConfigContext';
|
||||
|
||||
const CELL_HOST = 'calendar.cell';
|
||||
const CELL_IP = '172.20.0.21';
|
||||
|
||||
function CopyButton({ text }) {
|
||||
@@ -32,6 +32,8 @@ function InfoRow({ label, value }) {
|
||||
}
|
||||
|
||||
function Calendar() {
|
||||
const { domain = 'cell' } = useConfig();
|
||||
const cellHost = `calendar.${domain}`;
|
||||
const [users, setUsers] = useState([]);
|
||||
const [status, setStatus] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -81,9 +83,9 @@ function Calendar() {
|
||||
Use these settings in your calendar / contacts app (iOS, Android, Thunderbird, etc.)
|
||||
</p>
|
||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||
<InfoRow label="Server URL" value={`http://${CELL_HOST}`} />
|
||||
<InfoRow label="CalDAV path" value={`http://${CELL_HOST}/`} />
|
||||
<InfoRow label="CardDAV path" value={`http://${CELL_HOST}/`} />
|
||||
<InfoRow label="Server URL" value={`http://${cellHost}`} />
|
||||
<InfoRow label="CalDAV path" value={`http://${cellHost}/`} />
|
||||
<InfoRow label="CardDAV path" value={`http://${cellHost}/`} />
|
||||
<InfoRow label="Port" value="80" />
|
||||
<InfoRow label="Direct IP" value={CELL_IP} />
|
||||
<InfoRow label="Protocol" value="HTTP (CalDAV/CardDAV)" />
|
||||
@@ -104,7 +106,7 @@ function Calendar() {
|
||||
<p className="font-medium text-gray-900 mb-1">iOS (Settings → Calendar → Accounts)</p>
|
||||
<ol className="list-decimal ml-4 space-y-0.5 text-xs text-gray-600">
|
||||
<li>Add Account → Other → Add CalDAV Account</li>
|
||||
<li>Server: <span className="font-mono">calendar.cell</span></li>
|
||||
<li>Server: <span className="font-mono">{cellHost}</span></li>
|
||||
<li>Enter username & password</li>
|
||||
<li>For contacts: Add CardDAV Account, same server</li>
|
||||
</ol>
|
||||
@@ -113,7 +115,7 @@ function Calendar() {
|
||||
<p className="font-medium text-gray-900 mb-1">Android (DAVx⁵ app)</p>
|
||||
<ol className="list-decimal ml-4 space-y-0.5 text-xs text-gray-600">
|
||||
<li>Install DAVx⁵ from Play Store / F-Droid</li>
|
||||
<li>Login with URL: <span className="font-mono">http://calendar.cell/</span></li>
|
||||
<li>Login with URL: <span className="font-mono">http://{cellHost}/</span></li>
|
||||
<li>Select calendars & address books to sync</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -121,7 +123,7 @@ function Calendar() {
|
||||
<p className="font-medium text-gray-900 mb-1">Thunderbird</p>
|
||||
<ol className="list-decimal ml-4 space-y-0.5 text-xs text-gray-600">
|
||||
<li>Calendar → New Calendar → On the Network</li>
|
||||
<li>Format: CalDAV, Location: <span className="font-mono">http://calendar.cell/</span></li>
|
||||
<li>Format: CalDAV, Location: <span className="font-mono">http://{cellHost}/</span></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Mail, Users, Wifi, Copy, CheckCheck } from 'lucide-react';
|
||||
import { emailAPI } from '../services/api';
|
||||
import { useConfig } from '../contexts/ConfigContext';
|
||||
|
||||
const CELL_HOST = 'mail.cell';
|
||||
const CELL_IP = '172.20.0.23';
|
||||
|
||||
function CopyButton({ text }) {
|
||||
@@ -32,6 +32,8 @@ function InfoRow({ label, value }) {
|
||||
}
|
||||
|
||||
function Email() {
|
||||
const { domain = 'cell' } = useConfig();
|
||||
const cellHost = `mail.${domain}`;
|
||||
const [users, setUsers] = useState([]);
|
||||
const [status, setStatus] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -78,7 +80,7 @@ function Email() {
|
||||
<h3 className="text-lg font-medium text-gray-900">Incoming mail (IMAP)</h3>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||
<InfoRow label="Server" value={CELL_HOST} />
|
||||
<InfoRow label="Server" value={cellHost} />
|
||||
<InfoRow label="Port" value={String(status?.imap_port ?? 993)} />
|
||||
<InfoRow label="Security" value="SSL/TLS" />
|
||||
<InfoRow label="Direct IP" value={CELL_IP} />
|
||||
@@ -92,7 +94,7 @@ function Email() {
|
||||
<h3 className="text-lg font-medium text-gray-900">Outgoing mail (SMTP)</h3>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||
<InfoRow label="Server" value={CELL_HOST} />
|
||||
<InfoRow label="Server" value={cellHost} />
|
||||
<InfoRow label="Port" value={String(status?.smtp_port ?? 587)} />
|
||||
<InfoRow label="Security" value="STARTTLS" />
|
||||
<InfoRow label="Auth" value="Username + Password" />
|
||||
@@ -106,8 +108,8 @@ function Email() {
|
||||
<h3 className="text-lg font-medium text-gray-900">Webmail</h3>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||
<InfoRow label="URL" value="http://mail.cell" />
|
||||
<InfoRow label="Alt URL" value="http://webmail.cell" />
|
||||
<InfoRow label="URL" value={`http://mail.${domain}`} />
|
||||
<InfoRow label="Alt URL" value={`http://webmail.${domain}`} />
|
||||
<InfoRow label="Direct IP" value={`http://${CELL_IP}`} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-3">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FolderOpen, Users, HardDrive, Wifi, Copy, CheckCheck } from 'lucide-react';
|
||||
import { fileAPI } from '../services/api';
|
||||
import { useConfig } from '../contexts/ConfigContext';
|
||||
|
||||
const FILES_HOST = 'files.cell';
|
||||
const FILES_IP = '172.20.0.22';
|
||||
const WEBDAV_HOST = 'webdav.cell';
|
||||
const WEBDAV_IP = '172.20.0.24';
|
||||
|
||||
function CopyButton({ text }) {
|
||||
@@ -34,6 +33,9 @@ function InfoRow({ label, value }) {
|
||||
}
|
||||
|
||||
function Files() {
|
||||
const { domain = 'cell' } = useConfig();
|
||||
const filesHost = `files.${domain}`;
|
||||
const webdavHost = `webdav.${domain}`;
|
||||
const [users, setUsers] = useState([]);
|
||||
const [status, setStatus] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -80,7 +82,7 @@ function Files() {
|
||||
<h3 className="text-lg font-medium text-gray-900">Web file manager</h3>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||
<InfoRow label="URL" value={`http://${FILES_HOST}`} />
|
||||
<InfoRow label="URL" value={`http://${filesHost}`} />
|
||||
<InfoRow label="Direct IP" value={`http://${FILES_IP}`} />
|
||||
<InfoRow label="Port" value="80" />
|
||||
</div>
|
||||
@@ -96,7 +98,7 @@ function Files() {
|
||||
<h3 className="text-lg font-medium text-gray-900">WebDAV (mount as drive)</h3>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||
<InfoRow label="URL" value={`http://${WEBDAV_HOST}`} />
|
||||
<InfoRow label="URL" value={`http://${webdavHost}`} />
|
||||
<InfoRow label="Direct IP" value={`http://${WEBDAV_IP}`} />
|
||||
<InfoRow label="Port" value="80" />
|
||||
<InfoRow label="Auth" value="Basic (user / password)" />
|
||||
@@ -115,19 +117,19 @@ function Files() {
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 mb-1">macOS (Finder)</p>
|
||||
<p className="text-xs text-gray-600">Go → Connect to Server → <span className="font-mono">http://webdav.cell</span></p>
|
||||
<p className="text-xs text-gray-600">Go → Connect to Server → <span className="font-mono">http://{webdavHost}</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 mb-1">Windows</p>
|
||||
<p className="text-xs text-gray-600">Map Network Drive → <span className="font-mono">\\webdav.cell\DavWWWRoot</span> or use <span className="font-mono">http://webdav.cell</span> in "Connect to a Web Site"</p>
|
||||
<p className="text-xs text-gray-600">Map Network Drive → <span className="font-mono">\\{webdavHost}\DavWWWRoot</span> or use <span className="font-mono">http://{webdavHost}</span> in "Connect to a Web Site"</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 mb-1">iOS (Files app)</p>
|
||||
<p className="text-xs text-gray-600">Files → ... → Connect to Server → <span className="font-mono">http://webdav.cell</span></p>
|
||||
<p className="text-xs text-gray-600">Files → ... → Connect to Server → <span className="font-mono">http://{webdavHost}</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 mb-1">Android</p>
|
||||
<p className="text-xs text-gray-600">Use <strong>Solid Explorer</strong> or <strong>FX File Explorer</strong> → Add cloud → WebDAV</p>
|
||||
<p className="text-xs text-gray-600">Use <strong>Solid Explorer</strong> or <strong>FX File Explorer</strong> → Add cloud → WebDAV → <span className="font-mono">http://{webdavHost}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user