# 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 */}
{isControlLoading && !isPaused ? 'Pausing...' : 'Pause Auto Trading'}
{isControlLoading && isPaused ? 'Resuming...' : 'Resume Auto Trading'}
{/* Error Display */}
{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`