fix(probes): проверять расписание и сохранять подтверждённые данные

This commit is contained in:
Denozordec committed 2026-10-03 22:12:08 +07:00
1 parent 1d27127b52
commit 271291a910
10 files changed
+108 -98

No files matched your search

@@ -18,22 +18,8 @@ import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sor
import { EmptyState } from "@/components/empty-state"
import { ClockIcon } from "lucide-react"
export interface SpeedProbeApiRow {
id: string
srcServerId: string
dstServerId: string
srcInterface: string
dstInterface: string
protocol: string
direction: string
durationSec: string
enabled: boolean
lastRunAt: string | null
lastTxAvgMbps: number | null
lastRxAvgMbps: number | null
lastStatus: string | null
lastError: string | null
}
import type {SpeedProbeApiRow} from "@/lib/probe-schedule-response"
export type {SpeedProbeApiRow} from "@/lib/probe-schedule-response"
interface ProbesSpeedProbesDataGridProps {
rows: SpeedProbeApiRow[]
@@ -123,9 +109,9 @@ function ProbesSpeedProbesDataGrid({ rows, serverName }: ProbesSpeedProbesDataGr
return (
<span className="text-xs text-muted-foreground truncate block">
{r.lastRunAt ?? "—"}
{r.lastStatus === "done" && r.lastTxAvgMbps != null && (
{r.lastStatus === "done" && (
<span className="text-emerald-600 dark:text-emerald-400 ml-1">
TX≈{r.lastTxAvgMbps.toFixed(1)} RX≈{(r.lastRxAvgMbps ?? 0).toFixed(1)} Mb/s
TX: {r.lastTxAvgMbps==null?"неизвестно":r.lastTxAvgMbps.toFixed(1)+" Мбит/с"} · RX: {r.lastRxAvgMbps==null?"неизвестно":r.lastRxAvgMbps.toFixed(1)+" Мбит/с"}
</span>
)}
{r.lastStatus === "error" && r.lastError && (
@@ -157,7 +143,7 @@ function ProbesSpeedProbesDataGrid({ rows, serverName }: ProbesSpeedProbesDataGr
<EmptyState
icon={<ClockIcon className="size-4" />}
title="Нет записей speed-test"
description="Настраиваются через API PUT /api/uptime/speed-probes"
description="Добавьте пробы в разделе мониторинга."
className="border-0 py-10"
/>
)
+41
View File
@@ -0,0 +1,41 @@
"use client"
import Link from "next/link"
import {useCallback,useEffect,useRef,useState} from "react"
import {Button} from "@/components/ui/button"
import {DataPageCard} from "@/components/data-page-card"
import {ProbesSpeedProbesDataGrid} from "@/components/data-grids/probes-speed-probes-data-grid"
import {can,ensureAuthConfig} from "@/lib/auth"
import {readProbeSchedule,type SpeedProbeApiRow} from "@/lib/probe-schedule-response"
import type {Server} from "@/lib/data"
import {requestJson} from "@/shared/api/http-client"
type Snapshot={rows:SpeedProbeApiRow[]|undefined;loading:boolean;error:string|null;receivedAt:string|null}
export function ProbesSpeedSchedule({base,serversForName}:{base:string;serversForName:Server[]}){
const [state,setState]=useState<Snapshot>({rows:undefined,loading:true,error:null,receivedAt:null})
const active=useRef(false),revision=useRef(0),request=useRef<AbortController|null>(null),timer=useRef<ReturnType<typeof setTimeout>|null>(null)
const refresh=useCallback(async()=>{
if(!active.current||request.current)return
const id=++revision.current,controller=new AbortController();request.current=controller
const current=()=>active.current&&revision.current===id&&!controller.signal.aborted
setState(previous=>({...previous,loading:true,error:null}))
timer.current=setTimeout(()=>{if(current()){revision.current++;controller.abort();request.current=null;setState(previous=>({...previous,loading:false,error:"Время ожидания проб скорости истекло. Повторите обновление."}))}},30000)
try{
await ensureAuthConfig();if(!current())return
if(!can("mm:uptime:read")){setState({rows:undefined,loading:false,error:"Нет права читать пробы скорости.",receivedAt:null});return}
const rows=readProbeSchedule(await requestJson<unknown>(base,"/api/uptime/speed-probes",{signal:controller.signal,cache:"no-store"}))
if(current()){
if(!can("mm:uptime:read")){setState({rows:undefined,loading:false,error:"Нет права читать пробы скорости.",receivedAt:null});return}
setState({rows,loading:false,error:null,receivedAt:new Date().toISOString()})
}
}catch(error){if(current())setState(previous=>({...previous,loading:false,error:error instanceof Error?error.message:"Не удалось прочитать пробы скорости."}))}
finally{if(revision.current===id){if(timer.current)clearTimeout(timer.current);timer.current=null;request.current=null}}
},[base])
useEffect(()=>{const generation=revision;active.current=true;let pending=true;queueMicrotask(()=>{if(pending)void refresh()});return()=>{pending=false;active.current=false;generation.current++;request.current?.abort();request.current=null;if(timer.current)clearTimeout(timer.current)}},[refresh])
return <section aria-label="Пробы скорости" className="min-w-0 space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2"><p role="status" className="text-xs text-muted-foreground">{state.loading?"Обновление проб скорости…":state.receivedAt?"Последние данные получены: "+new Date(state.receivedAt).toLocaleString("ru-RU"):"Данные проб скорости ещё не получены."}</p><Button type="button" size="sm" variant="outline" disabled={state.loading} onClick={()=>void refresh()}>Обновить пробы скорости</Button></div>
{state.error?<p role="alert" className="text-sm text-destructive">{state.error}{state.rows!==undefined?" Показан последний успешный список.":""}</p>:null}
{state.rows!==undefined?<DataPageCard><ProbesSpeedProbesDataGrid rows={state.rows} serverName={id=>serversForName.find(s=>s.id===id)?.name??id}/></DataPageCard>:null}
<p className="text-xs text-muted-foreground">Показаны пробы скорости коллектора. Периодичность запуска задаётся в сборе данных; отдельного расписания диагностических GRE-тестов в живом режиме нет.</p>
<div className="flex flex-wrap gap-3 text-sm"><Link className="underline underline-offset-4" href="/uptime">Настроить пробы в мониторинге</Link><Link className="underline underline-offset-4" href="/data-collection">Открыть сбор данных</Link></div>
</section>
}