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:
+10
@@ -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:
|
||||
|
||||
+53
-2
@@ -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 () => {
|
||||
await cellAPI.applyPending();
|
||||
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 />} />
|
||||
|
||||
@@ -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 cellHost = `calendar.${domain}`;
|
||||
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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 filesHost = `files.${domain}`;
|
||||
const webdavHost = `webdav.${domain}`;
|
||||
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">
|
||||
|
||||
Reference in New Issue
Block a user