diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 81c16b89..5ff7c5d0 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -96,12 +96,6 @@ async function initializeDatabaseAsync(): Promise { }); try { - databaseLogger.info( - "Generating diagnostic information for database encryption failure", - { - operation: "db_encryption_diagnostic", - }, - ); const diagnosticInfo = DatabaseFileEncryption.getDiagnosticInfo(encryptedDbPath); databaseLogger.error( diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts index 521fe7af..079ffdac 100644 --- a/src/backend/database/routes/snippets.ts +++ b/src/backend/database/routes/snippets.ts @@ -279,7 +279,6 @@ router.post( } try { - // Get the snippet const snippetResult = await db .select() .from(snippets) @@ -296,11 +295,9 @@ router.post( const snippet = snippetResult[0]; - // Import SSH connection utilities const { Client } = await import("ssh2"); const { sshData, sshCredentials } = await import("../db/schema.js"); - // Get host configuration using SimpleDBOps to decrypt credentials const { SimpleDBOps } = await import("../../utils/simple-db-ops.js"); const hostResult = await SimpleDBOps.select( @@ -320,7 +317,6 @@ router.post( const host = hostResult[0]; - // Resolve credentials if needed let password = host.password; let privateKey = host.key; let passphrase = host.key_password; @@ -352,7 +348,6 @@ router.post( } } - // Create SSH connection const conn = new Client(); let output = ""; let errorOutput = ""; @@ -400,7 +395,6 @@ router.post( reject(err); }); - // Connect to SSH const config: any = { host: host.ip, port: host.port, @@ -471,7 +465,6 @@ router.post( }, }; - // Set auth based on authType (like terminal.ts does) if (authType === "password" && password) { config.password = password; } else if (authType === "key" && privateKey) { @@ -484,10 +477,8 @@ router.post( config.passphrase = passphrase; } } else if (password) { - // Fallback: if authType not set but password exists config.password = password; } else if (privateKey) { - // Fallback: if authType not set but key exists const cleanKey = (privateKey as string) .trim() .replace(/\r\n/g, "\n") diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 45336a2f..955135a4 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -361,7 +361,6 @@ router.post( }, ); - // Notify stats server to start polling this host try { const axios = (await import("axios")).default; const statsPort = process.env.STATS_PORT || 30005; @@ -603,7 +602,6 @@ router.put( }, ); - // Notify stats server to refresh polling for this host try { const axios = (await import("axios")).default; const statsPort = process.env.STATS_PORT || 30005; @@ -893,7 +891,6 @@ router.delete( const numericHostId = Number(hostId); - // Delete related records first to avoid foreign key constraint errors await db .delete(fileManagerRecent) .where( @@ -948,7 +945,6 @@ router.delete( ), ); - // Now delete the host itself await db .delete(sshData) .where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId))); @@ -966,7 +962,6 @@ router.delete( }, ); - // Notify stats server to stop polling this host try { const axios = (await import("axios")).default; const statsPort = process.env.STATS_PORT || 30005; @@ -1553,7 +1548,6 @@ router.put( DatabaseSaveTrigger.triggerSave("folder_rename"); - // Also update folder metadata if exists await db .update(sshFolders) .set({ @@ -1620,7 +1614,6 @@ router.put( } try { - // Check if folder metadata exists const existing = await db .select() .from(sshFolders) @@ -1628,7 +1621,6 @@ router.put( .limit(1); if (existing.length > 0) { - // Update existing await db .update(sshFolders) .set({ @@ -1638,7 +1630,6 @@ router.put( }) .where(and(eq(sshFolders.userId, userId), eq(sshFolders.name, name))); } else { - // Create new await db.insert(sshFolders).values({ userId, name, @@ -1677,7 +1668,6 @@ router.delete( } try { - // Get all hosts in the folder const hostsToDelete = await db .select() .from(sshData) @@ -1690,12 +1680,10 @@ router.delete( }); } - // Delete all hosts await db .delete(sshData) .where(and(eq(sshData.userId, userId), eq(sshData.folder, folderName))); - // Delete folder metadata await db .delete(sshFolders) .where( @@ -1704,14 +1692,6 @@ router.delete( DatabaseSaveTrigger.triggerSave("folder_hosts_delete"); - sshLogger.info("Deleted all hosts in folder", { - operation: "delete_folder_hosts", - userId, - folderName, - deletedCount: hostsToDelete.length, - }); - - // Notify stats server to stop polling these hosts try { const axios = (await import("axios")).default; const statsPort = process.env.STATS_PORT || 30005; diff --git a/src/backend/database/routes/terminal.ts b/src/backend/database/routes/terminal.ts index 0b327820..f2e3100b 100644 --- a/src/backend/database/routes/terminal.ts +++ b/src/backend/database/routes/terminal.ts @@ -85,8 +85,6 @@ router.get( } try { - // Get unique commands for this host, ordered by most recent - // Use DISTINCT to avoid duplicates, but keep the most recent occurrence const result = await db .selectDistinct({ command: commandHistory.command }) .from(commandHistory) @@ -97,9 +95,8 @@ router.get( ), ) .orderBy(desc(commandHistory.executedAt)) - .limit(500); // Limit to last 500 unique commands + .limit(500); - // Further deduplicate in case DISTINCT didn't work perfectly const uniqueCommands = Array.from(new Set(result.map((r) => r.command))); authLogger.info(`Fetched command history for host ${hostId}`, { @@ -142,7 +139,6 @@ router.post( try { const hostIdNum = parseInt(hostId, 10); - // Delete all instances of this command for this user and host await db .delete(commandHistory) .where( diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 02a7a2b5..a47fc087 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -763,7 +763,6 @@ router.get("/oidc/callback", async (req, res) => { .get(); isFirstUser = ((countResult as { count?: number })?.count || 0) === 0; - // Check if registration is allowed (unless this is the first user) if (!isFirstUser) { try { const regRow = db.$client @@ -822,8 +821,8 @@ router.get("/oidc/callback", async (req, res) => { try { const sessionDurationMs = deviceInfo.type === "desktop" || deviceInfo.type === "mobile" - ? 30 * 24 * 60 * 60 * 1000 // 30 days - : 7 * 24 * 60 * 60 * 1000; // 7 days + ? 30 * 24 * 60 * 60 * 1000 + : 7 * 24 * 60 * 60 * 1000; await authManager.registerOIDCUser(id, sessionDurationMs); } catch (encryptionError) { await db.delete(users).where(eq(users.id, id)); @@ -862,7 +861,6 @@ router.get("/oidc/callback", async (req, res) => { const userRecord = user[0]; - // For all OIDC logins (including dual-auth), use OIDC encryption try { await authManager.authenticateOIDCUser(userRecord.id, deviceInfo.type); } catch (setupError) { @@ -901,7 +899,6 @@ router.get("/oidc/callback", async (req, res) => { ? 30 * 24 * 60 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000; - // Clear any existing JWT cookie first to prevent conflicts res.clearCookie("jwt", authManager.getSecureCookieOptions(req)); return res @@ -941,7 +938,6 @@ router.post("/login", async (req, res) => { return res.status(400).json({ error: "Invalid username or password" }); } - // Check rate limiting const lockStatus = loginRateLimiter.isLocked(clientIp, username); if (lockStatus.locked) { authLogger.warn("Login attempt blocked due to rate limiting", { @@ -995,8 +991,6 @@ router.post("/login", async (req, res) => { const userRecord = user[0]; - // Only reject if user is OIDC-only (has no password set) - // Empty string "" is treated as no password if ( userRecord.is_oidc && (!userRecord.password_hash || userRecord.password_hash.trim() === "") @@ -1040,17 +1034,13 @@ router.post("/login", async (req, res) => { const deviceInfo = parseUserAgent(req); - // For dual-auth users (has both password and OIDC), use OIDC encryption - // For password-only users, use password-based encryption let dataUnlocked = false; if (userRecord.is_oidc) { - // Dual-auth user: verify password then use OIDC encryption dataUnlocked = await authManager.authenticateOIDCUser( userRecord.id, deviceInfo.type, ); } else { - // Password-only user: use password-based encryption dataUnlocked = await authManager.authenticateUser( userRecord.id, password, @@ -1079,7 +1069,6 @@ router.post("/login", async (req, res) => { deviceInfo: deviceInfo.deviceInfo, }); - // Reset rate limiter on successful login loginRateLimiter.resetAttempts(clientIp, username); authLogger.success(`User logged in successfully: ${username}`, { @@ -2255,7 +2244,6 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => { const targetUserId = targetUser[0].id; try { - // Delete all user-related data to avoid foreign key constraints await db .delete(sshCredentialUsage) .where(eq(sshCredentialUsage.userId, targetUserId)); @@ -2588,7 +2576,6 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { } try { - // Verify admin permissions const adminUser = await db .select() .from(users) @@ -2597,7 +2584,6 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { return res.status(403).json({ error: "Admin access required" }); } - // Get OIDC user const oidcUserRecords = await db .select() .from(users) @@ -2608,14 +2594,12 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { const oidcUser = oidcUserRecords[0]; - // Verify user is OIDC if (!oidcUser.is_oidc) { return res.status(400).json({ error: "Source user is not an OIDC user", }); } - // Get target password user const targetUserRecords = await db .select() .from(users) @@ -2626,14 +2610,12 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { const targetUser = targetUserRecords[0]; - // Verify target user has password authentication if (targetUser.is_oidc || !targetUser.password_hash) { return res.status(400).json({ error: "Target user must be a password-based account", }); } - // Check if target user already has OIDC configured if (targetUser.client_id && targetUser.oidc_identifier) { return res.status(400).json({ error: "Target user already has OIDC authentication configured", @@ -2649,11 +2631,10 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { adminUserId, }); - // Copy OIDC configuration from OIDC user to target password user await db .update(users) .set({ - is_oidc: true, // Enable OIDC login for this account + is_oidc: true, oidc_identifier: oidcUser.oidc_identifier, client_id: oidcUser.client_id, client_secret: oidcUser.client_secret, @@ -2666,14 +2647,8 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { }) .where(eq(users.id, targetUser.id)); - // Re-encrypt the user's DEK with OIDC system key for dual-auth support - // This allows OIDC login to decrypt user data without requiring password try { await authManager.convertToOIDCEncryption(targetUser.id); - authLogger.info("Converted user encryption to OIDC for dual-auth", { - operation: "link_convert_encryption", - userId: targetUser.id, - }); } catch (encryptionError) { authLogger.error( "Failed to convert encryption to OIDC during linking", @@ -2683,7 +2658,6 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { userId: targetUser.id, }, ); - // Rollback the OIDC configuration await db .update(users) .set({ @@ -2710,19 +2684,15 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { }); } - // Revoke all sessions for the OIDC user before deletion await authManager.revokeAllUserSessions(oidcUserId); authManager.logoutUser(oidcUserId); - // Delete OIDC user's recent activity first (to avoid NOT NULL constraint issues) await db .delete(recentActivity) .where(eq(recentActivity.userId, oidcUserId)); - // Delete the OIDC user (CASCADE will delete related data: hosts, credentials, sessions, etc.) await db.delete(users).where(eq(users.id, oidcUserId)); - // Clean up OIDC user's settings db.$client .prepare("DELETE FROM settings WHERE key LIKE ?") .run(`user_%_${oidcUserId}`); diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index dbbdf47f..b6344b68 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -261,7 +261,7 @@ interface SSHSession { isConnected: boolean; lastActive: number; timeout?: NodeJS.Timeout; - activeOperations: number; // Track number of active operations to prevent cleanup mid-operation + activeOperations: number; } interface PendingTOTPSession { @@ -286,7 +286,6 @@ const pendingTOTPSessions: Record = {}; function cleanupSession(sessionId: string) { const session = sshSessions[sessionId]; if (session) { - // Don't cleanup if there are active operations if (session.activeOperations > 0) { fileLogger.warn( `Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`, @@ -296,7 +295,6 @@ function cleanupSession(sessionId: string) { activeOperations: session.activeOperations, }, ); - // Reschedule cleanup scheduleSessionCleanup(sessionId); return; } @@ -577,7 +575,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { client, isConnected: true, lastActive: Date.now(), - activeOperations: 0, // Initialize active operations counter + activeOperations: 0, }; scheduleSessionCleanup(sessionId); res.json({ status: "success", message: "SSH connection established" }); @@ -932,7 +930,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => { client: session.client, isConnected: true, lastActive: Date.now(), - activeOperations: 0, // Initialize active operations counter + activeOperations: 0, }; scheduleSessionCleanup(sessionId); @@ -1076,12 +1074,12 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { } sshConn.lastActive = Date.now(); - sshConn.activeOperations++; // Track operation start + sshConn.activeOperations++; const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); sshConn.client.exec(`command ls -la '${escapedPath}'`, (err, stream) => { if (err) { - sshConn.activeOperations--; // Decrement on error + sshConn.activeOperations--; fileLogger.error("SSH listFiles error:", err); return res.status(500).json({ error: err.message }); } @@ -1098,7 +1096,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { }); stream.on("close", (code) => { - sshConn.activeOperations--; // Decrement when operation completes + sshConn.activeOperations--; if (code !== 0) { fileLogger.error( `SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, @@ -2892,7 +2890,6 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { const fileName = archivePath.split("/").pop() || ""; const fileExt = fileName.toLowerCase(); - // Determine extraction command based on file extension let extractCommand = ""; const targetPath = extractPath || archivePath.substring(0, archivePath.lastIndexOf("/")); @@ -2970,13 +2967,11 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { error: errorOutput, }); - // Check if command not found let friendlyError = errorOutput || "Failed to extract archive"; if ( errorOutput.includes("command not found") || errorOutput.includes("not found") ) { - // Detect which command is missing based on file extension let missingCmd = ""; let installHint = ""; @@ -3076,15 +3071,12 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); - // Determine compression format - const compressionFormat = format || "zip"; // Default to zip + const compressionFormat = format || "zip"; let compressCommand = ""; - // Get the directory where the first file is located const firstPath = paths[0]; const workingDir = firstPath.substring(0, firstPath.lastIndexOf("/")) || "/"; - // Extract just the file/folder names for the command const fileNames = paths .map((p) => { const name = p.split("/").pop(); @@ -3092,7 +3084,6 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { }) .join(" "); - // Construct archive path let archivePath = ""; if (archiveName.includes("/")) { archivePath = archiveName; @@ -3103,7 +3094,6 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { } if (compressionFormat === "zip") { - // Use zip command - need to cd to directory first compressCommand = `cd "${workingDir}" && zip -r "${archivePath}" ${fileNames}`; } else if (compressionFormat === "tar.gz" || compressionFormat === "tgz") { compressCommand = `cd "${workingDir}" && tar -czf "${archivePath}" ${fileNames}`; @@ -3170,7 +3160,6 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { error: errorOutput, }); - // Check if command not found let friendlyError = errorOutput || "Failed to compress files"; if ( errorOutput.includes("command not found") || diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index f1920c71..b4c958c9 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -652,15 +652,12 @@ class PollingManager { 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 { let temp: any = JSON.parse(statsConfigStr); - // Check if we got a string back (double-encoded JSON) if (typeof temp === "string") { temp = JSON.parse(temp); } @@ -678,7 +675,6 @@ class PollingManager { } } - // Merge with defaults, but prioritize the provided values const result = { ...DEFAULT_STATS_CONFIG, ...parsed }; return result; @@ -689,7 +685,6 @@ class PollingManager { const existingConfig = this.pollingConfigs.get(host.id); - // Always clear existing timers first if (existingConfig) { if (existingConfig.statusTimer) { clearInterval(existingConfig.statusTimer); @@ -701,7 +696,6 @@ class PollingManager { } } - // If both checks are disabled, stop all polling and clean up if (!statsConfig.statusCheckEnabled && !statsConfig.metricsEnabled) { this.pollingConfigs.delete(host.id); this.statusStore.delete(host.id); @@ -720,7 +714,6 @@ class PollingManager { this.pollHostStatus(host); config.statusTimer = setInterval(() => { - // 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); @@ -733,11 +726,9 @@ class PollingManager { if (statsConfig.metricsEnabled) { const intervalMs = statsConfig.metricsInterval * 1000; - // Poll immediately, but only if metrics are actually enabled this.pollHostMetrics(host); config.metricsTimer = setInterval(() => { - // 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); @@ -747,7 +738,6 @@ class PollingManager { this.metricsStore.delete(host.id); } - // Set the config with the new timers this.pollingConfigs.set(host.id, config); } @@ -769,14 +759,11 @@ class PollingManager { } private async pollHostMetrics(host: SSHHostWithCredentials): Promise { - // Double-check that metrics are still enabled before collecting - // Always get the LATEST config from the Map, not from the closure const config = this.pollingConfigs.get(host.id); if (!config || !config.statsConfig.metricsEnabled) { return; } - // Use the host from the config to ensure we have the latest version const currentHost = config.host; try { @@ -789,7 +776,6 @@ class PollingManager { const errorMessage = error instanceof Error ? error.message : String(error); - // Re-check config after the async operation in case it was disabled during collection const latestConfig = this.pollingConfigs.get(currentHost.id); if (latestConfig && latestConfig.statsConfig.metricsEnabled) { statsLogger.warn("Failed to collect metrics for host", { diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index f724dd7d..78c181e7 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -247,6 +247,7 @@ const wss = new WebSocketServer({ } const existingConnections = userConnections.get(payload.userId); + if (existingConnections && existingConnections.size >= 3) { return false; } @@ -323,7 +324,7 @@ wss.on("connection", async (ws: WebSocket, req) => { let isConnecting = false; let isConnected = false; let isCleaningUp = false; - let isShellInitializing = false; // Track shell initialization to prevent cleanup mid-setup + let isShellInitializing = false; ws.on("close", () => { const userWs = userConnections.get(userId); @@ -681,10 +682,8 @@ wss.on("connection", async (ws: WebSocket, req) => { sshConn.on("ready", () => { clearTimeout(connectionTimeout); - // Capture the connection reference immediately and verify it's still valid const conn = sshConn; - // Additional check: verify the connection is still active before proceeding if (!conn || isCleaningUp || !sshConn) { sshLogger.warn( "SSH connection was cleaned up before shell could be created", @@ -709,12 +708,10 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } - // Mark that we're initializing the shell to prevent cleanup isShellInitializing = true; isConnecting = false; isConnected = true; - // Verify connection is still valid right before shell() call if (!sshConn) { sshLogger.error( "SSH connection became null right before shell creation", @@ -740,7 +737,6 @@ wss.on("connection", async (ws: WebSocket, req) => { term: "xterm-256color", } as PseudoTtyOptions, (err, stream) => { - // Shell initialization complete, clear the flag isShellInitializing = false; if (err) { @@ -1265,7 +1261,6 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } - // Don't cleanup if we're in the middle of shell initialization if (isShellInitializing) { sshLogger.warn( "Cleanup attempted during shell initialization, deferring", @@ -1274,7 +1269,6 @@ wss.on("connection", async (ws: WebSocket, req) => { userId, }, ); - // Retry cleanup after a short delay setTimeout(() => cleanupSSH(timeoutId), 100); return; } @@ -1327,11 +1321,9 @@ wss.on("connection", async (ws: WebSocket, req) => { } function setupPingInterval() { - // More frequent keepalive to prevent idle disconnections pingInterval = setInterval(() => { if (sshConn && sshStream) { try { - // Send null byte as keepalive sshStream.write("\x00"); } catch (e: unknown) { sshLogger.error( @@ -1341,12 +1333,11 @@ wss.on("connection", async (ws: WebSocket, req) => { cleanupSSH(); } } else if (!sshConn || !sshStream) { - // If connection or stream is lost, clear the interval if (pingInterval) { clearInterval(pingInterval); pingInterval = null; } } - }, 30000); // Reduced from 60s to 30s for more reliable keepalive + }, 30000); } }); diff --git a/src/backend/ssh/widgets/common-utils.ts b/src/backend/ssh/widgets/common-utils.ts index 802c8571..bf8bf6aa 100644 --- a/src/backend/ssh/widgets/common-utils.ts +++ b/src/backend/ssh/widgets/common-utils.ts @@ -29,7 +29,10 @@ export function execCommand( }); } -export function toFixedNum(n: number | null | undefined, digits = 2): number | null { +export function toFixedNum( + n: number | null | undefined, + digits = 2, +): number | null { if (typeof n !== "number" || !Number.isFinite(n)) return null; return Number(n.toFixed(digits)); } diff --git a/src/backend/ssh/widgets/login-stats-collector.ts b/src/backend/ssh/widgets/login-stats-collector.ts index 5147b146..b34f3d80 100644 --- a/src/backend/ssh/widgets/login-stats-collector.ts +++ b/src/backend/ssh/widgets/login-stats-collector.ts @@ -36,9 +36,12 @@ export async function collectLoginStats(client: Client): Promise { if (parts.length >= 10) { const user = parts[0]; const tty = parts[1]; - const ip = parts[2] === ":" || parts[2].startsWith(":") ? "local" : parts[2]; + const ip = + parts[2] === ":" || parts[2].startsWith(":") ? "local" : parts[2]; - const timeStart = parts.indexOf(parts.find(p => /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/.test(p)) || ""); + const timeStart = parts.indexOf( + parts.find((p) => /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/.test(p)) || "", + ); if (timeStart > 0 && parts.length > timeStart + 4) { const timeStr = parts.slice(timeStart, timeStart + 5).join(" "); @@ -96,7 +99,9 @@ export async function collectLoginStats(client: Client): Promise { failedLogins.push({ user, ip, - time: timeStr ? new Date(timeStr).toISOString() : new Date().toISOString(), + time: timeStr + ? new Date(timeStr).toISOString() + : new Date().toISOString(), status: "failed", }); if (ip !== "unknown") { diff --git a/src/backend/ssh/widgets/processes-collector.ts b/src/backend/ssh/widgets/processes-collector.ts index 02f3ea11..1ee0f51c 100644 --- a/src/backend/ssh/widgets/processes-collector.ts +++ b/src/backend/ssh/widgets/processes-collector.ts @@ -24,10 +24,7 @@ export async function collectProcessesMetrics(client: Client): Promise<{ }> = []; try { - const psOut = await execCommand( - client, - "ps aux --sort=-%cpu | head -n 11", - ); + const psOut = await execCommand(client, "ps aux --sort=-%cpu | head -n 11"); const psLines = psOut.stdout .split("\n") .map((l) => l.trim()) @@ -48,10 +45,7 @@ export async function collectProcessesMetrics(client: Client): Promise<{ } const procCount = await execCommand(client, "ps aux | wc -l"); - const runningCount = await execCommand( - client, - "ps aux | grep -c ' R '", - ); + const runningCount = await execCommand(client, "ps aux | grep -c ' R '"); totalProcesses = Number(procCount.stdout.trim()) - 1; runningProcesses = Number(runningCount.stdout.trim()); } catch (e) { diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 1b0dc2e6..fd706176 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -98,8 +98,8 @@ class AuthManager { ): Promise { const sessionDurationMs = deviceType === "desktop" || deviceType === "mobile" - ? 30 * 24 * 60 * 60 * 1000 // 30 days - : 7 * 24 * 60 * 60 * 1000; // 7 days + ? 30 * 24 * 60 * 60 * 1000 + : 7 * 24 * 60 * 60 * 1000; const authenticated = await this.userCrypto.authenticateOIDCUser( userId, @@ -120,8 +120,8 @@ class AuthManager { ): Promise { const sessionDurationMs = deviceType === "desktop" || deviceType === "mobile" - ? 30 * 24 * 60 * 60 * 1000 // 30 days - : 7 * 24 * 60 * 60 * 1000; // 7 days + ? 30 * 24 * 60 * 60 * 1000 + : 7 * 24 * 60 * 60 * 1000; const authenticated = await this.userCrypto.authenticateUser( userId, @@ -136,6 +136,10 @@ class AuthManager { return authenticated; } + async convertToOIDCEncryption(userId: string): Promise { + await this.userCrypto.convertToOIDCEncryption(userId); + } + private async performLazyEncryptionMigration(userId: string): Promise { try { const userDataKey = this.getUserDataKey(userId); diff --git a/src/backend/utils/login-rate-limiter.ts b/src/backend/utils/login-rate-limiter.ts index 4e5ed704..105995f3 100644 --- a/src/backend/utils/login-rate-limiter.ts +++ b/src/backend/utils/login-rate-limiter.ts @@ -9,31 +9,34 @@ class LoginRateLimiter { private usernameAttempts = new Map(); private readonly MAX_ATTEMPTS = 5; - private readonly WINDOW_MS = 15 * 60 * 1000; // 15 minutes - private readonly LOCKOUT_MS = 15 * 60 * 1000; // 15 minutes + private readonly WINDOW_MS = 10 * 60 * 1000; + private readonly LOCKOUT_MS = 10 * 60 * 1000; - // Clean up old entries periodically constructor() { - setInterval(() => this.cleanup(), 5 * 60 * 1000); // Clean every 5 minutes + setInterval(() => this.cleanup(), 5 * 60 * 1000); } private cleanup(): void { const now = Date.now(); - // Clean IP attempts for (const [ip, attempt] of this.ipAttempts.entries()) { if (attempt.lockedUntil && attempt.lockedUntil < now) { this.ipAttempts.delete(ip); - } else if (!attempt.lockedUntil && (now - attempt.firstAttempt) > this.WINDOW_MS) { + } else if ( + !attempt.lockedUntil && + now - attempt.firstAttempt > this.WINDOW_MS + ) { this.ipAttempts.delete(ip); } } - // Clean username attempts for (const [username, attempt] of this.usernameAttempts.entries()) { if (attempt.lockedUntil && attempt.lockedUntil < now) { this.usernameAttempts.delete(username); - } else if (!attempt.lockedUntil && (now - attempt.firstAttempt) > this.WINDOW_MS) { + } else if ( + !attempt.lockedUntil && + now - attempt.firstAttempt > this.WINDOW_MS + ) { this.usernameAttempts.delete(username); } } @@ -42,15 +45,13 @@ class LoginRateLimiter { recordFailedAttempt(ip: string, username?: string): void { const now = Date.now(); - // Record IP attempt const ipAttempt = this.ipAttempts.get(ip); if (!ipAttempt) { this.ipAttempts.set(ip, { count: 1, firstAttempt: now, }); - } else if ((now - ipAttempt.firstAttempt) > this.WINDOW_MS) { - // Reset if outside window + } else if (now - ipAttempt.firstAttempt > this.WINDOW_MS) { this.ipAttempts.set(ip, { count: 1, firstAttempt: now, @@ -62,7 +63,6 @@ class LoginRateLimiter { } } - // Record username attempt if provided if (username) { const userAttempt = this.usernameAttempts.get(username); if (!userAttempt) { @@ -70,8 +70,7 @@ class LoginRateLimiter { count: 1, firstAttempt: now, }); - } else if ((now - userAttempt.firstAttempt) > this.WINDOW_MS) { - // Reset if outside window + } else if (now - userAttempt.firstAttempt > this.WINDOW_MS) { this.usernameAttempts.set(username, { count: 1, firstAttempt: now, @@ -92,10 +91,12 @@ class LoginRateLimiter { } } - isLocked(ip: string, username?: string): { locked: boolean; remainingTime?: number } { + isLocked( + ip: string, + username?: string, + ): { locked: boolean; remainingTime?: number } { const now = Date.now(); - // Check IP lockout const ipAttempt = this.ipAttempts.get(ip); if (ipAttempt?.lockedUntil && ipAttempt.lockedUntil > now) { return { @@ -104,7 +105,6 @@ class LoginRateLimiter { }; } - // Check username lockout if (username) { const userAttempt = this.usernameAttempts.get(username); if (userAttempt?.lockedUntil && userAttempt.lockedUntil > now) { @@ -122,18 +122,19 @@ class LoginRateLimiter { const now = Date.now(); let minRemaining = this.MAX_ATTEMPTS; - // Check IP attempts const ipAttempt = this.ipAttempts.get(ip); - if (ipAttempt && (now - ipAttempt.firstAttempt) <= this.WINDOW_MS) { + if (ipAttempt && now - ipAttempt.firstAttempt <= this.WINDOW_MS) { const ipRemaining = Math.max(0, this.MAX_ATTEMPTS - ipAttempt.count); minRemaining = Math.min(minRemaining, ipRemaining); } - // Check username attempts if (username) { const userAttempt = this.usernameAttempts.get(username); - if (userAttempt && (now - userAttempt.firstAttempt) <= this.WINDOW_MS) { - const userRemaining = Math.max(0, this.MAX_ATTEMPTS - userAttempt.count); + if (userAttempt && now - userAttempt.firstAttempt <= this.WINDOW_MS) { + const userRemaining = Math.max( + 0, + this.MAX_ATTEMPTS - userAttempt.count, + ); minRemaining = Math.min(minRemaining, userRemaining); } } @@ -142,5 +143,4 @@ class LoginRateLimiter { } } -// Export singleton instance export const loginRateLimiter = new LoginRateLimiter(); diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index 46248d65..e571e5db 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -344,7 +344,6 @@ class UserCrypto { const existingEncryptedDEK = await this.getEncryptedDEK(userId); const existingKEKSalt = await this.getKEKSalt(userId); - // If no existing encryption, nothing to convert if (!existingEncryptedDEK && !existingKEKSalt) { databaseLogger.info("No existing encryption to convert for user", { operation: "convert_to_oidc_encryption_skip", @@ -353,11 +352,9 @@ class UserCrypto { return; } - // Try to get the DEK from active session const existingDEK = this.getUserDataKey(userId); if (existingDEK) { - // User has active session - preserve their data by re-encrypting DEK const systemKey = this.deriveOIDCSystemKey(userId); const oidcEncryptedDEK = this.encryptDEK(existingDEK, systemKey); systemKey.fill(0); @@ -372,8 +369,6 @@ class UserCrypto { }, ); } else { - // No active session - delete old encryption keys - // OIDC will create new keys on next login, but old encrypted data will be inaccessible if (existingEncryptedDEK) { await getDb() .delete(settings) @@ -389,7 +384,6 @@ class UserCrypto { ); } - // Always remove password-based KEK salt if (existingKEKSalt) { await getDb() .delete(settings) diff --git a/src/components/ui/kbd.tsx b/src/components/ui/kbd.tsx index 253c69f3..44fefd02 100644 --- a/src/components/ui/kbd.tsx +++ b/src/components/ui/kbd.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { return ( @@ -8,11 +8,11 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none", "[&_svg:not([class*='size-'])]:size-3", "[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10", - className + className, )} {...props} /> - ) + ); } function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { @@ -22,7 +22,7 @@ function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { className={cn("inline-flex items-center gap-1", className)} {...props} /> - ) + ); } -export { Kbd, KbdGroup } +export { Kbd, KbdGroup }; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 12b4fc5c..8e30362e 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -60,7 +60,7 @@ function TabsContent({ "data-[state=active]:animate-in data-[state=inactive]:animate-out", "data-[state=inactive]:fade-out-0 data-[state=active]:fade-in-0", "duration-150", - className + className, )} {...props} /> diff --git a/src/constants/terminal-themes.ts b/src/constants/terminal-themes.ts index 0786a952..63f55055 100644 --- a/src/constants/terminal-themes.ts +++ b/src/constants/terminal-themes.ts @@ -617,7 +617,6 @@ export const TERMINAL_THEMES: Record = { }, }; -// Font families available for terminal export const TERMINAL_FONTS = [ { value: "Caskaydia Cove Nerd Font Mono", diff --git a/src/types/index.ts b/src/types/index.ts index 9bd159ed..c20ffa05 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -329,12 +329,11 @@ export interface TabContextTab { initialTab?: string; } -// Split Screen Layout Types export type SplitLayout = "2h" | "2v" | "3l" | "3r" | "3t" | "4grid"; export interface SplitConfiguration { layout: SplitLayout; - positions: Map; // position index -> tab ID + positions: Map; } export interface SplitLayoutOption { diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 4e7e3926..2fabfaae 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -44,7 +44,7 @@ function AppContent() { const now = Date.now(); if (now - lastShiftPressTime.current < 300) { setIsCommandPaletteOpen((isOpen) => !isOpen); - lastShiftPressTime.current = 0; // Reset on double press + lastShiftPressTime.current = 0; } else { lastShiftPressTime.current = now; } @@ -63,14 +63,12 @@ function AppContent() { useEffect(() => { const checkAuth = () => { setAuthLoading(true); - // Don't optimistically set isAuthenticated before checking getUserInfo() .then((meRes) => { if (typeof meRes === "string" || !meRes.username) { setIsAuthenticated(false); setIsAdmin(false); setUsername(null); - // Clear invalid token localStorage.removeItem("jwt"); } else { setIsAuthenticated(true); @@ -83,7 +81,6 @@ function AppContent() { setIsAdmin(false); setUsername(null); - // Clear invalid token on any auth error localStorage.removeItem("jwt"); const errorCode = err?.response?.data?.code; diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx index bccfa730..0de0dd8a 100644 --- a/src/ui/desktop/admin/AdminSettings.tsx +++ b/src/ui/desktop/admin/AdminSettings.tsx @@ -1533,7 +1533,6 @@ export function AdminSettings({ - {/* Link OIDC to Password Account Dialog */} {linkAccountAlertOpen && ( (null); - // Clear error when tab changes useEffect(() => { setFormError(null); }, [activeTab]); diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/desktop/apps/dashboard/Dashboard.tsx index e1e63d00..836b95c0 100644 --- a/src/ui/desktop/apps/dashboard/Dashboard.tsx +++ b/src/ui/desktop/apps/dashboard/Dashboard.tsx @@ -102,7 +102,7 @@ export function Dashboard({ const topMarginPx = isTopbarOpen ? 74 : 26; const leftMarginPx = sidebarState === "collapsed" ? 26 : 8; - const rightMarginPx = 17; // Base margin when closed + const rightMarginPx = 17; const bottomMarginPx = 8; useEffect(() => { @@ -213,7 +213,6 @@ export function Dashboard({ statsConfig?: string | { metricsEnabled?: boolean }; }) => { try { - // Parse statsConfig if it's a string let statsConfig: { metricsEnabled?: boolean } = { metricsEnabled: true, }; @@ -225,7 +224,6 @@ export function Dashboard({ } } - // Skip if metrics are disabled if (statsConfig.metricsEnabled === false) { return null; } diff --git a/src/ui/desktop/apps/file-manager/FileManager.tsx b/src/ui/desktop/apps/file-manager/FileManager.tsx index 80e76b39..0d33a0ba 100644 --- a/src/ui/desktop/apps/file-manager/FileManager.tsx +++ b/src/ui/desktop/apps/file-manager/FileManager.tsx @@ -1087,7 +1087,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { t("fileManager.archiveExtractedSuccessfully", { name: file.name }), ); - // Refresh directory to show extracted files handleRefreshDirectory(); } catch (error: unknown) { const err = error as { message?: string }; @@ -1132,7 +1131,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { }), ); - // Refresh directory to show compressed file handleRefreshDirectory(); clearSelection(); } catch (error: unknown) { diff --git a/src/ui/desktop/apps/file-manager/FileManagerContextMenu.tsx b/src/ui/desktop/apps/file-manager/FileManagerContextMenu.tsx index 55e190ee..bcd88386 100644 --- a/src/ui/desktop/apps/file-manager/FileManagerContextMenu.tsx +++ b/src/ui/desktop/apps/file-manager/FileManagerContextMenu.tsx @@ -261,7 +261,6 @@ export function FileManagerContextMenu({ }); } - // Add extract option for archive files if (isSingleFile && files[0].type === "file" && onExtractArchive) { const fileName = files[0].name.toLowerCase(); const isArchive = @@ -288,7 +287,6 @@ export function FileManagerContextMenu({ } } - // Add compress option for selected files/folders if (isFileContext && onCompress) { menuItems.push({ icon: , diff --git a/src/ui/desktop/apps/file-manager/components/CompressDialog.tsx b/src/ui/desktop/apps/file-manager/components/CompressDialog.tsx index 73726baf..a7f8d20e 100644 --- a/src/ui/desktop/apps/file-manager/components/CompressDialog.tsx +++ b/src/ui/desktop/apps/file-manager/components/CompressDialog.tsx @@ -38,7 +38,6 @@ export function CompressDialog({ useEffect(() => { if (open && fileNames.length > 0) { - // Generate default archive name if (fileNames.length === 1) { const baseName = fileNames[0].replace(/\.[^/.]+$/, ""); setArchiveName(baseName); @@ -51,7 +50,6 @@ export function CompressDialog({ const handleCompress = () => { if (!archiveName.trim()) return; - // Append extension if not already present let finalName = archiveName.trim(); const extensions: Record = { zip: ".zip", diff --git a/src/ui/desktop/apps/file-manager/components/PermissionsDialog.tsx b/src/ui/desktop/apps/file-manager/components/PermissionsDialog.tsx index 501575c2..701c5429 100644 --- a/src/ui/desktop/apps/file-manager/components/PermissionsDialog.tsx +++ b/src/ui/desktop/apps/file-manager/components/PermissionsDialog.tsx @@ -30,7 +30,6 @@ interface PermissionsDialogProps { onSave: (file: FileItem, permissions: string) => Promise; } -// Parse permissions like "rwxr-xr-x" or "755" to individual bits const parsePermissions = ( perms: string, ): { owner: number; group: number; other: number } => { @@ -38,7 +37,6 @@ const parsePermissions = ( return { owner: 0, group: 0, other: 0 }; } - // If numeric format like "755" if (/^\d{3,4}$/.test(perms)) { const numStr = perms.slice(-3); return { @@ -47,8 +45,6 @@ const parsePermissions = ( other: parseInt(numStr[2] || "0", 10), }; } - - // If symbolic format like "rwxr-xr-x" or "-rwxr-xr-x" const cleanPerms = perms.replace(/^-/, "").substring(0, 9); const calcBits = (str: string): number => { @@ -66,7 +62,6 @@ const parsePermissions = ( }; }; -// Convert individual bits to numeric format const toNumeric = (owner: number, group: number, other: number): string => { return `${owner}${group}${other}`; }; @@ -99,7 +94,6 @@ export function PermissionsDialog({ (initialPerms.other & 1) !== 0, ); - // Reset when file changes useEffect(() => { if (file) { const perms = parsePermissions(file.permissions || "644"); diff --git a/src/ui/desktop/apps/host-manager/HostManager.tsx b/src/ui/desktop/apps/host-manager/HostManager.tsx index 72b55a61..284ad37f 100644 --- a/src/ui/desktop/apps/host-manager/HostManager.tsx +++ b/src/ui/desktop/apps/host-manager/HostManager.tsx @@ -72,7 +72,6 @@ export function HostManager({ }; const handleTabChange = (value: string) => { - // Only clear editing state when leaving the respective tabs, not when entering them if (activeTab === "add_host" && value !== "add_host") { setEditingHost(null); } diff --git a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx index 81b2c77c..8f2e707a 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx @@ -348,7 +348,6 @@ export function HostManagerEditor({ const [activeTab, setActiveTab] = useState("general"); const [formError, setFormError] = useState(null); - // Clear error when tab changes useEffect(() => { setFormError(null); }, [activeTab]); @@ -948,7 +947,6 @@ export function HostManagerEditor({ window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - // Notify the stats server to start/update polling for this specific host if (savedHost?.id) { const { notifyHostCreatedOrUpdated } = await import( "@/ui/main-axios.ts" @@ -963,11 +961,9 @@ export function HostManagerEditor({ } }; - // Handle form validation errors const handleFormError = () => { const errors = form.formState.errors; - // Determine which tab contains the error if ( errors.ip || errors.port || @@ -1088,7 +1084,6 @@ export function HostManagerEditor({ let filtered = sshConfigurations; - // Filter out the current host being edited (by ID, not by name) if (currentHostId) { const currentHostName = hosts.find((h) => h.id === currentHostId)?.name; if (currentHostName) { @@ -1097,7 +1092,6 @@ export function HostManagerEditor({ ); } } else { - // If creating a new host, filter by the name being entered const currentHostName = form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`; filtered = sshConfigurations.filter( diff --git a/src/ui/desktop/apps/server/Server.tsx b/src/ui/desktop/apps/server/Server.tsx index 4b31a555..8001e4d9 100644 --- a/src/ui/desktop/apps/server/Server.tsx +++ b/src/ui/desktop/apps/server/Server.tsx @@ -114,7 +114,6 @@ export function Server({ React.useEffect(() => { if (hostConfig?.id !== currentHostConfig?.id) { - // Reset state when switching to a different host setServerStatus("offline"); setMetrics(null); setMetricsHistory([]); diff --git a/src/ui/desktop/apps/server/widgets/LoginStatsWidget.tsx b/src/ui/desktop/apps/server/widgets/LoginStatsWidget.tsx index ecea0082..f70e8727 100644 --- a/src/ui/desktop/apps/server/widgets/LoginStatsWidget.tsx +++ b/src/ui/desktop/apps/server/widgets/LoginStatsWidget.tsx @@ -25,9 +25,7 @@ interface LoginStatsWidgetProps { metricsHistory: ServerMetrics[]; } -export function LoginStatsWidget({ - metrics, -}: LoginStatsWidgetProps) { +export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) { const { t } = useTranslation(); const loginStats = metrics?.login_stats; @@ -52,7 +50,9 @@ export function LoginStatsWidget({ {t("serverStats.totalLogins")} -
{totalLogins}
+
+ {totalLogins} +
@@ -86,7 +86,9 @@ export function LoginStatsWidget({ {login.user} - {t("serverStats.from")} + + {t("serverStats.from")} + {login.ip} @@ -118,7 +120,9 @@ export function LoginStatsWidget({ {login.user} - {t("serverStats.from")} + + {t("serverStats.from")} + {login.ip} diff --git a/src/ui/desktop/apps/terminal/Terminal.tsx b/src/ui/desktop/apps/terminal/Terminal.tsx index a51bd2ad..0894319b 100644 --- a/src/ui/desktop/apps/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/terminal/Terminal.tsx @@ -131,13 +131,11 @@ export const Terminal = forwardRef( const activityLoggedRef = useRef(false); const keyHandlerAttachedRef = useRef(false); - // Command history tracking (Stage 1) const { trackInput, getCurrentCommand, updateCurrentCommand } = useCommandTracker({ hostId: hostConfig.id, enabled: true, onCommandExecuted: (command) => { - // Add to autocomplete history (Stage 3) if (!autocompleteHistory.current.includes(command)) { autocompleteHistory.current = [ command, @@ -147,7 +145,6 @@ export const Terminal = forwardRef( }, }); - // Create refs for callbacks to avoid triggering useEffect re-runs const getCurrentCommandRef = useRef(getCurrentCommand); const updateCurrentCommandRef = useRef(updateCurrentCommand); @@ -156,7 +153,6 @@ export const Terminal = forwardRef( updateCurrentCommandRef.current = updateCurrentCommand; }, [getCurrentCommand, updateCurrentCommand]); - // Real-time autocomplete (Stage 3) const [showAutocomplete, setShowAutocomplete] = useState(false); const [autocompleteSuggestions, setAutocompleteSuggestions] = useState< string[] @@ -170,23 +166,19 @@ export const Terminal = forwardRef( const autocompleteHistory = useRef([]); const currentAutocompleteCommand = useRef(""); - // Refs for accessing current state in event handlers const showAutocompleteRef = useRef(false); const autocompleteSuggestionsRef = useRef([]); const autocompleteSelectedIndexRef = useRef(0); - // Command history dialog (Stage 2) const [showHistoryDialog, setShowHistoryDialog] = useState(false); const [commandHistory, setCommandHistory] = useState([]); const [isLoadingHistory, setIsLoadingHistory] = useState(false); - // Create refs for context methods to avoid infinite loops const setIsLoadingRef = useRef(commandHistoryContext.setIsLoading); const setCommandHistoryContextRef = useRef( commandHistoryContext.setCommandHistory, ); - // Keep refs updated with latest context methods useEffect(() => { setIsLoadingRef.current = commandHistoryContext.setIsLoading; setCommandHistoryContextRef.current = @@ -196,7 +188,6 @@ export const Terminal = forwardRef( commandHistoryContext.setCommandHistory, ]); - // Load command history when dialog opens useEffect(() => { if (showHistoryDialog && hostConfig.id) { setIsLoadingHistory(true); @@ -219,9 +210,7 @@ export const Terminal = forwardRef( } }, [showHistoryDialog, hostConfig.id]); - // Load command history for autocomplete on mount (Stage 3) useEffect(() => { - // Check if command autocomplete is enabled const autocompleteEnabled = localStorage.getItem("commandAutocomplete") !== "false"; @@ -240,7 +229,6 @@ export const Terminal = forwardRef( } }, [hostConfig.id]); - // Sync autocomplete state to refs for event handlers useEffect(() => { showAutocompleteRef.current = showAutocomplete; }, [showAutocomplete]); @@ -642,9 +630,7 @@ export const Terminal = forwardRef( }), ); terminal.onData((data) => { - // Track command input for history (Stage 1) trackInput(data); - // Send input to server ws.send(JSON.stringify({ type: "input", data })); }); @@ -904,20 +890,16 @@ export const Terminal = forwardRef( return ""; } - // Handle command selection from history dialog (Stage 2) const handleSelectCommand = useCallback( (command: string) => { if (!terminal || !webSocketRef.current) return; - // Send the command to the terminal - // Simulate typing the command character by character for (const char of command) { webSocketRef.current.send( JSON.stringify({ type: "input", data: char }), ); } - // Return focus to terminal after selecting command setTimeout(() => { terminal.focus(); }, 100); @@ -925,12 +907,10 @@ export const Terminal = forwardRef( [terminal], ); - // Register handlers with context useEffect(() => { commandHistoryContext.setOnSelectCommand(handleSelectCommand); }, [handleSelectCommand]); - // Handle autocomplete selection (mouse click) const handleAutocompleteSelect = useCallback( (selectedCommand: string) => { if (!webSocketRef.current) return; @@ -938,22 +918,18 @@ export const Terminal = forwardRef( const currentCmd = currentAutocompleteCommand.current; const completion = selectedCommand.substring(currentCmd.length); - // Send completion characters to server for (const char of completion) { webSocketRef.current.send( JSON.stringify({ type: "input", data: char }), ); } - // Update current command tracker updateCurrentCommand(selectedCommand); - // Close autocomplete setShowAutocomplete(false); setAutocompleteSuggestions([]); currentAutocompleteCommand.current = ""; - // Return focus to terminal setTimeout(() => { terminal?.focus(); }, 50); @@ -963,26 +939,22 @@ export const Terminal = forwardRef( [terminal, updateCurrentCommand], ); - // Handle command deletion from history dialog const handleDeleteCommand = useCallback( async (command: string) => { if (!hostConfig.id) return; try { - // Call API to delete command const { deleteCommandFromHistory } = await import( "@/ui/main-axios.ts" ); await deleteCommandFromHistory(hostConfig.id, command); - // Update local state setCommandHistory((prev) => { const newHistory = prev.filter((cmd) => cmd !== command); setCommandHistoryContextRef.current(newHistory); return newHistory; }); - // Update autocomplete history autocompleteHistory.current = autocompleteHistory.current.filter( (cmd) => cmd !== command, ); @@ -995,7 +967,6 @@ export const Terminal = forwardRef( [hostConfig.id], ); - // Register delete handler with context useEffect(() => { commandHistoryContext.setOnDeleteCommand(handleDeleteCommand); }, [handleDeleteCommand]); @@ -1104,7 +1075,6 @@ export const Terminal = forwardRef( navigator.platform.toUpperCase().indexOf("MAC") >= 0 || navigator.userAgent.toUpperCase().indexOf("MAC") >= 0; - // Handle Ctrl+R for command history (Stage 2) if ( e.ctrlKey && e.key === "r" && @@ -1115,7 +1085,6 @@ export const Terminal = forwardRef( e.preventDefault(); e.stopPropagation(); setShowHistoryDialog(true); - // Also trigger the sidebar to open if (commandHistoryContext.openCommandHistory) { commandHistoryContext.openCommandHistory(); } @@ -1210,20 +1179,15 @@ export const Terminal = forwardRef( }; }, [xtermRef, terminal, hostConfig]); - // Register keyboard handler for autocomplete (Stage 3) - // Registered only once when terminal is created useEffect(() => { if (!terminal) return; const handleCustomKey = (e: KeyboardEvent): boolean => { - // Only handle keydown events, ignore keyup to prevent double triggering if (e.type !== "keydown") { return true; } - // If autocomplete is showing, handle keys specially if (showAutocompleteRef.current) { - // Handle Escape to close autocomplete if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); @@ -1233,7 +1197,6 @@ export const Terminal = forwardRef( return false; } - // Handle Arrow keys for autocomplete navigation if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); e.stopPropagation(); @@ -1253,7 +1216,6 @@ export const Terminal = forwardRef( return false; } - // Handle Enter to confirm autocomplete selection if ( e.key === "Enter" && autocompleteSuggestionsRef.current.length > 0 @@ -1268,7 +1230,6 @@ export const Terminal = forwardRef( const currentCmd = currentAutocompleteCommand.current; const completion = selectedCommand.substring(currentCmd.length); - // Send completion characters to server if (webSocketRef.current?.readyState === 1) { for (const char of completion) { webSocketRef.current.send( @@ -1277,10 +1238,8 @@ export const Terminal = forwardRef( } } - // Update current command tracker updateCurrentCommandRef.current(selectedCommand); - // Close autocomplete setShowAutocomplete(false); setAutocompleteSuggestions([]); currentAutocompleteCommand.current = ""; @@ -1288,7 +1247,6 @@ export const Terminal = forwardRef( return false; } - // Handle Tab to cycle through suggestions if ( e.key === "Tab" && !e.ctrlKey && @@ -1306,14 +1264,12 @@ export const Terminal = forwardRef( return false; } - // For any other key while autocomplete is showing, close it and let key through setShowAutocomplete(false); setAutocompleteSuggestions([]); currentAutocompleteCommand.current = ""; return true; } - // Handle Tab for autocomplete (when autocomplete is not showing) if ( e.key === "Tab" && !e.ctrlKey && @@ -1324,12 +1280,10 @@ export const Terminal = forwardRef( e.preventDefault(); e.stopPropagation(); - // Check if command autocomplete is enabled in settings const autocompleteEnabled = localStorage.getItem("commandAutocomplete") !== "false"; if (!autocompleteEnabled) { - // If disabled, let the terminal handle Tab normally (send to server) if (webSocketRef.current?.readyState === 1) { webSocketRef.current.send( JSON.stringify({ type: "input", data: "\t" }), @@ -1340,7 +1294,6 @@ export const Terminal = forwardRef( const currentCmd = getCurrentCommandRef.current().trim(); if (currentCmd.length > 0 && webSocketRef.current?.readyState === 1) { - // Filter commands that start with current input const matches = autocompleteHistory.current .filter( (cmd) => @@ -1348,10 +1301,9 @@ export const Terminal = forwardRef( cmd !== currentCmd && cmd.length > currentCmd.length, ) - .slice(0, 5); // Show up to 5 matches for better UX + .slice(0, 5); if (matches.length === 1) { - // Only one match - auto-complete directly const completedCommand = matches[0]; const completion = completedCommand.substring(currentCmd.length); @@ -1363,12 +1315,10 @@ export const Terminal = forwardRef( updateCurrentCommandRef.current(completedCommand); } else if (matches.length > 1) { - // Multiple matches - show selection list currentAutocompleteCommand.current = currentCmd; setAutocompleteSuggestions(matches); setAutocompleteSelectedIndex(0); - // Calculate position (below or above cursor based on available space) const cursorY = terminal.buffer.active.cursorY; const cursorX = terminal.buffer.active.cursorX; const rect = xtermRef.current?.getBoundingClientRect(); @@ -1379,8 +1329,6 @@ export const Terminal = forwardRef( const cellWidth = terminal.cols > 0 ? rect.width / terminal.cols : 10; - // Calculate actual menu height based on number of items - // Each item is ~32px (py-1.5), footer is ~32px, max total 240px const itemHeight = 32; const footerHeight = 32; const maxMenuHeight = 240; @@ -1388,14 +1336,11 @@ export const Terminal = forwardRef( matches.length * itemHeight + footerHeight, maxMenuHeight, ); - - // Get cursor position in viewport coordinates const cursorBottomY = rect.top + (cursorY + 1) * cellHeight; const cursorTopY = rect.top + cursorY * cellHeight; const spaceBelow = window.innerHeight - cursorBottomY; const spaceAbove = cursorTopY; - // Show above cursor if not enough space below and more space above const showAbove = spaceBelow < estimatedMenuHeight && spaceAbove > spaceBelow; @@ -1410,10 +1355,9 @@ export const Terminal = forwardRef( setShowAutocomplete(true); } } - return false; // Prevent default Tab behavior + return false; } - // Let terminal handle all other keys return true; }; @@ -1470,7 +1414,6 @@ export const Terminal = forwardRef( return; } - // Don't set isFitted to false - keep terminal visible during resize let rafId1: number; let rafId2: number; diff --git a/src/ui/desktop/apps/terminal/command-history/CommandAutocomplete.tsx b/src/ui/desktop/apps/terminal/command-history/CommandAutocomplete.tsx index 78a4b3d6..06c9d4c5 100644 --- a/src/ui/desktop/apps/terminal/command-history/CommandAutocomplete.tsx +++ b/src/ui/desktop/apps/terminal/command-history/CommandAutocomplete.tsx @@ -19,7 +19,6 @@ export function CommandAutocomplete({ const containerRef = useRef(null); const selectedRef = useRef(null); - // Scroll selected item into view useEffect(() => { if (selectedRef.current && containerRef.current) { selectedRef.current.scrollIntoView({ @@ -33,8 +32,6 @@ export function CommandAutocomplete({ return null; } - // Calculate max height for suggestions list to ensure footer is always visible - // Footer height is approximately 32px (text + padding + border) const footerHeight = 32; const maxSuggestionsHeight = 240 - footerHeight; @@ -62,9 +59,7 @@ export function CommandAutocomplete({ index === selectedIndex && "bg-gray-500/20 text-gray-400", )} onClick={() => onSelect(suggestion)} - onMouseEnter={() => { - // Optional: update selected index on hover - }} + onMouseEnter={() => {}} > {suggestion}
diff --git a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx b/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx index e0ecda8c..c8b0e5d2 100644 --- a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx +++ b/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx @@ -105,14 +105,12 @@ export function SSHToolsSidebar({ }; const [activeTab, setActiveTab] = useState(initialTab || "ssh-tools"); - // Update active tab when initialTab changes useEffect(() => { if (initialTab && isOpen) { setActiveTab(initialTab); } }, [initialTab, isOpen]); - // Call onTabChange when active tab changes const handleTabChange = (tab: string) => { setActiveTab(tab); if (onTabChange) { @@ -120,14 +118,12 @@ export function SSHToolsSidebar({ } }; - // SSH Tools state const [isRecording, setIsRecording] = useState(false); const [selectedTabIds, setSelectedTabIds] = useState([]); const [rightClickCopyPaste, setRightClickCopyPaste] = useState( () => getCookie("rightClickCopyPaste") === "true", ); - // Snippets state const [snippets, setSnippets] = useState([]); const [loading, setLoading] = useState(true); const [showDialog, setShowDialog] = useState(false); @@ -145,14 +141,12 @@ export function SSHToolsSidebar({ [], ); - // Command History state const [commandHistory, setCommandHistory] = useState([]); const [isHistoryLoading, setIsHistoryLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [historyRefreshCounter, setHistoryRefreshCounter] = useState(0); const commandHistoryScrollRef = React.useRef(null); - // Split Screen state const [splitMode, setSplitMode] = useState<"none" | "2" | "3" | "4">("none"); const [splitAssignments, setSplitAssignments] = useState>( new Map(), @@ -163,7 +157,6 @@ export function SSHToolsSidebar({ null, ); - // Resize state const [isResizing, setIsResizing] = useState(false); const startXRef = React.useRef(null); const startWidthRef = React.useRef(sidebarWidth); @@ -174,7 +167,6 @@ export function SSHToolsSidebar({ activeUiTab?.type === "terminal" ? activeUiTab : undefined; const activeTerminalHostId = activeTerminal?.hostConfig?.id; - // Get splittable tabs (terminal, server, file_manager) const splittableTabs = tabs.filter( (tab: TabData) => tab.type === "terminal" || @@ -183,20 +175,16 @@ export function SSHToolsSidebar({ tab.type === "user_profile", ); - // Fetch command history useEffect(() => { if (isOpen && activeTab === "command-history") { if (activeTerminalHostId) { - // Save current scroll position before any state updates const scrollTop = commandHistoryScrollRef.current?.scrollTop || 0; getCommandHistory(activeTerminalHostId) .then((history) => { setCommandHistory((prevHistory) => { const newHistory = Array.isArray(history) ? history : []; - // Only update if history actually changed if (JSON.stringify(prevHistory) !== JSON.stringify(newHistory)) { - // Use requestAnimationFrame to restore scroll after React finishes rendering requestAnimationFrame(() => { if (commandHistoryScrollRef.current) { commandHistoryScrollRef.current.scrollTop = scrollTop; @@ -223,7 +211,6 @@ export function SSHToolsSidebar({ historyRefreshCounter, ]); - // Auto-refresh command history every 2 seconds when history tab is active useEffect(() => { if (isOpen && activeTab === "command-history" && activeTerminalHostId) { const refreshInterval = setInterval(() => { @@ -234,14 +221,12 @@ export function SSHToolsSidebar({ } }, [isOpen, activeTab, activeTerminalHostId]); - // Filter command history based on search query const filteredCommands = searchQuery ? commandHistory.filter((cmd) => cmd.toLowerCase().includes(searchQuery.toLowerCase()), ) : commandHistory; - // Initialize CSS variable on mount and when sidebar width changes useEffect(() => { document.documentElement.style.setProperty( "--right-sidebar-width", @@ -249,7 +234,6 @@ export function SSHToolsSidebar({ ); }, [sidebarWidth]); - // Handle window resize to adjust sidebar width useEffect(() => { const handleResize = () => { const minWidth = Math.min(300, Math.floor(window.innerWidth * 0.2)); @@ -270,7 +254,6 @@ export function SSHToolsSidebar({ } }, [isOpen, activeTab]); - // Resize handlers const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); @@ -283,7 +266,7 @@ export function SSHToolsSidebar({ const handleMouseMove = (e: MouseEvent) => { if (startXRef.current == null) return; - const dx = startXRef.current - e.clientX; // Reversed because we're on the right + const dx = startXRef.current - e.clientX; const newWidth = Math.round(startWidthRef.current + dx); const minWidth = Math.min(300, Math.floor(window.innerWidth * 0.2)); const maxWidth = Math.round(window.innerWidth * 0.3); @@ -295,13 +278,11 @@ export function SSHToolsSidebar({ finalWidth = maxWidth; } - // Update CSS variable immediately for smooth animation document.documentElement.style.setProperty( "--right-sidebar-width", `${finalWidth}px`, ); - // Update React state (this will be batched/debounced naturally) setSidebarWidth(finalWidth); }; @@ -323,7 +304,6 @@ export function SSHToolsSidebar({ }; }, [isResizing]); - // SSH Tools handlers const handleTabToggle = (tabId: number) => { setSelectedTabIds((prev) => prev.includes(tabId) @@ -487,7 +467,6 @@ export function SSHToolsSidebar({ setRightClickCopyPaste(checked); }; - // Snippets handlers const fetchSnippets = async () => { try { setLoading(true); @@ -599,15 +578,12 @@ export function SSHToolsSidebar({ toast.success(t("snippets.copySuccess", { name: snippet.name })); }; - // Split Screen handlers const handleSplitModeChange = (mode: "none" | "2" | "3" | "4") => { setSplitMode(mode); if (mode === "none") { - // Clear all splits handleClearSplit(); } else { - // Clear assignments when changing modes setSplitAssignments(new Map()); setPreviewKey((prev) => prev + 1); } @@ -636,7 +612,6 @@ export function SSHToolsSidebar({ setSplitAssignments((prev) => { const newMap = new Map(prev); - // Remove this tab from any other cell Array.from(newMap.entries()).forEach(([idx, id]) => { if (id === draggedTabId && idx !== cellIndex) { newMap.delete(idx); @@ -677,7 +652,6 @@ export function SSHToolsSidebar({ const requiredSlots = parseInt(splitMode); - // Validate: All layout spots must be filled if (splitAssignments.size < requiredSlots) { toast.error( t("splitScreen.error.fillAllSlots", { @@ -688,7 +662,6 @@ export function SSHToolsSidebar({ return; } - // Build ordered array of tab IDs based on cell index (0, 1, 2, 3) const orderedTabIds: number[] = []; for (let i = 0; i < requiredSlots; i++) { const tabId = splitAssignments.get(i); @@ -697,18 +670,15 @@ export function SSHToolsSidebar({ } } - // First, clear ALL existing splits const currentSplits = [...allSplitScreenTab]; currentSplits.forEach((tabId) => { - setSplitScreenTab(tabId); // Toggle off + setSplitScreenTab(tabId); }); - // Then, add only the newly assigned tabs to split IN ORDER orderedTabIds.forEach((tabId) => { - setSplitScreenTab(tabId); // Toggle on + setSplitScreenTab(tabId); }); - // Set first assigned tab as active if current tab is not in split if (!orderedTabIds.includes(currentTab ?? 0)) { setCurrentTab(orderedTabIds[0]); } @@ -721,7 +691,6 @@ export function SSHToolsSidebar({ }; const handleClearSplit = () => { - // Remove all tabs from split screen allSplitScreenTab.forEach((tabId) => { setSplitScreenTab(tabId); }); @@ -741,7 +710,6 @@ export function SSHToolsSidebar({ handleClearSplit(); }; - // Command History handlers const handleCommandSelect = (command: string) => { if (activeTerminal?.terminalRef?.current?.sendInput) { activeTerminal.terminalRef.current.sendInput(command); diff --git a/src/ui/desktop/authentication/ElectronLoginForm.tsx b/src/ui/desktop/authentication/ElectronLoginForm.tsx index f52fb9ae..54cdfb99 100644 --- a/src/ui/desktop/authentication/ElectronLoginForm.tsx +++ b/src/ui/desktop/authentication/ElectronLoginForm.tsx @@ -26,7 +26,6 @@ export function ElectronLoginForm({ const hasLoadedOnce = useRef(false); useEffect(() => { - // Clear any existing token to prevent login loops with expired tokens localStorage.removeItem("jwt"); }, []); diff --git a/src/ui/desktop/navigation/AppView.tsx b/src/ui/desktop/navigation/AppView.tsx index e9bf4836..51baf6e4 100644 --- a/src/ui/desktop/navigation/AppView.tsx +++ b/src/ui/desktop/navigation/AppView.tsx @@ -111,7 +111,6 @@ export function AppView({ }, [updatePanelRects, fitActiveAndNotify]); const hideThenFit = React.useCallback(() => { - // Don't hide terminals, just fit them immediately requestAnimationFrame(() => { updatePanelRects(); requestAnimationFrame(() => { @@ -204,7 +203,6 @@ export function AppView({ const renderTerminalsLayer = () => { const styles: Record = {}; - // Use allSplitScreenTab order directly - it maintains the order tabs were added const layoutTabs = allSplitScreenTab .map((tabId) => terminalTabs.find((tab: TabData) => tab.id === tabId)) .filter((t): t is TabData => t !== null && t !== undefined); @@ -259,10 +257,8 @@ export function AppView({ const isVisible = hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab); - // Use previous style if available to maintain position const previousStyle = previousStylesRef.current[t.id]; - // For non-split screen tabs, always use the standard position const isFileManagerTab = t.type === "file_manager"; const standardStyle = { position: "absolute" as const, @@ -354,7 +350,6 @@ export function AppView({ }; const renderSplitOverlays = () => { - // Use allSplitScreenTab order directly - it maintains the order tabs were added const layoutTabs = allSplitScreenTab .map((tabId) => terminalTabs.find((tab: TabData) => tab.id === tabId)) .filter((t): t is TabData => t !== null && t !== undefined); diff --git a/src/ui/desktop/navigation/TopNavbar.tsx b/src/ui/desktop/navigation/TopNavbar.tsx index 69f45d04..e85e91d6 100644 --- a/src/ui/desktop/navigation/TopNavbar.tsx +++ b/src/ui/desktop/navigation/TopNavbar.tsx @@ -99,7 +99,6 @@ export function TopNavbar({ setCommandHistoryTabActive(true); }, []); - // Register function to open command history sidebar React.useEffect(() => { commandHistory.setOpenCommandHistory(openCommandHistorySidebar); }, [commandHistory, openCommandHistorySidebar]); @@ -133,11 +132,9 @@ export function TopNavbar({ }; const handleTabSplit = (tabId: number) => { - // Open the sidebar to the split-screen tab setToolsSidebarOpen(true); setCommandHistoryTabActive(false); setSplitScreenTabActive(true); - // Optional: could pass tabId to pre-select this tab in the sidebar }; const handleTabClose = (tabId: number) => { diff --git a/src/ui/desktop/user/ElectronVersionCheck.tsx b/src/ui/desktop/user/ElectronVersionCheck.tsx index 9496afc2..8f613d70 100644 --- a/src/ui/desktop/user/ElectronVersionCheck.tsx +++ b/src/ui/desktop/user/ElectronVersionCheck.tsx @@ -35,20 +35,17 @@ export function ElectronVersionCheck({ const updateInfo = await checkElectronUpdate(); setVersionInfo(updateInfo); - // Get current app version const currentVersion = await (window as any).electronAPI?.getAppVersion(); const dismissedVersion = localStorage.getItem( "electron-version-check-dismissed", ); - // If this version was already dismissed, skip the modal if (dismissedVersion === currentVersion) { onContinue(); return; } if (updateInfo?.status === "up_to_date") { - // Store this version as checked (but don't show modal since up to date) if (currentVersion) { localStorage.setItem( "electron-version-check-dismissed", @@ -73,7 +70,6 @@ export function ElectronVersionCheck({ }; const handleContinue = async () => { - // Store the current version as dismissed const currentVersion = await (window as any).electronAPI?.getAppVersion(); if (currentVersion) { localStorage.setItem("electron-version-check-dismissed", currentVersion); diff --git a/src/ui/desktop/user/UserProfile.tsx b/src/ui/desktop/user/UserProfile.tsx index 4ca1b3ba..9c7bd7a8 100644 --- a/src/ui/desktop/user/UserProfile.tsx +++ b/src/ui/desktop/user/UserProfile.tsx @@ -144,7 +144,6 @@ export function UserProfile({ const handleFileColorCodingToggle = (enabled: boolean) => { setFileColorCoding(enabled); localStorage.setItem("fileColorCoding", enabled.toString()); - // Trigger a re-render by dispatching a custom event window.dispatchEvent(new Event("fileColorCodingChanged")); }; diff --git a/src/ui/hooks/useCommandHistory.ts b/src/ui/hooks/useCommandHistory.ts index ef877759..68fdea8e 100644 --- a/src/ui/hooks/useCommandHistory.ts +++ b/src/ui/hooks/useCommandHistory.ts @@ -14,9 +14,6 @@ interface CommandHistoryResult { isLoading: boolean; } -/** - * Custom hook for managing command history and autocomplete suggestions - */ export function useCommandHistory({ hostId, enabled = true, @@ -25,8 +22,7 @@ export function useCommandHistory({ const [suggestions, setSuggestions] = useState([]); const [isLoading, setIsLoading] = useState(false); const historyCache = useRef>(new Map()); - - // Fetch command history when hostId changes + s; useEffect(() => { if (!enabled || !hostId) { setCommandHistory([]); @@ -34,14 +30,12 @@ export function useCommandHistory({ return; } - // Check cache first const cached = historyCache.current.get(hostId); if (cached) { setCommandHistory(cached); return; } - // Fetch from server const fetchHistory = async () => { setIsLoading(true); try { @@ -59,9 +53,6 @@ export function useCommandHistory({ fetchHistory(); }, [hostId, enabled]); - /** - * Get command suggestions based on current input - */ const getSuggestions = useCallback( (input: string): string[] => { if (!input || input.trim().length === 0) { @@ -73,7 +64,6 @@ export function useCommandHistory({ cmd.startsWith(trimmedInput), ); - // Return up to 10 suggestions, excluding exact matches const filtered = matches .filter((cmd) => cmd !== trimmedInput) .slice(0, 10); @@ -84,9 +74,6 @@ export function useCommandHistory({ [commandHistory], ); - /** - * Save a command to history - */ const saveCommand = useCallback( async (command: string) => { if (!enabled || !hostId || !command || command.trim().length === 0) { @@ -95,29 +82,24 @@ export function useCommandHistory({ const trimmedCommand = command.trim(); - // Skip if it's the same as the last command if (commandHistory.length > 0 && commandHistory[0] === trimmedCommand) { return; } try { - // Save to server await saveCommandToHistory(hostId, trimmedCommand); - // Update local state - add to beginning setCommandHistory((prev) => { const newHistory = [ trimmedCommand, ...prev.filter((c) => c !== trimmedCommand), ]; - // Keep max 500 commands in memory const limited = newHistory.slice(0, 500); historyCache.current.set(hostId, limited); return limited; }); } catch (error) { console.error("Failed to save command to history:", error); - // Still update local state even if server save fails setCommandHistory((prev) => { const newHistory = [ trimmedCommand, @@ -130,9 +112,6 @@ export function useCommandHistory({ [enabled, hostId, commandHistory], ); - /** - * Clear current suggestions - */ const clearSuggestions = useCallback(() => { setSuggestions([]); }, []); diff --git a/src/ui/hooks/useCommandTracker.ts b/src/ui/hooks/useCommandTracker.ts index e3d6d88e..069ac7d6 100644 --- a/src/ui/hooks/useCommandTracker.ts +++ b/src/ui/hooks/useCommandTracker.ts @@ -14,10 +14,6 @@ interface CommandTrackerResult { updateCurrentCommand: (command: string) => void; } -/** - * Hook to track terminal input and save executed commands to history - * Works with SSH terminals by monitoring input data - */ export function useCommandTracker({ hostId, enabled = true, @@ -26,33 +22,25 @@ export function useCommandTracker({ const currentCommandRef = useRef(""); const isInEscapeSequenceRef = useRef(false); - /** - * Track input data and detect command execution - */ const trackInput = useCallback( (data: string) => { if (!enabled || !hostId) { return; } - // Handle each character for (let i = 0; i < data.length; i++) { const char = data[i]; const charCode = char.charCodeAt(0); - // Detect escape sequences (e.g., arrow keys, function keys) if (charCode === 27) { - // ESC isInEscapeSequenceRef.current = true; continue; } - // Skip characters that are part of escape sequences if (isInEscapeSequenceRef.current) { - // Common escape sequence endings if ( - (charCode >= 65 && charCode <= 90) || // A-Z - (charCode >= 97 && charCode <= 122) || // a-z + (charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || charCode === 126 // ~ ) { isInEscapeSequenceRef.current = false; @@ -60,53 +48,41 @@ export function useCommandTracker({ continue; } - // Handle Enter key (CR or LF) if (charCode === 13 || charCode === 10) { - // \r or \n const command = currentCommandRef.current.trim(); - // Save non-empty commands if (command.length > 0) { - // Save to history (async, don't wait) saveCommandToHistory(hostId, command).catch((error) => { console.error("Failed to save command to history:", error); }); - // Callback for external handling if (onCommandExecuted) { onCommandExecuted(command); } } - // Clear current command currentCommandRef.current = ""; continue; } - // Handle Backspace/Delete if (charCode === 8 || charCode === 127) { - // Backspace or DEL if (currentCommandRef.current.length > 0) { currentCommandRef.current = currentCommandRef.current.slice(0, -1); } continue; } - // Handle Ctrl+C, Ctrl+D, etc. - clear current command if (charCode === 3 || charCode === 4) { currentCommandRef.current = ""; continue; } - // Handle Ctrl+U (clear line) - common in terminals if (charCode === 21) { currentCommandRef.current = ""; continue; } - // Add printable characters to current command if (charCode >= 32 && charCode <= 126) { - // Printable ASCII currentCommandRef.current += char; } } @@ -114,23 +90,14 @@ export function useCommandTracker({ [enabled, hostId, onCommandExecuted], ); - /** - * Get the current command being typed - */ const getCurrentCommand = useCallback(() => { return currentCommandRef.current; }, []); - /** - * Clear the current command buffer - */ const clearCurrentCommand = useCallback(() => { currentCommandRef.current = ""; }, []); - /** - * Update the current command buffer (used for autocomplete) - */ const updateCurrentCommand = useCallback((command: string) => { currentCommandRef.current = command; }, []); diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index c681283d..34c477f6 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -138,10 +138,8 @@ function getLoggerForService(serviceName: string) { } } -// Cache for Electron settings const electronSettingsCache = new Map(); -// Load settings from Electron IPC on startup if (isElectron()) { (async () => { try { @@ -153,13 +151,12 @@ if (isElectron()) { ).electronAPI; if (electronAPI?.getSetting) { - // Preload common settings const settingsToLoad = ["rightClickCopyPaste", "jwt"]; for (const key of settingsToLoad) { const value = await electronAPI.getSetting(key); if (value !== null && value !== undefined) { electronSettingsCache.set(key, value); - localStorage.setItem(key, value); // Sync to localStorage + localStorage.setItem(key, value); } } } @@ -172,13 +169,10 @@ if (isElectron()) { export function setCookie(name: string, value: string, days = 7): void { if (isElectron()) { try { - // Update cache electronSettingsCache.set(name, value); - // Set in localStorage localStorage.setItem(name, value); - // Persist to file system via Electron IPC const electronAPI = ( window as Window & typeof globalThis & { @@ -205,12 +199,10 @@ export function setCookie(name: string, value: string, days = 7): void { export function getCookie(name: string): string | undefined { if (isElectron()) { try { - // Try cache first if (electronSettingsCache.has(name)) { return electronSettingsCache.get(name); } - // Fallback to localStorage const token = localStorage.getItem(name) || undefined; if (token) { electronSettingsCache.set(name, token); @@ -397,22 +389,17 @@ function createApiInstance( errorMessage === "Authentication required"; if (isSessionExpired || isSessionNotFound || isInvalidToken) { - // Clear token from localStorage localStorage.removeItem("jwt"); - // Clear Electron settings cache if (isElectron()) { electronSettingsCache.delete("jwt"); } - // Clear cookie if (typeof window !== "undefined") { document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; } - // Only show toast and reload for explicit session expiration - // Let the auth check in DesktopApp.tsx handle the redirect silently if (isSessionExpired && typeof window !== "undefined") { console.warn("Session expired - please log in again"); import("sonner").then(({ toast }) => { @@ -1737,7 +1724,6 @@ export async function compressSSHFiles( // FILE MANAGER DATA // ============================================================================ -// Recent Files export async function getRecentFiles( hostId: number, ): Promise> { @@ -1916,7 +1902,6 @@ export async function refreshServerPolling(): Promise { try { await statsApi.post("/refresh"); } catch (error) { - // Silently fail - this is a background operation console.warn("Failed to refresh server polling:", error); } } @@ -1927,7 +1912,6 @@ export async function notifyHostCreatedOrUpdated( try { await statsApi.post("/host-updated", { hostId }); } catch (error) { - // Silently fail - this is a background operation console.warn("Failed to notify stats server of host update:", error); } } @@ -2961,9 +2945,6 @@ export async function resetRecentActivity(): Promise<{ message: string }> { // COMMAND HISTORY API // ============================================================================ -/** - * Save a command to history for a specific host - */ export async function saveCommandToHistory( hostId: number, command: string, @@ -2979,12 +2960,6 @@ export async function saveCommandToHistory( } } -/** - * Get command history for a specific host - * Returns array of unique commands ordered by most recent - * @param hostId - The host ID to fetch history for - * @param limit - Maximum number of commands to return (default: 100) - */ export async function getCommandHistory( hostId: number, limit: number = 100, @@ -2999,9 +2974,6 @@ export async function getCommandHistory( } } -/** - * Delete a specific command from history - */ export async function deleteCommandFromHistory( hostId: number, command: string, @@ -3017,9 +2989,6 @@ export async function deleteCommandFromHistory( } } -/** - * Clear command history for a specific host (optional feature) - */ export async function clearCommandHistory( hostId: number, ): Promise<{ success: boolean }> { @@ -3037,9 +3006,6 @@ export async function clearCommandHistory( // OIDC ACCOUNT LINKING // ============================================================================ -/** - * Link an OIDC user to an existing password account (merges OIDC into password account) - */ export async function linkOIDCToPasswordAccount( oidcUserId: string, targetUsername: string,