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:
@@ -9,6 +9,23 @@ const { readServersFromS3 } = require('./serversRoutes');
|
|||||||
|
|
||||||
const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json';
|
const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json';
|
||||||
const NETWORK_CONFIG_KEY = 'network-config.json';
|
const NETWORK_CONFIG_KEY = 'network-config.json';
|
||||||
|
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
||||||
|
|
||||||
|
const DEFAULT_AI_SETTINGS = {
|
||||||
|
latencyWeight: 0.55,
|
||||||
|
bandwidthWeight: 0.35,
|
||||||
|
freshnessWeight: 0.1,
|
||||||
|
combineHomeToJumphostWeight: 0.45,
|
||||||
|
combineJumphostToExitWeight: 0.55,
|
||||||
|
probabilityScale: 5,
|
||||||
|
minProbabilityGainForSwitch: 10,
|
||||||
|
noPingScore: 0.2,
|
||||||
|
noSpeedScore: 0.15,
|
||||||
|
staleScore: 0.35,
|
||||||
|
freshnessExcellentSeconds: 120,
|
||||||
|
freshnessGoodSeconds: 600,
|
||||||
|
freshnessFairSeconds: 1800,
|
||||||
|
};
|
||||||
|
|
||||||
function asObject(v) {
|
function asObject(v) {
|
||||||
return v && typeof v === 'object' ? v : {};
|
return v && typeof v === 'object' ? v : {};
|
||||||
@@ -23,6 +40,11 @@ function clamp(n, min, max) {
|
|||||||
return Math.max(min, Math.min(max, n));
|
return Math.max(min, Math.min(max, n));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positiveNumber(v, fallback) {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function makeServerKey(server) {
|
function makeServerKey(server) {
|
||||||
if (!server || typeof server !== 'object') return '';
|
if (!server || typeof server !== 'object') return '';
|
||||||
return String(server.id || server.dns || server.ip || '').trim();
|
return String(server.id || server.dns || server.ip || '').trim();
|
||||||
@@ -47,35 +69,41 @@ function pairProbability(items, scoreGetter) {
|
|||||||
return exps.map((x) => (x / sum) * 100);
|
return exps.map((x) => (x / sum) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function latencyScore(pingMs) {
|
function latencyScore(pingMs, aiSettings) {
|
||||||
if (typeof pingMs !== 'number') return 0.2;
|
if (typeof pingMs !== 'number') return aiSettings.noPingScore;
|
||||||
const normalized = 1 / (1 + pingMs / 35);
|
const normalized = 1 / (1 + pingMs / 35);
|
||||||
return clamp(normalized, 0, 1);
|
return clamp(normalized, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bandwidthScore(speedMbps) {
|
function bandwidthScore(speedMbps, aiSettings) {
|
||||||
if (typeof speedMbps !== 'number' || speedMbps <= 0) return 0.15;
|
if (typeof speedMbps !== 'number' || speedMbps <= 0) return aiSettings.noSpeedScore;
|
||||||
// log-scale to avoid dominance by very high channels
|
// log-scale to avoid dominance by very high channels
|
||||||
const normalized = Math.log10(1 + speedMbps) / Math.log10(1001);
|
const normalized = Math.log10(1 + speedMbps) / Math.log10(1001);
|
||||||
return clamp(normalized, 0, 1);
|
return clamp(normalized, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function freshnessScore(cacheUpdatedAt) {
|
function freshnessScore(cacheUpdatedAt, aiSettings) {
|
||||||
if (typeof cacheUpdatedAt !== 'number') return 0.4;
|
if (typeof cacheUpdatedAt !== 'number') return aiSettings.staleScore;
|
||||||
const ageMs = Math.max(0, Date.now() - cacheUpdatedAt);
|
const ageMs = Math.max(0, Date.now() - cacheUpdatedAt);
|
||||||
if (ageMs <= 2 * 60 * 1000) return 1;
|
const excellentMs = aiSettings.freshnessExcellentSeconds * 1000;
|
||||||
if (ageMs <= 10 * 60 * 1000) return 0.8;
|
const goodMs = aiSettings.freshnessGoodSeconds * 1000;
|
||||||
if (ageMs <= 30 * 60 * 1000) return 0.6;
|
const fairMs = aiSettings.freshnessFairSeconds * 1000;
|
||||||
return 0.35;
|
if (ageMs <= excellentMs) return 1;
|
||||||
|
if (ageMs <= goodMs) return 0.8;
|
||||||
|
if (ageMs <= fairMs) return 0.6;
|
||||||
|
return aiSettings.staleScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSegmentScore({ pingMs, speedMbps, cacheUpdatedAt }) {
|
function buildSegmentScore({ pingMs, speedMbps, cacheUpdatedAt, aiSettings }) {
|
||||||
const l = latencyScore(pingMs);
|
const l = latencyScore(pingMs, aiSettings);
|
||||||
const b = bandwidthScore(speedMbps);
|
const b = bandwidthScore(speedMbps, aiSettings);
|
||||||
const f = freshnessScore(cacheUpdatedAt);
|
const f = freshnessScore(cacheUpdatedAt, aiSettings);
|
||||||
const metricPresence = (typeof pingMs === 'number' ? 1 : 0) + (typeof speedMbps === 'number' ? 1 : 0);
|
const metricPresence = (typeof pingMs === 'number' ? 1 : 0) + (typeof speedMbps === 'number' ? 1 : 0);
|
||||||
const confidence = metricPresence === 2 ? 1 : metricPresence === 1 ? 0.65 : 0.35;
|
const confidence = metricPresence === 2 ? 1 : metricPresence === 1 ? 0.65 : 0.35;
|
||||||
const score = l * 0.55 + b * 0.35 + f * 0.1;
|
const score =
|
||||||
|
l * aiSettings.latencyWeight +
|
||||||
|
b * aiSettings.bandwidthWeight +
|
||||||
|
f * aiSettings.freshnessWeight;
|
||||||
return { score: clamp(score, 0, 1), confidence };
|
return { score: clamp(score, 0, 1), confidence };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +196,69 @@ async function loadNetworkConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap, cacheUpdatedAt }) {
|
async function loadAiSettings() {
|
||||||
|
try {
|
||||||
|
const raw = await readS3TextObject(UI_SETTINGS_KEY).catch(() => null);
|
||||||
|
const parsed = raw?.body ? JSON.parse(raw.body || '{}') : {};
|
||||||
|
const src = asObject(parsed?.aiRouteOptimizer);
|
||||||
|
const latencyWeight = positiveNumber(src.latencyWeight, DEFAULT_AI_SETTINGS.latencyWeight);
|
||||||
|
const bandwidthWeight = positiveNumber(src.bandwidthWeight, DEFAULT_AI_SETTINGS.bandwidthWeight);
|
||||||
|
const freshnessWeight = positiveNumber(src.freshnessWeight, DEFAULT_AI_SETTINGS.freshnessWeight);
|
||||||
|
const sum = latencyWeight + bandwidthWeight + freshnessWeight || 1;
|
||||||
|
const hjW = positiveNumber(
|
||||||
|
src.combineHomeToJumphostWeight,
|
||||||
|
DEFAULT_AI_SETTINGS.combineHomeToJumphostWeight
|
||||||
|
);
|
||||||
|
const jeW = positiveNumber(
|
||||||
|
src.combineJumphostToExitWeight,
|
||||||
|
DEFAULT_AI_SETTINGS.combineJumphostToExitWeight
|
||||||
|
);
|
||||||
|
const sum2 = hjW + jeW || 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
latencyWeight: latencyWeight / sum,
|
||||||
|
bandwidthWeight: bandwidthWeight / sum,
|
||||||
|
freshnessWeight: freshnessWeight / sum,
|
||||||
|
combineHomeToJumphostWeight: hjW / sum2,
|
||||||
|
combineJumphostToExitWeight: jeW / sum2,
|
||||||
|
probabilityScale: clamp(
|
||||||
|
positiveNumber(src.probabilityScale, DEFAULT_AI_SETTINGS.probabilityScale),
|
||||||
|
0.5,
|
||||||
|
20
|
||||||
|
),
|
||||||
|
minProbabilityGainForSwitch: clamp(
|
||||||
|
positiveNumber(
|
||||||
|
src.minProbabilityGainForSwitch,
|
||||||
|
DEFAULT_AI_SETTINGS.minProbabilityGainForSwitch
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
100
|
||||||
|
),
|
||||||
|
noPingScore: clamp(positiveNumber(src.noPingScore, DEFAULT_AI_SETTINGS.noPingScore), 0, 1),
|
||||||
|
noSpeedScore: clamp(positiveNumber(src.noSpeedScore, DEFAULT_AI_SETTINGS.noSpeedScore), 0, 1),
|
||||||
|
staleScore: clamp(positiveNumber(src.staleScore, DEFAULT_AI_SETTINGS.staleScore), 0, 1),
|
||||||
|
freshnessExcellentSeconds: clamp(
|
||||||
|
positiveNumber(src.freshnessExcellentSeconds, DEFAULT_AI_SETTINGS.freshnessExcellentSeconds),
|
||||||
|
10,
|
||||||
|
86400
|
||||||
|
),
|
||||||
|
freshnessGoodSeconds: clamp(
|
||||||
|
positiveNumber(src.freshnessGoodSeconds, DEFAULT_AI_SETTINGS.freshnessGoodSeconds),
|
||||||
|
10,
|
||||||
|
86400
|
||||||
|
),
|
||||||
|
freshnessFairSeconds: clamp(
|
||||||
|
positiveNumber(src.freshnessFairSeconds, DEFAULT_AI_SETTINGS.freshnessFairSeconds),
|
||||||
|
10,
|
||||||
|
86400
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} catch (_) {
|
||||||
|
return { ...DEFAULT_AI_SETTINGS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap, cacheUpdatedAt, aiSettings }) {
|
||||||
const byRef = new Map();
|
const byRef = new Map();
|
||||||
servers.forEach((s) => {
|
servers.forEach((s) => {
|
||||||
const refs = [s.id, s.ip, s.dns].filter(Boolean).map((v) => String(v));
|
const refs = [s.id, s.ip, s.dns].filter(Boolean).map((v) => String(v));
|
||||||
@@ -197,7 +287,7 @@ function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap
|
|||||||
const speedMbps = downBps != null || upBps != null
|
const speedMbps = downBps != null || upBps != null
|
||||||
? Math.max(downBps || 0, upBps || 0) / 1e6
|
? Math.max(downBps || 0, upBps || 0) / 1e6
|
||||||
: null;
|
: null;
|
||||||
const scoring = buildSegmentScore({ pingMs, speedMbps, cacheUpdatedAt });
|
const scoring = buildSegmentScore({ pingMs, speedMbps, cacheUpdatedAt, aiSettings });
|
||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
interfaceName: iface.name || null,
|
interfaceName: iface.name || null,
|
||||||
@@ -244,9 +334,9 @@ function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap
|
|||||||
return { homeToJh, jhToExit };
|
return { homeToJh, jhToExit };
|
||||||
}
|
}
|
||||||
|
|
||||||
function enrichProbabilities(candidates, scoreField = 'score') {
|
function enrichProbabilities(candidates, scoreField = 'score', aiSettings = DEFAULT_AI_SETTINGS) {
|
||||||
if (!Array.isArray(candidates) || candidates.length === 0) return [];
|
if (!Array.isArray(candidates) || candidates.length === 0) return [];
|
||||||
const probs = pairProbability(candidates, (c) => c[scoreField]);
|
const probs = pairProbability(candidates, (c) => c[scoreField] * aiSettings.probabilityScale);
|
||||||
return candidates.map((c, i) => ({
|
return candidates.map((c, i) => ({
|
||||||
...c,
|
...c,
|
||||||
probabilityOptimal: Number(probs[i].toFixed(2)),
|
probabilityOptimal: Number(probs[i].toFixed(2)),
|
||||||
@@ -267,11 +357,12 @@ function buildCommunityOptimization({
|
|||||||
exitsByJh,
|
exitsByJh,
|
||||||
serverFiltersByServerId,
|
serverFiltersByServerId,
|
||||||
communitiesIndex,
|
communitiesIndex,
|
||||||
|
aiSettings,
|
||||||
}) {
|
}) {
|
||||||
const jumphostByCommunity = [];
|
const jumphostByCommunity = [];
|
||||||
|
|
||||||
for (const [jumphostKey, rawCandidates] of exitsByJh.entries()) {
|
for (const [jumphostKey, rawCandidates] of exitsByJh.entries()) {
|
||||||
const candidates = enrichProbabilities([...rawCandidates].sort((a, b) => b.score - a.score));
|
const candidates = enrichProbabilities([...rawCandidates].sort((a, b) => b.score - a.score), 'score', aiSettings);
|
||||||
const gatewayBestMap = new Map();
|
const gatewayBestMap = new Map();
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
const gw = String(c.gatewayIpForJumphost || '').trim();
|
const gw = String(c.gatewayIpForJumphost || '').trim();
|
||||||
@@ -300,7 +391,7 @@ function buildCommunityOptimization({
|
|||||||
recommendedCandidate &&
|
recommendedCandidate &&
|
||||||
currentCandidate &&
|
currentCandidate &&
|
||||||
recommendedCandidate.gatewayIpForJumphost !== currentCandidate.gatewayIpForJumphost &&
|
recommendedCandidate.gatewayIpForJumphost !== currentCandidate.gatewayIpForJumphost &&
|
||||||
(recommendedCandidate.probabilityOptimal - currentCandidate.probabilityOptimal) >= 10
|
(recommendedCandidate.probabilityOptimal - currentCandidate.probabilityOptimal) >= aiSettings.minProbabilityGainForSwitch
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -344,12 +435,13 @@ function buildCommunityOptimization({
|
|||||||
|
|
||||||
async function getRouteOptimizer(req, res) {
|
async function getRouteOptimizer(req, res) {
|
||||||
try {
|
try {
|
||||||
const [servers, networkConfig, networkMapCache, communitiesIndex, serverFiltersByServerId] = await Promise.all([
|
const [servers, networkConfig, networkMapCache, communitiesIndex, serverFiltersByServerId, aiSettings] = await Promise.all([
|
||||||
readServersFromS3(),
|
readServersFromS3(),
|
||||||
loadNetworkConfig(),
|
loadNetworkConfig(),
|
||||||
loadNetworkMapCache(),
|
loadNetworkMapCache(),
|
||||||
loadCommunitiesIndex(),
|
loadCommunitiesIndex(),
|
||||||
loadServerFiltersByServerId(),
|
loadServerFiltersByServerId(),
|
||||||
|
loadAiSettings(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tunnelInterfaces = Array.isArray(networkConfig.tunnelInterfaces)
|
const tunnelInterfaces = Array.isArray(networkConfig.tunnelInterfaces)
|
||||||
@@ -362,6 +454,7 @@ async function getRouteOptimizer(req, res) {
|
|||||||
pingMap: networkMapCache.pingMap,
|
pingMap: networkMapCache.pingMap,
|
||||||
speedMap: networkMapCache.speedMap,
|
speedMap: networkMapCache.speedMap,
|
||||||
cacheUpdatedAt: networkMapCache.updatedAt,
|
cacheUpdatedAt: networkMapCache.updatedAt,
|
||||||
|
aiSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
const homeGroups = groupBy(homeToJh, (x) => x.homeKey);
|
const homeGroups = groupBy(homeToJh, (x) => x.homeKey);
|
||||||
@@ -370,14 +463,19 @@ async function getRouteOptimizer(req, res) {
|
|||||||
const homes = [];
|
const homes = [];
|
||||||
|
|
||||||
for (const [homeKey, hjListRaw] of homeGroups.entries()) {
|
for (const [homeKey, hjListRaw] of homeGroups.entries()) {
|
||||||
const hjList = enrichProbabilities(hjListRaw.sort((a, b) => b.score - a.score));
|
const hjList = enrichProbabilities(hjListRaw.sort((a, b) => b.score - a.score), 'score', aiSettings);
|
||||||
const bestHomeToJumphost = hjList[0] || null;
|
const bestHomeToJumphost = hjList[0] || null;
|
||||||
|
|
||||||
const fullRoutesRaw = [];
|
const fullRoutesRaw = [];
|
||||||
for (const hj of hjList) {
|
for (const hj of hjList) {
|
||||||
const exitCandidates = exitsByJh.get(hj.jumphostKey) || [];
|
const exitCandidates = exitsByJh.get(hj.jumphostKey) || [];
|
||||||
for (const jhExit of exitCandidates) {
|
for (const jhExit of exitCandidates) {
|
||||||
const combinedScore = clamp(hj.score * 0.45 + jhExit.score * 0.55, 0, 1);
|
const combinedScore = clamp(
|
||||||
|
hj.score * aiSettings.combineHomeToJumphostWeight +
|
||||||
|
jhExit.score * aiSettings.combineJumphostToExitWeight,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
fullRoutesRaw.push({
|
fullRoutesRaw.push({
|
||||||
id: `${hj.id}>>>${jhExit.id}`,
|
id: `${hj.id}>>>${jhExit.id}`,
|
||||||
home: hj.home,
|
home: hj.home,
|
||||||
@@ -403,7 +501,7 @@ async function getRouteOptimizer(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullRoutes = enrichProbabilities(fullRoutesRaw.sort((a, b) => b.score - a.score));
|
const fullRoutes = enrichProbabilities(fullRoutesRaw.sort((a, b) => b.score - a.score), 'score', aiSettings);
|
||||||
const bestFullRoute = fullRoutes[0] || null;
|
const bestFullRoute = fullRoutes[0] || null;
|
||||||
|
|
||||||
homes.push({
|
homes.push({
|
||||||
@@ -418,7 +516,7 @@ async function getRouteOptimizer(req, res) {
|
|||||||
// Also expose jumphost->exit view for transparency
|
// Also expose jumphost->exit view for transparency
|
||||||
const jumphostSummaries = [];
|
const jumphostSummaries = [];
|
||||||
for (const [jumphostKey, exitListRaw] of exitsByJh.entries()) {
|
for (const [jumphostKey, exitListRaw] of exitsByJh.entries()) {
|
||||||
const candidates = enrichProbabilities(exitListRaw.sort((a, b) => b.score - a.score));
|
const candidates = enrichProbabilities(exitListRaw.sort((a, b) => b.score - a.score), 'score', aiSettings);
|
||||||
jumphostSummaries.push({
|
jumphostSummaries.push({
|
||||||
jumphost: candidates[0]?.jumphost || null,
|
jumphost: candidates[0]?.jumphost || null,
|
||||||
bestCandidate: candidates[0] || null,
|
bestCandidate: candidates[0] || null,
|
||||||
@@ -430,6 +528,7 @@ async function getRouteOptimizer(req, res) {
|
|||||||
exitsByJh,
|
exitsByJh,
|
||||||
serverFiltersByServerId,
|
serverFiltersByServerId,
|
||||||
communitiesIndex,
|
communitiesIndex,
|
||||||
|
aiSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
homes.sort((a, b) => {
|
homes.sort((a, b) => {
|
||||||
@@ -442,6 +541,7 @@ async function getRouteOptimizer(req, res) {
|
|||||||
ok: true,
|
ok: true,
|
||||||
generatedAt: Date.now(),
|
generatedAt: Date.now(),
|
||||||
metricsUpdatedAt: networkMapCache.updatedAt || null,
|
metricsUpdatedAt: networkMapCache.updatedAt || null,
|
||||||
|
aiSettingsUsed: aiSettings,
|
||||||
homes,
|
homes,
|
||||||
jumphostToExitByJumphost: jumphostSummaries,
|
jumphostToExitByJumphost: jumphostSummaries,
|
||||||
communityOptimizationByJumphost: communityOptimization,
|
communityOptimizationByJumphost: communityOptimization,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
IconRoute2,
|
IconRoute2,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
@@ -54,10 +55,15 @@ export default function RouteOptimizerPage() {
|
|||||||
icon={<IconBrain size={24} />}
|
icon={<IconBrain size={24} />}
|
||||||
meta="Rule-based выбор маршрутов: Home → Jumphost → Exit"
|
meta="Rule-based выбор маршрутов: Home → Jumphost → Exit"
|
||||||
actions={(
|
actions={(
|
||||||
<button type="button" className="btn btn-outline-primary" onClick={load} disabled={loading}>
|
<div className="btn-list">
|
||||||
<IconRefresh className={loading ? 'spin me-2' : 'me-2'} size={18} />
|
<Link to="/settings#route-ai" className="btn btn-outline-secondary">
|
||||||
{loading ? 'Обновление...' : 'Обновить'}
|
Настройки AI
|
||||||
</button>
|
</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">
|
<div className="mt-4">
|
||||||
<h3 className="mb-3">Оптимизация по community и filters</h3>
|
<h3 className="mb-3">Оптимизация по community и filters</h3>
|
||||||
<div className="text-muted small mb-3">
|
<div className="text-muted small mb-3">
|
||||||
Рекомендации строятся на основе ваших `server-filters` (`community -> gateway`) и текущих метрик канала
|
Рекомендации строятся на основе ваших <code>server-filters</code> (связки
|
||||||
`jumphost -> exit`.
|
<code> community -> gateway</code>) и текущих метрик канала
|
||||||
|
<code> jumphost -> exit</code>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{communityOptimization.length === 0 ? (
|
{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">
|
<div className="row g-3">
|
||||||
{communityOptimization.map((item) => {
|
{communityOptimization.map((item) => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
IconCpu,
|
IconCpu,
|
||||||
IconDeviceDesktop,
|
IconDeviceDesktop,
|
||||||
IconDatabase,
|
IconDatabase,
|
||||||
|
IconBrain,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import FormField from './components/FormField';
|
import FormField from './components/FormField';
|
||||||
import ErrorAlert from './components/ErrorAlert';
|
import ErrorAlert from './components/ErrorAlert';
|
||||||
@@ -55,6 +56,7 @@ const SIDEBAR_GROUPS = [
|
|||||||
title: 'Аналитика',
|
title: 'Аналитика',
|
||||||
items: [
|
items: [
|
||||||
{ id: 'traffic-interfaces', title: 'Настройка Аналитики', icon: IconChartBar },
|
{ 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 [serversList, setServersList] = useState([]);
|
||||||
const [tunnelConnections, setTunnelConnections] = useState([]);
|
const [tunnelConnections, setTunnelConnections] = useState([]);
|
||||||
const [tunnelThresholds, setTunnelThresholds] = 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 [sidebarSearch, setSidebarSearch] = useState('');
|
||||||
const [activeSection, setActiveSection] = useState(() => {
|
const [activeSection, setActiveSection] = useState(() => {
|
||||||
const hash = (typeof location.hash === 'string' && location.hash.slice(1)) || '';
|
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 =
|
const e =
|
||||||
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
|
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
|
||||||
setEtag(e ? String(e) : '');
|
setEtag(e ? String(e) : '');
|
||||||
} catch (e) {
|
} catch {
|
||||||
setError('Не удалось загрузить настройки');
|
setError('Не удалось загрузить настройки');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -590,6 +658,51 @@ export default function SettingsPage() {
|
|||||||
interfaceName: p.interfaceName,
|
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: {
|
alertSettings: {
|
||||||
serverOffline: {
|
serverOffline: {
|
||||||
enabled: alertServerOffline,
|
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' && (
|
{activeSection === 'alerts' && (
|
||||||
<>
|
<>
|
||||||
<SectionHeading title="Настройки оповещений" icon={IconBell} />
|
<SectionHeading title="Настройки оповещений" icon={IconBell} />
|
||||||
|
|||||||
Reference in New Issue
Block a user