import { reactive } from "vue"; import { ApiError, fetchHost, fetchHostRemoteJob, runHostAgentUpdateFallback, updateHostAgentViaSsh, type HostDetail, type HostRemoteActionJobStatus, } from "../api"; import { emitHostPatch } from "../utils/hostPatchBus"; export type HostRemoteActionKind = "fallback" | "ssh-update"; export type HostRemoteActionLogEntry = { id: string; hostId: number; open: boolean; logVisible: boolean; loading: boolean; ok: boolean | null; title: string; message: string; output: string; }; export const hostRemoteActionLogs = reactive([]); const POLL_MS = 1500; const SUCCESS_AUTO_CLOSE_MS = 30_000; let nextLogSeq = 1; const autoCloseTimers = new Map>(); const hostRunningPromises = new Map>(); function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function newLogId(): string { return `ra-${Date.now()}-${nextLogSeq++}`; } function findLogById(entryId: string): HostRemoteActionLogEntry | undefined { return hostRemoteActionLogs.find((e) => e.id === entryId); } function findLoadingLogForHost(hostId: number): HostRemoteActionLogEntry | undefined { return hostRemoteActionLogs.find((e) => e.hostId === hostId && e.loading); } function clearAutoCloseTimer(entryId: string) { const timer = autoCloseTimers.get(entryId); if (timer != null) { clearTimeout(timer); autoCloseTimers.delete(entryId); } } function removeClosedFinishedLog(entryId: string) { const idx = hostRemoteActionLogs.findIndex((e) => e.id === entryId); if (idx < 0) return; const entry = hostRemoteActionLogs[idx]; if (!entry.loading && !entry.open) { hostRemoteActionLogs.splice(idx, 1); } } function scheduleSuccessAutoClose(entryId: string) { clearAutoCloseTimer(entryId); autoCloseTimers.set( entryId, setTimeout(() => { const entry = findLogById(entryId); if (entry) { entry.open = false; removeClosedFinishedLog(entryId); } autoCloseTimers.delete(entryId); }, SUCCESS_AUTO_CLOSE_MS), ); } function applyJobToEntry(entry: HostRemoteActionLogEntry, job: HostRemoteActionJobStatus) { if (job.title) { entry.title = job.title; } if (job.message) { entry.message = job.message; } const nextOutput = (job.output || job.stdout || "").trim(); if (nextOutput) { entry.output = job.output || job.stdout || ""; } } function finishEntry(entry: HostRemoteActionLogEntry, job: HostRemoteActionJobStatus) { if (!entry.loading && entry.ok != null) { return; } entry.loading = false; entry.ok = job.ok ?? job.status === "success"; applyJobToEntry(entry, job); entry.open = true; if (entry.ok) { scheduleSuccessAutoClose(entry.id); } } export function syncHostRemoteActionJobFinished( entryId: string, job: HostRemoteActionJobStatus, ): void { const entry = findLogById(entryId); if (!entry) return; finishEntry(entry, job); void patchHostFromJob(entry.hostId, job); } async function pollRemoteJob(hostId: number, entry: HostRemoteActionLogEntry): Promise { for (;;) { const job = await fetchHostRemoteJob(hostId); applyJobToEntry(entry, job); if (!job.active && job.status !== "running") { return job; } await sleep(POLL_MS); } } async function patchHostFromJob(hostId: number, job: HostRemoteActionJobStatus) { try { const detail: HostDetail = await fetchHost(hostId); if (job.product_version) { detail.product_version = job.product_version; } if (job.agent_update_state) { detail.agent_update_state = job.agent_update_state; } if (job.ok === false && job.message) { detail.agent_update_last_error = job.message; } emitHostPatch(detail); } catch { /* list refresh on next open is enough */ } } async function startRemoteAction(hostId: number, kind: HostRemoteActionKind) { if (kind === "fallback") { await runHostAgentUpdateFallback(hostId); return; } await updateHostAgentViaSsh(hostId); } async function executeRemoteAction( hostId: number, title: string, kind: HostRemoteActionKind, entry: HostRemoteActionLogEntry, ): Promise { try { try { await startRemoteAction(hostId, kind); } catch (e) { if (e instanceof ApiError && e.status === 409) { /* already running — poll existing job */ } else { throw e; } } entry.message = "Выполняется на удалённом хосте… Можно запустить обновление других хостов."; const job = await pollRemoteJob(hostId, entry); finishEntry(entry, job); await patchHostFromJob(hostId, job); return job; } catch (e) { entry.loading = false; entry.ok = false; entry.message = e instanceof Error ? e.message : "Ошибка выполнения"; entry.open = true; return null; } } export async function pollHostRemoteAction( hostId: number, title: string, ): Promise { const existingPromise = hostRunningPromises.get(hostId); if (existingPromise) { const existing = findLoadingLogForHost(hostId); if (existing) { existing.open = true; } return existingPromise; } const entry: HostRemoteActionLogEntry = { id: newLogId(), hostId, open: true, logVisible: false, loading: true, ok: null, title, message: "Выполняется на удалённом хосте…", output: "", }; hostRemoteActionLogs.push(entry); const promise = (async () => { try { const job = await pollRemoteJob(hostId, entry); finishEntry(entry, job); await patchHostFromJob(hostId, job); return job; } catch (e) { entry.loading = false; entry.ok = false; entry.message = e instanceof Error ? e.message : "Ошибка выполнения"; entry.open = true; return null; } })(); hostRunningPromises.set(hostId, promise); try { return await promise; } finally { hostRunningPromises.delete(hostId); } } export async function runHostRemoteAction( hostId: number, title: string, kind: HostRemoteActionKind, ): Promise { const existingPromise = hostRunningPromises.get(hostId); if (existingPromise) { const existing = findLoadingLogForHost(hostId); if (existing) { existing.open = true; } return existingPromise; } const entry: HostRemoteActionLogEntry = { id: newLogId(), hostId, open: true, logVisible: false, loading: true, ok: null, title, message: "Запуск на сервере SAC…", output: "", }; hostRemoteActionLogs.push(entry); const promise = executeRemoteAction(hostId, title, kind, entry); hostRunningPromises.set(hostId, promise); try { return await promise; } finally { hostRunningPromises.delete(hostId); } } export function toggleHostRemoteActionLog(entryId: string) { const entry = findLogById(entryId); if (!entry) return; entry.logVisible = !entry.logVisible; entry.open = true; } export function closeHostRemoteActionLog(entryId: string) { clearAutoCloseTimer(entryId); const entry = findLogById(entryId); if (!entry) return; entry.open = false; if (!entry.loading) { removeClosedFinishedLog(entryId); } } export function showHostRemoteActionLog(hostId?: number) { if (hostId != null) { for (const entry of hostRemoteActionLogs) { if (entry.hostId === hostId && (entry.loading || entry.ok != null)) { entry.open = true; } } return; } for (const entry of hostRemoteActionLogs) { if (entry.loading || entry.ok === false) { entry.open = true; } } } export function isHostRemoteActionActive(hostId: number): boolean { return hostRemoteActionLogs.some((e) => e.hostId === hostId && e.loading); } export function hasActiveHostRemoteAction(): boolean { return hostRemoteActionLogs.some((e) => e.loading); } export function hasOpenRemoteActionLogForHost(hostId: number): boolean { return hostRemoteActionLogs.some((e) => e.hostId === hostId && e.open); } export function anyRemoteActionLogOpen(): boolean { return hostRemoteActionLogs.some((e) => e.open); }