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 <cursoragent@cursor.com>
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
+159 -20
View File
@@ -27,12 +27,17 @@ import {
import {
collectGreEndpointHostnames,
greResolvedMapFromApi,
isIpv4String,
normalizeGreEndpointAddr,
} from "@/lib/gre-endpoint-resolve"
import {
assignSpeedProbesToGreTunnels,
assignSpeedProbesToWanJhEdges,
canonicalGreServerPairKey,
greOuterSummaryLine,
ifaceNameForGreOuterIp,
mergeGreMetricsWithSpeedProbe,
wanJhEdgeMapKey,
type GreSpeedProbeSnapshot,
} from "@/lib/map-gre-speed-probe"
import { Button } from "@/components/ui/button"
@@ -318,6 +323,12 @@ function edgeBadgePosition(
type FilterKey = "all" | "online" | "degraded" | "offline" | "jump-host" | "exit-node" | "home-router"
function greEdgeKey(e: GreMapEdge): string {
const la = (e.tunnel.localAddress ?? "").trim()
const ra = (e.tunnel.remoteAddress ?? "").trim()
return `${e.fromServer.id}|${e.toServer.id}|${e.tunnel.id}|${la}|${ra}`
}
// ─── SVG sub-components ───────────────────────────────────────────────────────
/** Бейдж на ребре WAN → JumpHost (не GRE): сумма latency домашнего узла и JH — те же поля, что в разделе Серверы. */
@@ -697,7 +708,7 @@ function Minimap({ pan, zoom, nodes, greEdges, satPos, wanJhEdges, homeRouters,
<rect width={W} height={H} fill="#060d1a" />
{/* simplified edges */}
{greEdges.map((e) => (
<line key={e.tunnel.id} x1={e.from.x} y1={e.from.y} x2={e.to.x} y2={e.to.y}
<line key={greEdgeKey(e)} x1={e.from.x} y1={e.from.y} x2={e.to.x} y2={e.to.y}
stroke={TUNNEL_STYLE[e.tunnel.status].stroke} strokeWidth="5" opacity="0.25" />
))}
{/* WAN edges */}
@@ -831,7 +842,18 @@ export default function NetworkMapPage() {
lastPingRttMs: p.lastPingRttMs,
})),
)
const hostnames = collectGreEndpointHostnames(mappedTunnels)
const hostSet = new Set<string>(collectGreEndpointHostnames(mappedTunnels))
for (const row of rows) {
if (!Array.isArray(row.wanUplinks)) continue
for (const w of row.wanUplinks) {
const raw = (w.ip ?? "").trim()
if (!raw) continue
const n = normalizeGreEndpointAddr(raw)
if (!n || n === "0.0.0.0") continue
if (!isIpv4String(n)) hostSet.add(n.toLowerCase())
}
}
const hostnames = [...hostSet]
if (hostnames.length > 0) {
try {
const res = await apiFetch<{ results: Record<string, string | null> }>(
@@ -900,6 +922,10 @@ export default function NetworkMapPage() {
try {
const k = "mm-network-map-layout-rev"
if (sessionStorage.getItem(k) !== String(NETWORK_MAP_LAYOUT_REVISION)) {
// Важно: без явного сброса старые drag-координаты остаются в state
// и перекрывают новый авто-лейаут на live-данных.
setNodePositions({})
setSatPositions({})
sessionStorage.setItem(k, String(NETWORK_MAP_LAYOUT_REVISION))
}
} catch {
@@ -957,12 +983,21 @@ export default function NetworkMapPage() {
[mapGreTunnels, mapServers, nodePosById, effectiveSatPos, greResolvedMap],
)
/** WAN→JH: не показывать PingBadge, если на том же дом+WAN+JH уже есть GRE с метрик-бейджем. */
/** Speed-пробы на сегментах WANJH (тот же формат бейджа, что на GRE JH↔EN). */
const speedProbeByWanJhKey = useMemo(
() => assignSpeedProbesToWanJhEdges(wanJhEdges, mapServers, speedProbes),
[wanJhEdges, mapServers, speedProbes],
)
/**
* WAN→JH: не дублировать линию спутник→пир, если уже есть GRE с того же WAN к тому же узлу
* (JH или exit — иначе «второй» бейдж speed посередине сегмента).
*/
const suppressWanJhPingBadge = useMemo(() => {
const s = new Set<string>()
for (const g of greEdges) {
if (g.fromServer.type !== "home-router") continue
if (g.toServer.type !== "jump-host") continue
if (g.toServer.type === "home-router") continue
const widx = greSourceWanIndexOnMap(g.fromServer, g.tunnel, greResolvedMap)
if (widx == null) continue
s.add(`${g.fromServer.id}\t${widx}\t${g.toServer.id}`)
@@ -986,7 +1021,7 @@ export default function NetworkMapPage() {
() =>
assignSpeedProbesToGreTunnels(
greEdges.map((e) => ({
tunnel: e.tunnel,
tunnel: { ...e.tunnel, id: greEdgeKey(e) },
fromServer: e.fromServer,
toServer: e.toServer,
})),
@@ -996,14 +1031,77 @@ export default function NetworkMapPage() {
[greEdges, speedProbes, greResolvedMap],
)
/** WAN→JH: не показывать GreEdgeMetricBadge со speed, если те же метрики уже на GRE (один бейдж на сегмент). */
const suppressWanJhSpeedBadgeDuplicate = useMemo(() => {
const s = new Set<string>()
for (const g of greEdges) {
if (g.fromServer.type !== "home-router") continue
if (g.toServer.type === "home-router") continue
const widx = greSourceWanIndexOnMap(g.fromServer, g.tunnel, greResolvedMap)
if (widx == null) continue
const spGre = speedProbeByTunnelId.get(greEdgeKey(g))
const merged = mergeGreMetricsWithSpeedProbe(spGre, greTunnelProbe(g.tunnel))
if (!merged.hasSpeedMonitor) continue
s.add(`${g.fromServer.id}\t${widx}\t${g.toServer.id}`)
}
return s
}, [greEdges, speedProbeByTunnelId, greResolvedMap])
/** WAN→JH: скрываем саму линию, если тот же отрезок уже отрисован GRE (одна связь на карте). */
const suppressWanJhLineDuplicate = useMemo(() => {
const s = new Set<string>()
for (const g of greEdges) {
if (g.fromServer.type !== "home-router") continue
if (g.toServer.type === "home-router") continue
const widx = greSourceWanIndexOnMap(g.fromServer, g.tunnel, greResolvedMap)
if (widx == null) continue
s.add(`${g.fromServer.id}\t${widx}\t${g.toServer.id}`)
}
return s
}, [greEdges, greResolvedMap])
const visibleWanJhEdges = useMemo(
() => wanJhEdges.filter((e) => !suppressWanJhLineDuplicate.has(wanJhEdgeMapKey(e))),
[wanJhEdges, suppressWanJhLineDuplicate],
)
/** Один бейдж на совпадающий отрезок карты (два туннеля в БД HR→MSK и MSK→HR — одна линия). */
const greTunnelIdsWithMapBadge = useMemo(() => {
const edgeByTunnel = new Map(greEdges.map((e) => [greEdgeKey(e), e] as const))
const geomKey = (e: GreMapEdge) => {
const a = `${Math.round(e.from.x)},${Math.round(e.from.y)}`
const b = `${Math.round(e.to.x)},${Math.round(e.to.y)}`
return a <= b ? `${a}|${b}` : `${b}|${a}`
}
const rank = (tid: string): number => {
const e = edgeByTunnel.get(tid)
if (!e) return -1
const sp = speedProbeByTunnelId.get(tid)
const m = mergeGreMetricsWithSpeedProbe(sp, greTunnelProbe(e.tunnel))
let r = m.hasSpeedMonitor ? 1000 : 0
r += m.pingMs ?? 0
return r
}
const winners = new Map<string, string>()
for (const e of greEdges) {
const key = geomKey(e)
const cur = winners.get(key)
const edgeId = greEdgeKey(e)
if (!cur || rank(edgeId) > rank(cur) || (rank(edgeId) === rank(cur) && edgeId.localeCompare(cur) < 0)) {
winners.set(key, edgeId)
}
}
return new Set(winners.values())
}, [greEdges, speedProbeByTunnelId])
/** Несколько GRE между одной парой узлов — смещаем бейджи по дуге и по нормали, без наложения. */
const greBadgeStaggerByTunnelId = useMemo(() => {
const pairOrder = new Map<string, number>()
const out = new Map<string, number>()
for (const ge of greEdges) {
const k = `${ge.fromServer.id}:${ge.toServer.id}`
const k = canonicalGreServerPairKey(ge.fromServer, ge.toServer)
const lane = pairOrder.get(k) ?? 0
out.set(ge.tunnel.id, lane)
out.set(greEdgeKey(ge), lane)
pairOrder.set(k, lane + 1)
}
return out
@@ -1216,6 +1314,16 @@ export default function NetworkMapPage() {
setSelWanIdx(prev => prev === wanIdx && selected?.id === s.id ? null : wanIdx)
}
function openWanJhSpeedDetail(ev: React.MouseEvent<SVGElement>, edge: WanJhEdge) {
ev.stopPropagation()
const home = mapServers.find((s) => s.id === edge.homeId)
if (!home) return
setSelectedGreEdge(null)
setSelected(home)
setSelWanIdx(edge.wanIdx)
setHoveredId(null)
}
const connectedTunnels = selected
? mapGreTunnels.filter((t) => {
const peer = findServerByGreRemote(mapServers, t.remoteAddress, greResolvedMap)
@@ -1410,6 +1518,7 @@ export default function NetworkMapPage() {
{/* ── GRE edges ── */}
{greEdges.map((e, i) => {
const edgeId = greEdgeKey(e)
const ts = TUNNEL_STYLE[e.tunnel.status]
const fromN = nodeById[e.fromServer.id]
const toN = nodeById[e.toServer.id]
@@ -1419,12 +1528,12 @@ export default function NetworkMapPage() {
!isVisible(fromN) &&
!isVisible(toN)
const baseProbe = greTunnelProbe(e.tunnel)
const spGre = speedProbeByTunnelId.get(e.tunnel.id)
const spGre = speedProbeByTunnelId.get(edgeId)
const merged = mergeGreMetricsWithSpeedProbe(spGre, baseProbe)
const rttFromMonitor = spGre?.lastPingRttMs != null
const throughputFromMonitor =
spGre?.lastTxAvgMbps != null || spGre?.lastRxAvgMbps != null
const badgeLane = greBadgeStaggerByTunnelId.get(e.tunnel.id) ?? 0
const badgeLane = greBadgeStaggerByTunnelId.get(edgeId) ?? 0
const tBadge = Math.min(0.78, Math.max(0.22, 0.34 + badgeLane * 0.052))
const normalPx =
(badgeLane % 2 === 0 ? 1 : -1) * (16 + badgeLane * 22)
@@ -1443,7 +1552,7 @@ export default function NetworkMapPage() {
setSelWanIdx(null)
}
return (
<g key={e.tunnel.id} opacity={dimmed ? 0.05 : 1} style={{ transition: "opacity 0.3s" }}>
<g key={edgeId} opacity={dimmed ? 0.05 : 1} style={{ transition: "opacity 0.3s" }}>
<line
x1={e.from.x} y1={e.from.y} x2={e.to.x} y2={e.to.y}
stroke={ts.stroke} strokeWidth="1.5"
@@ -1467,7 +1576,7 @@ export default function NetworkMapPage() {
path={`M ${e.from.x} ${e.from.y} L ${e.to.x} ${e.to.y}`} />
</circle>
)}
{showPingBadges && (
{showPingBadges && greTunnelIdsWithMapBadge.has(edgeId) && (
<GreEdgeMetricBadge
mx={mx}
my={my}
@@ -1503,7 +1612,7 @@ export default function NetworkMapPage() {
})}
{/* ── WAN→JH edges ── */}
{wanJhEdges.map((edge, i) => {
{visibleWanJhEdges.map((edge, i) => {
const jh = nodeById[edge.jhId]
const satPos = effectiveSatPos[edge.homeId]?.[edge.wanIdx]
if (!jh || !satPos) return null
@@ -1528,10 +1637,35 @@ export default function NetworkMapPage() {
path={`M ${satPos.x} ${satPos.y} L ${jh.x} ${jh.y}`} />
</circle>
)}
{showPingBadges &&
!suppressWanJhPingBadge.has(`${edge.homeId}\t${edge.wanIdx}\t${edge.jhId}`) && (
<PingBadge mx={mx} my={my} ping={edge.pingMs} color={pingColor(edge.pingMs)} />
)}
{showPingBadges && (() => {
const kj = wanJhEdgeMapKey(edge)
const sp = speedProbeByWanJhKey.get(kj)
const merged = mergeGreMetricsWithSpeedProbe(sp, {
pingMs: edge.pingMs,
dlMbps: null,
ulMbps: null,
})
const hasMonitor = merged.hasSpeedMonitor
if (!hasMonitor && suppressWanJhPingBadge.has(kj)) return null
if (hasMonitor && suppressWanJhSpeedBadgeDuplicate.has(kj)) return null
if (hasMonitor) {
return (
<GreEdgeMetricBadge
mx={mx}
my={my}
pingMs={merged.pingMs}
dl={merged.dlMbps}
ul={merged.ulMbps}
onOpen={(ev) => openWanJhSpeedDetail(ev, edge)}
rttFromMonitor={sp?.lastPingRttMs != null}
throughputFromMonitor={
sp?.lastTxAvgMbps != null || sp?.lastRxAvgMbps != null
}
/>
)
}
return <PingBadge mx={mx} my={my} ping={edge.pingMs} color={pingColor(edge.pingMs)} />
})()}
</g>
)
})}
@@ -1677,7 +1811,7 @@ export default function NetworkMapPage() {
nodes={nodes}
greEdges={greEdges}
satPos={effectiveSatPos}
wanJhEdges={wanJhEdges}
wanJhEdges={visibleWanJhEdges}
homeRouters={homeRouters}
onClose={() => setShowMinimap(false)}
onPan={(x, y) => setPan({ x, y })}
@@ -1743,10 +1877,11 @@ export default function NetworkMapPage() {
{(() => {
const edge = selectedGreEdge
const t = edge.tunnel
const selectedEdgeId = greEdgeKey(edge)
const localWanName = ifaceNameForGreOuterIp(edge.fromServer, t.localAddress, greResolvedMap)
const remoteWanName = ifaceNameForGreOuterIp(edge.toServer, t.remoteAddress, greResolvedMap)
const baseProbe = greTunnelProbe(t)
const spGr = speedProbeByTunnelId.get(t.id)
const spGr = speedProbeByTunnelId.get(selectedEdgeId)
const merged = mergeGreMetricsWithSpeedProbe(spGr, baseProbe)
const rttMon = spGr?.lastPingRttMs != null
const bwMon = spGr?.lastTxAvgMbps != null || spGr?.lastRxAvgMbps != null
@@ -2039,16 +2174,20 @@ export default function NetworkMapPage() {
: mapServers.find((s) => s.id === t.serverId)
const fromServer = mapServers.find((s) => s.id === t.serverId)
const toServer = findServerByGreRemote(mapServers, t.remoteAddress, greResolvedMap)
const tunnelPanelKey =
fromServer && toServer
? `${fromServer.id}|${toServer.id}|${t.id}|${t.localAddress}|${t.remoteAddress}`
: `${t.serverId}|${t.id}|${t.localAddress}|${t.remoteAddress}`
const baseProbe = greTunnelProbe(t)
const spGre =
fromServer && toServer ? speedProbeByTunnelId.get(t.id) : undefined
fromServer && toServer ? speedProbeByTunnelId.get(tunnelPanelKey) : undefined
const merged = mergeGreMetricsWithSpeedProbe(spGre, baseProbe)
const pc = pingColor(merged.pingMs)
const showMetrics =
merged.pingMs != null ||
(merged.dlMbps != null && merged.ulMbps != null)
return (
<div key={t.id} className="rounded-md border border-border/60 px-3 py-2 bg-muted/20">
<div key={tunnelPanelKey} className="rounded-md border border-border/60 px-3 py-2 bg-muted/20">
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-mono font-medium">{t.name}</span>
<span className="text-[10px] font-medium" style={{ color: ts.stroke }}>