fix: propagate dynamic IPs/ports to service pages; add apply restart feedback

Service pages (Email, Calendar, Files) now read IPs and ports from the
config API instead of hardcoded 172.20.0.x constants:
- GET /api/config now includes service_ips (dns, vip_mail, vip_calendar,
  vip_files, vip_webdav) computed from ip_range via ip_utils
- Email.jsx: mailIp, dnsIp, imapPort, smtpPort, webmailPort from context
- Calendar.jsx: calendarIp, dnsIp, calendarPort from context
- Files.jsx: filesIp, webdavIp, webdavPort, filegatorPort from context

Apply button now shows restart progress:
- "Restarting containers — please wait…" spinner while polling /health
- "Containers restarted successfully" on success (clears after 4s)
- "Timed out" / error message if health doesn't come back in 45s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 12:41:10 -04:00
parent b46d8d9b8f
commit 10878543a9
5 changed files with 96 additions and 23 deletions
+10
View File
@@ -400,6 +400,16 @@ def get_config():
'ip_range': identity.get('ip_range', os.environ.get('CELL_IP_RANGE', '172.20.0.0/16')),
'wireguard_port': identity.get('wireguard_port', int(os.environ.get('WG_PORT', '51820'))),
}
# Expose computed per-service IPs so the frontend doesn't need to derive them
import ip_utils as _ip_utils_cfg
_ips = _ip_utils_cfg.get_service_ips(config['ip_range'])
config['service_ips'] = {
'dns': _ips['dns'],
'vip_mail': _ips['vip_mail'],
'vip_calendar': _ips['vip_calendar'],
'vip_files': _ips['vip_files'],
'vip_webdav': _ips['vip_webdav'],
}
config['service_configs'] = service_configs
return jsonify(config)
except Exception as e:
+52 -1
View File
@@ -164,9 +164,39 @@ function App() {
};
}, [checkHealth, checkPending]);
const [applyStatus, setApplyStatus] = useState(null); // null | 'restarting' | 'done' | 'timeout' | 'error'
const [applyError, setApplyError] = useState('');
const handleApply = useCallback(async () => {
setApplyError('');
try {
await cellAPI.applyPending();
} catch (err) {
setApplyStatus('error');
setApplyError(err?.response?.data?.error || 'Apply request failed');
setTimeout(() => setApplyStatus(null), 6000);
return;
}
setPending({ needs_restart: false, changes: [] });
setApplyStatus('restarting');
// Poll health until API responds again (max 45 s; it may briefly drop if cell-api restarts)
const deadline = Date.now() + 45000;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 2000));
try {
await healthAPI.check();
setIsOnline(true);
setApplyStatus('done');
setTimeout(() => setApplyStatus(null), 4000);
return;
} catch {
setIsOnline(false);
}
}
setApplyStatus('timeout');
setApplyError('Containers may still be starting — check docker logs if services are unavailable');
setTimeout(() => setApplyStatus(null), 8000);
}, []);
const handleCancel = useCallback(async () => {
@@ -231,10 +261,31 @@ function App() {
</div>
)}
{isOnline && pending.needs_restart && (
{isOnline && pending.needs_restart && !applyStatus && (
<PendingRestartBanner pending={pending} onApply={handleApply} onCancel={handleCancel} />
)}
{applyStatus === 'restarting' && (
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center gap-3">
<RefreshCw className="h-5 w-5 text-blue-500 animate-spin flex-shrink-0" />
<span className="text-sm font-medium text-blue-800">Restarting containers please wait</span>
</div>
)}
{applyStatus === 'done' && (
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3">
<span className="h-5 w-5 text-green-500 flex-shrink-0 text-lg leading-none"></span>
<span className="text-sm font-medium text-green-800">Containers restarted successfully</span>
</div>
)}
{(applyStatus === 'timeout' || applyStatus === 'error') && (
<div className="mb-6 bg-danger-50 border border-danger-200 rounded-lg p-4 flex items-center gap-3">
<AlertTriangle className="h-5 w-5 text-danger-500 flex-shrink-0" />
<span className="text-sm font-medium text-danger-800">{applyError}</span>
</div>
)}
<Routes>
<Route path="/" element={<Dashboard isOnline={isOnline} />} />
<Route path="/peers" element={<Peers />} />
+7 -4
View File
@@ -3,7 +3,6 @@ import { Calendar as CalendarIcon, Users, Wifi, Copy, CheckCheck } from 'lucide-
import { calendarAPI } from '../services/api';
import { useConfig } from '../contexts/ConfigContext';
const CELL_IP = '172.20.0.21';
function CopyButton({ text }) {
const [copied, setCopied] = useState(false);
@@ -32,8 +31,11 @@ function InfoRow({ label, value }) {
}
function Calendar() {
const { domain = 'cell' } = useConfig();
const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
const cellHost = `calendar.${domain}`;
const calendarIp = service_ips.vip_calendar || '172.20.0.21';
const dnsIp = service_ips.dns || '172.20.0.3';
const calendarPort = service_configs.calendar?.port ?? 5232;
const [users, setUsers] = useState([]);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
@@ -87,11 +89,12 @@ function Calendar() {
<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="Direct IP" value={calendarIp} />
<InfoRow label="Direct port" value={String(calendarPort)} />
<InfoRow label="Protocol" value="HTTP (CalDAV/CardDAV)" />
</div>
<p className="text-xs text-gray-400 mt-3">
Requires VPN connection. DNS server must be set to <span className="font-mono">172.20.0.3</span>.
Requires VPN connection. DNS server must be set to <span className="font-mono">{dnsIp}</span>.
</p>
</div>
+13 -7
View File
@@ -3,7 +3,6 @@ import { Mail, Users, Wifi, Copy, CheckCheck } from 'lucide-react';
import { emailAPI } from '../services/api';
import { useConfig } from '../contexts/ConfigContext';
const CELL_IP = '172.20.0.23';
function CopyButton({ text }) {
const [copied, setCopied] = useState(false);
@@ -32,8 +31,14 @@ function InfoRow({ label, value }) {
}
function Email() {
const { domain = 'cell' } = useConfig();
const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
const cellHost = `mail.${domain}`;
const emailCfg = service_configs.email || {};
const mailIp = service_ips.vip_mail || '172.20.0.23';
const dnsIp = service_ips.dns || '172.20.0.3';
const imapPort = emailCfg.imap_port ?? 993;
const smtpPort = emailCfg.smtp_port ?? 25;
const webmailPort = emailCfg.webmail_port ?? 8888;
const [users, setUsers] = useState([]);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
@@ -81,9 +86,9 @@ function Email() {
</div>
<div className="bg-gray-50 rounded-lg px-4 py-2">
<InfoRow label="Server" value={cellHost} />
<InfoRow label="Port" value={String(status?.imap_port ?? 993)} />
<InfoRow label="Port" value={String(imapPort)} />
<InfoRow label="Security" value="SSL/TLS" />
<InfoRow label="Direct IP" value={CELL_IP} />
<InfoRow label="Direct IP" value={mailIp} />
</div>
</div>
@@ -95,7 +100,7 @@ function Email() {
</div>
<div className="bg-gray-50 rounded-lg px-4 py-2">
<InfoRow label="Server" value={cellHost} />
<InfoRow label="Port" value={String(status?.smtp_port ?? 587)} />
<InfoRow label="Port" value={String(smtpPort)} />
<InfoRow label="Security" value="STARTTLS" />
<InfoRow label="Auth" value="Username + Password" />
</div>
@@ -110,10 +115,11 @@ function Email() {
<div className="bg-gray-50 rounded-lg px-4 py-2">
<InfoRow label="URL" value={`http://mail.${domain}`} />
<InfoRow label="Alt URL" value={`http://webmail.${domain}`} />
<InfoRow label="Direct IP" value={`http://${CELL_IP}`} />
<InfoRow label="Direct IP" value={`http://${mailIp}`} />
<InfoRow label="Direct port" value={String(webmailPort)} />
</div>
<p className="text-xs text-gray-400 mt-3">
Requires VPN + DNS set to <span className="font-mono">172.20.0.3</span>.
Requires VPN + DNS set to <span className="font-mono">{dnsIp}</span>.
</p>
</div>
+10 -7
View File
@@ -3,8 +3,6 @@ import { FolderOpen, Users, HardDrive, Wifi, Copy, CheckCheck } from 'lucide-rea
import { fileAPI } from '../services/api';
import { useConfig } from '../contexts/ConfigContext';
const FILES_IP = '172.20.0.22';
const WEBDAV_IP = '172.20.0.24';
function CopyButton({ text }) {
const [copied, setCopied] = useState(false);
@@ -33,9 +31,14 @@ function InfoRow({ label, value }) {
}
function Files() {
const { domain = 'cell' } = useConfig();
const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
const filesHost = `files.${domain}`;
const webdavHost = `webdav.${domain}`;
const filesIp = service_ips.vip_files || '172.20.0.22';
const webdavIp = service_ips.vip_webdav || '172.20.0.24';
const filesCfg = service_configs.files || {};
const webdavPort = filesCfg.port ?? 8080;
const filegatorPort = filesCfg.manager_port ?? 8082;
const [users, setUsers] = useState([]);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
@@ -83,8 +86,8 @@ function Files() {
</div>
<div className="bg-gray-50 rounded-lg px-4 py-2">
<InfoRow label="URL" value={`http://${filesHost}`} />
<InfoRow label="Direct IP" value={`http://${FILES_IP}`} />
<InfoRow label="Port" value="80" />
<InfoRow label="Direct IP" value={`http://${filesIp}`} />
<InfoRow label="Direct port" value={String(filegatorPort)} />
</div>
<p className="text-xs text-gray-400 mt-3">
Browser-based file manager. Requires VPN.
@@ -99,8 +102,8 @@ function Files() {
</div>
<div className="bg-gray-50 rounded-lg px-4 py-2">
<InfoRow label="URL" value={`http://${webdavHost}`} />
<InfoRow label="Direct IP" value={`http://${WEBDAV_IP}`} />
<InfoRow label="Port" value="80" />
<InfoRow label="Direct IP" value={`http://${webdavIp}`} />
<InfoRow label="Direct port" value={String(webdavPort)} />
<InfoRow label="Auth" value="Basic (user / password)" />
</div>
<p className="text-xs text-gray-400 mt-3">