feat(route-optimizer): integrate AI route optimizer functionality into the application with routing endpoint and UI components
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
@@ -27,7 +27,8 @@ import {
|
||||
IconChartBar,
|
||||
IconClock,
|
||||
IconClockPlay,
|
||||
IconShield
|
||||
IconShield,
|
||||
IconRoute2
|
||||
} from '@tabler/icons-react';
|
||||
import ServerManager from './ServerManager';
|
||||
import FilterManager from './FilterManager';
|
||||
@@ -51,6 +52,7 @@ import PingServicesManager from './PingServicesManager.jsx';
|
||||
import FirewallPage from './FirewallPage.jsx';
|
||||
import SettingsPage from './SettingsPage.jsx';
|
||||
import ResourceStatsPage from './ResourceStatsPage.jsx';
|
||||
import RouteOptimizerPage from './RouteOptimizerPage.jsx';
|
||||
import './App.css';
|
||||
import { NotifyProvider } from './components/NotifyProvider.jsx';
|
||||
import ToastContainer from './components/ToastContainer.jsx';
|
||||
@@ -71,6 +73,7 @@ function LanguageProvider({ children }) {
|
||||
ru: {
|
||||
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
|
||||
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',
|
||||
light: 'Светлая', dark: 'Тёмная',
|
||||
@@ -79,6 +82,7 @@ function LanguageProvider({ children }) {
|
||||
en: {
|
||||
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
|
||||
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',
|
||||
light: 'Light', dark: 'Dark',
|
||||
@@ -165,7 +169,6 @@ function MainLayout() {
|
||||
useEffect(() => {
|
||||
if (layout !== LAYOUT_FLUID) return;
|
||||
const collapseEl = document.getElementById(SIDEBAR_COLLAPSE_ID);
|
||||
const toggler = () => sidebarTogglerRef.current?.click?.();
|
||||
const onShow = () => {
|
||||
if (window.innerWidth < MOBILE_BREAKPOINT) {
|
||||
setSidebarBackdrop(true);
|
||||
@@ -252,6 +255,7 @@ function MainLayout() {
|
||||
{ id: 'traffic', title: t('trafficTraffic'), path: '/traffic', icon: IconChartPie },
|
||||
{ id: 'resource-stats', title: t('resourceStats'), path: '/resource-stats', icon: IconChartBar },
|
||||
{ id: 'network-map', title: t('networkMap'), path: '/network-map', icon: IconNetwork },
|
||||
{ id: 'route-optimizer', title: t('routeOptimizer'), path: '/route-optimizer', icon: IconRoute2 },
|
||||
{ id: 'uptime-monitor', title: t('uptimeMonitor'), path: '/uptime-monitor', icon: IconClock }
|
||||
]
|
||||
},
|
||||
@@ -449,6 +453,7 @@ function MainLayout() {
|
||||
<Route path="/traffic" element={<TrafficDashboard />} />
|
||||
<Route path="/resource-stats" element={<ResourceStatsPage />} />
|
||||
<Route path="/network-map" element={<NetworkMapDashboard />} />
|
||||
<Route path="/route-optimizer" element={<RouteOptimizerPage />} />
|
||||
<Route path="/uptime-monitor" element={<UptimeMonitorPage />} />
|
||||
<Route path="/domains" element={<DomainsNewManager />} />
|
||||
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||
@@ -625,6 +630,7 @@ function MainLayout() {
|
||||
<Route path="/traffic" element={<TrafficDashboard />} />
|
||||
<Route path="/resource-stats" element={<ResourceStatsPage />} />
|
||||
<Route path="/network-map" element={<NetworkMapDashboard />} />
|
||||
<Route path="/route-optimizer" element={<RouteOptimizerPage />} />
|
||||
<Route path="/uptime-monitor" element={<UptimeMonitorPage />} />
|
||||
<Route path="/domains" element={<DomainsNewManager />} />
|
||||
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
IconRoute2,
|
||||
IconRefresh,
|
||||
IconBrain,
|
||||
IconAlertCircle,
|
||||
} from '@tabler/icons-react';
|
||||
import api from './lib/api.js';
|
||||
import PageHeader from './components/PageHeader.jsx';
|
||||
|
||||
function MetricBadge({ label, value, tone = 'secondary' }) {
|
||||
return (
|
||||
<span className={`badge bg-${tone}-lt text-${tone}`}>
|
||||
{label}: {value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RouteOptimizerPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await api.get('/route-optimizer', { timeout: 30000 });
|
||||
setData(res?.data || null);
|
||||
} catch (e) {
|
||||
console.error('[RouteOptimizerPage] load failed:', e);
|
||||
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить маршруты');
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
const id = setInterval(load, 30000);
|
||||
return () => clearInterval(id);
|
||||
}, [load]);
|
||||
|
||||
const homes = Array.isArray(data?.homes) ? data.homes : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Локальный ИИ: оптимизатор маршрутов"
|
||||
icon={<IconBrain size={24} />}
|
||||
meta="Rule-based выбор маршрутов: Home → Jumphost → Exit"
|
||||
actions={(
|
||||
<button type="button" className="btn btn-outline-primary" onClick={load} disabled={loading}>
|
||||
<IconRefresh className={loading ? 'spin me-2' : 'me-2'} size={18} />
|
||||
{loading ? 'Обновление...' : 'Обновить'}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger d-flex align-items-center">
|
||||
<IconAlertCircle className="me-2" size={18} />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && !data && (
|
||||
<div className="text-muted py-4">Расчёт маршрутов...</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && homes.length === 0 && (
|
||||
<div className="alert alert-info">
|
||||
Нет доступных маршрутов. Проверьте типы серверов (`home`, `jumphost`, `exit`) и `network-config.tunnelInterfaces`.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row g-3">
|
||||
{homes.map((entry) => {
|
||||
const best = entry.bestFullRoute;
|
||||
const home = entry.home || {};
|
||||
const routes = Array.isArray(entry.fullRouteCandidates) ? entry.fullRouteCandidates : [];
|
||||
return (
|
||||
<div className="col-12" key={home.id || home.ip || home.dns || Math.random()}>
|
||||
<div className="card">
|
||||
<div className="card-header d-flex flex-wrap gap-2 align-items-center justify-content-between">
|
||||
<h3 className="card-title mb-0 d-flex align-items-center">
|
||||
<IconRoute2 className="me-2" size={18} />
|
||||
{home.label || home.dns || home.ip || 'Home'}
|
||||
</h3>
|
||||
{best ? (
|
||||
<MetricBadge
|
||||
label="Вероятность оптимальности лучшего маршрута"
|
||||
value={`${best.probabilityOptimal}%`}
|
||||
tone="green"
|
||||
/>
|
||||
) : (
|
||||
<MetricBadge label="Статус" value="Нет полного маршрута до Exit" tone="orange" />
|
||||
)}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{best ? (
|
||||
<div className="mb-3">
|
||||
<div className="fw-semibold mb-2">Оптимальный маршрут</div>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
<span className="badge bg-blue-lt text-blue">{best.home?.label || 'home'}</span>
|
||||
<span className="text-muted">→</span>
|
||||
<span className="badge bg-indigo-lt text-indigo">{best.jumphost?.label || 'jumphost'}</span>
|
||||
<span className="text-muted">→</span>
|
||||
<span className="badge bg-orange-lt text-orange">{best.exit?.label || 'exit'}</span>
|
||||
<MetricBadge label="Score" value={best.score} tone="primary" />
|
||||
<MetricBadge label="Confidence" value={best.confidence} tone="secondary" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Маршрут</th>
|
||||
<th>Вероятность</th>
|
||||
<th>Home→Jumphost</th>
|
||||
<th>Jumphost→Exit</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{routes.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-muted">
|
||||
Полные маршруты до exit-ноды не найдены.
|
||||
</td>
|
||||
</tr>
|
||||
) : routes.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td>
|
||||
<span className="fw-semibold">{r.home?.label}</span>
|
||||
<span className="text-muted"> → </span>
|
||||
<span className="fw-semibold">{r.jumphost?.label}</span>
|
||||
<span className="text-muted"> → </span>
|
||||
<span className="fw-semibold">{r.exit?.label}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="badge bg-green-lt text-green">{r.probabilityOptimal}%</span>
|
||||
</td>
|
||||
<td className="small text-muted">
|
||||
ping: {r.homeToJumphost?.pingMs ?? '—'} ms, speed: {r.homeToJumphost?.speedMbps ?? '—'} Mbps
|
||||
</td>
|
||||
<td className="small text-muted">
|
||||
ping: {r.jumphostToExit?.pingMs ?? '—'} ms, speed: {r.jumphostToExit?.speedMbps ?? '—'} Mbps
|
||||
</td>
|
||||
<td className="small">{r.score}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user