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:
co-authored by
Claude Fable 5
parent
bd72c4b1f6
commit
5a69ea2f64
@@ -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 && (
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
@@ -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(`
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user