import React from 'react'; import { AlertTriangle, Clock3, RefreshCcw, Search, ShieldCheck, Undo2 } from 'lucide-react'; 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; batch_id: string; profile_id: string; symbol: string; trade_id: string; exchange_order_id?: string | null; exchange_client_order_id?: string | null; backfill_order_id?: string | null; filled_qty?: number | null; filled_price?: number | null; filled_at?: string | null; dry_run: boolean; decision: string; reason?: string | null; metadata?: Record | null; applied_at?: string | null; reverted_at?: string | null; created_at: string; } interface ReconciliationBackfillBatchSummary { batchId: string; firstSeenAt: string; lastSeenAt: string; profileIds: string[]; symbols: string[]; totalRows: number; byDecision: Record; dryRunRows: number; appliedRows: number; revertedRows: number; } interface ReconciliationAuditResponse { success: boolean; rows?: ReconciliationBackfillAuditRow[]; pagination?: { limit?: number; offset?: number; totalCount?: number; hasMore?: boolean; }; error?: string; } interface ReconciliationBatchesResponse { success: boolean; batches?: ReconciliationBackfillBatchSummary[]; error?: string; } interface ReconciliationFilters { profileId: string; symbol: string; batchId: string; decision: string; days: number; } const PAGE_LIMIT = 50; const BATCH_LIMIT = 20; const MANUAL_REVIEW_LIMIT = 200; const MANUAL_REVIEW_REASON = 'missing_fill_evidence_for_large_remainder'; const DEFAULT_FILTERS: ReconciliationFilters = { profileId: '', symbol: '', batchId: '', decision: '', days: 7 }; 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 => { const payload = (await response.json().catch(() => null)) as { error?: string; message?: string } | null; return payload?.error || payload?.message || `HTTP ${response.status}`; }; const formatDateTime = (value?: string | null): string => { if (!value) return '-'; const parsed = Date.parse(value); if (!Number.isFinite(parsed)) return '-'; return new Date(parsed).toLocaleString(); }; const formatNumber = (value?: number | null): string => { if (typeof value !== 'number' || !Number.isFinite(value)) return '-'; return value.toLocaleString(undefined, { maximumFractionDigits: 8 }); }; const shortId = (value: string): string => { if (!value) return '-'; return value.length > 16 ? `${value.slice(0, 8)}...${value.slice(-6)}` : value; }; const compactTag = (value?: string): string => { const token = String(value || '').trim(); if (!token) return '-'; return token.length > 24 ? `${token.slice(0, 12)}...${token.slice(-8)}` : token; }; const safeCount = (value: unknown): number => { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; }; const buildQueryParams = ( filters: ReconciliationFilters, limit: number, offset?: number, decisionOverride?: string ): URLSearchParams => { const params = new URLSearchParams(); params.set('limit', String(limit)); params.set('days', String(filters.days)); if (typeof offset === 'number') { params.set('offset', String(offset)); } const profileId = filters.profileId.trim(); const symbol = filters.symbol.trim().toUpperCase(); const batchId = filters.batchId.trim(); const decision = decisionOverride ?? filters.decision.trim(); if (profileId) params.set('profileId', profileId); if (symbol) params.set('symbol', symbol); if (batchId) params.set('batchId', batchId); if (decision) params.set('decision', decision); return params; }; const readMetadataNumber = (metadata: Record | null | undefined, key: string): number | null => { if (!metadata) return null; const parsed = Number(metadata[key]); return Number.isFinite(parsed) ? parsed : null; }; export const ReconciliationAuditPanel = () => { const [draftFilters, setDraftFilters] = React.useState(DEFAULT_FILTERS); const [filters, setFilters] = React.useState(DEFAULT_FILTERS); const [offset, setOffset] = React.useState(0); const [rows, setRows] = React.useState([]); const [batches, setBatches] = React.useState([]); const [manualReviewRows, setManualReviewRows] = React.useState([]); const [totalCount, setTotalCount] = React.useState(0); const [isLoading, setIsLoading] = React.useState(false); const [error, setError] = React.useState(null); const [lastLoadedAt, setLastLoadedAt] = React.useState(null); const [isReverting, setIsReverting] = React.useState(null); const loadAuditData = React.useCallback(async () => { setIsLoading(true); setError(null); try { const accessToken = await getPlatformAccessToken(); const apiUrl = tradingRuntime.tradingApiUrl; const auditParams = buildQueryParams(filters, PAGE_LIMIT, offset); const batchParams = buildQueryParams(filters, BATCH_LIMIT); const manualReviewParams = buildQueryParams(filters, MANUAL_REVIEW_LIMIT, 0, 'NO_GO'); const [auditResponse, batchResponse, manualReviewResponse] = await Promise.all([ fetch(`${apiUrl}/api/reconciliation/backfill/audit?${auditParams.toString()}`, { headers: { Authorization: `Bearer ${accessToken}`, 'x-request-id': createRequestId('web-recon') } }), fetch(`${apiUrl}/api/reconciliation/backfill/batches?${batchParams.toString()}`, { headers: { Authorization: `Bearer ${accessToken}`, 'x-request-id': createRequestId('web-recon') } }), fetch(`${apiUrl}/api/reconciliation/backfill/audit?${manualReviewParams.toString()}`, { headers: { Authorization: `Bearer ${accessToken}`, 'x-request-id': createRequestId('web-recon') } }) ]); if (!auditResponse.ok) { throw new Error(await parseResponseError(auditResponse)); } if (!batchResponse.ok) { throw new Error(await parseResponseError(batchResponse)); } if (!manualReviewResponse.ok) { throw new Error(await parseResponseError(manualReviewResponse)); } const auditPayload = (await auditResponse.json()) as ReconciliationAuditResponse; const batchPayload = (await batchResponse.json()) as ReconciliationBatchesResponse; const manualReviewPayload = (await manualReviewResponse.json()) as ReconciliationAuditResponse; if (!auditPayload.success) { throw new Error(auditPayload.error || 'Failed to load reconciliation audit rows'); } if (!batchPayload.success) { throw new Error(batchPayload.error || 'Failed to load reconciliation audit batches'); } if (!manualReviewPayload.success) { throw new Error(manualReviewPayload.error || 'Failed to load manual review rows'); } setRows(Array.isArray(auditPayload.rows) ? auditPayload.rows : []); setBatches(Array.isArray(batchPayload.batches) ? batchPayload.batches : []); setManualReviewRows( (Array.isArray(manualReviewPayload.rows) ? manualReviewPayload.rows : []) .filter((row) => String(row.reason || '').trim() === MANUAL_REVIEW_REASON) ); setTotalCount(Math.max(0, safeCount(auditPayload.pagination?.totalCount))); setLastLoadedAt(Date.now()); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to load reconciliation audit'; setError(message); setRows([]); setBatches([]); setManualReviewRows([]); setTotalCount(0); } finally { setIsLoading(false); } }, [filters, offset]); React.useEffect(() => { void loadAuditData(); }, [loadAuditData]); const decisionOptions = React.useMemo(() => { const values = new Set(DEFAULT_DECISIONS); for (const row of rows) { if (row.decision) values.add(row.decision); } for (const batch of batches) { for (const decision of Object.keys(batch.byDecision || {})) { if (decision) values.add(decision); } } return Array.from(values).sort((a, b) => a.localeCompare(b)); }, [rows, batches]); const aggregateDecisionCounts = React.useMemo(() => { const counts: Record = {}; for (const batch of batches) { for (const [decision, count] of Object.entries(batch.byDecision || {})) { counts[decision] = (counts[decision] || 0) + safeCount(count); } } return counts; }, [batches]); const hasPrevPage = offset > 0; const hasNextPage = offset + PAGE_LIMIT < totalCount; const pageStart = totalCount === 0 ? 0 : offset + 1; const pageEnd = totalCount === 0 ? 0 : Math.min(offset + rows.length, totalCount); const handleRevertBatch = async (batchId: string) => { if (!confirm(`Are you sure you want to revert batch ${batchId}? This performs status-only rollback for generated BFILL rows (non-destructive).`)) return; setIsReverting(batchId); setError(null); try { const accessToken = await getPlatformAccessToken(); const apiUrl = tradingRuntime.tradingApiUrl; const response = await fetch(`${apiUrl}/api/admin/revert-backfill-batch`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, 'x-request-id': createRequestId('web-recon') }, body: JSON.stringify({ batchId }) }); if (!response.ok) { throw new Error(await parseResponseError(response)); } const payload = await response.json(); if (!payload.success) throw new Error(payload.error || 'Revert failed'); void loadAuditData(); } catch (err: any) { setError(err.message || 'Failed to revert batch'); } finally { setIsReverting(null); } }; const handleApplyFilters = () => { setOffset(0); setFilters({ profileId: draftFilters.profileId.trim(), symbol: draftFilters.symbol.trim().toUpperCase(), batchId: draftFilters.batchId.trim(), decision: draftFilters.decision.trim(), days: draftFilters.days }); }; const handleResetFilters = () => { setDraftFilters(DEFAULT_FILTERS); setFilters(DEFAULT_FILTERS); setOffset(0); }; return (

Reconciliation EXIT Backfill Audit

Rows (Filter Scope)

{totalCount}

Batches (Latest)

{batches.length}

Applied

{aggregateDecisionCounts.APPLIED || 0}

No-Go

{aggregateDecisionCounts.NO_GO || 0}

Last refresh: {lastLoadedAt ? new Date(lastLoadedAt).toLocaleString() : 'Not loaded yet'}

Filters

setDraftFilters((prev) => ({ ...prev, profileId: event.target.value }))} placeholder="Profile ID" className="h-10 rounded-lg px-3 py-2 text-xs" /> setDraftFilters((prev) => ({ ...prev, symbol: event.target.value }))} placeholder="Symbol (e.g. SOL/USDT)" className="h-10 rounded-lg px-3 py-2 text-xs" /> setDraftFilters((prev) => ({ ...prev, batchId: event.target.value }))} placeholder="Batch ID" className="h-10 rounded-lg px-3 py-2 text-xs" />
{error && (

{error}

)}

Manual Review Queue

{manualReviewRows.length} large-remainder case{manualReviewRows.length === 1 ? '' : 's'}
{manualReviewRows.length === 0 ? ( ) : ( manualReviewRows.map((row) => { const metadata = row.metadata || null; const remaining = readMetadataNumber(metadata, 'remaining'); const openQty = readMetadataNumber(metadata, 'openQty'); const dustThreshold = readMetadataNumber(metadata, 'dustThreshold'); const evidenceRows = readMetadataNumber(metadata, 'evidenceRows'); const evidenceRowsUsed = readMetadataNumber(metadata, 'evidenceRowsUsed'); const unmatchedEvidenceCount = readMetadataNumber(metadata, 'unmatchedEvidenceCount'); return ( ); }) )}
Time Profile Symbol Trade Remaining Open Qty Dust Threshold Evidence Batch
{isLoading ? 'Loading manual review queue...' : 'No large remainder manual-review cases for selected filters'}
{formatDateTime(row.created_at)} {shortId(row.profile_id)} {row.symbol || '-'} {row.trade_id || '-'} {formatNumber(remaining)} {formatNumber(openQty)} {formatNumber(dustThreshold)} {formatNumber(evidenceRowsUsed)}/{formatNumber(evidenceRows)} unmatched:{formatNumber(unmatchedEvidenceCount)} {shortId(row.batch_id || '')}

Batch Summary

{batches.length === 0 ? ( ) : ( batches.map((batch) => ( )) )}
Batch Window Rows Decisions Profiles Symbols Actions
{isLoading ? 'Loading batch summaries...' : 'No backfill batches found for selected filters'}
{shortId(batch.batchId)}
{formatDateTime(batch.firstSeenAt)}
to {formatDateTime(batch.lastSeenAt)}
{batch.totalRows}
{Object.entries(batch.byDecision || {}).map(([decision, count]) => ( {decision}:{count} ))}
{batch.profileIds.map(shortId).join(', ') || '-'} {batch.symbols.join(', ') || '-'} {batch.appliedRows > 0 && ( )}

Audit Rows

Showing {pageStart}-{pageEnd} of {totalCount}
{rows.length === 0 ? ( ) : ( rows.map((row) => { const metadata = row.metadata || {}; const matchedBy = typeof metadata.matchedBy === 'string' ? metadata.matchedBy : null; const exchangeSubTag = typeof metadata.exchangeSubTag === 'string' ? metadata.exchangeSubTag : ''; return ( ); }) )}
Time Decision Profile Symbol Trade Qty / Price Reason Exchange Tag Order IDs
{isLoading ? 'Loading audit rows...' : 'No audit rows for selected filters'}
{formatDateTime(row.created_at)} {row.decision} {row.dry_run && DRY} {shortId(row.profile_id)} {row.symbol || '-'} {row.trade_id || '-'}
{formatNumber(row.filled_qty)}
{formatNumber(row.filled_price)}
{row.reason || '-'}
{matchedBy &&
matchedBy: {matchedBy}
}
{compactTag(exchangeSubTag)}
bf: {shortId(row.backfill_order_id || '')}
ex: {shortId(row.exchange_order_id || '')}
); };