# Admin Trade Control - Web UI Implementation ## Overview This document describes the **frontend implementation** of the Admin Trade Control feature in the trading dashboard. The UI allows authorized administrators to pause and resume auto-trading with clear visual indicators. For backend implementation details, see the backend service documentation. --- ## UI Components ### 1. Header Status Badge (Global) **Location**: `src/App.tsx` (lines 195-231) **Visibility**: All pages, all users (read-only) **Features**: - Shows current trading state: PAUSED or RUNNING - Color-coded: Orange (paused) / Green (running) - Icon indicators: ⏸️ (paused) / ▶️ (running) - Tooltip with who paused and when - Updates in real-time via WebSocket **Implementation**: ```tsx {botState.health?.tradingControl && (
{botState.health.tradingControl.mode === 'PAUSED' ? '⏸️' : '▶️'}
{botState.health.tradingControl.mode === 'PAUSED' ? 'Trading Paused' : 'Trading Active'} {botState.health.tradingControl.mode === 'PAUSED' ? 'No new entries' : 'Entries allowed'}
)} ``` --- ### 2. Admin Tab Controls (Admin Only) **Location**: `src/tabs/AdminTab.tsx` (lines 295-380) **Visibility**: Admin users only (role = 'admin') **Features**: - Trading Control Panel section - Status banner showing current mode - Pause/Resume buttons - Error display - Safety notice - Loading states - Disabled states **State Management**: ```tsx const [isControlLoading, setIsControlLoading] = React.useState(false); const [controlError, setControlError] = React.useState(null); const tradingControl = botState.health?.tradingControl; const isPaused = tradingControl?.mode === 'PAUSED'; ``` **Pause Handler**: ```tsx const handlePauseTrading = async () => { setIsControlLoading(true); setControlError(null); try { const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5000'; const { data: sessionData } = await supabase.auth.getSession(); const accessToken = sessionData.session?.access_token; if (!accessToken) { throw new Error('Not authenticated'); } const res = await fetch(`${apiUrl}/internal/trading/pause`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Admin pause from dashboard' }) }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Unknown error' })); throw new Error(errorData.error || `HTTP ${res.status}`); } } catch (err: any) { setControlError(err.message || 'Failed to pause trading'); } finally { setIsControlLoading(false); } }; ``` **Resume Handler**: ```tsx const handleResumeTrading = async () => { setIsControlLoading(true); setControlError(null); try { const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5000'; const { data: sessionData } = await supabase.auth.getSession(); const accessToken = sessionData.session?.access_token; if (!accessToken) { throw new Error('Not authenticated'); } const res = await fetch(`${apiUrl}/internal/trading/resume`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Admin resume from dashboard' }) }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Unknown error' })); throw new Error(errorData.error || `HTTP ${res.status}`); } } catch (err: any) { setControlError(err.message || 'Failed to resume trading'); } finally { setIsControlLoading(false); } }; ``` **UI Rendering**: ```tsx {/* Status Banner */}
{isPaused ? ( ) : ( )}

AUTO-TRADING: {isPaused ? 'PAUSED' : 'RUNNING'}

{isPaused ? 'No new positions will be opened. Existing positions are still managed.' : 'Bot is actively monitoring and executing trades based on strategy rules.'}

{tradingControl && (

Last Changed

{new Date(tradingControl.lastChangedAt).toLocaleString()}

by {tradingControl.lastChangedBy}

)}
{/* Control Buttons */}
{/* Error Display */} {controlError && (

{controlError}

)} {/* Safety Notice */}

Safety Note: Pausing trading blocks new entries only. Existing positions will continue to be monitored for exits, stop-losses, and take-profits.

