- Add tailwind.config.ts with proper content paths and theme config - Add postcss.config.js for Tailwind and autoprefixer processing - Downgrade tailwindcss to v3.4.17 and add missing PostCSS dependencies - Update Dockerfile to clarify build process - Fix UI styling issues in Docker environment
319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
"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<SessionInfo | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(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 (
|
||
<div className="card card-gradient">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className="text-lg font-bold text-white flex items-center">
|
||
<span className="w-8 h-8 bg-gradient-to-br from-blue-400 to-indigo-600 rounded-lg flex items-center justify-center mr-3">
|
||
🌐
|
||
</span>
|
||
Session Status
|
||
</h2>
|
||
<button
|
||
onClick={fetchSessionStatus}
|
||
disabled={loading || refreshing}
|
||
className="p-2 text-gray-400 hover:text-gray-300 transition-colors"
|
||
title="Refresh Status"
|
||
>
|
||
<svg className={`w-4 h-4 ${(loading || refreshing) ? 'animate-spin' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Connection Status */}
|
||
<div className="mb-6">
|
||
<div className="flex items-center justify-between p-4 bg-gray-800/30 rounded-lg border border-gray-700">
|
||
<div className="flex items-center space-x-3">
|
||
<div className={`w-3 h-3 ${status.color} rounded-full ${sessionInfo?.isAuthenticated ? 'animate-pulse' : ''}`}></div>
|
||
<div>
|
||
<h3 className="text-sm font-semibold text-white">{status.text}</h3>
|
||
<p className="text-xs text-gray-400">
|
||
{sessionInfo?.dockerEnv ? 'Docker Environment' : 'Local Environment'}
|
||
{sessionInfo?.environment && ` • ${sessionInfo.environment}`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<span className="text-2xl">{status.icon}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Session Details */}
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-3 bg-gray-800/20 rounded-lg">
|
||
<div className="text-xs text-gray-400">Browser Status</div>
|
||
<div className={`text-sm font-medium ${sessionInfo?.browserActive ? 'text-green-400' : 'text-red-400'}`}>
|
||
{loading ? 'Checking...' : sessionInfo?.browserActive ? 'Active' : 'Inactive'}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-3 bg-gray-800/20 rounded-lg">
|
||
<div className="text-xs text-gray-400">Cookies</div>
|
||
<div className="text-sm font-medium text-white">
|
||
{loading ? '...' : sessionInfo?.cookiesCount || 0} stored
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{sessionInfo?.currentUrl && (
|
||
<div className="p-3 bg-gray-800/20 rounded-lg">
|
||
<div className="text-xs text-gray-400 mb-1">Current URL</div>
|
||
<div className="text-xs text-gray-300 font-mono break-all">
|
||
{sessionInfo.currentUrl}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{sessionInfo?.lastChecked && (
|
||
<div className="text-xs text-gray-500 text-center">
|
||
Last updated: {new Date(sessionInfo.lastChecked).toLocaleString()}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Error Display */}
|
||
{error && (
|
||
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||
<div className="flex items-start space-x-2">
|
||
<span className="text-red-400 text-sm">⚠️</span>
|
||
<div>
|
||
<h4 className="text-red-400 font-medium text-sm">Connection Error</h4>
|
||
<p className="text-red-300 text-xs mt-1">{error}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Action Buttons */}
|
||
<div className="mt-6 grid grid-cols-2 gap-3">
|
||
<button
|
||
onClick={() => handleSessionAction('reconnect')}
|
||
disabled={refreshing}
|
||
className="btn-primary py-2 px-4 text-sm"
|
||
>
|
||
{refreshing ? 'Connecting...' : 'Reconnect'}
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => handleSessionAction('clear')}
|
||
disabled={refreshing}
|
||
className="btn-secondary py-2 px-4 text-sm"
|
||
>
|
||
Clear Session
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
} 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 (
|
||
<div className="bg-gray-900 rounded-lg shadow p-4">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-lg font-semibold text-white">TradingView Session</h3>
|
||
<button
|
||
onClick={() => 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 ? '⟳' : '🔄'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Status Indicator */}
|
||
<div className="flex items-center space-x-3 mb-4">
|
||
<div className={`w-3 h-3 rounded-full ${getStatusColor()}`}></div>
|
||
<span className="text-white font-medium">{getStatusText()}</span>
|
||
</div>
|
||
|
||
{/* Detailed Status */}
|
||
{sessionInfo && (
|
||
<div className="space-y-2 mb-4">
|
||
{getDetailedStatus().map((item, index) => (
|
||
<div key={index} className="flex justify-between text-sm">
|
||
<span className="text-gray-400">{item.label}:</span>
|
||
<span className="text-white">{item.value}</span>
|
||
</div>
|
||
))}
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Last Checked:</span>
|
||
<span className="text-white text-xs">
|
||
{new Date(sessionInfo.lastChecked).toLocaleTimeString()}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error Display */}
|
||
{error && (
|
||
<div className="bg-red-900 border border-red-700 rounded p-2 mb-4">
|
||
<p className="text-red-300 text-sm">{error}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex flex-wrap gap-2">
|
||
<button
|
||
onClick={() => 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
|
||
</button>
|
||
<button
|
||
onClick={() => 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
|
||
</button>
|
||
<button
|
||
onClick={() => 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
|
||
</button>
|
||
</div>
|
||
|
||
{/* Usage Instructions */}
|
||
{sessionInfo && !sessionInfo.isAuthenticated && (
|
||
<div className="mt-4 p-3 bg-yellow-900 border border-yellow-700 rounded">
|
||
<p className="text-yellow-300 text-sm">
|
||
💡 <strong>To establish session:</strong> Run the analysis or screenshot capture once to trigger manual login,
|
||
then future requests will use the saved session and avoid captchas.
|
||
{sessionInfo.dockerEnv && (
|
||
<><br/>🐳 <strong>Docker:</strong> Session data is persisted in the container volume for reuse across restarts.</>
|
||
)}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Docker Environment Info */}
|
||
{sessionInfo?.dockerEnv && (
|
||
<div className="mt-4 p-3 bg-blue-900 border border-blue-700 rounded">
|
||
<p className="text-blue-300 text-sm">
|
||
🐳 <strong>Docker Environment:</strong> Running in containerized mode. Session persistence is enabled
|
||
via volume mount at <code className="bg-blue-800 px-1 rounded">/.tradingview-session</code>
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|