fix: finish 2.9.0 plugin moves and get every check green

This commit is contained in:
LukeGus committed 2026-09-24 23:35:18 -05:00
1 parent 8c1e02af89
commit 6489b62ece
335 files changed
+29878 -17749

No files matched your search

@@ -130,9 +130,8 @@ export class ProxmoxPollingManager<
return intervalMs + jitter;
}
// Host Metrics' own retention setting is not on the core settings
// allowlist, so this plugin uses a fixed default rather than reaching for
// it. See FINISH-LIST.md.
// Host Metrics' retention setting belongs to that plugin, so node history
// keeps a fixed week.
private readonly retentionDays = 7;
private async pollHostStats(
+14 -37
View File
@@ -1,4 +1,3 @@
import { getErrorMessage } from "../../../../src/backend/utils/error-message.js";
import express, { type Router } from "express";
import type { Client as SSHClient } from "ssh2";
import { execElevated } from "@termix/plugin-sdk/host-commands";
@@ -16,6 +15,11 @@ import {
} from "./proxmox-jump-hosts.js";
import { isSafeNodeName } from "./proxmox-shared.js";
/** A thrown value's message, or the fallback when it is not an Error. */
function getErrorMessage(error: unknown, fallback = "Unknown error"): string {
return error instanceof Error ? error.message : fallback;
}
const router = express.Router();
const runningSyncs = new Set<string>();
@@ -630,9 +634,6 @@ async function syncProxmoxHost(
}
Object.assign(update, {
enableTerminal: connectionType !== "rdp",
enableFileManager: connectionType !== "rdp",
enableTunnel: connectionType !== "rdp",
enableDocker: guest.enableDocker,
enableSsh: connectionType === "ssh",
enableRdp: connectionType === "rdp",
@@ -657,8 +658,6 @@ async function syncProxmoxHost(
vncPassword: null,
telnetUser: null,
telnetPassword: null,
defaultPath: "/",
tunnelConnections: "[]",
jumpHosts: serializeProxmoxJumpHosts(discovery.jumpHosts),
quickActions: null,
statsConfig: null,
@@ -672,11 +671,6 @@ async function syncProxmoxHost(
socks5Password: null,
socks5ProxyChain: null,
portKnockSequence: null,
showTerminalInSidebar: 0,
showFileManagerInSidebar: 0,
showTunnelInSidebar: 0,
showDockerInSidebar: 0,
showServerStatsInSidebar: 0,
} as unknown as PluginHostCreateInput);
result.created++;
}
@@ -793,34 +787,17 @@ router.post("/sync", async (req, res) => {
async function runDueProxmoxAutoSyncs(): Promise<void> {
const ctx = pluginCtx();
try {
const {
createCurrentPluginSettingsRepository,
createCurrentHostRepository,
} =
await import("../../../../src/backend/database/repositories/factory.js");
const enabledRows = await createCurrentPluginSettingsRepository().listByKey(
"proxmox",
"host",
"enableProxmox",
);
const hostRepository = createCurrentHostRepository();
const enabled = await ctx.settings.listHostValues<boolean>("enableProxmox");
const now = Date.now();
for (const row of enabledRows) {
if (row.value !== "true") continue;
const hostId = Number(row.scopeId);
if (!Number.isInteger(hostId)) continue;
for (const { hostId, userId, value } of enabled) {
if (value !== true) continue;
const host = await hostRepository.findById(hostId);
if (!host) continue;
const configRow = await createCurrentPluginSettingsRepository().get(
"proxmox",
"host",
String(hostId),
"proxmoxConfig",
);
const configRaw = configRow?.value ? JSON.parse(configRow.value) : {};
const configRaw =
(await ctx.settings.getHost<Record<string, unknown>>(
hostId,
"proxmoxConfig",
)) ?? {};
const config = parseProxmoxConfig(configRaw);
if (!config.autoSyncEnabled) continue;
@@ -832,7 +809,7 @@ async function runDueProxmoxAutoSyncs(): Promise<void> {
if (lastSyncAt && now - lastSyncAt < intervalMs) continue;
ctx
.asUser(host.userId, () => syncProxmoxHost(host.userId, hostId))
.asUser(userId, () => syncProxmoxHost(userId, hostId))
.catch((error) => {
ctx.log.error(
`Scheduled Proxmox sync failed for host ${hostId}`,
@@ -4,19 +4,21 @@ import { Server } from "lucide-react";
import { Input } from "@/components/input";
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
import { getCredentials } from "@/main-axios";
import type { HostEditorForm } from "@/sidebar/HostEditorData";
import { Select2 } from "@/components/select2";
import type { ProxmoxHostConfig } from "./types";
type SetHostField = <K extends keyof HostEditorForm>(
key: K,
value: HostEditorForm[K],
) => void;
interface HostProxmoxForm {
enableProxmox: boolean;
proxmoxConfig: ProxmoxHostConfig | null;
}
type SetHostField = (key: string, value: unknown) => void;
export function HostProxmoxTab({
form,
setField,
}: {
form: HostEditorForm;
form: HostProxmoxForm;
setField: SetHostField;
}) {
const { t } = useTranslation();
@@ -158,9 +158,6 @@ export function ProxmoxDiscoverDialog({
// imported guests are reachable the same way; user can override.
jumpHosts: discoveredJumpHosts ?? undefined,
...importAuth,
enableTerminal: g.connectionType !== "rdp",
enableFileManager: g.connectionType !== "rdp",
enableTunnel: g.connectionType !== "rdp",
enableSsh: g.connectionType !== "rdp",
enableRdp: g.connectionType === "rdp",
enableDocker: g.enableDocker,
@@ -172,16 +169,21 @@ export function ProxmoxDiscoverDialog({
g.type === "lxc" ? `ct-${g.vmid}` : `vm-${g.vmid}`,
...(g.enableDocker ? ["docker"] : []),
],
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,
// This plugin's host settings travel with the import like an export's.
pluginSettings: {
proxmox: {
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,
},
},
},
},
}));
+39 -5
View File
@@ -25,19 +25,53 @@ interface DiscoverRequest {
type Opener = (request: DiscoverRequest) => void;
function ProxmoxHostSection({ form, setField }: HostEditorSectionProps) {
// The Proxmox fields are this plugin's host settings, saved with the host
// through form.pluginSettings.proxmox.
function ProxmoxHostSection({ form, updateForm }: HostEditorSectionProps) {
const values = (form.pluginSettings?.proxmox ?? {}) as Record<
string,
unknown
>;
const setField = (key: string, value: unknown) =>
updateForm((current) => {
const all = (current.pluginSettings ?? {}) as Record<
string,
Record<string, unknown>
>;
return {
...current,
pluginSettings: {
...all,
proxmox: { ...(all.proxmox ?? {}), [key]: value },
},
};
});
const sectionForm = {
enableProxmox: values.enableProxmox === true,
proxmoxConfig: values.proxmoxConfig ?? null,
enableProxmoxStats: values.enableProxmoxStats === true,
proxmoxStatsConfig: values.proxmoxStatsConfig ?? {
pollInterval: 60,
nodeName: null,
},
};
return (
<>
<HostProxmoxTab
form={form}
form={sectionForm as Parameters<typeof HostProxmoxTab>[0]["form"]}
setField={setField as Parameters<typeof HostProxmoxTab>[0]["setField"]}
/>
<HostProxmoxStatsTab
form={form}
setField={setField as Parameters<typeof HostProxmoxTab>[0]["setField"]}
form={sectionForm as Parameters<typeof HostProxmoxStatsTab>[0]["form"]}
setField={
setField as Parameters<typeof HostProxmoxStatsTab>[0]["setField"]
}
/>
{/* Other plugins add Proxmox-related settings here. */}
<ComponentSlot slotId="proxmox.hostEditor" props={{ form, setField }} />
<ComponentSlot
slotId="proxmox.hostEditor"
props={{ form: sectionForm, setField }}
/>
</>
);
}
@@ -3,7 +3,7 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Box, Search, Server as ServerIcon } from "lucide-react";
import { Input } from "@termix/plugin-sdk/ui";
import type { ProxmoxGuestSummary } from "../types";
import { cn } from "./utils";
import { cn } from "@termix/plugin-sdk/ui";
type StatusFilter = "all" | "running" | "stopped";
type TypeFilter = "all" | "qemu" | "lxc";
@@ -1,7 +1,7 @@
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Cpu, MemoryStick, HardDrive, Clock, Server } from "lucide-react";
import type { ProxmoxNodeStats } from "../types";
import { cn } from "./utils";
import { cn } from "@termix/plugin-sdk/ui";
function Tile({
icon,
@@ -20,7 +20,7 @@ import { NodeNetworkCard } from "./cards/NodeNetworkCard";
import { StoragePoolsCard } from "./cards/StoragePoolsCard";
import { ClusterHealthCard } from "./cards/ClusterHealthCard";
import { useConnectionRetry } from "@termix/plugin-sdk/frontend";
import { runAdaptivePolling } from "./adaptive-polling";
import { runAdaptivePolling } from "@termix/plugin-sdk/ui";
const HISTORY_LEN = 30;
@@ -1,132 +0,0 @@
export interface AdaptivePollingPolicy {
minIntervalMs: number;
maxIntervalMs: number;
stablePollsPerStep?: number;
jitterRatio?: number;
/** Prevent expensive requests from consuming more than this share of time. */
maxRequestDutyCycle?: number;
}
export interface AdaptivePollingState {
stablePolls: number;
consecutiveFailures: number;
lastPollDurationMs?: number;
}
export type AdaptivePollResult = boolean | void;
export function computeAdaptivePollDelay(
policy: AdaptivePollingPolicy,
state: AdaptivePollingState,
random = Math.random,
): number {
const min = Math.max(250, policy.minIntervalMs);
const max = Math.max(min, policy.maxIntervalMs);
const stableStep = Math.floor(
state.stablePolls / Math.max(1, policy.stablePollsPerStep ?? 3),
);
const exponent = state.consecutiveFailures || stableStep;
let base = Math.min(max, min * 2 ** Math.min(exponent, 10));
const dutyCycle = policy.maxRequestDutyCycle;
if (dutyCycle && dutyCycle > 0 && dutyCycle < 1) {
const duration = Math.max(0, state.lastPollDurationMs ?? 0);
base = Math.max(base, duration * ((1 - dutyCycle) / dutyCycle));
}
base = Math.min(max, base);
const jitterRatio = Math.max(0, Math.min(0.5, policy.jitterRatio ?? 0.1));
const jitter = base * jitterRatio * (random() * 2 - 1);
return Math.max(min, Math.min(max, Math.round(base + jitter)));
}
// Copy of src/ui/lib/adaptive-polling.ts. Duplicated here rather than exposed
// through the SDK because only this plugin's Proxmox stats tab uses it
// today; promote to @termix/plugin-sdk/frontend once a second plugin needs it.
export function runAdaptivePolling(
poll: () => AdaptivePollResult | Promise<AdaptivePollResult>,
policy: AdaptivePollingPolicy,
options: {
enabled?: () => boolean;
visible?: () => boolean;
runImmediately?: boolean;
random?: () => number;
intervalMultiplier?: () => number;
onError?: (error: unknown) => void;
} = {},
): () => void {
const enabled = options.enabled ?? (() => true);
const visible =
options.visible ??
(() =>
typeof document === "undefined" || document.visibilityState !== "hidden");
const state: AdaptivePollingState = {
stablePolls: 0,
consecutiveFailures: 0,
};
let timer: ReturnType<typeof setTimeout> | null = null;
let stopped = false;
let inFlight = false;
const clearTimer = () => {
if (!timer) return;
clearTimeout(timer);
timer = null;
};
const schedule = () => {
clearTimer();
if (stopped || !enabled() || !visible()) return;
timer = setTimeout(
() => void tick(),
Math.min(
policy.maxIntervalMs,
Math.round(
computeAdaptivePollDelay(policy, state, options.random) *
Math.max(1, options.intervalMultiplier?.() ?? 1),
),
),
);
};
const tick = async () => {
if (stopped || inFlight || !enabled() || !visible()) return;
inFlight = true;
const startedAt = Date.now();
try {
const changed = await poll();
state.consecutiveFailures = 0;
state.stablePolls = changed === false ? state.stablePolls + 1 : 0;
} catch (error) {
state.stablePolls = 0;
state.consecutiveFailures += 1;
options.onError?.(error);
} finally {
state.lastPollDurationMs = Date.now() - startedAt;
inFlight = false;
schedule();
}
};
const onVisibility = () => {
if (!visible()) {
clearTimer();
return;
}
state.stablePolls = 0;
state.consecutiveFailures = 0;
void tick();
};
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", onVisibility);
}
if (options.runImmediately ?? true) void tick();
else schedule();
return () => {
stopped = true;
clearTimer();
if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibility);
}
};
}
@@ -2,7 +2,7 @@ import { Network } from "lucide-react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { MetricCard } from "@termix/plugin-sdk/ui";
import type { ProxmoxStatsSnapshot } from "../../types";
import { cn } from "../utils";
import { cn } from "@termix/plugin-sdk/ui";
export function ClusterHealthCard({
snapshot,
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+32
View File
@@ -101,3 +101,35 @@ export interface ProxmoxStatsConfig {
pollInterval?: number;
enabledCards?: string[];
}
/** The proxmoxConfig host setting. */
export interface ProxmoxHostConfig {
source?: {
source: "proxmox";
sourceHostId: number;
node: string;
vmid: number;
type: "qemu" | "lxc";
lastSeenAt?: string;
lastStatus?: string;
missingSince?: string | null;
};
defaultCredentialId: number | null;
defaultAuthType?: string;
windowsPatterns: string;
dockerPatterns: 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[];
};
}