Add a wipe & recalculate button to the account detail page

Cached daily P&L rows outlive what the Cash History report still
produces (stale pre-roll dates being the recent example), and nothing
deletes them any more. POST /api/accounts/[id]/wipe drops the account's
cached P&L, fund transactions and meta, clears the in-memory view, and
rebuilds from a fresh report — fetchDaysTraded now takes an optional
account filter so one account doesn't cost a report round-trip on every
other. The button confirms inline first: rows older than the report's
28-day window cannot be refetched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-08 21:27:34 -05:00
co-authored by Claude Fable 5
parent bd72c4b1f6
commit 5a69ea2f64
4 changed files with 128 additions and 1 deletions
+64
View File
@@ -80,6 +80,69 @@ function fmt(value: number) {
return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 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 (
<div className="ml-auto flex items-center gap-2">
<span className="text-xs text-slate-500">
Wipe cached data and refetch? Days older than the 28-day report window are lost for good.
</span>
<button
onClick={wipe}
disabled={busy}
className="px-2.5 py-1 bg-red-100 hover:bg-red-200 disabled:opacity-50 text-red-700 text-xs font-semibold rounded-md transition-colors"
>
{busy ? 'Refetching…' : 'Wipe & recalculate'}
</button>
<button
onClick={() => { setConfirming(false); setError(''); }}
disabled={busy}
className="px-2.5 py-1 bg-slate-100 hover:bg-slate-200 disabled:opacity-50 text-slate-600 text-xs font-semibold rounded-md transition-colors"
>
Cancel
</button>
</div>
);
}
return (
<div className="ml-auto flex items-center gap-2">
{error && <span className="text-xs text-red-500">{error}</span>}
<button
onClick={() => setConfirming(true)}
className="px-2.5 py-1 text-xs font-semibold text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
title="Delete this account's cached daily P&L, fund transactions and meta, then rebuild them from a fresh Tradovate report"
>
Wipe & recalculate
</button>
</div>
);
}
function fmtDate(iso: string) { function fmtDate(iso: string) {
const [, m, d] = iso.split('-'); const [, m, d] = iso.split('-');
return `${parseInt(m)}/${parseInt(d)}`; return `${parseInt(m)}/${parseInt(d)}`;
@@ -410,6 +473,7 @@ export default function AccountPage() {
☠ DEAD ☠ DEAD
</span> </span>
)} )}
<WipeDataButton accountId={account.id} onDone={() => { void load(); }} />
</div> </div>
{isDead && ( {isDead && (
+47
View File
@@ -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 });
}
+13
View File
@@ -282,6 +282,19 @@ export function loadAccountMeta(accountId: number, key: string): string | null {
return row?.value ?? 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 ─────────────────────────────────────────────────────── // ── AutoBuyer captures ───────────────────────────────────────────────────────
db.exec(` db.exec(`
+4 -1
View File
@@ -437,7 +437,9 @@ export class TradovateClient {
return { active, prior }; 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 }; if (!this.accessInfo?.accessToken) return { failedAccounts: 0 };
this.fetchDaysComplete = false; this.fetchDaysComplete = false;
let failedAccounts = 0; let failedAccounts = 0;
@@ -524,6 +526,7 @@ export class TradovateClient {
}; };
for (const account of this.accountList) { for (const account of this.accountList) {
if (onlyAccountIds && !onlyAccountIds.includes(account.id)) continue;
// Load last known fund date and fund transactions from DB // Load last known fund date and fund transactions from DB
const storedFundDate = loadAccountMeta(account.id, 'last_fund_date'); const storedFundDate = loadAccountMeta(account.id, 'last_fund_date');
this.lastFundDates[account.id] = storedFundDate; this.lastFundDates[account.id] = storedFundDate;