Add Proxmox guest auto sync (#1053)

This commit is contained in:
ZacharyZcR
2026-07-16 12:10:12 +08:00
committed by GitHub
parent b2c5b9cde3
commit ed6cda4ea4
12 changed files with 1006 additions and 345 deletions
@@ -276,6 +276,9 @@ export function registerHostBulkRoutes(
dockerPatterns: existing.dockerPatterns ?? "docker", dockerPatterns: existing.dockerPatterns ?? "docker",
preferredPrefixes: preferredPrefixes:
existing.preferredPrefixes ?? "10., 192.168.", existing.preferredPrefixes ?? "10., 192.168.",
autoSyncEnabled: existing.autoSyncEnabled ?? false,
syncIntervalMinutes: existing.syncIntervalMinutes ?? 15,
markMissingGuests: existing.markMissingGuests ?? true,
}; };
await db await db
.update(hosts) .update(hosts)
File diff suppressed because it is too large Load Diff
+14
View File
@@ -70,9 +70,23 @@ export type GuacamoleAuthType = "password" | "credential";
export interface ProxmoxConfig { export interface ProxmoxConfig {
defaultCredentialId: number | null; defaultCredentialId: number | null;
defaultAuthType?: string;
windowsPatterns: string; windowsPatterns: string;
dockerPatterns: string; dockerPatterns: string;
preferredPrefixes: string; preferredPrefixes: string;
autoSyncEnabled?: boolean;
syncIntervalMinutes?: number;
markMissingGuests?: boolean;
lastSyncAt?: string;
lastSyncStatus?: "success" | "error";
lastSyncError?: string | null;
lastSyncResult?: {
created: number;
updated: number;
markedMissing: number;
skipped: number;
errors: string[];
};
} }
export interface HostFeatureFlags { export interface HostFeatureFlags {
+8
View File
@@ -14,3 +14,11 @@ export interface ProxmoxDiscoverResult {
credentialId: number | null; credentialId: number | null;
defaultCredentialId: number | null; defaultCredentialId: number | null;
} }
export interface ProxmoxSyncResult {
created: number;
updated: number;
markedMissing: number;
skipped: number;
errors: string[];
}
+13
View File
@@ -116,6 +116,19 @@ export type Host = {
windowsPatterns: string; windowsPatterns: string;
dockerPatterns: string; dockerPatterns: string;
preferredPrefixes: string; preferredPrefixes: string;
autoSyncEnabled?: boolean;
syncIntervalMinutes?: number;
markMissingGuests?: boolean;
lastSyncAt?: string;
lastSyncStatus?: "success" | "error";
lastSyncError?: string | null;
lastSyncResult?: {
created: number;
updated: number;
markedMissing: number;
skipped: number;
errors: string[];
};
} | null; } | null;
statsConfig?: { statsConfig?: {
+16 -1
View File
@@ -7,7 +7,7 @@ import {
} from "@/main-axios"; } from "@/main-axios";
import type { SSHHost, SSHHostData, ProxyNode } from "@/types/index"; import type { SSHHost, SSHHostData, ProxyNode } from "@/types/index";
import type { ServerStatus, SSHHostWithStatus } from "@/main-axios"; import type { ServerStatus, SSHHostWithStatus } from "@/main-axios";
import type { ProxmoxDiscoverResult } from "@/types/proxmox"; import type { ProxmoxDiscoverResult, ProxmoxSyncResult } from "@/types/proxmox";
import { import {
getCachedSSHHosts, getCachedSSHHosts,
invalidateHostsAndStatusCaches, invalidateHostsAndStatusCaches,
@@ -174,6 +174,21 @@ export async function discoverProxmoxGuests(
} }
} }
export async function syncProxmoxGuests(
hostId: number,
): Promise<ProxmoxSyncResult> {
try {
const response = await authApi.post(
"/proxmox/sync",
{ hostId },
{ timeout: 120000 },
);
return response.data;
} catch (error) {
handleApiError(error, "sync Proxmox guests");
}
}
export async function bulkUpdateSSHHosts( export async function bulkUpdateSSHHosts(
hostIds: number[], hostIds: number[],
updates: Record<string, unknown>, updates: Record<string, unknown>,
@@ -138,6 +138,18 @@ export function ProxmoxDiscoverDialog({
enableDocker: g.enableDocker, enableDocker: g.enableDocker,
connectionType: g.connectionType, connectionType: g.connectionType,
tags: ["proxmox", g.type, g.node], tags: ["proxmox", g.type, g.node],
proxmoxConfig: {
source: {
source: "proxmox",
sourceHostId: Number(effectiveHostId),
node: g.node,
vmid: g.vmid,
type: g.type,
lastSeenAt: new Date().toISOString(),
lastStatus: g.status,
missingSince: null,
},
},
})); }));
const result = await bulkImportSSHHosts(toImport, false); const result = await bulkImportSSHHosts(toImport, false);
+15
View File
@@ -607,6 +607,21 @@
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests", "proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
"proxmoxPreferredRanges": "Preferred IP ranges", "proxmoxPreferredRanges": "Preferred IP ranges",
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces", "proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
"proxmoxAutoSync": "Auto sync guests",
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
"proxmoxSyncInterval": "Sync interval (minutes)",
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
"proxmoxMarkMissing": "Mark missing guests",
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
"proxmoxLastSync": "Last sync",
"proxmoxLastSyncNever": "Not synced yet",
"proxmoxLastSyncNoResult": "No result details",
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
"proxmoxLastSyncStatus": {
"success": "Success",
"error": "Failed",
"pending": "Pending"
},
"proxmoxDiscoverAction": "Discover & import Proxmox guests", "proxmoxDiscoverAction": "Discover & import Proxmox guests",
"proxmoxImportTitle": "Import from Proxmox", "proxmoxImportTitle": "Import from Proxmox",
"proxmoxSelectHost": "Select a Proxmox host…", "proxmoxSelectHost": "Select a Proxmox host…",
+15
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "逗号分隔的名称模式使 Docker 能够匹配虚拟机", "proxmoxDockerDetectionDesc": "逗号分隔的名称模式使 Docker 能够匹配虚拟机",
"proxmoxPreferredRanges": "首选 IP 地址范围", "proxmoxPreferredRanges": "首选 IP 地址范围",
"proxmoxPreferredRangesDesc": "当访客拥有多个接口时,用于选择 IP 地址的优先级前缀以逗号分隔", "proxmoxPreferredRangesDesc": "当访客拥有多个接口时,用于选择 IP 地址的优先级前缀以逗号分隔",
"proxmoxAutoSync": "自动同步访客",
"proxmoxAutoSyncDesc": "在会话解锁时定期发现此 Proxmox 节点,并创建或更新导入的访客主机。",
"proxmoxSyncInterval": "同步间隔(分钟)",
"proxmoxSyncIntervalDesc": "最小 5 分钟。用户会话锁定时调度器会跳过同步。",
"proxmoxMarkMissing": "标记消失的访客",
"proxmoxMarkMissingDesc": "以前导入的访客消失时添加 proxmox-missing 标签,而不是删除主机。",
"proxmoxLastSync": "上次同步",
"proxmoxLastSyncNever": "尚未同步",
"proxmoxLastSyncNoResult": "暂无结果详情",
"proxmoxLastSyncSummary": "创建 {{created}},更新 {{updated}},消失 {{markedMissing}},跳过 {{skipped}}",
"proxmoxLastSyncStatus": {
"success": "成功",
"error": "失败",
"pending": "等待中"
},
"proxmoxDiscoverAction": "发现并导入 Proxmox 访客", "proxmoxDiscoverAction": "发现并导入 Proxmox 访客",
"proxmoxImportTitle": "从 Proxmox 导入", "proxmoxImportTitle": "从 Proxmox 导入",
"proxmoxSelectHost": "选择 Proxmox 主机…", "proxmoxSelectHost": "选择 Proxmox 主机…",
+1
View File
@@ -1517,6 +1517,7 @@ export {
bulkImportSSHHosts, bulkImportSSHHosts,
importSSHConfigHosts, importSSHConfigHosts,
discoverProxmoxGuests, discoverProxmoxGuests,
syncProxmoxGuests,
bulkUpdateSSHHosts, bulkUpdateSSHHosts,
deleteSSHHost, deleteSSHHost,
getSSHHostById, getSSHHostById,
+3
View File
@@ -114,6 +114,9 @@ export function createHostEditorForm(
windowsPatterns: "win, windows", windowsPatterns: "win, windows",
dockerPatterns: "docker", dockerPatterns: "docker",
preferredPrefixes: "10., 192.168.", preferredPrefixes: "10., 192.168.",
autoSyncEnabled: false,
syncIntervalMinutes: 15,
markMissingGuests: true,
}, },
enableTunnel: host?.enableTunnel ?? false, enableTunnel: host?.enableTunnel ?? false,
defaultPath: host?.defaultPath ?? "/", defaultPath: host?.defaultPath ?? "/",
+80
View File
@@ -105,7 +105,22 @@ export function HostProxmoxTab({
windowsPatterns: "win, windows", windowsPatterns: "win, windows",
dockerPatterns: "docker", dockerPatterns: "docker",
preferredPrefixes: "10., 192.168.", preferredPrefixes: "10., 192.168.",
autoSyncEnabled: false,
syncIntervalMinutes: 15,
markMissingGuests: true,
}; };
const lastSyncResult = cfg.lastSyncResult;
const lastSyncSummary = lastSyncResult
? t("hosts.proxmoxLastSyncSummary", {
created: lastSyncResult.created,
updated: lastSyncResult.updated,
markedMissing: lastSyncResult.markedMissing,
skipped: lastSyncResult.skipped,
})
: t("hosts.proxmoxLastSyncNoResult");
const lastSyncDescription = cfg.lastSyncAt
? `${new Date(cfg.lastSyncAt).toLocaleString()} · ${lastSyncSummary}`
: t("hosts.proxmoxLastSyncNever");
return ( return (
<SectionCard <SectionCard
@@ -231,6 +246,71 @@ export function HostProxmoxTab({
placeholder="10., 192.168." placeholder="10., 192.168."
/> />
</SettingRow> </SettingRow>
<SettingRow
label={t("hosts.proxmoxAutoSync")}
description={t("hosts.proxmoxAutoSyncDesc")}
>
<FakeSwitch
checked={cfg.autoSyncEnabled === true}
onChange={(v) =>
setField("proxmoxConfig", {
...cfg,
autoSyncEnabled: v,
})
}
/>
</SettingRow>
<SettingRow
label={t("hosts.proxmoxSyncInterval")}
description={t("hosts.proxmoxSyncIntervalDesc")}
>
<Input
className="w-24 h-7 text-xs"
type="number"
min={5}
value={cfg.syncIntervalMinutes ?? 15}
onChange={(e) =>
setField("proxmoxConfig", {
...cfg,
syncIntervalMinutes: Math.max(
5,
Number.parseInt(e.target.value || "15", 10),
),
})
}
/>
</SettingRow>
<SettingRow
label={t("hosts.proxmoxMarkMissing")}
description={t("hosts.proxmoxMarkMissingDesc")}
>
<FakeSwitch
checked={cfg.markMissingGuests !== false}
onChange={(v) =>
setField("proxmoxConfig", {
...cfg,
markMissingGuests: v,
})
}
/>
</SettingRow>
<SettingRow
label={t("hosts.proxmoxLastSync")}
description={lastSyncDescription}
>
<span
className={`text-xs ${
cfg.lastSyncStatus === "error"
? "text-destructive"
: "text-muted-foreground"
}`}
title={cfg.lastSyncError ?? undefined}
>
{cfg.lastSyncStatus
? t(`hosts.proxmoxLastSyncStatus.${cfg.lastSyncStatus}`)
: t("hosts.proxmoxLastSyncStatus.pending")}
</span>
</SettingRow>
</> </>
)} )}
</div> </div>