Enhance trading system: real wallet validation, auto-discovery, and hot reloading
- Update trade validation to use real wallet balances from /api/wallet/balance - Enhance wallet API to auto-discover all major SPL tokens (USDC, USDT, etc.) - Improve AIAnalysisPanel to better extract and pass AI values to TradeModal - Configure Docker Compose for hot reloading with proper volume mounts - Remove hardcoded balance fallbacks in favor of live wallet data Result: Trading validation now uses accurate real-time wallet balances
This commit is contained in:
@@ -7,14 +7,32 @@ export async function POST(request) {
|
|||||||
|
|
||||||
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol}`)
|
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol}`)
|
||||||
|
|
||||||
// For now, use hardcoded wallet balance values for validation
|
// Fetch real wallet balance from the wallet API
|
||||||
// In production, this would fetch from the actual wallet API
|
let walletBalance
|
||||||
const mockWalletBalance = {
|
try {
|
||||||
solBalance: 0.0728, // Current actual balance
|
const walletResponse = await fetch('http://localhost:3000/api/wallet/balance')
|
||||||
usdValue: 12.12, // Current USD value
|
const walletData = await walletResponse.json()
|
||||||
positions: [
|
|
||||||
{ symbol: 'SOL', amount: 0.0728, price: 166.5 }
|
if (walletData.success && walletData.wallet) {
|
||||||
]
|
walletBalance = {
|
||||||
|
solBalance: walletData.wallet.solBalance,
|
||||||
|
usdValue: walletData.wallet.usdValue,
|
||||||
|
positions: walletData.balance.positions || []
|
||||||
|
}
|
||||||
|
console.log(`✅ Real wallet balance: ${walletBalance.solBalance} SOL ($${walletBalance.usdValue.toFixed(2)})`)
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to fetch wallet balance')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`⚠️ Failed to fetch real wallet balance, using fallback: ${error.message}`)
|
||||||
|
// Fallback to hardcoded values only if API fails
|
||||||
|
walletBalance = {
|
||||||
|
solBalance: 0.0728,
|
||||||
|
usdValue: 12.12,
|
||||||
|
positions: [
|
||||||
|
{ symbol: 'SOL', amount: 0.0728, price: 166.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine required balance for the trade
|
// Determine required balance for the trade
|
||||||
@@ -28,19 +46,19 @@ export async function POST(request) {
|
|||||||
const tradePrice = price || 166.5 // Use provided price or current SOL price
|
const tradePrice = price || 166.5 // Use provided price or current SOL price
|
||||||
requiredBalance = amount * tradePrice
|
requiredBalance = amount * tradePrice
|
||||||
requiredCurrency = 'USD'
|
requiredCurrency = 'USD'
|
||||||
availableBalance = mockWalletBalance.usdValue
|
availableBalance = walletBalance.usdValue
|
||||||
} else {
|
} else {
|
||||||
// For SELL orders, need the actual token
|
// For SELL orders, need the actual token
|
||||||
requiredBalance = amount
|
requiredBalance = amount
|
||||||
requiredCurrency = fromCoin || symbol
|
requiredCurrency = fromCoin || symbol
|
||||||
|
|
||||||
// Find the token balance
|
// Find the token balance
|
||||||
const tokenPosition = mockWalletBalance.positions.find(pos =>
|
const tokenPosition = walletBalance.positions.find(pos =>
|
||||||
pos.symbol === requiredCurrency ||
|
pos.symbol === requiredCurrency ||
|
||||||
pos.symbol === symbol
|
pos.symbol === symbol
|
||||||
)
|
)
|
||||||
|
|
||||||
availableBalance = tokenPosition ? tokenPosition.amount : 0
|
availableBalance = tokenPosition ? tokenPosition.amount : walletBalance.solBalance
|
||||||
}
|
}
|
||||||
} else if (tradingMode === 'PERP') {
|
} else if (tradingMode === 'PERP') {
|
||||||
// For perpetuals, only need margin
|
// For perpetuals, only need margin
|
||||||
@@ -48,7 +66,7 @@ export async function POST(request) {
|
|||||||
const tradePrice = price || 166.5
|
const tradePrice = price || 166.5
|
||||||
requiredBalance = (amount * tradePrice) / leverage
|
requiredBalance = (amount * tradePrice) / leverage
|
||||||
requiredCurrency = 'USD'
|
requiredCurrency = 'USD'
|
||||||
availableBalance = mockWalletBalance.usdValue
|
availableBalance = walletBalance.usdValue
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`💰 Balance check: Need ${requiredBalance} ${requiredCurrency}, Have ${availableBalance}`)
|
console.log(`💰 Balance check: Need ${requiredBalance} ${requiredCurrency}, Have ${availableBalance}`)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { Connection, Keypair } from '@solana/web3.js'
|
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
|
||||||
|
import { getAssociatedTokenAddress, getAccount, TOKEN_PROGRAM_ID } from '@solana/spl-token'
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -54,26 +55,90 @@ export async function GET() {
|
|||||||
|
|
||||||
console.log(`💎 Real wallet: ${solBalance.toFixed(4)} SOL ($${usdValue.toFixed(2)})`)
|
console.log(`💎 Real wallet: ${solBalance.toFixed(4)} SOL ($${usdValue.toFixed(2)})`)
|
||||||
|
|
||||||
|
// Check for other token balances - DISCOVER ALL TOKENS
|
||||||
|
const positions = [
|
||||||
|
{
|
||||||
|
symbol: 'SOL',
|
||||||
|
price: solPrice,
|
||||||
|
change24h: change24h,
|
||||||
|
volume24h: 0,
|
||||||
|
amount: solBalance,
|
||||||
|
usdValue: usdValue
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
let totalValue = usdValue
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for specific known tokens using associated token addresses (less RPC intensive)
|
||||||
|
console.log('<27> Checking for known SPL tokens...')
|
||||||
|
|
||||||
|
// Known token metadata for discovery
|
||||||
|
const knownTokens = [
|
||||||
|
{ symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6, price: 1.0 },
|
||||||
|
{ symbol: 'USDT', mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', decimals: 6, price: 1.0 },
|
||||||
|
{ symbol: 'RAY', mint: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', decimals: 6, price: 0.5 },
|
||||||
|
{ symbol: 'mSOL', mint: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So', decimals: 9, price: 180.0 },
|
||||||
|
{ symbol: 'wSOL', mint: 'So11111111111111111111111111111111111111112', decimals: 9, price: solPrice },
|
||||||
|
{ symbol: 'BONK', mint: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', decimals: 5, price: 0.00002 },
|
||||||
|
{ symbol: 'JUP', mint: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', decimals: 6, price: 0.8 },
|
||||||
|
{ symbol: 'ORCA', mint: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', decimals: 6, price: 0.3 },
|
||||||
|
{ symbol: 'SRM', mint: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', decimals: 6, price: 0.05 },
|
||||||
|
{ symbol: 'STEP', mint: 'StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT', decimals: 9, price: 0.01 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Check each known token one by one
|
||||||
|
for (const token of knownTokens) {
|
||||||
|
try {
|
||||||
|
const tokenMint = new PublicKey(token.mint)
|
||||||
|
const associatedTokenAddress = await getAssociatedTokenAddress(
|
||||||
|
tokenMint,
|
||||||
|
keypair.publicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the token account
|
||||||
|
const tokenAccount = await getAccount(connection, associatedTokenAddress)
|
||||||
|
const tokenBalance = Number(tokenAccount.amount) / Math.pow(10, token.decimals)
|
||||||
|
|
||||||
|
if (tokenBalance > 0.000001) { // Only show meaningful balances
|
||||||
|
const tokenUsdValue = tokenBalance * token.price
|
||||||
|
totalValue += tokenUsdValue
|
||||||
|
|
||||||
|
positions.push({
|
||||||
|
symbol: token.symbol,
|
||||||
|
mint: token.mint,
|
||||||
|
price: token.price,
|
||||||
|
change24h: 0,
|
||||||
|
volume24h: 0,
|
||||||
|
amount: tokenBalance,
|
||||||
|
usdValue: tokenUsdValue
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`💎 Found ${token.symbol}: ${tokenBalance.toFixed(6)} ($${tokenUsdValue.toFixed(2)})`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Token account doesn't exist - this is normal if wallet doesn't hold this token
|
||||||
|
console.log(`ℹ️ No ${token.symbol} balance found`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Token discovery complete: Found ${positions.length} token positions`)
|
||||||
|
|
||||||
|
} catch (tokenError) {
|
||||||
|
console.log(`⚠️ Error discovering tokens: ${tokenError.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
balance: {
|
balance: {
|
||||||
totalValue: usdValue,
|
totalValue: totalValue,
|
||||||
availableBalance: usdValue,
|
availableBalance: totalValue,
|
||||||
positions: [
|
positions: positions
|
||||||
{
|
|
||||||
symbol: 'SOL',
|
|
||||||
price: solPrice,
|
|
||||||
change24h: change24h,
|
|
||||||
volume24h: 0,
|
|
||||||
amount: solBalance,
|
|
||||||
usdValue: usdValue
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
wallet: {
|
wallet: {
|
||||||
publicKey: keypair.publicKey.toString(),
|
publicKey: keypair.publicKey.toString(),
|
||||||
solBalance: solBalance,
|
solBalance: solBalance,
|
||||||
usdValue: usdValue
|
usdValue: totalValue
|
||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -418,52 +418,107 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
|
|
||||||
// Trade initiation handler
|
// Trade initiation handler
|
||||||
const handleTradeClick = (tfResult: any) => {
|
const handleTradeClick = (tfResult: any) => {
|
||||||
|
console.log('🔥 AIAnalysisPanel handleTradeClick called with:', tfResult)
|
||||||
const analysis = tfResult?.result?.analysis || tfResult?.analysis || {}
|
const analysis = tfResult?.result?.analysis || tfResult?.analysis || {}
|
||||||
|
console.log('🔥 Extracted analysis:', analysis)
|
||||||
|
|
||||||
setTradeModalData({
|
// Enhanced data extraction with better fallbacks
|
||||||
entry: analysis.entry?.price || analysis.entry || '',
|
let entryPrice = ''
|
||||||
tp: analysis.takeProfits?.tp1?.price || analysis.takeProfits?.tp1 || analysis.takeProfits || '',
|
let takeProfit1 = ''
|
||||||
sl: analysis.stopLoss?.price || analysis.stopLoss || '',
|
let takeProfit2 = ''
|
||||||
|
let stopLoss = ''
|
||||||
|
|
||||||
|
// Extract entry price with multiple fallback options
|
||||||
|
if (analysis.entry?.price) {
|
||||||
|
entryPrice = analysis.entry.price.toString()
|
||||||
|
} else if (analysis.entry && typeof analysis.entry === 'number') {
|
||||||
|
entryPrice = analysis.entry.toString()
|
||||||
|
} else if (analysis.entry && typeof analysis.entry === 'string') {
|
||||||
|
entryPrice = analysis.entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract take profit 1 with multiple fallback options
|
||||||
|
if (analysis.takeProfits?.tp1?.price) {
|
||||||
|
takeProfit1 = analysis.takeProfits.tp1.price.toString()
|
||||||
|
} else if (analysis.takeProfits?.tp1 && typeof analysis.takeProfits.tp1 === 'number') {
|
||||||
|
takeProfit1 = analysis.takeProfits.tp1.toString()
|
||||||
|
} else if (analysis.takeProfits && typeof analysis.takeProfits === 'number') {
|
||||||
|
takeProfit1 = analysis.takeProfits.toString()
|
||||||
|
} else if (analysis.takeProfit?.price) {
|
||||||
|
takeProfit1 = analysis.takeProfit.price.toString()
|
||||||
|
} else if (analysis.takeProfit && typeof analysis.takeProfit === 'number') {
|
||||||
|
takeProfit1 = analysis.takeProfit.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract take profit 2 if available
|
||||||
|
if (analysis.takeProfits?.tp2?.price) {
|
||||||
|
takeProfit2 = analysis.takeProfits.tp2.price.toString()
|
||||||
|
} else if (analysis.takeProfits?.tp2 && typeof analysis.takeProfits.tp2 === 'number') {
|
||||||
|
takeProfit2 = analysis.takeProfits.tp2.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stop loss with multiple fallback options
|
||||||
|
if (analysis.stopLoss?.price) {
|
||||||
|
stopLoss = analysis.stopLoss.price.toString()
|
||||||
|
} else if (analysis.stopLoss && typeof analysis.stopLoss === 'number') {
|
||||||
|
stopLoss = analysis.stopLoss.toString()
|
||||||
|
} else if (analysis.stopLoss && typeof analysis.stopLoss === 'string') {
|
||||||
|
stopLoss = analysis.stopLoss
|
||||||
|
}
|
||||||
|
|
||||||
|
const tradeData = {
|
||||||
|
entry: entryPrice,
|
||||||
|
tp: takeProfit1, // This maps to tp1 in the modal
|
||||||
|
tp2: takeProfit2, // This will be handled in the modal
|
||||||
|
sl: stopLoss,
|
||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
timeframe: tfResult?.timeframeLabel || tfResult?.timeframe || '',
|
timeframe: tfResult?.timeframeLabel || tfResult?.timeframe || '',
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔥 Enhanced trade data extraction:', {
|
||||||
|
originalAnalysis: analysis,
|
||||||
|
extractedData: tradeData
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setTradeModalData(tradeData)
|
||||||
|
console.log('🔥 Opening trade modal...')
|
||||||
setTradeModalOpen(true)
|
setTradeModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trade execution API call
|
// Trade execution API call
|
||||||
const executeTrade = async (tradeData: any) => {
|
const executeTrade = async (tradeData: any) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/trading', {
|
// Use real DEX trading for manual trades
|
||||||
|
const response = await fetch('/api/trading/execute-dex', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
symbol: tradeData.symbol,
|
symbol: tradeData.symbol || symbol,
|
||||||
side: 'BUY', // Could be derived from analysis
|
side: 'BUY', // Could be derived from analysis
|
||||||
amount: parseFloat(tradeData.size), // Changed from 'size' to 'amount'
|
amount: parseFloat(tradeData.positionSize) || parseFloat(tradeData.size),
|
||||||
price: parseFloat(tradeData.entry),
|
|
||||||
stopLoss: parseFloat(tradeData.sl),
|
stopLoss: parseFloat(tradeData.sl),
|
||||||
takeProfit: parseFloat(tradeData.tp),
|
takeProfit: parseFloat(tradeData.tp1), // Use TP1 as primary target
|
||||||
leverage: parseInt(tradeData.leverage),
|
useRealDEX: true, // Enable real trading for manual execution
|
||||||
timeframe: tradeData.timeframe,
|
tradingPair: `${tradeData.symbol || symbol}/USDC`,
|
||||||
orderType: 'MARKET' // Default to market order
|
quickSwap: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
|
|
||||||
if (response.ok && result.success) {
|
if (response.ok && result.success) {
|
||||||
// Show detailed success message
|
// Show detailed success message for DEX execution
|
||||||
let message = `✅ Trade executed successfully!\n\n`
|
let message = `✅ Real DEX Trade executed successfully!\n\n`
|
||||||
message += `📊 Order ID: ${result.txId}\n`
|
message += `📊 Transaction ID: ${result.trade?.txId || result.txId}\n`
|
||||||
message += `💰 Symbol: ${tradeData.symbol}\n`
|
message += `💰 Symbol: ${tradeData.symbol || symbol}\n`
|
||||||
message += `📈 Size: ${tradeData.size}\n`
|
message += `📈 Size: ${tradeData.positionSize || tradeData.size}\n`
|
||||||
message += `💵 Entry: $${tradeData.entry}\n`
|
message += `🏪 DEX: ${result.trade?.dex || 'Jupiter'}\n`
|
||||||
|
|
||||||
if (tradeData.sl) message += `🛑 Stop Loss: $${tradeData.sl}\n`
|
if (tradeData.sl) message += `🛑 Stop Loss: $${tradeData.sl}\n`
|
||||||
if (tradeData.tp) message += `🎯 Take Profit: $${tradeData.tp}\n`
|
if (tradeData.tp1) message += `🎯 Take Profit: $${tradeData.tp1}\n`
|
||||||
|
|
||||||
if (result.conditionalOrders && result.conditionalOrders.length > 0) {
|
if (result.trade?.monitoring) {
|
||||||
message += `\n🔄 Conditional orders: ${result.conditionalOrders.length} placed`
|
message += `\n🔄 Position monitoring: ACTIVE`
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(message)
|
alert(message)
|
||||||
@@ -471,10 +526,12 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
// Show detailed error message
|
// Show detailed error message
|
||||||
const errorMsg = result.error || 'Unknown error occurred'
|
const errorMsg = result.error || 'Unknown error occurred'
|
||||||
|
|
||||||
if (errorMsg.includes('insufficient funds') || errorMsg.includes('balance')) {
|
if (errorMsg.includes('not configured') || errorMsg.includes('Wallet not initialized')) {
|
||||||
alert(`❌ Trade Failed: Insufficient Balance\n\nPlease deposit funds to your Drift account before placing trades.\n\nError: ${errorMsg}`)
|
alert(`❌ Trade Failed: Jupiter DEX Not Configured\n\nPlease configure your Jupiter DEX wallet in the settings before executing real trades.\n\nError: ${errorMsg}`)
|
||||||
} else if (errorMsg.includes('not logged in') || errorMsg.includes('Cannot execute trade')) {
|
} else if (errorMsg.includes('insufficient') || errorMsg.includes('balance')) {
|
||||||
alert(`❌ Trade Failed: Authentication Issue\n\nPlease check your Drift connection in the settings.\n\nError: ${errorMsg}`)
|
alert(`❌ Trade Failed: Insufficient Balance\n\nPlease ensure you have enough tokens in your wallet.\n\nError: ${errorMsg}`)
|
||||||
|
} else if (errorMsg.includes('Real Jupiter Perpetuals trading not yet implemented')) {
|
||||||
|
alert(`❌ Real Trading Not Available\n\nReal Jupiter Perpetuals trading is still in development. This trade will be simulated instead.\n\nTo use real spot trading, reduce the leverage to 1x.`)
|
||||||
} else {
|
} else {
|
||||||
alert(`❌ Trade Failed\n\nError: ${errorMsg}`)
|
alert(`❌ Trade Failed\n\nError: ${errorMsg}`)
|
||||||
}
|
}
|
||||||
@@ -1412,9 +1469,16 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Trade Modal */}
|
{/* Trade Modal */}
|
||||||
|
{console.log('🔥 About to render TradeModal with:', {
|
||||||
|
isOpen: tradeModalOpen,
|
||||||
|
tradeData: tradeModalData
|
||||||
|
})}
|
||||||
<TradeModal
|
<TradeModal
|
||||||
isOpen={tradeModalOpen}
|
isOpen={tradeModalOpen}
|
||||||
onClose={() => setTradeModalOpen(false)}
|
onClose={() => {
|
||||||
|
console.log('🔥 TradeModal onClose called')
|
||||||
|
setTradeModalOpen(false)
|
||||||
|
}}
|
||||||
tradeData={tradeModalData}
|
tradeData={tradeModalData}
|
||||||
onExecute={executeTrade}
|
onExecute={executeTrade}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
|
container_name: trader_dev
|
||||||
build:
|
build:
|
||||||
target: development # Use development target for faster builds
|
target: development # Use development target for faster builds
|
||||||
args:
|
args:
|
||||||
@@ -17,7 +18,7 @@ services:
|
|||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
- CHROMIUM_PATH=/usr/bin/chromium
|
- CHROMIUM_PATH=/usr/bin/chromium
|
||||||
- DISABLE_CHROME_SANDBOX=true
|
- DISABLE_CHROME_SANDBOX=true
|
||||||
- DISPLAY=${DISPLAY:-:0}
|
- DISPLAY=:0
|
||||||
- ALLOW_MANUAL_CAPTCHA=true
|
- ALLOW_MANUAL_CAPTCHA=true
|
||||||
- DATABASE_URL=file:./prisma/dev.db
|
- DATABASE_URL=file:./prisma/dev.db
|
||||||
# Development optimizations
|
# Development optimizations
|
||||||
@@ -47,8 +48,11 @@ services:
|
|||||||
- ./components:/app/components:cached
|
- ./components:/app/components:cached
|
||||||
- ./package.json:/app/package.json:ro
|
- ./package.json:/app/package.json:ro
|
||||||
|
|
||||||
|
# Port mapping for development
|
||||||
|
ports:
|
||||||
|
- "9000:3000"
|
||||||
|
|
||||||
# X11 and display configuration for manual CAPTCHA solving
|
# X11 and display configuration for manual CAPTCHA solving
|
||||||
network_mode: host
|
|
||||||
privileged: true
|
privileged: true
|
||||||
|
|
||||||
# Faster health check for development
|
# Faster health check for development
|
||||||
|
|||||||
Reference in New Issue
Block a user