fix(ui): убрать выдуманные измерения из карты сети
This commit is contained in:
1 parent
329d6b82f7
commit
e47e2b4029
7 files changed
+100
-37
No files matched your search
@@ -94,7 +94,7 @@ function mapBackendRowToServer(s: BackendServerRow): Server {
|
|||||||
os: s.os ?? "—",
|
os: s.os ?? "—",
|
||||||
enabled: s.enabled,
|
enabled: s.enabled,
|
||||||
status: s.status ?? "unknown",
|
status: s.status ?? "unknown",
|
||||||
latency: s.latency != null ? Math.round(s.latency) : null,
|
latency: s.latency ?? null,
|
||||||
sessions: s.sessions ?? 0,
|
sessions: s.sessions ?? 0,
|
||||||
comment: s.comment || undefined,
|
comment: s.comment || undefined,
|
||||||
lanSubnet: s.lanSubnet || undefined,
|
lanSubnet: s.lanSubnet || undefined,
|
||||||
@@ -157,11 +157,13 @@ function makeApiFetch(backendUrl: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtMB(mb: number): string {
|
function fmtMB(mb: number | null): string {
|
||||||
|
if(mb==null)return "—"
|
||||||
if (mb >= 1024) return `${(mb / 1024).toFixed(mb >= 10240 ? 0 : 1)} ГБ`
|
if (mb >= 1024) return `${(mb / 1024).toFixed(mb >= 10240 ? 0 : 1)} ГБ`
|
||||||
return `${mb} МБ`
|
return Number(mb.toFixed(2))+" МБ"
|
||||||
}
|
}
|
||||||
function fmtUptime(sec: number): string {
|
function fmtUptime(sec: number | null): string {
|
||||||
|
if(sec==null)return "—"
|
||||||
const d = Math.floor(sec / 86400)
|
const d = Math.floor(sec / 86400)
|
||||||
const h = Math.floor((sec % 86400) / 3600)
|
const h = Math.floor((sec % 86400) / 3600)
|
||||||
const m = Math.floor((sec % 3600) / 60)
|
const m = Math.floor((sec % 3600) / 60)
|
||||||
@@ -174,7 +176,8 @@ function resBarColor(pct: number): string {
|
|||||||
if (pct >= 70) return "bg-amber-500"
|
if (pct >= 70) return "bg-amber-500"
|
||||||
return "bg-emerald-500"
|
return "bg-emerald-500"
|
||||||
}
|
}
|
||||||
function resPctColor(pct: number): string {
|
function resPctColor(pct: number | null): string {
|
||||||
|
if(pct==null)return "text-muted-foreground"
|
||||||
if (pct >= 85) return "text-red-400"
|
if (pct >= 85) return "text-red-400"
|
||||||
if (pct >= 70) return "text-amber-400"
|
if (pct >= 70) return "text-amber-400"
|
||||||
return "text-emerald-400"
|
return "text-emerald-400"
|
||||||
@@ -427,7 +430,7 @@ function drawableServiceEdges(
|
|||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function pingColor(ms: number | null) {
|
function pingColor(ms: number | null) {
|
||||||
if (ms == null) return "#f87171"
|
if (ms == null) return "var(--muted-foreground)"
|
||||||
if (ms < 15) return "#4ade80"
|
if (ms < 15) return "#4ade80"
|
||||||
if (ms < 50) return "#fbbf24"
|
if (ms < 50) return "#fbbf24"
|
||||||
return "#fb923c"
|
return "#fb923c"
|
||||||
@@ -1270,7 +1273,7 @@ function NetworkMapContent() {
|
|||||||
[mapServers, mapGreTunnels, greResolvedMap],
|
[mapServers, mapGreTunnels, greResolvedMap],
|
||||||
)
|
)
|
||||||
const wanJhEdges = useMemo(() => buildWanJhEdges(mapServers), [mapServers])
|
const wanJhEdges = useMemo(() => buildWanJhEdges(mapServers), [mapServers])
|
||||||
const srvResMap = useMemo(() => buildServerResourceMap(mapServers), [mapServers])
|
const srvResMap = useMemo(() => buildServerResourceMap(mapServers, !useLiveData), [mapServers,useLiveData])
|
||||||
const homeRouters = useMemo(
|
const homeRouters = useMemo(
|
||||||
() => mapServers.filter((s) => s.type === "home-router"),
|
() => mapServers.filter((s) => s.type === "home-router"),
|
||||||
[mapServers],
|
[mapServers],
|
||||||
@@ -1522,12 +1525,12 @@ function NetworkMapContent() {
|
|||||||
const widx = greSourceWanIndexOnMap(g.fromServer, g.tunnel, greResolvedMap)
|
const widx = greSourceWanIndexOnMap(g.fromServer, g.tunnel, greResolvedMap)
|
||||||
if (widx == null) continue
|
if (widx == null) continue
|
||||||
const spGre = speedProbeByTunnelId.get(greEdgeKey(g))
|
const spGre = speedProbeByTunnelId.get(greEdgeKey(g))
|
||||||
const merged = mergeGreMetricsWithSpeedProbe(spGre, greTunnelProbe(g.tunnel))
|
const merged = mergeGreMetricsWithSpeedProbe(spGre, greTunnelProbe(g.tunnel,!useLiveData))
|
||||||
if (!merged.hasSpeedMonitor) continue
|
if (!merged.hasSpeedMonitor) continue
|
||||||
s.add(`${g.fromServer.id}\t${widx}\t${g.toServer.id}`)
|
s.add(`${g.fromServer.id}\t${widx}\t${g.toServer.id}`)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}, [greEdges, speedProbeByTunnelId, greResolvedMap])
|
}, [greEdges, speedProbeByTunnelId, greResolvedMap,useLiveData])
|
||||||
|
|
||||||
/** WAN→JH: скрываем саму линию, если тот же отрезок уже отрисован GRE (одна связь на карте). */
|
/** WAN→JH: скрываем саму линию, если тот же отрезок уже отрисован GRE (одна связь на карте). */
|
||||||
const suppressWanJhLineDuplicate = useMemo(() => {
|
const suppressWanJhLineDuplicate = useMemo(() => {
|
||||||
@@ -1559,7 +1562,7 @@ function NetworkMapContent() {
|
|||||||
const e = edgeByTunnel.get(tid)
|
const e = edgeByTunnel.get(tid)
|
||||||
if (!e) return -1
|
if (!e) return -1
|
||||||
const sp = speedProbeByTunnelId.get(tid)
|
const sp = speedProbeByTunnelId.get(tid)
|
||||||
const m = mergeGreMetricsWithSpeedProbe(sp, greTunnelProbe(e.tunnel))
|
const m = mergeGreMetricsWithSpeedProbe(sp, greTunnelProbe(e.tunnel,!useLiveData))
|
||||||
let r = m.hasSpeedMonitor ? 1000 : 0
|
let r = m.hasSpeedMonitor ? 1000 : 0
|
||||||
r += m.pingMs ?? 0
|
r += m.pingMs ?? 0
|
||||||
return r
|
return r
|
||||||
@@ -1574,7 +1577,7 @@ function NetworkMapContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Set(winners.values())
|
return new Set(winners.values())
|
||||||
}, [greEdges, speedProbeByTunnelId])
|
}, [greEdges, speedProbeByTunnelId,useLiveData])
|
||||||
|
|
||||||
/** Несколько GRE между одной парой узлов — смещаем бейджи по дуге и по нормали, без наложения. */
|
/** Несколько GRE между одной парой узлов — смещаем бейджи по дуге и по нормали, без наложения. */
|
||||||
const greBadgeStaggerByTunnelId = useMemo(() => {
|
const greBadgeStaggerByTunnelId = useMemo(() => {
|
||||||
@@ -2235,7 +2238,7 @@ function NetworkMapContent() {
|
|||||||
fromN && toN &&
|
fromN && toN &&
|
||||||
!isVisible(fromN) &&
|
!isVisible(fromN) &&
|
||||||
!isVisible(toN)
|
!isVisible(toN)
|
||||||
const baseProbe = greTunnelProbe(e.tunnel)
|
const baseProbe = greTunnelProbe(e.tunnel,!useLiveData)
|
||||||
const spGre = speedProbeByTunnelId.get(edgeId)
|
const spGre = speedProbeByTunnelId.get(edgeId)
|
||||||
const merged = mergeGreMetricsWithSpeedProbe(spGre, baseProbe)
|
const merged = mergeGreMetricsWithSpeedProbe(spGre, baseProbe)
|
||||||
const rttFromMonitor = spGre?.lastPingRttMs != null
|
const rttFromMonitor = spGre?.lastPingRttMs != null
|
||||||
@@ -2845,7 +2848,7 @@ function NetworkMapContent() {
|
|||||||
const selectedEdgeId = greEdgeKey(edge)
|
const selectedEdgeId = greEdgeKey(edge)
|
||||||
const localWanName = ifaceNameForGreOuterIp(edge.fromServer, t.localAddress, greResolvedMap)
|
const localWanName = ifaceNameForGreOuterIp(edge.fromServer, t.localAddress, greResolvedMap)
|
||||||
const remoteWanName = ifaceNameForGreOuterIp(edge.toServer, t.remoteAddress, greResolvedMap)
|
const remoteWanName = ifaceNameForGreOuterIp(edge.toServer, t.remoteAddress, greResolvedMap)
|
||||||
const baseProbe = greTunnelProbe(t)
|
const baseProbe = greTunnelProbe(t,!useLiveData)
|
||||||
const spGr = speedProbeByTunnelId.get(selectedEdgeId)
|
const spGr = speedProbeByTunnelId.get(selectedEdgeId)
|
||||||
const merged = mergeGreMetricsWithSpeedProbe(spGr, baseProbe)
|
const merged = mergeGreMetricsWithSpeedProbe(spGr, baseProbe)
|
||||||
const rttMon = spGr?.lastPingRttMs != null
|
const rttMon = spGr?.lastPingRttMs != null
|
||||||
@@ -3369,7 +3372,7 @@ function NetworkMapContent() {
|
|||||||
fromServer && toServer
|
fromServer && toServer
|
||||||
? `${fromServer.id}|${toServer.id}|${t.id}|${t.localAddress}|${t.remoteAddress}`
|
? `${fromServer.id}|${toServer.id}|${t.id}|${t.localAddress}|${t.remoteAddress}`
|
||||||
: `${t.serverId}|${t.id}|${t.localAddress}|${t.remoteAddress}`
|
: `${t.serverId}|${t.id}|${t.localAddress}|${t.remoteAddress}`
|
||||||
const baseProbe = greTunnelProbe(t)
|
const baseProbe = greTunnelProbe(t,!useLiveData)
|
||||||
const groupKey = peer?.id ?? (normalizeGreEndpointAddr(t.remoteAddress) || t.remoteAddress)
|
const groupKey = peer?.id ?? (normalizeGreEndpointAddr(t.remoteAddress) || t.remoteAddress)
|
||||||
const isPrimary = grePrimaryByGroupKey.get(groupKey) === t.id
|
const isPrimary = grePrimaryByGroupKey.get(groupKey) === t.id
|
||||||
const spGre =
|
const spGre =
|
||||||
@@ -3462,8 +3465,8 @@ function NetworkMapContent() {
|
|||||||
const res = srvResMap[selected.id]
|
const res = srvResMap[selected.id]
|
||||||
if (!res) return null
|
if (!res) return null
|
||||||
const cpuPct = res.cpu
|
const cpuPct = res.cpu
|
||||||
const ramPct = Math.round(res.ramUsed / res.ramTotal * 100)
|
const ramPct = res.ramUsed==null||res.ramTotal==null||res.ramTotal===0?null:Math.round(res.ramUsed/res.ramTotal*100)
|
||||||
const hddPct = Math.round(res.hddUsed / res.hddTotal * 100)
|
const hddPct = res.hddUsed==null||res.hddTotal==null||res.hddTotal===0?null:Math.round(res.hddUsed/res.hddTotal*100)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
@@ -3476,7 +3479,7 @@ function NetworkMapContent() {
|
|||||||
)}
|
)}
|
||||||
{res.fromPoll && (
|
{res.fromPoll && (
|
||||||
<p className="text-[9px] text-muted-foreground mb-3 leading-snug">
|
<p className="text-[9px] text-muted-foreground mb-3 leading-snug">
|
||||||
CPU, RAM и uptime — с последнего poll MikroTik. Диск в опросе не передаётся, шкала условная.
|
Последний poll: {res.measuredAt ? new Date(res.measuredAt).toLocaleString("ru-RU") : "время неизвестно"}. Неизвестные ресурсы — «—». История CPU и диск API не передаются.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -3485,12 +3488,12 @@ function NetworkMapContent() {
|
|||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-[10px] text-muted-foreground">CPU</span>
|
<span className="text-[10px] text-muted-foreground">CPU</span>
|
||||||
<span className={cn("text-[10px] font-mono font-semibold", resPctColor(cpuPct))}>
|
<span className={cn("text-[10px] font-mono font-semibold", resPctColor(cpuPct))}>
|
||||||
{cpuPct}%
|
{cpuPct==null?"—":cpuPct+"%"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MiniBar pct={cpuPct} />
|
{cpuPct!=null&&<MiniBar pct={cpuPct} />}
|
||||||
<Sparkline history={res.cpuHistory} />
|
{res.cpuHistory.length>1&&<Sparkline history={res.cpuHistory} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3502,7 +3505,7 @@ function NetworkMapContent() {
|
|||||||
{fmtMB(res.ramUsed)} / {fmtMB(res.ramTotal)}
|
{fmtMB(res.ramUsed)} / {fmtMB(res.ramTotal)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MiniBar pct={ramPct} />
|
{ramPct!=null&&<MiniBar pct={ramPct} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HDD (нет в GET /api/servers — всегда условная шкала) */}
|
{/* HDD (нет в GET /api/servers — всегда условная шкала) */}
|
||||||
@@ -3510,7 +3513,7 @@ function NetworkMapContent() {
|
|||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
HDD
|
HDD
|
||||||
{res.fromPoll && (
|
{!useLiveData && (
|
||||||
<span className="text-[8px] font-normal text-muted-foreground/70 ml-1">(оценка)</span>
|
<span className="text-[8px] font-normal text-muted-foreground/70 ml-1">(оценка)</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -3518,7 +3521,7 @@ function NetworkMapContent() {
|
|||||||
{fmtMB(res.hddUsed)} / {fmtMB(res.hddTotal)}
|
{fmtMB(res.hddUsed)} / {fmtMB(res.hddTotal)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MiniBar pct={hddPct} />
|
{hddPct!=null&&<MiniBar pct={hddPct} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Misc metrics */}
|
{/* Misc metrics */}
|
||||||
|
|||||||
@@ -26,11 +26,18 @@
|
|||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.46; оптимизатор принят локально, карта сети в работе |
|
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.47; оптимизатор принят локально, карта сети в работе |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 10.47 — измерения карты без синтетических live значений (2026-10-01)
|
||||||
|
|
||||||
|
- CPU/RAM/uptime читаются независимо из API, сохраняют нули и неизвестность; HDD, температура и история CPU не генерируются для live. Указано фактическое polledAt либо неизвестное время. Демо-ресурсы доступны только с явным demo=true.
|
||||||
|
- GRE fallback по id отключён по умолчанию, карта явно включает его только в mock. Общий helper также убрал выдуманный fallback из live internet path. Probe DL/UL не округляются до целых, неизвестный Ping имеет нейтральный цвет. Catalog latency сохраняет дробь; модель WAN→JH ещё требует отдельного пояснения.
|
||||||
|
- 4 unit, 19 E2E карты/дашборда и TypeScript прошли. Lint сравнен с HEAD: новых проблем нет; прежние prefer-const в layout и React правила карты будут устранены при её приёмке.
|
||||||
|
- Далее Flow Map/DNS: строгие envelopes, частичный отказ/свежесть/гонки; модельные линии, URL-контекст, доступность и адаптивность ещё впереди.
|
||||||
|
|
||||||
## Блок 10.46 — неизвестный статус карты (2026-10-01)
|
## Блок 10.46 — неизвестный статус карты (2026-10-01)
|
||||||
|
|
||||||
- Nullable API status преобразуется в unknown: нейтральный узел/бейдж, отдельные фильтр и счётчик. Неизвестность не увеличивает offline. Общий ServerStatus расширен; потребители users/optimizer передают неизвестность как null, dashboard SVG имеет нейтральный стиль.
|
- Nullable API status преобразуется в unknown: нейтральный узел/бейдж, отдельные фильтр и счётчик. Неизвестность не увеличивает offline. Общий ServerStatus расширен; потребители users/optimizer передают неизвестность как null, dashboard SVG имеет нейтральный стиль.
|
||||||
|
|||||||
@@ -243,3 +243,11 @@ Shared контракт и backend observed исходные значения/д
|
|||||||
- 104 E2E карты/дашборда/серверов/мониторинга и TypeScript прошли. Новых lint ошибок нет: карта — прежние 5 ошибок/1 warning, users — прежний set-state-in-effect, dashboard helper — прежние 2 unused warning. В серверном URL тесте был внутренний Next dev warning; сценарий завершился успешно.
|
- 104 E2E карты/дашборда/серверов/мониторинга и TypeScript прошли. Новых lint ошибок нет: карта — прежние 5 ошибок/1 warning, users — прежний set-state-in-effect, dashboard helper — прежние 2 unused warning. В серверном URL тесте был внутренний Next dev warning; сценарий завершился успешно.
|
||||||
- Далее убрать синтетические live CPU/RAM/HDD/uptime/history и GRE, затем проверить Flow/DNS и доступность карты.
|
- Далее убрать синтетические live CPU/RAM/HDD/uptime/history и GRE, затем проверить Flow/DNS и доступность карты.
|
||||||
|
|
||||||
|
|
||||||
|
## Блок 10.47 — измерения карты без синтетических live значений (2026-10-01)
|
||||||
|
|
||||||
|
- CPU/RAM/uptime читаются независимо из API, сохраняют нули и неизвестность; HDD, температура и история CPU не генерируются для live. Указано фактическое polledAt либо неизвестное время. Демо-ресурсы доступны только с явным demo=true.
|
||||||
|
- GRE fallback по id отключён по умолчанию, карта явно включает его только в mock. Общий helper также убрал выдуманный fallback из live internet path. Probe DL/UL не округляются до целых, неизвестный Ping имеет нейтральный цвет. Catalog latency сохраняет дробь; модель WAN→JH ещё требует отдельного пояснения.
|
||||||
|
- 4 unit, 19 E2E карты/дашборда и TypeScript прошли. Lint сравнен с HEAD: новых проблем нет; прежние prefer-const в layout и React правила карты будут устранены при её приёмке.
|
||||||
|
- Далее Flow Map/DNS: строгие envelopes, частичный отказ/свежесть/гонки; модельные линии, URL-контекст, доступность и адаптивность ещё впереди.
|
||||||
|
|
||||||
@@ -315,9 +315,9 @@ export function mergeGreMetricsWithSpeedProbe(
|
|||||||
(sp.lastPingRttMs != null || sp.lastTxAvgMbps != null || sp.lastRxAvgMbps != null)
|
(sp.lastPingRttMs != null || sp.lastTxAvgMbps != null || sp.lastRxAvgMbps != null)
|
||||||
const pingMs = sp?.lastPingRttMs ?? fallback.pingMs
|
const pingMs = sp?.lastPingRttMs ?? fallback.pingMs
|
||||||
const dlMbps =
|
const dlMbps =
|
||||||
sp?.lastTxAvgMbps != null ? Math.round(sp.lastTxAvgMbps) : fallback.dlMbps
|
sp?.lastTxAvgMbps != null ? sp.lastTxAvgMbps : fallback.dlMbps
|
||||||
const ulMbps =
|
const ulMbps =
|
||||||
sp?.lastRxAvgMbps != null ? Math.round(sp.lastRxAvgMbps) : fallback.ulMbps
|
sp?.lastRxAvgMbps != null ? sp.lastRxAvgMbps : fallback.ulMbps
|
||||||
return { pingMs, dlMbps, ulMbps, hasSpeedMonitor }
|
return { pingMs, dlMbps, ulMbps, hasSpeedMonitor }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-11
@@ -10,14 +10,15 @@ import {
|
|||||||
wanUplinkIpMatchesGreOuter,
|
wanUplinkIpMatchesGreOuter,
|
||||||
} from "@/lib/gre-endpoint-resolve"
|
} from "@/lib/gre-endpoint-resolve"
|
||||||
|
|
||||||
/** Визуализация метрик на GRE (нет отдельного API замеров для карты). */
|
/** Неизвестные live метрики GRE; синтетические значения доступны только при demo=true. */
|
||||||
export interface TunnelProbe {
|
export interface TunnelProbe {
|
||||||
pingMs: number | null
|
pingMs: number | null
|
||||||
dlMbps: number | null
|
dlMbps: number | null
|
||||||
ulMbps: number | null
|
ulMbps: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function greTunnelProbe(t: GreTunnel): TunnelProbe {
|
export function greTunnelProbe(t: GreTunnel, demo = false): TunnelProbe {
|
||||||
|
if (!demo) return {pingMs:null,dlMbps:null,ulMbps:null}
|
||||||
if (t.status === "down" || !t.enabled)
|
if (t.status === "down" || !t.enabled)
|
||||||
return { pingMs: null, dlMbps: null, ulMbps: null }
|
return { pingMs: null, dlMbps: null, ulMbps: null }
|
||||||
const h = t.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0)
|
const h = t.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0)
|
||||||
@@ -89,15 +90,16 @@ export interface WanJhEdge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkMapSrvRes {
|
export interface NetworkMapSrvRes {
|
||||||
cpu: number
|
cpu: number | null
|
||||||
cpuHistory: number[]
|
cpuHistory: number[]
|
||||||
ramUsed: number
|
ramUsed: number | null
|
||||||
ramTotal: number
|
ramTotal: number | null
|
||||||
hddUsed: number
|
hddUsed: number | null
|
||||||
hddTotal: number
|
hddTotal: number | null
|
||||||
uptimeSeconds: number
|
uptimeSeconds: number | null
|
||||||
temp?: number
|
temp?: number
|
||||||
/** CPU/RAM/uptime сняты с последнего снапшота опроса; HDD в API нет — по-прежнему условная шкала. */
|
/** Live значения сняты с последнего опроса; отсутствующие поля и HDD неизвестны. */
|
||||||
|
measuredAt?: string | null
|
||||||
fromPoll?: boolean
|
fromPoll?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,8 +535,16 @@ export function buildWanJhEdges(servers: Server[]): WanJhEdge[] {
|
|||||||
return edges
|
return edges
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ресурсы для карточки узла на карте: при наличии снапшота опроса — как в разделе «Серверы», иначе демо по id. */
|
/** Ресурсы API без синтетической истории и fallback; demo=true включает демонстрацию по id. */
|
||||||
export function buildServerResourceMap(servers: Server[]): Record<string, NetworkMapSrvRes> {
|
export function buildServerResourceMap(servers: Server[], demo = false): Record<string, NetworkMapSrvRes> {
|
||||||
|
if (!demo) return Object.fromEntries(servers.map(s=>[s.id,{
|
||||||
|
cpu:s.cpuLoad ?? null,cpuHistory:[],
|
||||||
|
ramTotal:s.totalMemory==null?null:s.totalMemory/(1024*1024),
|
||||||
|
ramUsed:s.totalMemory==null||s.freeMemory==null?null:(s.totalMemory-s.freeMemory)/(1024*1024),
|
||||||
|
hddUsed:null,hddTotal:null,
|
||||||
|
uptimeSeconds:s.uptime && /^(?:\d+[wdhms])+$/.test(s.uptime)?parseRouterosUptimeToSeconds(s.uptime):null,
|
||||||
|
measuredAt:s.polledAt ?? null,fromPoll:true,
|
||||||
|
} satisfies NetworkMapSrvRes]))
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
servers.map((s) => {
|
servers.map((s) => {
|
||||||
const h = s.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0)
|
const h = s.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {test} from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import {buildServerResourceMap,greTunnelProbe} from "./network-map-layout"
|
||||||
|
import {mergeGreMetricsWithSpeedProbe} from "./map-gre-speed-probe"
|
||||||
|
import {servers,greTunnels} from "./data"
|
||||||
|
test("live resources preserve zero, partial poll and unknowns without fake history",()=>{
|
||||||
|
const base={...servers[0],cpuLoad:undefined,totalMemory:undefined,freeMemory:undefined,uptime:undefined,polledAt:undefined}
|
||||||
|
const unknown=buildServerResourceMap([base])[base.id]
|
||||||
|
assert.deepEqual(unknown,{cpu:null,cpuHistory:[],ramTotal:null,ramUsed:null,hddUsed:null,hddTotal:null,uptimeSeconds:null,measuredAt:null,fromPoll:true})
|
||||||
|
const partial=buildServerResourceMap([{...base,cpuLoad:0,uptime:"0s",polledAt:"2020-01-01T00:00:00Z"}])[base.id]
|
||||||
|
assert.equal(partial.cpu,0);assert.equal(partial.uptimeSeconds,0);assert.equal(partial.measuredAt,"2020-01-01T00:00:00Z");assert.equal(partial.ramUsed,null)
|
||||||
|
const memory=buildServerResourceMap([{...base,totalMemory:1048576,freeMemory:1048576,uptime:"bad"}])[base.id]
|
||||||
|
assert.equal(memory.ramUsed,0);assert.equal(memory.ramTotal,1);assert.equal(memory.uptimeSeconds,null)
|
||||||
|
assert.ok(buildServerResourceMap([base],true)[base.id].cpuHistory.length>0)
|
||||||
|
})
|
||||||
|
test("live GRE does not hash metrics and probe values retain zero and fractions",()=>{
|
||||||
|
const fallback=greTunnelProbe(greTunnels[0]);assert.deepEqual(fallback,{pingMs:null,dlMbps:null,ulMbps:null})
|
||||||
|
assert.equal(greTunnelProbe(greTunnels[0],true).pingMs!=null,true)
|
||||||
|
const merged=mergeGreMetricsWithSpeedProbe({id:"p",srcServerId:"1",dstServerId:"2",lastPingRttMs:0,lastTxAvgMbps:0,lastRxAvgMbps:0.125},fallback)
|
||||||
|
assert.deepEqual(merged,{pingMs:0,dlMbps:0,ulMbps:0.125,hasSpeedMonitor:true})
|
||||||
|
})
|
||||||
@@ -35,3 +35,17 @@ test("Карта: неизвестный статус не становится
|
|||||||
await page.getByRole("button",{name:"Неизвестно",exact:true}).click()
|
await page.getByRole("button",{name:"Неизвестно",exact:true}).click()
|
||||||
await expect(page.getByText("Роутер",{exact:true}).first()).toBeVisible()
|
await expect(page.getByText("Роутер",{exact:true}).first()).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for(const polled of [false,true])test("Карта: реальные и неизвестные ресурсы "+polled,async({page})=>{
|
||||||
|
await page.route("**/health",r=>r.fulfill({json:{ok:true}}))
|
||||||
|
await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;const data=body(p);return r.fulfill({json:p===paths.servers?[{...(data as object[])[0],...(polled?{cpuLoad:0,totalMemory:1048576,freeMemory:1048576,uptime:"0s",polledAt:"2020-01-01T00:00:00Z"}:{})}]:data})})
|
||||||
|
await page.goto("/network-map");await page.getByText("Роутер",{exact:true}).first().click()
|
||||||
|
const panel=page.getByText("Ресурсы",{exact:true}).locator("..")
|
||||||
|
await expect(panel.getByText("История CPU и диск API не передаются.",{exact:false})).toBeVisible()
|
||||||
|
await expect(panel.locator("svg")).toHaveCount(0)
|
||||||
|
await expect(panel.getByText("HDD",{exact:true})).toBeVisible()
|
||||||
|
await expect(panel.getByText("— / —",{exact:true})).toHaveCount(polled?1:2)
|
||||||
|
if(polled){await expect(panel.getByText("0%",{exact:true})).toBeVisible();await expect(panel.getByText("0 МБ / 1 МБ",{exact:true})).toBeVisible();await expect(panel.getByText("0м",{exact:true})).toBeVisible()}
|
||||||
|
else await expect(panel.getByText("время неизвестно",{exact:false})).toBeVisible()
|
||||||
|
await expect(panel.getByText("демо-значения",{exact:false})).toHaveCount(0)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user