fix(scheduler): исключить поздние ответы после смены фильтра и источника

This commit is contained in:
Denozordec committed 2026-10-01 21:39:04 +07:00
1 parent 1b51d2af01
commit 933a4eaf57
5 files changed
+79 -41

No files matched your search

+34 -39
View File
@@ -1,9 +1,8 @@
"use client"
import Link from "next/link"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { PageHeader } from "@/components/page-header"
import { FormToggle } from "@/components/form-kit"
import { Badge } from "@/components/reui/badge"
import { Button, buttonVariants } from "@/components/ui/button"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
@@ -554,6 +553,12 @@ function applySchedulerJobsToDrafts(
}
export default function DataCollectionPage() {
const { mode, backendUrl, prefsHydrated } = useDataSource()
if (!prefsHydrated) return <p role="status" className="p-6 text-sm text-muted-foreground">Загрузка настроек источника…</p>
return <DataCollectionContent key={JSON.stringify([mode, backendUrl])} />
}
function DataCollectionContent() {
const { mode, backendUrl, prefsHydrated } = useDataSource()
const isLive = prefsHydrated && mode === "live"
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
@@ -587,6 +592,13 @@ export default function DataCollectionPage() {
const [collectorError, setCollectorError] = useState<string | null>(null)
const [openRunIds, setOpenRunIds] = useState<Set<string>>(() => new Set())
const reader = useRef<{ active: boolean; revision: number; controller?: AbortController }>({ active: true, revision: 0 })
useEffect(() => {
reader.current.active = true
const state = reader.current
return () => { state.active = false; state.revision++; state.controller?.abort() }
}, [])
const toggleRunOpen = useCallback((id: string, open: boolean) => {
setOpenRunIds((prev) => {
const next = new Set(prev)
@@ -602,7 +614,12 @@ export default function DataCollectionPage() {
}, [uptimeCollector?.scheduler?.jobs])
const loadCollectors = useCallback(async () => {
if (!isLive) return
if (!isLive || !reader.current.active) return
const revision = ++reader.current.revision
reader.current.controller?.abort()
const controller = new AbortController(); reader.current.controller = controller
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(30_000)])
const current = () => reader.current.active && revision === reader.current.revision && !controller.signal.aborted
setCollectorError(null)
setPageBusy(true)
try {
@@ -611,14 +628,15 @@ export default function DataCollectionPage() {
? `?limit=80&jobKey=${encodeURIComponent(runFilterJobKey)}`
: "?limit=80"
const [trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes] = await Promise.allSettled([
apiFetch<CollectorSettingsDto>("/api/traffic/settings"),
apiFetch<CollectorSettingsDto>("/api/servers-api-ping/settings"),
apiFetch<UptimeSettingsDto>("/api/uptime/settings"),
apiFetch<CollectorSettingsDto>("/api/internet-path/settings"),
apiFetch<{ enabled: boolean; intervalSec: number; renewBeforeDays: number }>("/api/certificates/renew-settings"),
apiFetch<{ runs: SchedulerRunRowDto[] }>(`/api/scheduler/runs${runsQuery}`),
apiFetch<CollectorSettingsDto>("/api/traffic/settings", { signal }),
apiFetch<CollectorSettingsDto>("/api/servers-api-ping/settings", { signal }),
apiFetch<UptimeSettingsDto>("/api/uptime/settings", { signal }),
apiFetch<CollectorSettingsDto>("/api/internet-path/settings", { signal }),
apiFetch<{ enabled: boolean; intervalSec: number; renewBeforeDays: number }>("/api/certificates/renew-settings", { signal }),
apiFetch<{ runs: SchedulerRunRowDto[] }>(`/api/scheduler/runs${runsQuery}`, { signal }),
])
if (!current()) return
const loadErrors = collectSettledErrors(
[trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes],
["трафик", "серверы REST API", "uptime", "internet path", "сертификаты", "журнал планировщика"],
@@ -684,9 +702,9 @@ export default function DataCollectionPage() {
setSchedulerRuns(runsPayload.runs ?? [])
}
} catch (e) {
setCollectorError(e instanceof Error ? e.message : "Не удалось загрузить данные")
if (current()) setCollectorError(e instanceof Error ? e.message : "Не удалось загрузить данные")
} finally {
setPageBusy(false)
if (current()) setPageBusy(false)
}
}, [apiFetch, isLive, runFilterJobKey])
@@ -823,21 +841,10 @@ export default function DataCollectionPage() {
}, [loadCollectors, persistSchedulerDrafts])
useEffect(() => {
if (!isLive) {
setTrafficCollector(null)
setServersApiCollector(null)
setUptimeCollector(null)
setInternetPathCollector(null)
setSchedulerRuns([])
return
}
void loadCollectors()
if (!isLive) return
queueMicrotask(() => { void loadCollectors() })
}, [isLive, loadCollectors])
useEffect(() => {
setOpenRunIds(new Set())
}, [runFilterJobKey])
const handleRefreshScheduler = async () => {
if (!isLive) return
setCollectorError(null)
@@ -1005,8 +1012,7 @@ export default function DataCollectionPage() {
const errorRunsInView = useMemo(() => schedulerRuns.filter((r) => r.status === "error").length, [schedulerRuns])
const stats = useMemo(
() => [
const stats = [
{
label: "Включено задач",
value: `${enabledJobsCount} / ${schedulerJobCount}`,
@@ -1039,18 +1045,7 @@ export default function DataCollectionPage() {
sub: errorRunsInView ? `${errorRunsInView} с ошибкой` : "ошибок в показанных — нет",
icon: <DatabaseIcon className="size-4" />,
},
],
[
enabledJobsCount,
schedulerJobCount,
runningJobsCount,
trafficCollector?.lastCollectedAt,
trafficCollector?.lastDurationMs,
trafficCollector?.lastError,
schedulerRuns.length,
errorRunsInView,
],
)
]
return (
<div className="flex flex-col h-full">
@@ -1275,7 +1270,7 @@ export default function DataCollectionPage() {
id="run-filter"
className="h-8 rounded-md border border-input bg-background px-2 text-xs min-w-[180px]"
value={runFilterJobKey}
onChange={(e) => setRunFilterJobKey(e.target.value)}
onChange={(e) => { setRunFilterJobKey(e.target.value); setOpenRunIds(new Set()) }}
>
<option value="">Все</option>
{SCHEDULER_JOB_KEYS.map((k) => (