← back to Bertha

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>
  );
}