diff --git a/Dockerfile b/Dockerfile index 57784cf..a6edfef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Dockerfile for Next.js 15 + Playwright + Puppeteer/Chromium + Prisma + Tailwind + OpenAI +# Dockerfile for Next.js 15 + Puppeteer/Chromium + Prisma + Tailwind + OpenAI FROM node:20-slim # Use build arguments for CPU optimization @@ -10,7 +10,7 @@ ENV JOBS=${JOBS} ENV NODE_OPTIONS=${NODE_OPTIONS} ENV npm_config_jobs=${JOBS} -# Install system dependencies for Chromium and Playwright +# Install system dependencies for Chromium RUN apt-get update && apt-get install -y \ wget \ ca-certificates \ @@ -59,9 +59,6 @@ RUN npm config set maxsockets 8 && \ npm config set fetch-retries 3 && \ npm ci --no-audit --no-fund --prefer-offline -# Install Playwright browsers and dependencies with parallel downloads -RUN npx playwright install --with-deps chromium - # Copy the rest of the app COPY . . diff --git a/app/api/health/route.js b/app/api/health/route.js new file mode 100644 index 0000000..f6086ae --- /dev/null +++ b/app/api/health/route.js @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server' + +export async function GET() { + return NextResponse.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + service: 'AI Trading Bot', + version: '1.0.0' + }) +} diff --git a/lib/tradingview-automation.ts b/lib/tradingview-automation.ts index f3a3030..226e18a 100644 --- a/lib/tradingview-automation.ts +++ b/lib/tradingview-automation.ts @@ -192,11 +192,64 @@ export class TradingViewAutomation { async checkLoginStatus(): Promise { if (!this.page) throw new Error('Page not initialized') - console.log('CHECKING: Login status with 5 detection strategies...') + console.log('CHECKING: Login status with 6 detection strategies...') try { - // Strategy 1: Check for user account indicators (positive indicators) - console.log('CHECKING: Strategy 1: Checking for user account indicators...') + // Strategy 1: Check JavaScript authentication variables (most reliable) + console.log('CHECKING: Strategy 1: Checking JavaScript authentication variables...') + const authStatus = await this.page.evaluate(() => { + // Check if window.is_authenticated exists and is true + const w = window as any + + // Enhanced debugging - capture all relevant variables + const result = { + is_authenticated: w.is_authenticated, + user: w.user, + hasUser: typeof w.user === 'object' && w.user !== null, + authType: typeof w.is_authenticated, + userType: typeof w.user + } + + console.log('🔍 JavaScript auth detection:', { + is_authenticated: result.is_authenticated, + authType: result.authType, + hasUser: result.hasUser, + userType: result.userType, + userExists: !!w.user, + username: w.user?.username || 'N/A' + }) + + if (typeof w.is_authenticated === 'boolean') { + return { + isAuthenticated: w.is_authenticated, + hasUser: typeof w.user === 'object' && w.user !== null, + username: w.user?.username || null + } + } + return null + }) + + console.log('🔍 Auth status result:', authStatus) + + if (authStatus) { + if (authStatus.isAuthenticated && authStatus.hasUser) { + console.log(`SUCCESS: JavaScript indicates user is authenticated as "${authStatus.username}"`) + return true + } else { + console.log('INFO: JavaScript indicates user is not authenticated') + console.log('🔍 Auth details:', { + isAuthenticated: authStatus.isAuthenticated, + hasUser: authStatus.hasUser, + username: authStatus.username + }) + return false + } + } else { + console.log('INFO: No JavaScript authentication variables found') + } + + // Strategy 2: Check for user account indicators (positive indicators) + console.log('CHECKING: Strategy 2: Checking for user account indicators...') await this.takeDebugScreenshot('login_status_check') const userIndicators = [ @@ -221,8 +274,8 @@ export class TradingViewAutomation { } } - // Strategy 2: Check for anonymous/sign-in indicators (negative indicators) - console.log('CHECKING: Strategy 2: Checking for anonymous/sign-in indicators...') + // Strategy 3: Check for anonymous/sign-in indicators (negative indicators) + console.log('CHECKING: Strategy 3: Checking for anonymous/sign-in indicators...') const anonymousIndicators = [ '.tv-header__user-menu-button--anonymous', '[data-name="header-user-menu-sign-in"]', @@ -245,16 +298,16 @@ export class TradingViewAutomation { } } - // Strategy 3: Check URL patterns - console.log('CHECKING: Strategy 3: Checking URL patterns...') + // Strategy 4: Check URL patterns + console.log('CHECKING: Strategy 4: Checking URL patterns...') const currentUrl = this.page.url() if (currentUrl.includes('/signin') || currentUrl.includes('/login')) { console.log('ERROR: On login page - not logged in') return false } - // Strategy 4: Check authentication cookies - console.log('CHECKING: Strategy 4: Checking authentication cookies...') + // Strategy 5: Check authentication cookies + console.log('CHECKING: Strategy 5: Checking authentication cookies...') const cookies = await this.page.cookies() const authCookies = cookies.filter(cookie => cookie.name.includes('auth') || @@ -265,8 +318,8 @@ export class TradingViewAutomation { console.log('WARNING: No authentication cookies found') } - // Strategy 5: Check for personal content - console.log('CHECKING: Strategy 5: Checking for personal content...') + // Strategy 6: Check for personal content + console.log('CHECKING: Strategy 6: Checking for personal content...') const personalContentSelectors = [ '[data-name="watchlist"]', '.tv-header__watchlist', @@ -315,59 +368,325 @@ export class TradingViewAutomation { console.log('🔐 Starting login process...') - // Navigate to login page - console.log('📄 Navigating to TradingView login page...') - await this.page.goto('https://www.tradingview.com/accounts/signin/', { - waitUntil: 'domcontentloaded', - timeout: 30000 - }) + // Navigate to main TradingView page first, then find login + console.log('📄 Navigating to TradingView main page...') + + try { + await this.page.goto('https://www.tradingview.com/', { + waitUntil: 'domcontentloaded', + timeout: 30000 + }) + + await sleep(3000) + + // Look for sign in buttons on main page + console.log('🔍 Looking for Sign In button on main page...') + const signinButtons = [ + 'a[href*="signin"]', + 'a[href*="accounts/signin"]', + '[data-name="header-user-menu-sign-in"]', + '.tv-header__user-menu-button', + '.tv-header__user-menu-button--anonymous', + '.js-signin-switch', + 'button[data-role="button"]', + 'button.tv-button', + 'a.tv-button' + ] + + let foundSignin = false + for (const selector of signinButtons) { + try { + const elements = await this.page.$$(selector) + + for (const element of elements) { + const isVisible = await element.boundingBox() + if (isVisible) { + const text = await element.evaluate(el => { + const elem = el as HTMLElement + return elem.textContent || elem.getAttribute('title') || elem.getAttribute('href') || '' + }) + console.log(`🎯 Found signin element: ${selector} with text: "${text}"`) + + // Check if this looks like a sign in button + const lowerText = text.toLowerCase() + if (lowerText.includes('sign in') || lowerText.includes('login') || + lowerText.includes('signin') || selector.includes('signin') || + selector.includes('user-menu-button')) { + + await element.click() + console.log('✅ Clicked sign in button') + await sleep(5000) // Wait for login page/modal to load + foundSignin = true + break + } + } + } + if (foundSignin) break + } catch (e) { + const error = e as Error + console.log(`❌ Error with selector ${selector}: ${error.message}`) + continue + } + } + + // If no selector-based approach works, try finding by text content + if (!foundSignin) { + console.log('🔍 Trying to find sign-in button by text content...') + const clickableElements = await this.page.$$('button, a, div[role="button"], span[role="button"]') + + for (const element of clickableElements) { + try { + const isVisible = await element.boundingBox() + if (isVisible) { + const text = await element.evaluate(el => { + const elem = el as HTMLElement + return (elem.textContent || '').toLowerCase().trim() + }) + + if (text.includes('sign in') || text.includes('signin') || text.includes('log in') || text.includes('login')) { + console.log(`🎯 Found sign-in button with text: "${text}"`) + await element.click() + console.log('✅ Clicked sign-in button by text') + await sleep(5000) + foundSignin = true + break + } + } + } catch (e) { + continue + } + } + } + + if (!foundSignin) { + console.log('⚠️ No sign in button found on main page, trying direct navigation...') + await this.page.goto('https://www.tradingview.com/accounts/signin/', { + waitUntil: 'domcontentloaded', + timeout: 30000 + }) + } + + } catch (e) { + const error = e as Error + console.log(`❌ Navigation error: ${error.message}`) + // Fallback to direct login URL + await this.page.goto('https://www.tradingview.com/accounts/signin/', { + waitUntil: 'domcontentloaded', + timeout: 30000 + }) + } await sleep(3000) await this.takeDebugScreenshot('login_page_loaded') + + // Check current URL and page state + const currentUrl = await this.page.url() + const pageTitle = await this.page.title() + console.log(`🌐 Current URL: ${currentUrl}`) + console.log(`📋 Page title: ${pageTitle}`) // Wait for login form console.log('⏳ Waiting for login form...') await sleep(5000) - // Look for email login option + // Look for email login option with comprehensive approach console.log('CHECKING: Looking for Email login option...') - const emailTriggers = [ - 'button[data-overflow-tooltip-text="Email"]', - 'button:contains("Email")', - 'button:contains("email")', - '[data-name="email"]' + // Try to find email trigger button using XPath and text content + let emailFormVisible = false + + try { + // First try standard selectors + const emailTriggers = [ + 'button[data-overflow-tooltip-text="Email"]', + '[data-name="email"]', + '.tv-signin-dialog__toggle-email', + '.js-signin__email', + '[data-testid="email-signin"]', + 'button[class*="email"]', + '.signin-form__toggle' + ] + + for (const trigger of emailTriggers) { + try { + const elements = await this.page.$$(trigger) + for (const element of elements) { + const isVisible = await element.boundingBox() + if (isVisible) { + const text = await element.evaluate(el => { + const elem = el as HTMLElement + return elem.textContent || elem.getAttribute('data-overflow-tooltip-text') || elem.getAttribute('title') || '' + }) + console.log(`TARGET: Found email trigger: ${trigger} with text: "${text}"`) + await element.click() + console.log('SUCCESS: Clicked email trigger') + await sleep(3000) + emailFormVisible = true + break + } + } + if (emailFormVisible) break + } catch (e) { + continue + } + } + + // If no standard selectors work, try finding buttons by text content + if (!emailFormVisible) { + console.log('🔍 Trying to find email button by text content...') + const buttons = await this.page.$$('button, span, div[role="button"], a') + + for (const button of buttons) { + try { + const isVisible = await button.boundingBox() + if (isVisible) { + const text = await button.evaluate(el => { + const elem = el as HTMLElement + return (elem.textContent || '').toLowerCase().trim() + }) + + if (text.includes('email') || text.includes('continue with email')) { + console.log(`🎯 Found email button with text: "${text}"`) + await button.click() + console.log('✅ Clicked email button by text') + await sleep(3000) + emailFormVisible = true + break + } + } + } catch (e) { + continue + } + } + } + + } catch (e) { + console.log('⚠️ Error finding email trigger:', e) + } + + // If no email trigger found, check if we're in a modal or different login flow + if (!emailFormVisible) { + console.log('INFO: No email trigger found, checking for alternative login flows...') + + // Wait a bit more for any modals to fully load + await sleep(5000) + await this.takeDebugScreenshot('checking_modal_login') + + // Check for modal dialogs or different login containers + const modalSelectors = [ + '[role="dialog"]', + '.tv-dialog', + '.tv-dialog__wrapper', + '.js-dialog', + '.signin-dialog', + '.auth-modal', + '.modal', + '[data-testid="auth-modal"]' + ] + + for (const modalSelector of modalSelectors) { + try { + const modal = await this.page.$(modalSelector) + if (modal) { + console.log(`📋 Found modal: ${modalSelector}`) + // Look for email options within the modal + const modalEmailButtons = await modal.$$('button, a, div[role="button"]') + + for (const button of modalEmailButtons) { + try { + const isVisible = await button.boundingBox() + if (isVisible) { + const text = await button.evaluate(el => { + const elem = el as HTMLElement + return (elem.textContent || '').toLowerCase().trim() + }) + + console.log(`🎯 Modal button text: "${text}"`) + if (text.includes('email') || text.includes('continue with email') || + text.includes('sign in with email') || text.includes('use email')) { + console.log(`✅ Found modal email button: "${text}"`) + await button.click() + console.log('✅ Clicked modal email button') + await sleep(3000) + emailFormVisible = true + break + } + } + } catch (e) { + continue + } + } + if (emailFormVisible) break + } + } catch (e) { + continue + } + } + } + + if (!emailFormVisible) { + console.log('INFO: Still no email form, assuming email form is already visible') + } + + await this.takeDebugScreenshot('after_email_trigger') + + // Fill email - updated selectors for current TradingView + const emailInputSelectors = [ + 'input[type="email"]', + 'input[name*="email"]', + 'input[name="username"]', + 'input[name="id_username"]', + 'input[placeholder*="email" i]', + 'input[placeholder*="Email" i]', + 'input[autocomplete="username"]', + 'input[data-name="email"]', + 'input[class*="email"]', + '#id_username', + '#email', + '[data-testid="email-input"]', + '[data-testid="username-input"]' ] - let emailFormVisible = false - for (const trigger of emailTriggers) { + // Debug: Check what's actually on the page + await this.takeDebugScreenshot('before_email_search') + + // Get all input elements to debug + const allInputs = await this.page.$$('input') + console.log(`DEBUG: Found ${allInputs.length} input elements on page`) + + for (let i = 0; i < Math.min(allInputs.length, 10); i++) { try { - const element = await this.page.$(trigger) - if (element) { - const isVisible = await element.boundingBox() - if (isVisible) { - console.log("TARGET: Found email trigger: " + trigger) - await element.click() - console.log('SUCCESS: Clicked email trigger') - await sleep(3000) - emailFormVisible = true - break + const inputType = await allInputs[i].evaluate(el => el.type) + const inputName = await allInputs[i].evaluate(el => el.name) + const inputPlaceholder = await allInputs[i].evaluate(el => el.placeholder) + const inputId = await allInputs[i].evaluate(el => el.id) + const inputClass = await allInputs[i].evaluate(el => el.className) + const inputValue = await allInputs[i].evaluate(el => el.value) + const isVisible = await allInputs[i].boundingBox() + + console.log(`INPUT ${i}: type="${inputType}", name="${inputName}", placeholder="${inputPlaceholder}", id="${inputId}", class="${inputClass}", value="${inputValue}", visible=${!!isVisible}`) + + // Check if this looks like an email/username field even if it doesn't match selectors + if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) { + const lowerPlaceholder = (inputPlaceholder || '').toLowerCase() + const lowerName = (inputName || '').toLowerCase() + const lowerClass = (inputClass || '').toLowerCase() + + if (lowerPlaceholder.includes('email') || lowerPlaceholder.includes('username') || + lowerName.includes('email') || lowerName.includes('username') || + lowerClass.includes('email') || lowerClass.includes('username')) { + console.log(`🎯 INPUT ${i} looks like email field based on attributes`) } } } catch (e) { - continue + const error = e as Error + console.log(`INPUT ${i}: Error reading attributes - ${error.message}`) } } - - // Fill email - const emailInputSelectors = [ - 'input[type="email"]', - 'input[name*="email"]', - 'input[name="username"]', - 'input[placeholder*="email" i]' - ] let emailInput = null + + // First try specific selectors for (const selector of emailInputSelectors) { try { emailInput = await this.page.$(selector) @@ -381,10 +700,76 @@ export class TradingViewAutomation { } catch (e) { continue } + emailInput = null // Reset if not visible + } + + // If no specific selector worked, try to find the first visible text input + if (!emailInput) { + console.log('🔍 No specific email input found, trying first visible text input...') + for (const input of allInputs) { + try { + const isVisible = await input.boundingBox() + const inputType = await input.evaluate(el => el.type) + + if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) { + console.log('🎯 Found first visible text input, assuming it\'s email field') + emailInput = input + break + } + } catch (e) { + continue + } + } + } + + // Last resort: try to find inputs by looking for labels or surrounding text + if (!emailInput) { + console.log('🔍 Last resort: searching for inputs near email-related text...') + + // Look for text that says "email" and find nearby inputs + const emailTexts = await this.page.evaluate(() => { + const allElements = Array.from(document.querySelectorAll('*')) + return allElements + .filter(el => { + const text = (el.textContent || '').toLowerCase() + return text.includes('email') || text.includes('username') || text.includes('sign in') + }) + .map(el => { + const rect = el.getBoundingClientRect() + return { + text: (el.textContent || '').trim(), + x: rect.x, + y: rect.y, + tag: el.tagName + } + }) + }) + + console.log('📋 Found email-related text elements:', emailTexts.slice(0, 5)) + + // Try to find any input that's not already found + for (const input of allInputs) { + try { + const isVisible = await input.boundingBox() + if (isVisible) { + console.log('🎯 Using any visible input as potential email field') + emailInput = input + break + } + } catch (e) { + continue + } + } } if (!emailInput) { - throw new Error('Could not find email input field') + // Additional debug info + const currentUrl = await this.page.url() + const title = await this.page.title() + console.log(`❌ Email input not found on: ${currentUrl}`) + console.log(`❌ Page title: ${title}`) + await this.takeDebugScreenshot('email_input_not_found') + throw new Error(`Could not find email input field on ${currentUrl}. Found ${allInputs.length} inputs total.`) } await emailInput.click()