chore: lock the plugin boundary and remove 2.9.0 leftovers

This commit is contained in:
LukeGus committed 2026-09-25 07:24:46 -05:00
1 parent 6489b62ece
commit 2961297989
399 files changed
+3964 -5627

No files matched your search

+57
View File
@@ -1004,6 +1004,63 @@ router.post("/discover", async (req, res) => {
}
});
/**
* @openapi
* /plugin-api/proxmox/import:
* post:
* summary: Import discovered Proxmox guests as hosts
* description: >
* Creates one host per entry, owned by the caller, and stores each
* entry's proxmox host settings (the guest's source) on the new host.
* tags:
* - Proxmox
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - hosts
* properties:
* hosts:
* type: array
* items:
* type: object
* responses:
* 200:
* description: How many hosts were created and which failed.
* 400:
* description: No hosts given.
*/
router.post("/import", async (req, res) => {
const ctx = pluginCtx();
const { hosts } = req.body as { hosts?: unknown };
if (!Array.isArray(hosts) || hosts.length === 0) {
return res.status(400).json({ error: "No hosts to import" });
}
const result = { success: 0, failed: 0, errors: [] as string[] };
for (const entry of hosts as Array<Record<string, unknown>>) {
const { pluginSettings, ...input } = entry;
try {
const created = await ctx.hosts.create(
input as unknown as PluginHostCreateInput,
);
const own = (pluginSettings as Record<string, unknown> | undefined)?.[
ctx.pluginId
] as Record<string, unknown> | undefined;
for (const [key, value] of Object.entries(own ?? {})) {
await ctx.settings.setHost(Number(created.id), key, value);
}
result.success++;
} catch (err: unknown) {
result.failed++;
result.errors.push(`${String(input.name)}: ${getErrorMessage(err)}`);
}
}
return res.json(result);
});
/** 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
+18 -17
View File
@@ -1,10 +1,14 @@
import { useEffect, useState } from "react";
import { useSshAuthTypes, useTranslation } from "@termix/plugin-sdk/frontend";
import { Server } from "lucide-react";
import { Input } from "@/components/input";
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
import { getCredentials } from "@/main-axios";
import { Select2 } from "@/components/select2";
import {
FakeSwitch,
Input,
SectionCard,
Select2,
SettingRow,
} from "@termix/plugin-sdk/ui";
import { listCredentials } from "@termix/plugin-sdk/frontend";
import type { ProxmoxHostConfig } from "./types";
interface HostProxmoxForm {
@@ -28,19 +32,16 @@ export function HostProxmoxTab({
>([]);
useEffect(() => {
getCredentials()
.then((res: unknown) => {
const raw =
(res as { credentials?: unknown })?.credentials ?? res ?? [];
const list = (Array.isArray(raw) ? raw : []).map(
(c: Record<string, unknown>) => ({
id: c.id as number,
name: c.name as string,
username: (c.username as string | null) ?? null,
}),
);
setCredentials(list);
})
listCredentials()
.then((list) =>
setCredentials(
list.map((c) => ({
id: c.id,
name: c.name,
username: c.username ?? null,
})),
),
)
.catch(() => {});
}, []);
@@ -1,31 +1,26 @@
import { useRef, useState } from "react";
import type { HostData } from "@/types/index";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Server, RefreshCw, CheckSquare, Square, Download } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/button";
import { Button } from "@termix/plugin-sdk/ui";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/dialog";
import { Select2 } from "@/components/select2";
import {
discoverProxmoxGuestsStream,
bulkImportSSHHosts,
getSSHHosts,
} from "@/main-axios";
import type { SSHHostWithStatus } from "@/main-axios";
import type { ProxmoxGuest } from "@/types/proxmox";
} from "@termix/plugin-sdk/ui";
import { Select2 } from "@termix/plugin-sdk/ui";
import { discoverProxmoxGuestsStream, importProxmoxHosts } from "./proxmox-api";
import { listHosts, type PluginHostRecord } from "@termix/plugin-sdk/frontend";
import type { ProxmoxGuest } from "./types";
import { resolveProxmoxImportAuth } from "./proxmox-import-auth";
interface ProxmoxDiscoverDialogProps {
open: boolean;
onClose: () => void;
hosts: SSHHostWithStatus[];
onHostsChanged: (hosts: SSHHostWithStatus[]) => void;
hosts: PluginHostRecord[];
onHostsChanged: (hosts: PluginHostRecord[]) => void;
/** Pre-select a specific host and skip the host picker */
preselectedHostId?: number;
/** Credential to use for imported hosts */
@@ -69,7 +64,11 @@ export function ProxmoxDiscoverDialog({
// When opened from the dropdown (no preselectedHostId), only show Proxmox-enabled hosts
const sshHosts = hosts.filter(
(h) =>
!("isFolder" in h) && h.pluginSettings?.proxmox?.enableProxmox === true,
!("isFolder" in h) &&
(
h.pluginSettings as
Record<string, Record<string, unknown> | undefined> | undefined
)?.proxmox?.enableProxmox === true,
);
// The Proxmox host the discovery runs against — imported guests are grouped
@@ -189,11 +188,11 @@ export function ProxmoxDiscoverDialog({
}));
const result = toImport.length
? await bulkImportSSHHosts(toImport as unknown as HostData[], false)
: { success: 0, updated: 0, skipped: 0, failed: 0 };
? await importProxmoxHosts(toImport)
: { success: 0, failed: 0, errors: [] };
if (toImport.length) {
const updated = await getSSHHosts();
const updated = await listHosts();
onHostsChanged(updated);
window.dispatchEvent(new CustomEvent("termix:hosts-changed"));
}
@@ -202,9 +201,6 @@ export function ProxmoxDiscoverDialog({
result.success
? t("hosts.proxmoxResultImported", { count: result.success })
: null,
result.updated
? t("hosts.proxmoxResultUpdated", { count: result.updated })
: null,
result.failed
? t("hosts.proxmoxResultFailed", { count: result.failed })
: null,
+7 -5
View File
@@ -9,13 +9,13 @@ import {
type TermixApp,
} from "@termix/plugin-sdk/frontend";
import { ComponentSlot, DropdownMenuItem } from "@termix/plugin-sdk/ui";
import type { SSHHostWithStatus } from "@/main-axios";
import type { PluginHostRecord } from "@termix/plugin-sdk/frontend";
import { setProxmoxApp } from "./proxmox-api";
import { ProxmoxDiscoverDialog } from "./ProxmoxDiscoverDialog";
import { HostProxmoxTab } from "./HostProxmoxTab";
import { ProxmoxStatsTab } from "./stats/ProxmoxStatsTab";
import ProxmoxStatsApp from "./stats/ProxmoxStatsApp";
import { HostProxmoxStatsTab } from "./stats/HostProxmoxStatsTab";
import type { SSHHost } from "@/types";
interface DiscoverRequest {
hostId?: number;
@@ -79,7 +79,7 @@ function ProxmoxHostSection({ form, updateForm }: HostEditorSectionProps) {
function ProxmoxStatsTabView({ sshHost, label, isVisible }: TabProps) {
return (
<ProxmoxStatsTab
hostConfig={sshHost as unknown as SSHHost}
hostConfig={sshHost as never}
title={label}
isVisible={isVisible}
isTopbarOpen={false}
@@ -93,6 +93,8 @@ function ProxmoxStatsStandalone({ hostId }: StandaloneViewProps) {
}
export function activate(app: TermixApp): void {
setProxmoxApp(app);
app.onDispose(() => setProxmoxApp(null));
// The dialog lives in the hosts panel; the menu item and host action open
// it through this, created per activation.
const openers = new Set<Opener>();
@@ -104,8 +106,8 @@ export function activate(app: TermixApp): void {
hosts,
onHostsChanged,
}: {
hosts: SSHHostWithStatus[];
onHostsChanged: (hosts: SSHHostWithStatus[]) => void;
hosts: PluginHostRecord[];
onHostsChanged: (hosts: PluginHostRecord[]) => void;
}) {
const [request, setRequest] = useState<DiscoverRequest | null>(null);
useEffect(() => {
+117
View File
@@ -0,0 +1,117 @@
import type { TermixApp } from "@termix/plugin-sdk/frontend";
import type { ProxmoxDiscoverResult } from "./types";
let current: TermixApp | null = null;
/** Set in activate, cleared on deactivate. */
export function setProxmoxApp(app: TermixApp | null): void {
current = app;
}
function app(): TermixApp {
if (!current) throw new Error("The Proxmox plugin is not active");
return current;
}
/**
* Runs a guest discovery over the server-sent event stream and reports
* progress. Returns a function that stops listening.
*/
export function discoverProxmoxGuestsStream(
hostId: number,
handlers: {
onProgress?: (done: number, total: number) => void;
onResult: (result: ProxmoxDiscoverResult) => void;
onError: (message: string) => void;
},
): () => void {
const controller = new AbortController();
let settled = false;
const finish = () => {
settled = true;
controller.abort();
};
const handleEvent = (event: string, data: string) => {
if (event === "progress") {
try {
const parsed = JSON.parse(data);
handlers.onProgress?.(parsed.done, parsed.total);
} catch {
// ignore malformed progress frames
}
} else if (event === "result") {
finish();
try {
handlers.onResult(JSON.parse(data));
} catch {
handlers.onError("Failed to parse discovery result");
}
} else if (event === "fail") {
finish();
let message = "Discovery failed";
try {
message = JSON.parse(data).message || message;
} catch {
// keep default message
}
handlers.onError(message);
}
};
void (async () => {
try {
const response = await app().fetch(
`/discover/stream?hostId=${encodeURIComponent(String(hostId))}`,
{ signal: controller.signal },
);
if (!response.ok || !response.body) throw new Error("stream failed");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let split = buffer.indexOf("\n\n");
while (split !== -1) {
const frame = buffer.slice(0, split);
buffer = buffer.slice(split + 2);
let event = "message";
const data: string[] = [];
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data.push(line.slice(5).trim());
}
if (data.length) handleEvent(event, data.join("\n"));
split = buffer.indexOf("\n\n");
}
}
if (!settled) {
settled = true;
handlers.onError("Discovery connection lost");
}
} catch {
if (settled) return;
settled = true;
handlers.onError("Discovery connection lost");
}
})();
return finish;
}
export interface ProxmoxImportResult {
success: number;
failed: number;
errors: string[];
}
export async function importProxmoxHosts(
hosts: Record<string, unknown>[],
): Promise<ProxmoxImportResult> {
const response = await app().api.post<ProxmoxImportResult>("/import", {
hosts,
});
return response.data;
}
+18
View File
@@ -133,3 +133,21 @@ export interface ProxmoxHostConfig {
errors: string[];
};
}
export interface ProxmoxGuest {
name: string;
vmid: number;
type: "qemu" | "lxc";
node: string;
status: string;
ip: string | null;
connectionType: "ssh" | "rdp";
enableDocker: boolean;
}
export interface ProxmoxDiscoverResult {
guests: ProxmoxGuest[];
credentialId: number | null;
defaultCredentialId: number | null;
jumpHosts?: unknown[] | null;
}