← back to Ken
react-dash/src/pages/Settings.jsx
332 lines
import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
export default function Settings() {
const [h, setH] = useState(null);
// Polymarket credentials (manual entry from polymarket.com/settings?tab=builder)
const [polyKey, setPolyKey] = useState('');
const [polySecret, setPolySecret] = useState('');
const [polyPass, setPolyPass] = useState('');
const [polyWallet, setPolyWallet] = useState('');
const [showSecret, setShowSecret] = useState(false);
// Connection state
const [polyStatus, setPolyStatus] = useState(null);
const [polyInfo, setPolyInfo] = useState(null);
const [polyMsg, setPolyMsg] = useState('');
const [clobOk, setClobOk] = useState(null);
const [testResults, setTestResults] = useState({});
useEffect(() => {
api('/health').then(setH);
checkPolyStatus();
}, []);
async function checkPolyStatus() {
setPolyStatus('checking');
try {
const r = await post('/polymarket', { action: 'status' });
setPolyInfo(r);
setPolyStatus(r.connected ? 'connected' : 'disconnected');
} catch {
setPolyStatus('disconnected');
}
}
async function testPublicClob() {
setClobOk('testing');
try {
const r = await post('/polymarket', { action: 'test_public' });
setClobOk(r.success ? 'ok' : 'error');
if (r.success) setPolyMsg(`CLOB OK — Server time: ${r.server_time}`);
} catch {
setClobOk('error');
}
}
async function saveKeys() {
if (!polyKey || !polySecret || !polyPass) return;
setPolyStatus('saving');
setPolyMsg('');
try {
const r = await post('/polymarket', {
action: 'save_keys',
api_key: polyKey,
secret: polySecret,
passphrase: polyPass,
wallet: polyWallet || null,
});
if (r.success) {
setPolyStatus('connected');
setPolyInfo({ api_key: r.api_key, wallet: polyWallet, connected_at: new Date().toISOString(), connected: true });
setPolyMsg('Credentials saved! Use "Test Auth" to verify.');
setPolyKey(''); setPolySecret(''); setPolyPass('');
} else {
setPolyStatus('error');
setPolyMsg(r.error || 'Failed to save');
}
} catch (err) {
setPolyStatus('error');
setPolyMsg('Error: ' + err.message);
}
}
async function testAuth() {
setPolyMsg('Testing authenticated connection...');
try {
const r = await post('/polymarket', { action: 'auth_test' });
if (r.success) {
setPolyMsg(`${r.message} | ${r.trades ?? 0} trades found`);
setPolyStatus('connected');
} else {
setPolyMsg(r.error || 'Auth test failed');
}
} catch (err) {
setPolyMsg('Error: ' + err.message);
}
}
async function disconnectPoly() {
try {
await post('/polymarket', { action: 'disconnect' });
setPolyStatus('disconnected');
setPolyInfo(null);
setPolyMsg('Disconnected');
} catch {
setPolyMsg('Error disconnecting');
}
}
async function testService(name, fn) {
setTestResults(prev => ({ ...prev, [name]: 'testing' }));
try {
const r = await fn();
setTestResults(prev => ({ ...prev, [name]: r.success || r.sent ? 'ok' : 'error' }));
} catch {
setTestResults(prev => ({ ...prev, [name]: 'error' }));
}
}
async function testNWS() { await testService('nws', () => post('/weather', { action: 'test_nws' })); }
async function testSlack() { await testService('slack', () => post('/alerts', { action: 'test' })); }
async function runW(w) {
const r = await post('/workers', { action: 'run', worker: w });
setTestResults(prev => ({ ...prev, [w]: r.message ? 'ok' : 'error' }));
}
const dot = (s) => {
if (s === true || s === 'ok' || s === 'connected') return 'bg-green-500';
if (s === 'testing' || s === 'checking' || s === 'saving') return 'bg-yellow-500 animate-pulse';
if (s === 'error') return 'bg-red-500';
return 'bg-gray-600';
};
return (
<div className="space-y-6">
<div><h1 className="text-2xl font-bold">Settings</h1><p className="text-gray-500 text-sm">System config & Polymarket login</p></div>
{/* ── Polymarket CLOB Login ── */}
<div className="card border border-purple-500/20" style={{ background: 'linear-gradient(135deg, rgba(139,92,246,0.08), rgba(59,130,246,0.04))' }}>
<div className="flex items-center gap-3 mb-5">
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/20">
<svg className="w-6 h-6 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9" /></svg>
</div>
<div className="flex-1">
<h2 className="text-lg font-bold">Polymarket CLOB Login</h2>
<p className="text-xs text-gray-500">Connect with your Builder API keys for live trading on Polygon</p>
</div>
<div className="flex items-center gap-2">
<span className={`w-2.5 h-2.5 rounded-full ${dot(polyStatus)}`} />
<span className={`text-sm font-medium ${polyStatus === 'connected' ? 'text-green-400' : polyStatus === 'error' ? 'text-red-400' : 'text-gray-500'}`}>
{polyStatus === 'connected' ? 'Connected' : polyStatus === 'saving' ? 'Saving...' : polyStatus === 'checking' ? 'Checking...' : polyStatus === 'error' ? 'Error' : 'Not Connected'}
</span>
</div>
</div>
{/* Connected State */}
{polyStatus === 'connected' && polyInfo && (
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 mb-4">
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500 text-xs block mb-0.5">API Key</span>
<span className="font-mono text-green-400">{polyInfo.api_key || '—'}</span>
</div>
<div>
<span className="text-gray-500 text-xs block mb-0.5">Wallet</span>
<span className="font-mono text-green-400">{polyInfo.wallet ? `${polyInfo.wallet.substring(0, 6)}...${polyInfo.wallet.slice(-4)}` : 'Not set'}</span>
</div>
<div>
<span className="text-gray-500 text-xs block mb-0.5">Connected</span>
<span className="text-green-400">{polyInfo.connected_at ? new Date(polyInfo.connected_at).toLocaleDateString() : '—'}</span>
</div>
</div>
<div className="flex gap-3 mt-3">
<button onClick={testAuth} className="btn btn-s text-xs">Test Auth</button>
<button onClick={testPublicClob} className={`btn btn-s text-xs ${clobOk === 'ok' ? 'text-green-400 border-green-500/30' : ''}`}>
{clobOk === 'testing' ? '...' : clobOk === 'ok' ? 'CLOB OK' : 'Test CLOB'}
</button>
<button onClick={disconnectPoly} className="btn text-xs text-red-400 hover:text-red-300 border border-red-500/30 hover:border-red-500/50">Disconnect</button>
</div>
</div>
)}
{/* Not Connected — Credential Entry */}
{polyStatus !== 'connected' && (
<div className="space-y-3">
<div className="bg-gray-800/40 rounded-lg p-4">
<p className="text-xs text-gray-400 mb-3">
Get your Builder API keys from{' '}
<span className="text-purple-400 font-mono">polymarket.com/settings</span>
{' '}→ Builder tab → Create New
</p>
<div className="space-y-2.5">
<div>
<label className="text-[11px] text-gray-500 mb-1 block">API Key</label>
<input type="text" value={polyKey} onChange={e => setPolyKey(e.target.value)} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
</div>
<div>
<label className="text-[11px] text-gray-500 mb-1 block">API Secret</label>
<div className="relative">
<input type={showSecret ? 'text' : 'password'} value={polySecret} onChange={e => setPolySecret(e.target.value)} placeholder="Base64 encoded secret" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 pr-10 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
<button type="button" onClick={() => setShowSecret(!showSecret)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={showSecret ? 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.879L21 21' : 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z'} /></svg>
</button>
</div>
</div>
<div>
<label className="text-[11px] text-gray-500 mb-1 block">Passphrase</label>
<input type="password" value={polyPass} onChange={e => setPolyPass(e.target.value)} placeholder="Passphrase" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
</div>
<div>
<label className="text-[11px] text-gray-500 mb-1 block">Wallet Address <span className="text-gray-600">(optional)</span></label>
<input type="text" value={polyWallet} onChange={e => setPolyWallet(e.target.value)} placeholder="0x..." className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
</div>
</div>
</div>
<div className="flex gap-3">
<button onClick={saveKeys} disabled={!polyKey || !polySecret || !polyPass || polyStatus === 'saving'} className="btn btn-p flex items-center gap-2 disabled:opacity-50">
{polyStatus === 'saving' && <span className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" />}
{polyStatus === 'saving' ? 'Saving...' : 'Connect'}
</button>
<button onClick={testPublicClob} className={`btn btn-s ${clobOk === 'ok' ? 'text-green-400 border-green-500/30' : ''}`}>
{clobOk === 'testing' ? '...' : clobOk === 'ok' ? 'CLOB Reachable' : 'Test CLOB'}
</button>
</div>
</div>
)}
{/* Status message */}
{polyMsg && (
<div className={`mt-3 text-sm p-3 rounded-lg ${polyMsg.toLowerCase().includes('error') || polyMsg.toLowerCase().includes('fail') ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400'}`}>
{polyMsg}
</div>
)}
{/* API Architecture */}
<div className="mt-5 pt-4 border-t border-gray-800/50">
<p className="text-xs text-gray-500 mb-3 font-medium uppercase tracking-wider">API Endpoints</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
{[
{ tag: 'CLOB', color: 'purple', host: 'clob.polymarket.com', desc: 'Orders & trading' },
{ tag: 'GAMMA', color: 'blue', host: 'gamma-api.polymarket.com', desc: 'Market metadata' },
{ tag: 'DATA', color: 'green', host: 'data-api.polymarket.com', desc: 'User positions' },
{ tag: 'WS', color: 'yellow', host: 'ws-subscriptions-clob...', desc: 'Live orderbook' },
].map(e => (
<div key={e.tag} className="bg-gray-800/30 rounded-lg p-2.5">
<span className={`inline-block px-1.5 py-0.5 rounded bg-${e.color}-500/20 text-${e.color}-400 font-mono text-[10px] mb-1`}>{e.tag}</span>
<p className="text-gray-400 font-mono text-[11px]">{e.host}</p>
<p className="text-gray-600 mt-0.5">{e.desc}</p>
</div>
))}
</div>
<div className="mt-3 text-[11px] text-gray-600">
<span className="text-gray-500">Auth:</span> L2 HMAC-SHA256 — API Key + Secret + Passphrase per request
</div>
</div>
</div>
{/* System Status */}
<div className="card">
<h2 className="font-semibold mb-3">System Status</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{ l: 'Database', s: h?.db_connected, d: 'PostgreSQL poly_betting' },
{ l: 'NWS API', s: testResults.nws || 'unknown', d: 'api.weather.gov' },
{ l: 'Polymarket', s: polyStatus === 'connected' ? true : clobOk || 'unknown', d: 'clob.polymarket.com' },
{ l: 'Slack', s: h?.slack_configured ? 'ok' : testResults.slack || 'no_token', d: 'Alerts channel' },
].map(s =>
<div key={s.l} className="bg-gray-800/30 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<span className={`w-2 h-2 rounded-full ${dot(s.s)}`} />
<span className="text-sm font-medium">{s.l}</span>
</div>
<p className="text-xs text-gray-500">{s.d}</p>
</div>
)}
</div>
</div>
{/* Connectivity Tests */}
<div className="card">
<h2 className="font-semibold mb-3">Connectivity Tests</h2>
<div className="flex gap-3">
<button onClick={testNWS} className={`btn ${testResults.nws === 'ok' ? 'btn-s' : 'btn-p'}`}>
{testResults.nws === 'testing' ? '...' : testResults.nws === 'ok' ? 'NWS OK' : 'Test NWS'}
</button>
<button onClick={testPublicClob} className={`btn ${clobOk === 'ok' ? 'btn-s' : 'btn-p'}`}>
{clobOk === 'testing' ? '...' : clobOk === 'ok' ? 'CLOB OK' : 'Test CLOB'}
</button>
<button onClick={testSlack} className={`btn ${testResults.slack === 'ok' ? 'btn-s' : 'btn-p'}`}>
{testResults.slack === 'testing' ? '...' : testResults.slack === 'ok' ? 'Slack OK' : 'Test Slack'}
</button>
</div>
</div>
{/* Workers */}
<div className="card">
<h2 className="font-semibold mb-3">Manual Workers</h2>
<div className="flex flex-wrap gap-3">
{[
{ w: 'market_ingest', l: 'Ingest Markets' },
{ w: 'weather_fetch', l: 'Fetch Weather' },
{ w: 'run_predictions', l: 'Run Predictions' },
{ w: 'risk_scan', l: 'Risk Scan' },
{ w: 'paper_trade', l: 'Paper Trade' },
].map(({ w, l }) => (
<button key={w} onClick={() => runW(w)} className={`btn ${testResults[w] === 'ok' ? 'btn-s' : 'btn-p'}`}>
{testResults[w] === 'ok' ? `${l} Done` : l}
</button>
))}
</div>
</div>
{/* Architecture */}
<div className="card">
<h2 className="font-semibold mb-3">Architecture</h2>
<div className="grid grid-cols-2 gap-4 text-sm">
<div><p className="text-gray-500 text-xs uppercase mb-2">Data Sources</p>
<ul className="space-y-1">
<li className="flex items-center gap-2"><span className="badge bg-blue">NWS</span>Live US forecasts</li>
<li className="flex items-center gap-2"><span className="badge bg-purple">GEFS</span>Ensembles</li>
<li className="flex items-center gap-2"><span className="badge bg-green">Gamma</span>Polymarket</li>
<li className="flex items-center gap-2"><span className="badge bg-yellow">CLOB</span>Orderbooks</li>
</ul>
</div>
<div><p className="text-gray-500 text-xs uppercase mb-2">Pipeline</p>
<ol className="space-y-1 list-decimal list-inside text-gray-300">
<li>Market parse + event extraction</li>
<li>Ensemble exceedance probability</li>
<li>Climatological prior</li>
<li>Bayesian calibration</li>
<li>Edge gate + risk sizing</li>
<li>Paper/live execution</li>
</ol>
</div>
</div>
</div>
</div>
);
}