Skip to content

Commit f1f08a6

Browse files
Refactor database connection handling and improve query execution with automatic cleanup
1 parent 4ba10e5 commit f1f08a6

5 files changed

Lines changed: 117 additions & 29 deletions

File tree

app/api/status/clear/route.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ export async function POST(request: NextRequest) {
1919
try {
2020
if (!dbPool) return NextResponse.json({error: "DB not initialized"}, {status: 500})
2121

22-
await dbPool.query(`DELETE FROM status_logs WHERE project_slug = $1`, [projectSlug])
23-
24-
return NextResponse.json({message: `Cleared logs for ${projectSlug}`})
22+
const client = await dbPool.connect()
23+
try {
24+
await client.query(`DELETE FROM status_logs WHERE project_slug = $1`, [projectSlug])
25+
return NextResponse.json({message: `Cleared logs for ${projectSlug}`})
26+
} finally {
27+
client.release()
28+
}
2529
} catch (err) {
2630
return NextResponse.json({
2731
error: "Failed to clear logs",

app/page.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import {ProjectPreview} from "@/components/project-preview"
88
import {StatusBadge} from "@/components/status-badge"
99

1010
export default async function DashboardPage() {
11-
const projectStatuses = await Promise.all(
12-
projects.map(async (project) => ({
11+
// Process projects serially to avoid overwhelming the single DB connection
12+
const projectStatuses = []
13+
for (const project of projects) {
14+
const status = await getProjectStatus(project.slug, project.routes)
15+
projectStatuses.push({
1316
...project,
14-
status: await getProjectStatus(project.slug, project.routes),
15-
})),
16-
)
17+
status,
18+
})
19+
}
1720

1821
return (
1922
<div className="min-h-screen bg-background">

components/project-preview.tsx

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,79 @@ export async function ProjectPreview({url, title, renderUrl}: ProjectPreviewProp
2929
let html = await response.text()
3030

3131
// Remove script tags to make it static (no JavaScript execution)
32-
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
32+
// Use iterative replacement to handle nested or overlapping patterns
33+
let previousHtml = ''
34+
while (previousHtml !== html) {
35+
previousHtml = html
36+
html = html.replace(/<script[\s\S]*?<\/script>/gi, '')
37+
html = html.replace(/<script[^>]*>/gi, '')
38+
}
3339

3440
// Remove noscript tags (they're not needed in a static preview)
35-
html = html.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, '')
41+
previousHtml = ''
42+
while (previousHtml !== html) {
43+
previousHtml = html
44+
html = html.replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
45+
}
3646

3747
// Remove preload links for scripts (since we removed the scripts)
38-
html = html.replace(/<link[^>]*rel=["']?preload["'][^>]*as=["']?script["'][^>]*>/gi, '')
39-
html = html.replace(/<link[^>]*as=["']?script["'][^>]*rel=["']?preload["'][^>]*>/gi, '')
48+
html = html.replace(/<link[^>]*rel\s*=\s*["']?preload["'][^>]*as\s*=\s*["']?script["'][^>]*>/gi, '')
49+
html = html.replace(/<link[^>]*as\s*=\s*["']?script["'][^>]*rel\s*=\s*["']?preload["'][^>]*>/gi, '')
4050

4151
// Remove modulepreload links as well
42-
html = html.replace(/<link[^>]*rel=["']?modulepreload["'][^>]*>/gi, '')
52+
html = html.replace(/<link[^>]*rel\s*=\s*["']?modulepreload["'][^>]*>/gi, '')
4353

4454
// Remove inline event handlers (onclick, onload, onerror, etc.)
45-
html = html.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
46-
html = html.replace(/\s+on\w+\s*=\s*[^\s>]*/gi, '')
55+
// Use iterative replacement to handle overlapping patterns like "ononclick"
56+
previousHtml = ''
57+
while (previousHtml !== html) {
58+
previousHtml = html
59+
html = html.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, ' ')
60+
html = html.replace(/\son\w+\s*=\s*[^\s>]+/gi, ' ')
61+
}
62+
63+
// Remove javascript: URIs
64+
previousHtml = ''
65+
while (previousHtml !== html) {
66+
previousHtml = html
67+
html = html.replace(/\s(href|src|action|formaction|data)\s*=\s*["']?\s*javascript:/gi, ' data-blocked-$1="javascript:')
68+
}
69+
70+
// Remove data: URIs that could contain HTML/SVG with scripts
71+
previousHtml = ''
72+
while (previousHtml !== html) {
73+
previousHtml = html
74+
html = html.replace(/\s(href|src|action|formaction)\s*=\s*["']?\s*data:text\/html/gi, ' data-blocked-$1="data:text/html')
75+
}
76+
77+
// Remove potentially dangerous tags (object, embed, applet)
78+
previousHtml = ''
79+
while (previousHtml !== html) {
80+
previousHtml = html
81+
html = html.replace(/<(object|embed|applet)[\s\S]*?<\/\1>/gi, '')
82+
html = html.replace(/<(object|embed|applet)[^>]*>/gi, '')
83+
}
4784

4885
// Remove existing CSP meta tags that might conflict
49-
html = html.replace(/<meta[^>]*http-equiv=["']?Content-Security-Policy["']?[^>]*>/gi, '')
86+
html = html.replace(/<meta[^>]*http-equiv\s*=\s*["']?Content-Security-Policy["']?[^>]*>/gi, '')
5087

5188
// Get the base URL (in case of redirects, use the final URL)
5289
const baseUrl = new URL(response.url)
5390
// Use the full origin with trailing slash to ensure all resources load correctly
5491
const baseHref = baseUrl.origin + '/'
5592

93+
// Helper function to escape HTML attributes
94+
const escapeHtmlAttr = (str: string) => {
95+
return str
96+
.replace(/&/g, '&amp;')
97+
.replace(/"/g, '&quot;')
98+
.replace(/'/g, '&#x27;')
99+
.replace(/</g, '&lt;')
100+
.replace(/>/g, '&gt;')
101+
}
102+
56103
// Inject base tag, CSP meta tag (blocking all scripts), and no-scroll style in the head
57-
const baseTag = `<base href="${baseHref}">`
104+
const baseTag = `<base href="${escapeHtmlAttr(baseHref)}">`
58105
const cspMeta = `<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'none'; style-src * 'unsafe-inline'; img-src * data: blob:; font-src * data:; connect-src * data: blob:;">`
59106
const noScrollStyle = `<style>html, body { overflow: hidden !important; }</style>`
60107

lib/db.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ validateServerSettings(true)
1212
const caPath = path.join(process.cwd(), 'certs', 'ca.pem')
1313
const ca = fs.existsSync(caPath) ? fs.readFileSync(caPath).toString() : undefined
1414

15+
// TODO Put this in settings.ts
1516
const poolConfig: any = {
1617
connectionString: settings.db.url ?? undefined,
17-
max: settings.db.maxClients,
18+
max: 1, // Force single connection to avoid "remaining connection slots are reserved" error
19+
min: 0, // No minimum connections
1820
// Connection pool settings to prevent exhaustion
19-
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
21+
idleTimeoutMillis: 5000, // Close idle clients after 5 seconds (aggressive cleanup)
2022
connectionTimeoutMillis: 10000, // Wait max 10 seconds for a connection from the pool
21-
allowExitOnIdle: settings.db.allowExitOnIdle,
23+
allowExitOnIdle: true, // Always allow exit on idle to prevent hanging connections
24+
// Additional settings to ensure connection cleanup
25+
statement_timeout: 10000, // Kill queries that take longer than 10 seconds
26+
query_timeout: 10000, // Same as statement_timeout for safety
2227
}
2328

2429
if (ca) {
@@ -33,6 +38,19 @@ const g = global as unknown as {
3338

3439
if (!g.__dbPool) {
3540
g.__dbPool = new Pool(poolConfig)
41+
42+
// Log connection acquisition and release for debugging
43+
g.__dbPool.on('connect', () => {
44+
console.log('[DB Pool] Client connected')
45+
})
46+
47+
g.__dbPool.on('remove', () => {
48+
console.log('[DB Pool] Client removed')
49+
})
50+
51+
g.__dbPool.on('error', (err) => {
52+
console.error('[DB Pool] Unexpected error on idle client', err)
53+
})
3654
}
3755

3856
export const dbPool = g.__dbPool
@@ -42,17 +60,19 @@ export const dbPool = g.__dbPool
4260
if (!g.__dbPoolShutdownRegistered) {
4361
const shutdown = async () => {
4462
try {
63+
console.log('[DB Pool] Shutting down...')
4564
if (g.__dbPool) {
4665
await g.__dbPool.end()
66+
console.log('[DB Pool] Shutdown complete')
4767
}
4868
} catch (err) {
49-
// ignore
69+
console.error('[DB Pool] Error during shutdown', err)
5070
}
5171
}
5272

5373
process.on('SIGINT', shutdown)
5474
process.on('SIGTERM', shutdown)
55-
process.on('exit', shutdown)
75+
process.on('beforeExit', shutdown)
5676

5777
g.__dbPoolShutdownRegistered = true
5878
}

lib/utils.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,20 @@ function ensureDb() {
1919
if (!dbPool) throw new Error("Database pool not initialized")
2020
}
2121

22-
export async function insertStatusLog(projectSlug: string, routePath: string, statusCode: number, responseTime?: number) {
22+
// Wrapper to execute queries with automatic connection cleanup
23+
async function executeQuery<T = any>(query: string, params: any[]): Promise<T> {
2324
ensureDb()
25+
const client = await dbPool.connect()
26+
try {
27+
const result = await client.query(query, params)
28+
return result as T
29+
} finally {
30+
// Always release the connection back to the pool
31+
client.release()
32+
}
33+
}
2434

35+
export async function insertStatusLog(projectSlug: string, routePath: string, statusCode: number, responseTime?: number) {
2536
// Insert a single row into status_logs. Store response_time when available.
2637
const q = `INSERT INTO status_logs (project_slug, route_path, status_code, response_time)
2738
VALUES ($1, $2, $3, $4)`
@@ -30,8 +41,8 @@ export async function insertStatusLog(projectSlug: string, routePath: string, st
3041

3142
try {
3243
console.log(`[DB] insertStatusLog: project=${projectSlug}, route=${routePath}, status=${statusCode}, response_time=${dbResponseTime}`)
33-
const res = await dbPool.query(q, [projectSlug, routePath, statusCode, dbResponseTime])
34-
console.log(`[DB] insertStatusLog: query ok, rowsAffected=${(res as any)?.rowCount ?? 'unknown'}`)
44+
const res = await executeQuery(q, [projectSlug, routePath, statusCode, dbResponseTime])
45+
console.log('[DB] insertStatusLog: query ok, rowsAffected=', (res as any)?.rowCount ?? 'unknown')
3546
} catch (err) {
3647
console.error('[DB] insertStatusLog: query failed', err)
3748
throw err
@@ -40,15 +51,13 @@ export async function insertStatusLog(projectSlug: string, routePath: string, st
4051

4152
// Read recent logs for a route. Returns last N records ordered newest-first
4253
export async function getRecentRouteLogs(projectSlug: string, routePath: string, limit = 20): Promise<StatusLog[]> {
43-
ensureDb()
44-
4554
const q = `SELECT created_at, status_code, response_time
4655
FROM status_logs
4756
WHERE project_slug = $1
4857
AND route_path = $2
4958
ORDER BY created_at DESC
5059
LIMIT $3`
51-
const res = await dbPool.query(q, [projectSlug, routePath, limit])
60+
const res = await executeQuery(q, [projectSlug, routePath, limit])
5261

5362
// Map rows to StatusLog[] (newest first)
5463
return res.rows.map((r: any) => ({
@@ -114,7 +123,12 @@ export async function getRouteStatus(projectSlug: string, route: Route): Promise
114123
}
115124

116125
export async function getProjectStatus(projectSlug: string, routes: Route[]): Promise<ProjectStatus> {
117-
const routeStatuses = await Promise.all(routes.map((r) => getRouteStatus(projectSlug, r)))
126+
// Process routes serially to avoid overwhelming the single DB connection
127+
const routeStatuses: RouteStatus[] = []
128+
for (const route of routes) {
129+
const status = await getRouteStatus(projectSlug, route)
130+
routeStatuses.push(status)
131+
}
118132

119133
// Determine overall status: worst status among routes
120134
const statusOrder: Record<RouteStatus['currentStatus'] | ProjectStatus['overallStatus'], number> = {

0 commit comments

Comments
 (0)