ui: explicit Save button for shadow book settings
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m17s
Deploy / deploy (push) Successful in 37s

The shadow book toggle saved on change and the numeric fields on blur,
inconsistent with every other admin panel (which stages edits behind a
Save button). Stage all four fields in local state and commit them
together on Save, with an unsaved-changes hint. This also makes enabling
the live-trade toggle a deliberate two-step action rather than an
unguarded single click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 13:50:00 +02:00
co-authored by Claude Fable 5
parent 565484de87
commit 1e072346a9
@@ -49,6 +49,15 @@ export function PerformanceSettings() {
if (window.isLoading || shadow.isLoading || !book) return <SkeletonCard />; if (window.isLoading || shadow.isLoading || !book) return <SkeletonCard />;
// Staged edits: nothing about the shadow book persists until Save, matching
// the other admin panels — and making the live-trade toggle deliberate.
const bookDirty =
!!shadow.data &&
(book.enabled !== shadow.data.enabled ||
book.capacity !== shadow.data.capacity ||
book.risk_pct !== shadow.data.risk_pct ||
book.start_equity !== shadow.data.start_equity);
return ( return (
<div className="glass space-y-5 p-5"> <div className="glass space-y-5 p-5">
<div> <div>
@@ -106,14 +115,15 @@ export function PerformanceSettings() {
<input <input
type="checkbox" type="checkbox"
checked={book.enabled} checked={book.enabled}
onChange={(e) => saveBook.mutate({ enabled: e.target.checked })} onChange={(e) => setBook({ ...book, enabled: e.target.checked })}
className="mt-0.5" className="mt-0.5"
/> />
<span> <span>
<span className="text-sm text-gray-200">Shadow book enabled</span> <span className="text-sm text-gray-200">Shadow book enabled</span>
<span className="block text-[11px] leading-relaxed text-gray-500"> <span className="block text-[11px] leading-relaxed text-gray-500">
Starts opening real paper positions automatically on the next near-close scan. Verify its Starts opening real paper positions automatically on the next near-close scan takes
first selections match a backtest of that day's cross-section before trusting the curve. effect when you Save. Verify its first selections match the top-ranked qualified setups
before trusting the curve.
</span> </span>
</span> </span>
</label> </label>
@@ -127,7 +137,6 @@ export function PerformanceSettings() {
max={100} max={100}
value={book.capacity} value={book.capacity}
onChange={(e) => setBook({ ...book, capacity: Number(e.target.value) })} onChange={(e) => setBook({ ...book, capacity: Number(e.target.value) })}
onBlur={() => saveBook.mutate({ capacity: book.capacity })}
className="input-glass w-full px-3 py-2 text-sm" className="input-glass w-full px-3 py-2 text-sm"
/> />
</label> </label>
@@ -140,7 +149,6 @@ export function PerformanceSettings() {
max={10} max={10}
value={book.risk_pct} value={book.risk_pct}
onChange={(e) => setBook({ ...book, risk_pct: Number(e.target.value) })} onChange={(e) => setBook({ ...book, risk_pct: Number(e.target.value) })}
onBlur={() => saveBook.mutate({ risk_pct: book.risk_pct })}
className="input-glass w-full px-3 py-2 text-sm" className="input-glass w-full px-3 py-2 text-sm"
/> />
</label> </label>
@@ -152,7 +160,6 @@ export function PerformanceSettings() {
step={1000} step={1000}
value={book.start_equity} value={book.start_equity}
onChange={(e) => setBook({ ...book, start_equity: Number(e.target.value) })} onChange={(e) => setBook({ ...book, start_equity: Number(e.target.value) })}
onBlur={() => saveBook.mutate({ start_equity: book.start_equity })}
className="input-glass w-full px-3 py-2 text-sm" className="input-glass w-full px-3 py-2 text-sm"
/> />
</label> </label>
@@ -161,6 +168,20 @@ export function PerformanceSettings() {
Defaults match the validated configuration: 10 positions, 1% fixed-fractional risk. Start Defaults match the validated configuration: 10 positions, 1% fixed-fractional risk. Start
equity is only a sizing base the books are compared in R-multiples, not currency. equity is only a sizing base the books are compared in R-multiples, not currency.
</p> </p>
<div className="mt-4 flex items-center gap-3">
<button
type="button"
className="btn-primary px-4 py-2 text-sm disabled:opacity-50"
disabled={!bookDirty || saveBook.isPending}
onClick={() => saveBook.mutate(book)}
>
{saveBook.isPending ? 'Saving…' : 'Save shadow book'}
</button>
{bookDirty && !saveBook.isPending && (
<span className="text-[11px] text-amber-400/80">Unsaved changes</span>
)}
</div>
</div> </div>
</div> </div>
); );