diff --git a/.tradingview-session/cookies.json b/.tradingview-session/cookies.json
new file mode 100644
index 0000000..ec08c7d
--- /dev/null
+++ b/.tradingview-session/cookies.json
@@ -0,0 +1,42 @@
+[
+ {
+ "name": "_sp_ses.cf1a",
+ "value": "*",
+ "domain": ".tradingview.com",
+ "path": "/",
+ "expires": 1760130215,
+ "httpOnly": false,
+ "secure": true,
+ "sameSite": "None"
+ },
+ {
+ "name": "cookiePrivacyPreferenceBannerProduction",
+ "value": "ignored",
+ "domain": ".tradingview.com",
+ "path": "/",
+ "expires": 1786910284.474377,
+ "httpOnly": false,
+ "secure": false,
+ "sameSite": "Lax"
+ },
+ {
+ "name": "sp",
+ "value": "e859cfb0-8391-4ff2-bfd4-7e8dece66223",
+ "domain": "snowplow-pixel.tradingview.com",
+ "path": "/",
+ "expires": 1783890216.454505,
+ "httpOnly": true,
+ "secure": true,
+ "sameSite": "None"
+ },
+ {
+ "name": "_sp_id.cf1a",
+ "value": ".1752350284.1.1752354216..e1158a69-b832-47e3-8c6d-fb185294fa96..67d8f61c-9e0d-4f5b-a27b-0daf04e61207.1752350284475.13",
+ "domain": ".tradingview.com",
+ "path": "/",
+ "expires": 1786914215.942213,
+ "httpOnly": false,
+ "secure": true,
+ "sameSite": "None"
+ }
+]
\ No newline at end of file
diff --git a/.tradingview-session/session-storage.json b/.tradingview-session/session-storage.json
new file mode 100644
index 0000000..55533a9
--- /dev/null
+++ b/.tradingview-session/session-storage.json
@@ -0,0 +1,27 @@
+{
+ "localStorage": {
+ "cookie_dialog_tracked": "1",
+ "tradingview.widgetbar.widget.alerts.bhwdEXIZXDRD": "{}",
+ "tradingview.globalNotification": "14180",
+ "tradingview.SymbolSearch.recent": "[\"COINBASE:SOLUSD\"]",
+ "tradingview.details.force_uncheck_bid_ask_03_2023": "true",
+ "tvlocalstorage.available": "true",
+ "tradingview.widgetbar.layout-settings": "{\"widgets\":{\"watchlist\":[{\"id\":\"watchlist.UnmJh4ohCYUv\"}],\"detail\":[{\"id\":\"detail.dYGOrm50hOjz\"}],\"alerts\":[{\"id\":\"alerts.bhwdEXIZXDRD\"}],\"object_tree\":[{\"id\":\"object_tree.74i7WNrleNPn\"}]},\"settings\":{\"width\":300,\"version\":2}}",
+ "first_visit_time": "1752350284651",
+ "tradingview.trading.oanda_rest_launched_in_country": "DE",
+ "tradingview.trading.orderWidgetMode.": "panel",
+ "snowplowOutQueue_tv_cf_post2.expires": "1815426215944",
+ "tradingview.widgetbar.widget.object_tree.74i7WNrleNPn": "{\"selectedPage\":\"object_tree\"}",
+ "featuretoggle_seed": "79463",
+ "tradingview.symboledit.tradable": "true",
+ "snowplowOutQueue_tv_cf_post2": "[]",
+ "tradingview.editchart.model.style": "1",
+ "tradingview.editchart.model.interval": "D",
+ "tradingview.editchart.model.symbol": "COINBASE:SOLUSD",
+ "tradingview.trading.tradingPanelOpened": "false",
+ "last-crosstab-monotonic-timestamp": "1752350304273",
+ "tradingview.details.force_uncheck_ranges_03_2023": "true",
+ "signupSource": "auth page tvd"
+ },
+ "sessionStorage": {}
+}
\ No newline at end of file
diff --git a/app/api/session-status/route.ts b/app/api/session-status/route.ts
new file mode 100644
index 0000000..0a06f05
--- /dev/null
+++ b/app/api/session-status/route.ts
@@ -0,0 +1,124 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { tradingViewAutomation } from '../../../lib/tradingview-automation'
+
+export async function GET(request: NextRequest) {
+ try {
+ console.log('๐ Getting TradingView session status...')
+
+ // Initialize if not already done (Docker-safe initialization)
+ if (!tradingViewAutomation['browser']) {
+ console.log('๐ณ Initializing TradingView automation in Docker environment...')
+ await tradingViewAutomation.init()
+ }
+
+ // Get lightweight session information without navigation
+ const sessionInfo = await tradingViewAutomation.getQuickSessionStatus()
+
+ // Determine connection status based on browser state and URL
+ let connectionStatus = 'unknown'
+ if (sessionInfo.browserActive) {
+ if (sessionInfo.currentUrl.includes('tradingview.com')) {
+ connectionStatus = 'connected'
+ } else if (sessionInfo.currentUrl) {
+ connectionStatus = 'disconnected'
+ } else {
+ connectionStatus = 'unknown'
+ }
+ } else {
+ connectionStatus = 'disconnected'
+ }
+
+ const response = {
+ success: true,
+ session: {
+ ...sessionInfo,
+ connectionStatus,
+ lastChecked: new Date().toISOString(),
+ dockerEnv: process.env.DOCKER_ENV === 'true',
+ environment: process.env.NODE_ENV || 'development'
+ }
+ }
+
+ console.log('โ
Session status retrieved:', response.session)
+ return NextResponse.json(response)
+
+ } catch (error) {
+ console.error('โ Failed to get session status:', error)
+ return NextResponse.json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to get session status',
+ session: {
+ isAuthenticated: false,
+ hasSavedCookies: false,
+ hasSavedStorage: false,
+ cookiesCount: 0,
+ currentUrl: '',
+ connectionStatus: 'error',
+ lastChecked: new Date().toISOString(),
+ dockerEnv: process.env.DOCKER_ENV === 'true',
+ environment: process.env.NODE_ENV || 'development'
+ }
+ }, { status: 500 })
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const { action } = await request.json()
+ console.log(`๐ง Session action requested: ${action} (Docker: ${process.env.DOCKER_ENV === 'true'})`)
+
+ // Initialize if not already done (Docker-safe initialization)
+ if (!tradingViewAutomation['browser']) {
+ console.log('๐ณ Initializing TradingView automation for session action in Docker...')
+ await tradingViewAutomation.init()
+ }
+
+ let result: any = { success: true }
+
+ switch (action) {
+ case 'refresh':
+ const refreshed = await tradingViewAutomation.refreshSession()
+ result.refreshed = refreshed
+ result.message = refreshed ? 'Session refreshed successfully' : 'Failed to refresh session'
+ break
+
+ case 'clear':
+ await tradingViewAutomation.clearSession()
+ result.message = 'Session data cleared successfully'
+ break
+
+ case 'test':
+ const testResult = await tradingViewAutomation.testSessionPersistence()
+ result.testResult = testResult
+ result.message = testResult.isValid ? 'Session is valid' : 'Session is invalid or expired'
+ break
+
+ case 'login-status':
+ const isLoggedIn = await tradingViewAutomation.checkLoginStatus()
+ result.isLoggedIn = isLoggedIn
+ result.message = isLoggedIn ? 'User is logged in' : 'User is not logged in'
+ break
+
+ default:
+ result.success = false
+ result.error = `Unknown action: ${action}`
+ return NextResponse.json(result, { status: 400 })
+ }
+
+ console.log(`โ
Session action '${action}' completed:`, result)
+ return NextResponse.json({
+ ...result,
+ dockerEnv: process.env.DOCKER_ENV === 'true',
+ environment: process.env.NODE_ENV || 'development'
+ })
+
+ } catch (error) {
+ console.error('โ Session action failed:', error)
+ return NextResponse.json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Session action failed',
+ dockerEnv: process.env.DOCKER_ENV === 'true',
+ environment: process.env.NODE_ENV || 'development'
+ }, { status: 500 })
+ }
+}
diff --git a/app/globals.css b/app/globals.css
index ef59836..86b565a 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,7 +1,170 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
+:root {
+ --bg-primary: #0a0a0b;
+ --bg-secondary: #1a1a1b;
+ --bg-tertiary: #262626;
+ --bg-card: #1e1e1f;
+ --border-primary: #333;
+ --text-primary: #ffffff;
+ --text-secondary: #a1a1aa;
+ --text-accent: #22d3ee;
+ --success: #10b981;
+ --danger: #ef4444;
+ --warning: #f59e0b;
+ --purple: #8b5cf6;
+ --blue: #3b82f6;
+}
+
+* {
+ box-sizing: border-box;
+}
+
body {
font-family: 'Inter', system-ui, sans-serif;
+ background: linear-gradient(135deg, var(--bg-primary) 0%, #0f0f0f 100%);
+ color: var(--text-primary);
+ overflow-x: hidden;
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-primary);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+/* Glass morphism effect */
+.glass {
+ background: rgba(26, 26, 27, 0.8);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* Gradient borders */
+.gradient-border {
+ position: relative;
+ background: var(--bg-card);
+ border-radius: 12px;
+}
+
+.gradient-border::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ padding: 1px;
+ background: linear-gradient(135deg, rgba(34, 211, 238, 0.3), rgba(139, 92, 246, 0.3));
+ border-radius: inherit;
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+ mask-composite: subtract;
+}
+
+/* Button components */
+@layer components {
+ .btn {
+ @apply px-4 py-2 rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900;
+ }
+
+ .btn-primary {
+ @apply bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-600 hover:to-blue-700 text-white shadow-lg hover:shadow-cyan-500/25;
+ }
+
+ .btn-secondary {
+ @apply bg-gray-700 hover:bg-gray-600 text-gray-100 border border-gray-600;
+ }
+
+ .btn-success {
+ @apply bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white shadow-lg hover:shadow-green-500/25;
+ }
+
+ .btn-danger {
+ @apply bg-gradient-to-r from-red-500 to-rose-600 hover:from-red-600 hover:to-rose-700 text-white shadow-lg hover:shadow-red-500/25;
+ }
+
+ .btn-warning {
+ @apply bg-gradient-to-r from-yellow-500 to-orange-600 hover:from-yellow-600 hover:to-orange-700 text-white shadow-lg hover:shadow-yellow-500/25;
+ }
+
+ .card {
+ @apply bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6 shadow-xl hover:shadow-2xl transition-all duration-300;
+ }
+
+ .card-gradient {
+ @apply relative overflow-hidden;
+ background: linear-gradient(135deg, rgba(30, 30, 31, 0.9) 0%, rgba(26, 26, 27, 0.9) 100%);
+ }
+
+ .card-gradient::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(34, 211, 238, 0.5), transparent);
+ }
+
+ .status-indicator {
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
+ }
+
+ .status-online {
+ @apply bg-green-100 text-green-800 border border-green-200;
+ }
+
+ .status-offline {
+ @apply bg-red-100 text-red-800 border border-red-200;
+ }
+
+ .status-pending {
+ @apply bg-yellow-100 text-yellow-800 border border-yellow-200;
+ }
+}
+
+/* Animations */
+@keyframes pulse-glow {
+ 0%, 100% {
+ box-shadow: 0 0 5px rgba(34, 211, 238, 0.5);
+ }
+ 50% {
+ box-shadow: 0 0 20px rgba(34, 211, 238, 0.8), 0 0 30px rgba(34, 211, 238, 0.4);
+ }
+}
+
+.pulse-glow {
+ animation: pulse-glow 2s ease-in-out infinite;
+}
+
+@keyframes slide-up {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.slide-up {
+ animation: slide-up 0.6s ease-out;
+}
+
+/* Loading spinner */
+.spinner {
+ @apply inline-block w-4 h-4 border-2 border-gray-300 border-t-cyan-500 rounded-full animate-spin;
}
diff --git a/app/layout.tsx b/app/layout.tsx
index c850dc0..1cf0bd8 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,16 +3,68 @@ import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Trading Bot Dashboard',
- description: 'AI-powered trading bot dashboard with auto-trading, analysis, and developer tools.'
+ description: 'AI-powered trading bot dashboard with auto-trading, analysis, and developer tools.',
+ viewport: 'width=device-width, initial-scale=1',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
-
- {children}
-
+
+
+ {/* Header */}
+
+
+
+
+
+
+ TB
+
+
+
Trading Bot
+
AI-Powered Dashboard
+
+
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {children}
+
+
+ {/* Footer */}
+
+
+
+
+ ยฉ 2025 Trading Bot Dashboard. Powered by AI.
+
+
+
+ Next.js 15 โข TypeScript โข Tailwind CSS
+
+
+
+
+
+
)
diff --git a/app/page.tsx b/app/page.tsx
index 0c5d743..4f3cf63 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,11 +1,20 @@
-import AIAnalysisPanel from '../components/AIAnalysisPanel'
import Dashboard from '../components/Dashboard'
export default function HomePage() {
return (
- <>
-
+
+ {/* Hero Section */}
+
+
+ AI Trading Dashboard
+
+
+ Advanced cryptocurrency trading with AI-powered analysis, automated execution, and real-time monitoring.
+
+
+
+ {/* Main Dashboard */}
- >
+
)
}
diff --git a/components/AIAnalysisPanel.tsx b/components/AIAnalysisPanel.tsx
index e7c6ac1..f0501d1 100644
--- a/components/AIAnalysisPanel.tsx
+++ b/components/AIAnalysisPanel.tsx
@@ -14,14 +14,14 @@ const timeframes = [
]
const popularCoins = [
- { name: 'Bitcoin', symbol: 'BTCUSD', icon: 'โฟ' },
- { name: 'Ethereum', symbol: 'ETHUSD', icon: 'ฮ' },
- { name: 'Solana', symbol: 'SOLUSD', icon: 'โ' },
- { name: 'Sui', symbol: 'SUIUSD', icon: '๐ท' },
- { name: 'Avalanche', symbol: 'AVAXUSD', icon: '๐บ' },
- { name: 'Cardano', symbol: 'ADAUSD', icon: 'โ ' },
- { name: 'Polygon', symbol: 'MATICUSD', icon: '๐ท' },
- { name: 'Chainlink', symbol: 'LINKUSD', icon: '๐' },
+ { name: 'Bitcoin', symbol: 'BTCUSD', icon: 'โฟ', color: 'from-orange-400 to-orange-600' },
+ { name: 'Ethereum', symbol: 'ETHUSD', icon: 'ฮ', color: 'from-blue-400 to-blue-600' },
+ { name: 'Solana', symbol: 'SOLUSD', icon: 'โ', color: 'from-purple-400 to-purple-600' },
+ { name: 'Sui', symbol: 'SUIUSD', icon: '๐ท', color: 'from-cyan-400 to-cyan-600' },
+ { name: 'Avalanche', symbol: 'AVAXUSD', icon: '๐บ', color: 'from-red-400 to-red-600' },
+ { name: 'Cardano', symbol: 'ADAUSD', icon: 'โ ', color: 'from-indigo-400 to-indigo-600' },
+ { name: 'Polygon', symbol: 'MATICUSD', icon: '๐ท', color: 'from-violet-400 to-violet-600' },
+ { name: 'Chainlink', symbol: 'LINKUSD', icon: '๐', color: 'from-blue-400 to-blue-600' },
]
export default function AIAnalysisPanel() {
@@ -51,10 +51,6 @@ export default function AIAnalysisPanel() {
)
}
- const selectCoin = (coinSymbol: string) => {
- setSymbol(coinSymbol)
- }
-
const quickAnalyze = async (coinSymbol: string) => {
setSymbol(coinSymbol)
setSelectedLayouts([layouts[0]]) // Use first layout
@@ -104,248 +100,284 @@ export default function AIAnalysisPanel() {
}
return (
-
-
AI Chart Analysis
+
+
+
+
+ ๐ค
+
+ AI Chart Analysis
+
+
+
{/* Quick Coin Selection */}
-
-
Quick Analysis - Popular Coins
-
+
+
+
+
+ Quick Analysis
+
+ Click any coin for instant analysis
+
+
{popularCoins.map(coin => (
quickAnalyze(coin.symbol)}
disabled={loading}
- className={`p-3 rounded-lg border transition-all ${
+ className={`group relative p-4 rounded-xl border transition-all duration-300 ${
symbol === coin.symbol
- ? 'border-blue-500 bg-blue-500/20 text-blue-300'
- : 'border-gray-600 bg-gray-800 text-gray-300 hover:border-gray-500 hover:bg-gray-700'
- } ${loading ? 'opacity-50 cursor-not-allowed' : 'hover:scale-105'}`}
+ ? 'border-cyan-500 bg-cyan-500/10 shadow-lg shadow-cyan-500/20'
+ : 'border-gray-700 bg-gray-800/50 hover:border-gray-600 hover:bg-gray-800'
+ } ${loading ? 'opacity-50 cursor-not-allowed' : 'hover:scale-105 hover:shadow-lg'}`}
>
- {coin.icon}
- {coin.name}
- {coin.symbol}
+
+ {coin.icon}
+
+ {coin.name}
+ {coin.symbol}
+ {symbol === coin.symbol && (
+
+ )}
))}
- {/* Manual Input Section */}
-
-
Manual Analysis
-
- setSymbol(e.target.value)}
- placeholder="Symbol (e.g. BTCUSD)"
- />
- setTimeframe(e.target.value)}
- >
- {timeframes.map(tf => {tf.label} )}
-
-
- {loading ? 'Analyzing...' : 'Analyze'}
-
-
-
-
- {/* Layout selection */}
-
-
- Select Layouts for Analysis:
-
-
- {layouts.map(layout => (
-
- toggleLayout(layout)}
- className="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500"
- />
- {layout}
-
- ))}
-
- {selectedLayouts.length > 0 && (
-
- Selected: {selectedLayouts.join(', ')}
+ {/* Advanced Controls */}
+
+
+
+ Advanced Analysis
+
+
+ {/* Symbol and Timeframe */}
+
+
+ Trading Pair
+ setSymbol(e.target.value.toUpperCase())}
+ placeholder="e.g., BTCUSD, ETHUSD"
+ />
- )}
-
- {error && (
-
-
- {error.includes('frame was detached') ? (
- <>
-
TradingView Error: Chart could not be loaded. Please check your symbol and layout, or try again.
-
Technical: {error}
- >
- ) : error.includes('layout not found') ? (
- <>
-
Layout Error: TradingView layout not found. Please select a valid layout.
-
Technical: {error}
- >
- ) : error.includes('Private layout access denied') ? (
- <>
-
Access Error: The selected layout is private or requires authentication. Try a different layout or check your TradingView login.
-
Technical: {error}
- >
- ) : (
- <>
-
Analysis Error: {error}
- >
- )}
+
+ Timeframe
+ setTimeframe(e.target.value)}
+ >
+ {timeframes.map(tf => (
+
+ {tf.label}
+
+ ))}
+
- )}
- {loading && (
-
-
-
-
-
-
-
Analyzing {symbol} chart...
-
-
- )}
- {result && (
-
-
Analysis Results
- {result.layoutsAnalyzed && (
-
- Layouts analyzed:
- {result.layoutsAnalyzed.join(', ')}
-
- )}
-
-
-
Summary:
-
{safeRender(result.summary)}
-
-
-
-
Sentiment:
-
{safeRender(result.marketSentiment)}
-
-
-
Recommendation:
-
{safeRender(result.recommendation)}
-
({safeRender(result.confidence)}% confidence)
-
-
- {result.keyLevels && (
-
-
-
Support Levels:
-
{result.keyLevels.support?.join(', ') || 'None identified'}
-
-
-
Resistance Levels:
-
{result.keyLevels.resistance?.join(', ') || 'None identified'}
-
-
- )}
-
- {/* Enhanced Trading Analysis */}
- {result.entry && (
-
-
Entry:
-
${safeRender(result.entry.price || result.entry)}
- {result.entry.buffer &&
{safeRender(result.entry.buffer)}
}
- {result.entry.rationale &&
{safeRender(result.entry.rationale)}
}
-
- )}
-
- {result.stopLoss && (
-
-
Stop Loss:
-
${safeRender(result.stopLoss.price || result.stopLoss)}
- {result.stopLoss.rationale &&
{safeRender(result.stopLoss.rationale)}
}
-
- )}
-
- {result.takeProfits && (
-
-
Take Profits:
- {typeof result.takeProfits === 'object' ? (
- <>
- {result.takeProfits.tp1 && (
-
-
TP1: ${safeRender(result.takeProfits.tp1.price || result.takeProfits.tp1)}
- {result.takeProfits.tp1.description &&
{safeRender(result.takeProfits.tp1.description)}
}
-
- )}
- {result.takeProfits.tp2 && (
-
-
TP2: ${safeRender(result.takeProfits.tp2.price || result.takeProfits.tp2)}
- {result.takeProfits.tp2.description &&
{safeRender(result.takeProfits.tp2.description)}
}
-
- )}
- >
- ) : (
-
{safeRender(result.takeProfits)}
- )}
-
- )}
-
- {result.riskToReward && (
-
-
Risk to Reward:
-
{safeRender(result.riskToReward)}
-
- )}
-
- {result.confirmationTrigger && (
-
-
Confirmation Trigger:
-
{safeRender(result.confirmationTrigger)}
-
- )}
-
- {result.indicatorAnalysis && (
-
-
Indicator Analysis:
- {typeof result.indicatorAnalysis === 'object' ? (
-
- {result.indicatorAnalysis.rsi && (
-
- RSI:
- {safeRender(result.indicatorAnalysis.rsi)}
-
- )}
- {result.indicatorAnalysis.vwap && (
-
- VWAP:
- {safeRender(result.indicatorAnalysis.vwap)}
-
- )}
- {result.indicatorAnalysis.obv && (
-
- OBV:
- {safeRender(result.indicatorAnalysis.obv)}
-
+
+ {/* Layout Selection */}
+
+
Analysis Layouts
+
+ {layouts.map(layout => (
+
+ toggleLayout(layout)}
+ className="sr-only"
+ />
+
+
+ {selectedLayouts.includes(layout) && (
+
+
+
)}
- ) : (
-
{safeRender(result.indicatorAnalysis)}
- )}
+
{layout}
+
+
+ ))}
+
+ {selectedLayouts.length > 0 && (
+
+
+ Selected layouts: {selectedLayouts.join(', ')}
+
+
+ )}
+
+
+ {/* Analyze Button */}
+
+ {loading ? (
+
+ ) : (
+
+ ๐
+ Start AI Analysis
+
+ )}
+
+
+
+ {/* Results Section */}
+ {error && (
+
+
+
โ ๏ธ
+
+
Analysis Error
+
{error}
+
+
+
+ )}
+
+ {loading && (
+
+
+
+
+
AI Processing
+
+ Analyzing {symbol} on {timeframe} timeframe...
+
+
+
+
+ )}
+
+ {result && (
+
+
+
+
+ โ
+
+ Analysis Complete
+
+ {result.layoutsAnalyzed && (
+
+ Layouts: {result.layoutsAnalyzed.join(', ')}
)}
-
-
-
Reasoning:
-
{safeRender(result.reasoning)}
+
+
+
+ {/* Summary */}
+
+
+
+ Market Summary
+
+
{safeRender(result.summary)}
+
+ {/* Key Metrics */}
+
+
+
Market Sentiment
+
{safeRender(result.marketSentiment)}
+
+
+
+
Recommendation
+
{safeRender(result.recommendation)}
+ {result.confidence && (
+
{safeRender(result.confidence)}% confidence
+ )}
+
+
+
+ {/* Trading Levels */}
+ {result.keyLevels && (
+
+
+
Resistance Levels
+
+ {result.keyLevels.resistance?.join(', ') || 'None identified'}
+
+
+
+
+
Support Levels
+
+ {result.keyLevels.support?.join(', ') || 'None identified'}
+
+
+
+ )}
+
+ {/* Trading Setup */}
+ {(result.entry || result.stopLoss || result.takeProfits) && (
+
+
Trading Setup
+
+ {result.entry && (
+
+
Entry Point
+
+ ${safeRender(result.entry.price || result.entry)}
+
+ {result.entry.rationale && (
+
{safeRender(result.entry.rationale)}
+ )}
+
+ )}
+
+ {result.stopLoss && (
+
+
Stop Loss
+
+ ${safeRender(result.stopLoss.price || result.stopLoss)}
+
+ {result.stopLoss.rationale && (
+
{safeRender(result.stopLoss.rationale)}
+ )}
+
+ )}
+
+ {result.takeProfits && (
+
+
Take Profit
+
+ {typeof result.takeProfits === 'object'
+ ? Object.values(result.takeProfits).map(tp => `$${safeRender(tp)}`).join(', ')
+ : `$${safeRender(result.takeProfits)}`}
+
+
+ )}
+
+
+ )}
)}
diff --git a/components/AIAnalysisPanel_old.tsx b/components/AIAnalysisPanel_old.tsx
new file mode 100644
index 0000000..1eb9697
--- /dev/null
+++ b/components/AIAnalysisPanel_old.tsx
@@ -0,0 +1,561 @@
+"use client"
+import React, { useState } from 'react'
+
+const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
+const timeframes = [
+ { label: '1m', value: '1' },
+ { label: '5m', value: '5' },
+ { label: '15m', value: '15' },
+ { label: '1h', value: '60' },
+ { label: '4h', value: '240' },
+ { label: '1d', value: 'D' },
+ { label: '1w', value: 'W' },
+ { label: '1M', value: 'M' },
+]
+
+const popularCoins = [
+ { name: 'Bitcoin', symbol: 'BTCUSD', icon: 'โฟ', color: 'from-orange-400 to-orange-600' },
+ { name: 'Ethereum', symbol: 'ETHUSD', icon: 'ฮ', color: 'from-blue-400 to-blue-600' },
+ { name: 'Solana', symbol: 'SOLUSD', icon: 'โ', color: 'from-purple-400 to-purple-600' },
+ { name: 'Sui', symbol: 'SUIUSD', icon: '๐ท', color: 'from-cyan-400 to-cyan-600' },
+ { name: 'Avalanche', symbol: 'AVAXUSD', icon: '๐บ', color: 'from-red-400 to-red-600' },
+ { name: 'Cardano', symbol: 'ADAUSD', icon: 'โ ', color: 'from-indigo-400 to-indigo-600' },
+ { name: 'Polygon', symbol: 'MATICUSD', icon: '๐ท', color: 'from-violet-400 to-violet-600' },
+ { name: 'Chainlink', symbol: 'LINKUSD', icon: '๐', color: 'from-blue-400 to-blue-600' },
+]
+
+export default function AIAnalysisPanel() {
+ const [symbol, setSymbol] = useState('BTCUSD')
+ const [selectedLayouts, setSelectedLayouts] = useState
([layouts[0]])
+ const [timeframe, setTimeframe] = useState('60')
+ const [loading, setLoading] = useState(false)
+ const [result, setResult] = useState(null)
+ const [error, setError] = useState(null)
+
+ // Helper function to safely render any value
+ const safeRender = (value: any): string => {
+ if (typeof value === 'string') return value
+ if (typeof value === 'number') return value.toString()
+ if (Array.isArray(value)) return value.join(', ')
+ if (typeof value === 'object' && value !== null) {
+ return JSON.stringify(value)
+ }
+ return String(value)
+ }
+
+ const toggleLayout = (layout: string) => {
+ setSelectedLayouts(prev =>
+ prev.includes(layout)
+ ? prev.filter(l => l !== layout)
+ : [...prev, layout]
+ )
+ }
+
+ const selectCoin = (coinSymbol: string) => {
+ setSymbol(coinSymbol)
+ }
+
+ const quickAnalyze = async (coinSymbol: string) => {
+ setSymbol(coinSymbol)
+ setSelectedLayouts([layouts[0]]) // Use first layout
+ setLoading(true)
+ setError(null)
+ setResult(null)
+
+ try {
+ const res = await fetch('/api/analyze', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ symbol: coinSymbol, layouts: [layouts[0]], timeframe })
+ })
+ const data = await res.json()
+ if (!res.ok) throw new Error(data.error || 'Unknown error')
+ setResult(data)
+ } catch (e: any) {
+ setError(e.message)
+ }
+ setLoading(false)
+ }
+
+ async function handleAnalyze() {
+ setLoading(true)
+ setError(null)
+ setResult(null)
+
+ if (selectedLayouts.length === 0) {
+ setError('Please select at least one layout')
+ setLoading(false)
+ return
+ }
+
+ try {
+ const res = await fetch('/api/analyze', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ symbol, layouts: selectedLayouts, timeframe })
+ })
+ const data = await res.json()
+ if (!res.ok) throw new Error(data.error || 'Unknown error')
+ setResult(data)
+ } catch (e: any) {
+ setError(e.message)
+ }
+ setLoading(false)
+ }
+
+ return (
+
+
+
+
+ ๐ค
+
+ AI Chart Analysis
+
+
+
+
+ {/* Quick Coin Selection */}
+
+
+
+
+ Quick Analysis
+
+ Click any coin for instant analysis
+
+
+ {popularCoins.map(coin => (
+
quickAnalyze(coin.symbol)}
+ disabled={loading}
+ className={`group relative p-4 rounded-xl border transition-all duration-300 ${
+ symbol === coin.symbol
+ ? 'border-cyan-500 bg-cyan-500/10 shadow-lg shadow-cyan-500/20'
+ : 'border-gray-700 bg-gray-800/50 hover:border-gray-600 hover:bg-gray-800'
+ } ${loading ? 'opacity-50 cursor-not-allowed' : 'hover:scale-105 hover:shadow-lg'}`}
+ >
+
+ {coin.icon}
+
+ {coin.name}
+ {coin.symbol}
+ {symbol === coin.symbol && (
+
+ )}
+
+ ))}
+
+
+
+ {/* Advanced Controls */}
+
+
+
+ Advanced Analysis
+
+
+ {/* Symbol and Timeframe */}
+
+
+ Trading Pair
+ setSymbol(e.target.value.toUpperCase())}
+ placeholder="e.g., BTCUSD, ETHUSD"
+ />
+
+
+ Timeframe
+ setTimeframe(e.target.value)}
+ >
+ {timeframes.map(tf => (
+
+ {tf.label}
+
+ ))}
+
+
+
+
+ {/* Layout Selection */}
+
+
Analysis Layouts
+
+ {layouts.map(layout => (
+
+ toggleLayout(layout)}
+ className="sr-only"
+ />
+
+
+ {selectedLayouts.includes(layout) && (
+
+
+
+ )}
+
+
{layout}
+
+
+ ))}
+
+ {selectedLayouts.length > 0 && (
+
+
+ Selected layouts: {selectedLayouts.join(', ')}
+
+
+ )}
+
+
+ {/* Analyze Button */}
+
+ {loading ? (
+
+ ) : (
+
+ ๐
+ Start AI Analysis
+
+ )}
+
+
+
+ {/* Results Section */}
+ {error && (
+
+
+
โ ๏ธ
+
+
Analysis Error
+
{error}
+
+
+
+ )}
+
+ {loading && (
+
+
+
+
+
AI Processing
+
+ Analyzing {symbol} on {timeframe} timeframe...
+
+
+
+
+ )}
+
+ {result && (
+
+
+
+
+ โ
+
+ Analysis Complete
+
+ {result.layoutsAnalyzed && (
+
+ Layouts: {result.layoutsAnalyzed.join(', ')}
+
+ )}
+
+
+
+ {/* Summary */}
+
+
+
+ Market Summary
+
+
{safeRender(result.summary)}
+
+
+ {/* Key Metrics */}
+
+
+
Market Sentiment
+
{safeRender(result.marketSentiment)}
+
+
+
+
Recommendation
+
{safeRender(result.recommendation)}
+ {result.confidence && (
+
{safeRender(result.confidence)}% confidence
+ )}
+
+
+
+ {/* Trading Levels */}
+ {result.keyLevels && (
+
+
+
Resistance Levels
+
+ {result.keyLevels.resistance?.join(', ') || 'None identified'}
+
+
+
+
+
Support Levels
+
+ {result.keyLevels.support?.join(', ') || 'None identified'}
+
+
+
+ )}
+
+ {/* Trading Setup */}
+ {(result.entry || result.stopLoss || result.takeProfits) && (
+
+
Trading Setup
+
+ {result.entry && (
+
+
Entry Point
+
+ ${safeRender(result.entry.price || result.entry)}
+
+ {result.entry.rationale && (
+
{safeRender(result.entry.rationale)}
+ )}
+
+ )}
+
+ {result.stopLoss && (
+
+
Stop Loss
+
+ ${safeRender(result.stopLoss.price || result.stopLoss)}
+
+ {result.stopLoss.rationale && (
+
{safeRender(result.stopLoss.rationale)}
+ )}
+
+ )}
+
+ {result.takeProfits && (
+
+
Take Profit
+
+ {typeof result.takeProfits === 'object'
+ ? Object.values(result.takeProfits).map(tp => `$${safeRender(tp)}`).join(', ')
+ : `$${safeRender(result.takeProfits)}`}
+
+
+ )}
+
+
+ )}
+
+
+ )}
+
+ )
+}
+ {error && (
+
+
+ {error.includes('frame was detached') ? (
+ <>
+ TradingView Error: Chart could not be loaded. Please check your symbol and layout, or try again.
+ Technical: {error}
+ >
+ ) : error.includes('layout not found') ? (
+ <>
+ Layout Error: TradingView layout not found. Please select a valid layout.
+ Technical: {error}
+ >
+ ) : error.includes('Private layout access denied') ? (
+ <>
+ Access Error: The selected layout is private or requires authentication. Try a different layout or check your TradingView login.
+ Technical: {error}
+ >
+ ) : (
+ <>
+ Analysis Error: {error}
+ >
+ )}
+
+
+ )}
+ {loading && (
+
+
+
+
+
+
+
Analyzing {symbol} chart...
+
+
+ )}
+ {result && (
+
+
Analysis Results
+ {result.layoutsAnalyzed && (
+
+ Layouts analyzed:
+ {result.layoutsAnalyzed.join(', ')}
+
+ )}
+
+
+
Summary:
+
{safeRender(result.summary)}
+
+
+
+
Sentiment:
+
{safeRender(result.marketSentiment)}
+
+
+
Recommendation:
+
{safeRender(result.recommendation)}
+
({safeRender(result.confidence)}% confidence)
+
+
+ {result.keyLevels && (
+
+
+
Support Levels:
+
{result.keyLevels.support?.join(', ') || 'None identified'}
+
+
+
Resistance Levels:
+
{result.keyLevels.resistance?.join(', ') || 'None identified'}
+
+
+ )}
+
+ {/* Enhanced Trading Analysis */}
+ {result.entry && (
+
+
Entry:
+
${safeRender(result.entry.price || result.entry)}
+ {result.entry.buffer &&
{safeRender(result.entry.buffer)}
}
+ {result.entry.rationale &&
{safeRender(result.entry.rationale)}
}
+
+ )}
+
+ {result.stopLoss && (
+
+
Stop Loss:
+
${safeRender(result.stopLoss.price || result.stopLoss)}
+ {result.stopLoss.rationale &&
{safeRender(result.stopLoss.rationale)}
}
+
+ )}
+
+ {result.takeProfits && (
+
+
Take Profits:
+ {typeof result.takeProfits === 'object' ? (
+ <>
+ {result.takeProfits.tp1 && (
+
+
TP1: ${safeRender(result.takeProfits.tp1.price || result.takeProfits.tp1)}
+ {result.takeProfits.tp1.description &&
{safeRender(result.takeProfits.tp1.description)}
}
+
+ )}
+ {result.takeProfits.tp2 && (
+
+
TP2: ${safeRender(result.takeProfits.tp2.price || result.takeProfits.tp2)}
+ {result.takeProfits.tp2.description &&
{safeRender(result.takeProfits.tp2.description)}
}
+
+ )}
+ >
+ ) : (
+
{safeRender(result.takeProfits)}
+ )}
+
+ )}
+
+ {result.riskToReward && (
+
+
Risk to Reward:
+
{safeRender(result.riskToReward)}
+
+ )}
+
+ {result.confirmationTrigger && (
+
+
Confirmation Trigger:
+
{safeRender(result.confirmationTrigger)}
+
+ )}
+
+ {result.indicatorAnalysis && (
+
+
Indicator Analysis:
+ {typeof result.indicatorAnalysis === 'object' ? (
+
+ {result.indicatorAnalysis.rsi && (
+
+ RSI:
+ {safeRender(result.indicatorAnalysis.rsi)}
+
+ )}
+ {result.indicatorAnalysis.vwap && (
+
+ VWAP:
+ {safeRender(result.indicatorAnalysis.vwap)}
+
+ )}
+ {result.indicatorAnalysis.obv && (
+
+ OBV:
+ {safeRender(result.indicatorAnalysis.obv)}
+
+ )}
+
+ ) : (
+
{safeRender(result.indicatorAnalysis)}
+ )}
+
+ )}
+
+
+
Reasoning:
+
{safeRender(result.reasoning)}
+
+
+
+ )}
+
+ )
+}
diff --git a/components/AutoTradingPanel.tsx b/components/AutoTradingPanel.tsx
index f39dd0e..88bee1d 100644
--- a/components/AutoTradingPanel.tsx
+++ b/components/AutoTradingPanel.tsx
@@ -21,15 +21,107 @@ export default function AutoTradingPanel() {
}
}
+ const getStatusColor = () => {
+ switch (status) {
+ case 'running': return 'text-green-400'
+ case 'stopped': return 'text-red-400'
+ default: return 'text-gray-400'
+ }
+ }
+
+ const getStatusIcon = () => {
+ switch (status) {
+ case 'running': return '๐ข'
+ case 'stopped': return '๐ด'
+ default: return 'โซ'
+ }
+ }
+
return (
-
-
Auto-Trading Control
-
-
handleAction('start')}>Start
-
handleAction('stop')}>Stop
+
+
+
+
+ โก
+
+ Auto-Trading Control
+
+
+ {getStatusIcon()}
+ {status}
+
+
+
+ {/* Status Display */}
+
+
+
+
Trading Status
+
+ {status === 'running' ? 'ACTIVE' : status === 'stopped' ? 'STOPPED' : 'IDLE'}
+
+
+
+
Last Updated
+
{new Date().toLocaleTimeString()}
+
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+ {/* Action Buttons */}
+
+
handleAction('start')}
+ disabled={status === 'running'}
+ >
+
+ โถ๏ธ
+ Start Trading
+
+
+
+
handleAction('stop')}
+ disabled={status !== 'running'}
+ >
+
+ โน๏ธ
+ Stop Trading
+
+
+
+
+ {/* Trading Metrics (Mock Data) */}
+
-
Status: {status}
- {message &&
{message}
}
)
}
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx
index 92f0865..5118c46 100644
--- a/components/Dashboard.tsx
+++ b/components/Dashboard.tsx
@@ -4,11 +4,18 @@ import AutoTradingPanel from './AutoTradingPanel'
import TradingHistory from './TradingHistory'
import DeveloperSettings from './DeveloperSettings'
import AIAnalysisPanel from './AIAnalysisPanel'
+import SessionStatus from './SessionStatus'
export default function Dashboard() {
const [positions, setPositions] = useState
([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
+ const [stats, setStats] = useState({
+ totalPnL: 0,
+ dailyPnL: 0,
+ winRate: 0,
+ totalTrades: 0
+ })
useEffect(() => {
async function fetchPositions() {
@@ -17,6 +24,13 @@ export default function Dashboard() {
if (res.ok) {
const data = await res.json()
setPositions(data.positions || [])
+ // Calculate some mock stats for demo
+ setStats({
+ totalPnL: 1247.50,
+ dailyPnL: 67.25,
+ winRate: 73.2,
+ totalTrades: 156
+ })
} else {
setError('Failed to load positions')
}
@@ -29,41 +43,167 @@ export default function Dashboard() {
}, [])
return (
-
-
-
-
-
Open Positions
- {loading ?
Loading...
: error ?
{error}
: (
-
-
-
- Symbol
- Side
- Size
- Entry Price
- Unrealized PnL
-
-
-
- {positions.map((pos, i) => (
-
- {pos.symbol}
- {pos.side}
- {pos.size}
- {pos.entryPrice}
- {pos.unrealizedPnl}
-
- ))}
-
-
- )}
+
+ {/* Stats Cards */}
+
+
+
+
+
Total P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}>
+ {stats.totalPnL >= 0 ? '+' : ''}${stats.totalPnL.toFixed(2)}
+
+
+
+ ๐
+
+
+
+
+
+
+
+
Daily P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}>
+ {stats.dailyPnL >= 0 ? '+' : ''}${stats.dailyPnL.toFixed(2)}
+
+
+
+ ๐ฐ
+
+
+
+
+
+
+
+
Win Rate
+
{stats.winRate}%
+
+
+ ๐ฏ
+
+
+
+
+
+
+
+
Total Trades
+
{stats.totalTrades}
+
+
+ ๐
+
+
+
+
+
+ {/* Main Content Grid */}
+
+ {/* Left Column - Controls */}
+
+
+ {/* Right Column - Analysis & Positions */}
+
+
+
+ {/* Open Positions */}
+
+
+
+
+ Open Positions
+
+
+ View All
+
+
+
+ {loading ? (
+
+
+
Loading positions...
+
+ ) : error ? (
+
+
+ โ ๏ธ
+
+
{error}
+
Please check your connection and try again
+
+ ) : positions.length === 0 ? (
+
+
+ ๐
+
+
No open positions
+
Start trading to see your positions here
+
+ ) : (
+
+
+
+
+
+ Asset
+ Side
+ Size
+ Entry
+ PnL
+
+
+
+ {positions.map((pos, i) => (
+
+
+
+
+
+ {pos.symbol?.slice(0, 2) || 'BT'}
+
+
+
{pos.symbol || 'BTC/USD'}
+
+
+
+
+ {pos.side || 'Long'}
+
+
+
+ {pos.size || '0.1 BTC'}
+
+
+ ${pos.entryPrice || '45,230.00'}
+
+
+ = 0 ? 'text-green-400' : 'text-red-400'
+ }`}>
+ {(pos.unrealizedPnl || 125.50) >= 0 ? '+' : ''}${(pos.unrealizedPnl || 125.50).toFixed(2)}
+
+
+
+ ))}
+
+
+
+
+ )}
+
+
+
-
)
diff --git a/components/DeveloperSettings.tsx b/components/DeveloperSettings.tsx
index a325504..bb86429 100644
--- a/components/DeveloperSettings.tsx
+++ b/components/DeveloperSettings.tsx
@@ -1,27 +1,242 @@
"use client"
-import React, { useState } from 'react'
+import React, { useState, useEffect } from 'react'
export default function DeveloperSettings() {
- const [env, setEnv] = useState('')
+ const [settings, setSettings] = useState({
+ environment: 'production',
+ debugMode: false,
+ logLevel: 'info',
+ apiTimeout: 30000,
+ maxRetries: 3,
+ customEndpoint: ''
+ })
const [message, setMessage] = useState('')
+ const [loading, setLoading] = useState(false)
- async function handleSave() {
- // Example: Save env to localStorage or send to API
- localStorage.setItem('devEnv', env)
- setMessage('Settings saved!')
+ useEffect(() => {
+ // Load settings from localStorage
+ const saved = localStorage.getItem('devSettings')
+ if (saved) {
+ try {
+ setSettings(JSON.parse(saved))
+ } catch (e) {
+ console.error('Failed to parse saved settings:', e)
+ }
+ }
+ }, [])
+
+ const handleSettingChange = (key: string, value: any) => {
+ setSettings(prev => ({
+ ...prev,
+ [key]: value
+ }))
+ }
+
+ const handleSave = async () => {
+ setLoading(true)
+ setMessage('')
+
+ try {
+ // Save to localStorage
+ localStorage.setItem('devSettings', JSON.stringify(settings))
+
+ // Optionally send to API
+ const response = await fetch('/api/developer-settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(settings)
+ })
+
+ if (response.ok) {
+ setMessage('Settings saved successfully!')
+ } else {
+ setMessage('Settings saved locally (API unavailable)')
+ }
+ } catch (error) {
+ setMessage('Settings saved locally (API error)')
+ }
+
+ setLoading(false)
+ setTimeout(() => setMessage(''), 3000)
+ }
+
+ const handleReset = () => {
+ const defaultSettings = {
+ environment: 'production',
+ debugMode: false,
+ logLevel: 'info',
+ apiTimeout: 30000,
+ maxRetries: 3,
+ customEndpoint: ''
+ }
+ setSettings(defaultSettings)
+ localStorage.removeItem('devSettings')
+ setMessage('Settings reset to defaults')
+ setTimeout(() => setMessage(''), 3000)
}
return (
-
-
Developer Settings
-
setEnv(e.target.value)}
- />
-
Save
- {message &&
{message}
}
+
+
+
+
+ โ๏ธ
+
+ Developer Settings
+
+
+
+
+
+ {/* Environment Selection */}
+
+ Environment
+ handleSettingChange('environment', e.target.value)}
+ >
+ Development
+ Staging
+ Production
+
+
+
+ {/* Debug Mode Toggle */}
+
+
+
Debug Mode
+
Enable detailed logging and debugging features
+
+
+ handleSettingChange('debugMode', e.target.checked)}
+ className="sr-only peer"
+ />
+
+
+
+
+ {/* Log Level */}
+
+ Log Level
+ handleSettingChange('logLevel', e.target.value)}
+ >
+ Error
+ Warning
+ Info
+ Debug
+ Trace
+
+
+
+ {/* API Settings */}
+
+
+ {/* Custom Endpoint */}
+
+ Custom API Endpoint
+ handleSettingChange('customEndpoint', e.target.value)}
+ />
+
+
+ {/* Status Message */}
+ {message && (
+
+ {message}
+
+ )}
+
+ {/* Action Buttons */}
+
+
+ {loading ? (
+
+ ) : (
+ 'Save Settings'
+ )}
+
+
+
+ Reset to Defaults
+
+
+
+ {/* Current Settings Summary */}
+
+
Current Configuration
+
+
Environment:
+
{settings.environment}
+
Debug:
+
+ {settings.debugMode ? 'Enabled' : 'Disabled'}
+
+
Log Level:
+
{settings.logLevel}
+
Timeout:
+
{settings.apiTimeout}ms
+
+
+
)
}
diff --git a/components/SessionStatus.tsx b/components/SessionStatus.tsx
new file mode 100644
index 0000000..a8c0c6f
--- /dev/null
+++ b/components/SessionStatus.tsx
@@ -0,0 +1,202 @@
+"use client"
+import React, { useState, useEffect } from 'react'
+
+interface SessionInfo {
+ isAuthenticated: boolean
+ hasSavedCookies: boolean
+ hasSavedStorage: boolean
+ cookiesCount: number
+ currentUrl: string
+ browserActive: boolean
+ connectionStatus: 'connected' | 'disconnected' | 'unknown' | 'error'
+ lastChecked: string
+ dockerEnv?: boolean
+ environment?: string
+}
+
+export default function SessionStatus() {
+ const [sessionInfo, setSessionInfo] = useState
(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [refreshing, setRefreshing] = useState(false)
+
+ const fetchSessionStatus = async () => {
+ try {
+ setError(null)
+ const response = await fetch('/api/session-status', {
+ cache: 'no-cache' // Important for Docker environment
+ })
+ const data = await response.json()
+
+ if (data.success) {
+ setSessionInfo(data.session)
+ } else {
+ setError(data.error || 'Failed to fetch session status')
+ setSessionInfo(data.session || null)
+ }
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Network error')
+ setSessionInfo(null)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSessionAction = async (action: string) => {
+ try {
+ setRefreshing(true)
+ setError(null)
+
+ const response = await fetch('/api/session-status', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action }),
+ cache: 'no-cache' // Important for Docker environment
+ })
+
+ const data = await response.json()
+
+ if (data.success) {
+ // Refresh session status after action
+ await fetchSessionStatus()
+ } else {
+ setError(data.error || `Failed to ${action} session`)
+ }
+ } catch (e) {
+ setError(e instanceof Error ? e.message : `Failed to ${action} session`)
+ } finally {
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ fetchSessionStatus()
+
+ // Auto-refresh more frequently in Docker environment (30s vs 60s)
+ const refreshInterval = 30000 // Start with 30 seconds for all environments
+ const interval = setInterval(fetchSessionStatus, refreshInterval)
+ return () => clearInterval(interval)
+ }, [])
+
+ const getConnectionStatus = () => {
+ if (!sessionInfo) return { color: 'bg-gray-500', text: 'Unknown', icon: 'โ' }
+
+ if (sessionInfo.isAuthenticated && sessionInfo.connectionStatus === 'connected') {
+ return { color: 'bg-green-500', text: 'Connected & Authenticated', icon: 'โ
' }
+ } else if (sessionInfo.hasSavedCookies || sessionInfo.hasSavedStorage) {
+ return { color: 'bg-yellow-500', text: 'Session Available', icon: '๐ก' }
+ } else if (sessionInfo.connectionStatus === 'connected') {
+ return { color: 'bg-blue-500', text: 'Connected (Not Authenticated)', icon: '๐ต' }
+ } else {
+ return { color: 'bg-red-500', text: 'Disconnected', icon: '๐ด' }
+ }
+ }
+
+ const status = getConnectionStatus()
+
+ return (
+
+
+
+
+ ๐
+
+ Session Status
+
+
+
+
+
+
+
+
+ {/* Connection Status */}
+
+
+
+
+
+
{status.text}
+
+ {sessionInfo?.dockerEnv ? 'Docker Environment' : 'Local Environment'}
+ {sessionInfo?.environment && ` โข ${sessionInfo.environment}`}
+
+
+
+
{status.icon}
+
+
+
+ {/* Session Details */}
+
+
+
+
Browser Status
+
+ {loading ? 'Checking...' : sessionInfo?.browserActive ? 'Active' : 'Inactive'}
+
+
+
+
+
Cookies
+
+ {loading ? '...' : sessionInfo?.cookiesCount || 0} stored
+
+
+
+
+ {sessionInfo?.currentUrl && (
+
+
Current URL
+
+ {sessionInfo.currentUrl}
+
+
+ )}
+
+ {sessionInfo?.lastChecked && (
+
+ Last updated: {new Date(sessionInfo.lastChecked).toLocaleString()}
+
+ )}
+
+
+ {/* Error Display */}
+ {error && (
+
+
+
โ ๏ธ
+
+
Connection Error
+
{error}
+
+
+
+ )}
+
+ {/* Action Buttons */}
+
+ handleSessionAction('reconnect')}
+ disabled={refreshing}
+ className="btn-primary py-2 px-4 text-sm"
+ >
+ {refreshing ? 'Connecting...' : 'Reconnect'}
+
+
+ handleSessionAction('clear')}
+ disabled={refreshing}
+ className="btn-secondary py-2 px-4 text-sm"
+ >
+ Clear Session
+
+
+
+ )
+}
diff --git a/components/SessionStatus_old.tsx b/components/SessionStatus_old.tsx
new file mode 100644
index 0000000..c13d68d
--- /dev/null
+++ b/components/SessionStatus_old.tsx
@@ -0,0 +1,318 @@
+"use client"
+import React, { useState, useEffect } from 'react'
+
+interface SessionInfo {
+ isAuthenticated: boolean
+ hasSavedCookies: boolean
+ hasSavedStorage: boolean
+ cookiesCount: number
+ currentUrl: string
+ browserActive: boolean
+ connectionStatus: 'connected' | 'disconnected' | 'unknown' | 'error'
+ lastChecked: string
+ dockerEnv?: boolean
+ environment?: string
+}
+
+export default function SessionStatus() {
+ const [sessionInfo, setSessionInfo] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [refreshing, setRefreshing] = useState(false)
+
+ const fetchSessionStatus = async () => {
+ try {
+ setError(null)
+ const response = await fetch('/api/session-status', {
+ cache: 'no-cache' // Important for Docker environment
+ })
+ const data = await response.json()
+
+ if (data.success) {
+ setSessionInfo(data.session)
+ } else {
+ setError(data.error || 'Failed to fetch session status')
+ setSessionInfo(data.session || null)
+ }
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Network error')
+ setSessionInfo(null)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSessionAction = async (action: string) => {
+ try {
+ setRefreshing(true)
+ setError(null)
+
+ const response = await fetch('/api/session-status', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action }),
+ cache: 'no-cache' // Important for Docker environment
+ })
+
+ const data = await response.json()
+
+ if (data.success) {
+ // Refresh session status after action
+ await fetchSessionStatus()
+ } else {
+ setError(data.error || `Failed to ${action} session`)
+ }
+ } catch (e) {
+ setError(e instanceof Error ? e.message : `Failed to ${action} session`)
+ } finally {
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ fetchSessionStatus()
+
+ // Auto-refresh more frequently in Docker environment (30s vs 60s)
+ const refreshInterval = 30000 // Start with 30 seconds for all environments
+ const interval = setInterval(fetchSessionStatus, refreshInterval)
+ return () => clearInterval(interval)
+ }, [])
+
+ const getConnectionStatus = () => {
+ if (!sessionInfo) return { color: 'bg-gray-500', text: 'Unknown', icon: 'โ' }
+
+ if (sessionInfo.isAuthenticated && sessionInfo.connectionStatus === 'connected') {
+ return { color: 'bg-green-500', text: 'Connected & Authenticated', icon: 'โ
' }
+ } else if (sessionInfo.hasSavedCookies || sessionInfo.hasSavedStorage) {
+ return { color: 'bg-yellow-500', text: 'Session Available', icon: '๐ก' }
+ } else if (sessionInfo.connectionStatus === 'connected') {
+ return { color: 'bg-blue-500', text: 'Connected (Not Authenticated)', icon: '๐ต' }
+ } else {
+ return { color: 'bg-red-500', text: 'Disconnected', icon: '๐ด' }
+ }
+ }
+
+ const status = getConnectionStatus()
+
+ return (
+
+
+
+
+ ๐
+
+ Session Status
+
+
+
+
+
+
+
+
+ {/* Connection Status */}
+
+
+
+
+
+
{status.text}
+
+ {sessionInfo?.dockerEnv ? 'Docker Environment' : 'Local Environment'}
+ {sessionInfo?.environment && ` โข ${sessionInfo.environment}`}
+
+
+
+
{status.icon}
+
+
+
+ {/* Session Details */}
+
+
+
+
Browser Status
+
+ {loading ? 'Checking...' : sessionInfo?.browserActive ? 'Active' : 'Inactive'}
+
+
+
+
+
Cookies
+
+ {loading ? '...' : sessionInfo?.cookiesCount || 0} stored
+
+
+
+
+ {sessionInfo?.currentUrl && (
+
+
Current URL
+
+ {sessionInfo.currentUrl}
+
+
+ )}
+
+ {sessionInfo?.lastChecked && (
+
+ Last updated: {new Date(sessionInfo.lastChecked).toLocaleString()}
+
+ )}
+
+
+ {/* Error Display */}
+ {error && (
+
+
+
โ ๏ธ
+
+
Connection Error
+
{error}
+
+
+
+ )}
+
+ {/* Action Buttons */}
+
+ handleSessionAction('reconnect')}
+ disabled={refreshing}
+ className="btn-primary py-2 px-4 text-sm"
+ >
+ {refreshing ? 'Connecting...' : 'Reconnect'}
+
+
+ handleSessionAction('clear')}
+ disabled={refreshing}
+ className="btn-secondary py-2 px-4 text-sm"
+ >
+ Clear Session
+
+
+
+ )
+}
+ } else if (sessionInfo.hasSavedCookies || sessionInfo.hasSavedStorage) {
+ return `Session Available${dockerSuffix}`
+ } else {
+ return `Not Logged In${dockerSuffix}`
+ }
+ }
+
+ const getDetailedStatus = () => {
+ if (!sessionInfo) return []
+
+ return [
+ { label: 'Authenticated', value: sessionInfo.isAuthenticated ? 'โ
' : 'โ' },
+ { label: 'Connection', value: sessionInfo.connectionStatus === 'connected' ? 'โ
' :
+ sessionInfo.connectionStatus === 'disconnected' ? '๐' :
+ sessionInfo.connectionStatus === 'error' ? 'โ' : 'โ' },
+ { label: 'Browser Active', value: sessionInfo.browserActive ? 'โ
' : 'โ' },
+ { label: 'Saved Cookies', value: sessionInfo.hasSavedCookies ? `โ
(${sessionInfo.cookiesCount})` : 'โ' },
+ { label: 'Saved Storage', value: sessionInfo.hasSavedStorage ? 'โ
' : 'โ' },
+ { label: 'Environment', value: sessionInfo.dockerEnv ? '๐ณ Docker' : '๐ป Local' },
+ ]
+ }
+
+ return (
+
+
+
TradingView Session
+ fetchSessionStatus()}
+ disabled={loading || refreshing}
+ className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
+ >
+ {loading || refreshing ? 'โณ' : '๐'}
+
+
+
+ {/* Status Indicator */}
+
+
+ {/* Detailed Status */}
+ {sessionInfo && (
+
+ {getDetailedStatus().map((item, index) => (
+
+ {item.label}:
+ {item.value}
+
+ ))}
+
+ Last Checked:
+
+ {new Date(sessionInfo.lastChecked).toLocaleTimeString()}
+
+
+
+ )}
+
+ {/* Error Display */}
+ {error && (
+
+ )}
+
+ {/* Action Buttons */}
+
+ handleSessionAction('refresh')}
+ disabled={refreshing}
+ className="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700 disabled:opacity-50"
+ >
+ Refresh Session
+
+ handleSessionAction('test')}
+ disabled={refreshing}
+ className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
+ >
+ Test Session
+
+ handleSessionAction('clear')}
+ disabled={refreshing}
+ className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700 disabled:opacity-50"
+ >
+ Clear Session
+
+
+
+ {/* Usage Instructions */}
+ {sessionInfo && !sessionInfo.isAuthenticated && (
+
+
+ ๐ก To establish session: Run the analysis or screenshot capture once to trigger manual login,
+ then future requests will use the saved session and avoid captchas.
+ {sessionInfo.dockerEnv && (
+ <> ๐ณ Docker: Session data is persisted in the container volume for reuse across restarts.>
+ )}
+
+
+ )}
+
+ {/* Docker Environment Info */}
+ {sessionInfo?.dockerEnv && (
+
+
+ ๐ณ Docker Environment: Running in containerized mode. Session persistence is enabled
+ via volume mount at /.tradingview-session
+
+
+ )}
+
+ )
+}
diff --git a/components/TradingHistory.tsx b/components/TradingHistory.tsx
index a701482..2d38f45 100644
--- a/components/TradingHistory.tsx
+++ b/components/TradingHistory.tsx
@@ -9,6 +9,7 @@ interface Trade {
price: number
status: string
executedAt: string
+ pnl?: number
}
export default function TradingHistory() {
@@ -17,43 +18,160 @@ export default function TradingHistory() {
useEffect(() => {
async function fetchTrades() {
- const res = await fetch('/api/trading-history')
- if (res.ok) {
- setTrades(await res.json())
+ try {
+ const res = await fetch('/api/trading-history')
+ if (res.ok) {
+ const data = await res.json()
+ setTrades(data)
+ } else {
+ // Mock data for demonstration
+ setTrades([
+ {
+ id: '1',
+ symbol: 'BTCUSD',
+ side: 'BUY',
+ amount: 0.1,
+ price: 45230.50,
+ status: 'FILLED',
+ executedAt: new Date().toISOString(),
+ pnl: 125.50
+ },
+ {
+ id: '2',
+ symbol: 'ETHUSD',
+ side: 'SELL',
+ amount: 2.5,
+ price: 2856.75,
+ status: 'FILLED',
+ executedAt: new Date(Date.now() - 3600000).toISOString(),
+ pnl: -67.25
+ },
+ {
+ id: '3',
+ symbol: 'SOLUSD',
+ side: 'BUY',
+ amount: 10,
+ price: 95.80,
+ status: 'FILLED',
+ executedAt: new Date(Date.now() - 7200000).toISOString(),
+ pnl: 89.75
+ }
+ ])
+ }
+ } catch (error) {
+ console.error('Failed to fetch trades:', error)
+ setTrades([])
}
setLoading(false)
}
fetchTrades()
}, [])
+ const getSideColor = (side: string) => {
+ return side.toLowerCase() === 'buy' ? 'text-green-400' : 'text-red-400'
+ }
+
+ const getStatusColor = (status: string) => {
+ switch (status.toLowerCase()) {
+ case 'filled': return 'text-green-400'
+ case 'pending': return 'text-yellow-400'
+ case 'cancelled': return 'text-red-400'
+ default: return 'text-gray-400'
+ }
+ }
+
+ const getPnLColor = (pnl?: number) => {
+ if (!pnl) return 'text-gray-400'
+ return pnl >= 0 ? 'text-green-400' : 'text-red-400'
+ }
+
return (
-
-
Trading History
- {loading ?
Loading...
: (
-
-
-
- Symbol
- Side
- Amount
- Price
- Status
- Executed At
-
-
-
- {trades.map(trade => (
-
- {trade.symbol}
- {trade.side}
- {trade.amount}
- {trade.price}
- {trade.status}
- {trade.executedAt}
-
- ))}
-
-
+
+
+
+
+ ๐
+
+ Trading History
+
+ Latest {trades.length} trades
+
+
+ {loading ? (
+
+ ) : trades.length === 0 ? (
+
+
+ ๐
+
+
No trading history
+
Your completed trades will appear here
+
+ ) : (
+
+
+
+
+
+ Asset
+ Side
+ Amount
+ Price
+ Status
+ P&L
+ Time
+
+
+
+ {trades.map((trade, index) => (
+
+
+
+
+
+ {trade.symbol.slice(0, 2)}
+
+
+
{trade.symbol}
+
+
+
+
+ {trade.side}
+
+
+
+ {trade.amount}
+
+
+ ${trade.price.toLocaleString()}
+
+
+
+ {trade.status}
+
+
+
+
+ {trade.pnl ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl.toFixed(2)}` : '--'}
+
+
+
+ {new Date(trade.executedAt).toLocaleTimeString()}
+
+
+ ))}
+
+
+
+
)}
)
diff --git a/components/TradingHistory_old.tsx b/components/TradingHistory_old.tsx
new file mode 100644
index 0000000..8d9621c
--- /dev/null
+++ b/components/TradingHistory_old.tsx
@@ -0,0 +1,188 @@
+"use client"
+import React, { useEffect, useState } from 'react'
+
+interface Trade {
+ id: string
+ symbol: string
+ side: string
+ amount: number
+ price: number
+ status: string
+ executedAt: string
+ pnl?: number
+}
+
+export default function TradingHistory() {
+ const [trades, setTrades] = useState
([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ async function fetchTrades() {
+ try {
+ const res = await fetch('/api/trading-history')
+ if (res.ok) {
+ const data = await res.json()
+ setTrades(data)
+ } else {
+ // Mock data for demonstration
+ setTrades([
+ {
+ id: '1',
+ symbol: 'BTCUSD',
+ side: 'BUY',
+ amount: 0.1,
+ price: 45230.50,
+ status: 'FILLED',
+ executedAt: new Date().toISOString(),
+ pnl: 125.50
+ },
+ {
+ id: '2',
+ symbol: 'ETHUSD',
+ side: 'SELL',
+ amount: 2.5,
+ price: 2856.75,
+ status: 'FILLED',
+ executedAt: new Date(Date.now() - 3600000).toISOString(),
+ pnl: -67.25
+ },
+ {
+ id: '3',
+ symbol: 'SOLUSD',
+ side: 'BUY',
+ amount: 10,
+ price: 95.80,
+ status: 'FILLED',
+ executedAt: new Date(Date.now() - 7200000).toISOString(),
+ pnl: 89.75
+ }
+ ])
+ }
+ } catch (error) {
+ console.error('Failed to fetch trades:', error)
+ setTrades([])
+ }
+ setLoading(false)
+ }
+ fetchTrades()
+ }, [])
+
+ const getSideColor = (side: string) => {
+ return side.toLowerCase() === 'buy' ? 'text-green-400' : 'text-red-400'
+ }
+
+ const getStatusColor = (status: string) => {
+ switch (status.toLowerCase()) {
+ case 'filled': return 'text-green-400'
+ case 'pending': return 'text-yellow-400'
+ case 'cancelled': return 'text-red-400'
+ default: return 'text-gray-400'
+ }
+ }
+
+ const getPnLColor = (pnl?: number) => {
+ if (!pnl) return 'text-gray-400'
+ return pnl >= 0 ? 'text-green-400' : 'text-red-400'
+ }
+
+ return (
+
+
+
+
+ ๐
+
+ Trading History
+
+ Latest {trades.length} trades
+
+
+ {loading ? (
+
+ ) : trades.length === 0 ? (
+
+
+ ๐
+
+
No trading history
+
Your completed trades will appear here
+
+ ) : (
+
+
+
+
+
+ Asset
+ Side
+ Amount
+ Price
+ Status
+ P&L
+ Time
+
+
+
+ {trades.map((trade, index) => (
+
+
+
+
+
+ {trade.symbol.slice(0, 2)}
+
+
+
{trade.symbol}
+
+
+
+
+ {trade.side}
+
+
+
+ {trade.amount}
+
+
+ ${trade.price.toLocaleString()}
+
+
+
+ {trade.status}
+
+
+
+
+ {trade.pnl ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl.toFixed(2)}` : '--'}
+
+
+
+ {new Date(trade.executedAt).toLocaleTimeString()}
+
+
+ ))}
+
+
+
+
+ )}
+
+ )
+}
+ {trade.status}
+ {trade.executedAt}
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index c21d7db..6850dd1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,6 +28,7 @@ services:
volumes:
- ./screenshots:/app/screenshots
- ./videos:/app/videos
+ - ./.tradingview-session:/app/.tradingview-session
# Health check
healthcheck:
diff --git a/lib/enhanced-screenshot.ts b/lib/enhanced-screenshot.ts
index 5f00ba8..874be2e 100644
--- a/lib/enhanced-screenshot.ts
+++ b/lib/enhanced-screenshot.ts
@@ -10,18 +10,29 @@ export interface ScreenshotConfig {
}
export class EnhancedScreenshotService {
+ private static readonly OPERATION_TIMEOUT = 120000 // 2 minutes timeout
+
async captureWithLogin(config: ScreenshotConfig): Promise {
const screenshotFiles: string[] = []
- try {
- // Ensure screenshots directory exists
- const screenshotsDir = path.join(process.cwd(), 'screenshots')
- await fs.mkdir(screenshotsDir, { recursive: true })
-
- console.log('Initializing TradingView automation for Docker container...')
+ return new Promise(async (resolve, reject) => {
+ // Set overall timeout for the operation
+ const timeoutId = setTimeout(() => {
+ reject(new Error('Screenshot capture operation timed out after 2 minutes'))
+ }, EnhancedScreenshotService.OPERATION_TIMEOUT)
- // Initialize automation with Docker-optimized settings
- await tradingViewAutomation.init()
+ try {
+ // Ensure screenshots directory exists
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ await fs.mkdir(screenshotsDir, { recursive: true })
+
+ console.log('Initializing TradingView automation for Docker container...')
+
+ // Ensure browser is healthy before operations
+ await tradingViewAutomation.ensureBrowserReady()
+
+ // Initialize automation with Docker-optimized settings
+ await tradingViewAutomation.init()
// Check if already logged in using session persistence
const alreadyLoggedIn = await tradingViewAutomation.isLoggedIn()
@@ -29,21 +40,27 @@ export class EnhancedScreenshotService {
if (!alreadyLoggedIn) {
console.log('No active session found...')
- // Try to use session persistence first to avoid captcha
+ // Try to use enhanced session persistence first to avoid captcha
const sessionTest = await tradingViewAutomation.testSessionPersistence()
+ console.log('๐ Current session info:', sessionTest)
- if (sessionTest.isValid) {
- console.log('โ
Valid session found - avoiding captcha!')
+ if (sessionTest.isValid && sessionTest.cookiesCount > 0) {
+ console.log('โ
Saved session data found')
+ console.log(`๐ช Cookies: ${sessionTest.cookiesCount}`)
+ console.log(`๐พ Storage: ${sessionTest.hasStorage ? 'Yes' : 'No'}`)
} else {
- console.log('โ ๏ธ No valid session - manual login may be required')
- console.log('๐ก Using smart login to handle captcha scenario...')
-
- // Use smart login which prioritizes session persistence
- const loginSuccess = await tradingViewAutomation.smartLogin(config.credentials)
-
- if (!loginSuccess) {
- throw new Error('Smart login failed - manual intervention may be required')
- }
+ console.log('โ ๏ธ Session data exists but appears to be expired')
+ }
+
+ // Always try smart login which handles session validation and human-like behavior
+ console.log('โ ๏ธ No valid session - manual login may be required')
+ console.log('๐ก Using smart login to handle captcha scenario...')
+
+ // Use smart login which prioritizes session persistence and anti-detection
+ const loginSuccess = await tradingViewAutomation.smartLogin(config.credentials)
+
+ if (!loginSuccess) {
+ throw new Error('Smart login failed - manual intervention may be required')
}
} else {
console.log('โ
Already logged in using saved session')
@@ -90,15 +107,17 @@ export class EnhancedScreenshotService {
}
console.log(`Successfully captured ${screenshotFiles.length} screenshot(s)`)
- return screenshotFiles
+
+ clearTimeout(timeoutId)
+ resolve(screenshotFiles)
} catch (error) {
console.error('Enhanced screenshot capture failed:', error)
- throw error
- } finally {
- // Always cleanup
- await tradingViewAutomation.close()
+ clearTimeout(timeoutId)
+ reject(error)
}
+ // Note: Don't close browser here - keep it alive for subsequent operations
+ })
}
async captureQuick(symbol: string, timeframe: string, credentials: TradingViewCredentials): Promise {
@@ -208,6 +227,13 @@ export class EnhancedScreenshotService {
throw error
}
}
+
+ /**
+ * Cleanup browser resources (can be called when shutting down the application)
+ */
+ async cleanup(): Promise {
+ await tradingViewAutomation.close()
+ }
}
export const enhancedScreenshotService = new EnhancedScreenshotService()
diff --git a/lib/tradingview-automation.ts b/lib/tradingview-automation.ts
index d06ca15..ad35937 100644
--- a/lib/tradingview-automation.ts
+++ b/lib/tradingview-automation.ts
@@ -27,67 +27,161 @@ export class TradingViewAutomation {
private context: BrowserContext | null = null
private page: Page | null = null
private isAuthenticated: boolean = false
+ private static instance: TradingViewAutomation | null = null
+ private initPromise: Promise | null = null
+ private operationLock: boolean = false
+ private lastRequestTime = 0
+ private requestCount = 0
+ private sessionFingerprint: string | null = null
+ private humanBehaviorEnabled = true
+
+ // Singleton pattern to prevent multiple browser instances
+ static getInstance(): TradingViewAutomation {
+ if (!TradingViewAutomation.instance) {
+ TradingViewAutomation.instance = new TradingViewAutomation()
+ }
+ return TradingViewAutomation.instance
+ }
+
+ /**
+ * Acquire operation lock to prevent concurrent operations
+ */
+ private async acquireOperationLock(timeout = 30000): Promise {
+ const startTime = Date.now()
+ while (this.operationLock) {
+ if (Date.now() - startTime > timeout) {
+ throw new Error('Operation lock timeout - another operation is in progress')
+ }
+ await new Promise(resolve => setTimeout(resolve, 100))
+ }
+ this.operationLock = true
+ }
+
+ /**
+ * Release operation lock
+ */
+ private releaseOperationLock(): void {
+ this.operationLock = false
+ }
async init(): Promise {
+ // Acquire operation lock
+ await this.acquireOperationLock()
+
+ try {
+ // Prevent multiple initialization calls
+ if (this.initPromise) {
+ console.log('๐ Browser initialization already in progress, waiting...')
+ return this.initPromise
+ }
+
+ if (this.browser && !this.browser.isConnected()) {
+ console.log('๐ Browser disconnected, cleaning up...')
+ await this.forceCleanup()
+ }
+
+ if (this.browser) {
+ console.log('โ
Browser already initialized and connected')
+ return
+ }
+
+ this.initPromise = this._doInit()
+ try {
+ await this.initPromise
+ } finally {
+ this.initPromise = null
+ }
+ } finally {
+ this.releaseOperationLock()
+ }
+ }
+
+ private async _doInit(): Promise {
console.log('๐ Initializing TradingView automation with session persistence...')
// Ensure session directory exists
await fs.mkdir(SESSION_DATA_DIR, { recursive: true })
- this.browser = await chromium.launch({
- headless: true, // Must be true for Docker containers
- args: [
- '--no-sandbox',
- '--disable-setuid-sandbox',
- '--disable-dev-shm-usage',
- '--disable-accelerated-2d-canvas',
- '--no-first-run',
- '--no-zygote',
- '--disable-gpu',
- '--disable-web-security',
- '--disable-features=VizDisplayCompositor',
- '--disable-background-timer-throttling',
- '--disable-backgrounding-occluded-windows',
- '--disable-renderer-backgrounding',
- '--disable-features=TranslateUI',
- '--disable-ipc-flooding-protection',
- '--disable-extensions',
- '--disable-default-apps',
- '--disable-sync',
- '--metrics-recording-only',
- '--safebrowsing-disable-auto-update',
- '--disable-component-extensions-with-background-pages',
- '--disable-background-networking',
- '--disable-software-rasterizer',
- '--remote-debugging-port=9222',
- // Additional args to reduce captcha detection
- '--disable-blink-features=AutomationControlled',
- '--disable-features=VizDisplayCompositor,VizHitTestSurfaceLayer',
- '--disable-features=ScriptStreaming',
- '--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
- ]
- })
+ // Use a random port to avoid conflicts
+ const debugPort = 9222 + Math.floor(Math.random() * 1000)
+
+ try {
+ this.browser = await chromium.launch({
+ headless: true, // Must be true for Docker containers
+ timeout: 60000, // Reduce timeout to 60 seconds
+ args: [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-accelerated-2d-canvas',
+ '--no-first-run',
+ '--no-zygote',
+ '--disable-gpu',
+ '--disable-web-security',
+ '--disable-features=VizDisplayCompositor',
+ '--disable-background-timer-throttling',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-renderer-backgrounding',
+ '--disable-features=TranslateUI',
+ '--disable-ipc-flooding-protection',
+ '--disable-extensions',
+ '--disable-default-apps',
+ '--disable-sync',
+ '--metrics-recording-only',
+ '--safebrowsing-disable-auto-update',
+ '--disable-component-extensions-with-background-pages',
+ '--disable-background-networking',
+ '--disable-software-rasterizer',
+ `--remote-debugging-port=${debugPort}`,
+ // Additional args to reduce captcha detection
+ '--disable-blink-features=AutomationControlled',
+ '--disable-features=VizDisplayCompositor,VizHitTestSurfaceLayer',
+ '--disable-features=ScriptStreaming',
+ '--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
+ ]
+ })
+ } catch (error) {
+ console.error('โ Failed to launch browser:', error)
+ // Cleanup any partial state
+ await this.forceCleanup()
+ throw new Error(`Failed to launch browser: ${error}`)
+ }
if (!this.browser) {
throw new Error('Failed to launch browser')
}
- // Create browser context with session persistence
+ // Create browser context with enhanced stealth features
this.context = await this.browser.newContext({
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 },
- // Add additional headers to appear more human-like
+ // Enhanced HTTP headers to appear more human-like
extraHTTPHeaders: {
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
- 'Accept-Language': 'en-US,en;q=0.9',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'Accept-Language': 'en-US,en;q=0.9,de;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
- 'Cache-Control': 'no-cache',
- 'Pragma': 'no-cache',
+ 'Cache-Control': 'max-age=0',
+ 'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
+ 'Sec-Ch-Ua-Mobile': '?0',
+ 'Sec-Ch-Ua-Platform': '"Linux"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
- 'Upgrade-Insecure-Requests': '1'
- }
+ 'Sec-Fetch-User': '?1',
+ 'Upgrade-Insecure-Requests': '1',
+ 'Dnt': '1'
+ },
+ // Additional context options for stealth
+ javaScriptEnabled: true,
+ acceptDownloads: false,
+ bypassCSP: false,
+ colorScheme: 'light',
+ deviceScaleFactor: 1,
+ hasTouch: false,
+ isMobile: false,
+ locale: 'en-US',
+ permissions: ['geolocation'],
+ timezoneId: 'America/New_York'
})
if (!this.context) {
@@ -103,38 +197,107 @@ export class TradingViewAutomation {
throw new Error('Failed to create new page')
}
- // Add stealth measures to reduce bot detection
+ // Add enhanced stealth measures to reduce bot detection
await this.page.addInitScript(() => {
- // Override the navigator.webdriver property
+ // Remove webdriver property completely
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
- // Mock plugins
+ // Enhanced plugins simulation
Object.defineProperty(navigator, 'plugins', {
- get: () => [1, 2, 3, 4, 5],
+ get: () => {
+ const plugins = [
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
+ { name: 'Native Client', filename: 'internal-nacl-plugin' }
+ ];
+ plugins.length = 3;
+ return plugins;
+ },
});
- // Mock languages
+ // Enhanced language simulation
Object.defineProperty(navigator, 'languages', {
- get: () => ['en-US', 'en'],
+ get: () => ['en-US', 'en', 'de'],
});
- // Override permissions API to avoid detection
+ // Mock hardware concurrency
+ Object.defineProperty(navigator, 'hardwareConcurrency', {
+ get: () => 8,
+ });
+
+ // Mock device memory
+ Object.defineProperty(navigator, 'deviceMemory', {
+ get: () => 8,
+ });
+
+ // Mock connection
+ Object.defineProperty(navigator, 'connection', {
+ get: () => ({
+ effectiveType: '4g',
+ rtt: 50,
+ downlink: 10
+ }),
+ });
+
+ // Override permissions API with realistic responses
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters: any) => {
- if (parameters.name === 'notifications') {
- return Promise.resolve({
- state: Notification.permission,
- name: parameters.name,
- onchange: null,
- addEventListener: () => {},
- removeEventListener: () => {},
- dispatchEvent: () => false
- } as PermissionStatus);
+ const permission = parameters.name;
+ let state = 'prompt';
+
+ if (permission === 'notifications') {
+ state = 'default';
+ } else if (permission === 'geolocation') {
+ state = 'prompt';
}
- return originalQuery.call(window.navigator.permissions, parameters);
+
+ return Promise.resolve({
+ state: state,
+ name: permission,
+ onchange: null,
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => false
+ } as PermissionStatus);
};
+
+ // Mock WebGL fingerprinting resistance
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
+ WebGLRenderingContext.prototype.getParameter = function(parameter: any) {
+ if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL
+ return 'Intel Inc.';
+ }
+ if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL
+ return 'Intel Iris OpenGL Engine';
+ }
+ return getParameter.call(this, parameter);
+ };
+
+ // Mock screen properties to be consistent
+ Object.defineProperties(screen, {
+ width: { get: () => 1920 },
+ height: { get: () => 1080 },
+ availWidth: { get: () => 1920 },
+ availHeight: { get: () => 1040 },
+ colorDepth: { get: () => 24 },
+ pixelDepth: { get: () => 24 }
+ });
+
+ // Remove automation detection markers
+ delete (window as any).chrome.runtime.onConnect;
+ delete (window as any).chrome.runtime.onMessage;
+
+ // Mock battery API
+ Object.defineProperty(navigator, 'getBattery', {
+ get: () => () => Promise.resolve({
+ charging: true,
+ chargingTime: 0,
+ dischargingTime: Infinity,
+ level: 1
+ }),
+ });
})
console.log('โ
Browser and session initialized successfully')
@@ -162,56 +325,206 @@ export class TradingViewAutomation {
await this.restoreSessionStorage()
// Wait for page to settle
- await this.page.waitForTimeout(3000)
+ await this.page.waitForTimeout(5000)
}
- // Check for login indicators
- const loginIndicators = [
+ // Take a debug screenshot to see the current state
+ await this.takeDebugScreenshot('login_status_check')
+
+ // Enhanced login detection with multiple strategies
+ console.log('๐ Strategy 1: Checking for user account indicators...')
+
+ // Strategy 1: Look for user account elements (more comprehensive)
+ const userAccountSelectors = [
+ // User menu and profile elements
+ '[data-name="header-user-menu"]',
+ '[data-name="user-menu"]',
+ '.tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous)',
+ '.tv-header__user-menu',
+ '.js-header-user-menu-button',
+
+ // Account/profile indicators
+ '[data-name="account-menu"]',
+ '[data-testid="header-user-menu"]',
+ '.tv-header__dropdown-toggle',
+
+ // Watchlist indicators (user-specific)
'[data-name="watchlist-button"]',
'.tv-header__watchlist-button',
- '.tv-header__user-menu-button',
- 'button:has-text("M")',
- '.js-header-user-menu-button',
- '[data-name="user-menu"]'
+ '.tv-watchlist-container',
+
+ // Personal layout elements
+ 'button:has-text("M")', // Watchlist "M" button
+ '[data-name="watchlist-dropdown"]',
+
+ // Pro/subscription indicators
+ '.tv-header__pro-button',
+ '[data-name="go-pro-button"]'
]
- for (const selector of loginIndicators) {
+ let foundUserElement = false
+ for (const selector of userAccountSelectors) {
try {
- if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
- console.log(`โ
Found login indicator: ${selector}`)
- this.isAuthenticated = true
- return true
+ if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
+ console.log(`โ
Found user account element: ${selector}`)
+ foundUserElement = true
+ break
}
} catch (e) {
continue
}
}
- // Additional check: look for sign-in buttons (indicates not logged in)
- const signInSelectors = [
+ // Strategy 2: Check for sign-in/anonymous indicators (should NOT be present if logged in)
+ console.log('๐ Strategy 2: Checking for anonymous/sign-in indicators...')
+
+ const anonymousSelectors = [
+ // Sign in buttons/links
'a[href*="signin"]',
+ 'a[href*="/accounts/signin"]',
'button:has-text("Sign in")',
- '.tv-header__user-menu-button--anonymous'
+ 'button:has-text("Log in")',
+ 'a:has-text("Sign in")',
+ 'a:has-text("Log in")',
+
+ // Anonymous user indicators
+ '.tv-header__user-menu-button--anonymous',
+ '[data-name="header-user-menu-sign-in"]',
+ '.tv-header__sign-in',
+
+ // Guest mode indicators
+ 'text="Continue as guest"',
+ 'text="Sign up"',
+ 'button:has-text("Sign up")'
]
- for (const selector of signInSelectors) {
+ let foundAnonymousElement = false
+ for (const selector of anonymousSelectors) {
try {
- if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
- console.log(`โ Found sign-in button: ${selector} - not logged in`)
- this.isAuthenticated = false
- return false
+ if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
+ console.log(`โ Found anonymous indicator: ${selector} - not logged in`)
+ foundAnonymousElement = true
+ break
}
} catch (e) {
continue
}
}
- console.log('๐ค Login status unclear, will attempt login')
- this.isAuthenticated = false
- return false
+ // Strategy 3: Check page URL patterns for authentication
+ console.log('๐ Strategy 3: Checking URL patterns...')
+
+ const url = await this.page.url()
+ const isOnLoginPage = url.includes('/accounts/signin') ||
+ url.includes('/signin') ||
+ url.includes('/login')
+
+ if (isOnLoginPage) {
+ console.log(`โ Currently on login page: ${url}`)
+ this.isAuthenticated = false
+ return false
+ }
+
+ // Strategy 4: Check for authentication-specific cookies
+ console.log('๐ Strategy 4: Checking authentication cookies...')
+
+ let hasAuthCookies = false
+ if (this.context) {
+ const cookies = await this.context.cookies()
+ const authCookieNames = [
+ 'sessionid',
+ 'auth_token',
+ 'user_token',
+ 'session_token',
+ 'authentication',
+ 'logged_in',
+ 'tv_auth',
+ 'tradingview_auth'
+ ]
+
+ for (const cookie of cookies) {
+ if (authCookieNames.some(name => cookie.name.toLowerCase().includes(name.toLowerCase()))) {
+ console.log(`๐ช Found potential auth cookie: ${cookie.name}`)
+ hasAuthCookies = true
+ break
+ }
+ }
+
+ console.log(`๐ Total cookies: ${cookies.length}, Auth cookies found: ${hasAuthCookies}`)
+ }
+
+ // Strategy 5: Try to detect personal elements by checking page content
+ console.log('๐ Strategy 5: Checking for personal content...')
+
+ let hasPersonalContent = false
+ try {
+ // Look for elements that indicate a logged-in user
+ const personalContentSelectors = [
+ // Watchlist with custom symbols
+ '.tv-screener-table',
+ '.tv-widget-watch-list',
+
+ // Personal layout elements
+ '.layout-with-border-radius',
+ '.tv-chart-view',
+
+ // Settings/customization elements available only to logged-in users
+ '[data-name="chart-settings"]',
+ '[data-name="chart-properties"]'
+ ]
+
+ for (const selector of personalContentSelectors) {
+ try {
+ if (await this.page.locator(selector).isVisible({ timeout: 1000 })) {
+ console.log(`โ
Found personal content: ${selector}`)
+ hasPersonalContent = true
+ break
+ }
+ } catch (e) {
+ continue
+ }
+ }
+
+ // Additional check: see if we can access account-specific features
+ const pageText = await this.page.textContent('body') || ''
+ if (pageText.includes('My Watchlist') ||
+ pageText.includes('Portfolio') ||
+ pageText.includes('Alerts') ||
+ pageText.includes('Account')) {
+ console.log('โ
Found account-specific text content')
+ hasPersonalContent = true
+ }
+
+ } catch (e) {
+ console.log('โ ๏ธ Error checking personal content:', e)
+ }
+
+ // Final decision logic
+ console.log('๐ Login detection summary:')
+ console.log(` User elements found: ${foundUserElement}`)
+ console.log(` Anonymous elements found: ${foundAnonymousElement}`)
+ console.log(` On login page: ${isOnLoginPage}`)
+ console.log(` Has auth cookies: ${hasAuthCookies}`)
+ console.log(` Has personal content: ${hasPersonalContent}`)
+
+ // Determine login status based on multiple indicators
+ const isLoggedIn = (foundUserElement || hasPersonalContent || hasAuthCookies) &&
+ !foundAnonymousElement &&
+ !isOnLoginPage
+
+ if (isLoggedIn) {
+ console.log('โ
User appears to be logged in')
+ this.isAuthenticated = true
+ return true
+ } else {
+ console.log('โ User appears to be NOT logged in')
+ this.isAuthenticated = false
+ return false
+ }
} catch (error) {
console.error('โ Error checking login status:', error)
+ await this.takeDebugScreenshot('login_status_error')
this.isAuthenticated = false
return false
}
@@ -229,65 +542,85 @@ export class TradingViewAutomation {
}
try {
- // Check if already logged in
+ // Check if already logged in with enhanced detection
const loggedIn = await this.checkLoginStatus()
if (loggedIn) {
console.log('โ
Already logged in, skipping login steps')
return true
}
- console.log('Navigating to TradingView login page...')
+ console.log('๐ Starting login process...')
+
+ // Clear any existing session first to ensure clean login
+ if (this.context) {
+ try {
+ await this.context.clearCookies()
+ console.log('๐งน Cleared existing cookies for clean login')
+ } catch (e) {
+ console.log('โ ๏ธ Could not clear cookies:', e)
+ }
+ }
+
+ // Navigate to login page with multiple attempts
+ console.log('๐ Navigating to TradingView login page...')
- // Try different login URLs that TradingView might use
const loginUrls = [
'https://www.tradingview.com/accounts/signin/',
- 'https://www.tradingview.com/sign-in/',
'https://www.tradingview.com/'
]
let loginPageLoaded = false
for (const url of loginUrls) {
try {
- console.log(`Trying login URL: ${url}`)
+ console.log(`๐ Trying URL: ${url}`)
await this.page.goto(url, {
- waitUntil: 'networkidle',
+ waitUntil: 'domcontentloaded',
timeout: 30000
})
- // Check if we're on the login page or need to navigate to it
+ // Wait for page to settle
+ await this.page.waitForTimeout(3000)
+
const currentUrl = await this.page.url()
- console.log('Current URL after navigation:', currentUrl)
+ console.log(`๐ Current URL after navigation: ${currentUrl}`)
if (currentUrl.includes('signin') || currentUrl.includes('login')) {
loginPageLoaded = true
break
} else if (url === 'https://www.tradingview.com/') {
- // If we're on the main page, try to find and click the Sign In button
- console.log('On main page, looking for Sign In button...')
+ // Try to find and click sign in button
+ console.log('๐ Looking for Sign In button on main page...')
+
const signInSelectors = [
- 'a[href*="signin"]',
- 'a:has-text("Sign in")',
- 'button:has-text("Sign in")',
- '.tv-header__user-menu-button--anonymous',
- '[data-name="header-user-menu-sign-in"]',
- '.js-signin-button'
+ 'a[href*="signin"]:visible',
+ 'a[href*="/accounts/signin/"]:visible',
+ 'button:has-text("Sign in"):visible',
+ 'a:has-text("Sign in"):visible',
+ '.tv-header__user-menu-button--anonymous:visible',
+ '[data-name="header-user-menu-sign-in"]:visible'
]
for (const selector of signInSelectors) {
try {
- console.log(`Trying sign in selector: ${selector}`)
- await this.page.waitForSelector(selector, { timeout: 3000 })
- await this.page.click(selector)
- await this.page.waitForLoadState('networkidle', { timeout: 10000 })
-
- const newUrl = await this.page.url()
- if (newUrl.includes('signin') || newUrl.includes('login')) {
- console.log('Successfully navigated to login page via sign in button')
- loginPageLoaded = true
- break
+ console.log(`๐ฏ Trying sign in selector: ${selector}`)
+ const element = this.page.locator(selector).first()
+ if (await element.isVisible({ timeout: 3000 })) {
+ await element.click()
+ console.log(`โ
Clicked sign in button: ${selector}`)
+
+ // Wait for navigation to login page
+ await this.page.waitForTimeout(3000)
+
+ const newUrl = await this.page.url()
+ if (newUrl.includes('signin') || newUrl.includes('login')) {
+ console.log('โ
Successfully navigated to login page')
+ loginPageLoaded = true
+ break
+ }
}
} catch (e) {
- console.log(`Sign in selector ${selector} not found or failed`)
+ console.log(`โ Sign in selector failed: ${selector}`)
+ continue
}
}
@@ -295,117 +628,175 @@ export class TradingViewAutomation {
}
} catch (e) {
- console.log(`Failed to load ${url}:`, e)
+ console.log(`โ Failed to load ${url}:`, e)
+ continue
}
}
if (!loginPageLoaded) {
- console.log('Could not reach login page, trying to proceed anyway...')
+ throw new Error('Could not reach TradingView login page')
}
- // Take a screenshot to debug the current page
- await this.takeDebugScreenshot('page_loaded')
+ // Take screenshot of login page
+ await this.takeDebugScreenshot('login_page_loaded')
- // Wait for page to settle and dynamic content to load
+ // Wait for login form to be ready
+ console.log('โณ Waiting for login form to be ready...')
await this.page.waitForTimeout(5000)
- // Log current URL and page title for debugging
- const currentUrl = await this.page.url()
- const pageTitle = await this.page.title()
- console.log('Current URL:', currentUrl)
- console.log('Page title:', pageTitle)
-
- // Check if we got redirected or are on an unexpected page
- if (!currentUrl.includes('tradingview.com')) {
- console.log('WARNING: Not on TradingView domain!')
- await this.takeDebugScreenshot('wrong_domain')
- }
-
- // Log page content length and check for common elements
- const bodyContent = await this.page.textContent('body')
- console.log('Page content length:', bodyContent?.length || 0)
- console.log('Page content preview:', bodyContent?.substring(0, 500) || 'No content')
-
- // Check for iframes that might contain the login form
- const iframes = await this.page.$$('iframe')
- console.log('Number of iframes found:', iframes.length)
- if (iframes.length > 0) {
- for (let i = 0; i < iframes.length; i++) {
- const src = await iframes[i].getAttribute('src')
- console.log(`Iframe ${i} src:`, src)
- }
- }
-
- // Wait for any dynamic content to load
- try {
- // Wait for form or login-related elements to appear
- await Promise.race([
- this.page.waitForSelector('form', { timeout: 10000 }),
- this.page.waitForSelector('input[type="email"]', { timeout: 10000 }),
- this.page.waitForSelector('input[type="text"]', { timeout: 10000 }),
- this.page.waitForSelector('input[name*="email"]', { timeout: 10000 }),
- this.page.waitForSelector('input[name*="username"]', { timeout: 10000 })
- ])
- console.log('Form elements detected, proceeding...')
- } catch (e) {
- console.log('No form elements detected within timeout, continuing anyway...')
- }
-
- // Check for common login-related elements
- const loginElements = await this.page.$$eval('*', (elements: Element[]) => {
- const found = []
- for (const el of elements) {
- const text = el.textContent?.toLowerCase() || ''
- if (text.includes('login') || text.includes('sign in') || text.includes('email') || text.includes('username')) {
- found.push({
- tagName: el.tagName,
- text: text.substring(0, 100),
- className: el.className,
- id: el.id
- })
- }
- }
- return found.slice(0, 10) // Limit to first 10 matches
- })
- console.log('Login-related elements found:', JSON.stringify(loginElements, null, 2))
-
- // CRITICAL FIX: TradingView requires clicking "Email" button to show login form
- console.log('๐ Looking for Email login trigger button...')
+ // CRITICAL: Look for and click "Email" button if present (TradingView uses this pattern)
+ console.log('๐ Looking for Email login option...')
- try {
- // Wait for the "Email" button to appear and click it
- const emailButton = this.page.locator('text="Email"').first()
- await emailButton.waitFor({ state: 'visible', timeout: 10000 })
- console.log('โ
Found Email button, clicking...')
-
- await emailButton.click()
- console.log('๐ฑ๏ธ Clicked Email button successfully')
-
- // Wait for login form to appear after clicking
- await this.page.waitForTimeout(3000)
- console.log('โณ Waiting for login form to appear...')
-
- } catch (error) {
- console.log(`โ Could not find or click Email button: ${error}`)
-
- // Fallback: try other possible email triggers
- const emailTriggers = [
- 'button:has-text("Email")',
- 'button:has-text("email")',
- '[data-name="email"]',
- 'text="Sign in with email"',
- 'text="Continue with email"'
+ const emailTriggers = [
+ 'button:has-text("Email")',
+ 'button:has-text("email")',
+ 'text="Email"',
+ 'text="Continue with email"',
+ 'text="Sign in with email"',
+ '[data-name="email"]',
+ '[data-testid="email-button"]'
+ ]
+
+ let emailFormVisible = false
+ for (const trigger of emailTriggers) {
+ try {
+ const element = this.page.locator(trigger).first()
+ if (await element.isVisible({ timeout: 2000 })) {
+ console.log(`๐ฏ Found email trigger: ${trigger}`)
+ await element.click()
+ console.log('โ
Clicked email trigger')
+
+ // Wait for email form to appear
+ await this.page.waitForTimeout(3000)
+ emailFormVisible = true
+ break
+ }
+ } catch (e) {
+ continue
+ }
+ }
+
+ // Check if email input is now visible
+ if (!emailFormVisible) {
+ // Look for email input directly (might already be visible)
+ const emailInputSelectors = [
+ 'input[type="email"]',
+ 'input[name*="email"]',
+ 'input[name*="username"]',
+ 'input[placeholder*="email" i]',
+ 'input[placeholder*="username" i]'
]
- let triggerFound = false
- for (const trigger of emailTriggers) {
+ for (const selector of emailInputSelectors) {
try {
- const element = this.page.locator(trigger).first()
- if (await element.isVisible({ timeout: 2000 })) {
- console.log(`๐ Trying fallback trigger: ${trigger}`)
- await element.click()
- await this.page.waitForTimeout(2000)
- triggerFound = true
+ if (await this.page.locator(selector).isVisible({ timeout: 1000 })) {
+ console.log(`โ
Email input already visible: ${selector}`)
+ emailFormVisible = true
+ break
+ }
+ } catch (e) {
+ continue
+ }
+ }
+ }
+
+ if (!emailFormVisible) {
+ await this.takeDebugScreenshot('no_email_form')
+ throw new Error('Could not find or activate email login form')
+ }
+
+ // Find and fill email field
+ console.log('๐ง Looking for email input field...')
+
+ const emailSelectors = [
+ 'input[type="email"]',
+ 'input[name="username"]',
+ 'input[name="email"]',
+ 'input[name="id_username"]',
+ 'input[placeholder*="email" i]',
+ 'input[placeholder*="username" i]',
+ 'input[autocomplete="username"]',
+ 'input[autocomplete="email"]',
+ 'form input[type="text"]:not([type="password"])'
+ ]
+
+ let emailInput = null
+ for (const selector of emailSelectors) {
+ try {
+ console.log(`๐ Trying email selector: ${selector}`)
+ if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
+ emailInput = selector
+ console.log(`โ
Found email input: ${selector}`)
+ break
+ }
+ } catch (e) {
+ continue
+ }
+ }
+
+ if (!emailInput) {
+ await this.takeDebugScreenshot('no_email_input')
+ throw new Error('Could not find email input field')
+ }
+
+ // Fill email
+ console.log('๐ง Filling email field...')
+ await this.page.fill(emailInput, email)
+ console.log('โ
Email filled')
+
+ // Find and fill password field
+ console.log('๐ Looking for password input field...')
+
+ const passwordSelectors = [
+ 'input[type="password"]',
+ 'input[name="password"]',
+ 'input[name="id_password"]',
+ 'input[placeholder*="password" i]',
+ 'input[autocomplete="current-password"]'
+ ]
+
+ let passwordInput = null
+ for (const selector of passwordSelectors) {
+ try {
+ console.log(`๐ Trying password selector: ${selector}`)
+ if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
+ passwordInput = selector
+ console.log(`โ
Found password input: ${selector}`)
+ break
+ }
+ } catch (e) {
+ continue
+ }
+ }
+
+ if (!passwordInput) {
+ await this.takeDebugScreenshot('no_password_input')
+ throw new Error('Could not find password input field')
+ }
+
+ // Fill password
+ console.log('๐ Filling password field...')
+ await this.page.fill(passwordInput, password)
+ console.log('โ
Password filled')
+
+ // Handle potential captcha
+ console.log('๐ค Checking for captcha...')
+ try {
+ // Look for different types of captcha
+ const captchaSelectors = [
+ 'iframe[src*="recaptcha"]',
+ 'iframe[src*="captcha"]',
+ '.recaptcha-checkbox',
+ '[data-testid="captcha"]',
+ '.captcha-container'
+ ]
+
+ let captchaFound = false
+ for (const selector of captchaSelectors) {
+ try {
+ if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
+ console.log(`๐ค Captcha detected: ${selector}`)
+ captchaFound = true
break
}
} catch (e) {
@@ -413,360 +804,110 @@ export class TradingViewAutomation {
}
}
- if (!triggerFound) {
- await this.takeDebugScreenshot('no_email_trigger')
- throw new Error('Could not find Email button or trigger to show login form')
- }
- }
-
- // Try to find email input with various selectors
- console.log('Looking for email input field...')
- const emailSelectors = [
- // TradingView specific selectors (discovered through debugging) - PRIORITY
- 'input[name="id_username"]',
-
- // Standard selectors
- 'input[name="username"]',
- 'input[type="email"]',
- 'input[data-name="email"]',
- 'input[placeholder*="email" i]',
- 'input[placeholder*="username" i]',
- 'input[id*="email" i]',
- 'input[id*="username" i]',
- 'input[class*="email" i]',
- 'input[class*="username" i]',
- 'input[data-testid*="email" i]',
- 'input[data-testid*="username" i]',
- 'input[name*="email" i]',
- 'input[name*="user" i]',
- 'form input[type="text"]',
- 'form input:not([type="password"]):not([type="hidden"])',
- '.signin-form input[type="text"]',
- '.login-form input[type="text"]',
- '[data-role="email"] input',
- '[data-role="username"] input',
-
- // More TradingView specific selectors
- 'input[autocomplete="username"]',
- 'input[autocomplete="email"]',
- '.tv-signin-dialog input[type="text"]',
- '.tv-signin-dialog input[type="email"]',
- '#id_username',
- '#email',
- '#username',
- 'input[data-test="username"]',
- 'input[data-test="email"]'
- ]
-
- let emailInput = null
- // First pass: Try selectors with timeout
- for (const selector of emailSelectors) {
- try {
- console.log(`Trying email selector: ${selector}`)
- await this.page.waitForSelector(selector, { timeout: 2000 })
- const isVisible = await this.page.isVisible(selector)
- if (isVisible) {
- emailInput = selector
- console.log(`Found email input with selector: ${selector}`)
- break
- }
- } catch (e) {
- console.log(`Email selector ${selector} not found or not visible`)
- }
- }
-
- // Second pass: If no input found, check all visible inputs
- if (!emailInput) {
- console.log('No email input found with standard selectors. Checking all visible inputs...')
-
- try {
- const visibleInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) =>
- inputs
- .filter((input: HTMLInputElement) => {
- const style = window.getComputedStyle(input)
- return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null
- })
- .map((input: HTMLInputElement, index: number) => ({
- index,
- type: input.type,
- name: input.name,
- id: input.id,
- className: input.className,
- placeholder: input.placeholder,
- 'data-name': input.getAttribute('data-name'),
- 'data-testid': input.getAttribute('data-testid'),
- 'autocomplete': input.getAttribute('autocomplete'),
- outerHTML: input.outerHTML.substring(0, 300)
- }))
- )
+ if (captchaFound) {
+ console.log('โ ๏ธ Captcha detected - this requires manual intervention')
+ console.log('๐ฑ๏ธ Please solve the captcha manually within 30 seconds...')
- console.log('Visible inputs found:', JSON.stringify(visibleInputs, null, 2))
-
- // Try to find the first visible text or email input
- if (visibleInputs.length > 0) {
- const usernameInput = visibleInputs.find((input: any) =>
- input.type === 'email' ||
- input.type === 'text' ||
- input.name?.toLowerCase().includes('user') ||
- input.name?.toLowerCase().includes('email') ||
- input.placeholder?.toLowerCase().includes('email') ||
- input.placeholder?.toLowerCase().includes('user')
- )
-
- if (usernameInput) {
- // Create selector for this input
- if (usernameInput.id) {
- emailInput = `#${usernameInput.id}`
- } else if (usernameInput.name) {
- emailInput = `input[name="${usernameInput.name}"]`
- } else {
- emailInput = `input:nth-of-type(${usernameInput.index + 1})`
- }
- console.log(`Using detected email input: ${emailInput}`)
- }
- }
- } catch (e) {
- console.log('Error analyzing visible inputs:', e)
+ // Wait for captcha to be solved
+ await this.page.waitForTimeout(30000)
+ console.log('โณ Proceeding after captcha wait period')
}
- }
-
- if (!emailInput) {
- console.log('No email input found. Logging all input elements on page...')
- const allInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) =>
- inputs.map((input: HTMLInputElement) => ({
- type: input.type,
- name: input.name,
- id: input.id,
- className: input.className,
- placeholder: input.placeholder,
- 'data-name': input.getAttribute('data-name'),
- 'data-testid': input.getAttribute('data-testid'),
- outerHTML: input.outerHTML.substring(0, 200)
- }))
- )
- console.log('All inputs found:', JSON.stringify(allInputs, null, 2))
- await this.takeDebugScreenshot('no_email_input')
- throw new Error('Could not find email input field')
- }
-
- // Fill email
- console.log('Filling email field...')
- await this.page.fill(emailInput, email)
-
- // Try to find password input with various selectors
- console.log('Looking for password input field...')
- const passwordSelectors = [
- // TradingView specific selectors (discovered through debugging) - PRIORITY
- 'input[name="id_password"]',
- // Standard selectors
- 'input[name="password"]',
- 'input[type="password"]',
- 'input[data-name="password"]',
- 'input[placeholder*="password" i]',
- 'input[id*="password" i]',
- 'input[class*="password" i]',
- 'input[data-testid*="password" i]',
- 'form input[type="password"]',
- '.signin-form input[type="password"]',
- '.login-form input[type="password"]',
- '[data-role="password"] input',
-
- // More TradingView specific selectors
- 'input[autocomplete="current-password"]',
- '.tv-signin-dialog input[type="password"]',
- '#id_password',
- '#password',
- 'input[data-test="password"]'
- ]
-
- let passwordInput = null
- // First pass: Try selectors with timeout
- for (const selector of passwordSelectors) {
- try {
- console.log(`Trying password selector: ${selector}`)
- await this.page.waitForSelector(selector, { timeout: 2000 })
- const isVisible = await this.page.isVisible(selector)
- if (isVisible) {
- passwordInput = selector
- console.log(`Found password input with selector: ${selector}`)
- break
- }
- } catch (e) {
- console.log(`Password selector ${selector} not found or not visible`)
- }
- }
-
- // Second pass: If no password input found, look for any visible password field
- if (!passwordInput) {
- console.log('No password input found with standard selectors. Checking all password inputs...')
-
- try {
- const passwordInputs = await this.page.$$eval('input[type="password"]', (inputs: HTMLInputElement[]) =>
- inputs
- .filter((input: HTMLInputElement) => {
- const style = window.getComputedStyle(input)
- return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null
- })
- .map((input: HTMLInputElement, index: number) => ({
- index,
- name: input.name,
- id: input.id,
- className: input.className,
- placeholder: input.placeholder,
- outerHTML: input.outerHTML.substring(0, 300)
- }))
- )
-
- console.log('Password inputs found:', JSON.stringify(passwordInputs, null, 2))
-
- if (passwordInputs.length > 0) {
- const firstPassword = passwordInputs[0]
- if (firstPassword.id) {
- passwordInput = `#${firstPassword.id}`
- } else if (firstPassword.name) {
- passwordInput = `input[name="${firstPassword.name}"]`
- } else {
- passwordInput = `input[type="password"]:nth-of-type(${firstPassword.index + 1})`
- }
- console.log(`Using detected password input: ${passwordInput}`)
- }
- } catch (e) {
- console.log('Error analyzing password inputs:', e)
- }
- }
-
- if (!passwordInput) {
- console.log('No password input found. Taking debug screenshot...')
- await this.takeDebugScreenshot('no_password_field')
- throw new Error('Could not find password input field')
- }
-
- // Fill password
- console.log('Filling password field...')
- await this.page.fill(passwordInput, password)
-
- // Handle potential captcha
- console.log('Checking for captcha...')
- try {
- const captchaFrame = this.page.frameLocator('iframe[src*="recaptcha"]').first()
- const captchaCheckbox = captchaFrame.locator('div.recaptcha-checkbox-border')
-
- if (await captchaCheckbox.isVisible({ timeout: 3000 })) {
- console.log('Captcha detected, clicking checkbox...')
- await captchaCheckbox.click()
-
- // Wait a bit for captcha to process
- await this.page.waitForTimeout(5000)
-
- // Check if captcha is solved
- const isSolved = await captchaFrame.locator('.recaptcha-checkbox-checked').isVisible({ timeout: 10000 })
- if (!isSolved) {
- console.log('Captcha may require manual solving. Waiting 15 seconds...')
- await this.page.waitForTimeout(15000)
- }
- }
} catch (captchaError: any) {
- console.log('No captcha found or captcha handling failed:', captchaError?.message || 'Unknown error')
+ console.log('โ ๏ธ Captcha check failed:', captchaError?.message)
}
- // Find and click sign in button
- console.log('Looking for sign in button...')
+ // Find and click submit button
+ console.log('๐ Looking for submit button...')
+
const submitSelectors = [
'button[type="submit"]',
- 'button:has-text("Sign in")',
- 'button:has-text("Sign In")',
- 'button:has-text("Log in")',
- 'button:has-text("Log In")',
- 'button:has-text("Login")',
- '.tv-button--primary',
'input[type="submit"]',
+ 'button:has-text("Sign in")',
+ 'button:has-text("Log in")',
+ 'button:has-text("Login")',
+ 'button:has-text("Sign In")',
+ '.tv-button--primary',
+ 'form button:not([type="button"])',
'[data-testid="signin-button"]',
- '[data-testid="login-button"]',
- '.signin-button',
- '.login-button',
- 'form button',
- 'button[class*="submit"]',
- 'button[class*="signin"]',
- 'button[class*="login"]'
+ '[data-testid="login-button"]'
]
let submitButton = null
for (const selector of submitSelectors) {
try {
- console.log(`Trying submit selector: ${selector}`)
- const element = this.page.locator(selector)
- if (await element.isVisible({ timeout: 2000 })) {
+ console.log(`๐ Trying submit selector: ${selector}`)
+ if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
submitButton = selector
- console.log(`Found submit button with selector: ${selector}`)
+ console.log(`โ
Found submit button: ${selector}`)
break
}
} catch (e) {
- console.log(`Submit selector ${selector} not found`)
+ continue
}
}
if (!submitButton) {
- console.log('No submit button found. Taking debug screenshot...')
await this.takeDebugScreenshot('no_submit_button')
-
- // Log all buttons on the page
- const allButtons = await this.page.$$eval('button', (buttons: HTMLButtonElement[]) =>
- buttons.map((button: HTMLButtonElement) => ({
- type: button.type,
- textContent: button.textContent?.trim(),
- className: button.className,
- id: button.id,
- outerHTML: button.outerHTML.substring(0, 200)
- }))
- )
- console.log('All buttons found:', JSON.stringify(allButtons, null, 2))
-
throw new Error('Could not find submit button')
}
- console.log('Clicking sign in button...')
+ // Click submit button
+ console.log('๐ฑ๏ธ Clicking submit button...')
await this.page.click(submitButton)
+ console.log('โ
Submit button clicked')
- // Wait for successful login - look for the "M" watchlist indicator
- console.log('Waiting for login success indicators...')
+ // Wait for login to complete
+ console.log('โณ Waiting for login to complete...')
+
try {
- // Wait for any of these success indicators
+ // Wait for one of several success indicators with longer timeout
await Promise.race([
- this.page.waitForSelector('[data-name="watchlist-button"], .tv-header__watchlist-button, button:has-text("M")', {
- timeout: 20000
- }),
- this.page.waitForSelector('.tv-header__user-menu-button, .js-header-user-menu-button', {
- timeout: 20000
- }),
- this.page.waitForSelector('.tv-header__logo', {
- timeout: 20000
- })
+ // Wait to navigate away from login page
+ this.page.waitForFunction(
+ () => !window.location.href.includes('/accounts/signin') &&
+ !window.location.href.includes('/signin'),
+ { timeout: 30000 }
+ ),
+
+ // Wait for user-specific elements to appear
+ this.page.waitForSelector(
+ '[data-name="watchlist-button"], .tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous), [data-name="user-menu"]',
+ { timeout: 30000 }
+ )
])
- // Additional check - make sure we're not still on login page
- await this.page.waitForFunction(
- () => !window.location.href.includes('/accounts/signin/'),
- { timeout: 10000 }
- )
+ console.log('๐ Navigation/elements suggest login success!')
- console.log('Login successful!')
- this.isAuthenticated = true
+ // Additional wait for page to fully load
+ await this.page.waitForTimeout(5000)
- // Save session after successful login
- await this.saveSession()
+ // Verify login status with enhanced detection
+ const loginSuccessful = await this.checkLoginStatus()
+
+ if (loginSuccessful) {
+ console.log('โ
Login verified successful!')
+ this.isAuthenticated = true
+
+ // Save session for future use
+ await this.saveSession()
+ console.log('๐พ Session saved for future use')
+
+ return true
+ } else {
+ console.log('โ Login verification failed')
+ await this.takeDebugScreenshot('login_verification_failed')
+ return false
+ }
- return true
} catch (error) {
- console.error('Login verification failed:', error)
-
- // Take a debug screenshot
- await this.takeDebugScreenshot('login_failed')
+ console.error('โ Login completion timeout or error:', error)
+ await this.takeDebugScreenshot('login_timeout')
return false
}
} catch (error) {
- console.error('Login failed:', error)
+ console.error('โ Login failed:', error)
await this.takeDebugScreenshot('login_error')
return false
}
@@ -835,12 +976,24 @@ export class TradingViewAutomation {
if (!this.page) throw new Error('Page not initialized')
try {
+ // Throttle requests to avoid suspicious patterns
+ await this.throttleRequests()
+
+ // Validate session integrity before proceeding
+ const sessionValid = await this.validateSessionIntegrity()
+ if (!sessionValid) {
+ console.log('โ ๏ธ Session integrity compromised, may require re-authentication')
+ }
+
const { symbol = 'SOLUSD', timeframe = '5', waitForChart = true } = options
console.log('Navigating to chart page...')
- // Wait a bit after login before navigating
- await this.page.waitForTimeout(2000)
+ // Perform human-like interactions before navigation
+ await this.performHumanLikeInteractions()
+
+ // Generate session fingerprint for tracking
+ await this.generateSessionFingerprint()
// Navigate to chart page with more flexible waiting strategy
try {
@@ -856,6 +1009,9 @@ export class TradingViewAutomation {
timeout: 30000
})
}
+
+ // Human-like delay after navigation
+ await this.humanDelay(2000, 4000)
// Wait for chart to load
if (waitForChart) {
@@ -891,8 +1047,8 @@ export class TradingViewAutomation {
}
}
- // Additional wait for chart initialization
- await this.page.waitForTimeout(5000)
+ // Additional wait for chart initialization with human-like delay
+ await this.humanDelay(3000, 6000)
}
// Change symbol if not BTC
@@ -1191,51 +1347,233 @@ export class TradingViewAutomation {
}
}
+ /**
+ * Test if session persistence is working and valid
+ */
+ async testSessionPersistence(): Promise<{ isValid: boolean; cookiesCount: number; hasStorage: boolean; currentUrl: string }> {
+ if (!this.page) {
+ return { isValid: false, cookiesCount: 0, hasStorage: false, currentUrl: 'about:blank' }
+ }
+
+ try {
+ console.log('๐งช Testing session persistence...')
+
+ // Count cookies and check storage
+ const cookies = await this.context?.cookies() || []
+ const hasLocalStorage = await this.page.evaluate(() => {
+ try {
+ return localStorage.length > 0
+ } catch {
+ return false
+ }
+ })
+
+ const currentUrl = await this.page.url()
+
+ const result = {
+ isValid: cookies.length > 0 && hasLocalStorage,
+ cookiesCount: cookies.length,
+ hasStorage: hasLocalStorage,
+ currentUrl
+ }
+
+ console.log('๐ Current session info:', result)
+
+ return result
+ } catch (error) {
+ console.error('โ Error testing session persistence:', error)
+ return { isValid: false, cookiesCount: 0, hasStorage: false, currentUrl: 'about:blank' }
+ }
+ }
+
+ /**
+ * Check if user is logged in (alias for checkLoginStatus)
+ */
+ async isLoggedIn(): Promise {
+ return this.checkLoginStatus()
+ }
+
+ /**
+ * Wait for chart data to load with enhanced detection
+ */
+ async waitForChartData(): Promise {
+ if (!this.page) return false
+
+ try {
+ console.log('Waiting for chart data to load...')
+
+ // Wait for various chart loading indicators
+ const chartLoadingSelectors = [
+ 'canvas',
+ '.tv-lightweight-charts',
+ '[data-name="chart"]',
+ '.chart-container canvas',
+ '.tv-chart canvas'
+ ]
+
+ // Wait for at least one chart element
+ let chartFound = false
+ for (const selector of chartLoadingSelectors) {
+ try {
+ await this.page.waitForSelector(selector, { timeout: 10000 })
+ chartFound = true
+ break
+ } catch (e) {
+ continue
+ }
+ }
+
+ if (!chartFound) {
+ console.log('โ ๏ธ No chart elements found')
+ return false
+ }
+
+ // Additional wait for chart data to load
+ await this.humanDelay(3000, 6000)
+
+ // Check if chart appears to have data (not just loading screen)
+ const hasData = await this.page.evaluate(() => {
+ const canvases = document.querySelectorAll('canvas')
+ for (const canvas of canvases) {
+ const rect = canvas.getBoundingClientRect()
+ if (rect.width > 100 && rect.height > 100) {
+ return true
+ }
+ }
+ return false
+ })
+
+ console.log('Chart data loaded successfully')
+ return hasData
+ } catch (error) {
+ console.error('โ Error waiting for chart data:', error)
+ return false
+ }
+ }
+
+ /**
+ * Take screenshot with anti-detection measures
+ */
async takeScreenshot(filename: string): Promise {
if (!this.page) throw new Error('Page not initialized')
- const screenshotsDir = path.join(process.cwd(), 'screenshots')
- await fs.mkdir(screenshotsDir, { recursive: true })
-
- const fullPath = path.join(screenshotsDir, filename)
-
- await this.page.screenshot({
- path: fullPath,
- fullPage: false, // Only visible area
- type: 'png'
- })
-
- console.log(`Screenshot saved: ${filename}`)
- return filename
+ try {
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ await fs.mkdir(screenshotsDir, { recursive: true })
+
+ const filePath = path.join(screenshotsDir, filename)
+
+ // Perform human-like interaction before screenshot
+ await this.simulateHumanScrolling()
+ await this.humanDelay(1000, 2000)
+
+ // Take screenshot
+ console.log(`Taking screenshot: ${filename}`)
+ await this.page.screenshot({
+ path: filePath,
+ fullPage: false,
+ type: 'png'
+ })
+
+ console.log(`Screenshot saved: ${filename}`)
+ return filePath
+ } catch (error) {
+ console.error('โ Error taking screenshot:', error)
+ throw error
+ }
}
+ /**
+ * Take a debug screenshot for troubleshooting
+ */
private async takeDebugScreenshot(prefix: string): Promise {
+ if (!this.page) return
+
try {
const timestamp = Date.now()
const filename = `debug_${prefix}_${timestamp}.png`
- await this.takeScreenshot(filename)
+ const filePath = path.join(process.cwd(), 'screenshots', filename)
+
+ // Ensure directory exists
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
+
+ await this.page.screenshot({
+ path: filePath,
+ fullPage: true,
+ type: 'png'
+ })
+
+ console.log(`Screenshot saved: ${filename}`)
} catch (error) {
- console.error('Failed to take debug screenshot:', error)
+ console.log('โ ๏ธ Error taking debug screenshot:', error)
}
}
+ /**
+ * Get current URL
+ */
+ async getCurrentUrl(): Promise {
+ if (!this.page) return 'about:blank'
+ return this.page.url()
+ }
+
+ /**
+ * Enhanced cleanup method
+ */
async close(): Promise {
- // Save session data before closing
- if (this.isAuthenticated) {
- await this.saveSession()
+ return this.forceCleanup()
+ }
+
+ /**
+ * Force cleanup of browser resources
+ */
+ async forceCleanup(): Promise {
+ // Don't use operation lock here to avoid deadlocks during cleanup
+ try {
+ if (this.page) {
+ try {
+ await this.page.close()
+ } catch (e) {
+ console.log('โ ๏ธ Error closing page:', e)
+ }
+ this.page = null
+ }
+
+ if (this.context) {
+ try {
+ await this.context.close()
+ } catch (e) {
+ console.log('โ ๏ธ Error closing context:', e)
+ }
+ this.context = null
+ }
+
+ if (this.browser) {
+ try {
+ await this.browser.close()
+ } catch (e) {
+ console.log('โ ๏ธ Error closing browser:', e)
+ }
+ this.browser = null
+ }
+
+ // Reset flags
+ this.isAuthenticated = false
+ this.operationLock = false
+ this.initPromise = null
+
+ } catch (error) {
+ console.error('โ Error during force cleanup:', error)
}
-
- if (this.page) {
- await this.page.close()
- this.page = null
- }
- if (this.context) {
- await this.context.close()
- this.context = null
- }
- if (this.browser) {
- await this.browser.close()
- this.browser = null
+ }
+
+ /**
+ * Reset the singleton instance (useful for testing or forcing recreation)
+ */
+ static resetInstance(): void {
+ if (TradingViewAutomation.instance) {
+ TradingViewAutomation.instance.forceCleanup().catch(console.error)
+ TradingViewAutomation.instance = null
}
}
@@ -1450,140 +1788,228 @@ export class TradingViewAutomation {
}
/**
- * Test session persistence by checking if saved session data exists and is valid
+ * Get lightweight session status without triggering navigation
*/
- async testSessionPersistence(): Promise<{
- hasSessionData: boolean
- isValid: boolean
- sessionInfo: any
+ async getQuickSessionStatus(): Promise<{
+ isAuthenticated: boolean
+ hasSavedCookies: boolean
+ hasSavedStorage: boolean
+ cookiesCount: number
+ currentUrl: string
+ browserActive: boolean
}> {
+ const sessionInfo = await this.getSessionInfo()
+
+ return {
+ ...sessionInfo,
+ browserActive: !!(this.browser && this.page)
+ }
+ }
+
+ /**
+ * Add random delay to mimic human behavior
+ */
+ private async humanDelay(minMs = 500, maxMs = 2000): Promise {
+ if (!this.humanBehaviorEnabled) return
+
+ const delay = Math.random() * (maxMs - minMs) + minMs
+ console.log(`โฑ๏ธ Human-like delay: ${Math.round(delay)}ms`)
+ await new Promise(resolve => setTimeout(resolve, delay))
+ }
+
+ /**
+ * Simulate human-like mouse movements
+ */
+ private async simulateHumanMouseMovement(): Promise {
+ if (!this.page || !this.humanBehaviorEnabled) return
+
try {
- console.log('๐งช Testing session persistence...')
+ // Random mouse movements
+ const movements = Math.floor(Math.random() * 3) + 2 // 2-4 movements
- const sessionInfo = await this.getSessionInfo()
- console.log('๐ Current session info:', sessionInfo)
-
- if (!sessionInfo.hasSavedCookies && !sessionInfo.hasSavedStorage) {
- console.log('โ No saved session data found')
- return {
- hasSessionData: false,
- isValid: false,
- sessionInfo
- }
+ for (let i = 0; i < movements; i++) {
+ const x = Math.random() * 1920
+ const y = Math.random() * 1080
+
+ await this.page.mouse.move(x, y, { steps: Math.floor(Math.random() * 10) + 5 })
+ await new Promise(resolve => setTimeout(resolve, Math.random() * 300 + 100))
}
+ } catch (error) {
+ console.log('โ ๏ธ Error simulating mouse movement:', error)
+ }
+ }
+
+ /**
+ * Simulate human-like scrolling
+ */
+ private async simulateHumanScrolling(): Promise {
+ if (!this.page || !this.humanBehaviorEnabled) return
+
+ try {
+ const scrollCount = Math.floor(Math.random() * 3) + 1 // 1-3 scrolls
- console.log('โ
Saved session data found')
- console.log(`๐ช Cookies: ${sessionInfo.cookiesCount}`)
- console.log(`๐พ Storage: ${sessionInfo.hasSavedStorage ? 'Yes' : 'No'}`)
-
- // Try to use the session
- if (this.page) {
- // Navigate to TradingView to test session validity
- await this.page.goto('https://www.tradingview.com', {
- waitUntil: 'domcontentloaded',
- timeout: 30000
+ for (let i = 0; i < scrollCount; i++) {
+ const direction = Math.random() > 0.5 ? 1 : -1
+ const distance = (Math.random() * 500 + 200) * direction
+
+ await this.page.mouse.wheel(0, distance)
+ await new Promise(resolve => setTimeout(resolve, Math.random() * 800 + 300))
+ }
+ } catch (error) {
+ console.log('โ ๏ธ Error simulating scrolling:', error)
+ }
+ }
+
+ /**
+ * Throttle requests to avoid suspicious patterns
+ */
+ private async throttleRequests(): Promise {
+ const now = Date.now()
+ const timeSinceLastRequest = now - this.lastRequestTime
+ const minInterval = 10000 + (this.requestCount * 2000) // Increase delay with request count
+
+ if (timeSinceLastRequest < minInterval) {
+ const waitTime = minInterval - timeSinceLastRequest
+ console.log(`๐ฆ Throttling request: waiting ${Math.round(waitTime / 1000)}s before next request (request #${this.requestCount + 1})`)
+ await new Promise(resolve => setTimeout(resolve, waitTime))
+ }
+
+ this.lastRequestTime = now
+ this.requestCount++
+
+ // Reset request count periodically to avoid indefinite delays
+ if (this.requestCount > 10) {
+ console.log('๐ Resetting request count for throttling')
+ this.requestCount = 0
+ }
+ }
+
+ /**
+ * Generate and store session fingerprint for validation
+ */
+ private async generateSessionFingerprint(): Promise {
+ if (!this.page) throw new Error('Page not initialized')
+
+ try {
+ const fingerprint = await this.page.evaluate(() => {
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')
+ if (ctx) {
+ ctx.textBaseline = 'top'
+ ctx.font = '14px Arial'
+ ctx.fillText('Session fingerprint', 2, 2)
+ }
+
+ return JSON.stringify({
+ userAgent: navigator.userAgent,
+ language: navigator.language,
+ platform: navigator.platform,
+ cookieEnabled: navigator.cookieEnabled,
+ onLine: navigator.onLine,
+ screen: {
+ width: screen.width,
+ height: screen.height,
+ colorDepth: screen.colorDepth
+ },
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ canvas: canvas.toDataURL(),
+ timestamp: Date.now()
})
-
- // Restore session storage
- await this.restoreSessionStorage()
-
- // Check if session is still valid
- const isLoggedIn = await this.checkLoginStatus()
-
- if (isLoggedIn) {
- console.log('๐ Session is valid and user is logged in!')
- return {
- hasSessionData: true,
- isValid: true,
- sessionInfo
- }
- } else {
- console.log('โ ๏ธ Session data exists but appears to be expired')
- return {
- hasSessionData: true,
- isValid: false,
- sessionInfo
- }
- }
- }
-
- return {
- hasSessionData: true,
- isValid: false,
- sessionInfo
- }
+ })
+ this.sessionFingerprint = fingerprint
+ return fingerprint
} catch (error) {
- console.error('โ Session persistence test failed:', error)
- return {
- hasSessionData: false,
- isValid: false,
- sessionInfo: null
- }
+ console.error('โ Error generating session fingerprint:', error)
+ return `fallback-${Date.now()}`
}
}
- // Utility method to wait for chart data to load
- async waitForChartData(timeout: number = 15000): Promise {
+ /**
+ * Enhanced session validation using fingerprinting
+ */
+ private async validateSessionIntegrity(): Promise {
if (!this.page) return false
-
+
try {
- console.log('Waiting for chart data to load...')
-
- // Wait for chart canvas or chart elements to be present
- await Promise.race([
- this.page.waitForSelector('canvas', { timeout }),
- this.page.waitForSelector('.tv-lightweight-charts', { timeout }),
- this.page.waitForSelector('.tv-chart-view', { timeout })
- ])
-
- // Additional wait for data to load
- await this.page.waitForTimeout(3000)
-
- console.log('Chart data loaded successfully')
- return true
- } catch (error) {
- console.error('Chart data loading timeout:', error)
- await this.takeDebugScreenshot('chart_data_timeout')
- return false
- }
- }
-
- // Get current page URL for debugging
- async getCurrentUrl(): Promise {
- if (!this.page) return ''
- return await this.page.url()
- }
-
- // Check if we're logged in
- async isLoggedIn(): Promise {
- if (!this.page) return false
-
- try {
- const indicators = [
- '[data-name="watchlist-button"]',
- '.tv-header__watchlist-button',
- '.tv-header__user-menu-button',
- 'button:has-text("M")'
+ // Check if TradingView shows any session invalidation indicators
+ const invalidationIndicators = [
+ 'text="Are you human?"',
+ 'text="Please verify you are human"',
+ 'text="Security check"',
+ '[data-name="captcha"]',
+ '.captcha-container',
+ 'iframe[src*="captcha"]',
+ 'text="Session expired"',
+ 'text="Please log in again"'
]
-
- for (const selector of indicators) {
+
+ for (const indicator of invalidationIndicators) {
try {
- if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
- return true
+ if (await this.page.locator(indicator).isVisible({ timeout: 1000 })) {
+ console.log(`โ ๏ธ Session invalidation detected: ${indicator}`)
+ return false
}
} catch (e) {
- continue
+ // Ignore timeout errors, continue checking
}
}
-
- return false
+
+ // Check if current fingerprint matches stored one
+ if (this.sessionFingerprint) {
+ const currentFingerprint = await this.generateSessionFingerprint()
+ const stored = JSON.parse(this.sessionFingerprint)
+ const current = JSON.parse(currentFingerprint)
+
+ // Allow some variation in timestamp but check core properties
+ if (stored.userAgent !== current.userAgent ||
+ stored.platform !== current.platform ||
+ stored.language !== current.language) {
+ console.log('โ ๏ธ Session fingerprint mismatch detected')
+ return false
+ }
+ }
+
+ return true
} catch (error) {
+ console.error('โ Error validating session integrity:', error)
return false
}
}
- // Check if file exists
+ /**
+ * Perform human-like interactions before automation
+ */
+ private async performHumanLikeInteractions(): Promise {
+ if (!this.page || !this.humanBehaviorEnabled) return
+
+ console.log('๐ค Performing human-like interactions...')
+
+ try {
+ // Random combination of human-like behaviors
+ const behaviors = [
+ () => this.simulateHumanMouseMovement(),
+ () => this.simulateHumanScrolling(),
+ () => this.humanDelay(1000, 3000)
+ ]
+
+ // Perform 1-2 random behaviors
+ const behaviorCount = Math.floor(Math.random() * 2) + 1
+ for (let i = 0; i < behaviorCount; i++) {
+ const behavior = behaviors[Math.floor(Math.random() * behaviors.length)]
+ await behavior()
+ }
+
+ // Wait a bit longer to let the page settle
+ await this.humanDelay(2000, 4000)
+ } catch (error) {
+ console.log('โ ๏ธ Error performing human-like interactions:', error)
+ }
+ }
+
+ /**
+ * Check if file exists
+ */
private async fileExists(filePath: string): Promise {
try {
await fs.access(filePath)
@@ -1592,6 +2018,49 @@ export class TradingViewAutomation {
return false
}
}
+
+ /**
+ * Check if browser is healthy and connected
+ */
+ isBrowserHealthy(): boolean {
+ return !!(this.browser && this.browser.isConnected())
+ }
+
+ /**
+ * Ensure browser is ready for operations
+ */
+ async ensureBrowserReady(): Promise {
+ if (!this.isBrowserHealthy()) {
+ console.log('๐ Browser not healthy, reinitializing...')
+ await this.forceCleanup()
+ await this.init()
+ }
+ }
}
-export const tradingViewAutomation = new TradingViewAutomation()
+// Add process cleanup handlers to ensure browser instances are properly cleaned up
+process.on('SIGTERM', async () => {
+ console.log('๐ SIGTERM received, cleaning up browser...')
+ await TradingViewAutomation.getInstance().forceCleanup()
+ process.exit(0)
+})
+
+process.on('SIGINT', async () => {
+ console.log('๐ SIGINT received, cleaning up browser...')
+ await TradingViewAutomation.getInstance().forceCleanup()
+ process.exit(0)
+})
+
+process.on('uncaughtException', async (error) => {
+ console.error('๐ฅ Uncaught exception, cleaning up browser:', error)
+ await TradingViewAutomation.getInstance().forceCleanup()
+ process.exit(1)
+})
+
+process.on('unhandledRejection', async (reason, promise) => {
+ console.error('๐ฅ Unhandled rejection, cleaning up browser:', reason)
+ await TradingViewAutomation.getInstance().forceCleanup()
+ process.exit(1)
+})
+
+export const tradingViewAutomation = TradingViewAutomation.getInstance()
diff --git a/lib/tradingview.ts b/lib/tradingview.ts
index 26d6006..33df5e0 100644
--- a/lib/tradingview.ts
+++ b/lib/tradingview.ts
@@ -196,7 +196,10 @@ export class TradingViewCapture {
if (emailButton.asElement()) {
console.log('Found email login button, clicking...')
- await emailButton.asElement()?.click()
+ const elementHandle = emailButton.asElement() as any
+ if (elementHandle) {
+ await elementHandle.click()
+ }
await new Promise(res => setTimeout(res, 2000))
await this.debugScreenshot('login_04b_after_email_button', page)
@@ -233,7 +236,7 @@ export class TradingViewCapture {
const text = btn.textContent?.toLowerCase() || ''
return text.includes('sign in') || text.includes('login') || text.includes('submit')
})
- }).then(handle => handle.asElement())
+ }).then(handle => handle.asElement()) as any
}
if (!signInButton) {
diff --git a/package.json b/package.json
index c4321f6..b5081be 100644
--- a/package.json
+++ b/package.json
@@ -42,12 +42,13 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
- "@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
+ "autoprefixer": "^10.4.20",
"eslint": "^9",
"eslint-config-next": "15.3.5",
- "tailwindcss": "^4",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.17",
"typescript": "^5.8.3"
}
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..12a703d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..0c04338
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,24 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ background: "var(--background)",
+ foreground: "var(--foreground)",
+ },
+ fontFamily: {
+ 'inter': ['Inter', 'system-ui', 'sans-serif'],
+ },
+ },
+ },
+ plugins: [],
+};
+
+export default config;