← back to Ken

kalshi-dash/src/pages/Settings.jsx

611 lines

import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
import RiskKnob from '../components/RiskKnob';

// ── Interval presets ──
const INTERVAL_PRESETS = [
  { label: '30s', ms: 30000 },
  { label: '1m', ms: 60000 },
  { label: '2m', ms: 120000 },
  { label: '5m', ms: 300000 },
  { label: '8m', ms: 480000 },
  { label: '10m', ms: 600000 },
  { label: '15m', ms: 900000 },
  { label: '30m', ms: 1800000 },
  { label: '60m', ms: 3600000 },
];

function msToLabel(ms) {
  if (ms >= 3600000) return `${ms / 3600000}h`;
  if (ms >= 60000) return `${ms / 60000}m`;
  return `${ms / 1000}s`;
}

// ── Gear icon ──
function GearIcon() {
  return (
    <svg
      width="22"
      height="22"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <circle cx="12" cy="12" r="3" />
      <path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
    </svg>
  );
}

// ── Section panel ──
function SettingsPanel({ title, children }) {
  return (
    <div className="card space-y-4">
      <h2
        className="heading font-semibold text-base pb-3"
        style={{
          color: 'var(--text)',
          borderBottom: '1px solid var(--border)',
        }}
      >
        {title}
      </h2>
      {children}
    </div>
  );
}

// ── Label + description row ──
function FieldRow({ label, description, children }) {
  return (
    <div className="flex items-start justify-between gap-4">
      <div className="flex-1 min-w-0">
        <p className="text-sm font-medium" style={{ color: 'var(--text)' }}>{label}</p>
        {description && (
          <p className="text-xs mt-0.5" style={{ color: 'var(--text-muted)' }}>{description}</p>
        )}
      </div>
      <div className="flex-shrink-0">{children}</div>
    </div>
  );
}

// ── Toggle switch ──
function Toggle({ active, onChange }) {
  return (
    <div
      className={`toggle ${active ? 'active' : ''}`}
      onClick={() => onChange(!active)}
      role="switch"
      aria-checked={active}
      tabIndex={0}
      onKeyDown={e => (e.key === ' ' || e.key === 'Enter') && onChange(!active)}
    />
  );
}

// ── Interval control ──
function IntervalControl({ label, value, onChange, description }) {
  return (
    <div className="space-y-2">
      <FieldRow label={label} description={description}>
        <span
          className="mono font-bold text-lg"
          style={{ color: 'var(--cyan)' }}
        >
          {msToLabel(value)}
        </span>
      </FieldRow>
      <div className="flex flex-wrap gap-1.5">
        {INTERVAL_PRESETS.map(p => (
          <button
            key={p.ms}
            onClick={() => onChange(p.ms)}
            className="px-2.5 py-1 rounded text-xs font-medium transition"
            style={
              value === p.ms
                ? {
                    background: 'var(--cyan-dim)',
                    color: 'var(--cyan)',
                    border: '1px solid rgba(0,240,255,0.4)',
                  }
                : {
                    background: 'var(--surface-el)',
                    color: 'var(--text-muted)',
                    border: '1px solid var(--border)',
                  }
            }
          >
            {p.label}
          </button>
        ))}
      </div>
    </div>
  );
}

// ── Inline feedback message ──
function Msg({ msg }) {
  if (!msg) return null;
  return (
    <p
      className="text-xs px-3 py-2 rounded-lg"
      style={
        msg.type === 'ok'
          ? { background: 'var(--green-dim)', color: 'var(--green)' }
          : { background: 'var(--red-dim)', color: 'var(--red)' }
      }
    >
      {msg.text}
    </p>
  );
}

// ── Spinner ──
function Spinner() {
  return (
    <div className="flex items-center justify-center h-64">
      <div
        className="w-9 h-9 rounded-full border-2 animate-spin"
        style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }}
      />
    </div>
  );
}

