Repository Public
1/**2 * v7 perp live — the REAL mode of a perp agent: the go-live checklist, the mandate review (one passkey), and the LIVE page3 * (venue account, mandate card with the signer's counters, held / refused orders in plain words, pause / resume / stop).4 *5 * Which money the agent moves is said by the same band as the live terminal (`ModeBand`): SIMULATION, TESTNET — play money,6 * REAL — real money. Anything that is not known to be the testnet is shown as real money.7 * Everything here comes from the server (`GET /agents/:id`, `GET /agents/:id/perp/live`); nothing is predicted locally, and8 * nothing is signed by this screen: the passkey assertion is passed on untouched.9 */10import { startAuthentication } from '@simplewebauthn/browser';11import { useQuery, useQueryClient } from '@tanstack/react-query';12import { useContext, useEffect, useRef, useState } from 'react';13import { get, post } from '../api';14import type { Nav } from '../App';15import { PriceChart } from '../chart';16import { RISK_CODES } from '../derivatives/api';17import { DataTable, Pnl, Row, Seg, Sheet, SideTag, Stat, timeOf, type Col } from '../derivatives/bits';18import { D, dec, fmt, usd } from '../derivatives/dec';19import { liveError, sha256Hex, type LiveConfig, type LiveDetail, type LiveErr, type LiveFunding, type LiveLocale, type LiveNetwork, type LiveOrder, type LiveRiskEvent, type MandateBody, type MandatePrepared, type SignerMandate, type Tone, type VenueMandate } from '../derivatives/live';20import { Bullet, LiveErrNotice, LiveStatePill, ModeBand, ModeTag, SourceTag } from '../derivatives/LiveBits';21import type { Fill, Position } from '../derivatives/types';22import { I18n, useT, type Key } from '../i18n';23import { Btn, Card, Field, H, Icon, inputCls, Notice, Skeleton, when } from '../ui';2425// ------------------------------------------------------------------------------------------------ shapes of the server's answers26export interface AgentLive { available: boolean; reason: string | null; active: boolean; accountId: string | null; network: LiveNetwork | null; mandateId: string | null; strategySha256: string | null; since: number | null; capital: string | null;27 paused: boolean; exit: { ts: number; code: string; reason: string } | null; account: LiveDetail | null; signer: SignerMandate | null; holds: { ts: number; bar: string; symbol: string; code: string; reason: string }[] }28interface ReadyAccount { id: string; network: LiveNetwork; label: string; account: string; mode: string; modes: string[]; tradable: boolean; authorization: { active: boolean; expiresAt: number } | null; mandate: { id: string; phase: string; agentId: string } | null;29 usedBy: { agentId: string; name: string } | null; equity: string | null; error: string | null }30interface Proposal { capital: string; days: number; maxDays: number; body: MandateBody; derived: { field: string; value: string; from: string }[]; notes: string[] }31export interface Readiness { agentId: string; locale: LiveLocale; config: LiveConfig; accounts: ReadyAccount[]; accountId: string | null;32 steps: { strategy: { ok: boolean; versionId: string | null; sha256: string | null; name: string | null; version: string | null; reason: string | null };33 account: { ok: boolean; code: string | null; reason: string | null; network: LiveNetwork | null; authorization: { active: boolean; expiresAt: number; maxMandateDays: number } | null; instrumentsMissing: string[] };34 funded: { ok: boolean; code: string | null; equity: string | null; minEquity: string; basis: { instruments: { instrument: string; minOrder: string }[]; ordersPerPosition: number; grossX: number; floor: string }; reason: string | null };35 mandate: { ok: boolean; phase: string | null; mandateId: string | null; lock: { code: string; reason: string } | null } };36 next: 'strategy' | 'account' | 'fund' | 'mandate' | 'live' | 'locked'; proposal: Proposal | null; ts: number }3738// ------------------------------------------------------------------------------------------------ codes → sentences (unit-tested: every code has a French and an English sentence)39/** every refusal code of the signer under a mandate (docs/v6/DERIVATIVE-MANDATE.md §2), relayed by the API as `signer_<code>` */40export const SIGNER_CODES = ['not_found', 'no_authorization', 'revoked', 'expired', 'wind_down', 'halted', 'bad_request', 'wrong_venue', 'rejected', 'unsupported', 'policy', 'expired_action', 'stale_nonce', 'replay', 'mismatch', 'instrument',41 'order_notional', 'price', 'no_price', 'orders_day', 'orders_hour', 'turnover', 'venue_unreachable', 'venue_stale', 'venue_inconsistent', 'unpriced_position', 'floor_equity', 'leverage_setting', 'position_exceeded', 'gross_exceeded',42 'leverage_exceeded', 'leverage_rate', 'margin_mode'] as const;43/** the live executor's own reasons for not sending an order now */44export const EXECUTOR_CODES = ['live_cooldown', 'live_unknown_pending', 'floor_guard', 'leverage_not_set', 'venue_rejected', 'never_placed'] as const;45const OTHER_REFUSALS = ['signer_unreachable', 'rejected_earlier', 'withdrawn', 'not_signed', 'rejected', 'mandate_floor_equity', 'mandate_wind_down', 'mandate_none', 'halted'];46/** why an agent left real mode (`live.exit.code`) */47export const EXIT_CODES = ['owner_stop', 'mandate_revoked', 'mandate_expired', 'mandate_ended', 'authorization_ended', 'mainnet_disabled', 'mandate_mainnet_disabled', 'halted', 'divergence', 'manual_order', 'liquidation', 'strategy_changed',48 'loss_limit', 'engine_stop', 'signer_revoked', 'signer_expired', 'signer_wind_down', 'signer_no_authorization', 'signer_halted', 'signer_not_found'] as const;49const REFUSALS = new Set<string>([...SIGNER_CODES.map((c) => `signer_${c}`), ...EXECUTOR_CODES, ...OTHER_REFUSALS]), EXITS = new Set<string>(EXIT_CODES);50/** our sentence for a held or refused order: what happened and what to do */51export const refusalKey = (code: string): Key => (REFUSALS.has(code) ? `plR_${code}` : RISK_CODES.has(code) ? `rk_${code}` : code.startsWith('signer_') ? 'plR_signer_other' : 'plR_other') as Key;52export const exitKey = (code: string): Key => (EXITS.has(code) ? `plX_${code}` : 'plX_other') as Key;53/** codes that only mean "wait": shown as information, not as a failure */54const WAITS = new Set(['live_cooldown', 'live_unknown_pending', 'signer_position_exceeded', 'signer_gross_exceeded', 'signer_orders_hour', 'signer_orders_day', 'signer_turnover', 'floor_guard']);5556/** which money the agent moves. A live agent whose network is not known to be the testnet is shown as real money. */57export const agentTone = (a: { mode?: string; live?: { network?: string | null } | null; liveNetwork?: string | null }): Tone => (a.mode !== 'LIVE' ? 'paper' : (a.live?.network ?? a.liveNetwork) === 'testnet' ? 'testnet' : 'mainnet');58const sym = (instrument: string) => instrument.slice(instrument.indexOf(':') + 1);59const short = (h: string | null | undefined) => (h ? `${h.slice(0, 12)}…${h.slice(-6)}` : '—');60/** « 23 h 59 min », « 4 min » */61const span = (ms: number) => { const m = Math.max(0, Math.floor(ms / 60_000)); return m >= 60 ? `${Math.floor(m / 60)} h ${String(m % 60).padStart(2, '0')} min` : `${m} min`; };62function useNow(everyMs = 30_000) { const [n, setN] = useState(() => Date.now()); useEffect(() => { const h = setInterval(() => setN(Date.now()), everyMs); return () => clearInterval(h); }, [everyMs]); return n; }6364// ------------------------------------------------------------------------------------------------ band and state6566/** the permanent band of a LIVE agent: never dismissible, stays on screen while the page scrolls */67export function AgentModeBand({ a }: { a: any }) {68 const t = useT(); const tone = agentTone(a), L = a.live as AgentLive | undefined;69 // only the band itself stays on screen; the sentence under it scrolls away with the page70 return <><div className="sticky top-1 z-20" data-testid="agent-band" data-tone={tone}><ModeBand tone={tone} label={L?.account?.label ?? undefined} className="shadow-md" /></div>71 <p className="-mt-2 text-[12.5px] leading-snug text-mute">{a.state === 'STOPPING' ? '' : t(L?.active ? 'plBandLive' : 'plBandEnded')} <span data-testid="pl-signal-note">{t(tone === 'testnet' ? 'plSignalNote' : 'plSignalNoteReal')}</span></p></>;72}73const ST_CLS: Record<string, string> = { LIVE: 'text-emerald bg-emerald/10', PAUSED: 'text-warn bg-warn/12', STOPPING: 'text-warn bg-warn/12', STOPPED: 'text-mute bg-elev' };74/** the state of an agent in real mode — it never says « réel » by itself: the money is named by the tag beside it */75export function PerpLiveState({ a }: { a: { state: string; mode?: string; live?: { network?: string | null } | null; liveNetwork?: string | null } }) {76 const t = useT();77 return <span className="inline-flex shrink-0 flex-wrap items-center justify-end gap-1.5" data-testid="agent-live-state" data-state={a.state}>78 <span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-semibold ${ST_CLS[a.state] ?? 'text-mute bg-elev'}`}><Icon n={a.state === 'LIVE' ? 'check' : a.state === 'STOPPED' ? 'x' : 'clock'} className="h-3 w-3" />{t(`plSt_${a.state}` as Key)}</span>79 <ModeTag tone={agentTone(a)} /></span>;80}8182/** a server setting, shown as such: there is no control here that could change it */83function ServerLock({ title, reason }: { title: string; reason: string }) {84 const t = useT();85 return <div role="note" data-testid="server-lock" className="rounded-2xl border-2 border-dashed border-line bg-elev/60 p-3.5 text-[13px] leading-snug">86 <div className="flex items-start gap-2"><Icon n="lock" className="mt-0.5 h-4 w-4 shrink-0" /><b className="font-semibold">{title}</b></div>87 <p className="mt-1.5">{t('plLockBody')}</p>88 <p className="mt-1.5 text-[12px] text-mute">{t('plLockSays')}</p><p className="break-words rounded-lg bg-surface/70 px-2 py-1.5 font-mono text-[11.5px]" data-testid="server-lock-reason">{reason}</p></div>;89}9091// ------------------------------------------------------------------------------------------------ go live: the checklist9293type StepState = 'done' | 'next' | 'later' | 'locked';94function Step({ n, id, state, title, children }: { n: number; id: string; state: StepState; title: string; children: React.ReactNode }) {95 const t = useT();96 const pill = state === 'done' ? 'bg-emerald/10 text-emerald' : state === 'next' ? 'bg-blue/10 text-blue' : 'bg-elev text-mute';97 return <li data-testid={`pl-step-${id}`} data-state={state} className={`flex gap-3 rounded-2xl border p-3.5 ${state === 'next' ? 'border-blue/50' : 'border-line'} ${state === 'later' ? 'opacity-70' : ''}`}>98 <span className={`grid h-8 w-8 shrink-0 place-items-center rounded-full text-[13px] font-bold ${state === 'done' ? 'bg-emerald text-white' : state === 'locked' ? 'bg-elev text-mute' : state === 'next' ? 'bg-blue text-white' : 'bg-elev text-mute'}`} aria-hidden="true">99 {state === 'done' ? <Icon n="check" className="h-4 w-4" /> : state === 'locked' ? <Icon n="lock" className="h-4 w-4" /> : n}</span>100 <div className="min-w-0 flex-1"><div className="flex flex-wrap items-center justify-between gap-x-2 gap-y-1"><span className="text-[15px] font-semibold">{title}</span>101 <span className={`rounded-full px-2 py-0.5 text-[11.5px] font-semibold ${pill}`}>{t(state === 'done' ? 'plStepDone' : state === 'next' ? 'plStepNext' : state === 'locked' ? 'plStepLocked' : 'plStepLater')}</span></div>102 <div className="mt-1 space-y-2 text-[13px] leading-snug text-mute">{children}</div></div></li>;103}104105export function GoLiveCard({ a, id, nav }: { a: any; id: string; nav: Nav }) {106 const t = useT(); const { locale } = useContext(I18n); const qc = useQueryClient();107 const L = a.live as AgentLive;108 const [accountId, setAccountId] = useState<string | null>(null), [sheet, setSheet] = useState(false);109 const q = useQuery({ queryKey: ['agent', id, 'live-ready', accountId, locale], enabled: !!L?.available, retry: 0, refetchInterval: sheet ? false : 15_000,110 queryFn: () => get<Readiness>(`/agents/${id}/perp/live?locale=${locale}${accountId ? `&account=${encodeURIComponent(accountId)}` : ''}`) });111 if (!L?.available) return <div className="rounded-2xl border border-dashed border-line p-4" data-testid="pl-golive" data-next="unavailable"><div className="text-[16px] font-semibold">{t('plTitle')}</div>112 <p className="mt-2 text-[13px] leading-snug text-mute">{t('plUnavailable')}</p>{L?.reason && <p className="mt-1 break-words font-mono text-[11.5px] text-mute">{L.reason}</p>}</div>;113 const r = q.data, s = r?.steps, sel = r?.accounts.find((x) => x.id === r.accountId) ?? null;114 const order = ['strategy', 'account', 'fund', 'mandate'] as const, at = r ? (r.next === 'locked' || r.next === 'live' ? 3 : order.indexOf(r.next)) : -1;115 const st = (k: number, ok: boolean): StepState => (ok ? 'done' : k === at ? (r?.next === 'locked' ? 'locked' : 'next') : 'later');116 const accounts = () => nav({ tab: 'futures', view: 'accounts' });117 const tone: Tone = sel ? (sel.network === 'testnet' ? 'testnet' : 'mainnet') : 'testnet';118 const markets = s ? s.funded.basis.instruments.map((i) => sym(i.instrument)).join(', ') : '';119 return <div className="rounded-2xl border border-line p-4" data-testid="pl-golive" data-next={r?.next ?? 'loading'}>120 <div className="flex items-center justify-between gap-3"><span className="text-[16px] font-semibold">{t('paGoLive')}</span>{sel && <ModeTag tone={tone} />}</div>121 <p className="mt-1.5 text-[13px] leading-snug text-mute">{t('plIntro')}</p>122 {q.isError ? <div className="mt-3"><LiveErrNotice err={liveError(q.error)} /></div> : !r || !s ? <div className="mt-3"><Skeleton h="h-40" /></div> : <>123 {r.accounts.length > 1 && <label className="mt-3 block text-[12px] text-mute">{t('plAccountPick')}124 <select data-testid="pl-account" value={r.accountId ?? ''} onChange={(e) => setAccountId(e.target.value)} className="mt-1 min-h-11 w-full rounded-xl border border-line bg-surface px-3 text-[14px] text-ink">125 {r.accounts.map((x) => <option key={x.id} value={x.id}>{x.label} — {t(x.network === 'testnet' ? 'plNetTestnet' : 'plNetMainnet')}</option>)}</select></label>}126 <ol className="mt-3 space-y-2.5">127 <Step n={1} id="strategy" state={st(0, s.strategy.ok)} title={t('plStepStrategy')}>128 {s.strategy.ok ? <p className="break-words">{t('plStrategyOk', { name: s.strategy.name ?? '', version: s.strategy.version ?? '', sha: short(s.strategy.sha256) })}</p> : <p>{t('plStrategyNo')}</p>}</Step>129 <Step n={2} id="account" state={st(1, s.account.ok)} title={t('plStepAccount')}>130 {s.account.ok && sel && s.account.authorization ? <p>{t('plAccountOk', { label: sel.label, date: when(new Date(s.account.authorization.expiresAt), locale) })} <span className="font-semibold text-ink">{t(sel.network === 'testnet' ? 'plNetTestnet' : 'plNetMainnet')}</span></p>131 : <><p data-testid="pl-account-why" data-code={s.account.code ?? ''}>{t(`plAcc_${s.account.code ?? 'no_account'}` as Key, { list: s.account.instrumentsMissing.map(sym).join(', ') })}</p>132 {s.account.code === 'mainnet_disabled' && s.account.reason && <p className="break-words font-mono text-[11.5px]">{t('plLockSays')} {s.account.reason}</p>}133 {/* an account that never deposited cannot even be authorised: said here, where the owner is stopped */}134 {s.account.code === 'read_only' && s.funded.code === 'unfunded' && <p className="font-medium text-warn" data-testid="pl-unfunded-auth">{t('plFund_unfunded')}</p>}</>}</Step>135 <Step n={3} id="funded" state={st(2, s.funded.ok)} title={t('plStepFunded')}>136 {s.funded.ok ? <p>{t('plFundedOk', { equity: fmt(s.funded.equity, { locale, dp: 2 }), min: fmt(s.funded.minEquity, { locale, trim: true }) })} <SourceTag /></p>137 : s.account.ok || s.funded.code === 'unfunded' ? <p data-testid="pl-funded-why" data-code={s.funded.code ?? ''}>{t(`plFund_${s.funded.code ?? 'unfunded'}` as Key, { equity: fmt(s.funded.equity, { locale, dp: 2 }), min: fmt(s.funded.minEquity, { locale, trim: true }) })}</p> : null}138 <p className="text-[12px]" data-testid="pl-fund-why">{t('plFundWhy', { min: fmt(s.funded.minEquity, { locale, trim: true }), n: s.funded.basis.ordersPerPosition, markets, gross: fmt(String(s.funded.basis.grossX), { locale, trim: true }) })}</p>139 {sel?.network === 'testnet' && !s.funded.ok && <p className="text-[12px]">{t('plFundTestnetNote')}</p>}</Step>140 <Step n={4} id="mandate" state={st(3, s.mandate.ok)} title={t('plStepMandate')}>141 {s.mandate.lock ? <ServerLock title={t('plLockTitle')} reason={s.mandate.lock.reason || r.config.reasons?.[s.mandate.lock.code]?.[locale] || s.mandate.lock.code} />142 : <p>{t('plMandateTodo')}</p>}</Step>143 </ol>144 <div className="mt-3">145 {r.next === 'strategy' && <Btn full data-testid="pl-next" onClick={() => document.getElementById('pl-strategy')?.scrollIntoView({ behavior: 'smooth', block: 'start' })}>{t('plGoStrategy')}</Btn>}146 {r.next === 'account' && <Btn full data-testid="pl-next" onClick={accounts}><Icon n="chev" className="h-4 w-4" />{t('plGoAccounts')}</Btn>}147 {r.next === 'fund' && <Btn full data-testid="pl-next" onClick={accounts}><Icon n="down" className="h-4 w-4" />{t('plGoFund')}</Btn>}148 {r.next === 'mandate' && <Btn full data-testid="pl-next" kind={tone === 'mainnet' ? 'danger' : 'primary'} onClick={() => setSheet(true)}><Icon n="key" className="h-4 w-4" />{t('plGoMandate')}</Btn>}149 </div>150 <p className="mt-3 text-[12px] leading-snug text-mute">{t('plManualWarn')}</p>151 {sheet && sel && r.proposal && <MandateSheet id={id} r={r} account={sel} onClose={() => { setSheet(false); void qc.invalidateQueries({ queryKey: ['agent', id] }); void qc.invalidateQueries({ queryKey: ['agents'] }); void qc.invalidateQueries({ queryKey: ['deriv', 'live'] }); }} />}152 </>}153 </div>;154}155156// ------------------------------------------------------------------------------------------------ the mandate: review, ONE passkey157158function MandateRows({ m }: { m: VenueMandate }) {159 const t = useT(); const { locale } = useContext(I18n);160 return <div data-testid="mandate-limits">161 {m.instruments.map((i) => <Row key={i.instrument} label={t('plShInstrument', { symbol: sym(i.instrument) })} strong>{i.maxLeverage}× / {usd(i.maxPositionNotional, locale)}</Row>)}162 <Row label={t('plShOrder')}>{usd(m.maxOrderNotional, locale)}</Row><Row label={t('plShGross')}>{usd(m.maxGrossExposure, locale)}</Row>163 <Row label={t('plShPerDay')}>{m.maxOrdersPerDay}</Row><Row label={t('plShPerHour')}>{m.maxOrdersPerHour}</Row><Row label={t('plShTurnover')}>{usd(m.maxDailyTurnover, locale)}</Row>164 <Row label={t('plShFloor')} strong>{usd(m.floorEquity, locale)}</Row><Row label={t('plShBand')}>{m.maxPriceDeviationBps} pb</Row>165 <Row label={t('plShExpires')} strong>{when(new Date(m.expiresAt * 1000), locale)}</Row></div>;166}167168type MPhase = 'form' | 'preparing' | 'review' | 'passkey' | 'sending' | 'done' | 'void';169function MandateSheet({ id, r, account, onClose }: { id: string; r: Readiness; account: ReadyAccount; onClose: () => void }) {170 const t = useT(); const { locale } = useContext(I18n);171 const p0 = r.proposal!, tone: Tone = account.network === 'testnet' ? 'testnet' : 'mainnet';172 const [days, setDays] = useState(String(p0.days));173 const [phase, setPhase] = useState<MPhase>('form'), [prep, setPrep] = useState<(MandatePrepared & { proposal: Proposal }) | null>(null), [read, setRead] = useState(false);174 const [err, setErr] = useState<LiveErr | null>(null), [integrity, setIntegrity] = useState<string | null>(null);175 const lock = useRef(false), busy = phase === 'preparing' || phase === 'passkey' || phase === 'sending';176 const nDays = /^\d{1,2}$/.test(days.trim()) ? Number(days.trim()) : 0, daysOk = nDays >= 1 && nDays <= p0.maxDays;177 const once = async (f: () => Promise<void>) => { if (lock.current) return; lock.current = true; try { await f(); } finally { lock.current = false; } };178 const prepare = () => once(async () => {179 setPhase('preparing'); setErr(null); setIntegrity(null); setRead(false);180 try {181 const p = await post<MandatePrepared & { proposal: Proposal }>(`/agents/${id}/perp/live/prepare`, { accountId: account.id, days: nDays, locale });182 // what the passkey will sign must be THIS agent, THIS strategy version, THIS account's network, and the text on screen183 const why = p.mandate.agentId !== id ? 'agent' : p.mandate.strategySha256 !== r.steps.strategy.sha256 ? 'strategy' : p.mandate.network !== account.network ? 'network'184 : p.acknowledgement.sha256 !== p.mandate.ackSha256 || (await sha256Hex(p.acknowledgement.text)) !== p.mandate.ackSha256 ? 'ack' : null;185 setIntegrity(why); setPrep(p); setPhase('review');186 } catch (e) { setErr(liveError(e, 'propose')); setPhase('form'); }187 });188 const approve = () => once(async () => {189 if (phase !== 'review' || !prep || !read || integrity) return;190 setPhase('passkey'); setErr(null);191 let assertion: unknown;192 try { assertion = await startAuthentication({ optionsJSON: prep.options }); } catch (e) { setErr(liveError(e, 'approve')); setPhase('void'); return; }193 setPhase('sending');194 try { await post(`/agents/${id}/perp/live/approve`, { accountId: account.id, assertion, acknowledged: true }); setPhase('done'); }195 catch (e) { setErr(liveError(e, 'approve')); setPhase('void'); } // a refused approval consumes the preparation196 });197 const m = prep?.mandate, lockCode = err && (err.code === 'mandate_mainnet_disabled' || err.code === 'mainnet_disabled') ? err.code : null;198 const list = (title: string, items: string[], icon: string, cls: string, testid: string) => <section><h3 className="mb-1.5 text-[14px] font-semibold">{title}</h3>199 <ul className="space-y-1.5 text-[13px] leading-snug" data-testid={testid}>{items.map((x) => <Bullet key={x} icon={icon} tone={cls}>{x}</Bullet>)}</ul></section>;200 return <Sheet title={t('plShTitle')} onClose={() => { if (!busy) onClose(); }} wide>201 <div className="space-y-3.5" data-testid="mandate-sheet" data-phase={phase}>202 <ModeBand tone={tone} label={account.label} />203 {(phase === 'form' || phase === 'preparing') && <>204 <div className="rounded-2xl bg-elev/60 p-3 text-[13px] leading-snug" data-testid="mandate-capital"><div className="mb-0.5 text-[12px] font-semibold text-mute">{t('plShCapital')}</div>{t('plShCapitalIs', { capital: fmt(p0.capital, { locale, dp: 2 }) })}</div>205 <Field label={t('plShDays')} hint={t('plShDaysHint', { max: p0.maxDays })}><div className="flex items-center gap-2">206 <input className={inputCls} inputMode="numeric" aria-label={t('plShDays')} value={days} disabled={busy} onChange={(e) => setDays(e.target.value.replace(/[^0-9]/g, ''))} /><span className="w-12 shrink-0 text-[13px] text-mute">{t('plShDaysUnit')}</span></div></Field>207 {!daysOk && <Notice tone="warn"><div data-testid="mandate-problem">{t('plShBadDays', { max: p0.maxDays })}</div></Notice>}208 {lockCode ? <ServerLock title={t('plLockTitle')} reason={r.config.reasons?.[lockCode]?.[locale] ?? err!.detail} /> : err && <LiveErrNotice err={err} />}209 <Btn full busy={phase === 'preparing'} disabled={!daysOk} onClick={() => void prepare()} data-testid="mandate-continue">{t('plShContinue')}</Btn>210 <Btn full kind="ghost" disabled={busy} onClick={onClose}>{t('cancel')}</Btn>211 </>}212213 {prep && m && (phase === 'review' || phase === 'passkey' || phase === 'sending' || phase === 'void') && <div className={`space-y-3.5 ${phase === 'void' ? 'opacity-60' : ''}`}>214 <Notice tone="warn"><div data-testid="mandate-warning"><b className="font-semibold">{t('plShWarning')}</b><span className="mt-0.5 block">{prep.warning}</span></div></Notice>215 <section><h3 className="mb-1.5 text-[14px] font-semibold">1. {t('plShLimits')}</h3>216 <div className="rounded-2xl border border-line p-3">217 <Row label={t('plShNetwork')} strong><span data-testid="mandate-network">{t(m.network === 'testnet' ? 'plNetTestnet' : 'plNetMainnet')}</span></Row>218 <Row label={t('plShAccount')}><span className="break-all font-mono text-[11px]">{m.account}</span></Row>219 <Row label={t('plShStrategy')}><span className="break-all font-mono text-[11px]" data-testid="mandate-sha">{m.strategySha256}</span></Row>220 <Row label={t('plShAgent')}><span className="break-all font-mono text-[11px]">{m.agentId}</span></Row>221 <MandateRows m={m} />222 <Row label={t('plShAuthExpires')}>{when(new Date(prep.authorization.expiresAt), locale)}</Row></div>223 <p className="mt-1.5 text-[12px] leading-snug text-mute">{t('plShFloorNote')}</p>224 {prep.proposal.notes.length > 0 && <p className="mt-1 text-[12px] font-medium leading-snug text-warn">{t('plShTighter')}</p>}225 <details className="mt-1.5 rounded-2xl bg-elev/60 p-3 text-[12.5px]"><summary className="cursor-pointer font-semibold">{t('plShDerived')}</summary>226 <ul className="mt-2 space-y-1.5" data-testid="mandate-derived">{prep.proposal.derived.map((x) => { const k = t(`plD_${x.field}` as Key); return <li key={x.field}><span className="font-mono text-[11.5px]">{x.field} = {x.value}</span><span className="block text-mute">{k.startsWith('plD') || k.startsWith('lD') ? x.from : k}</span></li>; })}</ul></details></section>227 <section aria-label={t('flAckTitle')}><h3 className="mb-1.5 text-[14px] font-semibold">2. {t('flAckTitle')}</h3>228 <div tabIndex={0} className="max-h-[34dvh] overflow-y-auto whitespace-pre-wrap rounded-2xl border border-line bg-elev/50 p-3.5 text-[13px] leading-relaxed" data-testid="ack-text">{prep.acknowledgement.text}</div>229 <p className="mt-1 break-all font-mono text-[10.5px] text-mute">SHA-256 {prep.acknowledgement.sha256} · {prep.acknowledgement.version}</p>230 <label className="mt-2 flex min-h-11 cursor-pointer items-center gap-2.5 text-[14px] font-medium"><input type="checkbox" checked={read} disabled={phase !== 'review'} onChange={(e) => setRead(e.target.checked)} className="h-5 w-5 shrink-0 accent-[var(--blue)]" data-testid="ack-check" /><span>{t('flAckCheck')}</span></label>231 <p className="text-[12px] leading-snug text-mute">{t('plShAckSigned')}</p></section>232 {list(`3. ${t('plShEnforced')}`, prep.enforcedBySigner, 'shield', 'text-emerald', 'mandate-enforced')}233 {list(`4. ${t('plShTrusted')}`, prep.trustedBySigner, 'eye', 'text-mute', 'mandate-trusted')}234 {list(`5. ${t('plShNotEnforced')}`, prep.notEnforced, 'warn', 'text-warn', 'mandate-not-enforced')}235 <section><h3 className="mb-1.5 text-[14px] font-semibold">6. {t('plShWindDown')}</h3><p className="text-[13px] leading-snug" data-testid="mandate-winddown">{prep.windDown.text}</p></section>236 <section><h3 className="mb-1.5 text-[14px] font-semibold">7. {t('plShSigned')}</h3><p className="text-[13px] leading-snug">{prep.whatIsSigned.byPasskey}</p>237 <p className="mt-1 break-all font-mono text-[10.5px] text-mute">{t('fingerprint')} SHA-256 {prep.hash}</p></section>238 </div>}239 {integrity && phase !== 'form' && <Notice tone="danger"><div data-testid="mandate-integrity">{t(`plShInt_${integrity}` as Key)}</div></Notice>}240 {phase === 'passkey' && <Notice tone="info">{t('flPasskeyWaiting')}</Notice>}241 {phase === 'sending' && <p className="flex items-center gap-2 text-[13.5px] text-mute"><Icon n="spin" className="h-4 w-4" />{t('plShSending')}</p>}242 {phase !== 'form' && phase !== 'preparing' && (lockCode ? <ServerLock title={t('plLockTitle')} reason={r.config.reasons?.[lockCode]?.[locale] ?? err!.detail} /> : err && <LiveErrNotice err={err} tone={err.key === 'passkey_cancelled' ? 'warn' : 'danger'}>243 {phase === 'void' && <span className="mt-1 block">{t('plShConsumed')}</span>}</LiveErrNotice>)}244 {phase === 'done' && <Notice tone="ok"><div data-testid="mandate-done">{t('plShDone')}</div></Notice>}245246 {(phase === 'review' || phase === 'passkey' || phase === 'sending') && <>247 <Btn full kind={tone === 'mainnet' ? 'danger' : 'primary'} busy={phase !== 'review'} disabled={!read || !!integrity} onClick={() => void approve()} data-testid="mandate-approve"><Icon n="key" className="h-4 w-4" />{t(tone === 'mainnet' ? 'plShApproveReal' : 'plShApproveTestnet')}</Btn>248 <p className="text-center text-[12px] text-mute">{read ? t('plShOne') : t('flAckNeeded')}</p>249 <Btn full kind="ghost" disabled={busy} onClick={() => { setPhase('form'); setErr(null); setPrep(null); }}>{t('plShBack')}</Btn></>}250 {phase === 'void' && <Btn full kind="soft" data-testid="mandate-again" onClick={() => { setPhase('form'); setPrep(null); }}>{t('plShAgain')}</Btn>}251 {(phase === 'done' || phase === 'void') && <Btn full kind={phase === 'done' ? 'soft' : 'ghost'} onClick={onClose} data-testid="mandate-close">{t('fxClose')}</Btn>}252 </div>253 </Sheet>;254}255256// ------------------------------------------------------------------------------------------------ LIVE: the venue account, the mandate, holds, controls257258/** a held or refused order in plain words: ours first, then the server's own */259export function Refusal({ code, reason }: { code: string; reason?: string }) {260 const t = useT();261 return <span data-testid="pl-refusal" data-code={code}>{t(refusalKey(code))}262 <span className="mt-1 block break-words font-mono text-[11px] text-mute">{t('plServerDetail')} {code}{reason ? ` — ${reason}` : ''}</span></span>;263}264265function MandateCard({ a }: { a: any }) {266 const t = useT(); const { locale } = useContext(I18n); const now = useNow();267 const L = a.live as AgentLive, ms = L.account?.mandate ?? null, sg = L.signer, m = ms?.limits ?? sg?.mandate ?? null;268 if (!m) return null;269 const phase = ms?.phase ?? sg?.phase ?? 'ended', until = ms?.windDownUntil ?? (sg?.windDownUntil ? sg.windDownUntil * 1000 : null), equity = L.account?.venueState.state?.equity ?? null;270 const bar = (used: number, max: number) => <span className="mt-1 block h-1.5 overflow-hidden rounded-full bg-elev"><span className={`block h-full rounded-full ${used >= max ? 'bg-danger' : used >= 0.8 * max ? 'bg-warn' : 'bg-blue'}`} style={{ width: `${Math.max(0, Math.min(100, max > 0 ? (used / max) * 100 : 0))}%` }} /></span>;271 return <Card><div data-testid="pl-mandate" data-phase={phase}>272 <H right={<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-semibold ${phase === 'active' ? 'bg-emerald/10 text-emerald' : phase === 'wind_down' ? 'bg-warn/12 text-warn' : 'bg-elev text-mute'}`} data-testid="pl-mandate-phase">273 <Icon n={phase === 'active' ? 'key' : phase === 'wind_down' ? 'clock' : 'lock'} className="h-3 w-3" />{t(`plPhase_${phase}` as Key)}</span>}>{t('plMandate')}</H>274 {phase === 'wind_down' && <Notice tone="warn"><div data-testid="pl-winddown"><b className="font-semibold">{until ? t('plWindDownLeft', { time: span(until - now) }) : t('plPhase_wind_down')}</b><span className="mt-0.5 block">{t('plWindDownText')}</span></div></Notice>}275 <div className={phase === 'wind_down' ? 'mt-3' : ''}>276 <Row label={t('plShNetwork')} strong>{t(m.network === 'testnet' ? 'plNetTestnet' : 'plNetMainnet')}</Row>277 <Row label={t('plShStrategy')}><span className="break-all font-mono text-[11px]">{short(m.strategySha256)}</span></Row>278 <MandateRows m={m} /></div>279 {equity && <p className="mt-2 text-[12.5px] text-mute" data-testid="pl-floor">{t('plFloorNow', { floor: fmt(m.floorEquity, { locale, dp: 2 }), equity: fmt(equity, { locale, dp: 2 }) })}</p>}280 <div className="mt-3 rounded-2xl bg-elev/60 p-3" data-testid="pl-usage"><div className="mb-1.5 text-[12.5px] font-semibold">{t('plUsage')}</div>281 {!sg ? <p className="text-[12.5px] text-mute">{t('plUsageNone')}</p> : <div className="grid grid-cols-1 gap-x-5 gap-y-2.5 text-[13px] sm:grid-cols-3">282 <div><div className="flex justify-between gap-2"><span className="text-mute">{t('plUsageOrders')}</span><span className="font-semibold">{sg.today.orders} / {m.maxOrdersPerDay}</span></div>{bar(sg.today.orders, m.maxOrdersPerDay)}</div>283 <div><div className="flex justify-between gap-2"><span className="text-mute">{t('plUsageHour')}</span><span className="font-semibold">{sg.ordersLastHour} / {m.maxOrdersPerHour}</span></div>{bar(sg.ordersLastHour, m.maxOrdersPerHour)}</div>284 <div><div className="flex justify-between gap-2"><span className="text-mute">{t('plUsageTurnover')}</span><span className="font-semibold">{usd(sg.today.turnover, locale)} / {usd(m.maxDailyTurnover, locale)}</span></div>{bar(Number(sg.today.turnover), Number(m.maxDailyTurnover))}</div>285 <div className="flex justify-between gap-2 sm:col-span-2"><span className="text-mute">{t('plUsageReduce')}</span><span className="font-semibold">{sg.today.reduceOnly}</span></div>286 <div className="flex justify-between gap-2"><span className="text-mute">{t('plUsageLeverage')}</span><span className="font-semibold">{sg.today.leverage} / 20</span></div></div>}287 </div></div></Card>;288}289290function StopSheet({ a, id, onClose }: { a: any; id: string; onClose: () => void }) {291 const t = useT(); const { locale } = useContext(I18n); const qc = useQueryClient();292 const L = a.live as AgentLive, n = L.account?.venueState.error ? null : L.account?.venueState.positions.length ?? null;293 const [busy, setBusy] = useState(false), [err, setErr] = useState<LiveErr | null>(null), [done, setDone] = useState<{ windDown: { until: number | null; text: string; openPositions: number | null; openOrders: number | null } | null; result?: string } | null>(null);294 const lock = useRef(false);295 const stop = async () => {296 if (lock.current) return; lock.current = true; setBusy(true); setErr(null);297 try { setDone(await post(`/agents/${id}/perp/live/stop`, { locale })); } catch (e) { setErr(liveError(e)); }298 finally { lock.current = false; setBusy(false); void qc.invalidateQueries({ queryKey: ['agent', id] }); void qc.invalidateQueries({ queryKey: ['agents'] }); void qc.invalidateQueries({ queryKey: ['deriv', 'live'] }); }299 };300 return <Sheet title={t('plStopTitle')} onClose={() => { if (!busy) onClose(); }}><div className="space-y-3" data-testid="pl-stop-sheet">301 <ModeBand tone={agentTone(a)} label={L.account?.label} />302 {!done ? <>303 <Notice tone="warn">{t('plStopBody')}</Notice>304 {n !== null && <p className="text-[13.5px]">{t('plStopOpen', { n })}</p>}305 <p className="text-[12.5px] leading-snug text-mute">{t('plManual')}</p>306 {err && <LiveErrNotice err={err} />}307 <Btn full kind="danger" busy={busy} onClick={() => void stop()} data-testid="pl-stop-confirm"><Icon n="stop" className="h-4 w-4" />{t('plStopConfirm')}</Btn>308 <Btn full kind="ghost" disabled={busy} onClick={onClose}>{t('cancel')}</Btn></> : <div className="space-y-3" data-testid="pl-stop-done">309 <Notice tone="ok">{t('plStopping')}</Notice>310 {done.windDown && <p className="text-[13px] leading-snug">{done.windDown.text}</p>}311 {done.result && <p className="break-words font-mono text-[11.5px] text-mute" data-testid="pl-stop-result">{t('plStopResult')} {done.result}</p>}312 <Btn full kind="soft" onClick={onClose}>{t('fxClose')}</Btn></div>}313 </div></Sheet>;314}315316type ATab = 'orders' | 'fills' | 'funding' | 'events';317/** everything of the page that belongs to the real mode: status, venue account, mandate, holds, controls, activity */318export function LivePanel({ a, id, nav }: { a: any; id: string; nav: Nav }) {319 const t = useT(); const { locale } = useContext(I18n); const qc = useQueryClient(); const now = useNow();320 const L = a.live as AgentLive, acct = L.account, vs = acct?.venueState, state = vs?.state ?? null, tone = agentTone(a);321 const [busy, setBusy] = useState(''), [err, setErr] = useState<LiveErr | null>(null), [stop, setStop] = useState(false), [tab, setTab] = useState<ATab>('orders');322 const act = async (name: string, path: string) => { setBusy(name); setErr(null); try { await post(`/agents/${id}/perp/live/${path}`, {}); } catch (e) { setErr(liveError(e)); } finally { setBusy(''); void qc.invalidateQueries({ queryKey: ['agent', id] }); void qc.invalidateQueries({ queryKey: ['agents'] }); } };323 const px = (x: string | null | undefined) => fmt(x, { locale, trim: true });324 const result = state && L.capital ? D.sub(D.from(state.equity), D.from(L.capital)) : null;325 const eq = (acct?.equity ?? []).map((e) => ({ t: e.ts, p: Number(e.equity) }));326 const until = acct?.mandate?.windDownUntil ?? (L.signer?.windDownUntil ? L.signer.windDownUntil * 1000 : null);327 const posCols: Col<Position>[] = [328 { id: 'i', label: t('paColInst'), head: true, cell: (p) => <span className="font-semibold">{sym(p.instrument)}</span> }, { id: 's', label: t('paColSide'), head: true, cell: (p) => <SideTag side={p.side} /> },329 { id: 'z', label: t('paColSize'), num: true, cell: (p) => px(p.size) }, { id: 'e', label: t('paColEntry'), num: true, cell: (p) => px(p.entryPrice) }, { id: 'm', label: t('paColMark'), num: true, cell: (p) => px(p.markPrice) },330 { id: 'l', label: t('paColLev'), num: true, cell: (p) => `${px(p.leverage)}×` }, { id: 'q', label: t('paColLiq'), num: true, cell: (p) => (p.liquidationPrice ? px(p.liquidationPrice) : <span className="text-mute">—</span>) },331 { id: 'u', label: t('paColPnl'), num: true, cell: (p) => <Pnl value={p.unrealizedPnl} locale={locale} /> }];332 const ordCols: Col<LiveOrder>[] = [333 { id: 't', label: t('paColTime'), cell: (o) => timeOf(o.createdAt, locale, true) }, { id: 'i', label: t('paColInst'), head: true, cell: (o) => <span className="font-semibold">{sym(o.instrument)}</span> },334 { id: 's', label: t('paColSide'), head: true, cell: (o) => <span className="inline-flex items-center gap-2"><SideTag side={o.side} />{o.reduceOnly && <span className="rounded bg-elev px-1.5 text-[11px] font-semibold text-mute">{t('paReduceOnly')}</span>}</span> },335 { id: 'z', label: t('paColSize'), num: true, cell: (o) => `${px(o.filled)} / ${px(o.size)}` }, { id: 'p', label: t('paColAvg'), num: true, cell: (o) => px(o.avgFillPrice) }, { id: 'w', label: t('paColWorst'), num: true, cell: (o) => px(o.price) },336 { id: 'x', label: t('paColStatus'), cell: (o) => <span className="inline-flex items-center gap-1.5" title={o.reason ?? ''}><LiveStatePill state={o.state} /><SourceTag source={o.source} /></span> },337 { id: 'o', label: '', cell: (o) => <span className={`rounded px-1.5 text-[11px] font-semibold ${o.origin === 'mandate' ? 'bg-elev text-mute' : 'bg-warn/12 text-warn'}`} title={o.origin === 'mandate' ? '' : t('plNotFromAgent')}>{t(o.origin === 'mandate' ? 'plOriginAgent' : 'plOriginOwner')}</span> }];338 const fillCols: Col<Fill>[] = [339 { id: 't', label: t('paColTime'), cell: (x) => timeOf(x.ts, locale, true) }, { id: 'i', label: t('paColInst'), head: true, cell: (x) => <span className="font-semibold">{sym(x.instrument)}</span> }, { id: 's', label: t('paColSide'), head: true, cell: (x) => <SideTag side={x.side} /> },340 { id: 'z', label: t('paColSize'), num: true, cell: (x) => px(x.size) }, { id: 'p', label: t('paColPrice'), num: true, cell: (x) => px(x.price) }, { id: 'f', label: t('paColFee'), num: true, cell: (x) => usd(x.fee, locale) },341 { id: 'c', label: t('paColClosed'), num: true, cell: (x) => <Pnl value={x.closedPnl} locale={locale} /> }];342 const fundCols: Col<LiveFunding>[] = [343 { id: 't', label: t('paColTime'), cell: (x) => timeOf(x.ts, locale, true) }, { id: 'i', label: t('paColInst'), head: true, cell: (x) => <span className="font-semibold">{sym(x.instrument)}</span> },344 { id: 'r', label: t('paColRate'), num: true, cell: (x) => `${fmt(D.mul(D.from(x.rate), D.from(100n)), { dp: 5, locale })} %` }, { id: 'z', label: t('paColSize'), num: true, cell: (x) => px(x.size) },345 { id: 'a', label: t('paColAmount'), num: true, cell: (x) => <Pnl value={x.amount} locale={locale} dp={4} /> }];346 const evCols: Col<LiveRiskEvent>[] = [347 { id: 't', label: t('paColTime'), cell: (x) => timeOf(x.ts, locale, true) }, { id: 'k', label: t('paColKind'), head: true, cell: (x) => <span className="font-semibold">{x.kind}</span> },348 { id: 'm', label: '', full: true, cell: (x) => <span className="whitespace-normal text-mute">{x.code && x.kind === 'order_refused' ? <Refusal code={x.code} reason={x.message} /> : x.message}</span> }];349350 return <>351 {L.exit && <Notice tone={a.state === 'STOPPED' ? 'warn' : 'danger'}><div data-testid="pl-exit" data-code={L.exit.code}>352 <b className="font-semibold">{t('plExitTitle')}</b><span className="mt-0.5 block">{t(exitKey(L.exit.code))}</span>353 <span className="mt-1.5 block text-[11.5px] text-mute">{t('plServerDetail')}</span>354 <span className="block break-words font-mono text-[11px] text-mute" data-testid="pl-exit-detail">{L.exit.code} — {a.state === 'STOPPED' && a.stopReason ? a.stopReason : L.exit.reason} ({when(new Date(L.exit.ts), locale)})</span></div></Notice>}355 {a.state === 'STOPPING' && <Notice tone="warn"><div data-testid="pl-stopping"><b className="font-semibold">{t('plStopping')}</b>356 {vs && !vs.error && <span className="mt-0.5 block">{t('plStoppingLeft', { n: vs.positions.length })}</span>}357 {until && <span className="mt-0.5 block">{t('plWindDownLeft', { time: span(until - now) })}</span>}358 <span className="mt-1 block text-[12.5px]">{t('plManual')}</span></div></Notice>}359 {a.state === 'STOPPED' && vs && !vs.error && vs.positions.length > 0 && <div role="alert" data-testid="pl-open-left" className="rounded-2xl border-2 border-danger bg-danger/10 p-4 text-[14px] leading-snug">360 <b className="flex items-start gap-2 text-[15px] font-bold text-danger"><Icon n="warn" className="mt-0.5 h-5 w-5 shrink-0" />{t('plOpenLeftTitle', { n: vs.positions.length })}</b>361 <ul className="mt-2 space-y-0.5 font-mono text-[12.5px]">{vs.positions.map((p) => <li key={p.instrument}>{p.side} {px(p.size)} {sym(p.instrument)}</li>)}</ul>362 <p className="mt-2">{t('plOpenLeft')}</p>363 <Btn kind="danger" full className="mt-3" onClick={() => nav({ tab: 'futures' })}>{t('plOpenTerminal')}</Btn></div>}364 {a.state === 'PAUSED' && L.active && <Notice tone="warn"><div data-testid="pl-paused">{t('plPaused')}</div></Notice>}365 {acct?.halted && <Notice tone="danger"><span className="break-words">{t('plR_halted')} <span className="font-mono text-[11.5px]">{acct.halted}</span></span></Notice>}366 {err && <LiveErrNotice err={err} />}367368 <Card><div data-testid="pl-account" data-network={L.network ?? ''}><H right={<ModeTag tone={tone} />}>{t('plVenueAccount')}</H>369 {!acct ? <p className="text-[14px] text-mute">{t('plNoAccount')}</p> : vs?.error || !state ? <Notice tone="danger">{t('flUnreadable')} <span className="break-words font-mono text-[11.5px] text-mute">{vs?.error}</span></Notice> : <>370 <div className="text-[12px] text-mute">{t('plValue')}</div>371 <div className="text-[30px] font-semibold tracking-tight" data-testid="pl-equity">{usd(state.equity, locale)}</div>372 <div className="text-[13px] text-mute">{L.capital ? `${t('plCapitalRef', { capital: fmt(L.capital, { locale, dp: 2 }) })} · ` : ''}{t('plResult')} <Pnl value={result ? D.str(result) : null} locale={locale} /></div>373 <p className="mt-1 flex flex-wrap items-center gap-1.5 text-[11.5px] text-mute"><SourceTag />{t('flReadAt', { time: timeOf(vs!.ts, locale) })}{L.since ? ` · ${t('plSince', { date: when(new Date(L.since), locale) })}` : ''}</p>374 <div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-4">375 <Stat label={t('paCollateral')}>{usd(state.collateral, locale)}</Stat><Stat label={t('paMarginUsed')}>{usd(state.marginUsed, locale)}</Stat>376 <Stat label={t('paFree')}>{usd(state.freeCollateral, locale)}</Stat><Stat label={t('paLastBar')}>{a.lastBar ? when(a.lastBar, locale) : '—'}</Stat></div>377 <div className="mt-4"><div className="mb-1 text-[13px] font-semibold text-mute">{t('paPositions')}</div>378 <DataTable cols={posCols} rows={vs!.positions} rowKey={(p) => p.instrument} empty={t('paNoPositions')} label={t('paPositions')} /></div>379 </>}380 <button type="button" onClick={() => nav({ tab: 'futures' })} className="mt-3 inline-flex min-h-11 items-center gap-1 text-[13.5px] font-semibold underline">{t('plOpenTerminal')}<Icon n="chev" className="h-4 w-4" /></button>381 <p className="text-[12.5px] leading-snug text-mute">{t('plManual')}</p>382 </div></Card>383384 {eq.length > 1 && <Card><H right={<ModeTag tone={tone} />}>{t('plEquity')}</H><PriceChart points={eq} format={(v) => usd(dec(v.toFixed(2)), locale)} formatTime={(ts) => timeOf(ts, locale, true)} /></Card>}385386 <MandateCard a={a} />387388 <Card><div data-testid="pl-holds"><H>{t('plHolds')}</H>389 {L.holds.length === 0 ? <p className="text-[13.5px] text-mute">{t('plHoldsNone')}</p> : <ul className="space-y-2.5">{L.holds.slice(0, 12).map((h, k) => <li key={k} className={`rounded-2xl border p-3 text-[13px] leading-snug ${WAITS.has(h.code) ? 'border-line' : 'border-danger/40'}`}>390 <div className="mb-1 flex flex-wrap items-center justify-between gap-2"><span className="font-semibold">{h.symbol}</span><span className="text-[12px] text-mute">{t('plHoldBar', { bar: when(h.bar, locale) })}</span></div>391 <Refusal code={h.code} reason={h.reason} /></li>)}</ul>}</div></Card>392393 {L.active && (a.state === 'LIVE' || a.state === 'PAUSED') && <Card><div data-testid="pl-controls"><H>{t('plControl')}</H>394 <div className="space-y-2.5">395 {a.state === 'LIVE' && <><Btn kind="ghost" full busy={busy === 'pause'} onClick={() => void act('pause', 'pause')} data-testid="pl-pause">{t('plPause')}</Btn><p className="text-[12.5px] leading-snug text-mute">{t('plPauseNote')}</p></>}396 {a.state === 'PAUSED' && <Btn kind="soft" full busy={busy === 'resume'} onClick={() => void act('resume', 'resume')} data-testid="pl-resume">{t('plResume')}</Btn>}397 {(a.state === 'LIVE' || a.state === 'PAUSED') && <><Btn kind="danger" full onClick={() => setStop(true)} data-testid="pl-stop"><Icon n="stop" className="h-4 w-4" />{t('plStop')}</Btn><p className="text-[12.5px] leading-snug text-mute">{t('plStopNote')}</p></>}398 </div>399 <div className="mt-3"><Notice tone="warn"><div data-testid="pl-manual-warn">{t('plManualWarn')}</div></Notice></div>400 </div></Card>}401402 {acct && <Card><H right={<ModeTag tone={tone} />}>{t('plActivity')}</H>403 <Seg<ATab> value={tab} onChange={setTab} label={t('plActivity')} tabs size="sm" className="mb-3" options={[{ id: 'orders', label: `${t('paOrders')} (${acct.orders.length})` }, { id: 'fills', label: `${t('paFills')} (${acct.fills.length})` },404 { id: 'funding', label: `${t('paFunding')} (${acct.funding.length})` }, { id: 'events', label: `${t('paEvents')} (${acct.riskEvents.length})` }]} />405 {tab === 'orders' && <DataTable cols={ordCols} rows={acct.orders} rowKey={(o) => o.clientId} empty={t('paNone')} label={t('paOrders')} />}406 {tab === 'fills' && <DataTable cols={fillCols} rows={acct.fills} rowKey={(x) => x.id} empty={t('paNone')} label={t('paFills')} />}407 {tab === 'funding' && <DataTable cols={fundCols} rows={acct.funding} rowKey={(x) => `${x.instrument}:${x.ts}`} empty={t('paNone')} label={t('paFunding')} />}408 {tab === 'events' && <DataTable cols={evCols} rows={acct.riskEvents} rowKey={(x) => String(x.id)} empty={t('paNone')} label={t('paEvents')} />}409 </Card>}410 {stop && <StopSheet a={a} id={id} onClose={() => setStop(false)} />}411 </>;412}413