From 9130eb68a8937c4c4e252a1f1118c48d5341364b Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 27 Aug 2025 22:58:08 -0500 Subject: [PATCH] Improve server stats and tunnel stability --- src/backend/database/db/index.ts | 1 - src/backend/database/routes/users.ts | 7 ++- src/backend/ssh/server-stats.ts | 34 ++++++++----- src/backend/ssh/terminal.ts | 2 - src/backend/ssh/tunnel.ts | 71 +++++++++++++++++++++------- src/ui/apps/Server/Server.tsx | 4 +- 6 files changed, 84 insertions(+), 35 deletions(-) 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({ - {/* HDD */} + {/* Root Storage */}

@@ -254,7 +254,7 @@ export function Server({ const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A'; const usedText = used ?? 'N/A'; const totalText = total ?? 'N/A'; - return `HDD Space - ${pctText} (${usedText} of ${totalText})`; + return `Root Storage Space - ${pctText} (${usedText} of ${totalText})`; })()}