feat(route-optimizer): enhance AI settings management and UI integration for route optimization
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m35s

This commit is contained in:
2026-03-04 23:22:09 +07:00
parent 963828f9e8
commit 7b7ef1ab45
3 changed files with 450 additions and 34 deletions
+14 -7
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import {
IconRoute2,
IconRefresh,
@@ -54,10 +55,15 @@ export default function RouteOptimizerPage() {
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>
<div className="btn-list">
<Link to="/settings#route-ai" className="btn btn-outline-secondary">
Настройки AI
</Link>
<button type="button" className="btn btn-outline-primary" onClick={load} disabled={loading}>
<IconRefresh className={loading ? 'spin me-2' : 'me-2'} size={18} />
{loading ? 'Обновление...' : 'Обновить'}
</button>
</div>
)}
/>
@@ -169,12 +175,13 @@ export default function RouteOptimizerPage() {
<div className="mt-4">
<h3 className="mb-3">Оптимизация по community и filters</h3>
<div className="text-muted small mb-3">
Рекомендации строятся на основе ваших `server-filters` (`community -> gateway`) и текущих метрик канала
`jumphost -> exit`.
Рекомендации строятся на основе ваших <code>server-filters</code> (связки
<code> community -&gt; gateway</code>) и текущих метрик канала
<code> jumphost -&gt; exit</code>.
</div>
{communityOptimization.length === 0 ? (
<div className="alert alert-info">Нет данных по `server-filters` для jumphost.</div>
<div className="alert alert-info">Нет данных по <code>server-filters</code> для jumphost.</div>
) : (
<div className="row g-3">
{communityOptimization.map((item) => {
+310 -1
View File
@@ -18,6 +18,7 @@ import {
IconCpu,
IconDeviceDesktop,
IconDatabase,
IconBrain,
} from '@tabler/icons-react';
import FormField from './components/FormField';
import ErrorAlert from './components/ErrorAlert';
@@ -55,6 +56,7 @@ const SIDEBAR_GROUPS = [
title: 'Аналитика',
items: [
{ id: 'traffic-interfaces', title: 'Настройка Аналитики', icon: IconChartBar },
{ id: 'route-ai', title: 'AI оптимизация маршрутов', icon: IconBrain },
],
},
{
@@ -120,6 +122,19 @@ export default function SettingsPage() {
const [serversList, setServersList] = useState([]);
const [tunnelConnections, setTunnelConnections] = useState([]);
const [tunnelThresholds, setTunnelThresholds] = useState([]);
const [aiLatencyWeight, setAiLatencyWeight] = useState('0.55');
const [aiBandwidthWeight, setAiBandwidthWeight] = useState('0.35');
const [aiFreshnessWeight, setAiFreshnessWeight] = useState('0.10');
const [aiHomeToJhWeight, setAiHomeToJhWeight] = useState('0.45');
const [aiJhToExitWeight, setAiJhToExitWeight] = useState('0.55');
const [aiProbabilityScale, setAiProbabilityScale] = useState('5');
const [aiMinProbabilityGainForSwitch, setAiMinProbabilityGainForSwitch] = useState('10');
const [aiNoPingScore, setAiNoPingScore] = useState('0.2');
const [aiNoSpeedScore, setAiNoSpeedScore] = useState('0.15');
const [aiStaleScore, setAiStaleScore] = useState('0.35');
const [aiFreshnessExcellentSeconds, setAiFreshnessExcellentSeconds] = useState('120');
const [aiFreshnessGoodSeconds, setAiFreshnessGoodSeconds] = useState('600');
const [aiFreshnessFairSeconds, setAiFreshnessFairSeconds] = useState('1800');
const [sidebarSearch, setSidebarSearch] = useState('');
const [activeSection, setActiveSection] = useState(() => {
const hash = (typeof location.hash === 'string' && location.hash.slice(1)) || '';
@@ -434,10 +449,63 @@ export default function SettingsPage() {
}))
);
const ai = data?.aiRouteOptimizer || {};
setAiLatencyWeight(
ai?.latencyWeight != null ? String(ai.latencyWeight) : '0.55'
);
setAiBandwidthWeight(
ai?.bandwidthWeight != null ? String(ai.bandwidthWeight) : '0.35'
);
setAiFreshnessWeight(
ai?.freshnessWeight != null ? String(ai.freshnessWeight) : '0.10'
);
setAiHomeToJhWeight(
ai?.combineHomeToJumphostWeight != null
? String(ai.combineHomeToJumphostWeight)
: '0.45'
);
setAiJhToExitWeight(
ai?.combineJumphostToExitWeight != null
? String(ai.combineJumphostToExitWeight)
: '0.55'
);
setAiProbabilityScale(
ai?.probabilityScale != null ? String(ai.probabilityScale) : '5'
);
setAiMinProbabilityGainForSwitch(
ai?.minProbabilityGainForSwitch != null
? String(ai.minProbabilityGainForSwitch)
: '10'
);
setAiNoPingScore(
ai?.noPingScore != null ? String(ai.noPingScore) : '0.2'
);
setAiNoSpeedScore(
ai?.noSpeedScore != null ? String(ai.noSpeedScore) : '0.15'
);
setAiStaleScore(
ai?.staleScore != null ? String(ai.staleScore) : '0.35'
);
setAiFreshnessExcellentSeconds(
ai?.freshnessExcellentSeconds != null
? String(ai.freshnessExcellentSeconds)
: '120'
);
setAiFreshnessGoodSeconds(
ai?.freshnessGoodSeconds != null
? String(ai.freshnessGoodSeconds)
: '600'
);
setAiFreshnessFairSeconds(
ai?.freshnessFairSeconds != null
? String(ai.freshnessFairSeconds)
: '1800'
);
const e =
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
setEtag(e ? String(e) : '');
} catch (e) {
} catch {
setError('Не удалось загрузить настройки');
} finally {
setLoading(false);
@@ -590,6 +658,51 @@ export default function SettingsPage() {
interfaceName: p.interfaceName,
}))
: [],
aiRouteOptimizer: {
latencyWeight: Math.max(0, parseFloat(aiLatencyWeight) || 0.55),
bandwidthWeight: Math.max(0, parseFloat(aiBandwidthWeight) || 0.35),
freshnessWeight: Math.max(0, parseFloat(aiFreshnessWeight) || 0.1),
combineHomeToJumphostWeight: Math.max(
0,
parseFloat(aiHomeToJhWeight) || 0.45
),
combineJumphostToExitWeight: Math.max(
0,
parseFloat(aiJhToExitWeight) || 0.55
),
probabilityScale: Math.max(
0.5,
Math.min(20, parseFloat(aiProbabilityScale) || 5)
),
minProbabilityGainForSwitch: Math.max(
0,
Math.min(100, parseFloat(aiMinProbabilityGainForSwitch) || 10)
),
noPingScore: Math.max(
0,
Math.min(1, parseFloat(aiNoPingScore) || 0.2)
),
noSpeedScore: Math.max(
0,
Math.min(1, parseFloat(aiNoSpeedScore) || 0.15)
),
staleScore: Math.max(
0,
Math.min(1, parseFloat(aiStaleScore) || 0.35)
),
freshnessExcellentSeconds: Math.max(
10,
Math.min(86400, parseInt(aiFreshnessExcellentSeconds, 10) || 120)
),
freshnessGoodSeconds: Math.max(
10,
Math.min(86400, parseInt(aiFreshnessGoodSeconds, 10) || 600)
),
freshnessFairSeconds: Math.max(
10,
Math.min(86400, parseInt(aiFreshnessFairSeconds, 10) || 1800)
),
},
alertSettings: {
serverOffline: {
enabled: alertServerOffline,
@@ -1297,6 +1410,202 @@ export default function SettingsPage() {
</>
)}
{activeSection === 'route-ai' && (
<>
<SectionHeading title="AI оптимизация маршрутов" icon={IconBrain} />
<p className="text-muted mb-4">
Настройка правил локального AI: веса метрик, вероятностная модель и пороги решений
для рекомендаций по связке <code>community -&gt; gateway</code>.
</p>
<div className="row g-2">
<div className="col-12"><h4 className="subheader">Веса метрик сегмента</h4></div>
<div className="col-12 col-md-4">
<FormField
label="Вес latency"
name="aiLatencyWeight"
type="number"
value={aiLatencyWeight}
onChange={setAiLatencyWeight}
helpText="Вклад пинга в итоговый score сегмента."
disabled={saving}
min={0}
step="0.01"
/>
</div>
<div className="col-12 col-md-4">
<FormField
label="Вес bandwidth"
name="aiBandwidthWeight"
type="number"
value={aiBandwidthWeight}
onChange={setAiBandwidthWeight}
helpText="Вклад скорости (download/upload) в score."
disabled={saving}
min={0}
step="0.01"
/>
</div>
<div className="col-12 col-md-4">
<FormField
label="Вес freshness"
name="aiFreshnessWeight"
type="number"
value={aiFreshnessWeight}
onChange={setAiFreshnessWeight}
helpText="Вклад свежести метрик в score."
disabled={saving}
min={0}
step="0.01"
/>
</div>
<div className="col-12 mt-2"><h4 className="subheader">Сборка полного маршрута</h4></div>
<div className="col-12 col-md-6">
<FormField
label="Вес Home -> Jumphost"
name="aiHomeToJhWeight"
type="number"
value={aiHomeToJhWeight}
onChange={setAiHomeToJhWeight}
helpText="Влияние сегмента Home->Jumphost на общий score."
disabled={saving}
min={0}
step="0.01"
/>
</div>
<div className="col-12 col-md-6">
<FormField
label="Вес Jumphost -> Exit"
name="aiJhToExitWeight"
type="number"
value={aiJhToExitWeight}
onChange={setAiJhToExitWeight}
helpText="Влияние сегмента Jumphost->Exit на общий score."
disabled={saving}
min={0}
step="0.01"
/>
</div>
<div className="col-12 mt-2"><h4 className="subheader">Вероятности и решения</h4></div>
<div className="col-12 col-md-6">
<FormField
label="Коэффициент softmax (probability scale)"
name="aiProbabilityScale"
type="number"
value={aiProbabilityScale}
onChange={setAiProbabilityScale}
helpText="Чем выше значение, тем агрессивнее выделяется лучший маршрут (0.5-20)."
disabled={saving}
min={0.5}
max={20}
step="0.1"
/>
</div>
<div className="col-12 col-md-6">
<FormField
label="Мин. прирост вероятности для switch (%)"
name="aiMinProbabilityGainForSwitch"
type="number"
value={aiMinProbabilityGainForSwitch}
onChange={setAiMinProbabilityGainForSwitch}
helpText="Рекомендовать переключение только если новый gateway лучше на этот %."
disabled={saving}
min={0}
max={100}
step="0.1"
/>
</div>
<div className="col-12 mt-2"><h4 className="subheader">Поведение при неполных данных</h4></div>
<div className="col-12 col-md-4">
<FormField
label="Score если нет ping"
name="aiNoPingScore"
type="number"
value={aiNoPingScore}
onChange={setAiNoPingScore}
helpText="Оценка latency при отсутствии пинга (0-1)."
disabled={saving}
min={0}
max={1}
step="0.01"
/>
</div>
<div className="col-12 col-md-4">
<FormField
label="Score если нет speed"
name="aiNoSpeedScore"
type="number"
value={aiNoSpeedScore}
onChange={setAiNoSpeedScore}
helpText="Оценка bandwidth при отсутствии скорости (0-1)."
disabled={saving}
min={0}
max={1}
step="0.01"
/>
</div>
<div className="col-12 col-md-4">
<FormField
label="Score устаревших метрик"
name="aiStaleScore"
type="number"
value={aiStaleScore}
onChange={setAiStaleScore}
helpText="Оценка freshness для старых данных (0-1)."
disabled={saving}
min={0}
max={1}
step="0.01"
/>
</div>
<div className="col-12 mt-2"><h4 className="subheader">Пороги свежести (сек)</h4></div>
<div className="col-12 col-md-4">
<FormField
label="Excellent"
name="aiFreshnessExcellentSeconds"
type="number"
value={aiFreshnessExcellentSeconds}
onChange={setAiFreshnessExcellentSeconds}
helpText="До этого порога freshness = 1.0."
disabled={saving}
min={10}
max={86400}
/>
</div>
<div className="col-12 col-md-4">
<FormField
label="Good"
name="aiFreshnessGoodSeconds"
type="number"
value={aiFreshnessGoodSeconds}
onChange={setAiFreshnessGoodSeconds}
helpText="До этого порога freshness = 0.8."
disabled={saving}
min={10}
max={86400}
/>
</div>
<div className="col-12 col-md-4">
<FormField
label="Fair"
name="aiFreshnessFairSeconds"
type="number"
value={aiFreshnessFairSeconds}
onChange={setAiFreshnessFairSeconds}
helpText="До этого порога freshness = 0.6, затем staleScore."
disabled={saving}
min={10}
max={86400}
/>
</div>
</div>
</>
)}
{activeSection === 'alerts' && (
<>
<SectionHeading title="Настройки оповещений" icon={IconBell} />