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
+