``` --- ### 3. Database Synchronization Control (Admin Only) **Location**: `src/tabs/AdminTab.tsx` (lines 409-480) **Features**: - **State Snapshots Toggle**: Enable/Disable all database writes. - **Sync Interval Slider**: Adjustable frequency (1m to 60m). - **Audit Logging**: Saves settings to `bot_config` table. **Implementation**: ```tsx const [dbSyncEnabled, setDbSyncEnabled] = React.useState(true); const [dbSyncInterval, setDbSyncInterval] = React.useState(300000); const handleUpdateDbSync = async () => { const updates = [ { key: 'ENABLE_DB_SNAPSHOTS', value: String(dbSyncEnabled) }, { key: 'DB_SNAPSHOT_INTERVAL_MS', value: String(dbSyncInterval) } ]; await supabase.from('bot_config').upsert(updates); }; ``` --- ### 4. WebSocket Integration **Location**: `src/hooks/useWebSocket.ts` **Purpose**: Receive real-time trading control state updates from backend **Implementation**: ```tsx export interface TradingControlSnapshot { mode: 'RUNNING' | 'PAUSED'; lastChangedBy: string; lastChangedAt: number; reason?: string; } export interface HealthSnapshot { tradingLoopHealthy: boolean; tradingLoopLastRun: number | null; // ... other health metrics tradingControl: TradingControlSnapshot; } // In useWebSocket hook: newSocket.on('health_update', (health: HealthSnapshot) => { setBotState(prev => ({ ...prev, health })); }); ``` **Data Flow**: 1. Admin clicks "Pause" button 2. Frontend calls `POST /internal/trading/pause` 3. Backend updates state and broadcasts `health_update` event 4. WebSocket receives event 5. `botState.health.tradingControl` updates 6. UI re-renders with new state --- ## UI States ### Button States | Scenario | Pause Button | Resume Button | |----------|--------------|---------------| | Trading is RUNNING | Enabled | Disabled | | Trading is PAUSED | Disabled | Enabled | | API call in progress | Disabled (shows "Pausing...") | Disabled (shows "Resuming...") | | API error | Re-enabled | Re-enabled | ### Visual Indicators | State | Header Badge | Admin Panel Banner | |-------|--------------|-------------------| | RUNNING | ▶️ Trading Active (green) | AUTO-TRADING: RUNNING (green) | | PAUSED | ⏸️ Trading Paused (orange) | AUTO-TRADING: PAUSED (orange) | --- ## Error Handling ### API Errors **Scenarios**: - Network failure - Authentication failure (401) - Authorization failure (403) - Server error (500) **Handling**: ```tsx try { const res = await fetch(/* ... */); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Unknown error' })); throw new Error(errorData.error || `HTTP ${res.status}`); } } catch (err: any) { setControlError(err.message || 'Failed to pause trading'); } ``` **Display**: - Error message shown in red alert box - User can retry by clicking button again - Error clears on next successful action ### WebSocket Disconnection **Behavior**: - Last-known state remains visible - Timestamp shows when state was last updated - Connection status indicator in header shows "Reconnecting..." - State updates when WebSocket reconnects --- ## TypeScript Types ```tsx // From useWebSocket.ts export interface TradingControlSnapshot { mode: 'RUNNING' | 'PAUSED'; lastChangedBy: string; lastChangedAt: number; reason?: string; } export interface HealthSnapshot { tradingLoopHealthy: boolean; tradingLoopLastRun: number | null; monitorLoopHealthy: boolean; monitorLoopLastRun: number | null; orderSyncHealthy: boolean; orderSyncLastRun: number | null; lockContentionCount: number; reconciliationLoopHealthy: boolean; reconciliationLoopLastRun: number | null; capitalInvariantViolations: number; tradingControl: TradingControlSnapshot; } export interface BotState { health: HealthSnapshot; // ... other state properties } ``` --- ## Styling ### Color Palette | State | Background | Border | Text | |-------|-----------|--------|------| | PAUSED | `rgba(255,149,0,0.15)` | `rgba(255,149,0,0.4)` | `#ff9500` | | RUNNING | `rgba(52,199,89,0.08)` | `rgba(52,199,89,0.3)` | `#34c759` | | ERROR | `rgba(255,59,48,0.06)` | `rgba(255,59,48,0.2)` | `#ff3b30` | | INFO | `rgba(10,132,255,0.04)` | `rgba(10,132,255,0.1)` | `#0a84ff` | ### Icons - Pause: `⏸️` or `` from lucide-react - Play/Resume: `▶️` or `` from lucide-react - Warning: `` from lucide-react --- ## Testing ### Component Tests **File**: `src/tabs/AdminTab.test.tsx` ```tsx describe('AdminTab - Trading Control', () => { test('should show running status when mode is RUNNING', () => { const mockBotState = { health: { tradingControl: { mode: 'RUNNING', lastChangedBy: 'system', lastChangedAt: Date.now() } } }; render(); expect(screen.getByText(/AUTO-TRADING: RUNNING/i)).toBeInTheDocument(); }); test('should disable pause button when already paused', () => { const pausedState = { health: { tradingControl: { mode: 'PAUSED', lastChangedBy: 'admin', lastChangedAt: Date.now() } } }; render(); const pauseButton = screen.getByText(/Pause Auto Trading/i); expect(pauseButton).toBeDisabled(); }); test('should show error when API call fails', async () => { global.fetch = jest.fn(() => Promise.resolve({ ok: false, json: () => Promise.resolve({ error: 'Unauthorized' }) }) ); render(); const pauseButton = screen.getByText(/Pause Auto Trading/i); fireEvent.click(pauseButton); await waitFor(() => { expect(screen.getByText(/Unauthorized/i)).toBeInTheDocument(); }); }); }); ``` --- ## User Guide ### For Admins **How to Pause Trading**: 1. Login as admin user 2. Navigate to **Admin** tab (🛡️ icon in header) 3. Scroll to **Trading Control** section 4. Click **"Pause Auto Trading"** button 5. Verify header badge shows "⏸️ Trading Paused" 6. Confirm status banner shows "AUTO-TRADING: PAUSED" **How to Resume Trading**: 1. Navigate to **Admin** tab 2. Click **"Resume Auto Trading"** button 3. Verify header badge shows "▶️ Trading Active" 4. Confirm status banner shows "AUTO-TRADING: RUNNING" **What Happens When Paused**: - ❌ No new trade entries will be placed - ✅ Existing positions continue to be managed - ✅ Exit orders still execute - ✅ Stop-loss monitoring continues - ✅ Take-profit monitoring continues --- ## Troubleshooting ### Issue: Pause button not working **Check**: 1. User has `role = 'admin'` in profile 2. Valid JWT token in request (check Network tab) 3. Backend API is running 4. Browser console for errors ### Issue: Status not updating in UI **Check**: 1. WebSocket connection active (header shows "Connected") 2. `health_update` events received (check browser console) 3. Refresh page to force sync 4. Check backend logs for broadcast ### Issue: Error message displayed **Common Errors**: - "Not authenticated" → Login again - "Unauthorized" → User is not admin - "Failed to fetch" → Backend API not running - "HTTP 500" → Check backend logs --- ## Files Modified 1. ✅ `src/App.tsx` - Added header status badge 2. ✅ `src/tabs/AdminTab.tsx` - Added Trading Control Panel 3. ✅ `src/hooks/useWebSocket.ts` - Already had health_update handler --- ## Related Documentation - **Backend Implementation**: See `bytelyst-trading-bot-service/docs/ADMIN_TRADE_CONTROL_IMPLEMENTATION.md` - **State Persistence**: See `bytelyst-trading-bot-service/docs/TRADING_CONTROL_PERSISTENCE.md` - **Test Plan**: See `bytelyst-trading-bot-service/docs/ADMIN_TRADE_CONTROL_TEST_PLAN.md` - **Architecture**: See `bytelyst-trading-bot-service/docs/ADMIN_TRADE_CONTROL_ARCHITECTURE.md`