feat(route-optimizer): implement community full routes display with optimized scoring and detailed metrics in UI
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 57s

This commit is contained in:
2026-03-05 11:08:16 +07:00
parent aab9ed0601
commit baa230c0d2
+131
View File
@@ -29,6 +29,10 @@ function fmtSpeed(dl, ul, fallback) {
return `speed: ${fallback ?? '—'} Mbps`;
}
function routeNodeKey(node) {
return String(node?.id || node?.dns || node?.ip || node?.label || '').trim();
}
const OPT_TABLE_STYLE = { tableLayout: 'fixed', width: '100%' };
const OPT_COLS = ['24%', '20%', '22%', '18%', '16%'];
@@ -72,6 +76,56 @@ export default function RouteOptimizerPage() {
const communityOptimization = Array.isArray(data?.communityOptimizationByJumphost)
? data.communityOptimizationByJumphost
: [];
const communityFullRoutes = (() => {
const out = [];
const homesSafe = Array.isArray(homes) ? homes : [];
const byJh = new Map();
homesSafe.forEach((h) => {
const home = h?.home || null;
const routes = Array.isArray(h?.fullRouteCandidates) ? h.fullRouteCandidates : [];
routes.forEach((r) => {
const jhKey = routeNodeKey(r?.jumphost);
const exKey = routeNodeKey(r?.exit);
if (!jhKey || !exKey) return;
const mapKey = `${jhKey}:::${exKey}`;
const prev = byJh.get(mapKey);
if (!prev || Number(r?.score || 0) > Number(prev?.route?.score || 0)) {
byJh.set(mapKey, { home, route: r });
}
});
});
communityOptimization.forEach((item) => {
const jh = item?.jumphost || null;
const jhKey = routeNodeKey(jh);
const rows = Array.isArray(item?.recommendations) ? item.recommendations : [];
rows.forEach((r) => {
const rec = r?.recommended || null;
const exKey = routeNodeKey(rec?.exit);
if (!rec || !jhKey || !exKey) return;
const bestFull = byJh.get(`${jhKey}:::${exKey}`);
if (!bestFull?.route) return;
out.push({
community: r.community,
communityInfo: r.communityInfo,
home: bestFull.home || bestFull.route.home,
jumphost: bestFull.route.jumphost || jh,
exit: bestFull.route.exit || rec.exit,
fullRouteScore: bestFull.route.score,
fullRouteProbability: bestFull.route.probabilityOptimal,
fullRouteConfidence: bestFull.route.confidence,
homeToJumphost: bestFull.route.homeToJumphost || null,
jumphostToExit: bestFull.route.jumphostToExit || null,
recommendedGateway: rec.gateway || null,
tunnelGateway: rec.tunnelGateway || null,
pinnedBySettings: Boolean(r?.pinnedBySettings),
shouldSwitch: Boolean(r?.shouldSwitch),
});
});
});
return out.sort((a, b) => Number(b.fullRouteScore || 0) - Number(a.fullRouteScore || 0));
})();
return (
<div>
@@ -265,6 +319,83 @@ export default function RouteOptimizerPage() {
})}
</div>
<div className="mt-4">
<h3 className="mb-3">Оптимизированные маршруты Home -&gt; JumpHost -&gt; Exit по BGP community</h3>
<div className="text-muted small mb-3">
Таблица показывает лучший полный маршрут для каждой community с учетом рекомендованного
gateway на Jumphost.
</div>
<div className="table-responsive mb-4">
<table className="table table-vcenter card-table align-middle" style={OPT_TABLE_STYLE}>
<OptimizerColgroup />
<thead>
<tr>
<th>Community</th>
<th>Маршрут (Home -&gt; JumpHost -&gt; Exit)</th>
<th>Сегменты</th>
<th>Gateway / Действие</th>
<th>Вероятность</th>
</tr>
</thead>
<tbody>
{communityFullRoutes.length === 0 ? (
<tr>
<td colSpan={5} className="text-muted">Недостаточно данных для построения полных маршрутов по community.</td>
</tr>
) : communityFullRoutes.map((r, idx) => (
<tr key={`${r.community}-${routeNodeKey(r.home)}-${routeNodeKey(r.jumphost)}-${routeNodeKey(r.exit)}-${idx}`}>
<td className="text-break">
<div className="fw-semibold">{r.community}</div>
{(r.communityInfo?.name || r.communityInfo?.description) && (
<div className="small text-muted">{r.communityInfo?.name || r.communityInfo?.description}</div>
)}
</td>
<td className="small text-break">
<span className="fw-semibold">{r.home?.label || r.home?.dns || r.home?.ip || 'home'}</span>
<span className="text-muted"> </span>
<span className="fw-semibold">{r.jumphost?.label || r.jumphost?.dns || r.jumphost?.ip || 'jumphost'}</span>
<span className="text-muted"> </span>
<span className="fw-semibold">{r.exit?.label || r.exit?.dns || r.exit?.ip || 'exit'}</span>
</td>
<td className="small text-break">
<div>
HJ: ping {r.homeToJumphost?.pingMs ?? '—'} ms, {fmtSpeed(
r.homeToJumphost?.speedDownloadMbps,
r.homeToJumphost?.speedUploadMbps,
r.homeToJumphost?.speedMbps
)}
</div>
<div className="text-muted">
JE: ping {r.jumphostToExit?.pingMs ?? '—'} ms, {fmtSpeed(
r.jumphostToExit?.speedDownloadMbps,
r.jumphostToExit?.speedUploadMbps,
r.jumphostToExit?.speedMbps
)}
</div>
</td>
<td className="small text-break">
<div>{r.recommendedGateway || '—'}</div>
{r.tunnelGateway && <div className="text-muted">{'->'} {r.tunnelGateway}</div>}
<div className="mt-1">
{r.pinnedBySettings ? (
<span className="badge bg-blue-lt text-blue">pin</span>
) : r.shouldSwitch ? (
<span className="badge bg-orange-lt text-orange">switch</span>
) : (
<span className="badge bg-green-lt text-green">ok</span>
)}
</div>
</td>
<td className="small">
<div>{fmtProb(r.fullRouteProbability)}</div>
<div className="text-muted">score {r.fullRouteScore} / conf {r.fullRouteConfidence ?? '—'}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4">
<h3 className="mb-3">Оптимизация по community и filters</h3>
<div className="text-muted small mb-3">