b149f70507
- Add GET/PUT /api/admin/trading/approval-config endpoints - Add POST/DELETE /api/admin/trading/lockouts endpoints - Add useApprovalConfig, useUpdateApprovalConfig, useCreateLockout, useDeleteLockout hooks - Add Paper Order Approval toggle card with confirmation dialog - Add lockout creation form and delete button to Active Lockouts card - Add MSW handlers for all new endpoints - Add property-based tests for bug condition exploration and preservation
535 lines
23 KiB
TypeScript
535 lines
23 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
useTradingConfig,
|
|
useSetTradingMode,
|
|
usePendingApprovals,
|
|
useReviewApproval,
|
|
useActiveLockouts,
|
|
useCreateLockout,
|
|
useDeleteLockout,
|
|
useMacroStatus,
|
|
useToggleMacro,
|
|
useCompetitiveStatus,
|
|
useToggleCompetitive,
|
|
useApprovalConfig,
|
|
useUpdateApprovalConfig,
|
|
} from '../api/hooks';
|
|
import { useResetPaperTrading, useTradingStatus, useUpdateTradingConfig } from '../api/tradingHooks';
|
|
import { StatusBadge, LoadingSpinner, Card } from '../components/ui';
|
|
|
|
export function TradingPage() {
|
|
const { data: config, isLoading: configLoading } = useTradingConfig();
|
|
const { data: approvals } = usePendingApprovals();
|
|
const { data: lockouts } = useActiveLockouts();
|
|
const { data: macroStatus } = useMacroStatus();
|
|
const { data: competitiveStatus } = useCompetitiveStatus();
|
|
const setMode = useSetTradingMode();
|
|
const resetTrading = useResetPaperTrading();
|
|
const { data: tradingStatus } = useTradingStatus();
|
|
const updateConfig = useUpdateTradingConfig();
|
|
const reviewApproval = useReviewApproval();
|
|
const toggleMacro = useToggleMacro();
|
|
const toggleCompetitive = useToggleCompetitive();
|
|
const { data: approvalConfig } = useApprovalConfig();
|
|
const updateApprovalConfig = useUpdateApprovalConfig();
|
|
const createLockout = useCreateLockout();
|
|
const deleteLockout = useDeleteLockout();
|
|
|
|
const [lockoutTicker, setLockoutTicker] = useState('');
|
|
const [lockoutReason, setLockoutReason] = useState('');
|
|
const [lockoutDuration, setLockoutDuration] = useState(60);
|
|
|
|
// Normalize API field names (API returns macro_enabled/competitive_enabled, not enabled)
|
|
const macroEnabled = macroStatus?.enabled ?? macroStatus?.macro_enabled ?? false;
|
|
const competitiveEnabled = competitiveStatus?.enabled ?? competitiveStatus?.competitive_enabled ?? false;
|
|
const autoApprovePaper = approvalConfig?.auto_approve_paper ?? true;
|
|
const [confirmMode, setConfirmMode] = useState<string | null>(null);
|
|
const [confirmMacroToggle, setConfirmMacroToggle] = useState(false);
|
|
const [confirmCompetitiveToggle, setConfirmCompetitiveToggle] = useState(false);
|
|
const [confirmApprovalToggle, setConfirmApprovalToggle] = useState(false);
|
|
|
|
if (configLoading) return <LoadingSpinner />;
|
|
|
|
const currentMode = config?.trading_mode ?? 'paper';
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-xl font-semibold text-gray-100">Trading Controls</h1>
|
|
|
|
{/* Trading Mode */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Trading Mode</h2>
|
|
<div className="flex items-center gap-3">
|
|
{['paper', 'live', 'disabled'].map((mode) => (
|
|
<button
|
|
key={mode}
|
|
onClick={() => {
|
|
if (mode === currentMode) return;
|
|
if (mode === 'live') { setConfirmMode(mode); return; }
|
|
setMode.mutate(mode);
|
|
}}
|
|
className={`rounded-md px-4 py-2 text-sm font-medium capitalize transition-colors ${
|
|
currentMode === mode
|
|
? 'bg-brand-600 text-white'
|
|
: 'border border-surface-700 bg-surface-900 text-gray-400 hover:bg-surface-800'
|
|
}`}
|
|
aria-pressed={currentMode === mode}
|
|
>
|
|
{mode}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Confirmation dialog for live mode */}
|
|
{confirmMode && (
|
|
<div className="mt-4 rounded-lg border border-orange-700/50 bg-orange-900/20 p-4">
|
|
<p className="text-sm text-orange-300">
|
|
Are you sure you want to switch to <span className="font-semibold">{confirmMode}</span> mode?
|
|
This enables real order execution.
|
|
</p>
|
|
<div className="mt-3 flex gap-2">
|
|
<button
|
|
onClick={() => { setMode.mutate(confirmMode); setConfirmMode(null); }}
|
|
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700"
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button
|
|
onClick={() => setConfirmMode(null)}
|
|
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Paper Trading Reset */}
|
|
<ResetCard
|
|
onReset={() => resetTrading.mutate(0)}
|
|
isResetting={resetTrading.isPending}
|
|
/>
|
|
|
|
{/* Risk Tier */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Risk Tier</h2>
|
|
<p className="mb-3 text-[10px] text-gray-600">
|
|
Controls confidence gates, position sizing, and portfolio heat limits for the trading engine.
|
|
</p>
|
|
<div className="flex items-center gap-3">
|
|
{(['conservative', 'moderate', 'aggressive'] as const).map((tier) => {
|
|
const currentTier = tradingStatus?.risk_tier ?? 'moderate';
|
|
const descriptions: Record<string, string> = {
|
|
conservative: 'Min confidence 0.75, max 5% position, 10% heat',
|
|
moderate: 'Min confidence 0.55, max 10% position, 20% heat',
|
|
aggressive: 'Min confidence 0.40, max 15% position, 30% heat',
|
|
};
|
|
return (
|
|
<button
|
|
key={tier}
|
|
onClick={() => {
|
|
if (tier !== currentTier) updateConfig.mutate({ risk_tier: tier });
|
|
}}
|
|
className={`rounded-md px-4 py-2 text-sm font-medium capitalize transition-colors ${
|
|
currentTier === tier
|
|
? 'bg-brand-600 text-white'
|
|
: 'border border-surface-700 bg-surface-900 text-gray-400 hover:bg-surface-800'
|
|
}`}
|
|
aria-pressed={currentTier === tier}
|
|
title={descriptions[tier]}
|
|
>
|
|
{tier}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Macro Signal Layer Toggle */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Macro Signal Layer</h2>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => setConfirmMacroToggle(true)}
|
|
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 focus:ring-offset-surface-900 ${
|
|
macroEnabled ? 'bg-brand-600' : 'bg-surface-700'
|
|
}`}
|
|
role="switch"
|
|
aria-checked={macroEnabled ?? false}
|
|
aria-label="Toggle macro signal layer"
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition-transform ${
|
|
macroEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
<span className="text-sm text-gray-300">
|
|
{macroEnabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
{macroStatus?.toggled_at && (
|
|
<span className="text-xs text-gray-500">
|
|
Last changed: {new Date(macroStatus.toggled_at).toLocaleString()}
|
|
{macroStatus.toggled_by && ` by ${macroStatus.toggled_by}`}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Confirmation dialog for macro toggle */}
|
|
{confirmMacroToggle && (
|
|
<div className="mt-4 rounded-lg border border-orange-700/50 bg-orange-900/20 p-4">
|
|
<p className="text-sm text-orange-300">
|
|
Are you sure you want to {macroEnabled ? 'disable' : 'enable'} the macro signal layer?
|
|
{macroEnabled
|
|
? ' Disabling will exclude macro signals from trend summaries and recommendations.'
|
|
: ' Enabling will include global event macro signals in trend summaries and recommendations.'}
|
|
</p>
|
|
<div className="mt-3 flex gap-2">
|
|
<button
|
|
onClick={() => {
|
|
toggleMacro.mutate(!macroEnabled);
|
|
setConfirmMacroToggle(false);
|
|
}}
|
|
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700"
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button
|
|
onClick={() => setConfirmMacroToggle(false)}
|
|
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Competitive Signal Layer Toggle */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Competitive Signal Layer</h2>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => setConfirmCompetitiveToggle(true)}
|
|
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 focus:ring-offset-surface-900 ${
|
|
competitiveEnabled ? 'bg-brand-600' : 'bg-surface-700'
|
|
}`}
|
|
role="switch"
|
|
aria-checked={competitiveEnabled ?? false}
|
|
aria-label="Toggle competitive signal layer"
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition-transform ${
|
|
competitiveEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
<span className="text-sm text-gray-300">
|
|
{competitiveEnabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
{competitiveStatus?.toggled_at && (
|
|
<span className="text-xs text-gray-500">
|
|
Last changed: {new Date(competitiveStatus.toggled_at).toLocaleString()}
|
|
{competitiveStatus.toggled_by && ` by ${competitiveStatus.toggled_by}`}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Confirmation dialog for competitive toggle */}
|
|
{confirmCompetitiveToggle && (
|
|
<div className="mt-4 rounded-lg border border-orange-700/50 bg-orange-900/20 p-4">
|
|
<p className="text-sm text-orange-300">
|
|
Are you sure you want to {competitiveEnabled ? 'disable' : 'enable'} the competitive signal layer?
|
|
{competitiveEnabled
|
|
? ' Disabling will exclude historical pattern and competitive signals from trend summaries and recommendations.'
|
|
: ' Enabling will include historical pattern and competitive signals in trend summaries and recommendations.'}
|
|
</p>
|
|
<div className="mt-3 flex gap-2">
|
|
<button
|
|
onClick={() => {
|
|
toggleCompetitive.mutate(!competitiveEnabled);
|
|
setConfirmCompetitiveToggle(false);
|
|
}}
|
|
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700"
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button
|
|
onClick={() => setConfirmCompetitiveToggle(false)}
|
|
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Paper Order Approval Toggle */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Paper Order Approval</h2>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => setConfirmApprovalToggle(true)}
|
|
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 focus:ring-offset-surface-900 ${
|
|
!autoApprovePaper ? 'bg-brand-600' : 'bg-surface-700'
|
|
}`}
|
|
role="switch"
|
|
aria-checked={!autoApprovePaper}
|
|
aria-label="Toggle paper order approval requirement"
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition-transform ${
|
|
!autoApprovePaper ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
<span className="text-sm text-gray-300">
|
|
{autoApprovePaper ? 'Auto-approve paper orders' : 'Require approval for paper orders'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Confirmation dialog for approval toggle */}
|
|
{confirmApprovalToggle && (
|
|
<div className="mt-4 rounded-lg border border-orange-700/50 bg-orange-900/20 p-4">
|
|
<p className="text-sm text-orange-300">
|
|
Are you sure you want to {autoApprovePaper ? 'require approval for' : 'auto-approve'} paper orders?
|
|
{autoApprovePaper
|
|
? ' Enabling approval requirements will hold all paper orders for manual review before execution.'
|
|
: ' Disabling approval requirements will allow paper orders to execute automatically.'}
|
|
</p>
|
|
<div className="mt-3 flex gap-2">
|
|
<button
|
|
onClick={() => {
|
|
updateApprovalConfig.mutate({ auto_approve_paper: !autoApprovePaper });
|
|
setConfirmApprovalToggle(false);
|
|
}}
|
|
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700"
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button
|
|
onClick={() => setConfirmApprovalToggle(false)}
|
|
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Pending Approvals */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">
|
|
Pending Approvals ({approvals?.length ?? 0})
|
|
</h2>
|
|
{!approvals?.length ? (
|
|
<p className="text-sm text-gray-500">No pending approvals</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{approvals.map((a) => (
|
|
<ApprovalRow key={a.id} approval={a} onReview={(approved, note) => reviewApproval.mutate({ id: a.id, approved, review_note: note })} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Active Lockouts */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">
|
|
Active Lockouts ({lockouts?.length ?? 0})
|
|
</h2>
|
|
|
|
{/* Lockout Creation Form */}
|
|
<form
|
|
className="mb-4 flex flex-wrap items-end gap-2 rounded border border-surface-700 bg-surface-900 p-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if (!lockoutTicker.trim()) return;
|
|
createLockout.mutate(
|
|
{ ticker: lockoutTicker.trim().toUpperCase(), reason: lockoutReason.trim(), duration_minutes: lockoutDuration },
|
|
{
|
|
onSuccess: () => {
|
|
setLockoutTicker('');
|
|
setLockoutReason('');
|
|
setLockoutDuration(60);
|
|
},
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
<div className="flex flex-col gap-1">
|
|
<label htmlFor="lockout-ticker" className="text-xs text-gray-500">Ticker</label>
|
|
<input
|
|
id="lockout-ticker"
|
|
type="text"
|
|
placeholder="AAPL"
|
|
value={lockoutTicker}
|
|
onChange={(e) => setLockoutTicker(e.target.value.toUpperCase())}
|
|
className="w-24 rounded-md border border-surface-700 bg-surface-950 px-2 py-1 text-xs font-mono text-gray-200 uppercase placeholder-gray-600"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="flex flex-1 flex-col gap-1">
|
|
<label htmlFor="lockout-reason" className="text-xs text-gray-500">Reason</label>
|
|
<input
|
|
id="lockout-reason"
|
|
type="text"
|
|
placeholder="Manual lockout reason…"
|
|
value={lockoutReason}
|
|
onChange={(e) => setLockoutReason(e.target.value)}
|
|
className="rounded-md border border-surface-700 bg-surface-950 px-2 py-1 text-xs text-gray-200 placeholder-gray-600"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1">
|
|
<label htmlFor="lockout-duration" className="text-xs text-gray-500">Minutes</label>
|
|
<input
|
|
id="lockout-duration"
|
|
type="number"
|
|
min={1}
|
|
value={lockoutDuration}
|
|
onChange={(e) => setLockoutDuration(Number(e.target.value))}
|
|
className="w-20 rounded-md border border-surface-700 bg-surface-950 px-2 py-1 text-xs text-gray-200"
|
|
required
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={createLockout.isPending}
|
|
className="rounded-md bg-brand-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-brand-700 disabled:opacity-50"
|
|
>
|
|
{createLockout.isPending ? 'Creating…' : 'Add Lockout'}
|
|
</button>
|
|
</form>
|
|
|
|
{!lockouts?.length ? (
|
|
<p className="text-sm text-gray-500">No active lockouts</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{lockouts.map((l) => {
|
|
const expiresIn = l.expires_at ? Math.max(0, Math.round((new Date(l.expires_at).getTime() - Date.now()) / 60000)) : 0;
|
|
return (
|
|
<div key={l.id} className="flex items-center justify-between rounded border border-surface-700 bg-surface-950 p-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-mono font-semibold text-brand-300">{l.ticker}</span>
|
|
<StatusBadge status={l.lockout_type} />
|
|
<span className="text-sm text-gray-400">{l.reason}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-gray-500">
|
|
{expiresIn > 0 ? `${expiresIn}m remaining` : 'expired'}
|
|
</span>
|
|
<button
|
|
onClick={() => deleteLockout.mutate(l.id)}
|
|
disabled={deleteLockout.isPending}
|
|
className="rounded bg-red-700/80 px-2 py-0.5 text-xs font-medium text-red-100 hover:bg-red-600 disabled:opacity-50"
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ApprovalRow({ approval, onReview }: {
|
|
approval: { id: string; ticker: string; side: string; quantity: number; estimated_value: number | null; requested_at: string };
|
|
onReview: (approved: boolean, note: string) => void;
|
|
}) {
|
|
const [note, setNote] = useState('');
|
|
|
|
return (
|
|
<div className="rounded border border-surface-700 bg-surface-950 p-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-mono font-semibold text-brand-300">{approval.ticker}</span>
|
|
<StatusBadge status={approval.side} />
|
|
<span className="text-sm text-gray-300">qty: {approval.quantity}</span>
|
|
{approval.estimated_value != null && (
|
|
<span className="text-sm text-gray-500">${approval.estimated_value.toFixed(2)}</span>
|
|
)}
|
|
</div>
|
|
<span className="text-xs text-gray-500">{new Date(approval.requested_at).toLocaleString()}</span>
|
|
</div>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<input
|
|
type="text"
|
|
placeholder="Review note…"
|
|
value={note}
|
|
onChange={(e) => setNote(e.target.value)}
|
|
className="flex-1 rounded-md border border-surface-700 bg-surface-900 px-2 py-1 text-xs text-gray-200 placeholder-gray-500"
|
|
aria-label="Review note"
|
|
/>
|
|
<button onClick={() => onReview(true, note)} className="rounded bg-green-700 px-3 py-1 text-xs font-medium text-white hover:bg-green-600">
|
|
Approve
|
|
</button>
|
|
<button onClick={() => onReview(false, note)} className="rounded bg-red-700 px-3 py-1 text-xs font-medium text-white hover:bg-red-600">
|
|
Reject
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
function ResetCard({ onReset, isResetting }: {
|
|
onReset: () => void;
|
|
isResetting: boolean;
|
|
}) {
|
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
|
|
return (
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Paper Trading Account</h2>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-300">Full Reset</p>
|
|
<p className="text-[10px] text-gray-600">
|
|
Liquidates all broker positions, cancels open orders, wipes local trading history,
|
|
and syncs capital from the broker account.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowConfirm(true)}
|
|
disabled={isResetting}
|
|
className="rounded-md border border-red-700/50 bg-red-900/20 px-3 py-1.5 text-sm font-medium text-red-400 hover:bg-red-900/40 disabled:opacity-50"
|
|
>
|
|
Reset Everything
|
|
</button>
|
|
</div>
|
|
{showConfirm && (
|
|
<div className="mt-3 rounded-lg border border-red-700/50 bg-red-900/20 p-4">
|
|
<p className="text-sm text-red-300">
|
|
This will <span className="font-semibold">permanently delete</span> all positions, orders,
|
|
trading decisions, stop levels, portfolio snapshots, and backtest data.
|
|
All broker positions will be liquidated and capital will be set from the broker's account balance.
|
|
</p>
|
|
<div className="mt-3 flex gap-2">
|
|
<button
|
|
onClick={() => { onReset(); setShowConfirm(false); }}
|
|
disabled={isResetting}
|
|
className="rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
|
|
>
|
|
{isResetting ? 'Resetting…' : 'Yes, Reset Everything'}
|
|
</button>
|
|
<button
|
|
onClick={() => setShowConfirm(false)}
|
|
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|