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
+53 -2
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 () => {
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 />} />