From 8984e476cd77099ded57ae3b46a0fe0319d4b5d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:08:17 +0000 Subject: [PATCH 1/7] Initial plan From f31aaebf8dc602ba5c9641c1bae71cae8831405a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:12:41 +0000 Subject: [PATCH 2/7] Add comprehensive ROUTES.md documentation explaining all application paths Co-authored-by: Abdulmuiz44 <192426777+Abdulmuiz44@users.noreply.github.com> --- ROUTES.md | 495 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 ROUTES.md diff --git a/ROUTES.md b/ROUTES.md new file mode 100644 index 00000000..0da0d38f --- /dev/null +++ b/ROUTES.md @@ -0,0 +1,495 @@ +# Tradia Routes Documentation + +This document explains all the routes and paths available in the Tradia application. Tradia is built using Next.js 13+ with the App Router pattern. + +## Table of Contents +- [Public Pages](#public-pages) +- [Authentication Pages](#authentication-pages) +- [Dashboard Pages](#dashboard-pages) +- [API Endpoints](#api-endpoints) + - [Authentication APIs](#authentication-apis) + - [Trade APIs](#trade-apis) + - [MT5 Integration APIs](#mt5-integration-apis) + - [Payment APIs](#payment-apis) + - [AI & Coach APIs](#ai--coach-apis) + - [User Management APIs](#user-management-apis) + - [Analytics APIs](#analytics-apis) + +--- + +## Public Pages + +These pages are accessible to anyone without authentication: + +### `/` (Home/Landing Page) +- **Purpose**: Main landing page for Tradia +- **Features**: + - Overview of Tradia's AI-powered trading performance assistant + - Feature highlights (Smart Performance Tracking, Secure & Private, Lightning-Fast Feedback, Trade Anywhere) + - Call-to-action buttons to sign up or login + - Responsive design for mobile and desktop + +### `/about` +- **Purpose**: Information about Tradia +- **Features**: Company background, mission, and team information + +### `/contact` +- **Purpose**: Contact page for user inquiries +- **Features**: Form to reach out to Tradia support team + +### `/pricing` +- **Purpose**: Display subscription plans and pricing +- **Features**: + - Free plan ($0/month) + - Pro plan ($9/month or $90/year) + - Plus plan ($19/month or $190/year) + - Elite plan ($39/month or $390/year) + - Feature comparison between plans + +### `/privacy` +- **Purpose**: Privacy policy page +- **Features**: Details on how user data is collected, used, and protected + +### `/terms` +- **Purpose**: Terms of service page +- **Features**: Legal terms and conditions for using Tradia + +--- + +## Authentication Pages + +These pages handle user authentication and account management: + +### `/login` +- **Purpose**: User login page +- **Features**: + - Email and password login + - Google OAuth login option + - "Remember me" functionality + - Link to forgot password + - Link to signup page + +### `/signup` +- **Purpose**: New user registration +- **Features**: + - Email and password registration + - Google OAuth signup option + - Email verification required + - Password strength validation + +### `/forgot-password` +- **Purpose**: Password reset request +- **Features**: + - Email input to request password reset + - Sends reset link to user's email + +### `/reset-password` +- **Purpose**: Set new password after reset request +- **Features**: + - Token-based password reset + - New password confirmation + +### `/verify-email` +- **Purpose**: Email verification landing page +- **Features**: + - Verifies user email with token from email link + - Sub-routes: + - `/verify-email/success` - Successful verification confirmation + - `/verify-email/failed` - Failed verification message + +### `/resend-verification` +- **Purpose**: Resend email verification link +- **Features**: + - For users who didn't receive or lost verification email + +### `/check-email` +- **Purpose**: Confirmation page after signup +- **Features**: + - Informs user to check email for verification link + +--- + +## Dashboard Pages + +Protected pages requiring authentication. All dashboard pages are under the `/dashboard` route: + +### `/dashboard` (Main Dashboard) +- **Purpose**: Central hub for trading performance +- **Features**: + - Overview cards with key metrics (total trades, win rate, profit/loss, etc.) + - Trade history table + - Mental coach section with AI insights + - Weekly coach recap + - Risk guard alerts + - Risk metrics visualization + - Position sizing calculator + - Trade journal + - Trade analytics with charts + - Trade planner + - AI chat interface + - MT5 integration wizard + - Multiple tabs for different views: + - Overview + - Analytics + - Journal + - Planner + - Risk Management + - Education + - MT5 Integration + - AI Coach + +### `/dashboard/analytics` +- **Purpose**: Detailed trading analytics page +- **Features**: + - Performance charts (line, bar, pie, donut) + - Time-based insights + - Pair-level performance breakdown + - Win rate, profit factor, drawdown analysis + - Powered by Plotly for interactive visualizations + +### `/dashboard/profile` +- **Purpose**: User profile management +- **Features**: + - View and edit personal information + - Avatar upload + - Account details + +### `/dashboard/settings` +- **Purpose**: User preferences and settings +- **Features**: + - Account settings + - Notification preferences + - Password change + - Theme settings (dark/light mode) + +### `/dashboard/billing` +- **Purpose**: Subscription and billing management +- **Features**: + - Current plan information + - Upgrade/downgrade options + - Payment history + - Invoice downloads + +### `/dashboard/mt5/connect` +- **Purpose**: MT5 (MetaTrader 5) account connection +- **Features**: + - Connect MT5 trading account + - Input MT5 credentials + - Account validation + - Multiple account management + +### `/dashboard/mt5/sync` +- **Purpose**: Synchronize MT5 trade data +- **Features**: + - Manual and automatic sync options + - View sync status + - Import trades from MT5 account + +--- + +## API Endpoints + +All API endpoints are under the `/api` route and return JSON responses. + +### Authentication APIs + +#### `POST /api/auth/signup` +- **Purpose**: Create new user account +- **Body**: `{ email, password, name }` +- **Returns**: User object or error + +#### `POST /api/auth/login` +- **Purpose**: Authenticate user +- **Body**: `{ email, password }` +- **Returns**: JWT token and user session + +#### `POST /api/auth/forgot-password` +- **Purpose**: Request password reset +- **Body**: `{ email }` +- **Returns**: Success message + +#### `POST /api/auth/reset-password` +- **Purpose**: Reset password with token +- **Body**: `{ token, newPassword }` +- **Returns**: Success message + +#### `POST /api/auth/verify-email` +- **Purpose**: Verify user email address +- **Body**: `{ token }` +- **Returns**: Verification status + +#### `POST /api/auth/resend-verification` +- **Purpose**: Resend verification email +- **Body**: `{ email }` +- **Returns**: Success message + +#### `POST /api/auth/change-password` +- **Purpose**: Change password for authenticated user +- **Body**: `{ currentPassword, newPassword }` +- **Returns**: Success message + +#### `POST /api/auth/refresh` +- **Purpose**: Refresh authentication token +- **Returns**: New JWT token + +#### `GET/POST /api/auth/[...nextauth]` +- **Purpose**: NextAuth.js authentication handler +- **Features**: Handles OAuth and session management + +--- + +### Trade APIs + +#### `GET /api/trades` +- **Purpose**: Fetch user's trade history +- **Query Parameters**: + - `limit` - Number of trades to return (default: 100) + - `offset` - Pagination offset (default: 0) + - `symbol` - Filter by trading symbol + - `outcome` - Filter by win/loss + - `fromDate` - Start date filter + - `toDate` - End date filter +- **Returns**: Array of trade objects + +#### `POST /api/trades` +- **Purpose**: Create new trade manually +- **Body**: Trade details (symbol, entry, exit, profit, etc.) +- **Returns**: Created trade object + +#### `POST /api/trades/import` +- **Purpose**: Import trades from CSV/XLSX file +- **Body**: File upload with trade data +- **Returns**: Import summary (success count, errors) + +--- + +### MT5 Integration APIs + +#### `GET /api/mt5/accounts` +- **Purpose**: Get user's connected MT5 accounts +- **Returns**: Array of MT5 account objects + +#### `POST /api/mt5/accounts` +- **Purpose**: Add new MT5 account +- **Body**: MT5 account credentials +- **Returns**: Created account object + +#### `GET /api/mt5/accounts/[id]` +- **Purpose**: Get specific MT5 account details +- **Returns**: MT5 account object + +#### `PUT /api/mt5/accounts/[id]` +- **Purpose**: Update MT5 account +- **Body**: Updated account details +- **Returns**: Updated account object + +#### `DELETE /api/mt5/accounts/[id]` +- **Purpose**: Remove MT5 account +- **Returns**: Success message + +#### `POST /api/mt5/connect` +- **Purpose**: Establish connection to MT5 account +- **Body**: MT5 credentials and server details +- **Returns**: Connection status + +#### `POST /api/mt5/validate` +- **Purpose**: Validate MT5 credentials +- **Body**: MT5 credentials +- **Returns**: Validation result + +#### `GET /api/mt5/account-info` +- **Purpose**: Get MT5 account information +- **Query Parameters**: `accountId` +- **Returns**: Account balance, equity, margin info + +#### `POST /api/mt5/sync` +- **Purpose**: Synchronize trades from MT5 account +- **Body**: `{ accountId }` +- **Returns**: Sync status and imported trade count + +#### `POST /api/mt5/import` +- **Purpose**: Import specific trades from MT5 +- **Body**: Trade selection criteria +- **Returns**: Imported trades + +#### `GET /api/mt5/orders` +- **Purpose**: Get open orders from MT5 +- **Query Parameters**: `accountId` +- **Returns**: Array of open orders + +#### `GET /api/mt5/positions` +- **Purpose**: Get open positions from MT5 +- **Query Parameters**: `accountId` +- **Returns**: Array of open positions + +#### `GET /api/mt5/monitoring` +- **Purpose**: Get MT5 account monitoring status +- **Returns**: Connection status, last sync time + +#### `GET /api/mt5/credentials` +- **Purpose**: Get stored MT5 credentials (encrypted) +- **Returns**: Array of credential objects + +#### `POST /api/mt5/credentials` +- **Purpose**: Store new MT5 credentials +- **Body**: Credential details +- **Returns**: Created credential object + +#### `DELETE /api/mt5/credentials/[id]` +- **Purpose**: Delete stored credentials +- **Returns**: Success message + +--- + +### Payment APIs + +#### `POST /api/payments/create-checkout` +- **Purpose**: Create payment checkout session +- **Body**: `{ planId, billingPeriod }` +- **Returns**: Checkout URL or payment intent + +#### `POST /api/payments/webhook` +- **Purpose**: Handle payment provider webhooks +- **Body**: Webhook payload from payment provider +- **Returns**: Acknowledgment + +#### `POST /api/payments/webhook/flutterwave` +- **Purpose**: Specific webhook for Flutterwave payments +- **Body**: Flutterwave webhook payload +- **Returns**: Acknowledgment + +#### `GET /api/payments/subscriptions` +- **Purpose**: Get user's active subscriptions +- **Returns**: Array of subscription objects + +#### `POST /api/payments/verify` +- **Purpose**: Verify payment transaction +- **Body**: `{ transactionId }` +- **Returns**: Payment verification status + +--- + +### AI & Coach APIs + +#### `POST /api/ai/chat` +- **Purpose**: AI chatbot for trading insights +- **Body**: + ```json + { + "message": "User question", + "tradeHistory": [], + "attachments": [], + "conversationHistory": [] + } + ``` +- **Returns**: AI-generated response with trading insights + +#### `POST /api/ai/voice` +- **Purpose**: AI voice coach for spoken feedback +- **Body**: Voice input or text for voice synthesis +- **Returns**: Audio response or voice feedback + +#### `GET /api/coach/points` +- **Purpose**: Get coaching points for user +- **Returns**: Actionable coaching recommendations + +#### `POST /api/coach/weekly-email` +- **Purpose**: Send weekly coaching recap email +- **Returns**: Email send status + +--- + +### User Management APIs + +#### `GET /api/user/profile` +- **Purpose**: Get user profile information +- **Returns**: User profile object + +#### `PUT /api/user/profile` +- **Purpose**: Update user profile +- **Body**: Updated profile fields +- **Returns**: Updated profile object + +#### `POST /api/user/update` +- **Purpose**: Update user account information +- **Body**: User details to update +- **Returns**: Updated user object + +#### `GET /api/user/settings` +- **Purpose**: Get user settings +- **Returns**: Settings object + +#### `PUT /api/user/settings` +- **Purpose**: Update user settings +- **Body**: Settings to update +- **Returns**: Updated settings + +#### `GET /api/user/plan` +- **Purpose**: Get user's subscription plan +- **Returns**: Plan details (Free, Pro, Plus, Elite) + +#### `GET /api/user/trial-status` +- **Purpose**: Check if user is in trial period +- **Returns**: Trial status and expiry date + +#### `POST /api/user/upload-avatar` +- **Purpose**: Upload user profile picture +- **Body**: Form data with image file +- **Returns**: Avatar URL + +--- + +### Analytics APIs + +#### `POST /api/analytics/track` +- **Purpose**: Track user analytics events +- **Body**: `{ event, properties }` +- **Returns**: Tracking confirmation + +#### `GET /api/analytics/user-stats` +- **Purpose**: Get user trading statistics +- **Returns**: Comprehensive trading stats (win rate, profit factor, etc.) + +#### `GET /api/analytics/activity/recent` +- **Purpose**: Get recent user activity +- **Returns**: Array of recent actions/trades + +--- + +### Additional API + +#### `POST /api/verify-email` +- **Purpose**: Alternative email verification endpoint +- **Body**: `{ token }` +- **Returns**: Verification status + +--- + +## Route Organization + +The application follows Next.js 13+ App Router conventions: + +- **Page Routes**: Defined by `page.tsx` files in route directories +- **API Routes**: Defined by `route.ts` files in `/api` directories +- **Layout**: Each route section has a `layout.tsx` for shared UI +- **Dynamic Routes**: Use `[param]` syntax for dynamic segments (e.g., `/api/mt5/accounts/[id]`) + +## Authentication & Authorization + +- **Public routes**: `/`, `/about`, `/contact`, `/pricing`, `/privacy`, `/terms`, `/login`, `/signup` +- **Protected routes**: All `/dashboard/*` routes require authentication +- **API authentication**: Most API endpoints require valid JWT token or session cookie +- **Role-based access**: Some features are restricted by subscription plan (Free, Pro, Plus, Elite) + +## Key Features by Route + +- **Trade Management**: Import CSV/XLSX, manual entry, MT5 sync +- **Analytics**: Interactive charts, performance metrics, risk analysis +- **AI Coach**: Text chat and voice feedback on trading performance +- **Journal**: Tag trades, add notes, export reports +- **Subscription**: Upgrade plans, payment via Flutterwave +- **Security**: HTTPS, encrypted data storage in Supabase, secure authentication + +--- + +*This documentation is current as of the application's latest version. For development details, see the main README.md.* From 75cbde112a009d06bad49beba28621f394765b0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:14:33 +0000 Subject: [PATCH 3/7] Improve ROUTES.md with better field descriptions and clarifications Co-authored-by: Abdulmuiz44 <192426777+Abdulmuiz44@users.noreply.github.com> --- ROUTES.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ROUTES.md b/ROUTES.md index 0da0d38f..edc4c12c 100644 --- a/ROUTES.md +++ b/ROUTES.md @@ -376,12 +376,16 @@ All API endpoints are under the `/api` route and return JSON responses. - **Body**: ```json { - "message": "User question", - "tradeHistory": [], - "attachments": [], - "conversationHistory": [] + "message": "User question (string, required if no attachments)", + "tradeHistory": "Array of trade objects (optional, for context)", + "attachments": "Array of file metadata objects (optional)", + "conversationHistory": "Array of previous messages (optional, for context)" } ``` + - `message` (string, required*): The user's question or prompt (* required if attachments not provided) + - `tradeHistory` (array, optional): User's trade data for personalized insights + - `attachments` (array, optional): File metadata for analysis `[{ name, type, size }]` + - `conversationHistory` (array, optional): Previous chat messages for context `[{ role: 'user'|'assistant', content: string }]` - **Returns**: AI-generated response with trading insights #### `POST /api/ai/voice` @@ -459,9 +463,10 @@ All API endpoints are under the `/api` route and return JSON responses. ### Additional API #### `POST /api/verify-email` -- **Purpose**: Alternative email verification endpoint +- **Purpose**: Alternative email verification endpoint (server-side verification) - **Body**: `{ token }` - **Returns**: Verification status +- **Note**: This is a POST endpoint alternative to `POST /api/auth/verify-email`. Both verify email addresses but this endpoint may be used in different contexts (e.g., server-side verification vs client-side flows). --- From e7967c22e10635b4abd083680ea9671c062ba3f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:15:59 +0000 Subject: [PATCH 4/7] Clarify email verification endpoints with implementation details Co-authored-by: Abdulmuiz44 <192426777+Abdulmuiz44@users.noreply.github.com> --- ROUTES.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ROUTES.md b/ROUTES.md index edc4c12c..96adb979 100644 --- a/ROUTES.md +++ b/ROUTES.md @@ -213,10 +213,15 @@ All API endpoints are under the `/api` route and return JSON responses. - **Body**: `{ token, newPassword }` - **Returns**: Success message +#### `GET /api/auth/verify-email` +- **Purpose**: Verify user email address via token (from email link) +- **Query Parameters**: `token` - Verification token from email +- **Returns**: Redirects to `/verify-email/success` or `/verify-email/failed` + #### `POST /api/auth/verify-email` -- **Purpose**: Verify user email address +- **Purpose**: Programmatic email verification (for server-to-server calls) - **Body**: `{ token }` -- **Returns**: Verification status +- **Returns**: JSON response `{ ok: true }` or error #### `POST /api/auth/resend-verification` - **Purpose**: Resend verification email @@ -462,11 +467,11 @@ All API endpoints are under the `/api` route and return JSON responses. ### Additional API -#### `POST /api/verify-email` -- **Purpose**: Alternative email verification endpoint (server-side verification) -- **Body**: `{ token }` -- **Returns**: Verification status -- **Note**: This is a POST endpoint alternative to `POST /api/auth/verify-email`. Both verify email addresses but this endpoint may be used in different contexts (e.g., server-side verification vs client-side flows). +#### `GET /api/verify-email` +- **Purpose**: Alternative email verification endpoint with JWT re-issuance +- **Query Parameters**: `token` - Verification token +- **Returns**: Redirects to success/failed page and sets new JWT cookie with updated `email_verified` claim +- **Note**: This endpoint uses a different implementation than `/api/auth/verify-email`. It calls a SQL function `verify_email` and re-issues the JWT token with updated verification status. Use this when you need the JWT token to be automatically refreshed after verification. --- From caefa8441ad4db099913ad6c212567a0c4df2e03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:17:09 +0000 Subject: [PATCH 5/7] Final clarity improvements to ROUTES.md documentation Co-authored-by: Abdulmuiz44 <192426777+Abdulmuiz44@users.noreply.github.com> --- ROUTES.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ROUTES.md b/ROUTES.md index 96adb979..653acd7a 100644 --- a/ROUTES.md +++ b/ROUTES.md @@ -237,9 +237,10 @@ All API endpoints are under the `/api` route and return JSON responses. - **Purpose**: Refresh authentication token - **Returns**: New JWT token -#### `GET/POST /api/auth/[...nextauth]` +#### `* /api/auth/[...nextauth]` (supports GET and POST) - **Purpose**: NextAuth.js authentication handler - **Features**: Handles OAuth and session management +- **Note**: This is a catch-all route that handles multiple HTTP methods for NextAuth.js flows --- @@ -387,7 +388,7 @@ All API endpoints are under the `/api` route and return JSON responses. "conversationHistory": "Array of previous messages (optional, for context)" } ``` - - `message` (string, required*): The user's question or prompt (* required if attachments not provided) + - `message` (string, required when attachments not provided): The user's question or prompt - `tradeHistory` (array, optional): User's trade data for personalized insights - `attachments` (array, optional): File metadata for analysis `[{ name, type, size }]` - `conversationHistory` (array, optional): Previous chat messages for context `[{ role: 'user'|'assistant', content: string }]` From c2618e34bb3d5830b2a8034301aee1d4ff1a7f74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:26:58 +0000 Subject: [PATCH 6/7] Add comprehensive code explanation for AIChatInterface.tsx Co-authored-by: Abdulmuiz44 <192426777+Abdulmuiz44@users.noreply.github.com> --- AICHATINTERFACE_EXPLANATION.md | 469 +++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 AICHATINTERFACE_EXPLANATION.md diff --git a/AICHATINTERFACE_EXPLANATION.md b/AICHATINTERFACE_EXPLANATION.md new file mode 100644 index 00000000..25097a19 --- /dev/null +++ b/AICHATINTERFACE_EXPLANATION.md @@ -0,0 +1,469 @@ +# AIChatInterface.tsx - Code Explanation + +## Overview +`AIChatInterface.tsx` is a React component that provides an AI-powered trading coach chatbot interface. It enables users to interact with an AI assistant that analyzes their trading performance, provides personalized coaching, and offers strategic insights. + +**Location**: `src/components/ai/AIChatInterface.tsx` +**Size**: 678 lines +**Type**: Client-side React component ("use client") + +--- + +## Main Features + +### 1. **AI Chat Interface** +- Real-time conversation with an AI trading coach +- Context-aware responses based on trading history +- Message history with conversation context (keeps last 5 messages) +- Typing indicators for better UX + +### 2. **Voice Features** +- **Speech Recognition**: Convert voice to text for user input +- **Text-to-Speech**: AI responses can be read aloud +- **Voice Settings Panel**: + - Toggle voice responses on/off + - Auto-speak setting for automatic response reading + - Adjustable speech speed (0.5x to 2x) + - Voice pitch control + - Test voice functionality + +### 3. **File Upload & Image Analysis** +- Upload trading screenshots for AI analysis +- Multiple file support +- Preview uploaded files in chat +- Pro/Plus/Elite tier feature (locked for Free/Starter users) + +### 4. **Subscription Tier Integration** +- Displays user's current plan (Free, Starter, Pro, Plus, Elite) +- Feature restrictions based on subscription level: + - Free/Starter: Basic chat, no image analysis + - Pro+: Advanced AI, screenshot analysis, personalized strategies +- Visual tier indicators with crown icons and color coding + +### 5. **Intelligent Response System** +- Fallback responses when API fails +- Pattern-matching for common queries: + - Greetings → Personalized welcome + trading snapshot + - Performance queries → Advanced performance analysis + - Strategy questions → Strategy recommendations + - Risk management → Risk analysis and tips + - Timing/entry → Market timing recommendations + - Emotional support → Motivational insights + - Winning streaks → Celebration with growth tips + - Mindset questions → Personalized motivation + - Screenshots → Advanced visual analysis + +--- + +## Technical Architecture + +### State Management + +```typescript +// Message State +const [messages, setMessages] = useState() // Chat history +const [inputMessage, setInputMessage] = useState('') // Current input +const [isTyping, setIsTyping] = useState(false) // Loading indicator + +// File Upload State +const [uploadedFiles, setUploadedFiles] = useState([]) + +// Voice State +const [isRecording, setIsRecording] = useState(false) // Mic active +const [isSpeaking, setIsSpeaking] = useState(false) // TTS active +const [voiceSettings, setVoiceSettings] = useState({ + voiceEnabled: boolean, + autoSpeak: boolean, + voiceSpeed: number, + voicePitch: number, + selectedVoice: SpeechSynthesisVoice | null +}) + +// UI State +const [showVoiceSettings, setShowVoiceSettings] = useState(false) + +// User Context +const { trades } = useContext(TradeContext) // Trading data +const { plan } = useUser() // Subscription tier +const [userTier, setUserTier] = useState<'free' | 'starter' | 'pro' | 'plus' | 'elite'>() +``` + +### Key Interfaces + +#### Message Interface +```typescript +interface Message { + id: string; // Unique identifier (timestamp) + type: 'user' | 'assistant'; // Message sender + content: string; // Message text + timestamp: Date; // When sent + attachments?: File[]; // Optional uploaded files + isTyping?: boolean; // Loading state + isVoice?: boolean; // Voice input indicator +} +``` + +#### VoiceSettings Interface +```typescript +interface VoiceSettings { + voiceEnabled: boolean; // Enable/disable TTS + autoSpeak: boolean; // Auto-read AI responses + voiceSpeed: number; // 0.5-2.0x speed + voicePitch: number; // Voice pitch + selectedVoice: SpeechSynthesisVoice | null; // Voice selection +} +``` + +--- + +## Core Functions + +### 1. **handleSendMessage()** +**Purpose**: Processes and sends user messages to the AI backend + +**Flow**: +1. Creates user message object with content and attachments +2. Adds message to chat history +3. Checks tier restrictions (blocks image uploads for Free/Starter) +4. Sends POST request to `/api/ai/chat` with: + - User message + - Trade history (for context) + - File metadata (not raw files) + - Conversation history (last 5 messages) +5. Receives AI response from API +6. Displays AI response in chat +7. Auto-speaks response if voice settings enabled +8. Falls back to local intelligent response if API fails +9. Clears input and uploaded files + +**Error Handling**: Displays error message in chat if API fails + +### 2. **Voice Recognition Functions** + +#### startVoiceRecording() +- Activates Web Speech API speech recognition +- Sets recording state to true +- Converts voice to text and populates input field + +#### stopVoiceRecording() +- Stops active speech recognition +- Updates recording state + +#### cleanForSpeech(text: string) +- Sanitizes markdown and HTML from text +- Removes formatting characters (**bold**, *italic*, `code`) +- Removes HTML tags +- Trims whitespace +- Prepares text for natural speech output + +#### speakText(text: string) +- Uses Web Speech API SpeechSynthesis +- Cancels any ongoing speech +- Creates utterance with cleaned text +- Applies voice settings (speed, pitch, volume, voice) +- Sets speaking state indicators +- Handles start/end/error events + +#### stopSpeaking() +- Cancels ongoing speech synthesis +- Resets speaking state + +### 3. **File Upload Functions** + +#### handleFileUpload(event) +- Extracts files from input event +- Adds files to uploadedFiles state array +- Multiple files supported + +#### removeFile(index) +- Removes file at specified index from array +- Updates uploadedFiles state + +### 4. **Utility Functions** + +#### scrollToBottom() +- Auto-scrolls chat to show latest message +- Smooth scroll behavior + +#### handleKeyPress(event) +- Sends message on Enter key (without Shift) +- Allows Shift+Enter for multi-line input + +#### normalizeTier(plan) +- Converts plan string to standardized tier enum +- Returns 'free' as default for invalid values + +### 5. **generateIntelligentCoachingResponse()** +**Purpose**: Fallback response generator when API is unavailable + +**Pattern Matching**: +- Analyzes user message keywords +- Routes to appropriate response generator from `advancedAnalysis` library +- Provides contextual coaching based on: + - Greetings → Welcome + snapshot + - Performance queries → Analysis + - Strategy questions → Recommendations + - Risk queries → Risk analysis + - Timing questions → Market timing tips + - Emotional keywords → Support/motivation + - Screenshots → Visual analysis + +**Returns**: Formatted markdown string with personalized insights + +--- + +## UI Components + +### Header Section +- **AI Status Indicator**: Green pulsing dot showing AI is online +- **Subscription Tier Badge**: Visual indicator with crown icon +- **Voice Settings Button**: Opens settings panel +- **Speaking Indicator**: Shows when TTS is active + +### Voice Settings Panel (Collapsible) +- **Voice Responses Toggle**: Enable/disable text-to-speech +- **Auto Speak Toggle**: Automatically read AI responses +- **Speed Slider**: Adjust speech speed (0.5x - 2.0x) +- **Test Voice Button**: Preview voice settings +- **Stop Button**: Cancel ongoing speech + +### Messages Area +- **Scrollable Container**: Shows conversation history +- **User Messages**: Right-aligned, blue background +- **AI Messages**: Left-aligned, dark background with bot icon +- **Typing Indicator**: Animated dots when AI is processing +- **Timestamps**: Shows when each message was sent +- **Attachments Display**: Shows uploaded files in messages + +### Input Composer +- **Multiline Textarea**: Message input field +- **Voice Recording Button**: Toggle mic for voice input (red when active) +- **Image Upload Button**: Upload screenshots (locked for Free/Starter) + - Shows lock icon for restricted tiers + - Shows paperclip icon for Pro+ tiers +- **Send Button**: Submit message (disabled when input empty) +- **Suggestion Pills**: Quick action hints and stats + - Voice suggestion example + - Strategy advice hint + - Trade count display + - AI online status + +### File Preview +- Shows uploaded files before sending +- Individual remove buttons for each file +- Truncates long filenames + +--- + +## API Integration + +### Endpoint: `/api/ai/chat` +**Method**: POST + +**Request Body**: +```json +{ + "message": "User's question", + "tradeHistory": [/* array of trade objects */], + "attachments": [/* metadata only: { name, type, size } */], + "conversationHistory": [ + { "role": "user", "content": "..." }, + { "role": "assistant", "content": "..." } + ] +} +``` + +**Response**: +```json +{ + "response": "AI-generated response text" +} +``` + +**Error Handling**: Falls back to `generateIntelligentCoachingResponse()` + +--- + +## Subscription Tier Features + +| Feature | Free/Starter | Pro | Plus | Elite | +|---------|-------------|-----|------|-------| +| Basic Chat | ✅ | ✅ | ✅ | ✅ | +| Voice Input | ✅ | ✅ | ✅ | ✅ | +| Voice Output | ✅ | ✅ | ✅ | ✅ | +| Image Upload | ❌ | ✅ | ✅ | ✅ | +| Screenshot Analysis | ❌ | ✅ | ✅ | ✅ | +| Advanced AI | ❌ | ✅ | ✅ | ✅ | +| Personalized Strategies | ❌ | ✅ | ✅ | ✅ | + +--- + +## Browser API Usage + +### Web Speech API +1. **SpeechRecognition**: Voice-to-text conversion + - Supports Chrome, Edge, Safari (with webkit prefix) + - Continuous: false (single utterance) + - Language: en-US + +2. **SpeechSynthesis**: Text-to-speech output + - Available in all modern browsers + - Supports multiple voices (system-dependent) + - Adjustable rate, pitch, volume + +--- + +## Dependencies + +### External Libraries +- **React**: Component framework +- **lucide-react**: Icon library (Bot, Send, Mic, Volume2, etc.) +- **TradeContext**: Provides trade data +- **UserContext**: Provides user plan/subscription info + +### Custom Modules +- **@/lib/ai/advancedAnalysis**: AI response generation helpers + - `analyzeTradingPerformance()` + - `generatePersonalizedGreeting()` + - `generateTradingSnapshot()` + - `generateAdvancedPerformanceAnalysis()` + - `generateStrategyRecommendations()` + - `generateRiskManagementAnalysis()` + - `generateMarketTimingRecommendations()` + - `generateEmotionalSupportWithInsights()` + - `generateWinningCelebrationWithGrowth()` + - `generatePersonalizedMotivation()` + - `generateAdvancedScreenshotAnalysis()` + - `generateDefaultIntelligentResponse()` + +### Plan Access +- **@/lib/planAccess**: `PLAN_LIMITS` and `PlanType` for feature restrictions + +--- + +## Styling + +### Design System +- **Color Scheme**: Dark theme with blue accents + - Background: `#161B22`, `#0D1117`, `#1a1f2e` + - Borders: `#2a2f3a` + - Primary: Blue (`blue-400`, `blue-600`) + - Success: Green + - Warning: Yellow + - Danger: Red + +- **Responsive**: Mobile-first with md: breakpoints +- **Animations**: + - Bounce for typing indicator + - Pulse for active states + - Smooth transitions for interactions + +### Accessibility +- Touch-friendly buttons with `touch-manipulation` class +- Keyboard navigation (Enter to send) +- Screen reader labels via `title` attributes +- Disabled states for restricted features + +--- + +## Performance Optimizations + +1. **Client-Side Only**: `"use client"` directive for React 18+ Server Components +2. **Refs for DOM Elements**: Prevents re-renders + - `messagesEndRef`: Auto-scroll target + - `fileInputRef`: File input trigger + - `recognitionRef`: Speech recognition instance + - `synthRef`: Speech synthesis instance +3. **Cleanup**: Aborts speech recognition and cancels synthesis on unmount +4. **Conversation Context**: Only sends last 5 messages to API (reduces payload) +5. **Metadata Only**: Sends file metadata, not raw File objects to API + +--- + +## Use Cases + +### 1. Performance Review +**User**: "How's my trading this week?" +**AI**: Analyzes recent trades, calculates win rate, profit factor, identifies patterns + +### 2. Strategy Optimization +**User**: "What's my best trading pattern?" +**AI**: Reviews historical data, identifies winning strategies, provides recommendations + +### 3. Risk Management +**User**: "How can I improve my stop losses?" +**AI**: Analyzes risk metrics, suggests position sizing, reviews drawdown patterns + +### 4. Screenshot Analysis (Pro+) +**User**: Uploads chart screenshot +**AI**: Analyzes technical indicators, identifies patterns, suggests entry/exit points + +### 5. Emotional Support +**User**: "I'm stuck in a losing streak" +**AI**: Provides motivational support, analyzes what went wrong, offers recovery strategy + +### 6. Voice Interaction +**User**: Clicks mic, speaks "What should I trade next?" +**AI**: Converts speech to text, processes query, responds with voice output + +--- + +## Edge Cases Handled + +1. **Missing Token**: Graceful degradation when speech APIs unavailable +2. **API Failure**: Falls back to local intelligent response generator +3. **Tier Restrictions**: Shows upgrade prompts instead of errors +4. **Empty Messages**: Disables send button when no content +5. **Browser Compatibility**: Checks for webkit prefix on SpeechRecognition +6. **Storage Access**: Defensive try-catch for localStorage operations +7. **File Type Validation**: Accepts only image files +8. **Concurrent Speech**: Cancels previous utterances before starting new ones + +--- + +## Integration Points + +### 1. Dashboard Integration +- Embedded in main dashboard as "AI Coach" tab +- Access via dashboard navigation + +### 2. Trade Context +- Automatically receives trade data from TradeContext +- Uses real-time trading performance for analysis + +### 3. User Context +- Syncs with user subscription plan +- Enforces feature restrictions based on tier + +### 4. API Backend +- Connects to `/api/ai/chat` for advanced AI processing +- Falls back to local analysis if backend unavailable + +--- + +## Future Enhancement Opportunities + +1. **Conversation History Persistence**: Save chat history to database +2. **Custom Voice Selection**: Allow users to choose preferred voice +3. **Multi-language Support**: Translate interface and responses +4. **Export Chat**: Download conversation as PDF/text +5. **Suggested Prompts**: Dynamic quick actions based on trading data +6. **Real-time Notifications**: Push alerts for important insights +7. **Video Upload**: Analyze trading session recordings +8. **Group Chat**: Connect with other traders (community feature) +9. **Calendar Integration**: Schedule trading reviews +10. **Performance Graphs**: Inline chart rendering in chat + +--- + +## Summary + +`AIChatInterface.tsx` is a comprehensive AI chatbot component that serves as a personal trading coach. It combines: +- **Natural conversation** with context awareness +- **Voice interaction** for hands-free trading analysis +- **Visual analysis** of trading screenshots (Pro+ feature) +- **Intelligent fallbacks** when API is unavailable +- **Tier-based access** to premium features +- **Responsive design** for mobile and desktop + +The component is highly interactive, accessible, and provides real value to traders by analyzing their performance and offering actionable insights. From 9173bb52842c7258db94a2049ac1d32109db1e55 Mon Sep 17 00:00:00 2001 From: Abdulmuiz44 Date: Sun, 9 Nov 2025 04:37:07 +0100 Subject: [PATCH 7/7] MADE SOME MODIFICATIONS AND FIXED SOME ERRORS --- .npmrc | 2 +- build.log | Bin 0 -> 23672 bytes package.json | 3 ++- pnpm-debug.ndjson | Bin 0 -> 568718 bytes pnpm-lock.yaml | 10 ++++++++- src/app/api/auth/login/route.ts | 24 +++++++++++++++++++--- src/app/api/auth/refresh/route.ts | 24 +++++++++++++++++++--- src/app/api/mt5/credentials/[id]/route.ts | 19 ++++++++++------- src/app/api/mt5/credentials/route.ts | 10 ++++++--- src/app/api/mt5/sync/route.ts | 7 +++++-- src/app/api/user/trial-status/route.ts | 7 +++++++ src/app/api/verify-email/route.ts | 9 +++++++- src/components/mt5/CredentialManager.tsx | 1 - src/lib/connection-monitor.ts | 11 ++++++---- src/lib/credential-storage.ts | 4 +++- 15 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 build.log create mode 100644 pnpm-debug.ndjson diff --git a/.npmrc b/.npmrc index f2e2eba2..8c90ae71 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -package-manager=pnpm@latest +package-manager=pnpm@10.18.0 diff --git a/build.log b/build.log new file mode 100644 index 0000000000000000000000000000000000000000..7504fd98e44aa6a918c0caef1a11a8b25b2587f8 GIT binary patch literal 23672 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 LTn>B~Hl+GLwiWc= literal 0 HcmV?d00001 diff --git a/package.json b/package.json index c48a3106..db5dc077 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@tailwindcss/postcss": "^4", "@types/bcrypt": "^6.0.0", "@types/file-saver": "^2.0.7", + "@types/aria-query": "^5.0.1", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20", "@types/nodemailer": "^7.0.1", @@ -103,7 +104,7 @@ "eslint": "^9", "eslint-config-next": "15.4.2", "postcss": "^8.5.6", - "sharp": " ^0.33.5 ", + "sharp": "^0.33.5", "tailwindcss": "^3.4.1", "typescript": "^5" } diff --git a/pnpm-debug.ndjson b/pnpm-debug.ndjson new file mode 100644 index 0000000000000000000000000000000000000000..2e9f5b2ddc42fa0f1887cb522a43c1e6b3636629 GIT binary patch literal 568718 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 DIvTTD literal 0 HcmV?d00001 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7254dec5..031e9337 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,6 +231,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.1.12 + '@types/aria-query': + specifier: ^5.0.1 + version: 5.0.4 '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 @@ -271,7 +274,7 @@ importers: specifier: ^8.5.6 version: 8.5.6 sharp: - specifier: ' ^0.33.5 ' + specifier: ^0.33.5 version: 0.33.5 tailwindcss: specifier: ^3.4.1 @@ -2465,6 +2468,9 @@ packages: '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/bcrypt@6.0.0': resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} @@ -10143,6 +10149,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + '@types/bcrypt@6.0.0': dependencies: '@types/node': 20.19.11 diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index be73a7e5..45061274 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -5,8 +5,21 @@ import crypto from "crypto"; import { v4 as uuidv4 } from "uuid"; import { createAdminSupabase } from "@/utils/supabase/admin"; -const JWT_SECRET = process.env.JWT_SECRET!; -if (!JWT_SECRET) console.warn("JWT_SECRET not set"); +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +let loggedMissingSecret = false; +const getJwtSecret = (): string | null => { + const secret = process.env.JWT_SECRET; + if (!secret) { + if (!loggedMissingSecret) { + console.warn("JWT_SECRET not set; login route is disabled."); + loggedMissingSecret = true; + } + return null; + } + return secret; +}; /** * POST /api/auth/login @@ -23,6 +36,11 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Email and password required." }, { status: 400 }); } + const jwtSecret = getJwtSecret(); + if (!jwtSecret) { + return NextResponse.json({ error: "Server misconfigured." }, { status: 500 }); + } + const supabase = createAdminSupabase(); // --- Fetch user record --- @@ -77,7 +95,7 @@ export async function POST(req: Request) { plan, role, }, - JWT_SECRET, + jwtSecret, { expiresIn: "12h" } ); diff --git a/src/app/api/auth/refresh/route.ts b/src/app/api/auth/refresh/route.ts index 9a27e257..b9fbf6d7 100644 --- a/src/app/api/auth/refresh/route.ts +++ b/src/app/api/auth/refresh/route.ts @@ -6,8 +6,21 @@ import crypto from "crypto"; import { v4 as uuidv4 } from "uuid"; import { createAdminSupabase } from "@/utils/supabase/admin"; -const JWT_SECRET = process.env.JWT_SECRET!; -if (!JWT_SECRET) console.warn("JWT_SECRET not set"); +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +let loggedMissingSecret = false; +const getJwtSecret = (): string | null => { + const secret = process.env.JWT_SECRET; + if (!secret) { + if (!loggedMissingSecret) { + console.warn("JWT_SECRET not set; refresh route is disabled."); + loggedMissingSecret = true; + } + return null; + } + return secret; +}; export async function POST() { try { @@ -19,6 +32,11 @@ export async function POST() { return NextResponse.json({ error: "Missing refresh credentials" }, { status: 401 }); } + const jwtSecret = getJwtSecret(); + if (!jwtSecret) { + return NextResponse.json({ error: "Server misconfigured." }, { status: 500 }); + } + const supabase = createAdminSupabase(); // fetch session by id @@ -65,7 +83,7 @@ export async function POST() { name: user.name, email_verified: Boolean(user.email_verified), }, - JWT_SECRET, + jwtSecret, { expiresIn: "12h" } ); diff --git a/src/app/api/mt5/credentials/[id]/route.ts b/src/app/api/mt5/credentials/[id]/route.ts index 9731152f..43467bf9 100644 --- a/src/app/api/mt5/credentials/[id]/route.ts +++ b/src/app/api/mt5/credentials/[id]/route.ts @@ -2,10 +2,12 @@ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/authOptions"; -import { credentialStorage } from "@/lib/credential-storage"; +import { getCredentialStorage } from "@/lib/credential-storage"; import { createClient } from "@/utils/supabase/server"; import { requireActiveTrialOrPaid } from "@/lib/trial"; +export const runtime = "nodejs"; + function asString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } @@ -54,10 +56,11 @@ export async function GET( ); } + const storage = getCredentialStorage(); const credentialId = params.id; // Get the specific credential - const credential = await credentialStorage.getCredentials(user.id, credentialId); + const credential = await storage.getCredentials(user.id, credentialId); if (!credential) { return NextResponse.json( @@ -67,7 +70,7 @@ export async function GET( } // Get credential metadata (without password) - const credentials = await credentialStorage.getUserCredentials(user.id); + const credentials = await storage.getUserCredentials(user.id); const metadata = credentials.find(c => c.id === credentialId); if (!metadata) { @@ -147,6 +150,7 @@ export async function PUT( ); } + const storage = getCredentialStorage(); const credentialId = params.id; const body = await req.json() as { name?: string; @@ -154,7 +158,7 @@ export async function PUT( }; // Get existing credential for comparison - const existingCredential = await credentialStorage.getCredentials(user.id, credentialId); + const existingCredential = await storage.getCredentials(user.id, credentialId); if (!existingCredential) { return NextResponse.json( { error: "CREDENTIAL_NOT_FOUND", message: "Credential not found" }, @@ -170,7 +174,7 @@ export async function PUT( }; // Store updated credential - const storedCredential = await credentialStorage.storeCredentials(user.id, updatedCredential); + const storedCredential = await storage.storeCredentials(user.id, updatedCredential); // Log security audit await supabase.from("mt5_security_audit").insert({ @@ -256,10 +260,11 @@ export async function DELETE( ); } + const storage = getCredentialStorage(); const credentialId = params.id; // Get credential info before deletion for audit - const credential = await credentialStorage.getCredentials(user.id, credentialId); + const credential = await storage.getCredentials(user.id, credentialId); if (!credential) { return NextResponse.json( { error: "CREDENTIAL_NOT_FOUND", message: "Credential not found" }, @@ -268,7 +273,7 @@ export async function DELETE( } // Delete the credential - await credentialStorage.deleteCredentials(user.id, credentialId); + await storage.deleteCredentials(user.id, credentialId); // Log security audit await supabase.from("mt5_security_audit").insert({ diff --git a/src/app/api/mt5/credentials/route.ts b/src/app/api/mt5/credentials/route.ts index 3b69b1e4..68391dbb 100644 --- a/src/app/api/mt5/credentials/route.ts +++ b/src/app/api/mt5/credentials/route.ts @@ -2,10 +2,12 @@ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/authOptions"; -import { credentialStorage } from "@/lib/credential-storage"; +import { getCredentialStorage } from "@/lib/credential-storage"; import { MT5Credentials } from "@/types/mt5"; import { requireActiveTrialOrPaid } from "@/lib/trial"; +export const runtime = "nodejs"; + function asString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } @@ -54,7 +56,8 @@ export async function GET() { } // Get user's credentials - const credentials = await credentialStorage.getUserCredentials(user.id); + const storage = getCredentialStorage(); + const credentials = await storage.getUserCredentials(user.id); // Return credentials without sensitive data const safeCredentials = credentials.map(cred => ({ @@ -164,7 +167,8 @@ export async function POST(req: Request) { console.log('Credentials object before storeCredentials:', JSON.stringify(credentials, null, 2)); // Store credentials securely - const storedCredential = await credentialStorage.storeCredentials(user.id, credentials); + const storage = getCredentialStorage(); + const storedCredential = await storage.storeCredentials(user.id, credentials); // Log security audit await supabase.from("mt5_security_audit").insert({ diff --git a/src/app/api/mt5/sync/route.ts b/src/app/api/mt5/sync/route.ts index b8b220a3..754bf87d 100644 --- a/src/app/api/mt5/sync/route.ts +++ b/src/app/api/mt5/sync/route.ts @@ -2,10 +2,12 @@ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/authOptions"; -import { credentialStorage } from "@/lib/credential-storage"; +import { getCredentialStorage } from "@/lib/credential-storage"; import { fetchDeals } from "@/lib/mtapi"; import { requireActiveTrialOrPaid } from "@/lib/trial"; +export const runtime = "nodejs"; + type SyncBody = { credentialId?: string; from?: string; @@ -32,6 +34,7 @@ export async function POST(req: Request) { return NextResponse.json({ error: "UPGRADE_REQUIRED" }, { status: 403 }); } + const storage = getCredentialStorage(); const body: SyncBody = await req.json().catch(() => ({})); const { credentialId, from, to } = body; @@ -47,7 +50,7 @@ export async function POST(req: Request) { { status: 400 } ); } - const creds = await credentialStorage.getCredentials(userId, credentialId); + const creds = await storage.getCredentials(userId, credentialId); if (!creds) { return NextResponse.json( { error: "CREDENTIALS_NOT_FOUND", message: "MT5 credentials not found or inactive" }, diff --git a/src/app/api/user/trial-status/route.ts b/src/app/api/user/trial-status/route.ts index 775078fc..54d441f8 100644 --- a/src/app/api/user/trial-status/route.ts +++ b/src/app/api/user/trial-status/route.ts @@ -6,6 +6,10 @@ import { createClient } from "@/utils/supabase/server"; import { getTrialInfoByEmail } from "@/lib/trial"; import { sendTrialExpiredEmail } from "@/lib/mailer"; +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; +export const revalidate = 0; + export async function GET() { try { const session = await getServerSession(authOptions); @@ -44,6 +48,9 @@ export async function GET() { return NextResponse.json({ ok: true, info }); } catch (err) { + if (err && typeof err === "object" && "digest" in err && (err as { digest?: unknown }).digest === "DYNAMIC_SERVER_USAGE") { + throw err; + } console.error("trial-status error:", err); return NextResponse.json({ error: "INTERNAL_ERROR" }, { status: 500 }); } diff --git a/src/app/api/verify-email/route.ts b/src/app/api/verify-email/route.ts index f4443a8b..8e97cfa6 100644 --- a/src/app/api/verify-email/route.ts +++ b/src/app/api/verify-email/route.ts @@ -2,7 +2,9 @@ import { NextResponse } from "next/server"; import { createAdminClient } from "@/utils/supabase/admin"; import jwt from "jsonwebtoken"; -const JWT_SECRET = process.env.JWT_SECRET!; +export const runtime = "nodejs"; + +const JWT_SECRET = process.env.JWT_SECRET; export async function GET(req: Request) { const url = new URL(req.url); @@ -39,6 +41,11 @@ export async function GET(req: Request) { } // ✅ Re-issue JWT with updated email_verified + if (!JWT_SECRET) { + failUrl.searchParams.set("reason", "missing_secret"); + return NextResponse.redirect(failUrl); + } + const newToken = jwt.sign( { sub: user.id, diff --git a/src/components/mt5/CredentialManager.tsx b/src/components/mt5/CredentialManager.tsx index c2bc7377..03330433 100644 --- a/src/components/mt5/CredentialManager.tsx +++ b/src/components/mt5/CredentialManager.tsx @@ -3,7 +3,6 @@ import React, { useState, useEffect } from "react"; import { StoredCredential, MT5Credentials } from "@/types/mt5"; -import { credentialStorage } from "@/lib/credential-storage"; import { encryptionService } from "@/lib/encryption"; import { Plus, diff --git a/src/lib/connection-monitor.ts b/src/lib/connection-monitor.ts index 159c64d8..c6e1137e 100644 --- a/src/lib/connection-monitor.ts +++ b/src/lib/connection-monitor.ts @@ -1,6 +1,6 @@ // src/lib/connection-monitor.ts import { MT5Credentials, ConnectionStatus } from '@/types/mt5'; -import { credentialStorage } from '@/lib/credential-storage'; +import { getCredentialStorage } from '@/lib/credential-storage'; import { mt5ConnectionManager } from '@/lib/mt5-connection-manager'; import { createClient } from '@/utils/supabase/server'; @@ -68,7 +68,8 @@ export class ConnectionMonitor { this.monitoringConfigs.set(userId, monitoringConfig); // Load existing credentials - const credentials = await credentialStorage.getUserCredentials(userId); + const storage = getCredentialStorage(); + const credentials = await storage.getUserCredentials(userId); // Initialize health status for each credential for (const credential of credentials) { @@ -222,7 +223,8 @@ export class ConnectionMonitor { * Perform health check for all user credentials */ private async performHealthCheck(userId: string): Promise { - const credentials = await credentialStorage.getUserCredentials(userId); + const storage = getCredentialStorage(); + const credentials = await storage.getUserCredentials(userId); const config = this.monitoringConfigs.get(userId); if (!config) return; @@ -245,7 +247,8 @@ export class ConnectionMonitor { try { // Get credentials for health check - const credentials: MT5Credentials | null = await credentialStorage.getCredentials(userId, credentialId); + const storage = getCredentialStorage(); + const credentials: MT5Credentials | null = await storage.getCredentials(userId, credentialId); if (!credentials) { await this.updateHealthStatus(userId, credentialId, { status: 'error', diff --git a/src/lib/credential-storage.ts b/src/lib/credential-storage.ts index be75814c..a886bf36 100644 --- a/src/lib/credential-storage.ts +++ b/src/lib/credential-storage.ts @@ -388,4 +388,6 @@ export class CredentialStorageService { } // Export singleton instance -export const credentialStorage = CredentialStorageService.getInstance(); \ No newline at end of file +export function getCredentialStorage(): CredentialStorageService { + return CredentialStorageService.getInstance(); +} \ No newline at end of file