diff --git a/backend/routes/serverConfigsRoutes.js b/backend/routes/serverConfigsRoutes.js index 0a173c4..900cb5b 100644 --- a/backend/routes/serverConfigsRoutes.js +++ b/backend/routes/serverConfigsRoutes.js @@ -14,6 +14,24 @@ const serverConfigsListRoutes = createJsonDataRoutes('server-configs.json', (ser if (!server.id || !server.name) { return `Server at index ${i} is missing required fields`; } + if (server.gateways !== undefined) { + if (!Array.isArray(server.gateways)) { + return `Server at index ${i} has invalid gateways (must be array)`; + } + if (server.gateways.length === 0) { + return `Server at index ${i} must have at least one gateway`; + } + const defaults = server.gateways.filter(g => g && g.primary); + if (defaults.length !== 1) { + return `Server at index ${i} must have exactly one primary gateway`; + } + for (let j = 0; j < server.gateways.length; j++) { + const gw = server.gateways[j] || {}; + if (!gw.name || String(gw.name).trim().length === 0) { + return `Server at index ${i} gateway at index ${j} is missing name`; + } + } + } return null; }); diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index 415b526..c0a7058 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -43,6 +43,39 @@ const countryToFlag = (code) => { return String.fromCodePoint(...codePoints); }; +const normalizeGateways = (gateways = [], fallbackName = '') => { + if (!Array.isArray(gateways)) return []; + const cleaned = gateways + .map((g, idx) => { + const name = String(g?.name || '').trim(); + const ip = String(g?.ip || '').trim(); + const comment = String(g?.comment || '').trim(); + const primary = Boolean(g?.primary); + const id = g?.id || `${Date.now()}-${idx}-${Math.random().toString(16).slice(2, 6)}`; + return { id, name, ip, comment, primary }; + }) + .filter(g => g.name || g.ip || g.comment); + if (cleaned.length === 0) { + return [{ id: `${Date.now()}-primary`, name: fallbackName || 'gateway', ip: '', comment: '', primary: true }]; + } + if (!cleaned.some(g => g.primary)) { + cleaned[0].primary = true; + } else { + // если несколько primary, оставляем только первый + let found = false; + cleaned.forEach(g => { + if (g.primary) { + if (!found) { + found = true; + } else { + g.primary = false; + } + } + }); + } + return cleaned; +}; + function FilterManager() { const [error, setError] = useState(''); const [success, setSuccess] = useState(''); @@ -57,7 +90,15 @@ function FilterManager() { const [servers, setServers] = useState([]); const [selectedServer, setSelectedServer] = useState(null); const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false); - const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true }); + const makeGateway = (over = {}) => ({ + id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, + name: '', + ip: '', + comment: '', + primary: false, + ...over, + }); + const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] }); // Состояния для управления фильтрами сервера const [serverFilters, setServerFilters] = useState([]); @@ -71,6 +112,63 @@ function FilterManager() { // Новое состояние для редактирования сервера const [editServerModalOpen, setEditServerModalOpen] = useState(false); const [editingServer, setEditingServer] = useState(null); + + const updateNewGateway = (idx, field, value) => { + setNewServer((prev) => { + const list = prev.gateways.map((g, i) => { + if (i !== idx) return g; + const next = { ...g, [field]: value }; + return next; + }); + if (field === 'primary' && value) { + list.forEach((g, i) => { g.primary = i === idx; }); + } + return { ...prev, gateways: list }; + }); + }; + const addNewGateway = () => { + setNewServer((prev) => ({ ...prev, gateways: [...prev.gateways, makeGateway({ primary: prev.gateways.length === 0 })] })); + }; + const removeNewGateway = (idx) => { + setNewServer((prev) => { + if (prev.gateways.length === 1) return prev; + const list = prev.gateways.filter((_, i) => i !== idx); + if (!list.some(g => g.primary) && list.length > 0) list[0].primary = true; + return { ...prev, gateways: list }; + }); + }; + + const updateEditGateway = (idx, field, value) => { + setEditingServer((prev) => { + if (!prev) return prev; + const list = (prev.gateways || []).map((g, i) => { + if (i !== idx) return g; + const next = { ...g, [field]: value }; + return next; + }); + if (field === 'primary' && value) { + list.forEach((g, i) => { g.primary = i === idx; }); + } + return { ...prev, gateways: list }; + }); + }; + const addEditGateway = () => { + setEditingServer((prev) => { + if (!prev) return prev; + const list = [...(prev.gateways || []), makeGateway({ primary: !(prev.gateways || []).length })]; + return { ...prev, gateways: list }; + }); + }; + const removeEditGateway = (idx) => { + setEditingServer((prev) => { + if (!prev) return prev; + const list = (prev.gateways || []); + if (list.length === 1) return prev; + const next = list.filter((_, i) => i !== idx); + if (!next.some(g => g.primary) && next.length > 0) next[0].primary = true; + return { ...prev, gateways: next }; + }); + }; const [editServerError, setEditServerError] = useState(''); // Состояние поиска/сортировки @@ -181,7 +279,8 @@ function FilterManager() { const fetchServers = async () => { try { const response = await api.get(`/server-configs`); - setServers(response.data); + const payload = Array.isArray(response.data) ? response.data : []; + setServers(payload.map(s => ({ ...s, gateways: normalizeGateways(s.gateways, s.name) }))); } catch (error) { console.error('Error fetching servers:', error); // Если файл не существует, создаем пустой массив @@ -200,6 +299,7 @@ function FilterManager() { const serverToAdd = { id: Date.now().toString(), ...newServer, + gateways: normalizeGateways(newServer.gateways, newServer.name), createdAt: new Date().toISOString() }; @@ -207,7 +307,7 @@ function FilterManager() { await api.post(`/server-configs`, { servers: updatedServers }); setServers(updatedServers); - setNewServer({ name: '', description: '', enabled: true }); + setNewServer({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] }); setServerConfigModalOpen(false); setSuccess('Сервер успешно добавлен!'); setTimeout(() => setSuccess(''), 3000); @@ -522,7 +622,7 @@ function FilterManager() { // Открыть модалку редактирования const handleEditServer = (server) => { - setEditingServer({ ...server }); + setEditingServer({ ...server, gateways: normalizeGateways(server.gateways, server.name) }); setEditServerError(''); setEditServerModalOpen(true); }; @@ -534,14 +634,15 @@ function FilterManager() { return; } try { + const edited = { ...editingServer, gateways: normalizeGateways(editingServer.gateways, editingServer.name) }; const updatedServers = servers.map(s => - s.id === editingServer.id ? { ...s, name: editingServer.name, description: editingServer.description } : s + s.id === editingServer.id ? { ...s, name: edited.name, description: edited.description, gateways: edited.gateways } : s ); await api.post(`/server-configs`, { servers: updatedServers }); setServers(updatedServers); // Если редактируемый сервер выбран — обновить его if (selectedServer?.id === editingServer.id) { - setSelectedServer({ ...selectedServer, name: editingServer.name, description: editingServer.description }); + setSelectedServer({ ...selectedServer, name: edited.name, description: edited.description, gateways: edited.gateways }); } setEditServerModalOpen(false); setSuccess('Сервер успешно обновлён!'); @@ -1530,7 +1631,7 @@ function FilterManager() { show={serverConfigModalOpen} onClose={() => { setServerConfigModalOpen(false); - setNewServer({ name: '', description: '', enabled: true }); + setNewServer({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] }); }} onSubmit={handleAddServer} title="Добавить новый сервер" @@ -1565,6 +1666,79 @@ function FilterManager() { helpText="Краткое описание сервера (необязательно)" /> +