mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: finish the fleets plugin, add ctx.hosts and host-commands SDK (B4)
This commit is contained in:
1 parent
37c015a760
commit
c381955ed0
43 files changed
+835
-1999
No files matched your search
@@ -846,20 +846,6 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_host_metrics_prefs_user_host
|
||||
ON host_metrics_preferences (user_id, host_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proxmox_stats_preferences (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
host_id INTEGER NOT NULL,
|
||||
layout TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_proxmox_stats_prefs_user_host
|
||||
ON proxmox_stats_preferences (user_id, host_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS host_sidebar_preferences (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
@@ -1342,18 +1328,6 @@ const migrateSchema = () => {
|
||||
"INTEGER NOT NULL DEFAULT 0",
|
||||
);
|
||||
addColumnIfNotExists("ssh_data", "web_ui_config", "TEXT");
|
||||
addColumnIfNotExists(
|
||||
"ssh_data",
|
||||
"enable_proxmox",
|
||||
"INTEGER NOT NULL DEFAULT 0",
|
||||
);
|
||||
addColumnIfNotExists("ssh_data", "proxmox_config", "TEXT");
|
||||
addColumnIfNotExists(
|
||||
"ssh_data",
|
||||
"enable_proxmox_stats",
|
||||
"INTEGER NOT NULL DEFAULT 0",
|
||||
);
|
||||
addColumnIfNotExists("ssh_data", "proxmox_stats_config", "TEXT");
|
||||
addColumnIfNotExists(
|
||||
"ssh_data",
|
||||
"enable_tmux_monitor",
|
||||
@@ -2381,34 +2355,6 @@ const migrateSchema = () => {
|
||||
}
|
||||
// --- metrics-history end ---
|
||||
|
||||
// --- proxmox-node-history begin ---
|
||||
try {
|
||||
sqlite.prepare("SELECT id FROM proxmox_node_history LIMIT 1").get();
|
||||
} catch {
|
||||
try {
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS proxmox_node_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE,
|
||||
ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
cpu_percent REAL,
|
||||
mem_percent REAL,
|
||||
disk_percent REAL,
|
||||
net_rx_bytes INTEGER,
|
||||
net_tx_bytes INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxmox_node_history_host_ts
|
||||
ON proxmox_node_history (host_id, ts DESC);
|
||||
`);
|
||||
} catch (createError) {
|
||||
databaseLogger.warn("Failed to create proxmox_node_history table", {
|
||||
operation: "schema_migration",
|
||||
error: createError,
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- proxmox-node-history end ---
|
||||
|
||||
// --- alerts begin ---
|
||||
// alert_rules, alert_rule_channels and alert_firings were only ever created
|
||||
// here, never declared in schema.ts, so they never existed on Postgres or
|
||||
|
||||
@@ -298,14 +298,6 @@ export const hosts = mysqlTable(
|
||||
statsConfig: text("stats_config"),
|
||||
dockerConfig: text("docker_config"),
|
||||
webUiConfig: text("web_ui_config"),
|
||||
enableProxmox: boolean("enable_proxmox")
|
||||
.notNull()
|
||||
.default(false),
|
||||
proxmoxConfig: text("proxmox_config"),
|
||||
enableProxmoxStats: boolean("enable_proxmox_stats")
|
||||
.notNull()
|
||||
.default(false),
|
||||
proxmoxStatsConfig: text("proxmox_stats_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
notes: text("notes"),
|
||||
@@ -1161,27 +1153,6 @@ export const hostMetricsPreferences = mysqlTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const proxmoxStatsPreferences = mysqlTable(
|
||||
"proxmox_stats_preferences",
|
||||
{
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: varchar("user_id", { length: 255 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: int("host_id").notNull().references(() => hosts.id, { onDelete: "cascade" }),
|
||||
// JSON-encoded ProxmoxStatsLayout. Layout has no secrets, so it is stored as
|
||||
// plain JSON (no field-level encryption), same convention as hostMetricsPreferences.layout.
|
||||
layout: text("layout").notNull(),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: varchar("updated_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_proxmox_stats_prefs_user_host").on(table.userId, table.hostId),
|
||||
],
|
||||
);
|
||||
|
||||
export const hostSidebarPreferences = mysqlTable("host_sidebar_preferences", {
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.primaryKey()
|
||||
@@ -1387,23 +1358,6 @@ export const hostMetricsHistory = mysqlTable("host_metrics_history", {
|
||||
});
|
||||
// --- metrics-history end ---
|
||||
|
||||
// --- proxmox-node-history begin ---
|
||||
export const proxmoxNodeHistory = mysqlTable("proxmox_node_history", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
hostId: int("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
ts: text("ts")
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
cpuPercent: double("cpu_percent"),
|
||||
memPercent: double("mem_percent"),
|
||||
diskPercent: double("disk_percent"),
|
||||
netRxBytes: int("net_rx_bytes"),
|
||||
netTxBytes: int("net_tx_bytes"),
|
||||
});
|
||||
// --- proxmox-node-history end ---
|
||||
|
||||
// --- alerts begin ---
|
||||
export const notificationChannels = mysqlTable("notification_channels", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
|
||||
@@ -299,14 +299,6 @@ export const hosts = pgTable(
|
||||
statsConfig: text("stats_config"),
|
||||
dockerConfig: text("docker_config"),
|
||||
webUiConfig: text("web_ui_config"),
|
||||
enableProxmox: boolean("enable_proxmox")
|
||||
.notNull()
|
||||
.default(false),
|
||||
proxmoxConfig: text("proxmox_config"),
|
||||
enableProxmoxStats: boolean("enable_proxmox_stats")
|
||||
.notNull()
|
||||
.default(false),
|
||||
proxmoxStatsConfig: text("proxmox_stats_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
notes: text("notes"),
|
||||
@@ -1162,27 +1154,6 @@ export const hostMetricsPreferences = pgTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const proxmoxStatsPreferences = pgTable(
|
||||
"proxmox_stats_preferences",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
userId: varchar("user_id", { length: 255 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id").notNull().references(() => hosts.id, { onDelete: "cascade" }),
|
||||
// JSON-encoded ProxmoxStatsLayout. Layout has no secrets, so it is stored as
|
||||
// plain JSON (no field-level encryption), same convention as hostMetricsPreferences.layout.
|
||||
layout: text("layout").notNull(),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: varchar("updated_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_proxmox_stats_prefs_user_host").on(table.userId, table.hostId),
|
||||
],
|
||||
);
|
||||
|
||||
export const hostSidebarPreferences = pgTable("host_sidebar_preferences", {
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.primaryKey()
|
||||
@@ -1388,23 +1359,6 @@ export const hostMetricsHistory = pgTable("host_metrics_history", {
|
||||
});
|
||||
// --- metrics-history end ---
|
||||
|
||||
// --- proxmox-node-history begin ---
|
||||
export const proxmoxNodeHistory = pgTable("proxmox_node_history", {
|
||||
id: serial("id").primaryKey(),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
ts: text("ts")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
cpuPercent: doublePrecision("cpu_percent"),
|
||||
memPercent: doublePrecision("mem_percent"),
|
||||
diskPercent: doublePrecision("disk_percent"),
|
||||
netRxBytes: integer("net_rx_bytes"),
|
||||
netTxBytes: integer("net_tx_bytes"),
|
||||
});
|
||||
// --- proxmox-node-history end ---
|
||||
|
||||
// --- alerts begin ---
|
||||
export const notificationChannels = pgTable("notification_channels", {
|
||||
id: serial("id").primaryKey(),
|
||||
|
||||
@@ -291,14 +291,6 @@ export const hosts = sqliteTable(
|
||||
statsConfig: text("stats_config"),
|
||||
dockerConfig: text("docker_config"),
|
||||
webUiConfig: text("web_ui_config"),
|
||||
enableProxmox: integer("enable_proxmox", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
proxmoxConfig: text("proxmox_config"),
|
||||
enableProxmoxStats: integer("enable_proxmox_stats", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
proxmoxStatsConfig: text("proxmox_stats_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
notes: text("notes"),
|
||||
@@ -1160,27 +1152,6 @@ export const hostMetricsPreferences = sqliteTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const proxmoxStatsPreferences = sqliteTable(
|
||||
"proxmox_stats_preferences",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id").notNull().references(() => hosts.id, { onDelete: "cascade" }),
|
||||
// JSON-encoded ProxmoxStatsLayout. Layout has no secrets, so it is stored as
|
||||
// plain JSON (no field-level encryption), same convention as hostMetricsPreferences.layout.
|
||||
layout: text("layout").notNull(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_proxmox_stats_prefs_user_host").on(table.userId, table.hostId),
|
||||
],
|
||||
);
|
||||
|
||||
export const hostSidebarPreferences = sqliteTable("host_sidebar_preferences", {
|
||||
userId: text("user_id")
|
||||
.primaryKey()
|
||||
@@ -1386,23 +1357,6 @@ export const hostMetricsHistory = sqliteTable("host_metrics_history", {
|
||||
});
|
||||
// --- metrics-history end ---
|
||||
|
||||
// --- proxmox-node-history begin ---
|
||||
export const proxmoxNodeHistory = sqliteTable("proxmox_node_history", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
ts: text("ts")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
cpuPercent: real("cpu_percent"),
|
||||
memPercent: real("mem_percent"),
|
||||
diskPercent: real("disk_percent"),
|
||||
netRxBytes: integer("net_rx_bytes"),
|
||||
netTxBytes: integer("net_tx_bytes"),
|
||||
});
|
||||
// --- proxmox-node-history end ---
|
||||
|
||||
// --- alerts begin ---
|
||||
export const notificationChannels = sqliteTable("notification_channels", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
|
||||
@@ -21,7 +21,6 @@ import { HostFolderRepository } from "./host-folder-repository.js";
|
||||
import { HostHealthRepository } from "./host-health-repository.js";
|
||||
import { HostMetricsHistoryRepository } from "./host-metrics-history-repository.js";
|
||||
import { HostMetricsPreferenceRepository } from "./host-metrics-preference-repository.js";
|
||||
import { ProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js";
|
||||
import { HostRepository } from "./host-repository.js";
|
||||
import { HostResolutionRepository } from "./host-resolution-repository.js";
|
||||
import { HostSidebarPreferenceRepository } from "./host-sidebar-preference-repository.js";
|
||||
@@ -283,15 +282,6 @@ export function createCurrentHostMetricsPreferenceRepository(): HostMetricsPrefe
|
||||
);
|
||||
}
|
||||
|
||||
export function createCurrentProxmoxNodeHistoryRepository(): ProxmoxNodeHistoryRepository {
|
||||
return new ProxmoxNodeHistoryRepository(
|
||||
createCurrentRepositoryContext(),
|
||||
createCurrentRepositoryLazyWriteHook(
|
||||
"proxmox_node_history_repository_write",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function createCurrentHostRepository(): HostRepository {
|
||||
return new HostRepository(
|
||||
createCurrentRepositoryContext(),
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface HostBulkUpdateState {
|
||||
id: number;
|
||||
statsConfig: string | null;
|
||||
credentialId: number | null;
|
||||
proxmoxConfig: string | null;
|
||||
}
|
||||
|
||||
export class HostRepository {
|
||||
@@ -103,19 +102,6 @@ export class HostRepository {
|
||||
return DataCrypto.decryptRecord("ssh_data", row, userId, userDataKey);
|
||||
}
|
||||
|
||||
async listProxmoxEnabled(): Promise<
|
||||
Pick<HostRecord, "id" | "userId" | "proxmoxConfig">[]
|
||||
> {
|
||||
return this.context.drizzle
|
||||
.select({
|
||||
id: hosts.id,
|
||||
userId: hosts.userId,
|
||||
proxmoxConfig: hosts.proxmoxConfig,
|
||||
})
|
||||
.from(hosts)
|
||||
.where(eq(hosts.enableProxmox, true));
|
||||
}
|
||||
|
||||
async listByUserId(userId: string): Promise<HostRecord[]> {
|
||||
return this.context.drizzle
|
||||
.select()
|
||||
@@ -248,7 +234,6 @@ export class HostRepository {
|
||||
id: hosts.id,
|
||||
statsConfig: hosts.statsConfig,
|
||||
credentialId: hosts.credentialId,
|
||||
proxmoxConfig: hosts.proxmoxConfig,
|
||||
})
|
||||
.from(hosts)
|
||||
.where(and(inArray(hosts.id, hostIds), eq(hosts.userId, userId)));
|
||||
|
||||
@@ -89,6 +89,29 @@ export class PluginSettingsRepository {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every row across all scope ids for one plugin/scope/key, regardless of
|
||||
* owner. For a plugin's own background scan (auto-sync, polling) that needs
|
||||
* to find every host with a setting enabled, across every user, before it
|
||||
* knows which actor to run as.
|
||||
*/
|
||||
async listByKey(
|
||||
pluginId: string,
|
||||
scope: PluginSettingsScope,
|
||||
key: string,
|
||||
): Promise<PluginSettingsRecord[]> {
|
||||
return this.context.drizzle
|
||||
.select()
|
||||
.from(pluginSettings)
|
||||
.where(
|
||||
and(
|
||||
eq(pluginSettings.pluginId, pluginId),
|
||||
eq(pluginSettings.scope, scope),
|
||||
eq(pluginSettings.key, key),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async set(
|
||||
pluginId: string,
|
||||
scope: PluginSettingsScope,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { and, asc, eq, gte, lt, lte } from "drizzle-orm";
|
||||
import { proxmoxNodeHistory } from "../db/schema.js";
|
||||
import type { DatabaseContext } from "./database-context.js";
|
||||
import { sqlTimestampDaysAgo } from "./sql-timestamp.js";
|
||||
|
||||
export type ProxmoxNodeHistoryRecord = typeof proxmoxNodeHistory.$inferSelect;
|
||||
|
||||
export interface ProxmoxNodeHistoryCreateInput {
|
||||
hostId: number;
|
||||
cpuPercent?: number | null;
|
||||
memPercent?: number | null;
|
||||
diskPercent?: number | null;
|
||||
netRxBytes?: number | null;
|
||||
netTxBytes?: number | null;
|
||||
}
|
||||
|
||||
export class ProxmoxNodeHistoryRepository {
|
||||
constructor(
|
||||
private readonly context: DatabaseContext,
|
||||
private readonly onWrite?: () => void | Promise<void>,
|
||||
) {}
|
||||
|
||||
async create(input: ProxmoxNodeHistoryCreateInput): Promise<void> {
|
||||
await this.context.drizzle.insert(proxmoxNodeHistory).values({
|
||||
hostId: input.hostId,
|
||||
cpuPercent: input.cpuPercent,
|
||||
memPercent: input.memPercent,
|
||||
diskPercent: input.diskPercent,
|
||||
netRxBytes: input.netRxBytes,
|
||||
netTxBytes: input.netTxBytes,
|
||||
});
|
||||
|
||||
await this.afterWrite();
|
||||
}
|
||||
|
||||
async pruneOlderThan(hostId: number, retentionDays: number): Promise<void> {
|
||||
await this.context.drizzle
|
||||
.delete(proxmoxNodeHistory)
|
||||
.where(
|
||||
and(
|
||||
eq(proxmoxNodeHistory.hostId, hostId),
|
||||
lt(proxmoxNodeHistory.ts, sqlTimestampDaysAgo(retentionDays)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async listRange(
|
||||
hostId: number,
|
||||
fromTs: string,
|
||||
toTs: string,
|
||||
): Promise<ProxmoxNodeHistoryRecord[]> {
|
||||
return this.context.drizzle
|
||||
.select()
|
||||
.from(proxmoxNodeHistory)
|
||||
.where(
|
||||
and(
|
||||
eq(proxmoxNodeHistory.hostId, hostId),
|
||||
gte(proxmoxNodeHistory.ts, fromTs),
|
||||
lte(proxmoxNodeHistory.ts, toTs),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(proxmoxNodeHistory.ts));
|
||||
}
|
||||
|
||||
private async afterWrite(): Promise<void> {
|
||||
await this.onWrite?.();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user