feat: background remote agent updates survive SAC navigation (0.20.19)

This commit is contained in:
2026-06-21 11:01:26 +10:00
parent d7bbcc5337
commit 5635be1322
16 changed files with 642 additions and 161 deletions
@@ -0,0 +1,120 @@
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 const hostRemoteActionLog = reactive({
open: false,
title: "",
loading: false,
ok: null as boolean | null,
message: "",
output: "",
hostId: 0,
});
const POLL_MS = 1500;
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function applyJobToModal(job: HostRemoteActionJobStatus) {
hostRemoteActionLog.title = job.title || hostRemoteActionLog.title;
hostRemoteActionLog.message = job.message || hostRemoteActionLog.message;
hostRemoteActionLog.output = job.output || job.stdout || "";
}
function finishModal(job: HostRemoteActionJobStatus) {
hostRemoteActionLog.loading = false;
hostRemoteActionLog.ok = job.ok ?? job.status === "success";
applyJobToModal(job);
}
async function pollRemoteJob(hostId: number): Promise<HostRemoteActionJobStatus> {
for (;;) {
const job = await fetchHostRemoteJob(hostId);
applyJobToModal(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);
}
export async function runHostRemoteAction(
hostId: number,
title: string,
kind: HostRemoteActionKind,
): Promise<HostRemoteActionJobStatus | null> {
hostRemoteActionLog.open = true;
hostRemoteActionLog.loading = true;
hostRemoteActionLog.ok = null;
hostRemoteActionLog.title = title;
hostRemoteActionLog.message = "Запуск на сервере SAC…";
hostRemoteActionLog.output = "";
hostRemoteActionLog.hostId = hostId;
try {
try {
await startRemoteAction(hostId, kind);
} catch (e) {
if (e instanceof ApiError && e.status === 409) {
/* already running — poll existing job */
} else {
throw e;
}
}
hostRemoteActionLog.message =
"Выполняется на удалённом хосте… Можно перейти в другой раздел SAC.";
const job = await pollRemoteJob(hostId);
finishModal(job);
await patchHostFromJob(hostId, job);
return job;
} catch (e) {
hostRemoteActionLog.loading = false;
hostRemoteActionLog.ok = false;
hostRemoteActionLog.message = e instanceof Error ? e.message : "Ошибка выполнения";
return null;
}
}
export function closeHostRemoteActionLog() {
if (hostRemoteActionLog.loading) return;
hostRemoteActionLog.open = false;
}