← back to Bertha
kalshi-dash/src/pages/Settings.jsx
603 lines
import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
// ── 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>
{/* ══════════════════════════════
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>
);
}