refactor(web): normalize reconciliation audit theme surfaces

This commit is contained in:
Saravana Achu Mac 2026-05-05 22:06:05 -07:00
parent 5d0f138cd1
commit b8864ea276
3 changed files with 270 additions and 115 deletions

View File

@ -97,7 +97,7 @@ Current public bundle:
- [x] Normalize `web/src/components/TradeProfileManager.tsx`
- [x] Normalize `web/src/components/StrategyWizard.tsx`
- [ ] Normalize `web/src/tabs/ReconciliationAuditPanel.tsx`
- [x] Normalize `web/src/tabs/ReconciliationAuditPanel.tsx`
- [ ] Normalize `web/src/components/GlobalConfigManager.tsx`
- [ ] Normalize `web/src/components/EntryForm.tsx`
- [ ] Normalize remaining old admin/config surfaces

View File

@ -0,0 +1,136 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ReconciliationAuditPanel } from './ReconciliationAuditPanel';
const { getPlatformAccessTokenMock } = vi.hoisted(() => ({
getPlatformAccessTokenMock: vi.fn()
}));
vi.mock('../lib/authSession', () => ({
getPlatformAccessToken: getPlatformAccessTokenMock
}));
vi.mock('../lib/runtime', () => ({
tradingRuntime: { tradingApiUrl: 'https://trading.test' }
}));
const jsonResponse = (payload: unknown, ok = true, status = 200) => ({
ok,
status,
json: vi.fn(async () => payload)
}) as unknown as Response;
const auditRows = [
{
id: 1,
batch_id: 'batch-applied-123456',
profile_id: 'profile-123456789',
symbol: 'SOL/USDT',
trade_id: 'trade-1',
backfill_order_id: 'bfill-1',
exchange_order_id: 'exchange-1',
filled_qty: 2,
filled_price: 101.5,
dry_run: false,
decision: 'APPLIED',
reason: 'matched_exchange_fill',
metadata: { matchedBy: 'clientOrderId', exchangeSubTag: 'client-sub-tag-123456789' },
created_at: '2026-05-05T12:00:00.000Z'
}
];
const manualRows = [
{
id: 2,
batch_id: 'batch-review-123456',
profile_id: 'profile-review-123',
symbol: 'BTC/USDT',
trade_id: 'trade-review',
dry_run: true,
decision: 'NO_GO',
reason: 'missing_fill_evidence_for_large_remainder',
metadata: {
remaining: 1.25,
openQty: 2.5,
dustThreshold: 0.01,
evidenceRows: 3,
evidenceRowsUsed: 1,
unmatchedEvidenceCount: 2
},
created_at: '2026-05-05T12:30:00.000Z'
}
];
const batches = [
{
batchId: 'batch-applied-123456',
firstSeenAt: '2026-05-05T12:00:00.000Z',
lastSeenAt: '2026-05-05T12:10:00.000Z',
profileIds: ['profile-123456789'],
symbols: ['SOL/USDT'],
totalRows: 4,
byDecision: { APPLIED: 3, NO_GO: 1 },
dryRunRows: 0,
appliedRows: 3,
revertedRows: 0
}
];
const mockFetchPayloads = () => {
const fetchMock = vi.fn()
.mockResolvedValueOnce(jsonResponse({
success: true,
rows: auditRows,
pagination: { totalCount: 1, hasMore: false, limit: 50, offset: 0 }
}))
.mockResolvedValueOnce(jsonResponse({ success: true, batches }))
.mockResolvedValueOnce(jsonResponse({
success: true,
rows: manualRows,
pagination: { totalCount: 1, hasMore: false, limit: 200, offset: 0 }
}))
.mockResolvedValue(jsonResponse({
success: true,
rows: [],
batches: [],
pagination: { totalCount: 0, hasMore: false, limit: 50, offset: 0 }
}));
vi.stubGlobal('fetch', fetchMock);
return fetchMock;
};
describe('ReconciliationAuditPanel', () => {
beforeEach(() => {
getPlatformAccessTokenMock.mockReset();
getPlatformAccessTokenMock.mockResolvedValue('token-abc');
});
it('renders audit summaries, filters, manual review rows, and revert action', async () => {
const user = userEvent.setup();
const fetchMock = mockFetchPayloads();
vi.stubGlobal('confirm', vi.fn(() => false));
render(<ReconciliationAuditPanel />);
await waitFor(() => {
expect(screen.getByText('Reconciliation EXIT Backfill Audit')).toBeInTheDocument();
expect(screen.getByText('Manual Review Queue')).toBeInTheDocument();
expect(screen.getAllByText('SOL/USDT').length).toBeGreaterThan(0);
expect(screen.getByText('BTC/USDT')).toBeInTheDocument();
});
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('1 large-remainder case')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Revert Batch/i })).toBeInTheDocument();
await user.type(screen.getByPlaceholderText('Symbol (e.g. SOL/USDT)'), 'eth/usdt');
await user.click(screen.getByRole('button', { name: /Apply Filters/i }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledTimes(6);
});
expect(String(fetchMock.mock.calls[3][0])).toContain('symbol=ETH%2FUSDT');
});
});

