fix: Server stats not respecting interval and fixed SSH toool type issues
This commit is contained in:
@@ -1255,6 +1255,10 @@ async function deploySSHKeyToHost(
|
|||||||
return rejectAdd(err);
|
return rejectAdd(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream.on("data", () => {
|
||||||
|
// Consume output
|
||||||
|
});
|
||||||
|
|
||||||
stream.on("close", (code) => {
|
stream.on("close", (code) => {
|
||||||
clearTimeout(addTimeout);
|
clearTimeout(addTimeout);
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
@@ -1515,7 +1519,8 @@ router.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!credData.publicKey) {
|
const publicKey = credData.public_key || credData.publicKey;
|
||||||
|
if (!publicKey) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Public key is required for deployment",
|
error: "Public key is required for deployment",
|
||||||
@@ -1596,7 +1601,7 @@ router.post(
|
|||||||
|
|
||||||
const deployResult = await deploySSHKeyToHost(
|
const deployResult = await deploySSHKeyToHost(
|
||||||
hostConfig,
|
hostConfig,
|
||||||
credData.publicKey as string,
|
publicKey as string,
|
||||||
credData,
|
credData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -300,12 +300,18 @@ router.post(
|
|||||||
const { Client } = await import("ssh2");
|
const { Client } = await import("ssh2");
|
||||||
const { sshData, sshCredentials } = await import("../db/schema.js");
|
const { sshData, sshCredentials } = await import("../db/schema.js");
|
||||||
|
|
||||||
// Get host configuration
|
// Get host configuration using SimpleDBOps to decrypt credentials
|
||||||
const hostResult = await db
|
const { SimpleDBOps } = await import("../../utils/simple-db-ops.js");
|
||||||
|
|
||||||
|
const hostResult = await SimpleDBOps.select(
|
||||||
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(sshData)
|
.from(sshData)
|
||||||
.where(
|
.where(
|
||||||
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
|
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
|
||||||
|
),
|
||||||
|
"ssh_data",
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hostResult.length === 0) {
|
if (hostResult.length === 0) {
|
||||||
@@ -318,23 +324,31 @@ router.post(
|
|||||||
let password = host.password;
|
let password = host.password;
|
||||||
let privateKey = host.key;
|
let privateKey = host.key;
|
||||||
let passphrase = host.key_password;
|
let passphrase = host.key_password;
|
||||||
|
let authType = host.authType;
|
||||||
|
|
||||||
if (host.credentialId) {
|
if (host.credentialId) {
|
||||||
const credResult = await db
|
const credResult = await SimpleDBOps.select(
|
||||||
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(sshCredentials)
|
.from(sshCredentials)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(sshCredentials.id, host.credentialId),
|
eq(sshCredentials.id, host.credentialId as number),
|
||||||
eq(sshCredentials.userId, userId),
|
eq(sshCredentials.userId, userId),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
"ssh_credentials",
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (credResult.length > 0) {
|
if (credResult.length > 0) {
|
||||||
const cred = credResult[0];
|
const cred = credResult[0];
|
||||||
password = cred.password || undefined;
|
authType = (cred.auth_type || cred.authType || authType) as string;
|
||||||
privateKey = cred.private_key || cred.key || undefined;
|
password = (cred.password || undefined) as string | undefined;
|
||||||
passphrase = cred.key_password || undefined;
|
privateKey = (cred.private_key || cred.key || undefined) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
passphrase = (cred.key_password || undefined) as string | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,12 +471,24 @@ router.post(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (password) {
|
// Set auth based on authType (like terminal.ts does)
|
||||||
|
if (authType === "password" && password) {
|
||||||
config.password = password;
|
config.password = password;
|
||||||
|
} else if (authType === "key" && privateKey) {
|
||||||
|
const cleanKey = (privateKey as string)
|
||||||
|
.trim()
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.replace(/\r/g, "\n");
|
||||||
|
config.privateKey = Buffer.from(cleanKey, "utf8");
|
||||||
|
if (passphrase) {
|
||||||
|
config.passphrase = passphrase;
|
||||||
}
|
}
|
||||||
|
} else if (password) {
|
||||||
if (privateKey) {
|
// Fallback: if authType not set but password exists
|
||||||
const cleanKey = privateKey
|
config.password = password;
|
||||||
|
} else if (privateKey) {
|
||||||
|
// Fallback: if authType not set but key exists
|
||||||
|
const cleanKey = (privateKey as string)
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\r\n/g, "\n")
|
.replace(/\r\n/g, "\n")
|
||||||
.replace(/\r/g, "\n");
|
.replace(/\r/g, "\n");
|
||||||
|
|||||||
@@ -295,7 +295,12 @@ class SSHConnectionPool {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (host.password) {
|
} else if (host.password) {
|
||||||
const responses = prompts.map(() => host.password || "");
|
const responses = prompts.map((p) => {
|
||||||
|
if (/password/i.test(p.prompt)) {
|
||||||
|
return host.password || "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
finish(responses);
|
finish(responses);
|
||||||
} else {
|
} else {
|
||||||
finish(prompts.map(() => ""));
|
finish(prompts.map(() => ""));
|
||||||
@@ -595,7 +600,7 @@ interface SSHHostWithCredentials {
|
|||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: unknown[];
|
tunnelConnections: unknown[];
|
||||||
jumpHosts?: Array<{ hostId: number }>;
|
jumpHosts?: Array<{ hostId: number }>;
|
||||||
statsConfig?: string;
|
statsConfig?: string | StatsConfig;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -640,21 +645,45 @@ class PollingManager {
|
|||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
parseStatsConfig(statsConfigStr?: string): StatsConfig {
|
parseStatsConfig(statsConfigStr?: string | StatsConfig): StatsConfig {
|
||||||
if (!statsConfigStr) {
|
if (!statsConfigStr) {
|
||||||
return DEFAULT_STATS_CONFIG;
|
return DEFAULT_STATS_CONFIG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parsed: StatsConfig;
|
||||||
|
|
||||||
|
// If it's already an object, use it directly
|
||||||
|
if (typeof statsConfigStr === "object") {
|
||||||
|
parsed = statsConfigStr;
|
||||||
|
} else {
|
||||||
|
// Otherwise, parse as JSON string (may be double-encoded)
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(statsConfigStr);
|
let temp: any = JSON.parse(statsConfigStr);
|
||||||
return { ...DEFAULT_STATS_CONFIG, ...parsed };
|
|
||||||
|
// Check if we got a string back (double-encoded JSON)
|
||||||
|
if (typeof temp === "string") {
|
||||||
|
temp = JSON.parse(temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = temp;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
statsLogger.warn(
|
statsLogger.warn(
|
||||||
`Failed to parse statsConfig: ${error instanceof Error ? error.message : "Unknown error"}`,
|
`Failed to parse statsConfig: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
{
|
||||||
|
operation: "parse_stats_config_error",
|
||||||
|
statsConfigStr,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return DEFAULT_STATS_CONFIG;
|
return DEFAULT_STATS_CONFIG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge with defaults, but prioritize the provided values
|
||||||
|
const result = { ...DEFAULT_STATS_CONFIG, ...parsed };
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async startPollingForHost(host: SSHHostWithCredentials): Promise<void> {
|
async startPollingForHost(host: SSHHostWithCredentials): Promise<void> {
|
||||||
const statsConfig = this.parseStatsConfig(host.statsConfig);
|
const statsConfig = this.parseStatsConfig(host.statsConfig);
|
||||||
|
|
||||||
@@ -664,9 +693,11 @@ class PollingManager {
|
|||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
if (existingConfig.statusTimer) {
|
if (existingConfig.statusTimer) {
|
||||||
clearInterval(existingConfig.statusTimer);
|
clearInterval(existingConfig.statusTimer);
|
||||||
|
existingConfig.statusTimer = undefined;
|
||||||
}
|
}
|
||||||
if (existingConfig.metricsTimer) {
|
if (existingConfig.metricsTimer) {
|
||||||
clearInterval(existingConfig.metricsTimer);
|
clearInterval(existingConfig.metricsTimer);
|
||||||
|
existingConfig.metricsTimer = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,14 +706,6 @@ class PollingManager {
|
|||||||
this.pollingConfigs.delete(host.id);
|
this.pollingConfigs.delete(host.id);
|
||||||
this.statusStore.delete(host.id);
|
this.statusStore.delete(host.id);
|
||||||
this.metricsStore.delete(host.id);
|
this.metricsStore.delete(host.id);
|
||||||
statsLogger.info(
|
|
||||||
`Stopped all polling for host ${host.id} (${host.name || host.ip}) - both checks disabled`,
|
|
||||||
{
|
|
||||||
operation: "polling_stopped",
|
|
||||||
hostId: host.id,
|
|
||||||
hostName: host.name || host.ip,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,23 +714,20 @@ class PollingManager {
|
|||||||
statsConfig,
|
statsConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the config FIRST to prevent race conditions with old timers
|
|
||||||
this.pollingConfigs.set(host.id, config);
|
|
||||||
|
|
||||||
if (statsConfig.statusCheckEnabled) {
|
if (statsConfig.statusCheckEnabled) {
|
||||||
const intervalMs = statsConfig.statusCheckInterval * 1000;
|
const intervalMs = statsConfig.statusCheckInterval * 1000;
|
||||||
|
|
||||||
this.pollHostStatus(host);
|
this.pollHostStatus(host);
|
||||||
|
|
||||||
config.statusTimer = setInterval(() => {
|
config.statusTimer = setInterval(() => {
|
||||||
this.pollHostStatus(host);
|
// Always get the latest config to check if polling is still enabled
|
||||||
|
const latestConfig = this.pollingConfigs.get(host.id);
|
||||||
|
if (latestConfig && latestConfig.statsConfig.statusCheckEnabled) {
|
||||||
|
this.pollHostStatus(latestConfig.host);
|
||||||
|
}
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
} else {
|
} else {
|
||||||
this.statusStore.delete(host.id);
|
this.statusStore.delete(host.id);
|
||||||
statsLogger.debug(`Status polling disabled for host ${host.id}`, {
|
|
||||||
operation: "status_polling_disabled",
|
|
||||||
hostId: host.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statsConfig.metricsEnabled) {
|
if (statsConfig.metricsEnabled) {
|
||||||
@@ -717,17 +737,17 @@ class PollingManager {
|
|||||||
this.pollHostMetrics(host);
|
this.pollHostMetrics(host);
|
||||||
|
|
||||||
config.metricsTimer = setInterval(() => {
|
config.metricsTimer = setInterval(() => {
|
||||||
this.pollHostMetrics(host);
|
// Always get the latest config to check if polling is still enabled
|
||||||
|
const latestConfig = this.pollingConfigs.get(host.id);
|
||||||
|
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
|
||||||
|
this.pollHostMetrics(latestConfig.host);
|
||||||
|
}
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
} else {
|
} else {
|
||||||
this.metricsStore.delete(host.id);
|
this.metricsStore.delete(host.id);
|
||||||
statsLogger.debug(`Metrics polling disabled for host ${host.id}`, {
|
|
||||||
operation: "metrics_polling_disabled",
|
|
||||||
hostId: host.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update with the new timers
|
// Set the config with the new timers
|
||||||
this.pollingConfigs.set(host.id, config);
|
this.pollingConfigs.set(host.id, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,9 +807,11 @@ class PollingManager {
|
|||||||
if (config) {
|
if (config) {
|
||||||
if (config.statusTimer) {
|
if (config.statusTimer) {
|
||||||
clearInterval(config.statusTimer);
|
clearInterval(config.statusTimer);
|
||||||
|
config.statusTimer = undefined;
|
||||||
}
|
}
|
||||||
if (config.metricsTimer) {
|
if (config.metricsTimer) {
|
||||||
clearInterval(config.metricsTimer);
|
clearInterval(config.metricsTimer);
|
||||||
|
config.metricsTimer = undefined;
|
||||||
}
|
}
|
||||||
this.pollingConfigs.delete(hostId);
|
this.pollingConfigs.delete(hostId);
|
||||||
if (clearData) {
|
if (clearData) {
|
||||||
@@ -994,6 +1016,7 @@ async function resolveHostCredentials(
|
|||||||
tunnelConnections: host.tunnelConnections
|
tunnelConnections: host.tunnelConnections
|
||||||
? JSON.parse(host.tunnelConnections as string)
|
? JSON.parse(host.tunnelConnections as string)
|
||||||
: [],
|
: [],
|
||||||
|
jumpHosts: host.jumpHosts ? JSON.parse(host.jumpHosts as string) : [],
|
||||||
statsConfig: host.statsConfig || undefined,
|
statsConfig: host.statsConfig || undefined,
|
||||||
createdAt: host.createdAt,
|
createdAt: host.createdAt,
|
||||||
updatedAt: host.updatedAt,
|
updatedAt: host.updatedAt,
|
||||||
|
|||||||
@@ -827,13 +827,10 @@ export function CredentialsManager({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Sheet open={showDeployDialog} onOpenChange={setShowDeployDialog}>
|
<Sheet open={showDeployDialog} onOpenChange={setShowDeployDialog}>
|
||||||
<SheetContent className="w-[500px] max-w-[50vw] overflow-y-auto">
|
<SheetContent className="w-[500px] max-w-[50vw] overflow-y-auto bg-dark-bg">
|
||||||
<div className="px-4 py-4">
|
<div className="px-4 py-4">
|
||||||
<div className="space-y-3 pb-4">
|
<div className="space-y-3 pb-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/30">
|
|
||||||
<Upload className="h-5 w-5 text-green-600 dark:text-green-400" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-lg font-semibold">
|
<div className="text-lg font-semibold">
|
||||||
{t("credentials.deploySSHKey")}
|
{t("credentials.deploySSHKey")}
|
||||||
@@ -1009,7 +1006,7 @@ export function CredentialsManager({
|
|||||||
<Button
|
<Button
|
||||||
onClick={performDeploy}
|
onClick={performDeploy}
|
||||||
disabled={!selectedHostId || deployLoading}
|
disabled={!selectedHostId || deployLoading}
|
||||||
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{deployLoading ? (
|
{deployLoading ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -193,15 +193,16 @@ export function SSHToolsSidebar({
|
|||||||
getCommandHistory(activeTerminalHostId)
|
getCommandHistory(activeTerminalHostId)
|
||||||
.then((history) => {
|
.then((history) => {
|
||||||
setCommandHistory((prevHistory) => {
|
setCommandHistory((prevHistory) => {
|
||||||
|
const newHistory = Array.isArray(history) ? history : [];
|
||||||
// Only update if history actually changed
|
// Only update if history actually changed
|
||||||
if (JSON.stringify(prevHistory) !== JSON.stringify(history)) {
|
if (JSON.stringify(prevHistory) !== JSON.stringify(newHistory)) {
|
||||||
// Use requestAnimationFrame to restore scroll after React finishes rendering
|
// Use requestAnimationFrame to restore scroll after React finishes rendering
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (commandHistoryScrollRef.current) {
|
if (commandHistoryScrollRef.current) {
|
||||||
commandHistoryScrollRef.current.scrollTop = scrollTop;
|
commandHistoryScrollRef.current.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return history;
|
return newHistory;
|
||||||
}
|
}
|
||||||
return prevHistory;
|
return prevHistory;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export function SSHAuthDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-50 flex items-center justify-center bg-dark-bg animate-in fade-in duration-200"
|
className="absolute inset-0 z-9999 flex items-center justify-center bg-dark-bg animate-in fade-in duration-200"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
>
|
>
|
||||||
<Card className="w-full max-w-2xl mx-4 border-2 animate-in fade-in zoom-in-95 duration-200">
|
<Card className="w-full max-w-2xl mx-4 border-2 animate-in fade-in zoom-in-95 duration-200">
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const AppContent: FC = () => {
|
|||||||
|
|
||||||
if (authLoading) {
|
if (authLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen flex items-center justify-center bg-dark-bg-darkest">
|
<div className="h-screen w-screen flex items-center justify-center bg-dark-bg">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||||
<p className="text-muted-foreground">{t("common.loading")}</p>
|
<p className="text-muted-foreground">{t("common.loading")}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user