diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts
index 418e6dfd..186b8ff4 100644
--- a/src/backend/database/db/index.ts
+++ b/src/backend/database/db/index.ts
@@ -405,7 +405,6 @@ const migrateSchema = () => {
addColumnIfNotExists('users', 'token_url', 'TEXT');
try {
sqlite.prepare(`ALTER TABLE users DROP COLUMN redirect_uri`).run();
- logger.info('Removed redirect_uri column from users table');
} catch (e) {
}
diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts
index aca004ab..d43aa630 100644
--- a/src/backend/database/routes/users.ts
+++ b/src/backend/database/routes/users.ts
@@ -79,7 +79,12 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str
const key = await importJWK(publicKey);
const {payload} = await jwtVerify(idToken, key, {
- issuer: [issuerUrl, issuerUrl.replace(/\/application\/o\/[^\/]+$/, '')],
+ issuer: [
+ issuerUrl,
+ normalizedIssuerUrl,
+ issuerUrl.replace(/\/application\/o\/[^\/]+$/, ''),
+ normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '')
+ ],
audience: clientId,
});
diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts
index e4cca4ad..8fb6c050 100644
--- a/src/backend/ssh/server-stats.ts
+++ b/src/backend/ssh/server-stats.ts
@@ -127,13 +127,11 @@ function buildSshConfig(host: HostRecord): ConnectConfig {
if (host.keyPassword) {
(base as any).passphrase = host.keyPassword;
}
-
- logger.info(`SSH key authentication configured for host ${host.ip}`);
+
} catch (keyError) {
logger.error(`SSH key format error for host ${host.ip}: ${keyError.message}`);
if (host.password) {
(base as any).password = host.password;
- logger.info(`Falling back to password authentication for host ${host.ip}`);
} else {
throw new Error(`Invalid SSH key format for host ${host.ip}`);
}
@@ -297,15 +295,27 @@ async function collectMetrics(host: HostRecord): Promise<{
let usedHuman: string | null = null;
let totalHuman: string | null = null;
try {
- const diskOut = await execCommand(client, 'df -h -P / | tail -n +2');
- const line = diskOut.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
- const parts = line.split(/\s+/);
- if (parts.length >= 6) {
- totalHuman = parts[1] || null;
- usedHuman = parts[2] || null;
- const pctStr = (parts[4] || '').replace('%', '');
- const pctNum = Number(pctStr);
- diskPercent = Number.isFinite(pctNum) ? pctNum : null;
+ // Get both human-readable and bytes format for accurate calculation
+ const diskOutHuman = await execCommand(client, 'df -h -P / | tail -n +2');
+ const diskOutBytes = await execCommand(client, 'df -B1 -P / | tail -n +2');
+
+ const humanLine = diskOutHuman.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
+ const bytesLine = diskOutBytes.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
+
+ const humanParts = humanLine.split(/\s+/);
+ const bytesParts = bytesLine.split(/\s+/);
+
+ if (humanParts.length >= 6 && bytesParts.length >= 6) {
+ totalHuman = humanParts[1] || null;
+ usedHuman = humanParts[2] || null;
+
+ // Calculate our own percentage using bytes for accuracy
+ const totalBytes = Number(bytesParts[1]);
+ const usedBytes = Number(bytesParts[2]);
+
+ if (Number.isFinite(totalBytes) && Number.isFinite(usedBytes) && totalBytes > 0) {
+ diskPercent = Math.max(0, Math.min(100, (usedBytes / totalBytes) * 100));
+ }
}
} catch (e) {
diskPercent = null;
diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts
index e962df4e..bf49f1e0 100644
--- a/src/backend/ssh/terminal.ts
+++ b/src/backend/ssh/terminal.ts
@@ -310,8 +310,6 @@ wss.on('connection', (ws: WebSocket) => {
if (keyType && keyType !== 'auto') {
connectConfig.privateKeyType = keyType;
}
-
- logger.info('SSH key authentication configured successfully');
} catch (keyError) {
logger.error('SSH key format error: ' + keyError.message);
ws.send(JSON.stringify({type: 'error', message: 'SSH key format error: Invalid private key format'}));
diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts
index 2c2dad55..274af443 100644
--- a/src/backend/ssh/tunnel.ts
+++ b/src/backend/ssh/tunnel.ts
@@ -197,7 +197,8 @@ function classifyError(errorMessage: string): ErrorType {
if (message.includes("connect etimedout") ||
message.includes("timeout") ||
- message.includes("timed out")) {
+ message.includes("timed out") ||
+ message.includes("keepalive timeout")) {
return ERROR_TYPES.TIMEOUT;
}
@@ -267,7 +268,8 @@ function cleanupTunnelResources(tunnelName: string): void {
tunnelName,
`${tunnelName}_confirm`,
`${tunnelName}_retry`,
- `${tunnelName}_verify_retry`
+ `${tunnelName}_verify_retry`,
+ `${tunnelName}_ping`
];
timerKeys.forEach(key => {
@@ -302,7 +304,7 @@ function resetRetryState(tunnelName: string): void {
countdownIntervals.delete(tunnelName);
}
- ['', '_confirm', '_retry', '_verify_retry'].forEach(suffix => {
+ ['', '_confirm', '_retry', '_verify_retry', '_ping'].forEach(suffix => {
const timerKey = `${tunnelName}${suffix}`;
if (verificationTimers.has(timerKey)) {
clearTimeout(verificationTimers.get(timerKey)!);
@@ -353,7 +355,8 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
const maxRetries = tunnelConfig.maxRetries || 3;
const retryInterval = tunnelConfig.retryInterval || 5000;
- let retryCount = (retryCounters.get(tunnelName) || 0) + 1;
+ let retryCount = retryCounters.get(tunnelName) || 0;
+ retryCount = retryCount + 1;
if (retryCount > maxRetries) {
logger.error(`All ${maxRetries} retries failed for ${tunnelName}`);
@@ -420,7 +423,6 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
if (!manualDisconnects.has(tunnelName)) {
activeTunnels.delete(tunnelName);
-
connectSSHTunnel(tunnelConfig, retryCount);
}
}, retryInterval);
@@ -438,13 +440,43 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
}
function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig, isPeriodic = false): void {
- broadcastTunnelStatus(tunnelName, {
- connected: true,
- status: CONNECTION_STATES.CONNECTED
- });
+ if (isPeriodic) {
+ if (!activeTunnels.has(tunnelName)) {
+ broadcastTunnelStatus(tunnelName, {
+ connected: false,
+ status: CONNECTION_STATES.DISCONNECTED,
+ reason: 'Tunnel connection lost'
+ });
+ }
+ }
}
function setupPingInterval(tunnelName: string, tunnelConfig: TunnelConfig): void {
+ const pingKey = `${tunnelName}_ping`;
+ if (verificationTimers.has(pingKey)) {
+ clearInterval(verificationTimers.get(pingKey)!);
+ verificationTimers.delete(pingKey);
+ }
+
+ const pingInterval = setInterval(() => {
+ const currentStatus = connectionStatus.get(tunnelName);
+ if (currentStatus?.status === CONNECTION_STATES.CONNECTED) {
+ if (!activeTunnels.has(tunnelName)) {
+ broadcastTunnelStatus(tunnelName, {
+ connected: false,
+ status: CONNECTION_STATES.DISCONNECTED,
+ reason: 'Tunnel connection lost'
+ });
+ clearInterval(pingInterval);
+ verificationTimers.delete(pingKey);
+ }
+ } else {
+ clearInterval(pingInterval);
+ verificationTimers.delete(pingKey);
+ }
+ }, 120000);
+
+ verificationTimers.set(pingKey, pingInterval);
}
function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
@@ -527,6 +559,8 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
errorType === ERROR_TYPES.PORT ||
errorType === ERROR_TYPES.PERMISSION ||
manualDisconnects.has(tunnelName);
+
+
handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry);
});
@@ -590,7 +624,11 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
setTimeout(() => {
if (!manualDisconnects.has(tunnelName) && activeTunnels.has(tunnelName)) {
- verifyTunnelConnection(tunnelName, tunnelConfig, false);
+ broadcastTunnelStatus(tunnelName, {
+ connected: true,
+ status: CONNECTION_STATES.CONNECTED
+ });
+ setupPingInterval(tunnelName, tunnelConfig);
}
}, 2000);
@@ -650,7 +688,6 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
stream.stderr.on("data", (data) => {
const errorMsg = data.toString().trim();
- logger.debug(`Tunnel stderr for '${tunnelName}': ${errorMsg}`);
});
});
});
@@ -659,11 +696,11 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
- keepaliveInterval: 60000,
- keepaliveCountMax: 0,
+ keepaliveInterval: 30000,
+ keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
- tcpKeepAliveInitialDelay: 30000,
+ tcpKeepAliveInitialDelay: 15000,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',
@@ -750,11 +787,11 @@ function killRemoteTunnelByMarker(tunnelConfig: TunnelConfig, tunnelName: string
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
- keepaliveInterval: 60000,
- keepaliveCountMax: 0,
+ keepaliveInterval: 30000,
+ keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
- tcpKeepAliveInitialDelay: 30000,
+ tcpKeepAliveInitialDelay: 15000,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',
diff --git a/src/ui/apps/Server/Server.tsx b/src/ui/apps/Server/Server.tsx
index 4235eb2f..1b5c2f65 100644
--- a/src/ui/apps/Server/Server.tsx
+++ b/src/ui/apps/Server/Server.tsx
@@ -243,7 +243,7 @@ export function Server({