feat(route-optimizer): implement pinned community gateways feature with UI integration for settings and recommendations
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2026-03-04 23:39:43 +07:00
parent 624c0d6fd3
commit 8012968ab5
3 changed files with 297 additions and 11 deletions
+69 -10
View File
@@ -26,6 +26,7 @@ const DEFAULT_AI_SETTINGS = {
freshnessGoodSeconds: 600,
freshnessFairSeconds: 1800,
distancePenaltyPerStep: 0.03,
pinnedCommunityGateways: [],
};
function asObject(v) {
@@ -119,6 +120,32 @@ function compactServer(server) {
};
}
function normalizeRef(value) {
return String(value || '').trim();
}
function buildServerRefSet(server) {
return new Set(collectServerRefs(server).map(normalizeRef).filter(Boolean));
}
function findPinnedCommunityGateway(pinnedCommunityGateways, jumphost, community) {
const targetCommunity = normalizeRef(community);
if (!targetCommunity || !Array.isArray(pinnedCommunityGateways)) return null;
const jumphostRefs = buildServerRefSet(jumphost);
for (const entry of pinnedCommunityGateways) {
const entryCommunity = normalizeRef(entry?.community);
if (!entryCommunity || entryCommunity !== targetCommunity) continue;
const pinnedGateway = normalizeRef(entry?.gateway);
if (!pinnedGateway) continue;
const pinnedJumphost = normalizeRef(entry?.jumphost);
if (!pinnedJumphost) return { community: entryCommunity, gateway: pinnedGateway, jumphost: null };
if (jumphostRefs.has(pinnedJumphost)) {
return { community: entryCommunity, gateway: pinnedGateway, jumphost: pinnedJumphost };
}
}
return null;
}
async function loadCommunitiesIndex() {
try {
const raw = await readS3TextObject('bgp_data/communities.json').catch(() => null);
@@ -217,6 +244,16 @@ async function loadAiSettings() {
);
const sum2 = hjW + jeW || 1;
const pinnedCommunityGateways = Array.isArray(src.pinnedCommunityGateways)
? src.pinnedCommunityGateways
.map((x) => ({
community: normalizeRef(x?.community),
gateway: normalizeRef(x?.gateway),
jumphost: normalizeRef(x?.jumphost),
}))
.filter((x) => x.community && x.gateway)
: [];
return {
latencyWeight: latencyWeight / sum,
bandwidthWeight: bandwidthWeight / sum,
@@ -259,6 +296,7 @@ async function loadAiSettings() {
0,
1
),
pinnedCommunityGateways,
};
} catch (_) {
return { ...DEFAULT_AI_SETTINGS };
@@ -546,23 +584,41 @@ function buildCommunityOptimization({
const recommendations = filters.map((f) => {
const currentGateway = String(f.gateway || '').trim();
const pinned = findPinnedCommunityGateway(
aiSettings.pinnedCommunityGateways,
jumphost,
f.community
);
const currentCandidate =
recursiveByIp.get(currentGateway) ||
recursiveById.get(currentGateway) ||
gatewayBestMap.get(currentGateway) ||
null;
const recommendedCandidate =
(recursiveOptions.length > 0 ? recursiveOptions[0] : null) ||
candidates[0] ||
null;
const pinnedCandidate = pinned
? recursiveByIp.get(pinned.gateway) ||
recursiveById.get(pinned.gateway) ||
gatewayBestMap.get(pinned.gateway) ||
null
: null;
const recommendedCandidate = pinned
? pinnedCandidate
: (recursiveOptions.length > 0 ? recursiveOptions[0] : null) || candidates[0] || null;
const communityInfo = communitiesIndex.get(String(f.community)) || null;
const shouldSwitch = Boolean(
recommendedCandidate &&
currentCandidate &&
(recommendedCandidate.recursiveGateway || recommendedCandidate.gatewayIpForJumphost) !==
(currentCandidate.recursiveGateway || currentCandidate.gatewayIpForJumphost) &&
(recommendedCandidate.probabilityOptimal - currentCandidate.probabilityOptimal) >= aiSettings.minProbabilityGainForSwitch
const recommendedGatewayRef = normalizeRef(
recommendedCandidate?.recursiveGateway || recommendedCandidate?.gatewayIpForJumphost
);
const currentGatewayRef = normalizeRef(
currentCandidate?.recursiveGateway || currentCandidate?.gatewayIpForJumphost || currentGateway
);
const shouldSwitch = pinned
? Boolean(recommendedGatewayRef && recommendedGatewayRef !== currentGatewayRef)
: Boolean(
recommendedCandidate &&
currentCandidate &&
recommendedGatewayRef !== currentGatewayRef &&
(recommendedCandidate.probabilityOptimal - currentCandidate.probabilityOptimal) >=
aiSettings.minProbabilityGainForSwitch
);
return {
community: String(f.community),
@@ -595,6 +651,9 @@ function buildCommunityOptimization({
pingMs: recommendedCandidate.pingMs,
speedMbps: recommendedCandidate.speedMbps,
} : null,
pinnedGateway: pinned?.gateway || null,
pinnedBySettings: Boolean(pinned),
pinnedGatewayResolved: Boolean(pinned && recommendedCandidate),
shouldSwitch,
};
});