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

This commit is contained in:
Denozordec committed 2026-10-01 21:47:10 +07:00
1 parent 933a4eaf57
commit 1f75bc5cea
6 files changed
+95 -18

No files matched your search

+28 -12
View File
@@ -25,6 +25,7 @@ import {
type SchedulerRunRowDto,
type UptimeSettingsDto,
} from "@/lib/scheduler-settings"
import { listSchedulerRuns } from "@/shared/api/scheduler"
import { requestJson, ApiClientError } from "@/shared/api/http-client"
import {
parseSchedulerRunSnapshot,
@@ -585,6 +586,9 @@ function DataCollectionContent() {
const [certRenewIntervalDraft, setCertRenewIntervalDraft] = useState("21600")
const [renewBeforeDaysDraft, setRenewBeforeDaysDraft] = useState("30")
const [schedulerRuns, setSchedulerRuns] = useState<SchedulerRunRowDto[]>([])
const [runsKnown, setRunsKnown] = useState(false)
const [runsError, setRunsError] = useState<string | null>(null)
const [runsReceivedAt, setRunsReceivedAt] = useState<string | null>(null)
const [runFilterJobKey, setRunFilterJobKey] = useState<string>("")
const [runNowJobKey, setRunNowJobKey] = useState<string | null>(null)
const [schedulerSaveBusy, setSchedulerSaveBusy] = useState(false)
@@ -623,17 +627,14 @@ function DataCollectionContent() {
setCollectorError(null)
setPageBusy(true)
try {
const runsQuery =
runFilterJobKey && SCHEDULER_JOB_KEYS.includes(runFilterJobKey as (typeof SCHEDULER_JOB_KEYS)[number])
? `?limit=80&jobKey=${encodeURIComponent(runFilterJobKey)}`
: "?limit=80"
const runsFilter = runFilterJobKey && SCHEDULER_JOB_KEYS.includes(runFilterJobKey as (typeof SCHEDULER_JOB_KEYS)[number]) ? runFilterJobKey as (typeof SCHEDULER_JOB_KEYS)[number] : undefined
const [trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes] = await Promise.allSettled([
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 }),
listSchedulerRuns(backendUrl, { limit: 80, jobKey: runsFilter }, { signal }),
])
if (!current()) return
@@ -699,14 +700,17 @@ function DataCollectionContent() {
const runsPayload = readSettled(runsRes)
if (runsPayload) {
setSchedulerRuns(runsPayload.runs ?? [])
setSchedulerRuns(runsPayload.runs)
setRunsKnown(true); setRunsError(null); setRunsReceivedAt(new Date().toISOString())
} else if (runsRes.status === "rejected") {
setRunsError(extractApiError(runsRes.reason))
}
} catch (e) {
if (current()) setCollectorError(e instanceof Error ? e.message : "Не удалось загрузить данные")
} finally {
if (current()) setPageBusy(false)
}
}, [apiFetch, isLive, runFilterJobKey])
}, [apiFetch, backendUrl, isLive, runFilterJobKey])
const persistSchedulerDrafts = useCallback(async (overrides: SchedulerToggleOverrides = {}) => {
const trafficEnabled = overrides.trafficEnabled ?? draftTrafficEnabled
@@ -1041,8 +1045,8 @@ function DataCollectionContent() {
},
{
label: "Журнал (в списке)",
value: String(schedulerRuns.length),
sub: errorRunsInView ? `${errorRunsInView} с ошибкой` : "ошибок в показанных — нет",
value: runsKnown ? String(schedulerRuns.length) : "—",
sub: runsError ? (runsKnown ? "Показана последняя подтверждённая история" : "История не получена") : runsKnown ? (errorRunsInView ? `${errorRunsInView} с ошибкой` : "ошибок в показанных — нет") : "Ожидание истории",
icon: <DatabaseIcon className="size-4" />,
},
]
@@ -1260,7 +1264,7 @@ function DataCollectionContent() {
<OpsPanel
title="Журнал прогонов"
description="Таблица `scheduler_runs` — до 80 записей; раскройте строку для полей и текста ошибки."
description="Последние 80 запусков. Раскройте запись для результатов и объяснения ошибок."
headerRight={
<div className="flex items-center gap-2 shrink-0">
<label className="text-xs text-muted-foreground whitespace-nowrap" htmlFor="run-filter">
@@ -1270,7 +1274,7 @@ function DataCollectionContent() {
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); setOpenRunIds(new Set()) }}
onChange={(e) => { setRunFilterJobKey(e.target.value); setOpenRunIds(new Set()); setSchedulerRuns([]); setRunsKnown(false); setRunsError(null); setRunsReceivedAt(null) }}
>
<option value="">Все</option>
{SCHEDULER_JOB_KEYS.map((k) => (
@@ -1283,7 +1287,19 @@ function DataCollectionContent() {
}
contentClassName="px-0 pb-0"
>
{schedulerRuns.length === 0 ? (
{runsError ? (
<div className="mx-4 mb-3">
<Alert variant="warning">
<AlertCircleIcon aria-hidden="true" />
<AlertTitle>{runsKnown ? "История не обновлена" : "История не получена"}</AlertTitle>
<AlertDescription>{runsError}</AlertDescription>
</Alert>
</div>
) : null}
{runsReceivedAt ? <p className="px-4 pb-3 text-xs text-muted-foreground">История получена: {new Date(runsReceivedAt).toLocaleString("ru-RU")}</p> : null}
{!runsKnown ? (
!runsError ? <p role="status" className="px-4 py-10 text-center text-sm text-muted-foreground">Загрузка журнала…</p> : null
) : schedulerRuns.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-10 px-4">Пока нет прогонов</p>
) : (
<div className="divide-y divide-border">