refactor(ui): standardize operations table badges

This commit is contained in:
Saravana Achu Mac 2026-05-09 02:11:52 -07:00
parent c51544aa29
commit 3951767ab1
2 changed files with 85 additions and 94 deletions

View File

@ -7,7 +7,7 @@ import {
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle'; import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
import { fetchTradeHistory } from '../lib/tradeHistoryApi'; import { fetchTradeHistory } from '../lib/tradeHistoryApi';
import { fetchPositionsBootstrap } from '../lib/positionsApi'; import { fetchPositionsBootstrap } from '../lib/positionsApi';
import { Button, Input, Select } from '../components/ui/Primitives'; import { Badge, Button, Input, Select } from '../components/ui/Primitives';
interface TradeRecord { interface TradeRecord {
@ -53,6 +53,9 @@ interface HistoryTabProps {
botState?: any; botState?: any;
} }
const historySourceBadgeVariant = (source?: 'BOT' | 'MANUAL') => source === 'BOT' ? 'accent' : 'warning';
const historySideBadgeVariant = (side: string) => side === 'BUY' ? 'success' : side === 'SELL' ? 'danger' : 'neutral';
export interface HistoryDateBounds { export interface HistoryDateBounds {
from: number | null; from: number | null;
to: number | null; to: number | null;
@ -587,11 +590,11 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
className={`${isLoss ? 'bg-red-500/[0.06] border-l-2 border-l-red-500/50' : ''} hover:bg-white/[0.02] transition-colors`} className={`${isLoss ? 'bg-red-500/[0.06] border-l-2 border-l-red-500/50' : ''} hover:bg-white/[0.02] transition-colors`}
> >
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${t.source === 'BOT' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' : 'bg-orange-500/20 text-orange-400 border border-orange-500/20'}`}> <Badge variant={historySourceBadgeVariant(t.source)} size="sm">
{t.source === 'BOT' {t.source === 'BOT'
? (t.profile_id ? (profiles.find(p => p.id === t.profile_id)?.name || 'BOT') : 'BOT') ? (t.profile_id ? (profiles.find(p => p.id === t.profile_id)?.name || 'BOT') : 'BOT')
: 'MANUAL'} : 'MANUAL'}
</span> </Badge>
</td> </td>
<td className="px-6 py-4 text-[10px] text-gray-500 font-mono"> <td className="px-6 py-4 text-[10px] text-gray-500 font-mono">
{(t.timestamp || t.created_at) ? new Date(t.timestamp || t.created_at!).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'} {(t.timestamp || t.created_at) ? new Date(t.timestamp || t.created_at!).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
@ -604,9 +607,9 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
</td> </td>
<td className="px-6 py-4 font-mono font-bold text-white">{t.symbol}</td> <td className="px-6 py-4 font-mono font-bold text-white">{t.symbol}</td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`text-[10px] font-black ${t.side === 'BUY' ? 'text-green-400' : t.side === 'SELL' ? 'text-red-400' : 'text-gray-400'}`}> <Badge variant={historySideBadgeVariant(t.side)} size="sm">
{t.side} {t.side}
</span> </Badge>
</td> </td>
<td className="px-6 py-4 text-right text-xs font-mono text-cyan-300"> <td className="px-6 py-4 text-right text-xs font-mono text-cyan-300">
{capitalUsed !== null {capitalUsed !== null
@ -630,9 +633,9 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
${pnlValue.toFixed(2)} ${pnlValue.toFixed(2)}
</div> </div>
{isLoss && ( {isLoss && (
<div className="mt-1 inline-flex items-center px-1.5 py-0.5 rounded border border-red-500/40 bg-red-500/20 text-[8px] font-black uppercase tracking-wider text-red-200"> <Badge variant="danger" size="sm" className="mt-1 text-[10px]">
Loss Alert Loss Alert
</div> </Badge>
)} )}
</div> </div>
</td> </td>

View File

@ -8,7 +8,7 @@ import { createRequestId } from '../../../shared/request-id.js';
import { Layers, ListFilter, Link2, GitBranch, AlertTriangle, Lock, RefreshCw, CheckCircle, XCircle } from 'lucide-react'; import { Layers, ListFilter, Link2, GitBranch, AlertTriangle, Lock, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle'; import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
import { fetchPositionsBootstrap } from '../lib/positionsApi'; import { fetchPositionsBootstrap } from '../lib/positionsApi';
import { Button, Input, Select } from '../components/ui/Primitives'; import { Badge, Button, Input, Select } from '../components/ui/Primitives';
interface PositionsTabProps { interface PositionsTabProps {
botState: BotState; botState: BotState;
@ -274,7 +274,7 @@ export const normalizeOrder = (record: RawOrderRecord, fallbackSource?: OrderSou
}; };
}; };
const SourceTruthVariants: Record<'exchange' | 'db' | 'reconciled' | 'unknown', { label: string; className: string }> = { const SourceTruthVariants: Record<'exchange' | 'db' | 'reconciled' | 'unknown', { label: string; className: string }> = {
exchange: { label: 'Exchange', className: 'truth-pill exchange' }, exchange: { label: 'Exchange', className: 'truth-pill exchange' },
db: { label: 'DB', className: 'truth-pill db' }, db: { label: 'DB', className: 'truth-pill db' },
reconciled: { label: 'Reconciled', className: 'truth-pill reconciled' }, reconciled: { label: 'Reconciled', className: 'truth-pill reconciled' },
@ -293,6 +293,25 @@ const getTruthSourceForPosition = (entryOrder?: NormalizedOrder, canonicalAvaila
return SourceTruthVariants.reconciled; return SourceTruthVariants.reconciled;
}; };
const sourceBadgeVariant = (source: 'BOT' | 'MANUAL') => source === 'BOT' ? 'accent' : 'warning';
const sideBadgeVariant = (side: 'BUY' | 'SELL') => side === 'BUY' ? 'success' : 'danger';
const actionBadgeVariant = (action: OrderAction) => action === 'ENTRY' ? 'info' : 'warning';
const orderStatusBadgeVariant = (status: string, stale = false) => {
if (stale) return 'warning';
if (status === 'filled') return 'success';
if (status === 'expired') return 'warning';
if (status === 'unknown') return 'neutral';
if (status.includes('reject') || status.includes('fail') || status.includes('cancel')) return 'danger';
if (status.includes('pending') || status.includes('new')) return 'info';
return 'neutral';
};
const lifecycleStateBadgeVariant = (state: string) => {
if (state === 'CLOSED') return 'success';
if (state === 'PARTIAL_EXIT' || state === 'EXIT_PENDING') return 'warning';
if (state === 'ORPHAN_EXIT') return 'danger';
return 'info';
};
const getTruthSourceForOrder = (order: NormalizedOrder, historyKeys: Set<string>, canonicalAvailable: boolean = true) => { const getTruthSourceForOrder = (order: NormalizedOrder, historyKeys: Set<string>, canonicalAvailable: boolean = true) => {
const status = order.status || ''; const status = order.status || '';
if (status.includes('pending') || status.includes('new') || status.includes('accepted')) { if (status.includes('pending') || status.includes('new') || status.includes('accepted')) {
@ -1436,11 +1455,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
return ( return (
<tr key={pos.id} className="hover:bg-white/[0.02] transition-colors"> <tr key={pos.id} className="hover:bg-white/[0.02] transition-colors">
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${pos.source === 'BOT' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' : 'bg-orange-500/20 text-orange-400 border border-orange-500/20'}`}> <Badge variant={sourceBadgeVariant(pos.source)} size="sm">
{pos.source === 'BOT' ? (pos.profileName || profiles.find(pr => pr.id === pos.profileId)?.name || 'BOT') : 'MANUAL'} {pos.source === 'BOT' ? (pos.profileName || profiles.find(pr => pr.id === pos.profileId)?.name || 'BOT') : 'MANUAL'}
</span> </Badge>
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={positionTruth.className}>{positionTruth.label}</span> <span className={positionTruth.className}>{positionTruth.label}</span>
</td> </td>
@ -1458,16 +1477,12 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
</td> </td>
<td className="px-6 py-4 font-mono font-bold text-white">{pos.symbol}</td> <td className="px-6 py-4 font-mono font-bold text-white">{pos.symbol}</td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`text-[10px] font-black ${pos.side === 'BUY' ? 'text-green-400' : 'text-red-400'}`}>{pos.side}</span> <Badge variant={sideBadgeVariant(pos.side)} size="sm">{pos.side}</Badge>
{pos.planMode ? ( {pos.planMode ? (
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-black uppercase tracking-wider border ${ <Badge variant={pos.planMode === 'long_term' ? 'warning' : 'info'} size="sm" className="text-[10px]">
pos.planMode === 'long_term'
? 'bg-amber-500/10 text-amber-300 border-amber-500/20'
: 'bg-sky-500/10 text-sky-300 border-sky-500/20'
}`}>
{pos.planMode === 'long_term' ? 'Long-term hold' : 'Short-term managed'} {pos.planMode === 'long_term' ? 'Long-term hold' : 'Short-term managed'}
</span> </Badge>
</div> </div>
) : null} ) : null}
</td> </td>
@ -1479,14 +1494,14 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
{currentPrice !== null ? `$${currentPrice.toLocaleString()}` : '-'} {currentPrice !== null ? `$${currentPrice.toLocaleString()}` : '-'}
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
{slBreached && ( {slBreached && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-black uppercase tracking-wider bg-red-500/20 text-red-300 border border-red-500/20"> <Badge variant="danger" size="sm" className="text-[10px]">
SL breached SL breached
</span> </Badge>
)} )}
{!slBreached && tpHit && ( {!slBreached && tpHit && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-black uppercase tracking-wider bg-green-500/20 text-green-300 border border-green-500/20"> <Badge variant="success" size="sm" className="text-[10px]">
TP hit TP hit
</span> </Badge>
)} )}
</div> </div>
</td> </td>
@ -1704,9 +1719,9 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
)} )}
</td> </td>
<td className="px-4 py-4"> <td className="px-4 py-4">
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${order.source === 'BOT' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' : 'bg-orange-500/20 text-orange-400 border border-orange-500/20'}`}> <Badge variant={sourceBadgeVariant(order.source)} size="sm">
{order.profileId ? (profiles.find(pr => pr.id === order.profileId)?.name || 'BOT') : 'MANUAL'} {order.profileId ? (profiles.find(pr => pr.id === order.profileId)?.name || 'BOT') : 'MANUAL'}
</span> </Badge>
</td> </td>
<td className="px-4 py-4"> <td className="px-4 py-4">
<span className={truthSource.className}>{truthSource.label}</span> <span className={truthSource.className}>{truthSource.label}</span>
@ -1715,21 +1730,18 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
{order.timestamp ? new Date(order.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'} {order.timestamp ? new Date(order.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
</td> </td>
<td className="px-4 py-4 font-mono font-bold text-white text-xs">{order.symbol}</td> <td className="px-4 py-4 font-mono font-bold text-white text-xs">{order.symbol}</td>
<td className="px-4 py-4"> <td className="px-4 py-4">
{resolvedAction ? ( {resolvedAction ? (
<span className={`flex items-center gap-1 px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider w-fit ${isEntry <Badge variant={actionBadgeVariant(resolvedAction)} size="sm">
? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' {isEntry ? 'ENTRY' : 'EXIT'}
: 'bg-amber-500/10 text-amber-400 border border-amber-500/20' </Badge>
}`}> ) : (
{isEntry ? 'ENTRY' : 'EXIT'} <span className="text-[10px] text-gray-500 uppercase">{order.type}</span>
</span> )}
) : ( </td>
<span className="text-[10px] text-gray-500 uppercase">{order.type}</span> <td className="px-4 py-4">
)} <Badge variant={sideBadgeVariant(order.side)} size="sm">{order.side}</Badge>
</td> </td>
<td className="px-4 py-4">
<span className={`text-[10px] font-black ${order.side === 'BUY' ? 'text-green-400' : 'text-red-400'}`}>{order.side}</span>
</td>
<td className="px-4 py-4 text-xs font-mono text-gray-300">{Number(order.qty || 0).toFixed(4)}</td> <td className="px-4 py-4 text-xs font-mono text-gray-300">{Number(order.qty || 0).toFixed(4)}</td>
<td className="px-4 py-4 text-xs font-mono text-gray-400">${Number(order.price).toLocaleString()}</td> <td className="px-4 py-4 text-xs font-mono text-gray-400">${Number(order.price).toLocaleString()}</td>
<td className="px-4 py-4 text-right"> <td className="px-4 py-4 text-right">
@ -1746,30 +1758,21 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
const orderAge = order.timestamp ? Date.now() - order.timestamp : 0; const orderAge = order.timestamp ? Date.now() - order.timestamp : 0;
const isStale = isPendingNew && orderAge > 5 * 60 * 1000; const isStale = isPendingNew && orderAge > 5 * 60 * 1000;
let badgeClass = 'bg-white/10 text-gray-400'; let tooltip = '';
let tooltip = '';
if (isStale) {
if (order.status === 'filled') { tooltip = 'Order pending for >5 min - sync in progress';
badgeClass = 'bg-green-500/20 text-green-400 border border-green-500/20'; } else if (isExpired) {
} else if (isStale) { tooltip = 'Order not found on exchange - likely never executed';
badgeClass = 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/20'; } else if (isUnknown) {
tooltip = 'Order pending for >5 min - sync in progress'; tooltip = 'Order status could not be verified';
} else if (isExpired) { }
badgeClass = 'bg-orange-500/20 text-orange-400 border border-orange-500/20';
tooltip = 'Order not found on exchange - likely never executed'; return (
} else if (isUnknown) { <div className="flex items-center gap-1">
badgeClass = 'bg-gray-500/20 text-gray-400 border border-gray-500/20'; <Badge variant={orderStatusBadgeVariant(order.status, isStale)} size="sm" title={tooltip}>
tooltip = 'Order status could not be verified'; {order.status}
} </Badge>
return (
<div className="flex items-center gap-1">
<span
className={`px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-tighter ${badgeClass}`}
title={tooltip}
>
{order.status}
</span>
{isStale && ( {isStale && (
<span className="text-[8px] text-yellow-400" title="Order may be stale - sync in progress">!</span> <span className="text-[8px] text-yellow-400" title="Order may be stale - sync in progress">!</span>
)} )}
@ -1940,14 +1943,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
</span> </span>
</div> </div>
</td> </td>
<td className="px-4 py-4"> <td className="px-4 py-4">
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${trace.source === 'BOT' <Badge variant={sourceBadgeVariant(trace.source)} size="sm">
? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' {trace.profileName}
: 'bg-orange-500/20 text-orange-400 border border-orange-500/20' </Badge>
}`}> </td>
{trace.profileName}
</span>
</td>
<td className="px-4 py-4 text-xs font-mono font-bold text-white"> <td className="px-4 py-4 text-xs font-mono font-bold text-white">
{trace.symbol} {trace.symbol}
</td> </td>
@ -1955,14 +1955,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
<div className="space-y-1"> <div className="space-y-1">
{trace.orderedEvents.map((event) => { {trace.orderedEvents.map((event) => {
const actionLabel: OrderAction = event.action === 'EXIT' ? 'EXIT' : 'ENTRY'; const actionLabel: OrderAction = event.action === 'EXIT' ? 'EXIT' : 'ENTRY';
const actionClass = actionLabel === 'ENTRY' return (
? 'bg-blue-500/10 text-blue-300 border border-blue-500/20' <div key={`${trace.tradeId}-${event.id}-${event.timestamp}`} className="flex flex-wrap items-center gap-2 text-[9px]">
: 'bg-amber-500/10 text-amber-300 border border-amber-500/20'; <Badge variant={actionBadgeVariant(actionLabel)} size="sm">
return ( {actionLabel}
<div key={`${trace.tradeId}-${event.id}-${event.timestamp}`} className="flex flex-wrap items-center gap-2 text-[9px]"> </Badge>
<span className={`px-1.5 py-0.5 rounded font-bold ${actionClass}`}>
{actionLabel}
</span>
<span className="text-zinc-500 font-mono"> <span className="text-zinc-500 font-mono">
{event.timestamp ? new Date(event.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'} {event.timestamp ? new Date(event.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
</span> </span>
@ -1986,18 +1983,9 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
</td> </td>
<td className="px-4 py-4 text-right"> <td className="px-4 py-4 text-right">
<div className="space-y-1"> <div className="space-y-1">
<span className={`px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-wider ${trace.state === 'CLOSED' <Badge variant={lifecycleStateBadgeVariant(trace.state)} size="sm">
? 'bg-green-500/20 text-green-300 border border-green-500/20' {trace.state}
: trace.state === 'PARTIAL_EXIT' </Badge>
? 'bg-amber-500/20 text-amber-300 border border-amber-500/20'
: trace.state === 'ORPHAN_EXIT'
? 'bg-red-500/20 text-red-300 border border-red-500/20'
: trace.state === 'EXIT_PENDING'
? 'bg-yellow-500/20 text-yellow-300 border border-yellow-500/20'
: 'bg-blue-500/20 text-blue-300 border border-blue-500/20'
}`}>
{trace.state}
</span>
<div className="text-[9px] text-zinc-500 leading-tight text-right max-w-[240px] ml-auto"> <div className="text-[9px] text-zinc-500 leading-tight text-right max-w-[240px] ml-auto">
{trace.stateReason} {trace.stateReason}
</div> </div>