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_configtable.
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:
- Admin clicks "Pause" button
- Frontend calls
POST /internal/trading/pause - Backend updates state and broadcasts
health_updateevent - WebSocket receives event
botState.health.tradingControlupdates- 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:
- Login as admin user
- Navigate to Admin tab (🛡️ icon in header)
- Scroll to Trading Control section
- Click "Pause Auto Trading" button
- Verify header badge shows "⏸️ Trading Paused"
- Confirm status banner shows "AUTO-TRADING: PAUSED"
How to Resume Trading:
- Navigate to Admin tab
- Click "Resume Auto Trading" button
- Verify header badge shows "▶️ Trading Active"
- 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:
- User has
role = 'admin'in profile - Valid JWT token in request (check Network tab)
- Backend API is running
- Browser console for errors
Issue: Status not updating in UI
Check:
- WebSocket connection active (header shows "Connected")
health_updateevents received (check browser console)- Refresh page to force sync
- 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
- ✅
src/App.tsx- Added header status badge - ✅
src/tabs/AdminTab.tsx- Added Trading Control Panel - ✅
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