Files
trading_bot_v3/lib/enhanced-screenshot.ts
mindesbunister 28836c3e5b Add safe logging utility for credential protection
- Created lib/safe-logging.ts with utilities for safe logging
- logConfigSafely() automatically redacts credentials field
- logSafely() redacts common sensitive fields (password, email, token, etc)
- Updated enhanced-screenshot service to use safe logging utility
- Provides reusable pattern for secure logging throughout codebase
2025-07-17 14:48:19 +02:00

442 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'
import { logConfigSafely } from './safe-logging'
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)')
logConfigSafely(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()