feat(route-optimizer): refactor bandwidth scoring and enhance speed display in UI for improved clarity
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
This commit is contained in:
@@ -77,7 +77,7 @@ function latencyScore(pingMs, aiSettings) {
|
|||||||
return clamp(normalized, 0, 1);
|
return clamp(normalized, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bandwidthScore(speedMbps, aiSettings) {
|
function normalizeBandwidthValue(speedMbps, aiSettings) {
|
||||||
if (typeof speedMbps !== 'number' || speedMbps <= 0) return aiSettings.noSpeedScore;
|
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);
|
||||||
@@ -96,11 +96,29 @@ function freshnessScore(cacheUpdatedAt, aiSettings) {
|
|||||||
return aiSettings.staleScore;
|
return aiSettings.staleScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSegmentScore({ pingMs, speedMbps, cacheUpdatedAt, aiSettings }) {
|
function bandwidthScore({ speedDownloadMbps, speedUploadMbps, aiSettings }) {
|
||||||
|
// DL/UL considered separately with equal 50/50 contribution.
|
||||||
|
const dl = normalizeBandwidthValue(speedDownloadMbps, aiSettings);
|
||||||
|
const ul = normalizeBandwidthValue(speedUploadMbps, aiSettings);
|
||||||
|
return clamp((dl + ul) / 2, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSegmentScore({
|
||||||
|
pingMs,
|
||||||
|
speedMbps,
|
||||||
|
speedDownloadMbps,
|
||||||
|
speedUploadMbps,
|
||||||
|
cacheUpdatedAt,
|
||||||
|
aiSettings,
|
||||||
|
}) {
|
||||||
const l = latencyScore(pingMs, aiSettings);
|
const l = latencyScore(pingMs, aiSettings);
|
||||||
const b = bandwidthScore(speedMbps, aiSettings);
|
const b = bandwidthScore({ speedDownloadMbps, speedUploadMbps, aiSettings });
|
||||||
const f = freshnessScore(cacheUpdatedAt, aiSettings);
|
const f = freshnessScore(cacheUpdatedAt, aiSettings);
|
||||||
const metricPresence = (typeof pingMs === 'number' ? 1 : 0) + (typeof speedMbps === 'number' ? 1 : 0);
|
const hasAnySpeed =
|
||||||
|
typeof speedMbps === 'number' ||
|
||||||
|
typeof speedDownloadMbps === 'number' ||
|
||||||
|
typeof speedUploadMbps === 'number';
|
||||||
|
const metricPresence = (typeof pingMs === 'number' ? 1 : 0) + (hasAnySpeed ? 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 =
|
const score =
|
||||||
l * aiSettings.latencyWeight +
|
l * aiSettings.latencyWeight +
|
||||||
@@ -329,10 +347,24 @@ function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap
|
|||||||
const speedEntry = asObject(speedMap[skey]);
|
const speedEntry = asObject(speedMap[skey]);
|
||||||
const downBps = toNumber(speedEntry.tcpDownloadBps);
|
const downBps = toNumber(speedEntry.tcpDownloadBps);
|
||||||
const upBps = toNumber(speedEntry.tcpUploadBps);
|
const upBps = toNumber(speedEntry.tcpUploadBps);
|
||||||
const speedMbps = downBps != null || upBps != null
|
const speedDownloadMbps = downBps != null ? downBps / 1e6 : null;
|
||||||
? Math.max(downBps || 0, upBps || 0) / 1e6
|
const speedUploadMbps = upBps != null ? upBps / 1e6 : null;
|
||||||
: null;
|
const speedMbps =
|
||||||
const scoring = buildSegmentScore({ pingMs, speedMbps, cacheUpdatedAt, aiSettings });
|
speedDownloadMbps != null && speedUploadMbps != null
|
||||||
|
? (speedDownloadMbps + speedUploadMbps) / 2
|
||||||
|
: speedDownloadMbps != null
|
||||||
|
? speedDownloadMbps
|
||||||
|
: speedUploadMbps != null
|
||||||
|
? speedUploadMbps
|
||||||
|
: null;
|
||||||
|
const scoring = buildSegmentScore({
|
||||||
|
pingMs,
|
||||||
|
speedMbps,
|
||||||
|
speedDownloadMbps,
|
||||||
|
speedUploadMbps,
|
||||||
|
cacheUpdatedAt,
|
||||||
|
aiSettings,
|
||||||
|
});
|
||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
interfaceName: iface.name || null,
|
interfaceName: iface.name || null,
|
||||||
@@ -340,8 +372,8 @@ function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap
|
|||||||
remoteIp: iface.remoteIp || null,
|
remoteIp: iface.remoteIp || null,
|
||||||
pingMs,
|
pingMs,
|
||||||
speedMbps: speedMbps != null ? Number(speedMbps.toFixed(2)) : null,
|
speedMbps: speedMbps != null ? Number(speedMbps.toFixed(2)) : null,
|
||||||
speedDownloadMbps: downBps != null ? Number((downBps / 1e6).toFixed(2)) : null,
|
speedDownloadMbps: speedDownloadMbps != null ? Number(speedDownloadMbps.toFixed(2)) : null,
|
||||||
speedUploadMbps: upBps != null ? Number((upBps / 1e6).toFixed(2)) : null,
|
speedUploadMbps: speedUploadMbps != null ? Number(speedUploadMbps.toFixed(2)) : null,
|
||||||
score: Number(scoring.score.toFixed(4)),
|
score: Number(scoring.score.toFixed(4)),
|
||||||
confidence: Number(scoring.confidence.toFixed(4)),
|
confidence: Number(scoring.confidence.toFixed(4)),
|
||||||
};
|
};
|
||||||
@@ -639,6 +671,8 @@ function buildCommunityOptimization({
|
|||||||
probabilityOptimal: currentCandidate.probabilityOptimal,
|
probabilityOptimal: currentCandidate.probabilityOptimal,
|
||||||
pingMs: currentCandidate.pingMs,
|
pingMs: currentCandidate.pingMs,
|
||||||
speedMbps: currentCandidate.speedMbps,
|
speedMbps: currentCandidate.speedMbps,
|
||||||
|
speedDownloadMbps: currentCandidate.speedDownloadMbps,
|
||||||
|
speedUploadMbps: currentCandidate.speedUploadMbps,
|
||||||
} : null,
|
} : null,
|
||||||
recommended: recommendedCandidate ? {
|
recommended: recommendedCandidate ? {
|
||||||
gateway: recommendedCandidate.recursiveGateway || recommendedCandidate.gatewayIpForJumphost || null,
|
gateway: recommendedCandidate.recursiveGateway || recommendedCandidate.gatewayIpForJumphost || null,
|
||||||
@@ -650,6 +684,8 @@ function buildCommunityOptimization({
|
|||||||
probabilityOptimal: recommendedCandidate.probabilityOptimal,
|
probabilityOptimal: recommendedCandidate.probabilityOptimal,
|
||||||
pingMs: recommendedCandidate.pingMs,
|
pingMs: recommendedCandidate.pingMs,
|
||||||
speedMbps: recommendedCandidate.speedMbps,
|
speedMbps: recommendedCandidate.speedMbps,
|
||||||
|
speedDownloadMbps: recommendedCandidate.speedDownloadMbps,
|
||||||
|
speedUploadMbps: recommendedCandidate.speedUploadMbps,
|
||||||
} : null,
|
} : null,
|
||||||
pinnedGateway: pinned?.gateway || null,
|
pinnedGateway: pinned?.gateway || null,
|
||||||
pinnedBySettings: Boolean(pinned),
|
pinnedBySettings: Boolean(pinned),
|
||||||
@@ -721,6 +757,8 @@ async function getRouteOptimizer(req, res) {
|
|||||||
interfaceName: hj.interfaceName,
|
interfaceName: hj.interfaceName,
|
||||||
pingMs: hj.pingMs,
|
pingMs: hj.pingMs,
|
||||||
speedMbps: hj.speedMbps,
|
speedMbps: hj.speedMbps,
|
||||||
|
speedDownloadMbps: hj.speedDownloadMbps,
|
||||||
|
speedUploadMbps: hj.speedUploadMbps,
|
||||||
score: hj.score,
|
score: hj.score,
|
||||||
confidence: hj.confidence,
|
confidence: hj.confidence,
|
||||||
},
|
},
|
||||||
@@ -728,6 +766,8 @@ async function getRouteOptimizer(req, res) {
|
|||||||
interfaceName: jhExit.interfaceName,
|
interfaceName: jhExit.interfaceName,
|
||||||
pingMs: jhExit.pingMs,
|
pingMs: jhExit.pingMs,
|
||||||
speedMbps: jhExit.speedMbps,
|
speedMbps: jhExit.speedMbps,
|
||||||
|
speedDownloadMbps: jhExit.speedDownloadMbps,
|
||||||
|
speedUploadMbps: jhExit.speedUploadMbps,
|
||||||
score: jhExit.score,
|
score: jhExit.score,
|
||||||
confidence: jhExit.confidence,
|
confidence: jhExit.confidence,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ function fmtProb(v) {
|
|||||||
return typeof v === 'number' ? `${v}%` : '—';
|
return typeof v === 'number' ? `${v}%` : '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtSpeed(dl, ul, fallback) {
|
||||||
|
if (typeof dl === 'number' || typeof ul === 'number') {
|
||||||
|
return `DL: ${dl ?? '—'} Mbps, UL: ${ul ?? '—'} Mbps`;
|
||||||
|
}
|
||||||
|
return `speed: ${fallback ?? '—'} Mbps`;
|
||||||
|
}
|
||||||
|
|
||||||
const OPT_TABLE_STYLE = { tableLayout: 'fixed', width: '100%' };
|
const OPT_TABLE_STYLE = { tableLayout: 'fixed', width: '100%' };
|
||||||
const OPT_COLS = ['24%', '20%', '22%', '18%', '16%'];
|
const OPT_COLS = ['24%', '20%', '22%', '18%', '16%'];
|
||||||
|
|
||||||
@@ -167,7 +174,7 @@ export default function RouteOptimizerPage() {
|
|||||||
<td className="fw-semibold text-break">{c.jumphost?.label || c.jumphost?.dns || c.jumphost?.ip || 'jumphost'}</td>
|
<td className="fw-semibold text-break">{c.jumphost?.label || c.jumphost?.dns || c.jumphost?.ip || 'jumphost'}</td>
|
||||||
<td className="small text-muted text-break">{c.interfaceName || '—'}</td>
|
<td className="small text-muted text-break">{c.interfaceName || '—'}</td>
|
||||||
<td className="small text-muted text-break">
|
<td className="small text-muted text-break">
|
||||||
ping: {c.pingMs ?? '—'} ms, speed: {c.speedMbps ?? '—'} Mbps
|
ping: {c.pingMs ?? '—'} ms, {fmtSpeed(c.speedDownloadMbps, c.speedUploadMbps, c.speedMbps)}
|
||||||
</td>
|
</td>
|
||||||
<td className="small">
|
<td className="small">
|
||||||
{c.score} / {c.confidence}
|
{c.score} / {c.confidence}
|
||||||
@@ -229,10 +236,18 @@ export default function RouteOptimizerPage() {
|
|||||||
<span className="fw-semibold">{r.exit?.label}</span>
|
<span className="fw-semibold">{r.exit?.label}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="small text-muted text-break">
|
<td className="small text-muted text-break">
|
||||||
ping: {r.homeToJumphost?.pingMs ?? '—'} ms, speed: {r.homeToJumphost?.speedMbps ?? '—'} Mbps
|
ping: {r.homeToJumphost?.pingMs ?? '—'} ms, {fmtSpeed(
|
||||||
|
r.homeToJumphost?.speedDownloadMbps,
|
||||||
|
r.homeToJumphost?.speedUploadMbps,
|
||||||
|
r.homeToJumphost?.speedMbps
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="small text-muted text-break">
|
<td className="small text-muted text-break">
|
||||||
ping: {r.jumphostToExit?.pingMs ?? '—'} ms, speed: {r.jumphostToExit?.speedMbps ?? '—'} Mbps
|
ping: {r.jumphostToExit?.pingMs ?? '—'} ms, {fmtSpeed(
|
||||||
|
r.jumphostToExit?.speedDownloadMbps,
|
||||||
|
r.jumphostToExit?.speedUploadMbps,
|
||||||
|
r.jumphostToExit?.speedMbps
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="small">{r.score} / {r.confidence ?? '—'}</td>
|
<td className="small">{r.score} / {r.confidence ?? '—'}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
Reference in New Issue
Block a user