chore: synchronize pending app/backend updates and repository hygiene
Includes current frontend and backend work in progress and removes generated artifacts from tracking to keep the repository clean for дальнейшая разработка. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+223
-13
@@ -3,7 +3,7 @@
|
||||
* (см. блок `#route-ai` в `/settings` и страницу `/route-optimizer`).
|
||||
*/
|
||||
|
||||
import type { FilterRule } from "@/lib/data"
|
||||
import { servers, type FilterRule } from "@/lib/data"
|
||||
|
||||
// ── Совместимо с app/(main)/route-optimizer/page.tsx ─────────────────────────
|
||||
|
||||
@@ -130,6 +130,15 @@ export interface OptimizerApiServer {
|
||||
status: "online" | "offline" | null
|
||||
latency: number | null
|
||||
model: string | null
|
||||
wanUplinks?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
isp: string
|
||||
iface: string
|
||||
ip: string
|
||||
maxDl: number
|
||||
maxUl: number
|
||||
}>
|
||||
}
|
||||
|
||||
export interface FiltersRulesetRow {
|
||||
@@ -137,10 +146,29 @@ export interface FiltersRulesetRow {
|
||||
rules: FilterRule[]
|
||||
}
|
||||
|
||||
function calcScore(pingMs: number, dlMbps: number, ulMbps: number, pw: number) {
|
||||
/** Снимок speed-probe из /api/uptime/speed-probes (live-источник ping/speed по интерфейсам). */
|
||||
export interface RouteOptimizerSpeedProbe {
|
||||
id: string
|
||||
srcServerId: string
|
||||
dstServerId: string
|
||||
srcInterface: string
|
||||
dstInterface: string
|
||||
enabled: boolean
|
||||
lastPingRttMs: number | null
|
||||
lastPingLossPct: number | null
|
||||
lastTxAvgMbps: number | null
|
||||
lastRxAvgMbps: number | null
|
||||
lastPingAt: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Единый расчёт score для Route AI (используется в /route-optimizer и связанных оптимизаторах).
|
||||
* pingWeight = вес ping в процентах (0..100), оставшийся вес идёт в speed.
|
||||
*/
|
||||
export function calcRouteScore(pingMs: number, dlMbps: number, ulMbps: number, pingWeight: number) {
|
||||
const pingScore = Math.max(0, 100 - pingMs * 0.6)
|
||||
const speedScore = Math.min(100, (dlMbps + ulMbps) / 18)
|
||||
const w = pw / 100
|
||||
const w = pingWeight / 100
|
||||
return Math.round(w * pingScore + (1 - w) * speedScore)
|
||||
}
|
||||
|
||||
@@ -160,6 +188,45 @@ function syntheticWans(home: OptimizerApiServer): WanUplink[] {
|
||||
}]
|
||||
}
|
||||
|
||||
function normalizeApiWanUplinks(list: OptimizerApiServer["wanUplinks"]): WanUplink[] {
|
||||
const src = Array.isArray(list) ? list : []
|
||||
const out = src
|
||||
.map((w, idx) => ({
|
||||
id: String(w.id || `api-w-${idx + 1}`),
|
||||
name: String(w.name || `WAN${idx + 1}`),
|
||||
isp: String(w.isp || "—"),
|
||||
iface: String(w.iface || "").trim(),
|
||||
ip: String(w.ip || ""),
|
||||
maxDl: Math.max(1, Math.round(Number(w.maxDl) || 1000)),
|
||||
maxUl: Math.max(1, Math.round(Number(w.maxUl) || 1000)),
|
||||
}))
|
||||
.filter((w) => w.iface.length > 0)
|
||||
return out
|
||||
}
|
||||
|
||||
function catalogWansForHome(homeId: string, homeHost?: string, homeName?: string): WanUplink[] | null {
|
||||
const hostNorm = (homeHost ?? "").trim().toLowerCase()
|
||||
const nameNorm = (homeName ?? "").trim().toLowerCase()
|
||||
const row = servers.find((s) => {
|
||||
if (s.type !== "home-router") return false
|
||||
if (String(s.id) === homeId) return true
|
||||
if (hostNorm && String(s.host ?? "").trim().toLowerCase() === hostNorm) return true
|
||||
if (nameNorm && String(s.name ?? "").trim().toLowerCase() === nameNorm) return true
|
||||
return false
|
||||
})
|
||||
const list = row?.wanUplinks ?? []
|
||||
if (!list.length) return null
|
||||
return list.map((w, idx) => ({
|
||||
id: w.id || `w-${homeId}-${idx + 1}`,
|
||||
name: w.name || `WAN${idx + 1}`,
|
||||
isp: w.isp || "—",
|
||||
iface: w.iface || "",
|
||||
ip: w.ip || "",
|
||||
maxDl: Math.max(1, Math.round(Number(w.maxDl) || 1000)),
|
||||
maxUl: Math.max(1, Math.round(Number(w.maxUl) || 1000)),
|
||||
}))
|
||||
}
|
||||
|
||||
function legPing(
|
||||
a: number | null,
|
||||
b: number | null,
|
||||
@@ -202,7 +269,10 @@ export function mapApiServersToTopology(rows: OptimizerApiServer[]): {
|
||||
country: s.country || "UN",
|
||||
model: s.model ?? "—",
|
||||
ip: s.host,
|
||||
wans: syntheticWans(s),
|
||||
wans:
|
||||
normalizeApiWanUplinks(s.wanUplinks).length > 0
|
||||
? normalizeApiWanUplinks(s.wanUplinks)
|
||||
: (catalogWansForHome(String(s.id), s.host, s.name) ?? syntheticWans(s)),
|
||||
}))
|
||||
|
||||
const jumpHosts: JumpHost[] = rows
|
||||
@@ -236,6 +306,7 @@ export function buildLiveOptimizerData(
|
||||
rows: OptimizerApiServer[],
|
||||
rulesets: FiltersRulesetRow[] | null,
|
||||
settings: OptimizerSettings,
|
||||
speedProbes: RouteOptimizerSpeedProbe[] = [],
|
||||
): OptimizerData {
|
||||
const { homes, jumpHosts, exitNodes, byId } = mapApiServersToTopology(rows)
|
||||
const pw = settings.pingWeight
|
||||
@@ -255,13 +326,142 @@ export function buildLiveOptimizerData(
|
||||
const homeLat = homeRow?.latency ?? null
|
||||
|
||||
const wanJhLegs: WanJhLeg[] = []
|
||||
for (const wan of home.wans) {
|
||||
for (const jh of jumpHosts) {
|
||||
const probesForHome = speedProbes
|
||||
.filter((p) => p.enabled !== false && p.srcServerId === home.id)
|
||||
|
||||
function scoreProbeForWanIface(p: RouteOptimizerSpeedProbe, wanIface: string): number {
|
||||
const want = wanIface.trim().toLowerCase()
|
||||
const got = String(p.srcInterface ?? "").trim().toLowerCase()
|
||||
if (got && want && got !== want) return -1
|
||||
const ifaceScore =
|
||||
got && want && got === want ? 100
|
||||
: (!got && want) ? 35
|
||||
: (!want && got) ? 15
|
||||
: 5
|
||||
const freshness =
|
||||
p.lastPingAt ? Math.max(0, 20 - Math.floor((Date.now() - Date.parse(p.lastPingAt)) / (60 * 60 * 1000))) : 0
|
||||
return ifaceScore
|
||||
+ (p.lastPingRttMs != null ? 20 : 0)
|
||||
+ (p.lastTxAvgMbps != null ? 10 : 0)
|
||||
+ (p.lastRxAvgMbps != null ? 10 : 0)
|
||||
+ freshness
|
||||
}
|
||||
|
||||
function assignProbesForWanJh(jhId: string): Map<string, RouteOptimizerSpeedProbe | undefined> {
|
||||
const out = new Map<string, RouteOptimizerSpeedProbe | undefined>()
|
||||
const pool = probesForHome.filter((p) => p.dstServerId === jhId)
|
||||
if (!pool.length) {
|
||||
for (const wan of home.wans) out.set(wan.id, undefined)
|
||||
return out
|
||||
}
|
||||
|
||||
let bestSum = -Infinity
|
||||
let bestAssign: Map<string, RouteOptimizerSpeedProbe> | null = null
|
||||
const wanList = [...home.wans]
|
||||
|
||||
function dfs(i: number, used: Set<string>, cur: Map<string, RouteOptimizerSpeedProbe>, sum: number) {
|
||||
if (i === wanList.length) {
|
||||
if (sum > bestSum) {
|
||||
bestSum = sum
|
||||
bestAssign = new Map(cur)
|
||||
}
|
||||
return
|
||||
}
|
||||
const wan = wanList[i]!
|
||||
let picked = false
|
||||
for (const p of pool) {
|
||||
if (used.has(p.id)) continue
|
||||
const sc = scoreProbeForWanIface(p, wan.iface)
|
||||
if (sc < 0) continue
|
||||
picked = true
|
||||
used.add(p.id)
|
||||
cur.set(wan.id, p)
|
||||
dfs(i + 1, used, cur, sum + sc)
|
||||
cur.delete(wan.id)
|
||||
used.delete(p.id)
|
||||
}
|
||||
if (!picked) dfs(i + 1, used, cur, sum)
|
||||
}
|
||||
|
||||
if (wanList.length <= 7 && wanList.length <= pool.length) {
|
||||
dfs(0, new Set(), new Map(), 0)
|
||||
}
|
||||
|
||||
if (bestAssign) {
|
||||
for (const wan of home.wans) out.set(wan.id, bestAssign.get(wan.id))
|
||||
return out
|
||||
}
|
||||
|
||||
const used = new Set<string>()
|
||||
for (const wan of home.wans) {
|
||||
let best: RouteOptimizerSpeedProbe | undefined
|
||||
let bestScore = -1
|
||||
for (const p of pool) {
|
||||
if (used.has(p.id)) continue
|
||||
const sc = scoreProbeForWanIface(p, wan.iface)
|
||||
if (sc > bestScore) {
|
||||
bestScore = sc
|
||||
best = p
|
||||
}
|
||||
}
|
||||
if (best && bestScore >= 0) {
|
||||
out.set(wan.id, best)
|
||||
used.add(best.id)
|
||||
} else {
|
||||
out.set(wan.id, undefined)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function pickProbeForJhExit(jhId: string, exitId: string): RouteOptimizerSpeedProbe | undefined {
|
||||
const pool = speedProbes.filter((p) => {
|
||||
if (p.enabled === false) return false
|
||||
const a = p.srcServerId === jhId && p.dstServerId === exitId
|
||||
const b = p.srcServerId === exitId && p.dstServerId === jhId
|
||||
return a || b
|
||||
})
|
||||
if (!pool.length) return undefined
|
||||
let best: RouteOptimizerSpeedProbe | undefined
|
||||
let bestScore = -1
|
||||
for (const p of pool) {
|
||||
const forward = p.srcServerId === jhId && p.dstServerId === exitId
|
||||
const freshness =
|
||||
p.lastPingAt ? Math.max(0, 20 - Math.floor((Date.now() - Date.parse(p.lastPingAt)) / (60 * 60 * 1000))) : 0
|
||||
const score =
|
||||
(forward ? 5 : 0)
|
||||
+ (p.lastPingRttMs != null ? 25 : 0)
|
||||
+ (p.lastTxAvgMbps != null ? 12 : 0)
|
||||
+ (p.lastRxAvgMbps != null ? 12 : 0)
|
||||
+ freshness
|
||||
if (score > bestScore) {
|
||||
bestScore = score
|
||||
best = p
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
for (const jh of jumpHosts) {
|
||||
const assignedByWanId = assignProbesForWanJh(jh.id)
|
||||
for (const wan of home.wans) {
|
||||
const jhRow = byId.get(jh.id)
|
||||
const jhOnline = jhRow?.status === "online"
|
||||
const ping = legPing(homeLat, jhRow?.latency ?? null, homeOnline, !!jhOnline)
|
||||
const { dl, ul } = legBandwidthMbps(ping, wan.maxDl, wan.maxUl)
|
||||
const loss = !homeOnline || !jhOnline ? 100 : 0
|
||||
const probe = assignedByWanId.get(wan.id)
|
||||
const modelPing = legPing(homeLat, jhRow?.latency ?? null, homeOnline, !!jhOnline)
|
||||
const ping = probe?.lastPingRttMs != null
|
||||
? Math.max(1, Math.round(probe.lastPingRttMs))
|
||||
: modelPing
|
||||
const modelBw = legBandwidthMbps(ping, wan.maxDl, wan.maxUl)
|
||||
const dl = probe?.lastTxAvgMbps != null
|
||||
? Math.max(1, Math.min(wan.maxDl, Math.round(probe.lastTxAvgMbps)))
|
||||
: modelBw.dl
|
||||
const ul = probe?.lastRxAvgMbps != null
|
||||
? Math.max(1, Math.min(wan.maxUl, Math.round(probe.lastRxAvgMbps)))
|
||||
: modelBw.ul
|
||||
const loss = probe?.lastPingLossPct != null
|
||||
? Math.max(0, Math.min(100, Math.round(probe.lastPingLossPct)))
|
||||
: (!homeOnline || !jhOnline ? 100 : 0)
|
||||
wanJhLegs.push({
|
||||
wanId: wan.id,
|
||||
jhId: jh.id,
|
||||
@@ -269,7 +469,7 @@ export function buildLiveOptimizerData(
|
||||
dlMbps: dl,
|
||||
ulMbps: ul,
|
||||
loss,
|
||||
score: calcScore(ping, dl, ul, pw),
|
||||
score: calcRouteScore(ping, dl, ul, pw),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -280,8 +480,18 @@ export function buildLiveOptimizerData(
|
||||
const jhRow = byId.get(jh.id)
|
||||
const exRow = byId.get(ex.id)
|
||||
const ok = jhRow?.status === "online" && exRow?.status === "online"
|
||||
const ping = legPing(jhRow?.latency ?? null, exRow?.latency ?? null, !!ok, !!ok)
|
||||
const { dl, ul } = legBandwidthMbps(ping, 1000, 1000)
|
||||
const probe = pickProbeForJhExit(jh.id, ex.id)
|
||||
const modelPing = legPing(jhRow?.latency ?? null, exRow?.latency ?? null, !!ok, !!ok)
|
||||
const ping = probe?.lastPingRttMs != null
|
||||
? Math.max(1, Math.round(probe.lastPingRttMs))
|
||||
: modelPing
|
||||
const modelBw = legBandwidthMbps(ping, 1000, 1000)
|
||||
const dl = probe?.lastTxAvgMbps != null
|
||||
? Math.max(1, Math.round(probe.lastTxAvgMbps))
|
||||
: modelBw.dl
|
||||
const ul = probe?.lastRxAvgMbps != null
|
||||
? Math.max(1, Math.round(probe.lastRxAvgMbps))
|
||||
: modelBw.ul
|
||||
jhExMap.set(`${jh.id}::${ex.id}`, {
|
||||
jhId: jh.id,
|
||||
exitId: ex.id,
|
||||
@@ -302,7 +512,7 @@ export function buildLiveOptimizerData(
|
||||
const totalPing = hw.pingMs + je.pingMs
|
||||
const dl = Math.min(hw.dlMbps, je.dlMbps)
|
||||
const ul = Math.min(hw.ulMbps, je.ulMbps)
|
||||
const score = calcScore(totalPing, dl, ul, pw)
|
||||
const score = calcRouteScore(totalPing, dl, ul, pw)
|
||||
fullRoutes.push({
|
||||
id: `${home.id}-${wan.id}-${jh.id}-${ex.id}`,
|
||||
homeId: home.id,
|
||||
|
||||
Reference in New Issue
Block a user