fix(uptime): отличать ошибки опроса от измеренных потерь Ping
This commit is contained in:
1 parent
ca23284486
commit
8fb46b1552
35 files changed
+1582
-78
No files matched your search
+33
-33
@@ -4,7 +4,7 @@ import { publishSpeedProbeSnapshots } from "@/lib/speed-probe-snapshot"
|
||||
import { parseSpeedRunResponse } from "@/lib/speed-run-response"
|
||||
import { isSpeedPersistenceUnknown } from "@/lib/speed-persistence-outcome"
|
||||
|
||||
import { dashboardProbeOverviewSchema } from "@/lib/dashboard-probes"
|
||||
import { dashboardProbeOverviewSchema, parseDashboardProbes, type DashboardProbe as PingProbe } from "@/lib/dashboard-probes"
|
||||
|
||||
import { sameSpeedMeasurement, speedHistoryResponseSchema, speedProbesResponseSchema, speedProbesWriteSchema, speedProbesSavedSchema, pingProbesWriteSchema, probeCatalogSavedSchema } from "@mmapp/contracts"
|
||||
|
||||
@@ -38,7 +38,6 @@ import { UptimeSpeedHistoryDataGrid } from "@/components/data-grids/uptime-speed
|
||||
import { PING_PROBE_WARN_RTT_MS } from "@/lib/ping-probe"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { servers as mockServers, pingProbes as INIT_PROBES, filters, type Server, type Filter } from "@/lib/data"
|
||||
import type { PingProbe } from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
@@ -101,13 +100,15 @@ function jitter(base: number, pct: number) {
|
||||
return Math.round(Math.max(1, base + (Math.random() - 0.5) * base * pct * 2))
|
||||
}
|
||||
|
||||
function rttColor(rtt: number | null, loss: number): string {
|
||||
function rttColor(rtt: number | null, loss: number | null): string {
|
||||
if (loss === null) return "text-muted-foreground"
|
||||
if (rtt === null || loss >= 100) return "text-[var(--status-offline-fg)]"
|
||||
if (loss > 1 || rtt > PING_PROBE_WARN_RTT_MS) return "text-[var(--status-degraded-fg)]"
|
||||
return "text-[var(--status-online-fg)]"
|
||||
}
|
||||
|
||||
function probeSparkColor(status: PingProbe["status"]): string {
|
||||
if (status === "unknown") return "var(--muted-foreground)"
|
||||
return status === "down" ? "var(--status-offline)" : status === "warn" ? "var(--status-degraded)" : "var(--status-online)"
|
||||
}
|
||||
|
||||
@@ -118,14 +119,14 @@ function ProbePingRttDetailChart({
|
||||
probeName,
|
||||
target,
|
||||
}: {
|
||||
series: number[]
|
||||
series: Array<number | null>
|
||||
status: PingProbe["status"]
|
||||
probeName: string
|
||||
target: string
|
||||
}) {
|
||||
const stroke = probeSparkColor(status)
|
||||
const data = series.map((v) => (v != null && Number.isFinite(v) ? Math.max(0, v) : 0))
|
||||
const valid = data.filter((v) => Number.isFinite(v))
|
||||
const data = series.map((v) => (v != null && Number.isFinite(v) ? Math.max(0, v) : null))
|
||||
const valid = data.filter((v): v is number => v !== null)
|
||||
if (valid.length === 0) {
|
||||
return (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -133,22 +134,25 @@ function ProbePingRttDetailChart({
|
||||
</p>
|
||||
)
|
||||
}
|
||||
const chartPts = valid.length >= 2 ? data : [valid[0] ?? 0, valid[0] ?? 0]
|
||||
const chartPts = data
|
||||
const W = 720
|
||||
const H = 168
|
||||
const pad = { l: 48, r: 14, t: 14, b: 36 }
|
||||
const iw = W - pad.l - pad.r
|
||||
const ih = H - pad.t - pad.b
|
||||
const maxVal = Math.max(...chartPts, 1)
|
||||
const minVal = Math.min(...chartPts)
|
||||
const maxVal = Math.max(...valid, 1)
|
||||
const minVal = Math.min(...valid)
|
||||
const span = Math.max(1, maxVal - minVal) * 1.08
|
||||
const y0 = minVal - (span - (maxVal - minVal)) / 2
|
||||
const xAt = (i: number) => pad.l + (chartPts.length <= 1 ? iw / 2 : (i / (chartPts.length - 1)) * iw)
|
||||
const yAt = (v: number) => pad.t + (1 - (v - y0) / span) * ih
|
||||
const lineD = chartPts
|
||||
.map((v, i) => `${i === 0 ? "M" : "L"}${xAt(i).toFixed(1)},${yAt(v).toFixed(1)}`)
|
||||
.join(" ")
|
||||
const areaD = `${lineD} L ${xAt(chartPts.length - 1).toFixed(1)},${pad.t + ih} L ${pad.l},${pad.t + ih} Z`
|
||||
const segments: Array<Array<{ value: number; index: number }>> = []
|
||||
let segment: Array<{ value: number; index: number }> = []
|
||||
chartPts.forEach((value, index) => {
|
||||
if (value === null) { segment = []; return }
|
||||
if (!segment.length) segments.push(segment)
|
||||
segment.push({ value, index })
|
||||
})
|
||||
const gridVals = [0, 0.25, 0.5, 0.75, 1]
|
||||
const fmt = (v: number) => `${Math.round(v)} мс`
|
||||
return (
|
||||
@@ -191,15 +195,9 @@ function ProbePingRttDetailChart({
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
<path d={areaD} style={{ fill: stroke, fillOpacity: 0.12 }} />
|
||||
<path
|
||||
d={lineD}
|
||||
fill="none"
|
||||
style={{ stroke }}
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{segments.map((points, index) => points.length === 1
|
||||
? <circle key={index} cx={xAt(points[0].index)} cy={yAt(points[0].value)} r="2" fill={stroke} />
|
||||
: <path key={index} d={points.map((point, i) => `${i ? "L" : "M"}${xAt(point.index).toFixed(1)},${yAt(point.value).toFixed(1)}`).join(" ")} fill="none" stroke={stroke} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />)}
|
||||
{[0, Math.floor((chartPts.length - 1) / 2), chartPts.length - 1]
|
||||
.filter((i, idx, a) => a.indexOf(i) === idx)
|
||||
.map((i) => (
|
||||
@@ -224,7 +222,7 @@ function ProbePingRttDetailChart({
|
||||
max <span className="font-mono text-foreground">{Math.round(maxVal)}</span> мс
|
||||
</span>
|
||||
{valid.length < 2 && (
|
||||
<span className="text-amber-600 dark:text-amber-400">В ряду одна точка — линия для наглядности продублирована.</span>
|
||||
<span className="text-muted-foreground">В ряду одно измерение.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -408,7 +406,7 @@ function ServerPickerCards({
|
||||
</div>
|
||||
{!server.enabled && (
|
||||
<p className="mt-1 text-[10px] text-amber-600 dark:text-amber-400 leading-snug">
|
||||
В инвентаре выключен — для ping всё равно можно выбрать, если бекенд достигает REST API.
|
||||
Сервер выключен. Включите его перед опросом.
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
@@ -1132,7 +1130,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
if (myReq !== overviewReqRef.current || context !== saveContext.current || (savingRef.current && writeContext !== context)) return
|
||||
const overview = dashboardProbeOverviewSchema.safeParse(rawData)
|
||||
if (!overview.success || !pingProbesWriteSchema.safeParse({ probes: overview.data.probes }).success || !rawData || typeof rawData !== "object" || !Array.isArray((rawData as { resources?: unknown }).resources)) throw new Error("Ответ сервера содержит некорректные Ping-пробы.")
|
||||
const data = { probes: overview.data.probes, resources: (rawData as { resources: ServerResource[] }).resources }
|
||||
const data = { probes: parseDashboardProbes(rawData).probes, resources: (rawData as { resources: ServerResource[] }).resources }
|
||||
setAllServers(mapBackendServersToServers(serverRows))
|
||||
setProbes(data.probes)
|
||||
setPingReceivedAt(Date.now())
|
||||
@@ -1394,6 +1392,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
up: probes.filter(p => p.status === "up").length,
|
||||
warn: probes.filter(p => p.status === "warn").length,
|
||||
down: probes.filter(p => p.status === "down").length,
|
||||
unknown: probes.filter(p => p.status === "unknown").length,
|
||||
}), [probes])
|
||||
|
||||
const alertCount = useMemo(() =>
|
||||
@@ -1606,9 +1605,9 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
target: newTarget.trim(),
|
||||
filter: newFilter || "—",
|
||||
rtt: null,
|
||||
loss: 0,
|
||||
status: "up",
|
||||
series: Array(40).fill(0),
|
||||
loss: null,
|
||||
status: "unknown",
|
||||
series: [],
|
||||
enabled: true,
|
||||
showOnDashboard: false,
|
||||
}
|
||||
@@ -2382,6 +2381,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
<StatChip label="OK" value={pingCatalogKnown ? stats.up : "—"} color="emerald" active={statusFilter === "up"} onClick={() => cycleStatusFilter("up")} />
|
||||
<StatChip label="Предупреждение" value={pingCatalogKnown ? stats.warn : "—"} color="amber" active={statusFilter === "warn"} onClick={() => cycleStatusFilter("warn")} />
|
||||
<StatChip label="Недоступен" value={pingCatalogKnown ? stats.down : "—"} color="red" active={statusFilter === "down"} onClick={() => cycleStatusFilter("down")} />
|
||||
<StatChip label="Нет данных" value={pingCatalogKnown ? stats.unknown : "—"} active={statusFilter === "unknown"} onClick={() => cycleStatusFilter("unknown")} />
|
||||
|
||||
<div className="w-px h-4 bg-border mx-1 shrink-0" />
|
||||
|
||||
@@ -2548,11 +2548,11 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
>
|
||||
<label className={probeStyles.toggle}><span className="sr-only">Включить пробу {p.name}</span><span className={probeStyles.label} aria-hidden="true">Включена</span><FormToggle disabled={probeSaving || !canWrite || !pingCatalogReady} checked={p.enabled} onChange={v => toggleProbe(p.id, v)} /></label>
|
||||
<div className={probeStyles.status}><StatusDot
|
||||
status={p.status === "up" ? "online" : p.status === "warn" ? "degraded" : "offline"}
|
||||
status={p.status === "unknown" ? "unknown" : p.status === "up" ? "online" : p.status === "warn" ? "degraded" : "offline"}
|
||||
pulse={p.status === "up" && p.enabled}
|
||||
/><span className={probeStyles.mobile}>{!p.enabled ? "Отключена" : p.status === "up" ? "Доступна" : p.status === "warn" ? "Предупреждение" : "Недоступна"}</span></div>
|
||||
/><span className={probeStyles.mobile}>{!p.enabled ? "Отключена" : p.status === "unknown" ? "Нет данных" : p.status === "up" ? "Доступна" : p.status === "warn" ? "Предупреждение" : "Недоступна"}</span></div>
|
||||
<div className={probeStyles.iface}><span className={probeStyles.label}>Интерфейс</span><span className="font-mono text-xs text-muted-foreground">{p.srcInterface || "auto"}</span></div>
|
||||
<div className={probeStyles.name}><span className="text-sm font-medium">{p.name}</span></div>
|
||||
<div className={probeStyles.name}><span className="text-sm font-medium">{p.name}</span>{p.observationError && <p className="text-xs text-muted-foreground break-words">Ошибка измерения: {p.observationError}</p>}</div>
|
||||
<div className={probeStyles.filter}><span className={probeStyles.label}>Фильтр</span>{p.filter === "—"
|
||||
? <span className="text-muted-foreground/40 text-xs">—</span>
|
||||
: <span className="text-[11px] font-medium text-sky-600 dark:text-sky-400">{p.filter}</span>
|
||||
@@ -2561,12 +2561,12 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
{p.rtt === null ? "—" : `${p.rtt}мс`}
|
||||
</span></div>
|
||||
<div className={probeStyles.loss}><span className={probeStyles.label}>Потери</span><span className={cn("font-mono text-xs tabular-nums",
|
||||
p.loss === 0 ? "text-muted-foreground" :
|
||||
p.loss === null || p.loss === 0 ? "text-muted-foreground" :
|
||||
p.loss >= 100 ? "text-red-600 dark:text-red-400" :
|
||||
p.loss > 5 ? "text-red-600 dark:text-red-400" :
|
||||
"text-amber-600 dark:text-amber-400",
|
||||
)}>
|
||||
{p.loss}%
|
||||
{p.loss === null ? "—" : `${p.loss}%`}
|
||||
</span></div>
|
||||
<div className={cn(probeStyles.bt, "min-w-0 flex flex-col gap-0.5 justify-center")}><span className={probeStyles.label}>BT · ping</span>
|
||||
{!linkedSp?.lastRunAt ? (
|
||||
|
||||
Reference in new issue
Block a user