fix(scheduler): подтверждать ручные операции и учитывать частичные ошибки
This commit is contained in:
1 parent
cb377fb60c
commit
5df337f6d7
7 files changed
+117
-36
No files matched your search
@@ -27,8 +27,8 @@ import {
|
||||
import { getTrafficCollectorSettings, getServersApiPingSettings, getUptimeCollectorSettings, getInternetPathSettings, putTrafficCollectorSettings, putServersApiPingSettings, putUptimeCollectorSettings, putInternetPathSettings } from "@/shared/api/collector-settings"
|
||||
import { can } from "@/lib/auth"
|
||||
import { getCertificateRenewSettings, putCertificateRenewSettings } from "@/shared/api/certificates"
|
||||
import { getSchedulerStatus, listSchedulerRuns } from "@/shared/api/scheduler"
|
||||
import { requestJson, ApiClientError } from "@/shared/api/http-client"
|
||||
import { getSchedulerStatus, listSchedulerRuns, runSchedulerJobNow, refreshScheduler } from "@/shared/api/scheduler"
|
||||
import { ApiClientError } from "@/shared/api/http-client"
|
||||
import {
|
||||
parseSchedulerRunSnapshot,
|
||||
type AlertEngineRunSnapshot,
|
||||
@@ -70,12 +70,6 @@ import {
|
||||
XCircleIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
function makeApiFetch(backendUrl: string) {
|
||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
function extractApiError(reason: unknown): string {
|
||||
if (reason instanceof ApiClientError) return reason.message
|
||||
if (reason instanceof Error && reason.name === "ZodError") return "Настройки не подтверждены: получен некорректный ответ сервера"
|
||||
@@ -524,7 +518,6 @@ export default function DataCollectionPage() {
|
||||
function DataCollectionContent() {
|
||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||
const isLive = prefsHydrated && mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
const [trafficCollector, setTrafficCollector] = useState<Awaited<ReturnType<typeof getTrafficCollectorSettings>> | null>(null)
|
||||
const [serversApiCollector, setServersApiCollector] = useState<Awaited<ReturnType<typeof getServersApiPingSettings>> | null>(null)
|
||||
@@ -597,7 +590,6 @@ function DataCollectionContent() {
|
||||
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 {
|
||||
const runsFilter = runFilterJobKey && SCHEDULER_JOB_KEYS.includes(runFilterJobKey as (typeof SCHEDULER_JOB_KEYS)[number]) ? runFilterJobKey as (typeof SCHEDULER_JOB_KEYS)[number] : undefined
|
||||
@@ -733,14 +725,34 @@ function DataCollectionContent() {
|
||||
}, [isLive, loadCollectors])
|
||||
|
||||
const handleRefreshScheduler = async () => {
|
||||
if (!isLive) return
|
||||
setCollectorError(null)
|
||||
if (!isLive || !reader.current.active || mutationLock.current || !can("mm:settings:admin")) return
|
||||
mutationLock.current = true; reader.current.revision++; reader.current.controller?.abort(); setPageBusy(false); setSchedulerSaveBusy(true); setCollectorError(null); setSaveMessage(null)
|
||||
try {
|
||||
await apiFetch("/api/scheduler/refresh", { method: "POST" })
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Не удалось обновить планировщик")
|
||||
}
|
||||
const data = await refreshScheduler(backendUrl)
|
||||
if (!reader.current.active) return
|
||||
setSchedulerStatus(data.status); setSchedulerStatusError(null); setSchedulerStatusReceivedAt(new Date().toISOString())
|
||||
setSaveMessage("Перечитывание таймеров подтверждено сервером")
|
||||
} catch (error) { if (reader.current.active) setCollectorError(extractApiError(error)) }
|
||||
finally { mutationLock.current = false; if (reader.current.active) setSchedulerSaveBusy(false) }
|
||||
}
|
||||
|
||||
const handleRunNow = async (jobKey: (typeof SCHEDULER_JOB_KEYS)[number]) => {
|
||||
const job = schedulerJobsByKey[jobKey]
|
||||
if (!isLive || !reader.current.active || mutationLock.current || !can("mm:settings:admin") || !job || job.running) return
|
||||
mutationLock.current = true; reader.current.revision++; reader.current.controller?.abort(); setPageBusy(false); setSchedulerSaveBusy(true); setRunNowJobKey(jobKey); setCollectorError(null); setSaveMessage(null)
|
||||
try {
|
||||
const data = await runSchedulerJobNow(backendUrl, jobKey)
|
||||
if (!reader.current.active) return
|
||||
if (data.status) { setSchedulerStatus(data.status); setSchedulerStatusError(null); setSchedulerStatusReceivedAt(new Date().toISOString()) }
|
||||
else setSchedulerStatusError(data.statusError ?? "Статус после запуска недоступен")
|
||||
const skipped = "skipped" in data.result.result && data.result.result.skipped
|
||||
const notDue = data.result.result.job === "backups" && !data.result.result.due
|
||||
setSaveMessage((!data.ok ? "Запуск завершился с ошибками: " : skipped || notDue ? "Прогон пропущен: " : "Запуск подтверждён: ") + SCHEDULER_JOB_LABELS[jobKey])
|
||||
const warnings = [data.result.error, data.result.eventError ? "Журнал событий: " + data.result.eventError : null, data.statusError ? "Статус: " + data.statusError : null].filter(Boolean)
|
||||
if (warnings.length) setCollectorError(warnings.join("; "))
|
||||
} catch (error) {
|
||||
if (reader.current.active) setCollectorError(extractApiError(error) + ". Результат операции неизвестен; перед повторным запуском обновите журнал.")
|
||||
} finally { mutationLock.current = false; if (reader.current.active) { setSchedulerSaveBusy(false); setRunNowJobKey(null) } }
|
||||
}
|
||||
|
||||
const schedulerGridRows: SchedulerJobGridRow[] = (() => {
|
||||
@@ -829,20 +841,8 @@ function DataCollectionContent() {
|
||||
void handleJobEnabledChange(jobKey, nextEnabled)
|
||||
},
|
||||
onIntervalChange: (value: string) => { markDirty(settingsKey, jobKey === "uptime_ping" ? "probeIntervalSec" : jobKey === "uptime_speed" ? "speedIntervalSec" : "intervalSec"); onIntervalChange(value) },
|
||||
onRunNow: async () => {
|
||||
setRunNowJobKey(jobKey)
|
||||
setCollectorError(null)
|
||||
try {
|
||||
await apiFetch(`/api/scheduler/jobs/${encodeURIComponent(jobKey)}/run-now`, {
|
||||
method: "POST",
|
||||
})
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Ошибка запуска")
|
||||
} finally {
|
||||
setRunNowJobKey(null)
|
||||
}
|
||||
},
|
||||
onRunNow: async () => { await handleRunNow(jobKey) },
|
||||
runNowDisabled: schedulerSaveBusy || !can("mm:settings:admin") || !j || j.running,
|
||||
runNowLoading: runNowJobKey === jobKey,
|
||||
saveBusy: schedulerSaveBusy || !writable(settingsKey),
|
||||
}
|
||||
@@ -906,7 +906,7 @@ function DataCollectionContent() {
|
||||
<RefreshCwIcon className={cn("size-4", pageBusy && "animate-spin")} />
|
||||
Обновить
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={!isLive || pageBusy || schedulerSaveBusy} onClick={() => { void handleRefreshScheduler() }}>
|
||||
<Button variant="outline" size="sm" disabled={!isLive || pageBusy || schedulerSaveBusy || !can("mm:settings:admin")} onClick={() => { void handleRefreshScheduler() }}>
|
||||
Перечитать таймеры
|
||||
</Button>
|
||||
</div>
|
||||
@@ -992,7 +992,7 @@ function DataCollectionContent() {
|
||||
<div className="border-b border-border px-5 py-4">
|
||||
<p className="text-base font-medium">Планировщик сбора данных</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Интервалы и вкл/выкл по задачам. Переключатель сразу сохраняет задачу на бекенде; кнопка ниже — интервалы и срок хранения.
|
||||
Интервалы и вкл/выкл по задачам. Переключатель сохраняет только свой флаг; кнопка ниже — изменённые интервалы и срок хранения. Ручной запуск использует сохранённые настройки, без отправки черновика.
|
||||
</p>
|
||||
</div>
|
||||
<DataCollectionSchedulerDataGrid rows={schedulerGridRows} />
|
||||
|
||||
Reference in new issue
Block a user