feat: Fix TypeScript errors and implement Docker Compose V2 deployment
- Fix TypeScript errors in enhanced-screenshot.ts and tradingview-automation.ts - Add proper type assertions for page.screenshot() path parameter - Ensure compatibility with strict TypeScript compilation - Verify Docker Compose V2 deployment working on port 9000 - Application successfully containerized and production-ready - All build processes pass without TypeScript errors Ready for easy deployment on any machine with Docker & Docker Compose V2
This commit is contained in:
@@ -68,6 +68,9 @@ COPY . .
|
|||||||
# Generate Prisma client
|
# Generate Prisma client
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
|
|
||||||
|
# Build the Next.js application for production
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
# Fix permissions for node_modules binaries
|
# Fix permissions for node_modules binaries
|
||||||
RUN chmod +x node_modules/.bin/*
|
RUN chmod +x node_modules/.bin/*
|
||||||
|
|
||||||
@@ -78,5 +81,5 @@ EXPOSE 3000
|
|||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
# Start the app
|
# Start the app (default to development mode)
|
||||||
CMD ["npm", "run", "dev:docker"]
|
CMD ["npm", "run", "dev:docker"]
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Original behavior - capture new screenshots
|
// Original behavior - capture new screenshots
|
||||||
const baseFilename = `${finalSymbol}_${finalTimeframe}_${Date.now()}`
|
screenshots = await enhancedScreenshotService.captureWithLogin({
|
||||||
screenshots = await enhancedScreenshotService.capture(finalSymbol, `${baseFilename}.png`, finalLayouts, finalTimeframe)
|
symbol: finalSymbol,
|
||||||
|
timeframe: finalTimeframe,
|
||||||
|
layouts: finalLayouts
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let result
|
let result
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { Connection, PublicKey } from '@solana/web3.js'
|
import { Connection, PublicKey, Keypair } from '@solana/web3.js'
|
||||||
import { Wallet } from '@coral-xyz/anchor'
|
|
||||||
import { Keypair } from '@solana/web3.js'
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -19,14 +17,13 @@ export async function GET(request: Request) {
|
|||||||
// Convert private key to Keypair
|
// Convert private key to Keypair
|
||||||
const privateKeyBytes = JSON.parse(privateKeyString)
|
const privateKeyBytes = JSON.parse(privateKeyString)
|
||||||
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyBytes))
|
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyBytes))
|
||||||
const wallet = new Wallet(keypair)
|
|
||||||
|
|
||||||
// Connect to Helius RPC
|
// Connect to Helius RPC
|
||||||
const connection = new Connection(process.env.HELIUS_RPC_ENDPOINT || 'https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY')
|
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
|
// Get transaction signatures for this wallet
|
||||||
const signatures = await connection.getSignaturesForAddress(
|
const signatures = await connection.getSignaturesForAddress(
|
||||||
wallet.publicKey,
|
keypair.publicKey,
|
||||||
{ limit: limit * 2 } // Get more signatures to filter for Drift transactions
|
{ limit: limit * 2 } // Get more signatures to filter for Drift transactions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,29 @@ services:
|
|||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DOCKER_ENV=true
|
- DOCKER_ENV=true
|
||||||
|
|
||||||
|
# Load production environment variables
|
||||||
|
env_file:
|
||||||
|
- .env.production
|
||||||
|
|
||||||
# Production command
|
# Production command
|
||||||
command: ["npm", "start"]
|
command: ["npm", "start"]
|
||||||
|
|
||||||
# Only expose necessary port
|
# Only expose necessary port
|
||||||
ports:
|
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)
|
# Production volumes (no source code mounting)
|
||||||
volumes:
|
volumes:
|
||||||
- ./screenshots:/app/screenshots
|
- ./screenshots:/app/screenshots
|
||||||
- ./videos:/app/videos
|
- ./videos:/app/videos
|
||||||
- ./.env.production:/app/.env
|
- ./.tradingview-session:/app/.tradingview-session
|
||||||
|
- ./prisma:/app/prisma
|
||||||
|
- /tmp/.X11-unix:/tmp/.X11-unix:rw
|
||||||
|
|
||||||
# Production labels
|
# Production labels
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ services:
|
|||||||
# Playwright/TradingView automation settings
|
# Playwright/TradingView automation settings
|
||||||
- CHROMIUM_PATH=/usr/bin/chromium
|
- CHROMIUM_PATH=/usr/bin/chromium
|
||||||
- DISABLE_CHROME_SANDBOX=true
|
- DISABLE_CHROME_SANDBOX=true
|
||||||
- DISPLAY=${DISPLAY:-:0}
|
- DISPLAY=$${DISPLAY:-:0}
|
||||||
# CAPTCHA handling
|
# CAPTCHA handling
|
||||||
- ALLOW_MANUAL_CAPTCHA=true
|
- ALLOW_MANUAL_CAPTCHA=true
|
||||||
# Database configuration
|
# Database configuration
|
||||||
@@ -36,8 +36,14 @@ services:
|
|||||||
# X11 forwarding for GUI display (when ALLOW_MANUAL_CAPTCHA=true)
|
# X11 forwarding for GUI display (when ALLOW_MANUAL_CAPTCHA=true)
|
||||||
- /tmp/.X11-unix:/tmp/.X11-unix:rw
|
- /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
|
# 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
|
privileged: true
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
|
|||||||
@@ -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<string[]> {
|
||||||
|
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<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()
|
||||||
@@ -2488,7 +2488,7 @@ export class TradingViewAutomation {
|
|||||||
// Fallback to full page screenshot
|
// Fallback to full page screenshot
|
||||||
console.log("⚠️ Chart area not found, taking full page screenshot")
|
console.log("⚠️ Chart area not found, taking full page screenshot")
|
||||||
await this.page.screenshot({
|
await this.page.screenshot({
|
||||||
path: filePath,
|
path: filePath as `${string}.png`,
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
type: 'png'
|
type: 'png'
|
||||||
})
|
})
|
||||||
@@ -2517,7 +2517,7 @@ export class TradingViewAutomation {
|
|||||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||||
|
|
||||||
await this.page.screenshot({
|
await this.page.screenshot({
|
||||||
path: filePath,
|
path: filePath as `${string}.png`,
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
type: 'png'
|
type: 'png'
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user