SPB Git

Repository Public

PerpAgentLive.tsx 46.6 KB 413 lines tsx

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
Files main
Menu

Theme