From 310e7f699dbf11503a0b48283709b9629f69c2ec Mon Sep 17 00:00:00 2001 From: PTah Date: Wed, 8 Jul 2026 15:05:56 +1000 Subject: [PATCH] fix: ignore stale SSH update job until running or session start (v0.5.10) Modal finish only after sawRunning or started_at after sessionOpenedAt, so a previous success no longer closes the window instantly while SAC updater is still running on the host. Co-authored-by: Cursor --- backend/app/version.py | 2 +- .../src/components/HostActionLogModal.vue | 33 ++++++++++++++++++- .../src/components/HostActionLogStack.vue | 1 + .../src/composables/useHostRemoteAction.ts | 4 +++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/backend/app/version.py b/backend/app/version.py index e0d7854..964ca1c 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.5.9" +APP_VERSION = "0.5.10" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/frontend/src/components/HostActionLogModal.vue b/frontend/src/components/HostActionLogModal.vue index 3287b72..e0e1dcb 100644 --- a/frontend/src/components/HostActionLogModal.vue +++ b/frontend/src/components/HostActionLogModal.vue @@ -79,6 +79,7 @@ const props = defineProps<{ message: string; output: string; logVisible: boolean; + sessionStartedAt: string; }>(); const emit = defineEmits<{ @@ -98,6 +99,28 @@ const autoCloseSec = ref(0); let pollTimer: ReturnType | null = null; let countdownTimer: ReturnType | null = null; let finishedEmitted = false; +let sawRunning = false; + +function isTerminalJob(job: HostRemoteActionJobStatus): boolean { + const st = (job.status || "").toLowerCase(); + return st === "success" || st === "failed"; +} + +function jobStartedAfterSession(job: HostRemoteActionJobStatus): boolean { + const started = job.started_at; + if (!started) return false; + const jobMs = Date.parse(started); + const sessionMs = Date.parse(props.sessionStartedAt); + if (Number.isNaN(jobMs) || Number.isNaN(sessionMs)) return false; + return jobMs >= sessionMs - 3000; +} + +function isJobRunning(job: HostRemoteActionJobStatus): boolean { + if (job.active) return true; + const st = (job.status || "").toLowerCase(); + if (st === "running") return true; + return (job.agent_update_state || "").toLowerCase() === "running"; +} const displayTitle = computed(() => { if (localTitle.value) return localTitle.value; @@ -159,7 +182,14 @@ async function pollJobOnce() { if (out) polledOutput.value = out; if (job.message && isLoading.value) polledMessage.value = job.message; - if (!job.active && job.status !== "running") { + if (isJobRunning(job)) { + sawRunning = true; + return; + } + + const canFinish = + isTerminalJob(job) && (sawRunning || jobStartedAfterSession(job)); + if (canFinish) { if (!finishedEmitted) { finishedEmitted = true; applyFinishedJob(job); @@ -180,6 +210,7 @@ function startPoll() { function resetLocalState() { finishedEmitted = false; + sawRunning = false; isLoading.value = true; ok.value = null; localTitle.value = ""; diff --git a/frontend/src/components/HostActionLogStack.vue b/frontend/src/components/HostActionLogStack.vue index b96d821..218aca7 100644 --- a/frontend/src/components/HostActionLogStack.vue +++ b/frontend/src/components/HostActionLogStack.vue @@ -11,6 +11,7 @@ :message="entry.message" :output="entry.output" :log-visible="entry.logVisible" + :session-started-at="entry.sessionStartedAt" @close="closeHostRemoteActionLog(entry.id)" @toggle-log="toggleHostRemoteActionLog(entry.id)" @finished="syncHostRemoteActionJobFinished(entry.id, $event)" diff --git a/frontend/src/composables/useHostRemoteAction.ts b/frontend/src/composables/useHostRemoteAction.ts index 1d78984..921bd5d 100644 --- a/frontend/src/composables/useHostRemoteAction.ts +++ b/frontend/src/composables/useHostRemoteAction.ts @@ -22,6 +22,8 @@ export type HostRemoteActionLogEntry = { title: string; message: string; output: string; + /** ISO-время открытия окна — отсекаем stale success от прошлого job */ + sessionStartedAt: string; }; export const hostRemoteActionLogs = reactive([]); @@ -207,6 +209,7 @@ export async function pollHostRemoteAction( title, message: "Выполняется на удалённом хосте…", output: "", + sessionStartedAt: new Date().toISOString(), }; hostRemoteActionLogs.push(entry); @@ -257,6 +260,7 @@ export async function runHostRemoteAction( title, message: "Запуск на сервере SAC…", output: "", + sessionStartedAt: new Date().toISOString(), }; hostRemoteActionLogs.push(entry);