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;