diff --git a/Dockerfile b/Dockerfile index 9feb192..57784cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,6 +68,9 @@ COPY . . # Generate Prisma client RUN npx prisma generate +# Build the Next.js application for production +RUN npm run build + # Fix permissions for node_modules binaries RUN chmod +x node_modules/.bin/* @@ -78,5 +81,5 @@ EXPOSE 3000 ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium -# Start the app +# Start the app (default to development mode) CMD ["npm", "run", "dev:docker"] diff --git a/app/api/analyze/route.ts b/app/api/analyze/route.ts index 53b72fa..f81dbea 100644 --- a/app/api/analyze/route.ts +++ b/app/api/analyze/route.ts @@ -48,8 +48,11 @@ export async function POST(req: NextRequest) { } } else { // Original behavior - capture new screenshots - const baseFilename = `${finalSymbol}_${finalTimeframe}_${Date.now()}` - screenshots = await enhancedScreenshotService.capture(finalSymbol, `${baseFilename}.png`, finalLayouts, finalTimeframe) + screenshots = await enhancedScreenshotService.captureWithLogin({ + symbol: finalSymbol, + timeframe: finalTimeframe, + layouts: finalLayouts + }) } let result diff --git a/app/api/drift/transaction-history/route.ts b/app/api/drift/transaction-history/route.ts index 596530e..85839b1 100644 --- a/app/api/drift/transaction-history/route.ts +++ b/app/api/drift/transaction-history/route.ts @@ -1,7 +1,5 @@ import { NextResponse } from 'next/server' -import { Connection, PublicKey } from '@solana/web3.js' -import { Wallet } from '@coral-xyz/anchor' -import { Keypair } from '@solana/web3.js' +import { Connection, PublicKey, Keypair } from '@solana/web3.js' export async function GET(request: Request) { try { @@ -19,14 +17,13 @@ export async function GET(request: Request) { // Convert private key to Keypair const privateKeyBytes = JSON.parse(privateKeyString) const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyBytes)) - const wallet = new Wallet(keypair) // Connect to Helius RPC const connection = new Connection(process.env.HELIUS_RPC_ENDPOINT || 'https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY') // Get transaction signatures for this wallet const signatures = await connection.getSignaturesForAddress( - wallet.publicKey, + keypair.publicKey, { limit: limit * 2 } // Get more signatures to filter for Drift transactions ) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 621a38e..0fe5ebb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -8,18 +8,29 @@ services: - NODE_ENV=production - DOCKER_ENV=true + # Load production environment variables + env_file: + - .env.production + # Production command command: ["npm", "start"] # Only expose necessary port ports: - - "3000:3000" + - "9000:3000" + + # Remove network_mode: host to avoid port conflicts + # Use bridge network instead + networks: + - default # Production volumes (no source code mounting) volumes: - ./screenshots:/app/screenshots - ./videos:/app/videos - - ./.env.production:/app/.env + - ./.tradingview-session:/app/.tradingview-session + - ./prisma:/app/prisma + - /tmp/.X11-unix:/tmp/.X11-unix:rw # Production labels labels: diff --git a/docker-compose.yml b/docker-compose.yml index 12d8750..d9d0560 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: # Playwright/TradingView automation settings - CHROMIUM_PATH=/usr/bin/chromium - DISABLE_CHROME_SANDBOX=true - - DISPLAY=${DISPLAY:-:0} + - DISPLAY=$${DISPLAY:-:0} # CAPTCHA handling - ALLOW_MANUAL_CAPTCHA=true # Database configuration @@ -36,8 +36,14 @@ services: # X11 forwarding for GUI display (when ALLOW_MANUAL_CAPTCHA=true) - /tmp/.X11-unix:/tmp/.X11-unix:rw + # Port mapping - expose Next.js on port 9000 + ports: + - "9000:3000" + # X11 and display configuration for manual CAPTCHA solving - network_mode: host + # Use bridge network instead of host for better port management + networks: + - default privileged: true # Health check diff --git a/lib/enhanced-screenshot.ts b/lib/enhanced-screenshot.ts index e69de29..32604fd 100644 --- a/lib/enhanced-screenshot.ts +++ b/lib/enhanced-screenshot.ts @@ -0,0 +1,389 @@ +import { tradingViewAutomation, TradingViewAutomation, TradingViewCredentials, NavigationOptions } from './tradingview-automation' +import fs from 'fs/promises' +import path from 'path' +import puppeteer from 'puppeteer' +import { Browser, Page } from 'puppeteer' + +export interface ScreenshotConfig { + symbol: string + timeframe: string + layouts?: string[] // Multiple chart layouts if needed + credentials?: TradingViewCredentials // Optional if using .env +} + +// Layout URL mappings for direct navigation +const LAYOUT_URLS = { + 'ai': 'Z1TzpUrf', + 'diy': 'vWVvjLhP', + 'Diy module': 'vWVvjLhP' // Alternative mapping for 'Diy module' +} + +export class EnhancedScreenshotService { + private static readonly OPERATION_TIMEOUT = 120000 // 2 minutes timeout for Docker + private static aiSession: TradingViewAutomation | null = null + private static diySession: TradingViewAutomation | null = null + + async captureWithLogin(config: ScreenshotConfig): Promise { + console.log('πŸš€ Enhanced Screenshot Service - Docker Environment (Dual Session)') + console.log('πŸ“‹ Config:', config) + + const screenshotFiles: string[] = [] + + try { + // Ensure screenshots directory exists + const screenshotsDir = path.join(process.cwd(), 'screenshots') + await fs.mkdir(screenshotsDir, { recursive: true }) + + const timestamp = Date.now() + const layoutsToCapture = config.layouts || ['ai', 'diy'] + + console.log(`\nπŸ”„ Starting parallel capture of ${layoutsToCapture.length} layouts...`) + + // Create parallel session promises for true dual-session approach + const sessionPromises = layoutsToCapture.map(async (layout) => { + const layoutKey = layout.toLowerCase() + let layoutSession: TradingViewAutomation | null = null + + try { + console.log(`\nπŸ”§ Initializing ${layout.toUpperCase()} session (parallel)...`) + + // Get layout URL with better error handling + let layoutUrl = LAYOUT_URLS[layoutKey as keyof typeof LAYOUT_URLS] + + // Try alternative key for 'Diy module' + if (!layoutUrl && layout === 'Diy module') { + layoutUrl = LAYOUT_URLS['diy'] + } + + if (!layoutUrl) { + throw new Error(`No URL mapping found for layout: ${layout} (tried keys: ${layoutKey}, diy)`) + } + + console.log(`πŸ—ΊοΈ ${layout.toUpperCase()}: Using layout URL ${layoutUrl}`) + + // Create a dedicated automation instance for this layout + layoutSession = new TradingViewAutomation() + + console.log(`🐳 Starting ${layout} browser session...`) + await layoutSession.init() + + // Check login status and login if needed + const isLoggedIn = await layoutSession.isLoggedIn() + if (!isLoggedIn) { + console.log(`πŸ” Logging in to ${layout} session...`) + const loginSuccess = await layoutSession.smartLogin(config.credentials) + if (!loginSuccess) { + throw new Error(`Failed to login to ${layout} session`) + } + } else { + console.log(`βœ… ${layout} session already logged in`) + } + + // Navigate directly to the specific layout URL with symbol and timeframe + const directUrl = `https://www.tradingview.com/chart/${layoutUrl}/?symbol=${config.symbol}&interval=${config.timeframe}` + console.log(`🌐 ${layout.toUpperCase()}: Navigating directly to ${directUrl}`) + + // Get page from the session + const page = (layoutSession as any).page + if (!page) { + throw new Error(`Failed to get page for ${layout} session`) + } + + // Navigate directly to the layout URL with retries and progressive timeout strategy + let navigationSuccess = false + for (let attempt = 1; attempt <= 3; attempt++) { + try { + console.log(`πŸ”„ ${layout.toUpperCase()}: Navigation attempt ${attempt}/3`) + + // Progressive waiting strategy: first try domcontentloaded, then networkidle if that fails + const waitUntilStrategy = attempt === 1 ? 'domcontentloaded' : 'networkidle0' + const timeoutDuration = attempt === 1 ? 30000 : (60000 + (attempt - 1) * 30000) + + console.log(`πŸ“‹ ${layout.toUpperCase()}: Using waitUntil: ${waitUntilStrategy}, timeout: ${timeoutDuration}ms`) + + await page.goto(directUrl, { + waitUntil: waitUntilStrategy, + timeout: timeoutDuration + }) + + // If we used domcontentloaded, wait a bit more for dynamic content + if (waitUntilStrategy === 'domcontentloaded') { + console.log(`⏳ ${layout.toUpperCase()}: Waiting additional 5s for dynamic content...`) + await new Promise(resolve => setTimeout(resolve, 5000)) + } + + navigationSuccess = true + break + } catch (navError: any) { + console.warn(`⚠️ ${layout.toUpperCase()}: Navigation attempt ${attempt} failed:`, navError?.message || navError) + if (attempt === 3) { + throw new Error(`Failed to navigate to ${layout} layout after 3 attempts: ${navError?.message || navError}`) + } + // Progressive backoff + const waitTime = 2000 * attempt + console.log(`⏳ ${layout.toUpperCase()}: Waiting ${waitTime}ms before retry...`) + await new Promise(resolve => setTimeout(resolve, waitTime)) + } + } + + if (!navigationSuccess) { + throw new Error(`Failed to navigate to ${layout} layout`) + } + + console.log(`βœ… ${layout.toUpperCase()}: Successfully navigated to layout`) + + // Progressive loading strategy: shorter initial wait, then chart-specific wait + console.log(`⏳ ${layout.toUpperCase()}: Initial page stabilization (2s)...`) + await new Promise(resolve => setTimeout(resolve, 2000)) + + // Wait for chart to load with multiple strategies + console.log(`⏳ ${layout.toUpperCase()}: Waiting for chart to load...`) + let chartLoadSuccess = false + + try { + // Strategy 1: Use built-in chart data waiter (with shorter timeout) + await Promise.race([ + layoutSession.waitForChartData(), + new Promise((_, reject) => setTimeout(() => reject(new Error('Chart data timeout')), 30000)) + ]) + console.log(`βœ… ${layout.toUpperCase()}: Chart data loaded successfully`) + chartLoadSuccess = true + } catch (chartError: any) { + console.warn(`⚠️ ${layout.toUpperCase()}: Chart data wait failed:`, chartError?.message || chartError) + + // Strategy 2: Look for chart elements manually + try { + console.log(`πŸ” ${layout.toUpperCase()}: Checking for chart elements manually...`) + await page.waitForSelector('.layout__area--center', { timeout: 15000 }) + console.log(`βœ… ${layout.toUpperCase()}: Chart area found via selector`) + chartLoadSuccess = true + } catch (selectorError: any) { + console.warn(`⚠️ ${layout.toUpperCase()}: Chart selector check failed:`, selectorError?.message || selectorError) + } + } + + if (!chartLoadSuccess) { + console.warn(`⚠️ ${layout.toUpperCase()}: Chart loading uncertain, proceeding with fallback wait...`) + await new Promise(resolve => setTimeout(resolve, 8000)) + } else { + // Additional stabilization wait after chart loads + console.log(`⏳ ${layout.toUpperCase()}: Chart stabilization (3s)...`) + await new Promise(resolve => setTimeout(resolve, 3000)) + } + + // Take screenshot with better error handling + const filename = `${config.symbol}_${config.timeframe}_${layout}_${timestamp}.png` + console.log(`πŸ“Έ Taking ${layout} screenshot: ${filename}`) + + let screenshotFile = null + try { + screenshotFile = await layoutSession.takeScreenshot(filename) + if (screenshotFile) { + console.log(`βœ… ${layout} screenshot captured: ${screenshotFile}`) + } else { + throw new Error(`Screenshot file was not created for ${layout}`) + } + } catch (screenshotError: any) { + console.error(`❌ ${layout.toUpperCase()}: Screenshot failed:`, screenshotError?.message || screenshotError) + throw new Error(`Failed to capture ${layout} screenshot: ${screenshotError?.message || screenshotError}`) + } + + // Store session for potential reuse + if (layout === 'ai' || layoutKey === 'ai') { + EnhancedScreenshotService.aiSession = layoutSession + } else if (layout === 'diy' || layoutKey === 'diy' || layout === 'Diy module') { + EnhancedScreenshotService.diySession = layoutSession + } + + return screenshotFile + + } catch (error: any) { + console.error(`❌ Error capturing ${layout} layout:`, error?.message || error) + console.error(`❌ Full ${layout} error details:`, error) + console.error(`❌ ${layout} error stack:`, error?.stack) + + // Attempt to capture browser state for debugging + try { + const page = (layoutSession as any)?.page + if (page) { + const url = await page.url() + const title = await page.title() + console.error(`❌ ${layout} browser state - URL: ${url}, Title: ${title}`) + + // Try to get page content for debugging + const bodyText = await page.evaluate(() => document.body.innerText.slice(0, 200)) + console.error(`❌ ${layout} page content preview:`, bodyText) + } + } catch (debugError: any) { + console.error(`❌ Failed to capture ${layout} browser state:`, debugError?.message || debugError) + } + + throw error // Re-throw to be caught by Promise.allSettled + } + }) + + // Execute all sessions in parallel and wait for completion + console.log(`\n⚑ Executing ${layoutsToCapture.length} sessions in parallel...`) + const results = await Promise.allSettled(sessionPromises) + + // Collect successful screenshots + results.forEach((result, index) => { + const layout = layoutsToCapture[index] + if (result.status === 'fulfilled' && result.value) { + screenshotFiles.push(result.value) + console.log(`βœ… ${layout} parallel session completed successfully`) + } else { + console.error(`❌ ${layout} parallel session failed:`, result.status === 'rejected' ? result.reason : 'Unknown error') + } + }) + + console.log(`\n🎯 Parallel capture completed: ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`) + return screenshotFiles + + } catch (error) { + console.error('Enhanced parallel screenshot capture failed:', error) + throw error + } + } + + async captureQuick(symbol: string, timeframe: string, credentials?: TradingViewCredentials): Promise { + try { + console.log(`Starting quick screenshot capture for ${symbol} ${timeframe}...`); + + // Use the existing captureWithLogin method with a single default layout + const config: ScreenshotConfig = { + symbol, + timeframe, + layouts: ['ai'], // Default to AI layout for quick capture + credentials + }; + + const screenshots = await this.captureWithLogin(config); + + // Return the first screenshot path or null if none captured + return screenshots.length > 0 ? screenshots[0] : null; + + } catch (error) { + console.error('Error in quick screenshot capture:', error); + return null; + } + } + + async capture(symbol: string, filename: string): Promise { + try { + console.log(`Starting enhanced screenshot capture for ${symbol}...`); + + // Launch browser + const browser = await puppeteer.launch({ + headless: true, + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--no-first-run', + '--no-zygote', + '--disable-gpu' + ] + }); + + const page = await browser.newPage(); + await page.setViewport({ width: 1920, height: 1080 }); + + // Navigate to TradingView chart + await page.goto('https://www.tradingview.com/chart/', { + waitUntil: 'networkidle0', + timeout: 30000 + }); + + // Wait for chart to load + await page.waitForSelector('canvas', { timeout: 30000 }); + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Ensure screenshots directory exists + const screenshotsDir = path.join(process.cwd(), 'screenshots') + await fs.mkdir(screenshotsDir, { recursive: true }) + + // Take screenshot + const screenshotPath = path.join(screenshotsDir, filename); + await page.screenshot({ + path: screenshotPath as `${string}.png`, + type: 'png', + fullPage: false + }); + + await browser.close(); + + console.log(`Screenshot saved to: ${screenshotPath}`); + return [screenshotPath]; + } catch (error) { + console.error('Error capturing screenshot:', error); + throw error; + } + } + + async cleanup(): Promise { + console.log('🧹 Cleaning up parallel browser sessions...') + + const cleanupPromises = [] + + // Cleanup dedicated AI session if exists + if (EnhancedScreenshotService.aiSession) { + console.log('πŸ”§ Cleaning up AI session...') + cleanupPromises.push( + EnhancedScreenshotService.aiSession.close().catch((err: any) => + console.error('AI session cleanup error:', err) + ) + ) + EnhancedScreenshotService.aiSession = null + } + + // Cleanup dedicated DIY session if exists + if (EnhancedScreenshotService.diySession) { + console.log('πŸ”§ Cleaning up DIY session...') + cleanupPromises.push( + EnhancedScreenshotService.diySession.close().catch((err: any) => + console.error('DIY session cleanup error:', err) + ) + ) + EnhancedScreenshotService.diySession = null + } + + // Also cleanup the main singleton session + cleanupPromises.push( + tradingViewAutomation.close().catch((err: any) => + console.error('Main session cleanup error:', err) + ) + ) + + await Promise.allSettled(cleanupPromises) + console.log('βœ… All parallel browser sessions cleaned up') + } + + async healthCheck(): Promise<{ status: 'healthy' | 'error', message?: string }> { + try { + // Simple health check - try to launch a browser instance + const browser = await puppeteer.launch({ + headless: true, + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + ] + }); + + await browser.close(); + + return { status: 'healthy' }; + } catch (error) { + return { + status: 'error', + message: error instanceof Error ? error.message : 'Unknown error' + }; + } + } +} + +export const enhancedScreenshotService = new EnhancedScreenshotService() \ No newline at end of file diff --git a/lib/tradingview-automation.ts b/lib/tradingview-automation.ts index 4b6ffc8..4c61adb 100644 --- a/lib/tradingview-automation.ts +++ b/lib/tradingview-automation.ts @@ -2488,7 +2488,7 @@ export class TradingViewAutomation { // Fallback to full page screenshot console.log("⚠️ Chart area not found, taking full page screenshot") await this.page.screenshot({ - path: filePath, + path: filePath as `${string}.png`, fullPage: true, type: 'png' }) @@ -2517,7 +2517,7 @@ export class TradingViewAutomation { await fs.mkdir(path.dirname(filePath), { recursive: true }) await this.page.screenshot({ - path: filePath, + path: filePath as `${string}.png`, fullPage: true, type: 'png' })