diff --git a/api/app.py b/api/app.py
index b4d502c..c23eeb7 100644
--- a/api/app.py
+++ b/api/app.py
@@ -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:
diff --git a/webui/src/App.jsx b/webui/src/App.jsx
index 1d3299c..b81f7eb 100644
--- a/webui/src/App.jsx
+++ b/webui/src/App.jsx
@@ -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() {
)}
- {isOnline && pending.needs_restart && (
+ {isOnline && pending.needs_restart && !applyStatus && (
- Requires VPN connection. DNS server must be set to 172.20.0.3. + Requires VPN connection. DNS server must be set to {dnsIp}.
diff --git a/webui/src/pages/Email.jsx b/webui/src/pages/Email.jsx index 41852d3..97b978b 100644 --- a/webui/src/pages/Email.jsx +++ b/webui/src/pages/Email.jsx @@ -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() {- Requires VPN + DNS set to 172.20.0.3. + Requires VPN + DNS set to {dnsIp}.
diff --git a/webui/src/pages/Files.jsx b/webui/src/pages/Files.jsx index 4fdd0f3..aaeea44 100644 --- a/webui/src/pages/Files.jsx +++ b/webui/src/pages/Files.jsx @@ -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() {Browser-based file manager. Requires VPN. @@ -99,8 +102,8 @@ function Files() {