feat: finish the fleets plugin, add ctx.hosts and host-commands SDK (B4)

This commit is contained in:
LukeGus committed 2026-09-23 13:10:18 -05:00
1 parent 37c015a760
commit c381955ed0
43 files changed
+835 -1999

No files matched your search

+75 -34
View File
@@ -10,6 +10,7 @@ import {
createCurrentCredentialRepository,
createCurrentHostRepository,
createCurrentHostResolutionRepository,
createCurrentPluginSettingsRepository,
} from "../repositories/factory.js";
import { validateParentHostId } from "./host-parent-validation.js";
import { serializeWebUiConfig } from "./host-web-endpoints.js";
@@ -295,10 +296,6 @@ export function registerHostBulkRoutes(
simpleUpdates.enableTerminalToolbar = updates.enableTerminalToolbar;
if (typeof updates.enableAiAssistant === "boolean")
simpleUpdates.enableAiAssistant = updates.enableAiAssistant;
// Disabling Proxmox is a plain flag flip; enabling is handled per-host
// below so each host can default to its own stored credential.
if (updates.enableProxmox === false)
simpleUpdates.enableProxmox = false;
if (Object.keys(simpleUpdates).length > 0) {
await hostRepository.updateManyForUser(
@@ -324,33 +321,58 @@ export function registerHostBulkRoutes(
}
}
// Enabling Proxmox needs per-host handling: each host defaults its
// Proxmox credential to the credential already stored on that host, so
// discovery works right away without picking one by hand. Existing
// proxmoxConfig values are preserved.
if (updates.enableProxmox === true) {
// Proxmox enable/disable now lives in the proxmox plugin's own
// host-scope settings, not a hosts column. Disabling is a plain flag
// flip; enabling also defaults the Proxmox credential to the one
// already stored on the host, so discovery works without picking one
// by hand. Existing proxmoxConfig values are preserved.
if (typeof updates.enableProxmox === "boolean") {
const pluginSettingsRepository =
createCurrentPluginSettingsRepository();
for (const host of ownedHosts) {
try {
const existing = host.proxmoxConfig
? JSON.parse(host.proxmoxConfig as string)
: {};
const merged = {
defaultCredentialId:
existing.defaultCredentialId ?? host.credentialId ?? null,
windowsPatterns: existing.windowsPatterns ?? "win, windows",
dockerPatterns: existing.dockerPatterns ?? "docker",
preferredPrefixes:
existing.preferredPrefixes ?? "10., 192.168.",
autoSyncEnabled: existing.autoSyncEnabled ?? false,
syncIntervalMinutes: existing.syncIntervalMinutes ?? 15,
markMissingGuests: existing.markMissingGuests ?? true,
};
await hostRepository.updateForUser(userId, host.id, {
enableProxmox: true,
proxmoxConfig: JSON.stringify(merged),
});
const scopeId = String(host.id);
await pluginSettingsRepository.set(
"proxmox",
"host",
scopeId,
"enableProxmox",
JSON.stringify(updates.enableProxmox),
);
if (updates.enableProxmox) {
const existingRow = await pluginSettingsRepository.get(
"proxmox",
"host",
scopeId,
"proxmoxConfig",
);
const existing = existingRow?.value
? JSON.parse(existingRow.value)
: {};
const merged = {
defaultCredentialId:
existing.defaultCredentialId ?? host.credentialId ?? null,
windowsPatterns: existing.windowsPatterns ?? "win, windows",
dockerPatterns: existing.dockerPatterns ?? "docker",
preferredPrefixes:
existing.preferredPrefixes ?? "10., 192.168.",
autoSyncEnabled: existing.autoSyncEnabled ?? false,
syncIntervalMinutes: existing.syncIntervalMinutes ?? 15,
markMissingGuests: existing.markMissingGuests ?? true,
};
await pluginSettingsRepository.set(
"proxmox",
"host",
scopeId,
"proxmoxConfig",
JSON.stringify(merged),
);
}
} catch {
errors.push(`Failed to enable Proxmox for host ${host.id}`);
errors.push(
`Failed to ${updates.enableProxmox ? "enable" : "disable"} Proxmox for host ${host.id}`,
);
}
}
}
@@ -716,7 +738,6 @@ export function registerHostBulkRoutes(
enableFileManager: hostData.enableFileManager !== false,
enableDocker: hostData.enableDocker || false,
enableWebUi: hostData.enableWebUi || false,
enableProxmox: hostData.enableProxmox || false,
enableTmuxMonitor: hostData.enableTmuxMonitor || false,
enableTerminalToolbar: hostData.enableTerminalToolbar !== false,
enableAiAssistant: hostData.enableAiAssistant || false,
@@ -744,9 +765,6 @@ export function registerHostBulkRoutes(
webUiConfig: hostData.enableWebUi
? serializeWebUiConfig(hostData.webUiConfig)
: null,
proxmoxConfig: hostData.proxmoxConfig
? JSON.stringify(hostData.proxmoxConfig)
: null,
terminalConfig: hostData.terminalConfig
? JSON.stringify(hostData.terminalConfig)
: null,
@@ -820,6 +838,7 @@ export function registerHostBulkRoutes(
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
const existing = existingHostMap?.get(lookupKey);
let savedHostId: number;
if (existing) {
const saved = await hostRepository.updateEncryptedForUser(
userId,
@@ -828,6 +847,7 @@ export function registerHostBulkRoutes(
);
if (!saved) throw new Error("Host no longer exists");
if (exportId !== undefined) importedIds.set(exportId, existing.id);
savedHostId = existing.id;
results.updated++;
} else {
sshDataObj.createdAt = new Date().toISOString();
@@ -836,8 +856,31 @@ export function registerHostBulkRoutes(
sshDataObj,
);
if (exportId !== undefined) importedIds.set(exportId, saved.id);
savedHostId = saved.id;
results.success++;
}
if (hostData.enableProxmox || hostData.proxmoxConfig) {
const pluginSettingsRepository =
createCurrentPluginSettingsRepository();
const scopeId = String(savedHostId);
await pluginSettingsRepository.set(
"proxmox",
"host",
scopeId,
"enableProxmox",
JSON.stringify(hostData.enableProxmox || false),
);
if (hostData.proxmoxConfig) {
await pluginSettingsRepository.set(
"proxmox",
"host",
scopeId,
"proxmoxConfig",
JSON.stringify(hostData.proxmoxConfig),
);
}
}
} catch (error) {
results.failed++;
results.errors.push(`Host ${i + 1}: ${getErrorMessage(error)}`);
@@ -996,7 +1039,6 @@ export function registerHostBulkRoutes(
enableFileManager: true,
enableDocker: false,
enableWebUi: false,
enableProxmox: false,
enableTmuxMonitor: false,
enableTerminalToolbar: true,
enableAiAssistant: false,
@@ -1015,7 +1057,6 @@ export function registerHostBulkRoutes(
statsConfig: null,
dockerConfig: null,
webUiConfig: null,
proxmoxConfig: null,
terminalConfig: null,
forceKeyboardInteractive: "false",
notes: null,
@@ -366,8 +366,6 @@ const CONNECT_LEVEL_FIELDS = new Set([
"enableDocker",
"enableWebUi",
"webUiConfig",
"enableProxmox",
"enableProxmoxStats",
"enableTmuxMonitor",
"enableTerminalToolbar",
"enableAiAssistant",
@@ -470,8 +468,6 @@ export function transformHostResponse(
enableFileManager: host.enableFileManager !== false,
enableDocker: !!host.enableDocker,
enableWebUi: !!host.enableWebUi,
enableProxmox: !!host.enableProxmox,
enableProxmoxStats: !!host.enableProxmoxStats,
enableTmuxMonitor: !!host.enableTmuxMonitor,
enableTerminalToolbar: host.enableTerminalToolbar !== false,
enableAiAssistant: !!host.enableAiAssistant,
@@ -526,12 +522,6 @@ export function transformHostResponse(
// Guarded, unlike dockerConfig directly above: parseWebUiConfig never
// throws, so a half-written config cannot take out the whole host listing.
webUiConfig: parseWebUiConfig(host.webUiConfig),
proxmoxConfig: host.proxmoxConfig
? JSON.parse(host.proxmoxConfig as string)
: undefined,
proxmoxStatsConfig: host.proxmoxStatsConfig
? JSON.parse(host.proxmoxStatsConfig as string)
: undefined,
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
useWarpgate: !!host.useWarpgate,
socks5ProxyChain: host.socks5ProxyChain
@@ -128,3 +128,22 @@ export async function withHostPluginSettings(
const values = settings.get(hostId);
return values ? { ...host, pluginSettings: values } : host;
}
/**
* Writes a set of host-scope plugin settings in one call, for a core route
* that still accepts a plugin's fields inline on the host create/update body
* (the fields haven't grown their own editor UI flow yet). Values are
* JSON-stringified the same way ctx.settings.setHost stores them.
*/
export async function writeHostPluginSettings(
pluginId: string,
hostId: number,
values: Record<string, unknown>,
): Promise<void> {
const repository = createCurrentPluginSettingsRepository();
const scopeId = String(hostId);
for (const [key, value] of Object.entries(values)) {
if (value === undefined) continue;
await repository.set(pluginId, "host", scopeId, key, JSON.stringify(value));
}
}
+84 -38
View File
@@ -48,6 +48,7 @@ import {
import {
attachHostPluginSettings,
loadHostPluginSettings,
writeHostPluginSettings,
withHostPluginSettings,
} from "./host-plugin-settings.js";
import { validateParentHostId } from "./host-parent-validation.js";
@@ -351,7 +352,6 @@ router.post(
scpLegacy: scpLegacy ? 1 : 0,
enableDocker: enableDocker ? 1 : 0,
enableWebUi: enableWebUi ? 1 : 0,
enableProxmox: enableProxmox ? 1 : 0,
enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
enableAiAssistant: enableAiAssistant ? 1 : 0,
@@ -379,17 +379,6 @@ router.post(
: webUiConfig,
)
: null,
proxmoxConfig: proxmoxConfig
? typeof proxmoxConfig === "string"
? proxmoxConfig
: JSON.stringify(proxmoxConfig)
: null,
enableProxmoxStats: enableProxmoxStats ? 1 : 0,
proxmoxStatsConfig: proxmoxStatsConfig
? typeof proxmoxStatsConfig === "string"
? proxmoxStatsConfig
: JSON.stringify(proxmoxStatsConfig)
: null,
terminalConfig: terminalConfig
? typeof terminalConfig === "string"
? terminalConfig
@@ -522,6 +511,37 @@ router.post(
}
const createdHost = result;
if (
enableProxmox !== undefined ||
proxmoxConfig !== undefined ||
enableProxmoxStats !== undefined ||
proxmoxStatsConfig !== undefined
) {
try {
await writeHostPluginSettings("proxmox", createdHost.id as number, {
enableProxmox: !!enableProxmox,
proxmoxConfig: proxmoxConfig
? typeof proxmoxConfig === "string"
? safeParseJson(proxmoxConfig)
: proxmoxConfig
: null,
enableProxmoxStats: !!enableProxmoxStats,
proxmoxStatsConfig: proxmoxStatsConfig
? typeof proxmoxStatsConfig === "string"
? safeParseJson(proxmoxStatsConfig)
: proxmoxStatsConfig
: null,
});
} catch (proxmoxSettingsError) {
sshLogger.warn("Failed to save Proxmox host settings", {
operation: "host_create_proxmox_settings",
hostId: createdHost.id,
error: getErrorMessage(proxmoxSettingsError),
});
}
}
// Standing folder shares apply to the newcomer.
try {
await applyFolderAccessRules(
@@ -784,8 +804,6 @@ router.post(
enableFileManager: true,
enableDocker: false,
enableWebUi: false,
enableProxmox: false,
enableProxmoxStats: false,
enableTmuxMonitor: false,
enableTerminalToolbar: true,
enableAiAssistant: false,
@@ -1065,7 +1083,6 @@ router.put(
scpLegacy: scpLegacy ? 1 : 0,
enableDocker: enableDocker ? 1 : 0,
enableWebUi: enableWebUi ? 1 : 0,
enableProxmox: enableProxmox ? 1 : 0,
enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
enableAiAssistant: enableAiAssistant ? 1 : 0,
@@ -1093,17 +1110,6 @@ router.put(
: webUiConfig,
)
: null,
proxmoxConfig: proxmoxConfig
? typeof proxmoxConfig === "string"
? proxmoxConfig
: JSON.stringify(proxmoxConfig)
: null,
enableProxmoxStats: enableProxmoxStats ? 1 : 0,
proxmoxStatsConfig: proxmoxStatsConfig
? typeof proxmoxStatsConfig === "string"
? proxmoxStatsConfig
: JSON.stringify(proxmoxStatsConfig)
: null,
terminalConfig: terminalConfig
? typeof terminalConfig === "string"
? terminalConfig
@@ -1413,6 +1419,36 @@ router.put(
sshDataObj,
);
if (
enableProxmox !== undefined ||
proxmoxConfig !== undefined ||
enableProxmoxStats !== undefined ||
proxmoxStatsConfig !== undefined
) {
try {
await writeHostPluginSettings("proxmox", Number(hostId), {
enableProxmox: !!enableProxmox,
proxmoxConfig: proxmoxConfig
? typeof proxmoxConfig === "string"
? safeParseJson(proxmoxConfig)
: proxmoxConfig
: null,
enableProxmoxStats: !!enableProxmoxStats,
proxmoxStatsConfig: proxmoxStatsConfig
? typeof proxmoxStatsConfig === "string"
? safeParseJson(proxmoxStatsConfig)
: proxmoxStatsConfig
: null,
});
} catch (proxmoxSettingsError) {
sshLogger.warn("Failed to save Proxmox host settings", {
operation: "host_update_proxmox_settings",
hostId: parseInt(hostId),
error: getErrorMessage(proxmoxSettingsError),
});
}
}
// A host that moved into a folder inherits that folder's standing shares.
try {
await applyFolderAccessRules(
@@ -2067,6 +2103,16 @@ router.get(
}
const resolvedHost = (await resolveHostCredentials(host, userId)) || host;
const proxmoxSettings = (
await loadHostPluginSettings([Number(hostId)])
).get(Number(hostId))?.proxmox as
| {
enableProxmox?: boolean;
proxmoxConfig?: unknown;
enableProxmoxStats?: boolean;
proxmoxStatsConfig?: unknown;
}
| undefined;
const exportedConnectionType =
(resolvedHost.connectionType as string) || "ssh";
@@ -2135,8 +2181,8 @@ router.get(
scpLegacy: !!resolvedHost.scpLegacy,
enableDocker: !!resolvedHost.enableDocker,
enableWebUi: !!resolvedHost.enableWebUi,
enableProxmox: !!resolvedHost.enableProxmox,
enableProxmoxStats: !!resolvedHost.enableProxmoxStats,
enableProxmox: !!proxmoxSettings?.enableProxmox,
enableProxmoxStats: !!proxmoxSettings?.enableProxmoxStats,
enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor,
enableTerminalToolbar: resolvedHost.enableTerminalToolbar !== false,
enableAiAssistant: !!resolvedHost.enableAiAssistant,
@@ -2163,12 +2209,8 @@ router.get(
dockerConfig: resolvedHost.dockerConfig
? JSON.parse(resolvedHost.dockerConfig as string)
: null,
proxmoxConfig: resolvedHost.proxmoxConfig
? JSON.parse(resolvedHost.proxmoxConfig as string)
: null,
proxmoxStatsConfig: resolvedHost.proxmoxStatsConfig
? JSON.parse(resolvedHost.proxmoxStatsConfig as string)
: null,
proxmoxConfig: proxmoxSettings?.proxmoxConfig ?? null,
proxmoxStatsConfig: proxmoxSettings?.proxmoxStatsConfig ?? null,
terminalConfig: resolvedHost.terminalConfig
? JSON.parse(resolvedHost.terminalConfig as string)
: null,
@@ -2243,6 +2285,9 @@ router.get(
try {
const allHosts =
await createCurrentHostResolutionRepository().findHostsByUserId(userId);
const proxmoxSettingsByHost = await loadHostPluginSettings(
allHosts.map((h) => h.id as number),
);
const exportedHosts = [];
const usedCredentialIds = new Set<number>();
@@ -2251,6 +2296,9 @@ router.get(
const resolvedHost = shareMode
? host
: (await resolveHostCredentials(host, userId)) || host;
const proxmoxSettings = proxmoxSettingsByHost.get(host.id as number)
?.proxmox as
{ enableProxmox?: boolean; proxmoxConfig?: unknown } | undefined;
const exportedConnectionType =
(resolvedHost.connectionType as string) || "ssh";
@@ -2300,7 +2348,7 @@ router.get(
enableFileManager: resolvedHost.enableFileManager !== false,
enableDocker: !!resolvedHost.enableDocker,
enableWebUi: !!resolvedHost.enableWebUi,
enableProxmox: !!resolvedHost.enableProxmox,
enableProxmox: !!proxmoxSettings?.enableProxmox,
enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor,
enableTerminalToolbar:
resolvedHost.enableTerminalToolbar !== false,
@@ -2330,9 +2378,7 @@ router.get(
dockerConfig: resolvedHost.dockerConfig
? JSON.parse(resolvedHost.dockerConfig as string)
: null,
proxmoxConfig: resolvedHost.proxmoxConfig
? JSON.parse(resolvedHost.proxmoxConfig as string)
: null,
proxmoxConfig: proxmoxSettings?.proxmoxConfig ?? null,
terminalConfig: resolvedHost.terminalConfig
? JSON.parse(resolvedHost.terminalConfig as string)
: null,