feat: port conflict validation, autosave on Apply, extended integration tests
Port conflict validation: - api/port_registry.py: detect_conflicts() checks all service sections for shared port values - api/app.py: returns HTTP 409 on port conflict after existing range validation - webui/src/pages/Settings.jsx: JS-side detectPortConflicts() with useMemo shows inline conflict errors and blocks Save before the request is made; catch blocks surface server error messages (including 409) instead of generic fallbacks Config autosave on Apply: - webui/src/contexts/DraftConfigContext.jsx: new context; Settings registers flush callbacks per section; App calls flushAll() before applyPending() when any section is dirty - webui/src/App.jsx: wraps tree with DraftConfigProvider, handleApply shows 'saving' banner state and awaits flushAll() - webui/src/pages/Settings.jsx: registers identity + per-service flushers; propagates dirty state into context via setDirty; uses refs to avoid stale closures Extended integration test coverage (114 new tests): - tests/integration/test_config_api.py: GET/PUT config, export, import, backup lifecycle - tests/integration/test_network_services.py: DNS records + DHCP reservations CRUD - tests/integration/test_containers.py: list, restart, logs, stats; recovery polling - tests/integration/test_negative_scenarios.py: error-path coverage for all endpoints - tests/test_port_conflicts.py: 20 unit tests for port_registry.detect_conflicts() Pre-commit hook updated to skip tests/integration/ (live-stack tests require a running stack and must be run explicitly via `make test-integration`). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+22
-2
@@ -20,6 +20,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { healthAPI, cellAPI } from './services/api';
|
||||
import { ConfigProvider } from './contexts/ConfigContext';
|
||||
import { DraftConfigProvider, useDraftConfig } from './contexts/DraftConfigContext';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Peers from './pages/Peers';
|
||||
@@ -164,11 +165,21 @@ function App() {
|
||||
};
|
||||
}, [checkHealth, checkPending]);
|
||||
|
||||
const [applyStatus, setApplyStatus] = useState(null); // null | 'restarting' | 'done' | 'timeout' | 'error'
|
||||
const [applyStatus, setApplyStatus] = useState(null); // null | 'saving' | 'restarting' | 'done' | 'timeout' | 'error'
|
||||
const [applyError, setApplyError] = useState('');
|
||||
|
||||
const { flushAll, hasDirty } = useDraftConfig();
|
||||
|
||||
const handleApply = useCallback(async () => {
|
||||
setApplyError('');
|
||||
if (hasDirty()) {
|
||||
setApplyStatus('saving');
|
||||
try {
|
||||
await flushAll();
|
||||
} catch {
|
||||
// flush errors are shown via Settings toasts; continue with apply
|
||||
}
|
||||
}
|
||||
try {
|
||||
await cellAPI.applyPending();
|
||||
} catch (err) {
|
||||
@@ -197,7 +208,7 @@ function App() {
|
||||
setApplyStatus('timeout');
|
||||
setApplyError('Containers may still be starting — check docker logs if services are unavailable');
|
||||
setTimeout(() => setApplyStatus(null), 8000);
|
||||
}, []);
|
||||
}, [flushAll, hasDirty]);
|
||||
|
||||
const handleCancel = useCallback(async () => {
|
||||
await cellAPI.cancelPending();
|
||||
@@ -232,6 +243,7 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<DraftConfigProvider>
|
||||
<Router>
|
||||
<ConfigProvider>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
@@ -265,6 +277,13 @@ function App() {
|
||||
<PendingRestartBanner pending={pending} onApply={handleApply} onCancel={handleCancel} />
|
||||
)}
|
||||
|
||||
{applyStatus === 'saving' && (
|
||||
<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">Saving settings…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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" />
|
||||
@@ -307,6 +326,7 @@ function App() {
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</Router>
|
||||
</DraftConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user