'use client'; import { useEffect, useState } from 'react'; import Link from 'next/link'; interface Instrument { symbol: string; enabled: boolean; } interface ResolvedContract { name: string; alternative?: string; } interface AppSettings { max_concurrent_accounts: string | null; master_dashboard_url: string | null; instance_name: string | null; instance_id: string | null; trading_hours: string | null; } export default function SettingsPage() { const [instruments, setInstruments] = useState([]); const [contracts, setContracts] = useState>({}); const [maxConcurrent, setMaxConcurrent] = useState('5'); const [tradingHours, setTradingHours] = useState('full_cme'); const [masterUrl, setMasterUrl] = useState(''); const [catalog, setCatalog] = useState<{ version: number; firmCount: number; lastSyncAt: string | null; lastError: string | null; } | null>(null); const [syncing, setSyncing] = useState(false); const [instanceName, setInstanceName] = useState(''); const [instanceId, setInstanceId] = useState(''); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const fetchCatalog = () => { fetch('/api/firm-catalog') .then((r) => r.json()) .then(setCatalog) .catch(() => {}); }; const syncCatalogNow = async () => { setSyncing(true); try { await fetch('/api/firm-catalog', { method: 'POST' }); } catch { /* the error lands in lastError */ } fetchCatalog(); setSyncing(false); }; useEffect(() => { fetch('/api/instruments') .then((r) => r.json()) .then(setInstruments); fetchCatalog(); fetch('/api/settings') .then((r) => r.json()) .then((s: AppSettings) => { if (s.max_concurrent_accounts != null) { setMaxConcurrent(s.max_concurrent_accounts); } if (s.trading_hours != null) { setTradingHours(s.trading_hours); } if (s.master_dashboard_url != null) { setMasterUrl(s.master_dashboard_url); } if (s.instance_name != null) { setInstanceName(s.instance_name); } if (s.instance_id != null) { setInstanceId(s.instance_id); } }); // Load cached contracts; if cache is empty, auto-resolve fetch('/api/instruments/contracts') .then((r) => r.json()) .then((data: Record) => { if (data.error) return; const hasData = Object.values(data).some((v) => v !== null); if (hasData) { setContracts(data); } else { // Cache empty — trigger a fresh resolve automatically fetch('/api/instruments/contracts', { method: 'POST' }) .then((r) => r.json()) .then((fresh) => { if (!fresh.error) setContracts(fresh); }) .catch(() => {}); } }) .catch(() => {}); }, []); async function toggle(symbol: string, enabled: boolean) { setInstruments((prev) => prev.map((i) => (i.symbol === symbol ? { ...i, enabled } : i)) ); await fetch(`/api/instruments/${symbol}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }), }); // When enabling a symbol, auto-resolve its active contract if (enabled) { fetch('/api/instruments/contracts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbols: [symbol] }), }) .then((r) => r.json()) .then((fresh: Record) => { if (!fresh.error) { setContracts((prev) => ({ ...prev, ...fresh })); } }) .catch(() => {}); } } async function saveSettings() { setSaving(true); setSaved(false); await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ max_concurrent_accounts: maxConcurrent, trading_hours: tradingHours, master_dashboard_url: masterUrl, instance_name: instanceName }), }); setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 2000); } return (
← Back

Settings

{/* ── Trading ── */}

Trading

Max concurrent accounts

How many accounts trade in parallel per /api/trade call

setMaxConcurrent(e.target.value)} className="w-20 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm text-right font-mono text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500" />

Allowed trading hours

5 min buffer

{/* ── Master Dashboard ── */}

Master Dashboard

Instance Name

Identifier for this autotrader on the master dashboard. Renaming is safe — the master tracks this machine by its person ID{instanceId ? ( <> ({instanceId.slice(0, 8)}) ) : ' once it has connected'}, so history follows the new name.

setInstanceName(e.target.value)} className="w-48 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm font-mono text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500" />

Dashboard URL

Reports state here every 30 seconds, and holds a control socket open so the dashboard can trigger trades and buys

setMasterUrl(e.target.value)} className="w-72 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm font-mono text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500" />

Firm catalog

{catalog ? `v${catalog.version} · ${catalog.firmCount} firm${catalog.firmCount === 1 ? '' : 's'} · ${ catalog.lastSyncAt ? `last synced ${new Date(catalog.lastSyncAt).toLocaleTimeString()}` : 'never synced' }` : 'Loading…'}

{catalog?.lastError && (

{catalog.lastError}

)}
{/* ── Instruments ── */}

Instruments

{instruments.length === 0 ? (
Loading…
) : ( {instruments.map((instr) => { const c = contracts[instr.symbol]; const wasRolled = !!c?.alternative; return ( ); })}
Symbol Active Contract Enabled
{instr.symbol} {instr.enabled && c ? (
{c.name} {wasRolled && ( rolled from {c.alternative} )}
) : null}
)}
); }