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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m35s
This commit is contained in:
@@ -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 -> gateway</code>) и текущих метрик канала
|
||||
<code> jumphost -> 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) => {
|
||||
|
||||
@@ -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 -> 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} />
|
||||
|
||||
Reference in New Issue
Block a user