chart data loading
This commit is contained in:
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/protobuf
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
@@ -40,11 +40,18 @@ watch(isMobile, (mobile) => {
|
||||
// Check if user is already authenticated on page load
|
||||
onMounted(async () => {
|
||||
// Try to restore session from stored token
|
||||
if (authService.getToken()) {
|
||||
const token = authService.getToken()
|
||||
if (token) {
|
||||
const sessionValid = await authService.checkAuth()
|
||||
if (sessionValid) {
|
||||
isAuthenticated.value = true
|
||||
await initializeApp()
|
||||
// Connect WebSocket with existing token
|
||||
try {
|
||||
await wsManager.connect(token)
|
||||
await initializeApp()
|
||||
} catch (err) {
|
||||
console.error('Failed to connect WebSocket on session restore:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +77,8 @@ const handleAuthenticate = async (email: string, password: string) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: Connect WebSocket with JWT token (if WebSocket is used for real-time sync)
|
||||
// For now, we're not connecting WebSocket until it's implemented in the gateway
|
||||
// await wsManager.connect(result.token)
|
||||
// Step 2: Connect WebSocket with JWT token for real-time sync
|
||||
await wsManager.connect(result.token)
|
||||
|
||||
// Step 3: Initialize application
|
||||
await initializeApp()
|
||||
|
||||
@@ -27,8 +27,27 @@ class AuthService {
|
||||
private gatewayUrl: string
|
||||
|
||||
constructor() {
|
||||
// Use environment variable for gateway URL, fallback to localhost for dev
|
||||
this.gatewayUrl = import.meta.env.VITE_GATEWAY_URL || 'http://localhost:3000'
|
||||
// Use environment variable for gateway URL, fallback based on current location
|
||||
// VITE_GATEWAY_URL can be:
|
||||
// - Relative path: "/api" (for production with ingress)
|
||||
// - Absolute URL: "http://localhost:3000" (for local dev)
|
||||
// - Absolute URL: "http://dexorder.local" (for minikube)
|
||||
const envUrl = import.meta.env.VITE_GATEWAY_URL
|
||||
|
||||
if (envUrl) {
|
||||
// If it starts with /, it's a relative path - use current origin
|
||||
if (envUrl.startsWith('/')) {
|
||||
this.gatewayUrl = window.location.origin + envUrl
|
||||
} else {
|
||||
this.gatewayUrl = envUrl
|
||||
}
|
||||
} else if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
// Local development default
|
||||
this.gatewayUrl = 'http://localhost:3000'
|
||||
} else {
|
||||
// Production default: use current origin
|
||||
this.gatewayUrl = window.location.origin
|
||||
}
|
||||
|
||||
// Try to restore token from localStorage
|
||||
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||
@@ -51,6 +70,15 @@ class AuthService {
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
|
||||
// Check if response is JSON before parsing
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Server error (${response.status}): ${response.statusText || 'Service unavailable'}`,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -31,12 +31,18 @@ interface Subscription {
|
||||
subscriptionId: string
|
||||
}
|
||||
|
||||
interface SymbolDenominators {
|
||||
tick: number
|
||||
base: number
|
||||
}
|
||||
|
||||
export class WebSocketDatafeed implements IBasicDataFeed {
|
||||
private pendingRequests: Map<string, PendingRequest> = new Map()
|
||||
private subscriptions: Map<string, Subscription> = new Map()
|
||||
private requestTimeout = 10000 // 10 seconds
|
||||
private configuration: DatafeedConfiguration | null = null
|
||||
private messageHandler: MessageHandler
|
||||
private symbolDenominators: Map<string, SymbolDenominators> = new Map() // Track denominators per symbol
|
||||
|
||||
constructor() {
|
||||
// Use the shared WebSocket connection (managed by App.vue authentication)
|
||||
@@ -53,19 +59,27 @@ export class WebSocketDatafeed implements IBasicDataFeed {
|
||||
const requestId = message.request_id || this.generateRequestId()
|
||||
message.request_id = requestId
|
||||
|
||||
console.log('[TradingView Datafeed] Sending request:', requestId, message.type, message)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
console.error('[TradingView Datafeed] Request timeout:', requestId, message.type)
|
||||
this.pendingRequests.delete(requestId)
|
||||
reject(new Error('Request timeout'))
|
||||
}, this.requestTimeout)
|
||||
|
||||
this.pendingRequests.set(requestId, { resolve, reject, timeout })
|
||||
|
||||
const wsState = wsManager.getWebSocket()?.readyState
|
||||
console.log('[TradingView Datafeed] WebSocket state before send:', wsState, 'OPEN=' + WebSocket.OPEN)
|
||||
|
||||
wsManager.send(message)
|
||||
})
|
||||
}
|
||||
|
||||
private handleMessage(message: any): void {
|
||||
console.log('[TradingView Datafeed] Received message:', message)
|
||||
console.log('[TradingView Datafeed] Received message:', message.type, message)
|
||||
console.log('[TradingView Datafeed] Pending requests count:', this.pendingRequests.size)
|
||||
|
||||
// Handle responses to pending requests
|
||||
if (message.request_id && this.pendingRequests.has(message.request_id)) {
|
||||
@@ -75,27 +89,30 @@ export class WebSocketDatafeed implements IBasicDataFeed {
|
||||
clearTimeout(pending.timeout)
|
||||
|
||||
if (message.type === 'error') {
|
||||
console.log('[TradingView Datafeed] Resolving with error:', message.error_message)
|
||||
console.error('[TradingView Datafeed] Resolving with error:', message.error_message)
|
||||
pending.reject(new Error(message.error_message || 'Unknown error'))
|
||||
} else {
|
||||
console.log('[TradingView Datafeed] Resolving with response')
|
||||
console.log('[TradingView Datafeed] Resolving with response:', message.type)
|
||||
pending.resolve(message)
|
||||
}
|
||||
} else if (message.request_id) {
|
||||
console.log('[TradingView Datafeed] No pending request found for:', message.request_id)
|
||||
console.warn('[TradingView Datafeed] No pending request found for:', message.request_id, 'Available:', Array.from(this.pendingRequests.keys()))
|
||||
}
|
||||
|
||||
// Handle real-time bar updates
|
||||
if (message.type === 'bar_update') {
|
||||
const subscription = this.subscriptions.get(message.subscription_id)
|
||||
if (subscription && message.bar) {
|
||||
const symbolKey = subscription.symbolInfo.ticker || subscription.symbolInfo.name
|
||||
const denoms = this.symbolDenominators.get(symbolKey) || { tick: 1, base: 1 }
|
||||
|
||||
const bar: Bar = {
|
||||
time: message.bar.time * 1000, // Convert to milliseconds
|
||||
open: parseFloat(message.bar.data.open),
|
||||
high: parseFloat(message.bar.data.high),
|
||||
low: parseFloat(message.bar.data.low),
|
||||
close: parseFloat(message.bar.data.close),
|
||||
volume: parseFloat(message.bar.data.volume)
|
||||
open: parseFloat(message.bar.open) / denoms.tick,
|
||||
high: parseFloat(message.bar.high) / denoms.tick,
|
||||
low: parseFloat(message.bar.low) / denoms.tick,
|
||||
close: parseFloat(message.bar.close) / denoms.tick,
|
||||
volume: parseFloat(message.bar.volume) / denoms.base
|
||||
}
|
||||
subscription.onTick(bar)
|
||||
}
|
||||
@@ -159,20 +176,42 @@ export class WebSocketDatafeed implements IBasicDataFeed {
|
||||
onResolve: (symbolInfo: LibrarySymbolInfo) => void,
|
||||
onError: (reason: string) => void
|
||||
): void {
|
||||
console.log('[TradingView Datafeed] Resolving symbol:', symbolName)
|
||||
console.log('[TradingView Datafeed] WebSocket state:', wsManager.getWebSocket()?.readyState)
|
||||
|
||||
this.sendRequest<any>({
|
||||
type: 'resolve_symbol',
|
||||
symbol: symbolName
|
||||
})
|
||||
.then((response) => {
|
||||
console.log('[TradingView Datafeed] Received response:', response)
|
||||
if (response.symbol_info) {
|
||||
console.log('[TradingView Datafeed] Resolved symbol info:', response.symbol_info)
|
||||
|
||||
// Store the denominators for this symbol
|
||||
const symbolKey = response.symbol_info.ticker || response.symbol_info.name
|
||||
const tickDenom = response.symbol_info.tick_denominator || 1
|
||||
const baseDenom = response.symbol_info.base_denominator || 1
|
||||
|
||||
this.symbolDenominators.set(symbolKey, {
|
||||
tick: tickDenom,
|
||||
base: baseDenom
|
||||
})
|
||||
console.log('[TradingView Datafeed] Stored denominators:', symbolKey, { tick: tickDenom, base: baseDenom })
|
||||
|
||||
onResolve(response.symbol_info)
|
||||
} else {
|
||||
console.error('[TradingView Datafeed] No symbol_info in response')
|
||||
onError('Symbol not found')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to resolve symbol:', error)
|
||||
console.error('[TradingView Datafeed] Failed to resolve symbol:', symbolName, error)
|
||||
console.error('[TradingView Datafeed] Error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
pendingRequests: this.pendingRequests.size
|
||||
})
|
||||
onError(error instanceof Error ? error.message : 'Unknown error')
|
||||
})
|
||||
}
|
||||
@@ -189,9 +228,12 @@ export class WebSocketDatafeed implements IBasicDataFeed {
|
||||
onResult: (bars: Bar[], meta: HistoryMetadata) => void,
|
||||
onError: (reason: string) => void
|
||||
): void {
|
||||
const symbolKey = symbolInfo.ticker || symbolInfo.name
|
||||
const denoms = this.symbolDenominators.get(symbolKey) || { tick: 1, base: 1 }
|
||||
|
||||
this.sendRequest<any>({
|
||||
type: 'get_bars',
|
||||
symbol: symbolInfo.ticker || symbolInfo.name,
|
||||
symbol: symbolKey,
|
||||
resolution: resolution,
|
||||
from_time: periodParams.from,
|
||||
to_time: periodParams.to,
|
||||
@@ -199,15 +241,20 @@ export class WebSocketDatafeed implements IBasicDataFeed {
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.history) {
|
||||
console.log('[TradingView Datafeed] Raw bar sample:', response.history.bars?.[0])
|
||||
console.log('[TradingView Datafeed] Denominators:', denoms)
|
||||
|
||||
const bars: Bar[] = (response.history.bars || []).map((bar: any) => ({
|
||||
time: bar.time * 1000, // Convert to milliseconds
|
||||
open: parseFloat(bar.data.open),
|
||||
high: parseFloat(bar.data.high),
|
||||
low: parseFloat(bar.data.low),
|
||||
close: parseFloat(bar.data.close),
|
||||
volume: parseFloat(bar.data.volume)
|
||||
open: parseFloat(bar.open) / denoms.tick,
|
||||
high: parseFloat(bar.high) / denoms.tick,
|
||||
low: parseFloat(bar.low) / denoms.tick,
|
||||
close: parseFloat(bar.close) / denoms.tick,
|
||||
volume: parseFloat(bar.volume) / denoms.base
|
||||
}))
|
||||
|
||||
console.log('[TradingView Datafeed] Scaled bar sample:', bars[0])
|
||||
|
||||
const meta: HistoryMetadata = {
|
||||
noData: bars.length === 0,
|
||||
nextTime: response.history.next_time
|
||||
|
||||
@@ -12,6 +12,7 @@ class WebSocketManager {
|
||||
private handlers: Set<MessageHandler> = new Set()
|
||||
private reconnectTimeout: number | null = null
|
||||
public isConnected = ref(false)
|
||||
public isAuthenticated = ref(false)
|
||||
private token: string | null = null
|
||||
|
||||
/**
|
||||
@@ -27,38 +28,73 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Use env variable for WebSocket URL, fallback to localhost for dev
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws'
|
||||
// Build WebSocket URL based on environment
|
||||
// VITE_GATEWAY_URL can be:
|
||||
// - Relative path: "/api" (for production with ingress)
|
||||
// - Absolute URL: "http://localhost:3000" (for local dev)
|
||||
const envUrl = import.meta.env.VITE_GATEWAY_URL
|
||||
let baseUrl: string
|
||||
|
||||
// Append token as query parameter
|
||||
if (envUrl) {
|
||||
// If it starts with /, it's a relative path - use current origin
|
||||
if (envUrl.startsWith('/')) {
|
||||
baseUrl = window.location.origin + envUrl
|
||||
} else {
|
||||
baseUrl = envUrl
|
||||
}
|
||||
} else if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
baseUrl = 'http://localhost:3000'
|
||||
} else {
|
||||
// Production: use current origin
|
||||
baseUrl = window.location.origin
|
||||
}
|
||||
|
||||
// Gateway WebSocket endpoint is /ws/chat (baseUrl already includes /api if needed)
|
||||
const wsUrl = baseUrl.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws/chat'
|
||||
|
||||
// WebSocket API doesn't support custom headers, so we'll send the token in the URL
|
||||
// The gateway will need to handle token extraction from query params
|
||||
const wsUrlWithToken = `${wsUrl}?token=${encodeURIComponent(token)}`
|
||||
console.log('[WebSocket] Connecting to:', wsUrl)
|
||||
console.log('[WebSocket] Full URL (token hidden):', wsUrl + '?token=***')
|
||||
this.ws = new WebSocket(wsUrlWithToken)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected')
|
||||
console.log('[WebSocket] Connected successfully')
|
||||
this.isConnected.value = true
|
||||
this.isAuthenticated.value = false // Wait for 'connected' message from server
|
||||
resolve()
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
console.log('[WebSocket] Raw message received:', event.data.substring(0, 200))
|
||||
try {
|
||||
const message = JSON.parse(event.data)
|
||||
console.log('[WebSocket] Parsed message type:', message.type)
|
||||
|
||||
// Mark as authenticated when we receive the 'connected' message
|
||||
if (message.type === 'connected') {
|
||||
console.log('[WebSocket] Received connected message, marking as authenticated')
|
||||
this.isAuthenticated.value = true
|
||||
}
|
||||
|
||||
// Pass to all handlers
|
||||
this.handlers.forEach(handler => handler(message))
|
||||
} catch (err) {
|
||||
console.error('Error parsing WebSocket message:', err)
|
||||
console.error('[WebSocket] Error parsing message:', err)
|
||||
console.error('[WebSocket] Raw data:', event.data.substring(0, 500))
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
console.error('[WebSocket] Connection error:', error)
|
||||
this.isConnected.value = false
|
||||
reject(error)
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
this.isConnected.value = false
|
||||
this.isAuthenticated.value = false
|
||||
console.log('WebSocket disconnected:', event.code, event.reason)
|
||||
}
|
||||
|
||||
@@ -81,8 +117,12 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
send(message: WebSocketMessage) {
|
||||
console.log('[WebSocket] Attempting to send message:', message.type, 'readyState:', this.ws?.readyState)
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
console.log('[WebSocket] Sending message:', JSON.stringify(message))
|
||||
this.ws.send(JSON.stringify(message))
|
||||
} else {
|
||||
console.error('[WebSocket] Cannot send message - WebSocket not open. State:', this.ws?.readyState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +134,7 @@ class WebSocketManager {
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
this.isAuthenticated.value = false
|
||||
}
|
||||
|
||||
getWebSocket() {
|
||||
|
||||
Reference in New Issue
Block a user