export default function Settings() {
  const [status, setStatus] = useState(null);
  const [loading, setLoading] = useState(true);
  const [apiKey, setApiKey] = useState('');
  const [privateKey, setPrivateKey] = useState('');
  const [env, setEnv] = useState('demo');
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState(null);

  // Scan config state
  const [scanCfg, setScanCfg] = useState(null);
  const [scanMsg, setScanMsg] = useState(null);
  const [scanSaving, setScanSaving] = useState(false);

  useEffect(() => {
    loadStatus();
    loadScanConfig();
    const iv = setInterval(loadScanConfig, 30000);
    return () => clearInterval(iv);
  }, []);

  async function loadStatus() {
    try {
      const r = await post('/kalshi', { action: 'status' });
      setStatus(r);
      if (r.env) setEnv(r.env);
    } catch {}
    setLoading(false);
  }

  async function loadScanConfig() {
    try {
      const r = await api('/scan-config');
      setScanCfg(r);
    } catch {}
  }

  async function updateScanField(field, value) {
    setScanSaving(true);
    setScanMsg(null);
    try {
      const r = await api('/scan-config', {
        method: 'POST',
        body: JSON.stringify({ [field]: value }),
      });
      if (r.success) {
        setScanCfg(r);
        setScanMsg({ type: 'ok', text: `Updated ${field.replace(/_/g, ' ')}` });
      } else {
        setScanMsg({ type: 'err', text: r.error || 'Failed' });
      }
    } catch {
      setScanMsg({ type: 'err', text: 'Connection error' });
    }
    setScanSaving(false);
    setTimeout(() => setScanMsg(null), 3000);
  }

  async function saveKeys() {
    setSaving(true);
    setMsg(null);
    try {
      const r = await post('/kalshi', {
        action: 'save_keys',
        api_key: apiKey,
        private_key: privateKey,
        env,
      });
      if (r.success) {
        setMsg({ type: 'ok', text: 'Keys saved and connected' });
        loadStatus();
        setApiKey('');
        setPrivateKey('');
      } else {
        setMsg({ type: 'err', text: r.error || 'Failed' });
      }
    } catch {
      setMsg({ type: 'err', text: 'Connection error' });
    }
    setSaving(false);
  }

  async function disconnect() {
    try {
      await post('/kalshi', { action: 'disconnect' });
      loadStatus();
    } catch {}
  }

  async function testConnection() {
    setMsg(null);
    try {
      const r = await post('/kalshi', { action: 'test_connection' });
      setMsg({
        type: r.success ? 'ok' : 'err',
        text: r.success
          ? `Connected. Exchange: ${r.exchange_status}`
          : 'Connection failed',
      });
    } catch {
      setMsg({ type: 'err', text: 'Connection error' });
    }
  }

  if (loading) return <Spinner />;

  return (
    <div className="space-y-6 fade-in">

      {/* ── Page Header ── */}
      <div className="flex items-center gap-3">
        <div
          className="flex items-center justify-center w-10 h-10 rounded-xl"
          style={{ background: 'var(--cyan-dim)', color: 'var(--cyan)' }}
        >
          <GearIcon />
        </div>
        <div>
          <h1 className="heading text-3xl" style={{ color: 'var(--text)' }}>Settings</h1>
          <p className="section-subtitle mt-0.5">Configure scan pipeline, API keys, and risk parameters</p>
        </div>
      </div>

      {/* ══════════════════════════════
          RISK LEVEL KNOB (live control)
          ══════════════════════════════ */}
      <SettingsPanel title="Risk Level">
        <RiskKnob />
      </SettingsPanel>

      {/* ══════════════════════════════
          SCAN & PIPELINE CONFIG
          ══════════════════════════════ */}
      {scanCfg && (
        <SettingsPanel title="Scan & Pipeline Configuration">
          <div className="flex items-center justify-between">
            <p className="text-xs" style={{ color: 'var(--text-muted)' }}>
              Adjust intervals, thresholds, and risk parameters in real-time
            </p>
            {scanSaving && (
              <span className="text-xs animate-pulse" style={{ color: 'var(--orange)' }}>
                Saving...
              </span>
            )}
          </div>

          <Msg msg={scanMsg} />

          {/* Cached markets indicator */}
          <div
            className="flex items-center gap-3 px-4 py-3 rounded-xl"
            style={{
              background: 'var(--surface-el)',
              border: '1px solid var(--border)',
            }}
          >
            <span className="stat-label">Cached Markets</span>
            <span className="mono font-bold text-xl" style={{ color: 'var(--cyan)' }}>
              {scanCfg.cached_markets || 0}
            </span>
            <div
              className="w-2 h-2 rounded-full pulse-cyan ml-auto"
              style={{ background: 'var(--cyan)' }}
            />
          </div>

          {/* Interval controls */}
          <div
            className="space-y-5 pt-2 pb-1"
            style={{ borderTop: '1px solid var(--border)' }}
          >
            <h3 className="text-sm font-semibold pt-3" style={{ color: 'var(--text-dim)' }}>
              Pipeline Intervals
            </h3>

            <IntervalControl
              label="Market Scan"
              description="How often to fetch and analyze all active Kalshi markets"
              value={scanCfg.scan_interval_ms}
              onChange={v => updateScanField('scan_interval_ms', v)}
            />

            <IntervalControl
              label="Signal Pipeline"
              description="News sentiment + probability + Monte Carlo signal generation"
              value={scanCfg.signal_pipeline_ms}
              onChange={v => updateScanField('signal_pipeline_ms', v)}
            />

            <IntervalControl
              label="Intelligence Sweep"
              description="Reddit, news feeds, Bluesky, Mastodon, Lemmy + 20 more sources"
              value={scanCfg.reddit_interval_ms}
              onChange={v => updateScanField('reddit_interval_ms', v)}
            />

            <IntervalControl
              label="Trade Resolution"
              description="Take-profit / stop-loss check on open portfolio positions"
              value={scanCfg.resolve_interval_ms}
              onChange={v => updateScanField('resolve_interval_ms', v)}
            />
          </div>

          {/* Risk Parameters */}
          <div className="space-y-5" style={{ borderTop: '1px solid var(--border)', paddingTop: '1rem' }}>
            <h3 className="text-sm font-semibold" style={{ color: 'var(--text-dim)' }}>
              Risk Parameters
            </h3>

            {/* Min Edge Threshold */}
            <div className="space-y-2">
              <FieldRow
                label="Min Edge Threshold"
                description="Minimum statistical edge required to approve a signal"
              >
                <span className="mono font-bold text-lg" style={{ color: 'var(--cyan)' }}>
                  {(scanCfg.min_edge_threshold * 100).toFixed(1)}%
                </span>
              </FieldRow>
              <div className="flex flex-wrap gap-1.5">
                {[0.01, 0.02, 0.03, 0.05, 0.08, 0.10, 0.15, 0.20].map(v => (
                  <button
                    key={v}
                    onClick={() => updateScanField('min_edge_threshold', v)}
                    className="px-2.5 py-1 rounded text-xs font-medium transition"
                    style={
                      Math.abs(scanCfg.min_edge_threshold - v) < 0.001
                        ? {
                            background: 'var(--cyan-dim)',
                            color: 'var(--cyan)',
                            border: '1px solid rgba(0,240,255,0.4)',
                          }
                        : {
                            background: 'var(--surface-el)',
                            color: 'var(--text-muted)',
                            border: '1px solid var(--border)',
                          }
                    }
                  >
                    {(v * 100).toFixed(0)}%
                  </button>
                ))}
              </div>
            </div>

            {/* Min Liquidity */}
            <div className="space-y-2">
              <FieldRow
                label="Min Liquidity (24h Vol)"
                description="Minimum 24-hour trading volume to consider a market"
              >
                <span className="mono font-bold text-lg" style={{ color: 'var(--cyan)' }}>
                  {scanCfg.min_liquidity}
                </span>
              </FieldRow>
              <div className="flex flex-wrap gap-1.5">
                {[0, 10, 25, 50, 100, 250, 500, 1000].map(v => (
                  <button
                    key={v}
                    onClick={() => updateScanField('min_liquidity', v)}
                    className="px-2.5 py-1 rounded text-xs font-medium transition"
                    style={
                      scanCfg.min_liquidity === v
                        ? {
                            background: 'var(--cyan-dim)',
                            color: 'var(--cyan)',
                            border: '1px solid rgba(0,240,255,0.4)',
                          }
                        : {
                            background: 'var(--surface-el)',
                            color: 'var(--text-muted)',
                            border: '1px solid var(--border)',
                          }
                    }
                  >
                    {v === 0 ? 'None' : v}
                  </button>
                ))}
              </div>
            </div>
          </div>
        </SettingsPanel>
      )}

      {/* ══════════════════════════════
          API CONFIGURATION
          ══════════════════════════════ */}
      <SettingsPanel title="API Configuration">
        {/* Connection status indicator */}
        <div className="flex items-center gap-3 mb-2">
          <div
            className={`w-2.5 h-2.5 rounded-full ${status?.connected ? 'pulse-cyan' : ''}`}
            style={{ background: status?.connected ? 'var(--cyan)' : 'var(--text-muted)' }}
          />
          <span
            className="text-sm font-semibold"
            style={{ color: status?.connected ? 'var(--cyan)' : 'var(--text-muted)' }}
          >
            {status?.connected ? 'Connected' : 'Not Connected'}
          </span>
        </div>

        {status?.connected ? (
          <div className="space-y-3">
            <div
              className="rounded-xl px-4 py-3 space-y-2"
              style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
            >
              <div className="flex justify-between text-sm">
                <span style={{ color: 'var(--text-muted)' }}>API Key</span>
                <span className="mono" style={{ color: 'var(--text)' }}>
                  {status.api_key_masked || '•••••'}
                </span>
              </div>
              <div className="flex justify-between text-sm">
                <span style={{ color: 'var(--text-muted)' }}>Environment</span>
                <span
                  className="mono font-bold"
                  style={{ color: status.env === 'prod' ? 'var(--green)' : 'var(--orange)' }}
                >
                  {(status.env || 'demo').toUpperCase()}
                </span>
              </div>
            </div>
            <div className="flex gap-2">
              <button onClick={testConnection} className="btn btn-o btn-sm">
                Test Connection
              </button>
              <button onClick={disconnect} className="btn btn-d btn-sm">
                Disconnect
              </button>
            </div>
          </div>
        ) : (
          <div className="space-y-4">
            {/* Environment toggle */}
            <div className="flex gap-2">
              <button
                onClick={() => setEnv('demo')}
                className="flex-1 py-2 rounded-xl text-sm font-semibold transition"
                style={
                  env === 'demo'
                    ? {
                        background: 'var(--orange-dim)',
                        color: 'var(--orange)',
                        border: '1px solid rgba(247,147,26,0.4)',
                      }
                    : {
                        background: 'var(--surface-el)',
                        color: 'var(--text-muted)',
                        border: '1px solid var(--border)',
                      }
                }
              >
                Demo
              </button>
              <button
                onClick={() => setEnv('prod')}
                className="flex-1 py-2 rounded-xl text-sm font-semibold transition"
                style={
                  env === 'prod'
                    ? {
                        background: 'var(--green-dim)',
                        color: 'var(--green)',
                        border: '1px solid rgba(0,195,137,0.4)',
                      }
                    : {
                        background: 'var(--surface-el)',
                        color: 'var(--text-muted)',
                        border: '1px solid var(--border)',
                      }
                }
              >
                Production
              </button>
            </div>

            <div>
              <label className="stat-label block mb-1.5">API Key ID</label>
              <input
                type="text"
                value={apiKey}
                onChange={e => setApiKey(e.target.value)}
                placeholder="Your API key"
                className="input mono"
              />
            </div>
            <div>
              <label className="stat-label block mb-1.5">Private Key (PEM)</label>
              <textarea
                value={privateKey}
                onChange={e => setPrivateKey(e.target.value)}
                placeholder="-----BEGIN RSA PRIVATE KEY-----"
                rows={4}
                className="input mono text-xs"
                style={{ resize: 'vertical' }}
              />
            </div>
            <button
              onClick={saveKeys}
              disabled={saving || !apiKey}
              className="btn btn-p w-full"
              style={saving || !apiKey ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
            >
              {saving ? 'Saving...' : 'Save & Connect'}
            </button>
          </div>
        )}

        <Msg msg={msg} />
      </SettingsPanel>

      {/* ══════════════════════════════
          API REFERENCE
          ══════════════════════════════ */}
      <SettingsPanel title="Ken API Reference">
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-xs" style={{ color: 'var(--text-muted)' }}>
          <div className="space-y-2">
            <p className="stat-label mb-2">Environments</p>
            <div className="space-y-1">
              <div className="flex gap-2">
                <span>Demo:</span>
                <span className="mono" style={{ color: 'var(--orange)' }}>demo-api.kalshi.co</span>
              </div>
              <div className="flex gap-2">
                <span>Prod:</span>
                <span className="mono" style={{ color: 'var(--green)' }}>api.elections.kalshi.com</span>
              </div>
            </div>
          </div>
          <div className="space-y-2">
            <p className="stat-label mb-2">Authentication</p>
            <div className="space-y-1">
              <div className="flex gap-2">
                <span>Method:</span>
                <span className="mono" style={{ color: 'var(--text-dim)' }}>RSA-PSS + SHA-256</span>
              </div>
              <div>
                <span>Headers: </span>
                <span className="mono" style={{ color: 'var(--text-dim)' }}>
                  KALSHI-ACCESS-KEY, TIMESTAMP, SIGNATURE
                </span>
              </div>
            </div>
          </div>
        </div>
      </SettingsPanel>

    </div>
  );
}