fix(ui): сохранить историю скорости при частичном сбое
This commit is contained in:
1 parent
611e542ea7
commit
e5ff69e243
5 files changed
+102
-38
No files matched your search
+56
-37
@@ -1106,6 +1106,8 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
const [speedError, setSpeedError] = useState<string | null>(null)
|
||||
const [speedRuns, setSpeedRuns] = useState<SpeedTestRun[]>([])
|
||||
const [speedProbes, setSpeedProbes] = useState<SpeedProbeRow[]>([])
|
||||
const speedReadRevision = useRef(0)
|
||||
const [speedReads, setSpeedReads] = useState<Record<"probes" | "history", { receivedAt: number | null; error: string | null }>>({ probes: { receivedAt: null, error: null }, history: { receivedAt: null, error: null } })
|
||||
const [speedIfaces, setSpeedIfaces] = useState<Record<string, Array<{ name: string; running: boolean; disabled: boolean; addresses: string[] }>>>({})
|
||||
const [speedCollapsed, setSpeedCollapsed] = useState<Set<string>>(new Set())
|
||||
const [speedSheetOpen, setSpeedSheetOpen] = useState(false)
|
||||
@@ -1182,40 +1184,49 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
afterBtPing?: { rttMs: number | null; lossPct: number | null; error: string | null } | null
|
||||
createdAt: string
|
||||
}
|
||||
try {
|
||||
const [sp, runsRes] = await Promise.all([
|
||||
apiFetch<{ probes: SpeedProbeRow[] }>("/api/uptime/speed-probes"),
|
||||
apiFetch<{ runs: RunRow[] }>("/api/uptime/speed-test/runs"),
|
||||
])
|
||||
if (context !== saveContext.current || (savingRef.current && writeContext !== context)) return
|
||||
setSpeedProbes(sp.probes ?? [])
|
||||
const rows = (runsRes.runs ?? []).map((r) => ({
|
||||
id: r.id,
|
||||
startedAt: Date.parse(r.createdAt),
|
||||
srcServerId: r.srcServerId,
|
||||
dstServerId: r.dstServerId,
|
||||
srcInterface: r.srcInterface || undefined,
|
||||
dstInterface: r.dstInterface || undefined,
|
||||
protocol: r.protocol,
|
||||
direction: r.direction,
|
||||
durationSec: r.durationSec,
|
||||
txAvgMbps: Math.round(r.txAvgMbps ?? 0),
|
||||
rxAvgMbps: Math.round(r.rxAvgMbps ?? 0),
|
||||
status: (r.status === "error" ? "error" : "done") as "error" | "done",
|
||||
command: "",
|
||||
lines: r.error ? [`status: error`, r.error] : [],
|
||||
afterBtPing: r.afterBtPing ?? null,
|
||||
srcAddress: r.srcAddress ?? null,
|
||||
dstAddress: r.dstAddress ?? null,
|
||||
srcInterfaceAddress: r.srcInterfaceAddress ?? null,
|
||||
dstInterfaceAddress: r.dstInterfaceAddress ?? null,
|
||||
}))
|
||||
setSpeedRuns(rows)
|
||||
} catch {
|
||||
if (context !== saveContext.current || (savingRef.current && writeContext !== context)) return
|
||||
setSpeedProbes([])
|
||||
setSpeedRuns([])
|
||||
const revision = ++speedReadRevision.current
|
||||
const current = () => revision === speedReadRevision.current && context === saveContext.current && (!savingRef.current || writeContext === context)
|
||||
const read = async (resource: "probes" | "history", task: () => Promise<void>) => {
|
||||
try {
|
||||
await task()
|
||||
if (current()) setSpeedReads(previous => ({ ...previous, [resource]: { receivedAt: Date.now(), error: null } }))
|
||||
} catch (error) {
|
||||
if (current()) setSpeedReads(previous => ({ ...previous, [resource]: { ...previous[resource], error: serverOperationError(error) } }))
|
||||
}
|
||||
}
|
||||
await Promise.all([
|
||||
read("probes", async () => {
|
||||
const sp = await apiFetch<{ probes: SpeedProbeRow[] }>("/api/uptime/speed-probes")
|
||||
if (!Array.isArray(sp.probes)) throw new Error("Некорректный список speed-проб")
|
||||
if (current()) setSpeedProbes(sp.probes)
|
||||
}),
|
||||
read("history", async () => {
|
||||
const runsRes = await apiFetch<{ runs: RunRow[] }>("/api/uptime/speed-test/runs")
|
||||
if (!Array.isArray(runsRes.runs)) throw new Error("Некорректная история тестов")
|
||||
const rows = (runsRes.runs ?? []).map((r) => ({
|
||||
id: r.id,
|
||||
startedAt: Date.parse(r.createdAt),
|
||||
srcServerId: r.srcServerId,
|
||||
dstServerId: r.dstServerId,
|
||||
srcInterface: r.srcInterface || undefined,
|
||||
dstInterface: r.dstInterface || undefined,
|
||||
protocol: r.protocol,
|
||||
direction: r.direction,
|
||||
durationSec: r.durationSec,
|
||||
txAvgMbps: Math.round(r.txAvgMbps ?? 0),
|
||||
rxAvgMbps: Math.round(r.rxAvgMbps ?? 0),
|
||||
status: (r.status === "error" ? "error" : "done") as "error" | "done",
|
||||
command: "",
|
||||
lines: r.error ? [`status: error`, r.error] : [],
|
||||
afterBtPing: r.afterBtPing ?? null,
|
||||
srcAddress: r.srcAddress ?? null,
|
||||
dstAddress: r.dstAddress ?? null,
|
||||
srcInterfaceAddress: r.srcInterfaceAddress ?? null,
|
||||
dstInterfaceAddress: r.dstInterfaceAddress ?? null,
|
||||
}))
|
||||
if (current()) setSpeedRuns(rows)
|
||||
}),
|
||||
])
|
||||
}, [apiFetch, liveApi])
|
||||
|
||||
const refreshUptimeLive = useCallback(async (opts?: { showSpinner?: boolean; pollDevices?: boolean }) => {
|
||||
@@ -2097,7 +2108,15 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
{/* ── speed tab ── */}
|
||||
{tab === "speed" && (
|
||||
<div className="flex flex-col gap-0">
|
||||
|
||||
{liveApi && <div className="px-6 py-3 space-y-2">
|
||||
{(["probes", "history"] as const).map(resource => {
|
||||
const state = speedReads[resource], label = resource === "probes" ? "Speed-пробы" : "История тестов"
|
||||
return <div key={resource}>
|
||||
<p role="status" className="text-xs text-muted-foreground">{label}: {state.receivedAt ? "получено " + new Date(state.receivedAt).toLocaleString("ru-RU") : "данные пока неизвестны"}</p>
|
||||
{state.error && <ContextAlert variant="warning" aria-label={"Чтение скорости: " + resource}><ContextTitle>{label}</ContextTitle><ContextDescription>{state.error} {state.receivedAt ? "Последние данные сохранены." : "Данные пока неизвестны."}</ContextDescription></ContextAlert>}
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
{/* ── KPI strip ── */}
|
||||
{(() => {
|
||||
const doneRuns = speedRuns.filter(r => r.status === "done")
|
||||
@@ -2112,14 +2131,14 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
{
|
||||
id: "probes",
|
||||
label: "Speed-пробы",
|
||||
value: `${speedProbes.length} шт`,
|
||||
value: liveApi && !speedReads.probes.receivedAt ? "—" : `${speedProbes.length} шт`,
|
||||
icon: <ArrowUpDownIcon className="size-4" />,
|
||||
iconClassName: "text-muted-foreground",
|
||||
},
|
||||
{
|
||||
id: "runs",
|
||||
label: "Тестов выполнено",
|
||||
value: `${doneRuns.length} run`,
|
||||
value: liveApi && !speedReads.history.receivedAt ? "—" : `${doneRuns.length} run`,
|
||||
hint: runningCnt > 0 ? `${runningCnt} выполняется` : undefined,
|
||||
icon: <PlayIcon className="size-4" />,
|
||||
iconClassName: runningCnt > 0 ? "text-warning" : "text-muted-foreground",
|
||||
@@ -2158,7 +2177,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
<div className="flex-1 overflow-y-auto p-6 flex flex-col gap-4">
|
||||
|
||||
{/* ── empty state ── */}
|
||||
{speedGrouped.length === 0 && (
|
||||
{speedGrouped.length === 0 && (!liveApi || speedReads.probes.receivedAt !== null) && (
|
||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
|
||||
<div className="size-12 rounded-full bg-muted flex items-center justify-center">
|
||||
<ArrowUpDownIcon className="size-5 text-muted-foreground" />
|
||||
|
||||
Reference in new issue
Block a user