diff --git a/backend/routes/routeOptimizerRoutes.js b/backend/routes/routeOptimizerRoutes.js index 2a26b6f..515f744 100644 --- a/backend/routes/routeOptimizerRoutes.js +++ b/backend/routes/routeOptimizerRoutes.js @@ -77,7 +77,7 @@ function latencyScore(pingMs, aiSettings) { return clamp(normalized, 0, 1); } -function bandwidthScore(speedMbps, aiSettings) { +function normalizeBandwidthValue(speedMbps, aiSettings) { if (typeof speedMbps !== 'number' || speedMbps <= 0) return aiSettings.noSpeedScore; // log-scale to avoid dominance by very high channels const normalized = Math.log10(1 + speedMbps) / Math.log10(1001); @@ -96,11 +96,29 @@ function freshnessScore(cacheUpdatedAt, aiSettings) { 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 b = bandwidthScore(speedMbps, aiSettings); + const b = bandwidthScore({ speedDownloadMbps, speedUploadMbps, 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 score = l * aiSettings.latencyWeight + @@ -329,10 +347,24 @@ function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap const speedEntry = asObject(speedMap[skey]); const downBps = toNumber(speedEntry.tcpDownloadBps); const upBps = toNumber(speedEntry.tcpUploadBps); - const speedMbps = downBps != null || upBps != null - ? Math.max(downBps || 0, upBps || 0) / 1e6 - : null; - const scoring = buildSegmentScore({ pingMs, speedMbps, cacheUpdatedAt, aiSettings }); + const speedDownloadMbps = downBps != null ? downBps / 1e6 : null; + const speedUploadMbps = upBps != null ? upBps / 1e6 : null; + const speedMbps = + 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 = { interfaceName: iface.name || null, @@ -340,8 +372,8 @@ function buildInterfaceCandidates({ servers, tunnelInterfaces, pingMap, speedMap remoteIp: iface.remoteIp || null, pingMs, speedMbps: speedMbps != null ? Number(speedMbps.toFixed(2)) : null, - speedDownloadMbps: downBps != null ? Number((downBps / 1e6).toFixed(2)) : null, - speedUploadMbps: upBps != null ? Number((upBps / 1e6).toFixed(2)) : null, + speedDownloadMbps: speedDownloadMbps != null ? Number(speedDownloadMbps.toFixed(2)) : null, + speedUploadMbps: speedUploadMbps != null ? Number(speedUploadMbps.toFixed(2)) : null, score: Number(scoring.score.toFixed(4)), confidence: Number(scoring.confidence.toFixed(4)), }; @@ -639,6 +671,8 @@ function buildCommunityOptimization({ probabilityOptimal: currentCandidate.probabilityOptimal, pingMs: currentCandidate.pingMs, speedMbps: currentCandidate.speedMbps, + speedDownloadMbps: currentCandidate.speedDownloadMbps, + speedUploadMbps: currentCandidate.speedUploadMbps, } : null, recommended: recommendedCandidate ? { gateway: recommendedCandidate.recursiveGateway || recommendedCandidate.gatewayIpForJumphost || null, @@ -650,6 +684,8 @@ function buildCommunityOptimization({ probabilityOptimal: recommendedCandidate.probabilityOptimal, pingMs: recommendedCandidate.pingMs, speedMbps: recommendedCandidate.speedMbps, + speedDownloadMbps: recommendedCandidate.speedDownloadMbps, + speedUploadMbps: recommendedCandidate.speedUploadMbps, } : null, pinnedGateway: pinned?.gateway || null, pinnedBySettings: Boolean(pinned), @@ -721,6 +757,8 @@ async function getRouteOptimizer(req, res) { interfaceName: hj.interfaceName, pingMs: hj.pingMs, speedMbps: hj.speedMbps, + speedDownloadMbps: hj.speedDownloadMbps, + speedUploadMbps: hj.speedUploadMbps, score: hj.score, confidence: hj.confidence, }, @@ -728,6 +766,8 @@ async function getRouteOptimizer(req, res) { interfaceName: jhExit.interfaceName, pingMs: jhExit.pingMs, speedMbps: jhExit.speedMbps, + speedDownloadMbps: jhExit.speedDownloadMbps, + speedUploadMbps: jhExit.speedUploadMbps, score: jhExit.score, confidence: jhExit.confidence, }, diff --git a/frontend/src/RouteOptimizerPage.jsx b/frontend/src/RouteOptimizerPage.jsx index c53015b..d927d5a 100644 --- a/frontend/src/RouteOptimizerPage.jsx +++ b/frontend/src/RouteOptimizerPage.jsx @@ -22,6 +22,13 @@ function fmtProb(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_COLS = ['24%', '20%', '22%', '18%', '16%']; @@ -167,7 +174,7 @@ export default function RouteOptimizerPage() {