From b64e2e11db5073e4dad5f1d95ceb597552c4389c Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 24 Dec 2025 12:20:22 -0500 Subject: [PATCH 001/154] Try to remove deadlocks on client updates --- .../newt/handleReceiveBandwidthMessage.ts | 104 +++++++++++++----- 1 file changed, 79 insertions(+), 25 deletions(-) diff --git a/server/routers/newt/handleReceiveBandwidthMessage.ts b/server/routers/newt/handleReceiveBandwidthMessage.ts index 3d060a0c..eb930e68 100644 --- a/server/routers/newt/handleReceiveBandwidthMessage.ts +++ b/server/routers/newt/handleReceiveBandwidthMessage.ts @@ -1,7 +1,7 @@ import { db } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; -import { clients, Newt } from "@server/db"; -import { eq } from "drizzle-orm"; +import { clients } from "@server/db"; +import { eq, sql } from "drizzle-orm"; import logger from "@server/logger"; interface PeerBandwidth { @@ -10,13 +10,57 @@ interface PeerBandwidth { bytesOut: number; } +// Retry configuration for deadlock handling +const MAX_RETRIES = 3; +const BASE_DELAY_MS = 50; + +/** + * Check if an error is a deadlock error + */ +function isDeadlockError(error: any): boolean { + return ( + error?.code === "40P01" || + error?.cause?.code === "40P01" || + (error?.message && error.message.includes("deadlock")) + ); +} + +/** + * Execute a function with retry logic for deadlock handling + */ +async function withDeadlockRetry( + operation: () => Promise, + context: string +): Promise { + let attempt = 0; + while (true) { + try { + return await operation(); + } catch (error: any) { + if (isDeadlockError(error) && attempt < MAX_RETRIES) { + attempt++; + const baseDelay = Math.pow(2, attempt - 1) * BASE_DELAY_MS; + const jitter = Math.random() * baseDelay; + const delay = baseDelay + jitter; + logger.warn( + `Deadlock detected in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms` + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + throw error; + } + } +} + export const handleReceiveBandwidthMessage: MessageHandler = async ( context ) => { - const { message, client, sendToClient } = context; + const { message } = context; if (!message.data.bandwidthData) { logger.warn("No bandwidth data provided"); + return; } const bandwidthData: PeerBandwidth[] = message.data.bandwidthData; @@ -25,30 +69,40 @@ export const handleReceiveBandwidthMessage: MessageHandler = async ( throw new Error("Invalid bandwidth data"); } - await db.transaction(async (trx) => { - for (const peer of bandwidthData) { - const { publicKey, bytesIn, bytesOut } = peer; + // Sort bandwidth data by publicKey to ensure consistent lock ordering across all instances + // This is critical for preventing deadlocks when multiple instances update the same clients + const sortedBandwidthData = [...bandwidthData].sort((a, b) => + a.publicKey.localeCompare(b.publicKey) + ); - // Find the client by public key - const [client] = await trx - .select() - .from(clients) - .where(eq(clients.pubKey, publicKey)) - .limit(1); + const currentTime = new Date().toISOString(); - if (!client) { - continue; - } + // Update each client individually with retry logic + // This reduces transaction scope and allows retries per-client + for (const peer of sortedBandwidthData) { + const { publicKey, bytesIn, bytesOut } = peer; - // Update the client's bandwidth usage - await trx - .update(clients) - .set({ - megabytesOut: (client.megabytesIn || 0) + bytesIn, - megabytesIn: (client.megabytesOut || 0) + bytesOut, - lastBandwidthUpdate: new Date().toISOString() - }) - .where(eq(clients.clientId, client.clientId)); + try { + await withDeadlockRetry(async () => { + // Use atomic SQL increment to avoid SELECT then UPDATE pattern + // This eliminates the need to read the current value first + await db + .update(clients) + .set({ + // Note: bytesIn from peer goes to megabytesOut (data sent to client) + // and bytesOut from peer goes to megabytesIn (data received from client) + megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`, + megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`, + lastBandwidthUpdate: currentTime + }) + .where(eq(clients.pubKey, publicKey)); + }, `update client bandwidth ${publicKey}`); + } catch (error) { + logger.error( + `Failed to update bandwidth for client ${publicKey}:`, + error + ); + // Continue with other clients even if one fails } - }); + } }; From c5ece144d0da460bd67ef466b3c3a1e5f35f6977 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 24 Dec 2025 12:25:11 -0500 Subject: [PATCH 002/154] Attempt to fix loginPageOrg undefined error --- server/private/routers/hybrid.ts | 20 +++++++++---------- .../routers/loginPage/loadLoginPage.ts | 10 ++++++++++ .../loginPage/loadLoginPageBranding.ts | 5 +++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index bbc0e0c8..a398dfe6 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -618,6 +618,16 @@ hybridRouter.get( ) .limit(1); + if (!result) { + return response(res, { + data: null, + success: true, + error: false, + message: "Login page not found", + status: HttpCode.OK + }); + } + if ( await checkExitNodeOrg( remoteExitNode.exitNodeId, @@ -633,16 +643,6 @@ hybridRouter.get( ); } - if (!result) { - return response(res, { - data: null, - success: true, - error: false, - message: "Login page not found", - status: HttpCode.OK - }); - } - return response(res, { data: result.loginPage, success: true, diff --git a/server/private/routers/loginPage/loadLoginPage.ts b/server/private/routers/loginPage/loadLoginPage.ts index 1b10e205..7a631c8a 100644 --- a/server/private/routers/loginPage/loadLoginPage.ts +++ b/server/private/routers/loginPage/loadLoginPage.ts @@ -40,6 +40,11 @@ async function query(orgId: string | undefined, fullDomain: string) { eq(loginPage.loginPageId, loginPageOrg.loginPageId) ) .limit(1); + + if (!res) { + return null; + } + return { ...res.loginPage, orgId: res.loginPageOrg.orgId @@ -65,6 +70,11 @@ async function query(orgId: string | undefined, fullDomain: string) { ) ) .limit(1); + + if (!res) { + return null; + } + return { ...res, orgId: orgLink.orgId diff --git a/server/private/routers/loginPage/loadLoginPageBranding.ts b/server/private/routers/loginPage/loadLoginPageBranding.ts index 823f75a6..1197bb10 100644 --- a/server/private/routers/loginPage/loadLoginPageBranding.ts +++ b/server/private/routers/loginPage/loadLoginPageBranding.ts @@ -48,6 +48,11 @@ async function query(orgId: string) { ) ) .limit(1); + + if (!res) { + return null; + } + return { ...res, orgId: orgLink.orgs.orgId, From 9fba9bd6b7301679e775375d524c02cceb60a22b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 24 Dec 2025 15:53:08 -0500 Subject: [PATCH 003/154] ui enhancements --- messages/en-US.json | 2 +- server/routers/client/getClient.ts | 2 +- .../settings/(private)/idp/create/page.tsx | 2 +- .../[orgId]/settings/(private)/idp/page.tsx | 34 +------- .../resources/proxy/[niceId]/proxy/page.tsx | 2 +- src/app/globals.css | 12 +++ src/app/layout.tsx | 4 +- src/components/Layout.tsx | 4 +- src/components/LayoutMobileMenu.tsx | 2 +- src/components/ProxyResourcesTable.tsx | 2 +- src/components/ViewportHeightFix.tsx | 79 +++++++++++++++++++ src/components/ui/button.tsx | 43 +++++----- src/components/ui/checkbox.tsx | 16 ++-- 13 files changed, 131 insertions(+), 73 deletions(-) create mode 100644 src/components/ViewportHeightFix.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 57821a6d..8b04e4ea 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1480,7 +1480,7 @@ "IAgreeToThe": "I agree to the", "termsOfService": "terms of service", "and": "and", - "privacyPolicy": "privacy policy" + "privacyPolicy": "privacy policy." }, "signUpMarketing": { "keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email." diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index cfb2652b..f054ce80 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -36,7 +36,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) { .select() .from(clients) .where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId))) - .leftJoin(olms, eq(olms.clientId, olms.clientId)) + .leftJoin(olms, eq(clients.clientId, olms.clientId)) .limit(1); return res; } diff --git a/src/app/[orgId]/settings/(private)/idp/create/page.tsx b/src/app/[orgId]/settings/(private)/idp/create/page.tsx index 786c8635..f6260073 100644 --- a/src/app/[orgId]/settings/(private)/idp/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/create/page.tsx @@ -285,7 +285,7 @@ export default function Page() { diff --git a/src/app/globals.css b/src/app/globals.css index 70c614c0..731e1bff 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -178,4 +178,16 @@ p { .animate-dot-pulse { animation: dot-pulse 1.4s ease-in-out infinite; } + + /* Use JavaScript-set viewport height for mobile to handle keyboard properly */ + .h-screen-safe { + height: 100vh; /* Default for desktop and fallback */ + } + + /* Only apply custom viewport height on mobile */ + @media (max-width: 767px) { + .h-screen-safe { + height: var(--vh, 100vh); /* Use CSS variable set by ViewportHeightFix on mobile */ + } + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e76a5d2f..203dd778 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,6 +22,7 @@ import { TopLoader } from "@app/components/Toploader"; import Script from "next/script"; import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider"; import { TailwindIndicator } from "@app/components/TailwindIndicator"; +import { ViewportHeightFix } from "@app/components/ViewportHeightFix"; export const metadata: Metadata = { title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, @@ -77,7 +78,7 @@ export default async function RootLayout({ return ( - + {build === "saas" && (