- Use exact layout names: 'ai' and 'Diy module' (as in TradingView) - Update default selected layouts to ['ai', 'Diy module'] - Keep display names exactly as they appear in TradingView - Add lowercase fallback mapping in backend for 'diy module' - Remove normalization that was changing the exact names This ensures UI shows exactly what's in the TradingView account and selection state matches display.
441 lines
18 KiB
TypeScript
441 lines
18 KiB
TypeScript
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'
|
|
import { progressTracker, ProgressStep } from './progress-tracker'
|
|
|
|
export interface ScreenshotConfig {
|
|
symbol: string
|
|
timeframe: string
|
|
layouts?: string[] // Multiple chart layouts if needed
|
|
credentials?: TradingViewCredentials // Optional if using .env
|
|
sessionId?: string // For progress tracking
|
|
}
|
|
|
|
// Layout URL mappings for direct navigation
|
|
const LAYOUT_URLS = {
|
|
'ai': 'Z1TzpUrf',
|
|
'diy': 'vWVvjLhP',
|
|
'Diy module': 'vWVvjLhP', // Exact TradingView name
|
|
'diy module': 'vWVvjLhP' // Lowercase fallback
|
|
}
|
|
|
|
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<string[]> {
|
|
console.log('🚀 Enhanced Screenshot Service - Docker Environment (Dual Session)')
|
|
console.log('📋 Config:', config)
|
|
|
|
const screenshotFiles: string[] = []
|
|
const { sessionId } = config
|
|
console.log('🔍 Enhanced Screenshot Service received sessionId:', sessionId)
|
|
|
|
// Progress tracking (session already created in API)
|
|
if (sessionId) {
|
|
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting browser sessions...')
|
|
}
|
|
|
|
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...`)
|
|
|
|
if (sessionId) {
|
|
progressTracker.updateStep(sessionId, 'init', 'completed', `Started ${layoutsToCapture.length} browser sessions`)
|
|
progressTracker.updateStep(sessionId, 'auth', 'active', 'Authenticating with TradingView...')
|
|
}
|
|
|
|
// Create parallel session promises for true dual-session approach
|
|
const sessionPromises = layoutsToCapture.map(async (layout, index) => {
|
|
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...`)
|
|
if (sessionId && index === 0) {
|
|
progressTracker.updateStep(sessionId, 'auth', 'active', `Logging into ${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`)
|
|
}
|
|
|
|
// Update auth progress when first session completes auth
|
|
if (sessionId && index === 0) {
|
|
progressTracker.updateStep(sessionId, 'auth', 'completed', 'TradingView authentication successful')
|
|
progressTracker.updateStep(sessionId, 'navigation', 'active', `Navigating to ${config.symbol} chart...`)
|
|
}
|
|
|
|
// 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`)
|
|
|
|
// Update navigation progress when first session completes navigation
|
|
if (sessionId && index === 0) {
|
|
progressTracker.updateStep(sessionId, 'navigation', 'completed', 'Chart navigation successful')
|
|
progressTracker.updateStep(sessionId, 'loading', 'active', 'Loading chart data and indicators...')
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
|
|
// Update loading progress when first session completes loading
|
|
if (sessionId && index === 0) {
|
|
progressTracker.updateStep(sessionId, 'loading', 'completed', 'Chart data loaded successfully')
|
|
progressTracker.updateStep(sessionId, 'capture', 'active', 'Capturing screenshots...')
|
|
}
|
|
|
|
// 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')
|
|
}
|
|
})
|
|
|
|
if (sessionId) {
|
|
progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`)
|
|
}
|
|
|
|
console.log(`\n🎯 Parallel capture completed: ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`)
|
|
return screenshotFiles
|
|
|
|
} catch (error) {
|
|
console.error('Enhanced parallel screenshot capture failed:', error)
|
|
|
|
if (sessionId) {
|
|
// Mark the current active step as error
|
|
const progress = progressTracker.getProgress(sessionId)
|
|
if (progress) {
|
|
const activeStep = progress.steps.find(step => step.status === 'active')
|
|
if (activeStep) {
|
|
progressTracker.updateStep(sessionId, activeStep.id, 'error', error instanceof Error ? error.message : 'Unknown error')
|
|
}
|
|
}
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async captureQuick(symbol: string, timeframe: string, credentials?: TradingViewCredentials): Promise<string | null> {
|
|
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<string[]> {
|
|
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<void> {
|
|
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() |