feat: parallel agent upgrades with per-host log panels (0.4.2)
Allow multiple remote update jobs at once; each upgrade gets its own bottom-right log dock that auto-closes 30s after success. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||||
|
|
||||||
APP_NAME = "Security Alert Center"
|
APP_NAME = "Security Alert Center"
|
||||||
APP_VERSION = "0.4.1"
|
APP_VERSION = "0.4.2"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
+2
-15
@@ -4,16 +4,7 @@
|
|||||||
<main :class="showShell ? 'app-main' : ''">
|
<main :class="showShell ? 'app-main' : ''">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
</main>
|
||||||
<HostActionLogModal
|
<HostActionLogStack v-if="showShell" />
|
||||||
v-if="showShell"
|
|
||||||
:open="hostRemoteActionLog.open"
|
|
||||||
:title="hostRemoteActionLog.title"
|
|
||||||
:loading="hostRemoteActionLog.loading"
|
|
||||||
:ok="hostRemoteActionLog.ok"
|
|
||||||
:message="hostRemoteActionLog.message"
|
|
||||||
:output="hostRemoteActionLog.output"
|
|
||||||
@close="closeHostRemoteActionLog"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -22,11 +13,7 @@ import { computed, onMounted } from "vue";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { getToken } from "./api";
|
import { getToken } from "./api";
|
||||||
import AppSidebar from "./components/AppSidebar.vue";
|
import AppSidebar from "./components/AppSidebar.vue";
|
||||||
import HostActionLogModal from "./components/HostActionLogModal.vue";
|
import HostActionLogStack from "./components/HostActionLogStack.vue";
|
||||||
import {
|
|
||||||
closeHostRemoteActionLog,
|
|
||||||
hostRemoteActionLog,
|
|
||||||
} from "./composables/useHostRemoteAction";
|
|
||||||
import { refreshSessionRole } from "./router";
|
import { refreshSessionRole } from "./router";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
v-if="remoteJobRunning && !remoteJobPanelOpen"
|
v-if="remoteJobRunning && !remoteJobPanelOpen"
|
||||||
type="button"
|
type="button"
|
||||||
class="sac-nav-item sac-remote-job-badge"
|
class="sac-nav-item sac-remote-job-badge"
|
||||||
title="Обновление агента выполняется на сервере SAC"
|
title="Обновления агентов выполняются на сервере SAC"
|
||||||
@click="showHostRemoteActionLog"
|
@click="showHostRemoteActionLog()"
|
||||||
>
|
>
|
||||||
<span class="sac-nav-icon sac-remote-job-spinner" aria-hidden="true" />
|
<span class="sac-nav-icon sac-remote-job-spinner" aria-hidden="true" />
|
||||||
<span class="sac-nav-label">Обновление…</span>
|
<span class="sac-nav-label">Обновления…</span>
|
||||||
</button>
|
</button>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-for="item in navItems"
|
v-for="item in navItems"
|
||||||
@@ -46,8 +46,8 @@ import { clearToken, isAdmin } from "../api";
|
|||||||
import SidebarSystemStats from "./SidebarSystemStats.vue";
|
import SidebarSystemStats from "./SidebarSystemStats.vue";
|
||||||
import { useSacVersion } from "../composables/useSacVersion";
|
import { useSacVersion } from "../composables/useSacVersion";
|
||||||
import {
|
import {
|
||||||
|
anyRemoteActionLogOpen,
|
||||||
hasActiveHostRemoteAction,
|
hasActiveHostRemoteAction,
|
||||||
hostRemoteActionLog,
|
|
||||||
showHostRemoteActionLog,
|
showHostRemoteActionLog,
|
||||||
} from "../composables/useHostRemoteAction";
|
} from "../composables/useHostRemoteAction";
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ const { appName, appVersion } = useSacVersion();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const remoteJobRunning = computed(() => hasActiveHostRemoteAction());
|
const remoteJobRunning = computed(() => hasActiveHostRemoteAction());
|
||||||
const remoteJobPanelOpen = computed(() => hostRemoteActionLog.open);
|
const remoteJobPanelOpen = computed(() => anyRemoteActionLogOpen());
|
||||||
|
|
||||||
const navItems = computed(() => {
|
const navItems = computed(() => {
|
||||||
const items = [
|
const items = [
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="loading" class="host-action-log-status">
|
<p v-if="loading" class="host-action-log-status">
|
||||||
<span class="host-action-log-spinner" aria-hidden="true" />
|
<span class="host-action-log-spinner" aria-hidden="true" />
|
||||||
Выполняется на удалённом хосте… Можно перейти в другой раздел SAC.
|
Выполняется на удалённом хосте… Можно запустить обновление других хостов.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="message && !loading" class="host-action-log-message" :class="messageClass">{{ message }}</p>
|
<p v-if="message && !loading" class="host-action-log-message" :class="messageClass">{{ message }}</p>
|
||||||
<p v-else-if="message && loading" class="muted host-action-log-sub host-action-log-message">{{ message }}</p>
|
<p v-else-if="message && loading" class="muted host-action-log-sub host-action-log-message">{{ message }}</p>
|
||||||
@@ -56,12 +56,7 @@ const messageClass = computed(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.host-action-log-dock {
|
.host-action-log-dock {
|
||||||
position: fixed;
|
width: 100%;
|
||||||
right: 1rem;
|
|
||||||
bottom: 1rem;
|
|
||||||
z-index: 1000;
|
|
||||||
width: min(56rem, calc(100vw - 2rem));
|
|
||||||
max-width: calc(100vw - 2rem);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="openLogs.length" class="host-action-log-stack" aria-live="polite">
|
||||||
|
<HostActionLogModal
|
||||||
|
v-for="entry in openLogs"
|
||||||
|
:key="entry.id"
|
||||||
|
:open="true"
|
||||||
|
:title="entry.title"
|
||||||
|
:loading="entry.loading"
|
||||||
|
:ok="entry.ok"
|
||||||
|
:message="entry.message"
|
||||||
|
:output="entry.output"
|
||||||
|
@close="closeHostRemoteActionLog(entry.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import HostActionLogModal from "./HostActionLogModal.vue";
|
||||||
|
import {
|
||||||
|
closeHostRemoteActionLog,
|
||||||
|
hostRemoteActionLogs,
|
||||||
|
} from "../composables/useHostRemoteAction";
|
||||||
|
|
||||||
|
const openLogs = computed(() => hostRemoteActionLogs.filter((e) => e.open));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.host-action-log-stack {
|
||||||
|
position: fixed;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.65rem;
|
||||||
|
width: min(56rem, calc(100vw - 2rem));
|
||||||
|
max-height: min(90vh, 48rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-action-log-stack :deep(.host-action-log-panel) {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-height: min(36rem, 45vh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,11 +7,8 @@
|
|||||||
v-if="showUpgrade"
|
v-if="showUpgrade"
|
||||||
type="button"
|
type="button"
|
||||||
class="agent-version-upgrade"
|
class="agent-version-upgrade"
|
||||||
:class="{
|
:class="{ 'agent-version-upgrade--updating': updating }"
|
||||||
'agent-version-upgrade--inactive': upgradeBlocked,
|
:disabled="updating"
|
||||||
'agent-version-upgrade--updating': updating,
|
|
||||||
}"
|
|
||||||
:disabled="updating || upgradeBlocked"
|
|
||||||
:title="buttonTitle"
|
:title="buttonTitle"
|
||||||
@click.stop="emit('upgrade')"
|
@click.stop="emit('upgrade')"
|
||||||
>
|
>
|
||||||
@@ -35,8 +32,6 @@ const props = defineProps<{
|
|||||||
host: HostSummary;
|
host: HostSummary;
|
||||||
git: AgentGitVersions | null | undefined;
|
git: AgentGitVersions | null | undefined;
|
||||||
updating?: boolean;
|
updating?: boolean;
|
||||||
/** Другой хост сейчас обновляется — стрелка видна, но неактивна (серая). */
|
|
||||||
upgradeBlocked?: boolean;
|
|
||||||
outdatedHighlight?: boolean;
|
outdatedHighlight?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -65,9 +60,6 @@ const buttonTitle = computed(() => {
|
|||||||
if (props.updating) {
|
if (props.updating) {
|
||||||
return "Обновление выполняется…";
|
return "Обновление выполняется…";
|
||||||
}
|
}
|
||||||
if (props.upgradeBlocked) {
|
|
||||||
return "Дождитесь завершения обновления на другом хосте";
|
|
||||||
}
|
|
||||||
return upgradeTitle.value;
|
return upgradeTitle.value;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -127,13 +119,11 @@ const buttonTitle = computed(() => {
|
|||||||
background: rgba(63, 185, 80, 0.28);
|
background: rgba(63, 185, 80, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-version-upgrade--inactive .agent-version-upgrade__arrow,
|
|
||||||
.agent-version-upgrade--updating .agent-version-upgrade__arrow {
|
.agent-version-upgrade--updating .agent-version-upgrade__arrow {
|
||||||
background: rgba(139, 148, 158, 0.2);
|
background: rgba(139, 148, 158, 0.2);
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-version-upgrade--inactive .agent-version-upgrade__target,
|
|
||||||
.agent-version-upgrade--updating .agent-version-upgrade__target {
|
.agent-version-upgrade--updating .agent-version-upgrade__target {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -12,60 +12,94 @@ import { emitHostPatch } from "../utils/hostPatchBus";
|
|||||||
|
|
||||||
export type HostRemoteActionKind = "fallback" | "ssh-update";
|
export type HostRemoteActionKind = "fallback" | "ssh-update";
|
||||||
|
|
||||||
export const hostRemoteActionLog = reactive({
|
export type HostRemoteActionLogEntry = {
|
||||||
open: false,
|
id: string;
|
||||||
title: "",
|
hostId: number;
|
||||||
loading: false,
|
open: boolean;
|
||||||
ok: null as boolean | null,
|
loading: boolean;
|
||||||
message: "",
|
ok: boolean | null;
|
||||||
output: "",
|
title: string;
|
||||||
hostId: 0,
|
message: string;
|
||||||
});
|
output: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hostRemoteActionLogs = reactive<HostRemoteActionLogEntry[]>([]);
|
||||||
|
|
||||||
const POLL_MS = 1500;
|
const POLL_MS = 1500;
|
||||||
const SUCCESS_AUTO_CLOSE_MS = 30_000;
|
const SUCCESS_AUTO_CLOSE_MS = 30_000;
|
||||||
|
|
||||||
let successAutoCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
let nextLogSeq = 1;
|
||||||
|
const autoCloseTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
const hostRunningPromises = new Map<number, Promise<HostRemoteActionJobStatus | null>>();
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSuccessAutoCloseTimer() {
|
function newLogId(): string {
|
||||||
if (successAutoCloseTimer != null) {
|
return `ra-${Date.now()}-${nextLogSeq++}`;
|
||||||
clearTimeout(successAutoCloseTimer);
|
}
|
||||||
successAutoCloseTimer = null;
|
|
||||||
|
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 scheduleSuccessAutoClose() {
|
function removeClosedFinishedLog(entryId: string) {
|
||||||
clearSuccessAutoCloseTimer();
|
const idx = hostRemoteActionLogs.findIndex((e) => e.id === entryId);
|
||||||
successAutoCloseTimer = setTimeout(() => {
|
if (idx < 0) return;
|
||||||
hostRemoteActionLog.open = false;
|
const entry = hostRemoteActionLogs[idx];
|
||||||
successAutoCloseTimer = null;
|
if (!entry.loading && !entry.open) {
|
||||||
}, SUCCESS_AUTO_CLOSE_MS);
|
hostRemoteActionLogs.splice(idx, 1);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
hostRemoteActionLog.open = true;
|
|
||||||
if (hostRemoteActionLog.ok) {
|
|
||||||
scheduleSuccessAutoClose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollRemoteJob(hostId: number): Promise<HostRemoteActionJobStatus> {
|
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) {
|
||||||
|
entry.title = job.title || entry.title;
|
||||||
|
entry.message = job.message || entry.message;
|
||||||
|
entry.output = job.output || job.stdout || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishEntry(entry: HostRemoteActionLogEntry, job: HostRemoteActionJobStatus) {
|
||||||
|
entry.loading = false;
|
||||||
|
entry.ok = job.ok ?? job.status === "success";
|
||||||
|
applyJobToEntry(entry, job);
|
||||||
|
entry.open = true;
|
||||||
|
if (entry.ok) {
|
||||||
|
scheduleSuccessAutoClose(entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollRemoteJob(hostId: number, entry: HostRemoteActionLogEntry): Promise<HostRemoteActionJobStatus> {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const job = await fetchHostRemoteJob(hostId);
|
const job = await fetchHostRemoteJob(hostId);
|
||||||
applyJobToModal(job);
|
applyJobToEntry(entry, job);
|
||||||
if (!job.active && job.status !== "running") {
|
if (!job.active && job.status !== "running") {
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
@@ -99,20 +133,12 @@ async function startRemoteAction(hostId: number, kind: HostRemoteActionKind) {
|
|||||||
await updateHostAgentViaSsh(hostId);
|
await updateHostAgentViaSsh(hostId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runHostRemoteAction(
|
async function executeRemoteAction(
|
||||||
hostId: number,
|
hostId: number,
|
||||||
title: string,
|
title: string,
|
||||||
kind: HostRemoteActionKind,
|
kind: HostRemoteActionKind,
|
||||||
|
entry: HostRemoteActionLogEntry,
|
||||||
): Promise<HostRemoteActionJobStatus | null> {
|
): Promise<HostRemoteActionJobStatus | null> {
|
||||||
clearSuccessAutoCloseTimer();
|
|
||||||
hostRemoteActionLog.open = true;
|
|
||||||
hostRemoteActionLog.loading = true;
|
|
||||||
hostRemoteActionLog.ok = null;
|
|
||||||
hostRemoteActionLog.title = title;
|
|
||||||
hostRemoteActionLog.message = "Запуск на сервере SAC…";
|
|
||||||
hostRemoteActionLog.output = "";
|
|
||||||
hostRemoteActionLog.hostId = hostId;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await startRemoteAction(hostId, kind);
|
await startRemoteAction(hostId, kind);
|
||||||
@@ -123,31 +149,93 @@ export async function runHostRemoteAction(
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hostRemoteActionLog.message =
|
entry.message = "Выполняется на удалённом хосте… Можно запустить обновление других хостов.";
|
||||||
"Выполняется на удалённом хосте… Можно перейти в другой раздел SAC.";
|
const job = await pollRemoteJob(hostId, entry);
|
||||||
const job = await pollRemoteJob(hostId);
|
finishEntry(entry, job);
|
||||||
finishModal(job);
|
|
||||||
await patchHostFromJob(hostId, job);
|
await patchHostFromJob(hostId, job);
|
||||||
return job;
|
return job;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hostRemoteActionLog.loading = false;
|
entry.loading = false;
|
||||||
hostRemoteActionLog.ok = false;
|
entry.ok = false;
|
||||||
hostRemoteActionLog.message = e instanceof Error ? e.message : "Ошибка выполнения";
|
entry.message = e instanceof Error ? e.message : "Ошибка выполнения";
|
||||||
|
entry.open = true;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeHostRemoteActionLog() {
|
export async function runHostRemoteAction(
|
||||||
clearSuccessAutoCloseTimer();
|
hostId: number,
|
||||||
hostRemoteActionLog.open = false;
|
title: string,
|
||||||
|
kind: HostRemoteActionKind,
|
||||||
|
): Promise<HostRemoteActionJobStatus | null> {
|
||||||
|
const existingPromise = hostRunningPromises.get(hostId);
|
||||||
|
if (existingPromise) {
|
||||||
|
const existing = findLoadingLogForHost(hostId);
|
||||||
|
if (existing) {
|
||||||
|
existing.open = true;
|
||||||
|
}
|
||||||
|
return existingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showHostRemoteActionLog() {
|
const entry: HostRemoteActionLogEntry = {
|
||||||
if (hostRemoteActionLog.loading || hostRemoteActionLog.ok != null) {
|
id: newLogId(),
|
||||||
hostRemoteActionLog.open = true;
|
hostId,
|
||||||
|
open: true,
|
||||||
|
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 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 {
|
export function hasActiveHostRemoteAction(): boolean {
|
||||||
return hostRemoteActionLog.loading;
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||||
export const APP_NAME = "Security Alert Center";
|
export const APP_NAME = "Security Alert Center";
|
||||||
export const APP_VERSION = "0.4.0";
|
export const APP_VERSION = "0.4.2";
|
||||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
v-if="host"
|
v-if="host"
|
||||||
:host="host"
|
:host="host"
|
||||||
:git="agentGitVersions"
|
:git="agentGitVersions"
|
||||||
:updating="updatingAgent || runningAgentFallback"
|
:updating="isHostRemoteActionActive(hostId)"
|
||||||
:outdated-highlight="host.version_status === 'outdated'"
|
:outdated-highlight="host.version_status === 'outdated'"
|
||||||
@upgrade="runAgentUpgradeFromCell"
|
@upgrade="runAgentUpgradeFromCell"
|
||||||
/>
|
/>
|
||||||
@@ -91,13 +91,13 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
:disabled="runningAgentFallback"
|
:disabled="isHostRemoteActionActive(hostId)"
|
||||||
@click="runAgentFallback"
|
@click="runAgentFallback"
|
||||||
>
|
>
|
||||||
{{ runningAgentFallback ? "Обновление…" : "Обновить через WinRM" }}
|
{{ isHostRemoteActionActive(hostId) ? "Обновление…" : "Обновить через WinRM" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="agentControlMessage && !hostRemoteActionLog.open" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
<p v-if="agentControlMessage && !hasOpenRemoteActionLogForHost(hostId)" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isLinuxHost" class="host-actions">
|
<div v-if="isLinuxHost" class="host-actions">
|
||||||
<div class="host-actions-row">
|
<div class="host-actions-row">
|
||||||
@@ -119,10 +119,10 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
:disabled="updatingAgent"
|
:disabled="isHostRemoteActionActive(hostId)"
|
||||||
@click="runAgentUpdate"
|
@click="runAgentUpdate"
|
||||||
>
|
>
|
||||||
{{ updatingAgent ? "Обновление…" : "Обновить ssh-monitor (SSH)" }}
|
{{ isHostRemoteActionActive(hostId) ? "Обновление…" : "Обновить ssh-monitor (SSH)" }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -135,13 +135,13 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
:disabled="runningAgentFallback"
|
:disabled="isHostRemoteActionActive(hostId)"
|
||||||
@click="runAgentFallback"
|
@click="runAgentFallback"
|
||||||
>
|
>
|
||||||
{{ runningAgentFallback ? "Fallback…" : "Fallback SSH" }}
|
{{ isHostRemoteActionActive(hostId) ? "Fallback…" : "Fallback SSH" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="agentControlMessage && !hostRemoteActionLog.open" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
<p v-if="agentControlMessage && !hasOpenRemoteActionLogForHost(hostId)" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
||||||
<div v-if="showSshTestFeedback && (sshTestMessage || sshTestOutput)" class="host-action-feedback">
|
<div v-if="showSshTestFeedback && (sshTestMessage || sshTestOutput)" class="host-action-feedback">
|
||||||
<p :class="sshTestOk ? 'success' : 'error'">{{ sshTestMessage }}</p>
|
<p :class="sshTestOk ? 'success' : 'error'">{{ sshTestMessage }}</p>
|
||||||
<pre v-if="sshTestOutput" class="host-ssh-output">{{ sshTestOutput }}</pre>
|
<pre v-if="sshTestOutput" class="host-ssh-output">{{ sshTestOutput }}</pre>
|
||||||
@@ -289,7 +289,8 @@ import { computed, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useSacLiveEventStream } from "../composables/useSacLiveEventStream";
|
import { useSacLiveEventStream } from "../composables/useSacLiveEventStream";
|
||||||
import {
|
import {
|
||||||
hostRemoteActionLog,
|
hasOpenRemoteActionLogForHost,
|
||||||
|
isHostRemoteActionActive,
|
||||||
runHostRemoteAction,
|
runHostRemoteAction,
|
||||||
} from "../composables/useHostRemoteAction";
|
} from "../composables/useHostRemoteAction";
|
||||||
import {
|
import {
|
||||||
@@ -338,13 +339,11 @@ const winRmTestMessage = ref("");
|
|||||||
const winRmTestOk = ref(false);
|
const winRmTestOk = ref(false);
|
||||||
const showWinRmTestFeedback = ref(false);
|
const showWinRmTestFeedback = ref(false);
|
||||||
const testingSsh = ref(false);
|
const testingSsh = ref(false);
|
||||||
const updatingAgent = ref(false);
|
|
||||||
const showSshTestFeedback = ref(false);
|
const showSshTestFeedback = ref(false);
|
||||||
const sshTestMessage = ref("");
|
const sshTestMessage = ref("");
|
||||||
const sshTestOk = ref(false);
|
const sshTestOk = ref(false);
|
||||||
const sshTestOutput = ref("");
|
const sshTestOutput = ref("");
|
||||||
const requestingAgentUpdate = ref(false);
|
const requestingAgentUpdate = ref(false);
|
||||||
const runningAgentFallback = ref(false);
|
|
||||||
const agentControlMessage = ref("");
|
const agentControlMessage = ref("");
|
||||||
const agentControlOk = ref(false);
|
const agentControlOk = ref(false);
|
||||||
const savingAgentConfig = ref(false);
|
const savingAgentConfig = ref(false);
|
||||||
@@ -637,10 +636,8 @@ async function runRequestAgentUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runAgentFallback() {
|
async function runAgentFallback() {
|
||||||
runningAgentFallback.value = true;
|
|
||||||
agentControlMessage.value = "";
|
agentControlMessage.value = "";
|
||||||
const title = isWindowsHost.value ? "Обновление через WinRM" : "Fallback SSH (ssh-monitor)";
|
const title = isWindowsHost.value ? "Обновление через WinRM" : "Fallback SSH (ssh-monitor)";
|
||||||
try {
|
|
||||||
const job = await runHostRemoteAction(hostId.value, title, "fallback");
|
const job = await runHostRemoteAction(hostId.value, title, "fallback");
|
||||||
if (job) {
|
if (job) {
|
||||||
agentControlOk.value = job.ok ?? false;
|
agentControlOk.value = job.ok ?? false;
|
||||||
@@ -651,9 +648,6 @@ async function runAgentFallback() {
|
|||||||
agentControlMessage.value = job.message || "";
|
agentControlMessage.value = job.message || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
runningAgentFallback.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAgentConfig() {
|
async function saveAgentConfig() {
|
||||||
@@ -708,8 +702,6 @@ async function loadHost() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runAgentUpdate() {
|
async function runAgentUpdate() {
|
||||||
updatingAgent.value = true;
|
|
||||||
try {
|
|
||||||
const job = await runHostRemoteAction(
|
const job = await runHostRemoteAction(
|
||||||
hostId.value,
|
hostId.value,
|
||||||
"Обновление ssh-monitor (SSH)",
|
"Обновление ssh-monitor (SSH)",
|
||||||
@@ -723,21 +715,15 @@ async function runAgentUpdate() {
|
|||||||
applyHostDetail(detail);
|
applyHostDetail(detail);
|
||||||
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
updatingAgent.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAgentUpgradeFromCell() {
|
async function runAgentUpgradeFromCell() {
|
||||||
if (!host.value) return;
|
if (!host.value) return;
|
||||||
const kind = remoteActionKindForHost(host.value);
|
const kind = remoteActionKindForHost(host.value);
|
||||||
if (!kind) return;
|
if (!kind) return;
|
||||||
if (kind === "ssh-update") {
|
if (isHostRemoteActionActive(hostId.value)) {
|
||||||
await runAgentUpdate();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runningAgentFallback.value = true;
|
|
||||||
try {
|
|
||||||
const job = await runHostRemoteAction(
|
const job = await runHostRemoteAction(
|
||||||
hostId.value,
|
hostId.value,
|
||||||
remoteActionTitleForHost(host.value),
|
remoteActionTitleForHost(host.value),
|
||||||
@@ -751,9 +737,6 @@ async function runAgentUpgradeFromCell() {
|
|||||||
applyHostDetail(detail);
|
applyHostDetail(detail);
|
||||||
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
runningAgentFallback.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLiveRefreshTimer() {
|
function clearLiveRefreshTimer() {
|
||||||
|
|||||||
@@ -71,8 +71,7 @@
|
|||||||
<HostAgentVersionUpgrade
|
<HostAgentVersionUpgrade
|
||||||
:host="h"
|
:host="h"
|
||||||
:git="data"
|
:git="data"
|
||||||
:updating="upgradingHostId === h.id"
|
:updating="isHostRemoteActionActive(h.id)"
|
||||||
:upgrade-blocked="upgradingHostId != null && upgradingHostId !== h.id"
|
|
||||||
:outdated-highlight="isVersionOutdated(h)"
|
:outdated-highlight="isVersionOutdated(h)"
|
||||||
@upgrade="startAgentUpgrade(h)"
|
@upgrade="startAgentUpgrade(h)"
|
||||||
/>
|
/>
|
||||||
@@ -132,7 +131,11 @@ import {
|
|||||||
remoteActionKindForHost,
|
remoteActionKindForHost,
|
||||||
remoteActionTitleForHost,
|
remoteActionTitleForHost,
|
||||||
} from "../utils/hostAgentUpgrade";
|
} from "../utils/hostAgentUpgrade";
|
||||||
import { runHostRemoteAction } from "../composables/useHostRemoteAction";
|
import {
|
||||||
|
isHostRemoteActionActive,
|
||||||
|
runHostRemoteAction,
|
||||||
|
showHostRemoteActionLog,
|
||||||
|
} from "../composables/useHostRemoteAction";
|
||||||
import HostAgentVersionUpgrade from "../components/HostAgentVersionUpgrade.vue";
|
import HostAgentVersionUpgrade from "../components/HostAgentVersionUpgrade.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -197,7 +200,6 @@ const initialSort = loadHostsSortPrefs();
|
|||||||
const sortBy = ref<SortKey>(initialSort.sortBy);
|
const sortBy = ref<SortKey>(initialSort.sortBy);
|
||||||
const sortDir = ref<"asc" | "desc">(initialSort.sortDir);
|
const sortDir = ref<"asc" | "desc">(initialSort.sortDir);
|
||||||
const deletingId = ref<number | null>(null);
|
const deletingId = ref<number | null>(null);
|
||||||
const upgradingHostId = ref<number | null>(null);
|
|
||||||
let patchingHostRow = false;
|
let patchingHostRow = false;
|
||||||
let unsubscribeHostPatch: (() => void) | null = null;
|
let unsubscribeHostPatch: (() => void) | null = null;
|
||||||
|
|
||||||
@@ -234,17 +236,16 @@ function isVersionOutdated(h: HostSummary): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startAgentUpgrade(h: HostSummary) {
|
async function startAgentUpgrade(h: HostSummary) {
|
||||||
if (!isGitAgentUpgradeAvailable(h, data.value) || upgradingHostId.value != null) {
|
if (!isGitAgentUpgradeAvailable(h, data.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isHostRemoteActionActive(h.id)) {
|
||||||
|
showHostRemoteActionLog(h.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const kind = remoteActionKindForHost(h);
|
const kind = remoteActionKindForHost(h);
|
||||||
if (!kind) return;
|
if (!kind) return;
|
||||||
upgradingHostId.value = h.id;
|
void runHostRemoteAction(h.id, remoteActionTitleForHost(h), kind);
|
||||||
try {
|
|
||||||
await runHostRemoteAction(h.id, remoteActionTitleForHost(h), kind);
|
|
||||||
} finally {
|
|
||||||
upgradingHostId.value = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openHost(id: number) {
|
function openHost(id: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user