diff --git a/bin/dev b/bin/dev index 49b579ae..8953a873 100755 --- a/bin/dev +++ b/bin/dev @@ -553,6 +553,9 @@ deep_restart() { # Force restart iceberg-catalog since it depends on postgres and minio echo -e "${GREEN}→${NC} Force restarting iceberg-catalog (depends on postgres/minio)..." kubectl delete pod -l app=iceberg-catalog 2>/dev/null || true + # Remove all sandbox deployments and services to free quota + echo -e "${GREEN}→${NC} Removing all sandbox deployments and services..." + kubectl delete deployments,services --all -n dexorder-sandboxes 2>/dev/null || true ;; *) echo -e "${RED}Error: Unknown service '$service'${NC}" @@ -711,19 +714,75 @@ case "$COMMAND" in rebuild_images deploy_services else - # Multiple services specified + # Multiple services specified: rebuild ALL first, then deploy ALL together. + # Deploying one-at-a-time causes each deploy to revert the previous service's + # image tag override (each kubectl apply -k . only carries one tag at a time). + sandbox_requested=0 + deploy_services_list=() + for service in "$@"; do rebuild_images "$service" - # Special handling for sandbox: delete sandbox deployments instead of applying kustomization if [ "$service" == "sandbox" ]; then - echo -e "${GREEN}→${NC} Deleting user container deployments in dexorder-sandboxes namespace..." - kubectl delete deployments --all -n dexorder-sandboxes 2>/dev/null || true - echo -e "${GREEN}✓ User containers will be recreated by gateway on next login${NC}" + sandbox_requested=1 else - deploy_service "$service" + deploy_services_list+=("$service") fi done + + # Deploy all non-sandbox services together in one kustomize apply + if [ ${#deploy_services_list[@]} -gt 0 ]; then + if [ -f "$ROOT_DIR/.dev-image-tag" ]; then + source "$ROOT_DIR/.dev-image-tag" + fi + + cd "$ROOT_DIR/deploy/k8s/dev" + + # Template gateway-config if gateway is in the list + for svc in "${deploy_services_list[@]}"; do + if [ "$svc" == "gateway" ]; then + sed -i "s/SANDBOX_TAG_PLACEHOLDER/$SANDBOX_TAG/g" "$ROOT_DIR/deploy/k8s/dev/configs/gateway-config.yaml" + sed -i "s/SIDECAR_TAG_PLACEHOLDER/$SIDECAR_TAG/g" "$ROOT_DIR/deploy/k8s/dev/configs/gateway-config.yaml" + "$SCRIPT_DIR/config-update" dev + break + fi + done + + # Build the images stanza for all services at once + echo "" >> kustomization.yaml + echo "# Image tags (added by bin/dev)" >> kustomization.yaml + echo "images:" >> kustomization.yaml + for svc in "${deploy_services_list[@]}"; do + case "$svc" in + relay) echo " - name: dexorder/ai-relay" >> kustomization.yaml; echo " newTag: $RELAY_TAG" >> kustomization.yaml ;; + ingestor) echo " - name: dexorder/ai-ingestor" >> kustomization.yaml; echo " newTag: $INGEST_TAG" >> kustomization.yaml ;; + flink) echo " - name: dexorder/ai-flink" >> kustomization.yaml; echo " newTag: $FLINK_TAG" >> kustomization.yaml ;; + gateway) echo " - name: dexorder/ai-gateway" >> kustomization.yaml; echo " newTag: $GATEWAY_TAG" >> kustomization.yaml ;; + web) echo " - name: dexorder/ai-web" >> kustomization.yaml; echo " newTag: $WEB_TAG" >> kustomization.yaml ;; + lifecycle-sidecar|sidecar) echo " - name: dexorder/ai-lifecycle-sidecar" >> kustomization.yaml; echo " newTag: $SIDECAR_TAG" >> kustomization.yaml ;; + esac + done + + kubectl apply -k . + + sed -i '/# Image tags (added by bin\/dev)/,$d' kustomization.yaml + + # Restore gateway-config placeholders if gateway was deployed + for svc in "${deploy_services_list[@]}"; do + if [ "$svc" == "gateway" ]; then + sed -i "s/$SANDBOX_TAG/SANDBOX_TAG_PLACEHOLDER/g" "$ROOT_DIR/deploy/k8s/dev/configs/gateway-config.yaml" + sed -i "s/$SIDECAR_TAG/SIDECAR_TAG_PLACEHOLDER/g" "$ROOT_DIR/deploy/k8s/dev/configs/gateway-config.yaml" + break + fi + done + fi + + # Handle sandbox separately + if [ "$sandbox_requested" == "1" ]; then + echo -e "${GREEN}→${NC} Deleting user container deployments in dexorder-sandboxes namespace..." + kubectl delete deployments --all -n dexorder-sandboxes 2>/dev/null || true + echo -e "${GREEN}✓ User containers will be recreated by gateway on next login${NC}" + fi fi ;; rebuild) diff --git a/deploy/k8s/dev/kustomization.yaml b/deploy/k8s/dev/kustomization.yaml index 820cf0be..1872ef47 100644 --- a/deploy/k8s/dev/kustomization.yaml +++ b/deploy/k8s/dev/kustomization.yaml @@ -255,6 +255,19 @@ generatorOptions: + + + + + + + + + + + + + diff --git a/gateway/src/channels/websocket-handler.ts b/gateway/src/channels/websocket-handler.ts index 461a8796..e0c7992d 100644 --- a/gateway/src/channels/websocket-handler.ts +++ b/gateway/src/channels/websocket-handler.ts @@ -16,7 +16,6 @@ import { type SnapshotMessage, type PatchMessage, } from '../workspace/index.js'; -import { resolutionToSeconds } from '../types/ohlc.js'; /** * Safe JSON stringifier that handles BigInt values @@ -487,7 +486,7 @@ export class WebSocketHandler { } const history = await ohlcService.fetchOHLC( payload.symbol, - resolutionToSeconds(payload.resolution), + payload.period_seconds, payload.from_time, payload.to_time, payload.countback diff --git a/gateway/src/clients/duckdb-client.ts b/gateway/src/clients/duckdb-client.ts index 4aa3c8a4..1229306e 100644 --- a/gateway/src/clients/duckdb-client.ts +++ b/gateway/src/clients/duckdb-client.ts @@ -525,9 +525,9 @@ export class DuckDBClient { // Check if we have continuous data // For now, simple check: if we have any data, assume complete // TODO: Implement proper gap detection by checking for missing periods - const periodMicros = BigInt(period_seconds) * 1000000n; + const periodNanos = BigInt(period_seconds) * 1_000_000_000n; // end_time is exclusive, so expected count = (end - start) / period (no +1) - const expectedBars = Number((end_time - start_time) / periodMicros); + const expectedBars = Number((end_time - start_time) / periodNanos); if (data.length < expectedBars * 0.95) { // Allow 5% tolerance this.logger.debug({ diff --git a/web/src/App.vue b/web/src/App.vue index 95081bc4..50accf15 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -172,7 +172,7 @@ onBeforeUnmount(() => { } .main-splitter :deep(.p-splitter-gutter-handle) { - background: #1c1c1c !important; + background: #2e2e2e !important; } .chart-panel, diff --git a/web/src/components/ChatPanel.vue b/web/src/components/ChatPanel.vue index 587a56f3..5c60dbcf 100644 --- a/web/src/components/ChatPanel.vue +++ b/web/src/components/ChatPanel.vue @@ -28,7 +28,7 @@ const rooms = computed(() => [{ avatar: null, users: [ { _id: CURRENT_USER_ID, username: 'You' }, - { _id: AGENT_ID, username: 'AI Agent', status: { state: isConnected.value ? 'online' : 'offline' } } + { _id: AGENT_ID, username: 'AI Agent' } ], unreadCount: 0, typingUsers: isAgentProcessing.value ? [AGENT_ID] : [] diff --git a/web/src/composables/useTradingViewDatafeed.ts b/web/src/composables/useTradingViewDatafeed.ts index 051ae389..88fa6177 100644 --- a/web/src/composables/useTradingViewDatafeed.ts +++ b/web/src/composables/useTradingViewDatafeed.ts @@ -79,6 +79,19 @@ export class WebSocketDatafeed implements IBasicDataFeed { } private handleMessage(message: any): void { + // On reconnect the server sends a fresh 'connected' message. + // Any pending requests were sent on the old socket and will never be answered, + // so reject them immediately so TradingView can retry on the new connection. + if (message.type === 'connected' && this.pendingRequests.size > 0) { + console.warn('[TradingView Datafeed] WebSocket reconnected — rejecting', this.pendingRequests.size, 'stale pending request(s)') + for (const [requestId, pending] of this.pendingRequests) { + clearTimeout(pending.timeout) + pending.reject(new Error('WebSocket reconnected')) + this.pendingRequests.delete(requestId) + } + return + } + // Handle responses to pending requests if (message.request_id && this.pendingRequests.has(message.request_id)) { console.log('[TradingView Datafeed] Found pending request for:', message.request_id) @@ -186,10 +199,12 @@ export class WebSocketDatafeed implements IBasicDataFeed { if (response.symbol_info) { console.log('[TradingView Datafeed] Resolved symbol info:', response.symbol_info) - // Store the denominators for this symbol + // Derive scale denominators from Nautilus precision fields. + // price_precision=2 → tick divisor=100 (prices stored as integer cents) + // size_precision=6 → base divisor=1_000_000 (volumes stored as integer micro-units) 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 + const tickDenom = Math.pow(10, response.symbol_info.price_precision ?? 0) + const baseDenom = Math.pow(10, response.symbol_info.size_precision ?? 0) this.symbolDenominators.set(symbolKey, { tick: tickDenom, @@ -232,7 +247,7 @@ export class WebSocketDatafeed implements IBasicDataFeed { this.sendRequest({ type: 'get_bars', symbol: symbolKey, - resolution: resolution, + period_seconds: intervalToSeconds(resolution), from_time: periodParams.from, to_time: periodParams.to, countback: periodParams.countBack