feat: one-click agent upgrade from hosts list (0.3.9)

Add Omada-style git version arrow in the agent version column to trigger WinRM/SSH background updates, and soften host detail live refresh with debounced incremental event patches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 15:32:09 +10:00
parent 0601aafd2d
commit 9053ef0588
7 changed files with 352 additions and 38 deletions
+11
View File
@@ -53,3 +53,14 @@ export function isAgentVersionNewer(
if (!prev) return true;
return compareVersions(next, prev) > 0;
}
/** Хост строго младше целевой версии (например из git), без порога lag. */
export function isAgentVersionBehind(
hostVersion: string | null | undefined,
targetVersion: string | null | undefined,
): boolean {
const host = parseAgentVersion(hostVersion);
const target = parseAgentVersion(targetVersion);
if (!host || !target) return false;
return compareVersions(host, target) < 0;
}
+60
View File
@@ -0,0 +1,60 @@
import type { HostSummary } from "../api";
import type { HostRemoteActionKind } from "../composables/useHostRemoteAction";
import { isAgentVersionBehind } from "./agentVersion";
export const PRODUCT_RDP = "rdp-login-monitor";
export const PRODUCT_SSH = "ssh-monitor";
export interface AgentGitVersions {
git_latest_rdp_version?: string | null;
git_latest_ssh_version?: string | null;
}
export function isWindowsAgentHost(host: Pick<HostSummary, "os_family" | "product">): boolean {
if ((host.os_family || "").toLowerCase() === "windows") return true;
return host.product === PRODUCT_RDP;
}
export function isLinuxAgentHost(host: Pick<HostSummary, "os_family" | "product">): boolean {
if ((host.os_family || "").toLowerCase() === "linux") return true;
return host.product === PRODUCT_SSH;
}
export function gitTargetVersionForHost(
host: Pick<HostSummary, "os_family" | "product">,
git: AgentGitVersions | null | undefined,
): string | null {
if (!git) return null;
if (isWindowsAgentHost(host)) {
return git.git_latest_rdp_version?.trim() || null;
}
if (isLinuxAgentHost(host)) {
return git.git_latest_ssh_version?.trim() || null;
}
return null;
}
export function isGitAgentUpgradeAvailable(
host: Pick<HostSummary, "os_family" | "product" | "product_version">,
git: AgentGitVersions | null | undefined,
): boolean {
const target = gitTargetVersionForHost(host, git);
if (!target || !host.product_version?.trim()) return false;
return isAgentVersionBehind(host.product_version, target);
}
export function remoteActionKindForHost(
host: Pick<HostSummary, "os_family" | "product">,
): HostRemoteActionKind | null {
if (isWindowsAgentHost(host)) return "fallback";
if (isLinuxAgentHost(host)) return "ssh-update";
return null;
}
export function remoteActionTitleForHost(host: HostSummary): string {
const label = host.display_name || host.hostname;
if (isWindowsAgentHost(host)) {
return `Обновление ${label} через WinRM`;
}
return `Обновление ssh-monitor (SSH): ${label}`;
}
+37
View File
@@ -0,0 +1,37 @@
import type { EventListResponse, EventSummary, HostDetail } from "../api";
import { isAgentVersionBehind } from "./agentVersion";
import type { AgentGitVersions } from "./hostAgentUpgrade";
import { gitTargetVersionForHost } from "./hostAgentUpgrade";
/** Точечное обновление карточки хоста по событию ingest (без полного GET /hosts/{id}). */
export function patchHostDetailFromEvent(host: HostDetail, ev: EventSummary): void {
host.last_seen_at = ev.received_at || ev.occurred_at;
host.event_count = (host.event_count ?? 0) + 1;
if (ev.product_version?.trim()) {
host.product_version = ev.product_version.trim();
}
}
export function recalcVersionStatusFromGit(
host: HostDetail,
git: AgentGitVersions | null | undefined,
): void {
const target = gitTargetVersionForHost(host, git);
if (!host.product_version?.trim() || !target) {
return;
}
host.version_status = isAgentVersionBehind(host.product_version, target) ? "outdated" : "ok";
}
/** Добавляет событие в начало списка на стр. 1, если его ещё нет. */
export function prependHostEventIfMissing(
events: EventListResponse | null,
ev: EventSummary,
page: number,
): boolean {
if (!events || page !== 1) return false;
if (events.items.some((row) => row.id === ev.id)) return false;
events.items = [ev, ...events.items];
events.total = (events.total ?? 0) + 1;
return true;
}