diff --git a/app/accounts/[id]/page.tsx b/app/accounts/[id]/page.tsx index 13601f3..2d28621 100644 --- a/app/accounts/[id]/page.tsx +++ b/app/accounts/[id]/page.tsx @@ -80,6 +80,69 @@ function fmt(value: number) { return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } +/** Wipes the account's cached P&L / fund / meta rows and refetches from + * Tradovate. Confirmed inline because rows older than the report's 28-day + * window cannot come back. */ +function WipeDataButton({ accountId, onDone }: { accountId: number; onDone: () => void }) { + const [confirming, setConfirming] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + + async function wipe() { + setBusy(true); + setError(''); + try { + const res = await fetch(`/api/accounts/${accountId}/wipe`, { method: 'POST' }); + const body = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(body.error ?? 'Wipe failed'); + if (body.fetchError) setError(`Wiped, but refetch: ${body.fetchError}`); + setConfirming(false); + onDone(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Wipe failed'); + } finally { + setBusy(false); + } + } + + if (confirming) { + return ( +
+ + Wipe cached data and refetch? Days older than the 28-day report window are lost for good. + + + +
+ ); + } + + return ( +
+ {error && {error}} + +
+ ); +} + function fmtDate(iso: string) { const [, m, d] = iso.split('-'); return `${parseInt(m)}/${parseInt(d)}`; @@ -410,6 +473,7 @@ export default function AccountPage() { ☠ DEAD )} + { void load(); }} /> {isDead && ( diff --git a/app/api/accounts/[id]/wipe/route.ts b/app/api/accounts/[id]/wipe/route.ts new file mode 100644 index 0000000..c124f15 --- /dev/null +++ b/app/api/accounts/[id]/wipe/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server'; +import { getClients } from '@/lib/clients'; +import { wipeAccountData } from '@/lib/db'; + +/** + * POST /api/accounts/[id]/wipe — drop every cached row for the account and + * rebuild it from a fresh Tradovate report. + * + * Destructive on purpose: cached rows older than the report's 28-day window + * cannot be refetched. The UI warns and confirms before calling this. + */ +export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const accountId = Number(id); + if (!Number.isInteger(accountId)) { + return NextResponse.json({ error: 'Bad account id' }, { status: 400 }); + } + + for (const client of getClients().values()) { + const account = client.accountList.find((a) => a.id === accountId); + if (!account) continue; + + wipeAccountData(accountId); + + // Clear the in-memory view too — a failed refetch must leave the + // account visibly empty, not showing the data that was just wiped. + delete client.dailyPnL[accountId]; + delete client.fullDailyPnL[accountId]; + delete client.daysTraded[accountId]; + delete client.priorProfit[accountId]; + delete client.lastFundDates[accountId]; + delete client.fundTransactions[accountId]; + delete client.lastFetchErrors[account.name]; + delete client.lastFetchRaw[account.name]; + + await client.fetchDaysTraded([accountId]); + + return NextResponse.json({ + ok: true, + entries: (client.dailyPnL[accountId] ?? []).length, + daysTraded: client.daysTraded[accountId] ?? 0, + fetchError: client.lastFetchErrors[account.name] ?? null, + }); + } + + return NextResponse.json({ error: 'Account not found' }, { status: 404 }); +} diff --git a/lib/db.ts b/lib/db.ts index 3fa1bdf..eea6ecf 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -282,6 +282,19 @@ export function loadAccountMeta(accountId: number, key: string): string | null { return row?.value ?? null; } +/** Deletes every cached row for an account — daily P&L, fund transactions and + * meta — so the next report fetch rebuilds it from scratch. Rows older than + * the report's 28-day window are gone for good; that is the point (stale + * pre-roll dates), but it is why callers confirm first. */ +export function wipeAccountData(accountId: number): void { + const txn = db.transaction(() => { + db.prepare('DELETE FROM daily_pnl WHERE account_id = ?').run(accountId); + db.prepare('DELETE FROM fund_transactions WHERE account_id = ?').run(accountId); + db.prepare('DELETE FROM account_meta WHERE account_id = ?').run(accountId); + }); + txn(); +} + // ── AutoBuyer captures ─────────────────────────────────────────────────────── db.exec(` diff --git a/lib/tradovate-class.ts b/lib/tradovate-class.ts index 4058ff3..7bc36a5 100644 --- a/lib/tradovate-class.ts +++ b/lib/tradovate-class.ts @@ -437,7 +437,9 @@ export class TradovateClient { return { active, prior }; } - public async fetchDaysTraded(): Promise<{ failedAccounts: number }> { + /** Pass onlyAccountIds to refresh a subset (e.g. one wiped account) without + * paying for a report round-trip on every other account of the firm. */ + public async fetchDaysTraded(onlyAccountIds?: number[]): Promise<{ failedAccounts: number }> { if (!this.accessInfo?.accessToken) return { failedAccounts: 0 }; this.fetchDaysComplete = false; let failedAccounts = 0; @@ -524,6 +526,7 @@ export class TradovateClient { }; for (const account of this.accountList) { + if (onlyAccountIds && !onlyAccountIds.includes(account.id)) continue; // Load last known fund date and fund transactions from DB const storedFundDate = loadAccountMeta(account.id, 'last_fund_date'); this.lastFundDates[account.id] = storedFundDate;