From d440ce90736cec06ded451fcb5e1895536281ffd Mon Sep 17 00:00:00 2001 From: LukeGus Date: Mon, 21 Sep 2026 19:20:17 -0500 Subject: [PATCH] feat: move proxmox discovery/import/sync into a first-party plugin --- plugins/proxmox/backend/index.mjs | 63 +++++++++++++++++++ .../proxmox/backend}/proxmox-import-auth.ts | 2 +- .../proxmox/backend}/proxmox-jump-hosts.ts | 0 .../proxmox/backend/routes.ts | 59 +++++++++++------ .../frontend}/ProxmoxDiscoverDialog.tsx | 0 plugins/proxmox/frontend/index.mjs | 44 +++++++++++++ .../proxmox/frontend}/proxmox-import-auth.ts | 0 plugins/proxmox/manifest.json | 27 ++++++++ src/backend/database/database.ts | 4 +- .../database/routes/proxmox-dispatch.ts | 48 ++++++++++++++ src/backend/plugins/first-party.ts | 9 +++ src/backend/starter.ts | 8 ++- src/backend/tests/plugins/first-party.test.ts | 20 +++++- .../proxmox}/proxmox-import-auth.test.ts | 4 +- .../proxmox}/proxmox-jump-hosts.test.ts | 2 +- .../proxmox}/proxmox-sudo.test.ts | 3 +- src/ui/sidebar/HostEditor.tsx | 6 +- src/ui/sidebar/HostEditorFeatureTabs.tsx | 20 ++++++ src/ui/sidebar/HostsPanel.tsx | 2 +- .../proxmox/proxmox-import-auth.test.ts | 2 +- tsconfig.plugins.json | 3 +- 21 files changed, 290 insertions(+), 36 deletions(-) create mode 100644 plugins/proxmox/backend/index.mjs rename {src/backend/database/routes => plugins/proxmox/backend}/proxmox-import-auth.ts (93%) rename {src/backend/database/routes => plugins/proxmox/backend}/proxmox-jump-hosts.ts (100%) rename src/backend/database/routes/proxmox.ts => plugins/proxmox/backend/routes.ts (93%) rename {src/ui/components/proxmox => plugins/proxmox/frontend}/ProxmoxDiscoverDialog.tsx (100%) create mode 100644 plugins/proxmox/frontend/index.mjs rename {src/ui/components/proxmox => plugins/proxmox/frontend}/proxmox-import-auth.ts (100%) create mode 100644 plugins/proxmox/manifest.json create mode 100644 src/backend/database/routes/proxmox-dispatch.ts rename src/backend/tests/{database/routes => plugins/proxmox}/proxmox-import-auth.test.ts (90%) rename src/backend/tests/{database/routes => plugins/proxmox}/proxmox-jump-hosts.test.ts (91%) rename src/backend/tests/{database/routes => plugins/proxmox}/proxmox-sudo.test.ts (92%) rename src/ui/tests/{components => plugins}/proxmox/proxmox-import-auth.test.ts (94%) diff --git a/plugins/proxmox/backend/index.mjs b/plugins/proxmox/backend/index.mjs new file mode 100644 index 000000000..2a6f5eab1 --- /dev/null +++ b/plugins/proxmox/backend/index.mjs @@ -0,0 +1,63 @@ +/** + * Proxmox discovery/import/sync - first-party, in-process plugin. + * + * This plugin runs on the main thread rather than in a worker for the same + * reason ai does (see src/backend/plugins/first-party.ts): it writes + * directly across hosts through the host repository, resolves and connects + * to hosts over the shared SSH pool via resolveHostById and + * createJumpHostChain, verifies host keys via SSHHostKeyVerifier, and + * streams its discovery progress as server-sent events. None of that fits + * the worker ctx -- ctx.hosts is a 13-field read-only view with no write + * methods, and ctx.http.route replies with a single postMessage value + * rather than a stream. + * + * Like ai, this plugin does not own a live transport (no WebSocket server, + * no dedicated port), so it registers its Express router with the + * always-mounted /proxmox dispatcher in + * src/backend/database/routes/proxmox-dispatch.ts instead of getting its + * own port. + * + * routes.ts is what used to be src/backend/database/routes/proxmox.ts, + * moved here with its sibling modules (proxmox-import-auth.ts, + * proxmox-jump-hosts.ts) and rewritten from `export default router` into + * startProxmoxService()/stopProxmoxService(), called from + * activate()/deactivate() below. Those two functions also start and stop + * the background auto-sync interval that used to run at module scope in + * proxmox.ts -- keeping it there would mean it fires even when this plugin + * is disabled and can never be torn down. + */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Loads a module by its path relative to this file's own directory. + * + * The plugin ships beside dist/backend, so in a built server the compiled + * .js for the moved Proxmox files (routes.js, ...) sits right next to this + * .mjs, and core modules resolve via ../../../backend/backend/... from + * there (dist/plugins/proxmox/backend -> dist/backend/backend). Under + * vitest/dev nothing imports this plugin directly (the loader loads + * in-process plugins with a plain dynamic import, no tsx loader involved), + * so the only layout that needs to resolve here is the built one. + */ +async function loadRelative(relativePath) { + const here = path.dirname(fileURLToPath(import.meta.url)); + const target = path.join(here, `${relativePath}.js`); + return import(new URL(`file://${target.replace(/\\/g, "/")}`).href); +} + +let routesModule = null; + +export async function activate(ctx) { + routesModule = await loadRelative("routes"); + routesModule.startProxmoxService(); + ctx.log.info("Proxmox router registered at /proxmox"); +} + +export async function deactivate() { + if (routesModule) { + routesModule.stopProxmoxService(); + routesModule = null; + } +} diff --git a/src/backend/database/routes/proxmox-import-auth.ts b/plugins/proxmox/backend/proxmox-import-auth.ts similarity index 93% rename from src/backend/database/routes/proxmox-import-auth.ts rename to plugins/proxmox/backend/proxmox-import-auth.ts index 5a1adeca4..65194c0dd 100644 --- a/src/backend/database/routes/proxmox-import-auth.ts +++ b/plugins/proxmox/backend/proxmox-import-auth.ts @@ -1,7 +1,7 @@ // Pure decision: which auth settings an imported Proxmox guest inherits. // // The frontend carries a parallel copy in -// src/ui/components/proxmox/proxmox-import-auth.ts. The two drifting apart is +// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is // what produced the reported import bug, so both are kept behaviourally // identical and each is unit-tested against the same matrix. export function resolveProxmoxImportAuth( diff --git a/src/backend/database/routes/proxmox-jump-hosts.ts b/plugins/proxmox/backend/proxmox-jump-hosts.ts similarity index 100% rename from src/backend/database/routes/proxmox-jump-hosts.ts rename to plugins/proxmox/backend/proxmox-jump-hosts.ts diff --git a/src/backend/database/routes/proxmox.ts b/plugins/proxmox/backend/routes.ts similarity index 93% rename from src/backend/database/routes/proxmox.ts rename to plugins/proxmox/backend/routes.ts index 02f8ce3cb..0eaca6cdb 100644 --- a/src/backend/database/routes/proxmox.ts +++ b/plugins/proxmox/backend/routes.ts @@ -1,24 +1,28 @@ -import { getErrorMessage } from "../../utils/error-message.js"; +import { getErrorMessage } from "../../../src/backend/utils/error-message.js"; import express from "express"; import { Client as SSHClient } from "ssh2"; -import { logger } from "../../utils/logger.js"; -import { DataCrypto } from "../../utils/data-crypto.js"; -import { createCurrentHostRepository } from "../repositories/factory.js"; -import { AuthManager } from "../../utils/auth-manager.js"; +import { logger } from "../../../src/backend/utils/logger.js"; +import { DataCrypto } from "../../../src/backend/utils/data-crypto.js"; +import { createCurrentHostRepository } from "../../../src/backend/database/repositories/factory.js"; +import { AuthManager } from "../../../src/backend/utils/auth-manager.js"; import { type AuthenticatedRequest, type SSHHost, -} from "../../../types/index.js"; -import { SSHHostKeyVerifier } from "../../hosts/host-key-verifier.js"; -import { resolveHostById } from "../../hosts/host-resolver.js"; -import { createJumpHostChain } from "../../hosts/jump-host-chain.js"; +} from "../../../src/types/index.js"; +import { SSHHostKeyVerifier } from "../../../src/backend/hosts/host-key-verifier.js"; +import { resolveHostById } from "../../../src/backend/hosts/host-resolver.js"; +import { createJumpHostChain } from "../../../src/backend/hosts/jump-host-chain.js"; import { resolveProxmoxImportAuth } from "./proxmox-import-auth.js"; import { parseProxmoxJumpHosts, serializeProxmoxJumpHosts, } from "./proxmox-jump-hosts.js"; -import { isSafeNodeName } from "../../hosts/proxmox-shared.js"; -import { execElevated } from "../../hosts/metrics-shared/exec-elevated.js"; +import { isSafeNodeName } from "../../../src/backend/hosts/proxmox-shared.js"; +import { execElevated } from "../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +import { + registerProxmoxRouter, + unregisterProxmoxRouter, +} from "../../../src/backend/database/routes/proxmox-dispatch.js"; const router = express.Router(); const proxmoxLogger = logger; @@ -346,7 +350,7 @@ async function discoverProxmoxGuestsForHost( sshConfig.passphrase = resolvedCredentials.keyPassword; } else if (authType === "agent") { const { applyAgentAuth } = - await import("../../hosts/terminal-auth-helpers.js"); + await import("../../../src/backend/hosts/terminal-auth-helpers.js"); const result = await applyAgentAuth( sshConfig, host.terminalConfig as unknown as Record | undefined, @@ -931,13 +935,8 @@ async function runDueProxmoxAutoSyncs(): Promise { } } -const proxmoxAutoSyncTimer = setInterval(runDueProxmoxAutoSyncs, 60 * 1000); -proxmoxAutoSyncTimer.unref?.(); -const proxmoxAutoSyncStartupTimer = setTimeout( - runDueProxmoxAutoSyncs, - 30 * 1000, -); -proxmoxAutoSyncStartupTimer.unref?.(); +let proxmoxAutoSyncTimer: NodeJS.Timeout | undefined; +let proxmoxAutoSyncStartupTimer: NodeJS.Timeout | undefined; /** * @openapi @@ -1136,4 +1135,24 @@ router.post( }, ); -export default router; +/** Called from activate(). Mounts this router at /proxmox via the shared + * dispatcher, and starts the background auto-sync scan (a 60s interval plus + * a one-off 30s-delayed startup run, both unref'd so they never keep the + * process alive on their own). */ +export function startProxmoxService(): void { + registerProxmoxRouter(router); + proxmoxAutoSyncTimer = setInterval(runDueProxmoxAutoSyncs, 60 * 1000); + proxmoxAutoSyncTimer.unref?.(); + proxmoxAutoSyncStartupTimer = setTimeout(runDueProxmoxAutoSyncs, 30 * 1000); + proxmoxAutoSyncStartupTimer.unref?.(); +} + +/** Called from deactivate(). /proxmox/* falls back to 404 until reactivated, + * and the auto-sync timers are cleared so a disabled plugin stops syncing. */ +export function stopProxmoxService(): void { + unregisterProxmoxRouter(); + if (proxmoxAutoSyncTimer) clearInterval(proxmoxAutoSyncTimer); + if (proxmoxAutoSyncStartupTimer) clearTimeout(proxmoxAutoSyncStartupTimer); + proxmoxAutoSyncTimer = undefined; + proxmoxAutoSyncStartupTimer = undefined; +} diff --git a/src/ui/components/proxmox/ProxmoxDiscoverDialog.tsx b/plugins/proxmox/frontend/ProxmoxDiscoverDialog.tsx similarity index 100% rename from src/ui/components/proxmox/ProxmoxDiscoverDialog.tsx rename to plugins/proxmox/frontend/ProxmoxDiscoverDialog.tsx diff --git a/plugins/proxmox/frontend/index.mjs b/plugins/proxmox/frontend/index.mjs new file mode 100644 index 000000000..d04b5a248 --- /dev/null +++ b/plugins/proxmox/frontend/index.mjs @@ -0,0 +1,44 @@ +/** + * Frontend half of the proxmox plugin. + * + * Unlike ssh-terminal/docker, proxmox has no rail tab or standalone view — + * only a discovery dialog (opened imperatively from HostsPanel.tsx, not + * through a tab seam) and a host-editor settings tab. Registers the + * host-editor tab through the same registerHostEditorTab seam docker uses. + * + * HostProxmoxTab itself stays in src/ui/sidebar/HostEditorFeatureTabs.tsx + * rather than moving into this plugin: it takes the same {form, setField} + * pair every other host editor tab does, and those types (HostEditorForm, + * the setField signature) are core types HostEditor.tsx owns, the same + * reason ssh-terminal leaves session-manager.ts in core rather than copying + * it here. + * + * register() is called when the plugin is enabled, unregister() when it is + * disabled. As of this change neither is actually invoked anywhere yet -- + * see plugins/ssh-terminal/frontend/index.mjs and + * src/ui/tests/sidebar/plugin-extension-seam.test.tsx, which says outright + * "there is no plugin loader yet" for the frontend half. This file follows + * the same shape so it is ready the moment that loader exists. The dialog + * is reached today through a direct import in HostsPanel.tsx, not through + * this seam. + */ + +export const id = "proxmox"; +export const hostEditorTabId = "proxmox"; + +export async function register({ registerHostEditorTab, icons }) { + if (registerHostEditorTab) { + const { HostProxmoxTab } = + await import("../../../src/ui/sidebar/HostEditorFeatureTabs.tsx"); + registerHostEditorTab({ + id: hostEditorTabId, + labelKey: "hosts.tabProxmox", + icon: icons.Server, + component: HostProxmoxTab, + }); + } +} + +export async function unregister({ unregisterHostEditorTab }) { + unregisterHostEditorTab?.(hostEditorTabId); +} diff --git a/src/ui/components/proxmox/proxmox-import-auth.ts b/plugins/proxmox/frontend/proxmox-import-auth.ts similarity index 100% rename from src/ui/components/proxmox/proxmox-import-auth.ts rename to plugins/proxmox/frontend/proxmox-import-auth.ts diff --git a/plugins/proxmox/manifest.json b/plugins/proxmox/manifest.json new file mode 100644 index 000000000..43695dc59 --- /dev/null +++ b/plugins/proxmox/manifest.json @@ -0,0 +1,27 @@ +{ + "id": "proxmox", + "name": "Proxmox", + "version": "1.0.0", + "description": "Discover Proxmox VE guests over an existing SSH host, import them as Termix hosts, and keep them in sync.", + "author": { "name": "Termix" }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Infrastructure", + "icon": "Server", + "engine": { "termix": ">=2.9.0", "api": "1" }, + "capabilities": { + "backend": true, + "frontend": true, + "electron": true, + "platforms": ["linux", "win32", "darwin"] + }, + "permissions": ["process:transport-owner"], + "contributes": { + "hostCapability": { + "key": "enableProxmox", + "labelKey": "hosts.proxmoxIntegration", + "editorTab": "proxmox" + } + }, + "sidecars": [] +} diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index dc5b36cb1..d95d2d4ea 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -25,7 +25,7 @@ import userPreferencesRoutes from "./routes/user-preferences.js"; import hostSidebarPreferencesRoutes from "./routes/host-sidebar-preferences.js"; import credentialSidebarPreferencesRoutes from "./routes/credential-sidebar-preferences.js"; import uiPreferencesRoutes from "./routes/ui-preferences.js"; -import proxmoxRoutes from "./routes/proxmox.js"; +import proxmoxDispatch from "./routes/proxmox-dispatch.js"; import termixIdRoutes from "./routes/termix-id.js"; import { registerAuditLogRoutes } from "./routes/audit-log-routes.js"; import { registerTailscaleRoutes } from "./routes/tailscale-routes.js"; @@ -1768,7 +1768,7 @@ app.use("/user-preferences", userPreferencesRoutes); app.use("/host-sidebar/preferences", hostSidebarPreferencesRoutes); app.use("/credential-sidebar/preferences", credentialSidebarPreferencesRoutes); app.use("/ui-preferences", uiPreferencesRoutes); -app.use("/proxmox", proxmoxRoutes); +app.use("/proxmox", proxmoxDispatch); app.use("/termix-id", termixIdRoutes); registerAuditLogRoutes(app, authenticateJWT); registerTailscaleRoutes(app, authenticateJWT); diff --git a/src/backend/database/routes/proxmox-dispatch.ts b/src/backend/database/routes/proxmox-dispatch.ts new file mode 100644 index 000000000..173dd8a06 --- /dev/null +++ b/src/backend/database/routes/proxmox-dispatch.ts @@ -0,0 +1,48 @@ +// Mounted at /proxmox. The Proxmox discovery/import/sync feature ships as a +// first-party in-process plugin (plugins/proxmox) rather than a route file +// imported directly here, so this module exists to keep /proxmox/* working +// at its original path without handing the plugin a reference to the shared +// app instance. The plugin runtime registers its router via +// registerProxmoxRouter when it activates, and clears it on deactivate, so a +// disabled plugin falls back to the 404 below. +// +// Unlike /plugin-api, this dispatcher applies no auth of its own: it forwards +// to exactly one first-party router (the Proxmox plugin's own), and every +// route in that router already applies authenticateJWT/requireDataAccess +// itself, the same as every other router mounted alongside it here (/users, +// /host, /snippets, ...). /plugin-api gates at the mount point because it +// forwards to arbitrary third-party plugin code that cannot be assumed to +// gate itself; that reasoning does not apply to a single first-party router +// that already does. + +import type { Request, Response, NextFunction, Router } from "express"; +import { databaseLogger } from "../../utils/logger.js"; + +let activeRouter: Router | null = null; + +export function registerProxmoxRouter(router: Router): void { + activeRouter = router; + databaseLogger.info("Registered Proxmox plugin router", { + operation: "proxmox_dispatch_register", + }); +} + +export function unregisterProxmoxRouter(): void { + if (!activeRouter) return; + activeRouter = null; + databaseLogger.info("Unregistered Proxmox plugin router", { + operation: "proxmox_dispatch_register", + }); +} + +export default function proxmoxDispatch( + req: Request, + res: Response, + next: NextFunction, +): void { + if (!activeRouter) { + res.status(404).json({ error: "Proxmox discovery is not available" }); + return; + } + activeRouter(req, res, next); +} diff --git a/src/backend/plugins/first-party.ts b/src/backend/plugins/first-party.ts index 8f81bb939..4c9685ba1 100644 --- a/src/backend/plugins/first-party.ts +++ b/src/backend/plugins/first-party.ts @@ -54,12 +54,21 @@ * with no write methods and no snippets/fleets/alerts/automations surface at * all, and ctx.http.route replies with one postMessage value rather than a * stream, so none of that fits the worker ctx either. + * + * proxmox is here for the same reason as ai: it does not own a transport, + * but it writes directly across hosts through the host repository during + * discovery and sync, resolves and connects to hosts via resolveHostById and + * createJumpHostChain the same way the terminal and ai's command executor + * do, verifies host keys via SSHHostKeyVerifier, and streams its discovery + * progress as server-sent events. Neither the worker's 13-field read-only + * ctx.hosts nor its single-postMessage-reply ctx.http.route can support that. */ export const FIRST_PARTY_PLUGIN_IDS: ReadonlySet = new Set([ "ssh-terminal", "docker", "host-metrics", "ai", + "proxmox", ]); export const TRANSPORT_OWNER_CAPABILITY = "process:transport-owner"; diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 8ae3c1131..956935259 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -299,9 +299,11 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { // ssh-terminal, docker and host-metrics plugins start their own servers, // so disabling any of them stops its WS/HTTP server. See // plugins/ssh-terminal, plugins/docker and plugins/host-metrics. - // AI is also absent: the ai plugin registers its router with the /ai - // dispatcher in database.ts on activate, so disabling it makes /ai/* 404 - // instead of leaving a dead import here. See plugins/ai. + // AI and Proxmox are also absent for the same reason: each plugin + // registers its router with its own dispatcher in database.ts on + // activate (/ai, /proxmox), so disabling either makes its routes 404 + // instead of leaving a dead import here. See plugins/ai and + // plugins/proxmox. await import("./hosts/tunnel/index.js"); await import("./hosts/file-manager/index.js"); await import("./hosts/tmux/index.js"); diff --git a/src/backend/tests/plugins/first-party.test.ts b/src/backend/tests/plugins/first-party.test.ts index 6d431924a..eefe28e5a 100644 --- a/src/backend/tests/plugins/first-party.test.ts +++ b/src/backend/tests/plugins/first-party.test.ts @@ -39,20 +39,22 @@ function manifest(overrides: Record = {}) { } describe("first-party allowlist", () => { - it("contains only ssh-terminal, docker, host-metrics and ai", () => { + it("contains only ssh-terminal, docker, host-metrics, ai and proxmox", () => { expect([...FIRST_PARTY_PLUGIN_IDS].sort()).toEqual([ "ai", "docker", "host-metrics", + "proxmox", "ssh-terminal", ]); }); - it("recognises ssh-terminal, docker, host-metrics and ai and nothing else", () => { + it("recognises ssh-terminal, docker, host-metrics, ai and proxmox and nothing else", () => { expect(isFirstParty("ssh-terminal")).toBe(true); expect(isFirstParty("docker")).toBe(true); expect(isFirstParty("host-metrics")).toBe(true); expect(isFirstParty("ai")).toBe(true); + expect(isFirstParty("proxmox")).toBe(true); expect(isFirstParty("ssh-terminal-pro")).toBe(false); expect(isFirstParty("community-plugin")).toBe(false); expect(isFirstParty("")).toBe(false); @@ -69,6 +71,7 @@ describe("runsInProcess", () => { true, ); expect(runsInProcess("ai", [TRANSPORT_OWNER_CAPABILITY])).toBe(true); + expect(runsInProcess("proxmox", [TRANSPORT_OWNER_CAPABILITY])).toBe(true); // On the list but not asking for it: stays in a worker, so the manifest // remains an honest description of what the plugin does. @@ -76,6 +79,7 @@ describe("runsInProcess", () => { expect(runsInProcess("docker", ["hosts.read"])).toBe(false); expect(runsInProcess("host-metrics", ["hosts.read"])).toBe(false); expect(runsInProcess("ai", ["hosts.read"])).toBe(false); + expect(runsInProcess("proxmox", ["hosts.read"])).toBe(false); // Asking for it but not on the list. expect( @@ -155,6 +159,18 @@ describe("manifest gate on the reserved capability", () => { expect(parsed?.id).toBe("ai"); }); + it("allows it for proxmox", () => { + const { manifest: parsed, errors } = parseManifest( + manifest({ + id: "proxmox", + permissions: [TRANSPORT_OWNER_CAPABILITY], + }), + ); + + expect(errors).toEqual([]); + expect(parsed?.id).toBe("proxmox"); + }); + it("still accepts an ordinary manifest that never mentions it", () => { const { manifest: parsed, errors } = parseManifest( manifest({ permissions: ["hosts.read"] }), diff --git a/src/backend/tests/database/routes/proxmox-import-auth.test.ts b/src/backend/tests/plugins/proxmox/proxmox-import-auth.test.ts similarity index 90% rename from src/backend/tests/database/routes/proxmox-import-auth.test.ts rename to src/backend/tests/plugins/proxmox/proxmox-import-auth.test.ts index 47d62acce..bb61fc345 100644 --- a/src/backend/tests/database/routes/proxmox-import-auth.test.ts +++ b/src/backend/tests/plugins/proxmox/proxmox-import-auth.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import { resolveProxmoxImportAuth } from "../../../database/routes/proxmox-import-auth.js"; +import { resolveProxmoxImportAuth } from "../../../../../plugins/proxmox/backend/proxmox-import-auth.js"; // The frontend carries its own copy of this decision in -// src/ui/components/proxmox/proxmox-import-auth.ts. The two drifting apart is +// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is // what produced the reported bug, so both are held to the same matrix. describe("resolveProxmoxImportAuth", () => { it("uses the default credential for key auth when one is configured", () => { diff --git a/src/backend/tests/database/routes/proxmox-jump-hosts.test.ts b/src/backend/tests/plugins/proxmox/proxmox-jump-hosts.test.ts similarity index 91% rename from src/backend/tests/database/routes/proxmox-jump-hosts.test.ts rename to src/backend/tests/plugins/proxmox/proxmox-jump-hosts.test.ts index 0a103d508..7322a3ad7 100644 --- a/src/backend/tests/database/routes/proxmox-jump-hosts.test.ts +++ b/src/backend/tests/plugins/proxmox/proxmox-jump-hosts.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { parseProxmoxJumpHosts, serializeProxmoxJumpHosts, -} from "../../../database/routes/proxmox-jump-hosts.js"; +} from "../../../../../plugins/proxmox/backend/proxmox-jump-hosts.js"; describe("Proxmox jump-host persistence", () => { const jumpHosts = [{ hostId: 7 }, { hostId: 9 }]; diff --git a/src/backend/tests/database/routes/proxmox-sudo.test.ts b/src/backend/tests/plugins/proxmox/proxmox-sudo.test.ts similarity index 92% rename from src/backend/tests/database/routes/proxmox-sudo.test.ts rename to src/backend/tests/plugins/proxmox/proxmox-sudo.test.ts index a9c72495d..f3309ec0a 100644 --- a/src/backend/tests/database/routes/proxmox-sudo.test.ts +++ b/src/backend/tests/plugins/proxmox/proxmox-sudo.test.ts @@ -6,7 +6,8 @@ vi.mock("../../../hosts/metrics-shared/exec-elevated.js", () => ({ execElevated: (...args: unknown[]) => execElevated(...args), })); -import { execPveshCommand } from "../../../database/routes/proxmox.js"; +const { execPveshCommand } = + await import("../../../../../plugins/proxmox/backend/routes.js"); const client = {} as Client; diff --git a/src/ui/sidebar/HostEditor.tsx b/src/ui/sidebar/HostEditor.tsx index 0a534ff01..185132ec0 100644 --- a/src/ui/sidebar/HostEditor.tsx +++ b/src/ui/sidebar/HostEditor.tsx @@ -84,6 +84,7 @@ import { HostDockerTab, HostWebUiTab, HostProxmoxTab, + HostProxmoxStatsTab, HostFilesTab, } from "./HostEditorFeatureTabs"; import { HostEditorGeneralTab } from "./HostEditorGeneralTab"; @@ -2597,7 +2598,10 @@ export function HostEditor({ )} {activeTab === "proxmox" && ( - + <> + + + )} {activeTab === "files" && ( diff --git a/src/ui/sidebar/HostEditorFeatureTabs.tsx b/src/ui/sidebar/HostEditorFeatureTabs.tsx index 3d672b42e..1e09253d4 100644 --- a/src/ui/sidebar/HostEditorFeatureTabs.tsx +++ b/src/ui/sidebar/HostEditorFeatureTabs.tsx @@ -317,6 +317,26 @@ export function HostProxmoxTab({ )} + + + ); +} + +export function HostProxmoxStatsTab({ + form, + setField, +}: { + form: HostEditorForm; + setField: SetHostField; +}) { + const { t } = useTranslation(); + + return ( + } + > +
{ it("inherits an available credential even with the default 'password' authType", () => { diff --git a/tsconfig.plugins.json b/tsconfig.plugins.json index a2a273700..05f0df14d 100644 --- a/tsconfig.plugins.json +++ b/tsconfig.plugins.json @@ -29,7 +29,8 @@ "include": [ "plugins/docker/backend/**/*.ts", "plugins/host-metrics/backend/**/*.ts", - "plugins/ai/backend/**/*.ts" + "plugins/ai/backend/**/*.ts", + "plugins/proxmox/backend/**/*.ts" ], "exclude": ["src/**/*.test.ts"] }