diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 30b3f24..4f029d7 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -53,6 +53,7 @@ import FirewallPage from './FirewallPage.jsx';
import SettingsPage from './SettingsPage.jsx';
import ResourceStatsPage from './ResourceStatsPage.jsx';
import RouteOptimizerPage from './RouteOptimizerPage.jsx';
+import OspfToolsPage from './OspfToolsPage.jsx';
import './App.css';
import { NotifyProvider } from './components/NotifyProvider.jsx';
import ToastContainer from './components/ToastContainer.jsx';
@@ -75,7 +76,7 @@ function LanguageProvider({ children }) {
dashboard: 'Панель', trafficTraffic: 'Расход трафика', resourceStats: 'Статистика ресурсов', networkMap: 'Карта сети', uptimeMonitor: 'Uptime Monitor', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
routeOptimizer: 'Оптимизация маршрутов ИИ',
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
- easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', firewall: 'Firewall',
+ easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', firewall: 'Firewall', ospfTools: 'Инструменты OSPF',
light: 'Светлая', dark: 'Тёмная',
layoutSidebar: 'Сайдбар', layoutHorizontal: 'Верхнее меню'
},
@@ -84,7 +85,7 @@ function LanguageProvider({ children }) {
dashboard: 'Dashboard', trafficTraffic: 'Traffic Usage', resourceStats: 'Resource Stats', networkMap: 'Network Map', uptimeMonitor: 'Uptime Monitor', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
routeOptimizer: 'AI Route Optimizer',
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
- easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', firewall: 'Firewall',
+ easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', firewall: 'Firewall', ospfTools: 'OSPF Tools',
light: 'Light', dark: 'Dark',
layoutSidebar: 'Sidebar', layoutHorizontal: 'Top menu'
}
@@ -289,6 +290,7 @@ function MainLayout() {
items: [
{ id: 'auto-urls', title: t('autoUrls'), path: '/auto-urls', icon: IconDownload },
{ id: 'mikrotik-backups', title: t('mikrotikBackups'), path: '/mikrotik-backups', icon: IconDatabase },
+ { id: 'ospf-tools', title: t('ospfTools'), path: '/ospf-tools', icon: IconRoute2 },
{ id: 'mikrotik-tools', title: 'MikroTik Инструменты', path: '/mikrotik-tools', icon: IconNetwork },
{ id: 'firewall', title: t('firewall'), path: '/firewall', icon: IconShield },
{ id: 'interface-speed', title: 'Скорость интерфейсов', path: '/interface-speed', icon: IconNetwork },
@@ -466,6 +468,7 @@ function MainLayout() {
} />
} />
} />
+ } />
} />
} />
} />
@@ -643,6 +646,7 @@ function MainLayout() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx
index dd396df..5dc500b 100644
--- a/frontend/src/NetworkConfigManager.jsx
+++ b/frontend/src/NetworkConfigManager.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useMemo, useRef } from 'react';
+import { useState, useEffect, useMemo } from 'react';
import api from './lib/api.js';
import { useNotify } from './components/NotifyProvider.jsx';
import FormModal from './components/FormModal.jsx';
@@ -125,78 +125,6 @@ const GATEWAY_TEMPLATES = [
},
];
-const OSPF_COST_STEP = 10;
-
-function isUnknownOspfInterfaceName(name) {
- const value = String(name || '').trim();
- if (!value) return true;
- if (value.startsWith('*')) return true; // служебные ссылки RouterOS вида *50
- if (value.toLowerCase() === 'unknown') return true;
- return false;
-}
-
-function buildOspfTemplatesFromRouterResults(routerResults = [], servers = []) {
- const serverById = new Map((servers || []).map((s) => [String(s.id || s.ip || s.dns || ''), s]));
- const byComposite = new Map();
-
- (routerResults || []).forEach((result) => {
- const serverId = String(result?.serverId || '').trim();
- if (!serverId) return;
- const server = serverById.get(serverId) || {
- id: serverId,
- ip: String(result?.serverLabel || serverId),
- dns: String(result?.serverLabel || ''),
- };
- const identity = getOspfRouterIdentity(server);
- const templates = Array.isArray(result?.templates) ? result.templates : [];
-
- templates.forEach((tpl) => {
- const interfaceName = String(tpl?.interfaceName || '').trim();
- if (isUnknownOspfInterfaceName(interfaceName)) return;
- const area = String(tpl?.area || '').trim() || 'backbone';
- const numericCost = Number(tpl?.cost);
- const cost = Number.isFinite(numericCost) && numericCost >= 0 ? numericCost : OSPF_COST_STEP;
-
- const compositeKey = `${identity.key}::${area.toLowerCase()}::${interfaceName.toUpperCase()}`;
- if (byComposite.has(compositeKey)) return;
-
- byComposite.set(compositeKey, {
- key: compositeKey,
- rosId: tpl?.id || null,
- serverId,
- routerKey: identity.key,
- routerLabel: identity.label,
- interfaceName,
- area,
- cost,
- active: !Boolean(tpl?.disabled),
- });
- });
- });
-
- return Array.from(byComposite.values());
-}
-
-function moveInArray(arr, fromIndex, toIndex) {
- if (fromIndex < 0 || toIndex < 0 || fromIndex >= arr.length || toIndex >= arr.length) return arr;
- const copy = [...arr];
- const [moved] = copy.splice(fromIndex, 1);
- copy.splice(toIndex, 0, moved);
- return copy;
-}
-
-function getOspfRouterIdentity(server) {
- if (!server || typeof server !== 'object') {
- return { key: 'unknown', label: 'Unknown router' };
- }
- const host = String(server.mikrotikHost || server.ip || server.dns || server.id || '').trim();
- const port = String(server.mikrotikPort || 80).trim();
- const user = String(server.mikrotikUser || 'admin').trim();
- const key = `${host}::${port}::${user}`;
- const label = host || String(server.dns || server.ip || server.id || 'Unknown router');
- return { key, label };
-}
-
// Пустые объекты
const getEmptyGateway = () => ({
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
@@ -235,7 +163,6 @@ const getDefaultConfig = () => ({
gateways: [],
tunnelInterfaces: [],
ipPools: [],
- ospfInterfaceTemplates: [],
});
function NetworkConfigManager() {
@@ -295,10 +222,6 @@ function NetworkConfigManager() {
const [templateMtu, setTemplateMtu] = useState(''); // MTU для шаблона
const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания
const [templateServerPairs, setTemplateServerPairs] = useState([]); // Массив пар серверов для множественного создания
- const [ospfDraggingItem, setOspfDraggingItem] = useState(null);
- const [ospfLoading, setOspfLoading] = useState(false);
- const [ospfSaving, setOspfSaving] = useState(false);
- const ospfAutoLoadedRef = useRef(false);
// === IPSec Passwords Management ===
const [ipsecPasswords, setIpsecPasswords] = useState([]);
@@ -362,7 +285,6 @@ function NetworkConfigManager() {
gateways: Array.isArray(migratedGateways) ? migratedGateways : [],
tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [],
ipPools: Array.isArray(data.ipPools) ? data.ipPools : [],
- ospfInterfaceTemplates: Array.isArray(data.ospfInterfaceTemplates) ? data.ospfInterfaceTemplates : [],
});
} catch (error) {
if (error.response?.status !== 404) {
@@ -559,195 +481,6 @@ function NetworkConfigManager() {
return result;
}, [config.ipPools, serverFilter, searchTerm, servers]);
- const ospfTemplatesByRouter = useMemo(() => {
- const grouped = {};
- (config.ospfInterfaceTemplates || []).forEach((item) => {
- const sid = String(item?.serverId || '');
- if (!sid) return;
- const server = getServerInfo(sid);
- const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
- if (!grouped[identity.key]) {
- grouped[identity.key] = {
- routerKey: identity.key,
- routerLabel: identity.label,
- serverIds: new Set(),
- representativeServerId: sid,
- items: [],
- };
- }
- grouped[identity.key].serverIds.add(sid);
- grouped[identity.key].items.push(item);
- });
-
- const groups = Object.values(grouped).map((group) => {
- const areasMap = {};
- group.items.forEach((item) => {
- const area = String(item?.area || '').trim() || 'backbone';
- if (!areasMap[area]) areasMap[area] = [];
- areasMap[area].push(item);
- });
-
- const areas = Object.entries(areasMap)
- .map(([area, items]) => ({
- area,
- items: [...items].sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0)),
- }))
- .sort((a, b) => a.area.localeCompare(b.area));
-
- return {
- ...group,
- serverIds: Array.from(group.serverIds),
- areas,
- };
- });
-
- if (serverFilter) {
- const filterId = String(serverFilter);
- return groups.filter((group) => group.serverIds.includes(filterId));
- }
-
- return groups;
- }, [config.ospfInterfaceTemplates, serverFilter, servers]);
-
- const handleDropOspfTemplate = (routerKey, area, targetKey) => {
- if (!ospfDraggingItem || String(ospfDraggingItem.routerKey) !== String(routerKey)) return;
- if (String(ospfDraggingItem.area || '') !== String(area || '')) return;
- const draggedKey = String(ospfDraggingItem.key || '');
- const destinationKey = String(targetKey || '');
- if (!draggedKey || !destinationKey || draggedKey === destinationKey) {
- setOspfDraggingItem(null);
- return;
- }
-
- setConfig((prev) => {
- const current = (prev.ospfInterfaceTemplates || [])
- .filter((item) => {
- const sid = String(item.serverId || '');
- const server = getServerInfo(sid);
- const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
- const itemArea = String(item?.area || '').trim() || 'backbone';
- return identity.key === String(routerKey) && itemArea === String(area || 'backbone');
- })
- .sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0));
- const fromIndex = current.findIndex((item) => String(item.key || '') === draggedKey);
- const toIndex = current.findIndex((item) => String(item.key || '') === destinationKey);
- if (fromIndex < 0 || toIndex < 0) return prev;
-
- const reordered = moveInArray(current, fromIndex, toIndex).map((item, idx) => ({
- ...item,
- cost: (idx + 1) * OSPF_COST_STEP,
- }));
-
- const otherServers = (prev.ospfInterfaceTemplates || []).filter((item) => {
- const sid = String(item.serverId || '');
- const server = getServerInfo(sid);
- const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
- const itemArea = String(item?.area || '').trim() || 'backbone';
- return identity.key !== String(routerKey) || itemArea !== String(area || 'backbone');
- });
-
- return {
- ...prev,
- ospfInterfaceTemplates: [...otherServers, ...reordered],
- };
- });
-
- setOspfDraggingItem(null);
- };
-
- const handleLoadOspfFromMikrotik = async ({ silent = false } = {}) => {
- setOspfLoading(true);
- try {
- const response = await api.get('/mikrotik/ospf-interface-templates');
- const results = Array.isArray(response.data?.results) ? response.data.results : [];
- const errors = Array.isArray(response.data?.errors) ? response.data.errors : [];
- const loadedTemplates = buildOspfTemplatesFromRouterResults(results, servers);
-
- setConfig((prev) => ({
- ...prev,
- ospfInterfaceTemplates: loadedTemplates,
- }));
-
- if (!silent && results.length > 0) {
- notify.success(`OSPF загружен с HOME роутеров: ${results.length}`);
- } else if (!silent) {
- notify.warning('Не удалось получить OSPF шаблоны ни с одного HOME роутера');
- }
- if (!silent && errors.length > 0) {
- notify.warning(`Ошибки чтения OSPF: ${errors.length} роутер(ов)`);
- }
- } catch (error) {
- console.error('Error loading OSPF templates:', error);
- if (!silent) {
- notify.error('Не удалось загрузить OSPF Interface Template с MikroTik');
- }
- } finally {
- setOspfLoading(false);
- }
- };
-
- const handleSaveOspfToMikrotik = async () => {
- if (ospfTemplatesByRouter.length === 0) {
- notify.warning('Нет OSPF шаблонов для сохранения');
- return;
- }
-
- setOspfSaving(true);
- try {
- const payload = {
- servers: ospfTemplatesByRouter.map((group) => {
- const dedup = new Map();
- (group.areas || []).forEach((areaGroup) => {
- (areaGroup.items || []).forEach((item) => {
- const interfaceName = String(item.interfaceName || '').trim();
- const area = String(item.area || '').trim() || 'backbone';
- if (!interfaceName) return;
- const key = `${area.toUpperCase()}::${interfaceName.toUpperCase()}`;
- if (!dedup.has(key)) {
- dedup.set(key, {
- rosId: item.rosId || null,
- interfaceName,
- area,
- cost: item.cost,
- });
- }
- });
- });
- return {
- serverId: group.representativeServerId,
- templates: Array.from(dedup.values()),
- };
- }),
- };
-
- const response = await api.post('/mikrotik/ospf-interface-templates/apply', payload);
- const summary = response.data?.summary || {};
- const errors = Array.isArray(response.data?.errors) ? response.data.errors : [];
- notify.success(
- `OSPF сохранен: обновлено ${summary.updated || 0}, пропущено ${summary.skipped || 0}, не найдено ${summary.missing || 0}`
- );
- if (Number(summary.invalid || 0) > 0) {
- notify.warning(`Некорректных записей OSPF: ${summary.invalid}`);
- }
- if (errors.length > 0) {
- notify.warning(`Ошибки применения OSPF: ${errors.length} роутер(ов)`);
- }
- } catch (error) {
- console.error('Error applying OSPF templates:', error);
- notify.error('Не удалось сохранить OSPF Interface Template в MikroTik');
- } finally {
- setOspfSaving(false);
- }
- };
-
- useEffect(() => {
- if (activeTab !== 'interfaces') return;
- if (!Array.isArray(servers) || servers.length === 0) return;
- if (ospfAutoLoadedRef.current) return;
- ospfAutoLoadedRef.current = true;
- handleLoadOspfFromMikrotik({ silent: true });
- }, [activeTab, servers]);
-
// === Группировка по серверам ===
const gatewaysByServer = useMemo(() => {
const grouped = {};
@@ -3933,111 +3666,13 @@ function NetworkConfigManager() {
{activeTab === 'interfaces' && (
<>
-
-
-
OSPF Interface Template (HOME)
-
- Только для HOME роутеров. Источник данных — MikroTik, группировка по Area. Чем меньше cost, тем выше приоритет.
-
+
+
+ Настройки OSPF вынесены в отдельный раздел проекта.
-
-
- {config.ospfInterfaceTemplates?.length || 0}
-
-
-
-
-
-
- {ospfTemplatesByRouter.length === 0 ? (
-
- OSPF Interface Template не найдены на HOME роутерах.
-
- ) : (
-
- {ospfTemplatesByRouter.map((group) => {
- const areas = group.areas || [];
- return (
-
-
-
-
- {areas.length === 0 ? (
-
- Нет доступных интерфейсов для OSPF шаблонов.
-
- ) : (
- areas.map((areaGroup) => (
-
-
- Area: {areaGroup.area}
-
-
- {areaGroup.items.map((template) => (
-
- setOspfDraggingItem({
- routerKey: group.routerKey,
- area: areaGroup.area,
- key: template.key,
- })
- }
- onDragOver={(e) => e.preventDefault()}
- onDrop={() =>
- handleDropOspfTemplate(group.routerKey, areaGroup.area, template.key)
- }
- onDragEnd={() => setOspfDraggingItem(null)}
- style={{ cursor: 'grab' }}
- >
-
- ::
- {template.interfaceName}
-
- {template.active ? 'Активен' : 'Неактивен'}
-
-
-
- cost {template.cost}
-
-
- ))}
-
-
- ))
- )}
-
-
-
- );
- })}
-
- )}
+
+ Открыть Инструменты OSPF
+
diff --git a/frontend/src/OspfToolsPage.jsx b/frontend/src/OspfToolsPage.jsx
new file mode 100644
index 0000000..e53f517
--- /dev/null
+++ b/frontend/src/OspfToolsPage.jsx
@@ -0,0 +1,307 @@
+import { useEffect, useMemo, useState } from 'react';
+import { IconDatabase, IconRefresh, IconRoute2 } from '@tabler/icons-react';
+import PageHeader from './components/PageHeader.jsx';
+import api from './lib/api.js';
+import { useNotify } from './components/NotifyProvider.jsx';
+
+const OSPF_COST_STEP = 10;
+
+function moveInArray(arr, fromIndex, toIndex) {
+ if (fromIndex < 0 || toIndex < 0 || fromIndex >= arr.length || toIndex >= arr.length) return arr;
+ const copy = [...arr];
+ const [moved] = copy.splice(fromIndex, 1);
+ copy.splice(toIndex, 0, moved);
+ return copy;
+}
+
+function isUnknownOspfInterfaceName(name) {
+ const value = String(name || '').trim();
+ if (!value) return true;
+ if (value.startsWith('*')) return true;
+ if (value.toLowerCase() === 'unknown') return true;
+ return false;
+}
+
+function getOspfRouterIdentity(server) {
+ if (!server || typeof server !== 'object') {
+ return { key: 'unknown', label: 'Unknown router' };
+ }
+ const host = String(server.mikrotikHost || server.ip || server.dns || server.id || '').trim();
+ const port = String(server.mikrotikPort || 80).trim();
+ const user = String(server.mikrotikUser || 'admin').trim();
+ const key = `${host}::${port}::${user}`;
+ const label = host || String(server.dns || server.ip || server.id || 'Unknown router');
+ return { key, label };
+}
+
+function buildOspfTemplatesFromRouterResults(routerResults = [], servers = []) {
+ const serverById = new Map((servers || []).map((s) => [String(s.id || s.ip || s.dns || ''), s]));
+ const byComposite = new Map();
+
+ (routerResults || []).forEach((result) => {
+ const serverId = String(result?.serverId || '').trim();
+ if (!serverId) return;
+ const server = serverById.get(serverId) || {
+ id: serverId,
+ ip: String(result?.serverLabel || serverId),
+ dns: String(result?.serverLabel || ''),
+ };
+ const identity = getOspfRouterIdentity(server);
+ const templates = Array.isArray(result?.templates) ? result.templates : [];
+
+ templates.forEach((tpl) => {
+ const interfaceName = String(tpl?.interfaceName || '').trim();
+ if (isUnknownOspfInterfaceName(interfaceName)) return;
+ const area = String(tpl?.area || '').trim() || 'backbone';
+ const numericCost = Number(tpl?.cost);
+ const cost = Number.isFinite(numericCost) && numericCost >= 0 ? numericCost : OSPF_COST_STEP;
+
+ const compositeKey = `${identity.key}::${area.toLowerCase()}::${interfaceName.toUpperCase()}`;
+ if (byComposite.has(compositeKey)) return;
+
+ byComposite.set(compositeKey, {
+ key: compositeKey,
+ rosId: tpl?.id || null,
+ serverId,
+ routerKey: identity.key,
+ routerLabel: identity.label,
+ interfaceName,
+ area,
+ cost,
+ active: !Boolean(tpl?.disabled),
+ });
+ });
+ });
+
+ return Array.from(byComposite.values());
+}
+
+export default function OspfToolsPage() {
+ const notify = useNotify();
+ const [servers, setServers] = useState([]);
+ const [templates, setTemplates] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [saving, setSaving] = useState(false);
+ const [dragging, setDragging] = useState(null);
+
+ const groupedByRouter = useMemo(() => {
+ const grouped = {};
+ (templates || []).forEach((item) => {
+ const sid = String(item?.serverId || '');
+ const server = servers.find((s) => String(s.id || s.ip || s.dns || '') === sid);
+ const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
+ if (!grouped[identity.key]) {
+ grouped[identity.key] = {
+ routerKey: identity.key,
+ routerLabel: identity.label,
+ representativeServerId: sid,
+ areas: {},
+ };
+ }
+ if (!grouped[identity.key].areas[item.area]) grouped[identity.key].areas[item.area] = [];
+ grouped[identity.key].areas[item.area].push(item);
+ });
+
+ return Object.values(grouped).map((group) => ({
+ ...group,
+ areas: Object.entries(group.areas)
+ .map(([area, areaItems]) => ({
+ area,
+ items: [...areaItems].sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0)),
+ }))
+ .sort((a, b) => a.area.localeCompare(b.area)),
+ }));
+ }, [templates, servers]);
+
+ const loadServers = async () => {
+ try {
+ const response = await api.get('/servers');
+ setServers(Array.isArray(response.data) ? response.data : []);
+ } catch (error) {
+ console.error('Failed to load servers:', error);
+ setServers([]);
+ }
+ };
+
+ const loadOspf = async ({ silent = false } = {}) => {
+ setLoading(true);
+ try {
+ const response = await api.get('/mikrotik/ospf-interface-templates');
+ const results = Array.isArray(response.data?.results) ? response.data.results : [];
+ const errors = Array.isArray(response.data?.errors) ? response.data.errors : [];
+ setTemplates(buildOspfTemplatesFromRouterResults(results, servers));
+
+ if (!silent) {
+ if (results.length > 0) notify.success(`OSPF загружен: ${results.length} роутер(ов)`);
+ else notify.warning('OSPF Interface Template не найдены');
+ if (errors.length > 0) notify.warning(`Ошибки чтения OSPF: ${errors.length}`);
+ }
+ } catch (error) {
+ console.error('Failed to load OSPF:', error);
+ if (!silent) notify.error('Не удалось загрузить OSPF с MikroTik');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadServers();
+ }, []);
+
+ useEffect(() => {
+ if (servers.length === 0) return;
+ loadOspf({ silent: true });
+ }, [servers.length]);
+
+ const onDrop = (routerKey, area, targetKey) => {
+ if (!dragging || dragging.routerKey !== routerKey || dragging.area !== area) return;
+ if (dragging.key === targetKey) {
+ setDragging(null);
+ return;
+ }
+
+ setTemplates((prev) => {
+ const current = prev
+ .filter((item) => item.routerKey === routerKey && String(item.area) === String(area))
+ .sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0));
+ const fromIndex = current.findIndex((item) => item.key === dragging.key);
+ const toIndex = current.findIndex((item) => item.key === targetKey);
+ if (fromIndex < 0 || toIndex < 0) return prev;
+
+ const reordered = moveInArray(current, fromIndex, toIndex).map((item, idx) => ({
+ ...item,
+ cost: (idx + 1) * OSPF_COST_STEP,
+ }));
+
+ const other = prev.filter(
+ (item) => !(item.routerKey === routerKey && String(item.area) === String(area))
+ );
+ return [...other, ...reordered];
+ });
+ setDragging(null);
+ };
+
+ const saveOspf = async () => {
+ if (groupedByRouter.length === 0) {
+ notify.warning('Нет OSPF данных для сохранения');
+ return;
+ }
+ setSaving(true);
+ try {
+ const payload = {
+ servers: groupedByRouter.map((group) => ({
+ serverId: group.representativeServerId,
+ templates: group.areas.flatMap((areaGroup) =>
+ areaGroup.items.map((item) => ({
+ rosId: item.rosId || null,
+ interfaceName: item.interfaceName,
+ area: item.area,
+ cost: item.cost,
+ }))
+ ),
+ })),
+ };
+
+ const response = await api.post('/mikrotik/ospf-interface-templates/apply', payload);
+ const summary = response.data?.summary || {};
+ notify.success(`OSPF сохранен: обновлено ${summary.updated || 0}, пропущено ${summary.skipped || 0}`);
+ } catch (error) {
+ console.error('Failed to save OSPF:', error);
+ notify.error('Не удалось сохранить OSPF в MikroTik');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
}
+ actions={(
+
+
+
+
+ )}
+ />
+
+
+
+
+ Данные OSPF берутся напрямую с MikroTik. Группировка: роутер → Area → интерфейсы.
+ Чем меньше cost, тем выше приоритет.
+
+
+
+ {groupedByRouter.length === 0 ? (
+
OSPF Interface Template не найдены.
+ ) : (
+
+ {groupedByRouter.map((routerGroup) => (
+
+
+
{routerGroup.routerLabel}
+
+ {routerGroup.areas.map((areaGroup) => (
+
+
+ Area: {areaGroup.area}
+
+
+ {areaGroup.items.map((item) => (
+
setDragging({
+ routerKey: routerGroup.routerKey,
+ area: areaGroup.area,
+ key: item.key,
+ })}
+ onDragOver={(e) => e.preventDefault()}
+ onDrop={() => onDrop(routerGroup.routerKey, areaGroup.area, item.key)}
+ onDragEnd={() => setDragging(null)}
+ style={{ cursor: 'grab' }}
+ >
+
+ ::
+ {item.interfaceName}
+
+ {item.active ? 'Активен' : 'Неактивен'}
+
+
+
cost {item.cost}
+
+ ))}
+
+
+ ))}
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
+