fix: batch 4 — connection pooling, graceful shutdown, email verification UX
- Prisma: connection_limit=10, pool_timeout=30 on DATABASE_URL in both compose files - Graceful shutdown: SIGTERM/SIGINT forwarded to Node process in docker-entrypoint.sh - testEmailConnection: replaced real email send with transporter.verify(), simplified UI to single button - NotificationLog.userId index: confirmed already present, no change needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@ services:
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-mopc}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-mopc}
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-mopc}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-mopc}?connection_limit=10&pool_timeout=30
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-dev-secret-key-for-local-development-only}
|
||||
- AUTH_SECRET=${AUTH_SECRET:-dev-secret-key-for-local-development-only}
|
||||
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc
|
||||
- DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc?connection_limit=10&pool_timeout=30
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- AUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
|
||||
@@ -59,4 +59,18 @@ else
|
||||
fi
|
||||
|
||||
echo "==> Starting application..."
|
||||
exec node server.js
|
||||
|
||||
# Graceful shutdown: forward SIGTERM/SIGINT to the Node process
|
||||
# so in-flight requests can complete before the container exits.
|
||||
shutdown() {
|
||||
echo "==> Received shutdown signal, stopping gracefully..."
|
||||
kill -TERM "$NODE_PID" 2>/dev/null
|
||||
wait "$NODE_PID"
|
||||
exit $?
|
||||
}
|
||||
|
||||
trap shutdown TERM INT
|
||||
|
||||
node server.js &
|
||||
NODE_PID=$!
|
||||
wait "$NODE_PID"
|
||||
|
||||
@@ -11,6 +11,10 @@ generator client {
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
// connection_limit and pool_timeout are set via query params in DATABASE_URL:
|
||||
// ?connection_limit=10&pool_timeout=30
|
||||
// Defaults: connection_limit = num_cpus * 2 + 1, pool_timeout = 10s.
|
||||
// Override in .env for production to prevent connection exhaustion.
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
@@ -18,15 +17,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
const formSchema = z.object({
|
||||
smtp_host: z.string().min(1, 'SMTP host is required'),
|
||||
@@ -51,8 +41,6 @@ interface EmailSettingsFormProps {
|
||||
}
|
||||
|
||||
export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
||||
const [testDialogOpen, setTestDialogOpen] = useState(false)
|
||||
const [testEmail, setTestEmail] = useState('')
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@@ -77,17 +65,16 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
||||
},
|
||||
})
|
||||
|
||||
const sendTestEmail = trpc.settings.testEmailConnection.useMutation({
|
||||
const verifyConnection = trpc.settings.testEmailConnection.useMutation({
|
||||
onSuccess: (result) => {
|
||||
setTestDialogOpen(false)
|
||||
if (result.success) {
|
||||
toast.success('Test email sent successfully')
|
||||
toast.success('SMTP connection verified successfully')
|
||||
} else {
|
||||
toast.error(`Failed to send test email: ${result.error}`)
|
||||
toast.error(`SMTP verification failed: ${result.error}`)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Test failed: ${error.message}`)
|
||||
toast.error(`Verification failed: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -107,12 +94,8 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
||||
updateSettings.mutate({ settings: settingsToUpdate })
|
||||
}
|
||||
|
||||
const handleSendTest = () => {
|
||||
if (!testEmail) {
|
||||
toast.error('Please enter an email address')
|
||||
return
|
||||
}
|
||||
sendTestEmail.mutate({ testEmail })
|
||||
const handleVerifyConnection = () => {
|
||||
verifyConnection.mutate()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -243,49 +226,24 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Dialog open={testDialogOpen} onOpenChange={setTestDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="outline">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleVerifyConnection}
|
||||
disabled={verifyConnection.isPending}
|
||||
>
|
||||
{verifyConnection.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
Send Test Email
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Send Test Email</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter an email address to receive a test email
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="test@example.com"
|
||||
value={testEmail}
|
||||
onChange={(e) => setTestEmail(e.target.value)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setTestDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSendTest}
|
||||
disabled={sendTestEmail.isPending}
|
||||
>
|
||||
{sendTestEmail.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
'Send Test'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
Verify Connection
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -322,12 +322,11 @@ export const settingsRouter = router({
|
||||
* Test email connection
|
||||
*/
|
||||
testEmailConnection: superAdminProcedure
|
||||
.input(z.object({ testEmail: z.string().email() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
.mutation(async () => {
|
||||
try {
|
||||
const { sendTestEmail } = await import('@/lib/email')
|
||||
const success = await sendTestEmail(input.testEmail)
|
||||
return { success, error: success ? null : 'Failed to send test email' }
|
||||
const { verifyEmailConnection } = await import('@/lib/email')
|
||||
const success = await verifyEmailConnection()
|
||||
return { success, error: success ? null : 'SMTP connection verification failed' }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return { success: false, error: `Email configuration error: ${message}` }
|
||||
|
||||
Reference in New Issue
Block a user