/** * Снимок прогона планировщика (JSON из `scheduler_runs.result_json`). * Дублирует контракт `backend/src/types/scheduler-run-snapshot.ts` для UI. */ export interface ServerRestPingSnapshot { serverId: number name: string host: string ok: boolean latencyMs?: number | null error?: string } export interface ServersRestPingRunSnapshot { v: number job: "servers_rest_ping" sampledAt: string skipped?: boolean fatalError?: string servers: ServerRestPingSnapshot[] } export type AlertEngineRuleDiagSnapshot = { ruleId: string inGroup: boolean evalHit: boolean hitTransition?: "problem" | "recovery" | "neutral" hitMessage?: string stabilityOk: boolean cooldownOk: boolean telegramOk: boolean blocked?: | "no_hit" | "stability" | "cooldown" | "no_telegram" | "dedupe_positive" | "in_group" } /** Сводка прогона движка оповещений (JSON из `scheduler_runs` для job `alert_engine`). */ export interface AlertEngineRunSnapshot { v: number job: "alert_engine" sampledAt: string rulesChecked: number standaloneFires: number groupFires: number skippedNoTelegram: boolean errors?: string[] ruleDiag?: AlertEngineRuleDiagSnapshot[] } export interface GreBgpSnapshotRunSnapshot { v: number job: "gre_bgp" sampledAt: string skipped?: boolean fatalError?: string greWritten: number bgpWritten: number errors?: string[] } export interface InternetPathRunSnapshot { v: number job: "internet_path" sampledAt: string homes: number snapshotSaved: boolean fatalError?: string } export interface CertificatesRenewRunSnapshot { v: number job: "certificates_renew" sampledAt: string skipped?: boolean fatalError?: string checked: number renewed: number skippedTargets: number errors: string[] } export interface BackupsRunSnapshot { v: number job: "backups" sampledAt: string skipped?: boolean due: boolean created: number failures: number pruned: number uploadFailures?: number pruneFailures?: number metadataError?: string eventErrors?: string[] errors?: string[] fatalError?: string } export interface GeoipUpdateRunSnapshot { v: number job: "geoip_update" sampledAt: string skipped?: boolean fatalError?: string checked: number downloaded: number skippedUnchanged: number bytes: number errors: string[] } export interface RouterLogsRunSnapshot { v: 1 job: "router_logs" sampledAt: string skipped?: boolean errors: string[] servers: { serverId: number; name: string; added: number; gap: boolean; error?: string }[] } export type SchedulerRunSnapshot = | RouterLogsRunSnapshot | TrafficRunSnapshot | ResourcesRunSnapshot | PingRunSnapshot | SpeedScheduledRunSnapshot | ServersRestPingRunSnapshot | GreBgpSnapshotRunSnapshot | InternetPathRunSnapshot | CertificatesRenewRunSnapshot | BackupsRunSnapshot | GeoipUpdateRunSnapshot | AlertEngineRunSnapshot export interface TrafficServerSnapshot { serverId: number name: string host: string ok: boolean error?: string interfaces?: number sumRxMbps?: number sumTxMbps?: number } export interface TrafficRunSnapshot { v: number job: "traffic" sampledAt: string skipped?: boolean fatalError?: string servers: TrafficServerSnapshot[] } export interface ResourceServerSnapshot { serverId: number name: string host: string status: "online" | "offline" error?: string cpuLoadPct?: number memUsedMb?: number memTotalMb?: number memUsedPct?: number diskFreeMb?: number diskTotalMb?: number uptimeSeconds?: number boardName?: string rosVersion?: string } export interface ResourcesRunSnapshot { v: number job: "uptime_resources" sampledAt: string skipped?: boolean fatalError?: string servers: ResourceServerSnapshot[] } export interface PingProbeSnapshot { probeId: string name: string target: string srcServerId: number srcServerName: string srcInterface: string ok: boolean rttMs: number | null lossPct: number | null status: "up" | "warn" | "down" | "unknown" error?: string } export interface PingRunSnapshot { v: number job: "uptime_ping" sampledAt: string skipped?: boolean fatalError?: string probes: PingProbeSnapshot[] skippedByInterval?: number } export interface SpeedRunSnapshot { probeId: string srcServerId: number srcServerName: string dstServerId: number dstServerName: string srcInterface: string dstInterface: string protocol: string direction: string durationSec: number ok: boolean txAvgMbps?: number | null rxAvgMbps?: number | null pingRttMs?: number | null pingLossPct?: number | null pingError?: string | null error?: string } export interface SpeedScheduledRunSnapshot { v: number job: "uptime_speed" sampledAt: string runs: SpeedRunSnapshot[] } /** PostgreSQL JSONB returns an object; older records may still contain JSON text. */ export function parseSchedulerRunSnapshot(raw: unknown): SchedulerRunSnapshot | null { try { const value: unknown = typeof raw === "string" ? (raw.trim() ? JSON.parse(raw) : null) : raw if (!value || typeof value !== "object" || Array.isArray(value)) return null const o = value as Record if (o.v !== 1 || typeof o.job !== "string" || typeof o.sampledAt !== "string" || !Number.isFinite(Date.parse(o.sampledAt))) return null if (o.skipped !== undefined && typeof o.skipped !== "boolean") return null if (o.fatalError !== undefined && typeof o.fatalError !== "string") return null for (const field of ["errors", "eventErrors"]) if (o[field] !== undefined && (!Array.isArray(o[field]) || !(o[field] as unknown[]).every(error => typeof error === "string"))) return null const arrays: Record = { router_logs: "servers", traffic: "servers", uptime_resources: "servers", servers_rest_ping: "servers", uptime_ping: "probes", uptime_speed: "runs" } if (Object.hasOwn(arrays, o.job)) { if (!Array.isArray(o[arrays[o.job]])) return null } else { const fields: Record = { gre_bgp: ["greWritten", "bgpWritten"], internet_path: ["homes"], certificates_renew: ["checked", "renewed", "skippedTargets"], backups: ["created", "failures", "pruned"], geoip_update: ["checked", "downloaded", "skippedUnchanged", "bytes"], alert_engine: ["rulesChecked", "standaloneFires", "groupFires"] } if (!Object.hasOwn(fields, o.job) || !fields[o.job].every(field => typeof o[field] === "number" && Number.isSafeInteger(o[field]) && (o[field] as number) >= 0)) return null if (["certificates_renew", "geoip_update"].includes(o.job) && !Array.isArray(o.errors)) return null if (o.job === "backups") { if (typeof o.due !== "boolean") return null for (const field of ["uploadFailures", "pruneFailures"]) if (o[field] !== undefined && (typeof o[field] !== "number" || !Number.isSafeInteger(o[field]) || (o[field] as number) < 0)) return null if (o.metadataError !== undefined && typeof o.metadataError !== "string") return null } if (o.job === "internet_path" && typeof o.snapshotSaved !== "boolean") return null if (o.job === "alert_engine" && typeof o.skippedNoTelegram !== "boolean") return null } return o as unknown as SchedulerRunSnapshot } catch { return null } }