From b9d45bb8b794b7163eb9eceeeee5f27aafee4ba6 Mon Sep 17 00:00:00 2001 From: shats Date: Sun, 15 Mar 2026 21:40:33 +0700 Subject: [PATCH] feat(OspfToolsPage): add cost optimization feature for OSPF interfaces with user notifications --- frontend/src/OspfToolsPage.jsx | 74 +++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/frontend/src/OspfToolsPage.jsx b/frontend/src/OspfToolsPage.jsx index 32a61b1..27e87fa 100644 --- a/frontend/src/OspfToolsPage.jsx +++ b/frontend/src/OspfToolsPage.jsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { IconDatabase, IconRefresh, IconRoute2 } from '@tabler/icons-react'; +import { IconDatabase, IconRefresh, IconRoute2, IconWand } from '@tabler/icons-react'; import PageHeader from './components/PageHeader.jsx'; import api from './lib/api.js'; import { useNotify } from './components/NotifyProvider.jsx'; @@ -235,10 +235,12 @@ export default function OspfToolsPage() { const response = await api.get('/route-optimizer', { timeout: 30000 }); const probabilities = buildOptimizerProbabilityMap(response?.data, servers); setOptimizerProbabilities(probabilities); + return probabilities; } catch (error) { console.error('Failed to load route optimizer hints:', error); setOptimizerProbabilities({}); if (!silent) notify.warning('Не удалось загрузить рекомендации из оптимизатора маршрутов'); + return {}; } finally { setOptimizerLoading(false); } @@ -282,6 +284,66 @@ export default function OspfToolsPage() { setDragging(null); }; + const optimizeCosts = async () => { + let hints = optimalHints; + if (!hints || Object.keys(hints).length === 0) { + const probabilities = await loadOptimizerHints({ silent: true }); + const refreshedHints = {}; + groupedByRouter.forEach((routerGroup) => { + (routerGroup.areas || []).forEach((areaGroup) => { + const ranked = [...(areaGroup.items || [])] + .map((item) => { + const ifaceKey = `${routerGroup.routerKey}::${String(item.interfaceName || '').toUpperCase()}`; + const probabilityOptimal = Number((probabilities || {})[ifaceKey] || 0); + return { item, probabilityOptimal }; + }) + .sort((a, b) => + Number(b.probabilityOptimal || 0) - Number(a.probabilityOptimal || 0) || + String(a.item.interfaceName || '').localeCompare(String(b.item.interfaceName || '')) + ); + ranked.forEach(({ item, probabilityOptimal }, idx) => { + const hintKey = `${routerGroup.routerKey}::${areaGroup.area}::${String(item.interfaceName || '').toUpperCase()}`; + refreshedHints[hintKey] = { + optimalCost: (idx + 1) * OSPF_COST_STEP, + probabilityOptimal, + }; + }); + }); + }); + hints = refreshedHints; + } + + if (!hints || Object.keys(hints).length === 0) { + notify.warning('Нет рекомендаций оптимизатора для расчета cost'); + return; + } + + let changedCount = 0; + let hintedCount = 0; + setTemplates((prev) => + prev.map((item) => { + const hintKey = `${item.routerKey}::${item.area}::${String(item.interfaceName || '').toUpperCase()}`; + const hint = hints[hintKey]; + if (!hint) return item; + hintedCount += 1; + const nextCost = Number(hint.optimalCost); + if (!Number.isFinite(nextCost) || nextCost < 0 || Number(item.cost) === nextCost) return item; + changedCount += 1; + return { ...item, cost: nextCost }; + }) + ); + + if (hintedCount === 0) { + notify.warning('Для интерфейсов на странице нет рекомендаций оптимизатора'); + return; + } + if (changedCount === 0) { + notify.success('Текущие cost уже соответствуют оптимальным рекомендациям'); + return; + } + notify.success(`Оптимизация применена: обновлено cost у ${changedCount} интерфейсов`); + }; + const saveOspf = async () => { if (groupedByRouter.length === 0) { notify.warning('Нет OSPF данных для сохранения'); @@ -333,6 +395,16 @@ export default function OspfToolsPage() { Загрузить из MikroTik +