View File

@ -3,6 +3,11 @@ import { AlertTriangle, Clock3, RefreshCcw, Search, ShieldCheck, Undo2 } from 'l
import { tradingRuntime } from '../lib/runtime';
import { getPlatformAccessToken } from '../lib/authSession';
import { createRequestId } from '../../../shared/request-id.js';
import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card';
import { Input } from '../components/ui/input';
import { Select } from '../components/ui/select';
import { cn } from '../lib/utils';
interface ReconciliationBackfillAuditRow {
id: number;
@ -77,6 +82,12 @@ const DEFAULT_FILTERS: ReconciliationFilters = {
};
const DEFAULT_DECISIONS = ['APPLIED', 'DRY_RUN', 'NO_GO', 'SKIP_EXISTING', 'PENDING_APPLY'];
const DAY_OPTIONS = [1, 3, 7, 14, 30];
const sectionTitleClass = 'text-xs font-bold uppercase tracking-wider text-[var(--foreground)]';
const mutedTextClass = 'text-[var(--muted-foreground)]';
const tableHeadClass = 'bg-[var(--muted)]/45 text-[var(--muted-foreground)] uppercase tracking-wider text-[10px]';
const tableDividerClass = 'divide-y divide-[var(--border)]';
const emptyCellClass = 'px-4 py-8 text-center text-[var(--muted-foreground)]';
const monoMutedClass = 'font-mono text-[var(--muted-foreground)]';
const parseResponseError = async (response: Response): Promise<string> => {
const payload = (await response.json().catch(() => null)) as { error?: string; message?: string } | null;
@ -316,131 +327,133 @@ export const ReconciliationAuditPanel = () => {
return (
<div className="space-y-6">
<section className="bg-[#12131a] border border-white/[0.04] rounded-2xl p-5 space-y-4">
<Card className="space-y-4 rounded-2xl p-5">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<ShieldCheck size={14} className="text-cyan-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Reconciliation EXIT Backfill Audit</h3>
<ShieldCheck size={14} className="text-[var(--accent)]" />
<h3 className={sectionTitleClass}>Reconciliation EXIT Backfill Audit</h3>
</div>
<button
<Button
onClick={() => { void loadAuditData(); }}
disabled={isLoading}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider border transition-colors ${isLoading
? 'bg-zinc-800 border-zinc-700 text-zinc-500 cursor-not-allowed'
: 'bg-cyan-500/10 border-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20'
}`}
variant="outline"
size="sm"
className="text-[10px] uppercase tracking-wider"
>
<RefreshCcw size={11} className={isLoading ? 'animate-spin' : ''} />
Refresh
</button>
</Button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.04]">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Rows (Filter Scope)</p>
<p className="text-sm font-mono text-white mt-1">{totalCount}</p>
<div className="rounded-lg border border-[var(--border)] bg-[var(--card-elevated)] px-3 py-2">
<p className="text-[9px] uppercase tracking-wider text-[var(--muted-foreground)]">Rows (Filter Scope)</p>
<p className="mt-1 font-mono text-sm text-[var(--foreground)]">{totalCount}</p>
</div>
<div className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.04]">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Batches (Latest)</p>
<p className="text-sm font-mono text-white mt-1">{batches.length}</p>
<div className="rounded-lg border border-[var(--border)] bg-[var(--card-elevated)] px-3 py-2">
<p className="text-[9px] uppercase tracking-wider text-[var(--muted-foreground)]">Batches (Latest)</p>
<p className="mt-1 font-mono text-sm text-[var(--foreground)]">{batches.length}</p>
</div>
<div className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.04]">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">Applied</p>
<p className="text-sm font-mono text-emerald-400 mt-1">{aggregateDecisionCounts.APPLIED || 0}</p>
<div className="rounded-lg border border-[var(--border)] bg-[var(--card-elevated)] px-3 py-2">
<p className="text-[9px] uppercase tracking-wider text-[var(--muted-foreground)]">Applied</p>
<p className="mt-1 font-mono text-sm text-emerald-600 dark:text-emerald-400">{aggregateDecisionCounts.APPLIED || 0}</p>
</div>
<div className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.04]">
<p className="text-[9px] text-zinc-500 uppercase tracking-wider">No-Go</p>
<p className="text-sm font-mono text-orange-400 mt-1">{aggregateDecisionCounts.NO_GO || 0}</p>
<div className="rounded-lg border border-[var(--border)] bg-[var(--card-elevated)] px-3 py-2">
<p className="text-[9px] uppercase tracking-wider text-[var(--muted-foreground)]">No-Go</p>
<p className="mt-1 font-mono text-sm text-orange-600 dark:text-orange-400">{aggregateDecisionCounts.NO_GO || 0}</p>
</div>
</div>
<p className="text-[10px] text-zinc-500 flex items-center gap-1.5">
<p className="flex items-center gap-1.5 text-[10px] text-[var(--muted-foreground)]">
<Clock3 size={11} />
Last refresh: {lastLoadedAt ? new Date(lastLoadedAt).toLocaleString() : 'Not loaded yet'}
</p>
</section>
</Card>
<section className="bg-[#12131a] border border-white/[0.04] rounded-2xl p-5 space-y-4">
<Card className="space-y-4 rounded-2xl p-5">
<div className="flex items-center gap-2">
<Search size={13} className="text-zinc-400" />
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Filters</h3>
<Search size={13} className="text-[var(--muted-foreground)]" />
<h3 className={sectionTitleClass}>Filters</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-3">
<input
<Input
value={draftFilters.profileId}
onChange={(event) => setDraftFilters((prev) => ({ ...prev, profileId: event.target.value }))}
placeholder="Profile ID"
className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.06] text-xs text-white placeholder:text-zinc-600 focus:outline-none focus:border-cyan-500/40"
className="h-10 rounded-lg px-3 py-2 text-xs"
/>
<input
<Input
value={draftFilters.symbol}
onChange={(event) => setDraftFilters((prev) => ({ ...prev, symbol: event.target.value }))}
placeholder="Symbol (e.g. SOL/USDT)"
className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.06] text-xs text-white placeholder:text-zinc-600 focus:outline-none focus:border-cyan-500/40"
className="h-10 rounded-lg px-3 py-2 text-xs"
/>
<input
<Input
value={draftFilters.batchId}
onChange={(event) => setDraftFilters((prev) => ({ ...prev, batchId: event.target.value }))}
placeholder="Batch ID"
className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.06] text-xs text-white placeholder:text-zinc-600 focus:outline-none focus:border-cyan-500/40"
className="h-10 rounded-lg px-3 py-2 text-xs"
/>
<select
<Select
value={draftFilters.decision}
onChange={(event) => setDraftFilters((prev) => ({ ...prev, decision: event.target.value }))}
className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.06] text-xs text-white focus:outline-none focus:border-cyan-500/40"
className="h-10 rounded-lg px-3 py-2 text-xs"
>
<option value="">All Decisions</option>
{decisionOptions.map((decision) => (
<option key={decision} value={decision}>{decision}</option>
))}
</select>
<select
</Select>
<Select
value={String(draftFilters.days)}
onChange={(event) => setDraftFilters((prev) => ({ ...prev, days: Number.parseInt(event.target.value, 10) || 7 }))}
className="px-3 py-2 rounded-lg bg-[#0e0f14] border border-white/[0.06] text-xs text-white focus:outline-none focus:border-cyan-500/40"
className="h-10 rounded-lg px-3 py-2 text-xs"
>
{DAY_OPTIONS.map((days) => (
<option key={days} value={String(days)}>{days} day{days === 1 ? '' : 's'}</option>
))}
</select>
</Select>
</div>
<div className="flex gap-2">
<button
<Button
onClick={handleApplyFilters}
disabled={isLoading}
className="px-3 py-2 rounded-lg bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 text-[10px] font-bold uppercase tracking-wider hover:bg-cyan-500/20 disabled:opacity-50"
size="sm"
className="text-[10px] uppercase tracking-wider"
>
Apply Filters
</button>
<button
</Button>
<Button
onClick={handleResetFilters}
disabled={isLoading}
className="px-3 py-2 rounded-lg bg-zinc-800 border border-zinc-700 text-zinc-300 text-[10px] font-bold uppercase tracking-wider hover:bg-zinc-700 disabled:opacity-50"
variant="outline"
size="sm"
className="text-[10px] uppercase tracking-wider"
>
Reset
</button>
</Button>
</div>
</section>
</Card>
{error && (
<div className="flex items-start gap-2 p-4 rounded-xl bg-red-500/[0.06] border border-red-500/20">
<AlertTriangle size={14} className="text-red-400 shrink-0 mt-0.5" />
<p className="text-xs text-red-400">{error}</p>
<AlertTriangle size={14} className="mt-0.5 shrink-0 text-red-600 dark:text-red-400" />
<p className="text-xs text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<section className="bg-[#12131a] border border-white/[0.04] rounded-2xl overflow-hidden">
<div className="px-5 py-3 border-b border-white/[0.04] flex items-center justify-between">
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Manual Review Queue</h3>
<span className="text-[10px] text-orange-400 font-mono">
<Card className="overflow-hidden rounded-2xl">
<div className="flex items-center justify-between border-b border-[var(--border)] px-5 py-3">
<h3 className={sectionTitleClass}>Manual Review Queue</h3>
<span className="font-mono text-[10px] text-orange-600 dark:text-orange-400">
{manualReviewRows.length} large-remainder case{manualReviewRows.length === 1 ? '' : 's'}
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead className="bg-[#0e0f14] text-zinc-500 uppercase tracking-wider text-[10px]">
<thead className={tableHeadClass}>
<tr>
<th className="px-4 py-2">Time</th>
<th className="px-4 py-2">Profile</th>
@ -453,10 +466,10 @@ export const ReconciliationAuditPanel = () => {
<th className="px-4 py-2">Batch</th>
</tr>
</thead>
<tbody className="divide-y divide-white/[0.03]">
<tbody className={tableDividerClass}>
{manualReviewRows.length === 0 ? (
<tr>
<td colSpan={9} className="px-4 py-8 text-center text-zinc-600">
<td colSpan={9} className={emptyCellClass}>
{isLoading ? 'Loading manual review queue...' : 'No large remainder manual-review cases for selected filters'}
</td>
</tr>
@ -471,19 +484,19 @@ export const ReconciliationAuditPanel = () => {
const unmatchedEvidenceCount = readMetadataNumber(metadata, 'unmatchedEvidenceCount');
return (
<tr key={`manual-${row.id}`} className="hover:bg-white/[0.01]">
<td className="px-4 py-2 text-zinc-400 font-mono">{formatDateTime(row.created_at)}</td>
<td className="px-4 py-2 text-zinc-300 font-mono">{shortId(row.profile_id)}</td>
<td className="px-4 py-2 text-zinc-300">{row.symbol || '-'}</td>
<td className="px-4 py-2 text-zinc-400 font-mono">{row.trade_id || '-'}</td>
<td className="px-4 py-2 text-orange-300 font-mono">{formatNumber(remaining)}</td>
<td className="px-4 py-2 text-zinc-400 font-mono">{formatNumber(openQty)}</td>
<td className="px-4 py-2 text-zinc-400 font-mono">{formatNumber(dustThreshold)}</td>
<td className="px-4 py-2 text-zinc-400 font-mono">
<tr key={`manual-${row.id}`} className="hover:bg-[var(--muted)]/20">
<td className={cn('px-4 py-2', monoMutedClass)}>{formatDateTime(row.created_at)}</td>
<td className="px-4 py-2 font-mono text-[var(--foreground)]">{shortId(row.profile_id)}</td>
<td className="px-4 py-2 text-[var(--foreground)]">{row.symbol || '-'}</td>
<td className={cn('px-4 py-2', monoMutedClass)}>{row.trade_id || '-'}</td>
<td className="px-4 py-2 font-mono text-orange-700 dark:text-orange-300">{formatNumber(remaining)}</td>
<td className={cn('px-4 py-2', monoMutedClass)}>{formatNumber(openQty)}</td>
<td className={cn('px-4 py-2', monoMutedClass)}>{formatNumber(dustThreshold)}</td>
<td className={cn('px-4 py-2', monoMutedClass)}>
{formatNumber(evidenceRowsUsed)}/{formatNumber(evidenceRows)}
<span className="text-[10px] text-zinc-600 ml-1">unmatched:{formatNumber(unmatchedEvidenceCount)}</span>
<span className="ml-1 text-[10px] text-[var(--muted-foreground)]">unmatched:{formatNumber(unmatchedEvidenceCount)}</span>
</td>
<td className="px-4 py-2 text-zinc-500 font-mono">{shortId(row.batch_id || '')}</td>
<td className={cn('px-4 py-2', monoMutedClass)}>{shortId(row.batch_id || '')}</td>
</tr>
);
})
@ -491,15 +504,15 @@ export const ReconciliationAuditPanel = () => {
</tbody>
</table>
</div>
</section>
</Card>
<section className="bg-[#12131a] border border-white/[0.04] rounded-2xl overflow-hidden">
<div className="px-5 py-3 border-b border-white/[0.04]">
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Batch Summary</h3>
<Card className="overflow-hidden rounded-2xl">
<div className="border-b border-[var(--border)] px-5 py-3">
<h3 className={sectionTitleClass}>Batch Summary</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead className="bg-[#0e0f14] text-zinc-500 uppercase tracking-wider text-[10px]">
<thead className={tableHeadClass}>
<tr>
<th className="px-4 py-2">Batch</th>
<th className="px-4 py-2">Window</th>
@ -510,47 +523,49 @@ export const ReconciliationAuditPanel = () => {
<th className="px-4 py-2 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-white/[0.03]">
<tbody className={tableDividerClass}>
{batches.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-zinc-600">
<td colSpan={7} className={emptyCellClass}>
{isLoading ? 'Loading batch summaries...' : 'No backfill batches found for selected filters'}
</td>
</tr>
) : (
batches.map((batch) => (
<tr key={batch.batchId} className="hover:bg-white/[0.01]">
<td className="px-4 py-2 font-mono text-zinc-300">{shortId(batch.batchId)}</td>
<td className="px-4 py-2 text-zinc-400">
<tr key={batch.batchId} className="hover:bg-[var(--muted)]/20">
<td className="px-4 py-2 font-mono text-[var(--foreground)]">{shortId(batch.batchId)}</td>
<td className={cn('px-4 py-2', mutedTextClass)}>
<div>{formatDateTime(batch.firstSeenAt)}</div>
<div className="text-[10px] text-zinc-600">to {formatDateTime(batch.lastSeenAt)}</div>
<div className="text-[10px] text-[var(--muted-foreground)]">to {formatDateTime(batch.lastSeenAt)}</div>
</td>
<td className="px-4 py-2 text-white font-mono">{batch.totalRows}</td>
<td className="px-4 py-2 font-mono text-[var(--foreground)]">{batch.totalRows}</td>
<td className="px-4 py-2">
<div className="flex flex-wrap gap-1">
{Object.entries(batch.byDecision || {}).map(([decision, count]) => (
<span
key={`${batch.batchId}-${decision}`}
className="px-1.5 py-0.5 rounded bg-white/[0.04] text-zinc-300 text-[10px] font-mono"
className="rounded border border-[var(--border)] bg-[var(--muted)] px-1.5 py-0.5 font-mono text-[10px] text-[var(--foreground)]"
>
{decision}:{count}
</span>
))}
</div>
</td>
<td className="px-4 py-2 text-zinc-400">{batch.profileIds.map(shortId).join(', ') || '-'}</td>
<td className="px-4 py-2 text-zinc-400">{batch.symbols.join(', ') || '-'}</td>
<td className={cn('px-4 py-2', mutedTextClass)}>{batch.profileIds.map(shortId).join(', ') || '-'}</td>
<td className={cn('px-4 py-2', mutedTextClass)}>{batch.symbols.join(', ') || '-'}</td>
<td className="px-4 py-2 text-right">
{batch.appliedRows > 0 && (
<button
<Button
onClick={() => handleRevertBatch(batch.batchId)}
disabled={isReverting === batch.batchId || isLoading}
title="Revert APPLIED BFILL rows in this batch via status-only rollback"
className="inline-flex items-center gap-1.5 px-2 py-1 rounded bg-red-500/10 border border-red-500/20 text-red-400 text-[10px] font-bold uppercase hover:bg-red-500/20 disabled:opacity-50"
variant="destructive"
size="sm"
className="h-7 rounded px-2 text-[10px] uppercase"
>
<Undo2 size={10} className={isReverting === batch.batchId ? 'animate-spin' : ''} />
Revert Batch
</button>
</Button>
)}
</td>
</tr>
@ -559,18 +574,18 @@ export const ReconciliationAuditPanel = () => {
</tbody>
</table>
</div>
</section>
</Card>
<section className="bg-[#12131a] border border-white/[0.04] rounded-2xl overflow-hidden">
<div className="px-5 py-3 border-b border-white/[0.04] flex items-center justify-between">
<h3 className="text-xs font-bold text-zinc-300 uppercase tracking-wider">Audit Rows</h3>
<span className="text-[10px] text-zinc-500 font-mono">
<Card className="overflow-hidden rounded-2xl">
<div className="flex items-center justify-between border-b border-[var(--border)] px-5 py-3">
<h3 className={sectionTitleClass}>Audit Rows</h3>
<span className="font-mono text-[10px] text-[var(--muted-foreground)]">
Showing {pageStart}-{pageEnd} of {totalCount}
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead className="bg-[#0e0f14] text-zinc-500 uppercase tracking-wider text-[10px]">
<thead className={tableHeadClass}>
<tr>
<th className="px-4 py-2">Time</th>
<th className="px-4 py-2">Decision</th>
@ -583,10 +598,10 @@ export const ReconciliationAuditPanel = () => {
<th className="px-4 py-2">Order IDs</th>
</tr>
</thead>
<tbody className="divide-y divide-white/[0.03]">
<tbody className={tableDividerClass}>
{rows.length === 0 ? (
<tr>
<td colSpan={9} className="px-4 py-10 text-center text-zinc-600">
<td colSpan={9} className="px-4 py-10 text-center text-[var(--muted-foreground)]">
{isLoading ? 'Loading audit rows...' : 'No audit rows for selected filters'}
</td>
</tr>
@ -598,35 +613,35 @@ export const ReconciliationAuditPanel = () => {
? metadata.exchangeSubTag
: '';
return (
<tr key={row.id} className="hover:bg-white/[0.01]">
<td className="px-4 py-2 text-zinc-400 font-mono">{formatDateTime(row.created_at)}</td>
<tr key={row.id} className="hover:bg-[var(--muted)]/20">
<td className={cn('px-4 py-2', monoMutedClass)}>{formatDateTime(row.created_at)}</td>
<td className="px-4 py-2">
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${row.decision === 'APPLIED'
? 'bg-emerald-500/10 text-emerald-400'
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: row.decision === 'NO_GO'
? 'bg-orange-500/10 text-orange-400'
: 'bg-zinc-700/40 text-zinc-300'
? 'bg-orange-500/10 text-orange-600 dark:text-orange-400'
: 'bg-[var(--muted)] text-[var(--foreground)]'
}`}
>
{row.decision}
</span>
{row.dry_run && <span className="ml-1 text-[10px] text-zinc-500">DRY</span>}
{row.dry_run && <span className="ml-1 text-[10px] text-[var(--muted-foreground)]">DRY</span>}
</td>
<td className="px-4 py-2 text-zinc-300 font-mono">{shortId(row.profile_id)}</td>
<td className="px-4 py-2 text-zinc-300">{row.symbol || '-'}</td>
<td className="px-4 py-2 text-zinc-400 font-mono">{row.trade_id || '-'}</td>
<td className="px-4 py-2 text-zinc-400 font-mono">
<td className="px-4 py-2 font-mono text-[var(--foreground)]">{shortId(row.profile_id)}</td>
<td className="px-4 py-2 text-[var(--foreground)]">{row.symbol || '-'}</td>
<td className={cn('px-4 py-2', monoMutedClass)}>{row.trade_id || '-'}</td>
<td className={cn('px-4 py-2', monoMutedClass)}>
<div>{formatNumber(row.filled_qty)}</div>
<div className="text-[10px] text-zinc-600">{formatNumber(row.filled_price)}</div>
<div className="text-[10px] text-[var(--muted-foreground)]">{formatNumber(row.filled_price)}</div>
</td>
<td className="px-4 py-2 text-zinc-400">
<td className={cn('px-4 py-2', mutedTextClass)}>
<div>{row.reason || '-'}</div>
{matchedBy && <div className="text-[10px] text-zinc-600">matchedBy: {matchedBy}</div>}
{matchedBy && <div className="text-[10px] text-[var(--muted-foreground)]">matchedBy: {matchedBy}</div>}
</td>
<td className="px-4 py-2 text-zinc-500 font-mono" title={exchangeSubTag || ''}>
<td className={cn('px-4 py-2', monoMutedClass)} title={exchangeSubTag || ''}>
{compactTag(exchangeSubTag)}
</td>
<td className="px-4 py-2 text-zinc-500 font-mono">
<td className={cn('px-4 py-2', monoMutedClass)}>
<div>bf: {shortId(row.backfill_order_id || '')}</div>
<div className="text-[10px]">ex: {shortId(row.exchange_order_id || '')}</div>
</td>
@ -638,23 +653,27 @@ export const ReconciliationAuditPanel = () => {
</table>
</div>
<div className="px-5 py-3 border-t border-white/[0.04] flex items-center justify-end gap-2">
<button
<div className="flex items-center justify-end gap-2 border-t border-[var(--border)] px-5 py-3">
<Button
onClick={() => setOffset((prev) => Math.max(0, prev - PAGE_LIMIT))}
disabled={!hasPrevPage || isLoading}
className="px-3 py-1.5 rounded-lg bg-zinc-800 border border-zinc-700 text-[10px] text-zinc-300 uppercase tracking-wider disabled:opacity-40"
variant="outline"
size="sm"
className="h-8 text-[10px] uppercase tracking-wider"
>
Previous
</button>
<button
</Button>
<Button
onClick={() => setOffset((prev) => prev + PAGE_LIMIT)}
disabled={!hasNextPage || isLoading}
className="px-3 py-1.5 rounded-lg bg-zinc-800 border border-zinc-700 text-[10px] text-zinc-300 uppercase tracking-wider disabled:opacity-40"
variant="outline"
size="sm"
className="h-8 text-[10px] uppercase tracking-wider"
>
Next
</button>
</Button>
</div>
</section>
</Card>
</div>
);
};