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:
Denozordec
2026-05-07 12:29:04 +07:00
co-authored by Cursor
parent bdb9b72fac
commit 5f31bb47fb
81 changed files with 11976 additions and 1239 deletions
+223 -13
View File
@@ -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,