Files
MikrotikManager/shared/api/scheduler.ts

49 lines
4.9 KiB
TypeScript

import { z } from "zod"
import { SCHEDULER_JOB_KEYS, type SchedulerJobKey } from "@/lib/scheduler-settings"
import { parseSchedulerRunSnapshot } from "@/lib/scheduler-run-snapshot"
import { requestJson } from "./http-client"
const key = z.enum(SCHEDULER_JOB_KEYS)
const count = z.number().int().nonnegative().safe()
const timestamp = z.string().min(1).refine(value => Number.isFinite(Date.parse(value)), "Некорректное время планировщика").transform(value => new Date(value).toISOString())
const jobStatus = z.object({ jobKey: key, enabled: z.boolean(), intervalSec: count.positive(), running: z.boolean(), lastFinishedAt: timestamp.nullable(), lastStatus: z.enum(["ok", "error"]).nullable(), lastDurationMs: count.nullable(), lastError: z.string().nullable() })
const statusSchema = z.object({ jobs: z.array(jobStatus).refine(jobs => new Set(jobs.map(job => job.jobKey)).size === jobs.length, "Повторяющиеся задачи планировщика") })
const runSchema = z.object({ id: z.string().min(1), jobKey: key, startedAt: timestamp, finishedAt: timestamp, status: z.enum(["ok", "error"]), error: z.string().nullable(), durationMs: count, resultJson: z.unknown().optional() })
const runsSchema = z.object({ runs: z.array(runSchema).refine(rows => new Set(rows.map(row => row.id)).size === rows.length, "Повторяющиеся записи планировщика") })
const receiptSchema = z.object({ status: z.enum(["ok", "error"]), error: z.string().nullable(), result: z.unknown(), eventError: z.string().optional() }).refine(receipt => receipt.status === "ok" ? receipt.error === null : !!receipt.error, "Статус запуска не согласован с ошибкой")
const responseSchema = z.object({ ok: z.boolean(), result: receiptSchema, status: statusSchema.nullable(), statusError: z.string().optional() }).refine(response => response.ok === (response.result.status === "ok") && (response.status !== null || !!response.statusError), "Подтверждение запуска не согласовано с результатом")
function confirmed<T>(schema: z.ZodType<T>, raw: unknown): T {
const parsed = schema.safeParse(raw)
if (!parsed.success) throw new Error("Ответ планировщика не подтверждён: получены некорректные данные")
return parsed.data
}
function options(init?: RequestInit): RequestInit {
return { ...init, signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(30_000)]) }
}
async function json(baseUrl: string, path: string, init?: RequestInit) {
return requestJson<unknown>(baseUrl, path, options(init))
}
export async function getSchedulerStatus(baseUrl: string, init?: RequestInit) {
return confirmed(statusSchema, await json(baseUrl, "/api/scheduler/status", { ...init, method: "GET" }))
}
export async function listSchedulerRuns(baseUrl: string, filter: { jobKey?: SchedulerJobKey; limit?: number; offset?: number } = {}, init?: RequestInit) {
const query = z.object({ jobKey: key.optional(), limit: count.min(1).max(200).optional(), offset: count.optional() }).parse(filter)
const params = new URLSearchParams({ limit: String(query.limit ?? 80) })
if (query.jobKey) params.set("jobKey", query.jobKey)
if (query.offset !== undefined) params.set("offset", String(query.offset))
const result = confirmed(runsSchema, await json(baseUrl, "/api/scheduler/runs?" + params.toString(), { ...init, method: "GET" }))
if (query.jobKey && result.runs.some(run => run.jobKey !== query.jobKey)) throw new Error("Журнал относится к другой задаче планировщика")
return result
}
export async function runSchedulerJobNow(baseUrl: string, jobKey: SchedulerJobKey, init?: RequestInit) {
const target = key.parse(jobKey)
const response = confirmed(responseSchema, await json(baseUrl, "/api/scheduler/jobs/" + encodeURIComponent(target) + "/run-now", { ...init, method: "POST" }))
const snapshot = parseSchedulerRunSnapshot(response.result.result)
if (!snapshot || snapshot.job !== target) throw new Error("Результат запуска не подтверждён для выбранной задачи планировщика")
const snapshotHasErrors = ("fatalError" in snapshot && !!snapshot.fatalError) || ("errors" in snapshot && !!snapshot.errors?.length) || (snapshot.job === "backups" && (!!snapshot.failures || !!snapshot.uploadFailures || !!snapshot.pruneFailures || !!snapshot.metadataError || !!snapshot.eventErrors?.length))
if (response.ok && snapshotHasErrors) throw new Error("Успешный статус не согласован с ошибками снимка планировщика")
return { ...response, result: { ...response.result, result: snapshot } }
}
export async function refreshScheduler(baseUrl: string, init?: RequestInit) {
return confirmed(z.object({ ok: z.literal(true), status: statusSchema }), await json(baseUrl, "/api/scheduler/refresh", { ...init, method: "POST" }))
}