Files
pic/webui/src/contexts/DraftConfigContext.jsx
T
roof 768571f2b7 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>
2026-04-24 04:45:47 -04:00

38 lines
1.0 KiB
React

import { createContext, useContext, useRef, useCallback } from 'react';
const DraftConfigContext = createContext(null);
export function DraftConfigProvider({ children }) {
const flushersRef = useRef({}); // key → async flush fn
const registerFlusher = useCallback((key, fn) => {
flushersRef.current[key] = fn;
return () => { delete flushersRef.current[key]; }; // cleanup
}, []);
const hasDirtyRef = useRef({}); // key → boolean
const setDirty = useCallback((key, isDirty) => {
hasDirtyRef.current[key] = isDirty;
}, []);
const hasDirty = useCallback(() => {
return Object.values(hasDirtyRef.current).some(Boolean);
}, []);
const flushAll = useCallback(async () => {
const flushers = Object.values(flushersRef.current);
await Promise.all(flushers.map(fn => fn()));
}, []);
return (
<DraftConfigContext.Provider value={{ registerFlusher, setDirty, hasDirty, flushAll }}>
{children}
</DraftConfigContext.Provider>
);
}
export function useDraftConfig() {
return useContext(DraftConfigContext);
}