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:
+159
-20
@@ -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-пробы на сегментах WAN→JH (тот же формат бейджа, что на 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 }}>
|
||||
|
||||
Reference in New Issue
Block a user