diff --git a/src/api/wrenClient.ts b/src/api/wrenClient.ts index 71cfb0f..b2d540e 100644 --- a/src/api/wrenClient.ts +++ b/src/api/wrenClient.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; import type { OffsetOrder, Portfolio } from '../types'; import { config } from '../utils/config'; @@ -44,9 +44,13 @@ const DEFAULT_PORTFOLIO: Portfolio = { // Create API client with error handling, timeout, and retry logic const createApiClient = () => { if (!config.wrenApiKey) { + console.error('Wren API token is missing! Token:', config.wrenApiKey); + console.error('Environment:', window?.env ? JSON.stringify(window.env) : 'No window.env available'); throw new Error('Wren API token is not configured'); } + console.log('[wrenClient] Creating API client with key:', config.wrenApiKey ? '********' + config.wrenApiKey.slice(-4) : 'MISSING'); + const client = axios.create({ baseURL: 'https://api.wren.co/v1', headers: { @@ -54,40 +58,45 @@ const createApiClient = () => { 'Content-Type': 'application/json' }, timeout: 10000, // 10 second timeout - validateStatus: (status) => status >= 200 && status < 500, // Handle 4xx errors gracefully + validateStatus: (status: number) => status >= 200 && status < 500, // Handle 4xx errors gracefully }); // Add request interceptor for logging client.interceptors.request.use( - (config) => { - if (!config.headers.Authorization) { + (config: AxiosRequestConfig) => { + if (!config.headers?.Authorization) { throw new Error('API token is required'); } + console.log('[wrenClient] Making API request to:', config.url); return config; }, - (error) => { - console.error('Request configuration error:', error.message); + (error: Error) => { + console.error('[wrenClient] Request configuration error:', error.message); return Promise.reject(error); } ); // Add response interceptor for error handling client.interceptors.response.use( - (response) => response, - (error) => { + (response: AxiosResponse) => { + console.log('[wrenClient] Received API response:', response.status); + return response; + }, + (error: unknown) => { if (axios.isAxiosError(error)) { if (error.code === 'ECONNABORTED') { - console.warn('Request timeout, using fallback data'); + console.warn('[wrenClient] Request timeout, using fallback data'); return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } }); } if (!error.response) { - console.warn('Network error, using fallback data'); + console.warn('[wrenClient] Network error, using fallback data'); return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } }); } if (error.response.status === 401) { - console.warn('Authentication failed, using fallback data'); + console.warn('[wrenClient] Authentication failed, using fallback data'); return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } }); } + console.error('[wrenClient] API error:', error.response?.status, error.response?.data); } return Promise.reject(error); } @@ -104,24 +113,26 @@ const logError = (error: unknown) => { message: error.message, stack: error.stack }; - console.error('API Error:', JSON.stringify(errorInfo, null, 2)); + console.error('[wrenClient] API Error:', JSON.stringify(errorInfo, null, 2)); } else { - console.error('Unknown error:', String(error)); + console.error('[wrenClient] Unknown error:', String(error)); } }; export async function getPortfolios(): Promise { try { if (!config.wrenApiKey) { - console.warn('No Wren API token configured, using fallback portfolio'); + console.warn('[wrenClient] No Wren API token configured, using fallback portfolio'); return [DEFAULT_PORTFOLIO]; } + console.log('[wrenClient] Getting portfolios with token:', config.wrenApiKey ? '********' + config.wrenApiKey.slice(-4) : 'MISSING'); + const api = createApiClient(); const response = await api.get('/portfolios'); if (!response.data?.portfolios?.length) { - console.warn('No portfolios returned from API, using fallback'); + console.warn('[wrenClient] No portfolios returned from API, using fallback'); return [DEFAULT_PORTFOLIO]; } @@ -151,7 +162,7 @@ export async function getPortfolios(): Promise { }); } catch (error) { logError(error); - console.warn('Failed to fetch portfolios from API, using fallback'); + console.warn('[wrenClient] Failed to fetch portfolios from API, using fallback'); return [DEFAULT_PORTFOLIO]; } } @@ -163,9 +174,12 @@ export async function createOffsetOrder( ): Promise { try { if (!config.wrenApiKey) { + console.error('[wrenClient] Cannot create order - missing API token'); throw new Error('Carbon offset service is currently unavailable. Please contact support.'); } + console.log(`[wrenClient] Creating offset order: portfolio=${portfolioId}, tons=${tons}`); + const api = createApiClient(); const response = await api.post('/orders', { portfolio_id: portfolioId, @@ -199,21 +213,34 @@ export async function createOffsetOrder( createdAt: order.created_at, dryRun: order.dry_run }; - } catch (error) { + } catch (error: unknown) { logError(error); if (axios.isAxiosError(error)) { - if (error.code === 'ECONNABORTED') { + const axiosError = error as AxiosError; + + console.error('[wrenClient] Axios error details:', { + status: axiosError.response?.status, + data: axiosError.response?.data, + config: { + url: axiosError.config?.url, + method: axiosError.config?.method, + headers: axiosError.config?.headers ? 'Headers present' : 'No headers', + baseURL: axiosError.config?.baseURL + } + }); + + if (axiosError.code === 'ECONNABORTED') { throw new Error('Request timed out. Please try again.'); } - if (!error.response) { + if (!axiosError.response) { throw new Error('Network error. Please check your connection and try again.'); } - if (error.response.status === 401) { - throw new Error('Carbon offset service is currently unavailable. Please try again later or contact support.'); + if (axiosError.response.status === 401) { + throw new Error('Carbon offset service authentication failed. Please check your API token.'); } } throw new Error('Failed to create offset order. Please try again.'); } -} \ No newline at end of file +} diff --git a/src/utils/config.ts b/src/utils/config.ts index b29eae9..8cad69b 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -5,19 +5,23 @@ interface Config { isProduction: boolean; } -// First try to get from window.env (for containerized environment) -// Fall back to Vite's import.meta.env (for development) +// Get environment variables either from window.env (for Docker) or import.meta.env (for development) const getEnv = (key: string): string => { - // Remove VITE_ prefix when checking window.env - const windowKey = key.replace('VITE_', ''); + // Extract the name without VITE_ prefix + const varName = key.replace('VITE_', ''); - // Check if window.env exists and has the variable - if (typeof window !== 'undefined' && window.env && window.env[windowKey]) { - return window.env[windowKey] || ''; + // First check if window.env exists and has the variable + if (typeof window !== 'undefined' && window.env) { + // In Docker, the env.sh script has already removed the VITE_ prefix + const envValue = window.env[varName]; + if (envValue) { + console.log(`Using ${varName} from window.env`); + return envValue; + } } - // Fall back to Vite's import.meta.env - // Ensure we're only returning string values + // Fall back to Vite's import.meta.env (for development) + // Here we need the full name with VITE_ prefix const value = import.meta.env[key]; return typeof value === 'string' ? value : ''; }; @@ -37,7 +41,8 @@ export const config: Config = { // Validate required environment variables if (!config.wrenApiKey) { - console.error('Missing required environment variable: VITE_WREN_API_TOKEN'); + console.error('Missing required environment variable: WREN_API_TOKEN'); + console.error('Current environment:', window?.env ? JSON.stringify(window.env) : 'No window.env available'); } // Log config in development