learning_ai_invt_trdg/web/ADMIN_TRADE_CONTROL_UI.md

16 KiB

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:

{botState.health?.tradingControl && (
  <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    padding: '0.4rem 0.8rem',
    background: botState.health.tradingControl.mode === 'PAUSED' 
      ? 'rgba(255,149,0,0.15)' 
      : 'rgba(52,199,89,0.08)',
    border: `1px solid ${botState.health.tradingControl.mode === 'PAUSED' 
      ? 'rgba(255,149,0,0.4)' 
      : 'rgba(52,199,89,0.3)'}`,
    borderRadius: '8px'
  }}
  title={botState.health.tradingControl.mode === 'PAUSED' 
    ? `Trading paused by ${botState.health.tradingControl.lastChangedBy}. No new entries will be placed.` 
    : 'Auto-trading is active'}
  >
    <span style={{ fontSize: '0.85rem' }}>
      {botState.health.tradingControl.mode === 'PAUSED' ? '⏸️' : '▶️'}
    </span>
    <div style={{ display: 'flex', flexDirection: 'column' }}>
      <span style={{
        fontSize: '0.75rem',
        fontWeight: '900',
        color: botState.health.tradingControl.mode === 'PAUSED' ? '#ff9500' : '#34c759',
        letterSpacing: '0.05em',
        textTransform: 'uppercase'
      }}>
        {botState.health.tradingControl.mode === 'PAUSED' ? 'Trading Paused' : 'Trading Active'}
      </span>
      <span style={{ fontSize: '10px', color: '#666', fontWeight: 'bold' }}>
        {botState.health.tradingControl.mode === 'PAUSED' ? 'No new entries' : 'Entries allowed'}
      </span>
    </div>
  </div>
)}

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:

const [isControlLoading, setIsControlLoading] = React.useState(false);
const [controlError, setControlError] = React.useState<string | null>(null);
const tradingControl = botState.health?.tradingControl;
const isPaused = tradingControl?.mode === 'PAUSED';

Pause Handler:

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:

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:

{/* Status Banner */}
<div className={`flex items-center justify-between px-4 py-3 rounded-xl border ${
    isPaused 
        ? 'bg-orange-500/[0.06] border-orange-500/20' 
        : 'bg-emerald-500/[0.06] border-emerald-500/20'
}`}>
    <div className="flex items-center gap-3">
        {isPaused ? (
            <Pause size={16} className="text-orange-400" />
        ) : (
            <Play size={16} className="text-emerald-400" />
        )}
        <div>
            <p className={`text-sm font-bold ${isPaused ? 'text-orange-300' : 'text-emerald-300'}`}>
                AUTO-TRADING: {isPaused ? 'PAUSED' : 'RUNNING'}
            </p>
            <p className="text-[10px] text-zinc-500 mt-0.5">
                {isPaused 
                    ? 'No new positions will be opened. Existing positions are still managed.' 
                    : 'Bot is actively monitoring and executing trades based on strategy rules.'}
            </p>
        </div>
    </div>
    {tradingControl && (
        <div className="text-right">
            <p className="text-[9px] text-zinc-500 uppercase tracking-wider">Last Changed</p>
            <p className="text-[10px] text-zinc-400 font-mono">
                {new Date(tradingControl.lastChangedAt).toLocaleString()}
            </p>
            <p className="text-[9px] text-zinc-600">by {tradingControl.lastChangedBy}</p>
        </div>
    )}
</div>

{/* Control Buttons */}
<div className="grid grid-cols-2 gap-3">
    <button
        onClick={handlePauseTrading}
        disabled={isPaused || isControlLoading}
        className={/* ... */}
        title={isPaused ? "Trading is already paused" : "Pause all new trade entries"}
    >
        <Pause size={16} />
        {isControlLoading && !isPaused ? 'Pausing...' : 'Pause Auto Trading'}
    </button>
    <button
        onClick={handleResumeTrading}
        disabled={!isPaused || isControlLoading}
        className={/* ... */}
        title={!isPaused ? "Trading is already running" : "Resume automated trade execution"}
    >
        <Play size={16} />
        {isControlLoading && isPaused ? 'Resuming...' : 'Resume Auto Trading'}
    </button>
</div>

{/* Error Display */}
{controlError && (
    <div className="flex items-center gap-2 px-4 py-3 bg-red-500/[0.06] border border-red-500/20 rounded-xl">
        <AlertTriangle size={14} className="text-red-400 shrink-0" />
        <p className="text-xs text-red-400">{controlError}</p>
    </div>
)}

{/* Safety Notice */}
<div className="flex items-start gap-2 px-4 py-3 bg-blue-500/[0.04] border border-blue-500/10 rounded-xl">
    <AlertTriangle size={12} className="text-blue-400 shrink-0 mt-0.5" />
    <p className="text-[10px] text-blue-400/80 leading-relaxed">
        <strong>Safety Note:</strong> Pausing trading blocks new entries only. 
        Existing positions will continue to be monitored for exits, stop-losses, and take-profits.
    </p>
</div>

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:

const [dbSyncEnabled, setDbSyncEnabled] = React.useState<boolean>(true);
const [dbSyncInterval, setDbSyncInterval] = React.useState<number>(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:

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:

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

// 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 <Pause /> from lucide-react
  • Play/Resume: ▶️ or <Play /> from lucide-react
  • Warning: <AlertTriangle /> from lucide-react

Testing

Component Tests

File: src/tabs/AdminTab.test.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(<AdminTab botState={mockBotState} />);
        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(<AdminTab botState={pausedState} />);
        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(<AdminTab botState={mockBotState} />);
        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

  • 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