mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
fix: finish 2.9.0 plugin moves and get every check green
This commit is contained in:
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(
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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[];
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user