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 { fetchTradeHistory } from '../lib/tradeHistoryApi';
import { fetchPositionsBootstrap } from '../lib/positionsApi';
import { Button, Input, Select } from '../components/ui/Primitives';
import { Badge, Button, Input, Select } from '../components/ui/Primitives';
interface TradeRecord {
@ -53,6 +53,9 @@ interface HistoryTabProps {
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 {
from: 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`}
>
<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.profile_id ? (profiles.find(p => p.id === t.profile_id)?.name || 'BOT') : 'BOT')
: 'MANUAL'}
</span>
</Badge>
</td>
<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' }) : '-'}
@ -604,9 +607,9 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
</td>
<td className="px-6 py-4 font-mono font-bold text-white">{t.symbol}</td>
<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}
</span>
</Badge>
</td>
<td className="px-6 py-4 text-right text-xs font-mono text-cyan-300">
{capitalUsed !== null
@ -630,9 +633,9 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
${pnlValue.toFixed(2)}
</div>
{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
</div>
</Badge>
)}
</div>
</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 { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
import { fetchPositionsBootstrap } from '../lib/positionsApi';
import { Button, Input, Select } from '../components/ui/Primitives';
import { Badge, Button, Input, Select } from '../components/ui/Primitives';
interface PositionsTabProps {
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' },
db: { label: 'DB', className: 'truth-pill db' },
reconciled: { label: 'Reconciled', className: 'truth-pill reconciled' },
@ -293,6 +293,25 @@ const getTruthSourceForPosition = (entryOrder?: NormalizedOrder, canonicalAvaila
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 status = order.status || '';
if (status.includes('pending') || status.includes('new') || status.includes('accepted')) {
@ -1436,11 +1455,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
return (
<tr key={pos.id} className="hover:bg-white/[0.02] transition-colors">
<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'}`}>
{pos.source === 'BOT' ? (pos.profileName || profiles.find(pr => pr.id === pos.profileId)?.name || 'BOT') : 'MANUAL'}
</span>
</td>
<td className="px-6 py-4">
<Badge variant={sourceBadgeVariant(pos.source)} size="sm">
{pos.source === 'BOT' ? (pos.profileName || profiles.find(pr => pr.id === pos.profileId)?.name || 'BOT') : 'MANUAL'}
</Badge>
</td>
<td className="px-6 py-4">
<span className={positionTruth.className}>{positionTruth.label}</span>
</td>
@ -1458,16 +1477,12 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
</td>
<td className="px-6 py-4 font-mono font-bold text-white">{pos.symbol}</td>
<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 ? (
<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 ${
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'
}`}>
<Badge variant={pos.planMode === 'long_term' ? 'warning' : 'info'} size="sm" className="text-[10px]">
{pos.planMode === 'long_term' ? 'Long-term hold' : 'Short-term managed'}
</span>
</Badge>
</div>
) : null}
</td>
@ -1479,14 +1494,14 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
{currentPrice !== null ? `$${currentPrice.toLocaleString()}` : '-'}
<div className="mt-1 flex flex-wrap gap-1">
{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
</span>
</Badge>
)}
{!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
</span>
</Badge>
)}
</div>
</td>
@ -1704,9 +1719,9 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
)}
</td>
<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'}
</span>
</Badge>
</td>
<td className="px-4 py-4">
<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' }) : '-'}
</td>
<td className="px-4 py-4 font-mono font-bold text-white text-xs">{order.symbol}</td>
<td className="px-4 py-4">
{resolvedAction ? (
<span className={`flex items-center gap-1 px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider w-fit ${isEntry
? 'bg-blue-500/10 text-blue-400 border border-blue-500/20'
: 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
}`}>
{isEntry ? 'ENTRY' : 'EXIT'}
</span>
) : (
<span className="text-[10px] text-gray-500 uppercase">{order.type}</span>
)}
</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">
{resolvedAction ? (
<Badge variant={actionBadgeVariant(resolvedAction)} size="sm">
{isEntry ? 'ENTRY' : 'EXIT'}
</Badge>
) : (
<span className="text-[10px] text-gray-500 uppercase">{order.type}</span>
)}
</td>
<td className="px-4 py-4">
<Badge variant={sideBadgeVariant(order.side)} size="sm">{order.side}</Badge>
</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-right">
@ -1746,30 +1758,21 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
const orderAge = order.timestamp ? Date.now() - order.timestamp : 0;
const isStale = isPendingNew && orderAge > 5 * 60 * 1000;
let badgeClass = 'bg-white/10 text-gray-400';
let tooltip = '';
if (order.status === 'filled') {
badgeClass = 'bg-green-500/20 text-green-400 border border-green-500/20';
} else if (isStale) {
badgeClass = 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/20';
tooltip = 'Order pending for >5 min - sync in progress';
} 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';
} else if (isUnknown) {
badgeClass = 'bg-gray-500/20 text-gray-400 border border-gray-500/20';
tooltip = 'Order status could not be verified';
}
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>
let tooltip = '';
if (isStale) {
tooltip = 'Order pending for >5 min - sync in progress';
} else if (isExpired) {
tooltip = 'Order not found on exchange - likely never executed';
} else if (isUnknown) {
tooltip = 'Order status could not be verified';
}
return (
<div className="flex items-center gap-1">
<Badge variant={orderStatusBadgeVariant(order.status, isStale)} size="sm" title={tooltip}>
{order.status}
</Badge>
{isStale && (
<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>
</div>
</td>
<td className="px-4 py-4">
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${trace.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'
}`}>
{trace.profileName}
</span>
</td>
<td className="px-4 py-4">
<Badge variant={sourceBadgeVariant(trace.source)} size="sm">
{trace.profileName}
</Badge>
</td>
<td className="px-4 py-4 text-xs font-mono font-bold text-white">
{trace.symbol}
</td>
@ -1955,14 +1955,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
<div className="space-y-1">
{trace.orderedEvents.map((event) => {
const actionLabel: OrderAction = event.action === 'EXIT' ? 'EXIT' : 'ENTRY';
const actionClass = actionLabel === 'ENTRY'
? 'bg-blue-500/10 text-blue-300 border border-blue-500/20'
: 'bg-amber-500/10 text-amber-300 border border-amber-500/20';
return (
<div key={`${trace.tradeId}-${event.id}-${event.timestamp}`} className="flex flex-wrap items-center gap-2 text-[9px]">
<span className={`px-1.5 py-0.5 rounded font-bold ${actionClass}`}>
{actionLabel}
</span>
return (
<div key={`${trace.tradeId}-${event.id}-${event.timestamp}`} className="flex flex-wrap items-center gap-2 text-[9px]">
<Badge variant={actionBadgeVariant(actionLabel)} size="sm">
{actionLabel}
</Badge>
<span className="text-zinc-500 font-mono">
{event.timestamp ? new Date(event.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
</span>
@ -1986,18 +1983,9 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
</td>
<td className="px-4 py-4 text-right">
<div className="space-y-1">
<span className={`px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-wider ${trace.state === 'CLOSED'
? 'bg-green-500/20 text-green-300 border border-green-500/20'
: trace.state === 'PARTIAL_EXIT'
? '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>
<Badge variant={lifecycleStateBadgeVariant(trace.state)} size="sm">
{trace.state}
</Badge>
<div className="text-[9px] text-zinc-500 leading-tight text-right max-w-[240px] ml-auto">
{trace.stateReason}
</div>